X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=git-gui.sh;h=0c2dbbebe1287256ff4b3e5c2c086cdd9b491044;hb=ce9735dfbd77ab7cbcb97ba8749b2f6eaa7f2527;hp=f618a60d7be6d7091918ac915d6b01d219e93b5e;hpb=68c30b4af1b1d6f95ae6724364641aa787247f0f;p=git.git diff --git a/git-gui.sh b/git-gui.sh index f618a60d7..0c2dbbebe 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -335,7 +335,7 @@ proc PARENT {} { return $empty_tree } -proc rescan {after} { +proc rescan {after {honor_trustmtime 1}} { global HEAD PARENT MERGE_HEAD commit_type global ui_index ui_workdir ui_status_value ui_comm global rescan_active file_states @@ -366,7 +366,7 @@ proc rescan {after} { $ui_comm edit modified false } - if {$repo_config(gui.trustmtime) eq {true}} { + if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} { rescan_stage2 {} $after } else { set rescan_active 1 @@ -410,9 +410,9 @@ proc rescan_stage2 {fd after} { set fd_df [open "| git diff-files -z" r] set fd_lo [open $ls_others r] - fconfigure $fd_di -blocking 0 -translation binary - fconfigure $fd_df -blocking 0 -translation binary - fconfigure $fd_lo -blocking 0 -translation binary + fconfigure $fd_di -blocking 0 -translation binary -encoding binary + fconfigure $fd_df -blocking 0 -translation binary -encoding binary + fconfigure $fd_lo -blocking 0 -translation binary -encoding binary fileevent $fd_di readable [list read_diff_index $fd_di $after] fileevent $fd_df readable [list read_diff_files $fd_df $after] fileevent $fd_lo readable [list read_ls_others $fd_lo $after] @@ -428,6 +428,7 @@ proc load_message {file} { } set content [string trim [read $fd]] close $fd + regsub -all -line {[ \r\t]+$} $content {} content $ui_comm delete 0.0 end $ui_comm insert end $content return 1 @@ -450,8 +451,9 @@ proc read_diff_index {fd after} { incr c set i [split [string range $buf_rdi $c [expr {$z1 - 2}]] { }] + set p [string range $buf_rdi $z1 [expr {$z2 - 1}]] merge_state \ - [string range $buf_rdi $z1 [expr {$z2 - 1}]] \ + [encoding convertfrom $p] \ [lindex $i 4]? \ [list [lindex $i 0] [lindex $i 2]] \ [list] @@ -482,8 +484,9 @@ proc read_diff_files {fd after} { incr c set i [split [string range $buf_rdf $c [expr {$z1 - 2}]] { }] + set p [string range $buf_rdf $z1 [expr {$z2 - 1}]] merge_state \ - [string range $buf_rdf $z1 [expr {$z2 - 1}]] \ + [encoding convertfrom $p] \ ?[lindex $i 4] \ [list] \ [list [lindex $i 0] [lindex $i 2]] @@ -506,7 +509,7 @@ proc read_ls_others {fd after} { set pck [split $buf_rlo "\0"] set buf_rlo [lindex $pck end] foreach p [lrange $pck 0 end-1] { - merge_state $p ?O + merge_state [encoding convertfrom $p] ?O } rescan_done $fd buf_rlo $after } @@ -543,13 +546,15 @@ proc prune_selection {} { ## diff proc clear_diff {} { - global ui_diff current_diff_path ui_index ui_workdir + global ui_diff current_diff_path current_diff_header + global ui_index ui_workdir $ui_diff conf -state normal $ui_diff delete 0.0 end $ui_diff conf -state disabled set current_diff_path {} + set current_diff_header {} $ui_index tag remove in_diff 0.0 end $ui_workdir tag remove in_diff 0.0 end @@ -563,7 +568,7 @@ proc reshow_diff {} { if {$p eq {} || $current_diff_side eq {} || [catch {set s $file_states($p)}] - || [lsearch -sorted $file_lists($current_diff_side) $p] == -1} { + || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} { clear_diff } else { show_diff $p $current_diff_side @@ -582,42 +587,33 @@ proc handle_empty_diff {} { [short_path $path] has no changes. The modification date of this file was updated -by another application and you currently have -the Trust File Modification Timestamps option -enabled, so Git did not automatically detect -that there are no content differences in this -file. - -This file will now be removed from the modified -files list, to prevent possible confusion. -" - if {[catch {exec git update-index -- $path} err]} { - error_popup "Failed to refresh index:\n\n$err" - } +by another application, but the content within +the file was not changed. + +A rescan will be automatically started to find +other files which may have the same state." clear_diff display_file $path __ + rescan {set ui_status_value {Ready.}} 0 } proc show_diff {path w {lno {}}} { global file_states file_lists global is_3way_diff diff_active repo_config global ui_diff ui_status_value ui_index ui_workdir - global current_diff_path current_diff_side + global current_diff_path current_diff_side current_diff_header if {$diff_active || ![lock_index read]} return clear_diff - if {$w eq {} || $lno == {}} { - foreach w [array names file_lists] { - set lno [lsearch -sorted $file_lists($w) $path] - if {$lno >= 0} { - incr lno - break - } + if {$lno == {}} { + set lno [lsearch -sorted -exact $file_lists($w) $path] + if {$lno >= 0} { + incr lno } } - if {$w ne {} && $lno >= 1} { + if {$lno >= 1} { $w tag add in_diff $lno.0 [expr {$lno + 1}].0 } @@ -627,15 +623,18 @@ proc show_diff {path w {lno {}}} { set diff_active 1 set current_diff_path $path set current_diff_side $w + set current_diff_header {} set ui_status_value "Loading diff of [escape_path $path]..." # - Git won't give us the diff, there's nothing to compare to! # if {$m eq {_O}} { + set max_sz [expr {128 * 1024}] if {[catch { set fd [open $path r] - set content [read $fd] + set content [read $fd $max_sz] close $fd + set sz [file size $path] } err ]} { set diff_active 0 unlock_index @@ -644,7 +643,33 @@ proc show_diff {path w {lno {}}} { return } $ui_diff conf -state normal - $ui_diff insert end $content + if {![catch {set type [exec file $path]}]} { + set n [string length $path] + if {[string equal -length $n $path $type]} { + set type [string range $type $n end] + regsub {^:?\s*} $type {} type + } + $ui_diff insert end "* $type\n" d_@ + } + if {[string first "\0" $content] != -1} { + $ui_diff insert end \ + "* Binary file (not showing content)." \ + d_@ + } else { + if {$sz > $max_sz} { + $ui_diff insert end \ +"* Untracked file is $sz bytes. +* Showing only first $max_sz bytes. +" d_@ + } + $ui_diff insert end $content + if {$sz > $max_sz} { + $ui_diff insert end " +* Untracked file clipped here by [appname]. +* To see the entire file, use an external editor. +" d_@ + } + } $ui_diff conf -state disabled set diff_active 0 unlock_index @@ -683,23 +708,30 @@ proc show_diff {path w {lno {}}} { return } - fconfigure $fd -blocking 0 -translation auto + fconfigure $fd \ + -blocking 0 \ + -encoding binary \ + -translation binary fileevent $fd readable [list read_diff $fd] } proc read_diff {fd} { - global ui_diff ui_status_value is_3way_diff diff_active - global repo_config + global ui_diff ui_status_value diff_active + global is_3way_diff current_diff_header $ui_diff conf -state normal while {[gets $fd line] >= 0} { # -- Cleanup uninteresting diff header lines. # - if {[string match {diff --git *} $line]} continue - if {[string match {diff --cc *} $line]} continue - if {[string match {diff --combined *} $line]} continue - if {[string match {--- *} $line]} continue - if {[string match {+++ *} $line]} continue + if { [string match {diff --git *} $line] + || [string match {diff --cc *} $line] + || [string match {diff --combined *} $line] + || [string match {--- *} $line] + || [string match {+++ *} $line]} { + append current_diff_header $line "\n" + continue + } + if {[string match {index *} $line]} continue if {$line eq {deleted file mode 120000}} { set line "deleted symlink" } @@ -708,7 +740,12 @@ proc read_diff {fd} { # if {[string match {@@@ *} $line]} {set is_3way_diff 1} - if {[string match {index *} $line]} { + if {[string match {mode *} $line] + || [string match {new file *} $line] + || [string match {deleted file *} $line] + || [string match {Binary files * and * differ} $line] + || $line eq {\ No newline at end of file} + || [regexp {^\* Unmerged path } $line]} { set tags {} } elseif {$is_3way_diff} { set op [string range $line 0 1] @@ -754,6 +791,9 @@ proc read_diff {fd} { } } $ui_diff insert end $line $tags + if {[string index $line end] eq "\r"} { + $ui_diff tag add d_cr {end - 2c} + } $ui_diff insert end "\n" $tags } $ui_diff conf -state disabled @@ -764,19 +804,90 @@ proc read_diff {fd} { unlock_index set ui_status_value {Ready.} - if {$repo_config(gui.trustmtime) eq {true} - && [$ui_diff index end] eq {2.0}} { + if {[$ui_diff index end] eq {2.0}} { handle_empty_diff } } } +proc apply_hunk {x y} { + global current_diff_path current_diff_header current_diff_side + global ui_diff ui_index file_states + + if {$current_diff_path eq {} || $current_diff_header eq {}} return + if {![lock_index apply_hunk]} return + + set apply_cmd {git apply --cached --whitespace=nowarn} + set mi [lindex $file_states($current_diff_path) 0] + if {$current_diff_side eq $ui_index} { + set mode unstage + lappend apply_cmd --reverse + if {[string index $mi 0] ne {M}} { + unlock_index + return + } + } else { + set mode stage + if {[string index $mi 1] ne {M}} { + unlock_index + return + } + } + + set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0] + set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0] + if {$s_lno eq {}} { + unlock_index + return + } + + set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end] + if {$e_lno eq {}} { + set e_lno end + } + + if {[catch { + set p [open "| $apply_cmd" w] + fconfigure $p -translation binary -encoding binary + puts -nonewline $p $current_diff_header + puts -nonewline $p [$ui_diff get $s_lno $e_lno] + close $p} err]} { + error_popup "Failed to $mode selected hunk.\n\n$err" + unlock_index + return + } + + $ui_diff conf -state normal + $ui_diff delete $s_lno $e_lno + $ui_diff conf -state disabled + + if {[$ui_diff get 1.0 end] eq "\n"} { + set o _ + } else { + set o ? + } + + if {$current_diff_side eq $ui_index} { + set mi ${o}M + } elseif {[string index $mi 0] eq {_}} { + set mi M$o + } else { + set mi ?$o + } + unlock_index + display_file $current_diff_path $mi + if {$o eq {_}} { + clear_diff + } +} + ###################################################################### ## ## commit proc load_last_commit {} { global HEAD PARENT MERGE_HEAD commit_type ui_comm + global repo_config if {[llength $PARENT] == 0} { error_popup {There is nothing to amend. @@ -803,11 +914,18 @@ current merge activity. set parents [list] if {[catch { set fd [open "| git cat-file commit $curHEAD" r] + fconfigure $fd -encoding binary -translation lf + if {[catch {set enc $repo_config(i18n.commitencoding)}]} { + set enc utf-8 + } while {[gets $fd line] > 0} { if {[string match {parent *} $line]} { lappend parents [string range $line 7 end] + } elseif {[string match {encoding *} $line]} { + set enc [string tolower [string range $line 9 end]] } } + fconfigure $fd -encoding $enc set msg [string trim [read $fd]] close $fd } err]} { @@ -865,8 +983,8 @@ proc commit_tree {} { global HEAD commit_type file_states ui_comm repo_config global ui_status_value pch_error - if {![lock_index update]} return if {[committer_ident] eq {}} return + if {![lock_index update]} return # -- Our in memory state should match the repository. # @@ -896,12 +1014,12 @@ The rescan will be automatically started now. _? {continue} A? - D? - - M? {set files_ready 1; break} + M? {set files_ready 1} U? { error_popup "Unmerged files cannot be committed. File [short_path $path] has merge conflicts. -You must resolve them and include the file before committing. +You must resolve them and add the file before committing. " unlock_index return @@ -915,9 +1033,9 @@ File [short_path $path] cannot be committed by this program. } } if {!$files_ready} { - error_popup {No included files to commit. + info_popup {No changes to commit. -You must include at least 1 file before you can commit. +You must add at least 1 file before you can commit. } unlock_index return @@ -926,6 +1044,7 @@ You must include at least 1 file before you can commit. # -- A message is required. # set msg [string trim [$ui_comm get 1.0 end]] + regsub -all -line {[ \t\r]+$} $msg {} msg if {$msg eq {}} { error_popup {Please supply a commit message. @@ -996,9 +1115,10 @@ proc commit_writetree {curHEAD msg} { proc commit_committree {fd_wt curHEAD msg} { global HEAD PARENT MERGE_HEAD commit_type - global single_commit + global single_commit all_heads current_branch global ui_status_value ui_comm selected_commit_type global file_states selected_paths rescan_active + global repo_config gets $fd_wt tree_id if {$tree_id eq {} || [catch {close $fd_wt} err]} { @@ -1008,6 +1128,17 @@ proc commit_committree {fd_wt curHEAD msg} { return } + # -- Build the message. + # + set msg_p [gitdir COMMIT_EDITMSG] + set msg_wt [open $msg_p w] + if {[catch {set enc $repo_config(i18n.commitencoding)}]} { + set enc utf-8 + } + fconfigure $msg_wt -encoding $enc -translation binary + puts -nonewline $msg_wt $msg + close $msg_wt + # -- Create the commit. # set cmd [list git commit-tree $tree_id] @@ -1020,7 +1151,7 @@ proc commit_committree {fd_wt curHEAD msg} { # git commit-tree writes to stderr during initial commit. lappend cmd 2>/dev/null } - lappend cmd << $msg + lappend cmd <$msg_p if {[catch {set cmt_id [eval exec $cmd]} err]} { error_popup "commit-tree failed:\n\n$err" set ui_status_value {Commit failed.} @@ -1048,8 +1179,17 @@ proc commit_committree {fd_wt curHEAD msg} { return } + # -- Make sure our current branch exists. + # + if {$commit_type eq {initial}} { + lappend all_heads $current_branch + set all_heads [lsort -unique $all_heads] + populate_branch_menu + } + # -- Cleanup after ourselves. # + catch {file delete $msg_p} catch {file delete [gitdir MERGE_HEAD]} catch {file delete [gitdir MERGE_MSG]} catch {file delete [gitdir SQUASH_MSG]} @@ -1129,87 +1269,25 @@ proc commit_committree {fd_wt curHEAD msg} { ###################################################################### ## -## fetch pull push +## fetch push proc fetch_from {remote} { - set w [new_console "fetch $remote" \ + set w [new_console \ + "fetch $remote" \ "Fetching new changes from $remote"] set cmd [list git fetch] lappend cmd $remote - console_exec $w $cmd -} - -proc pull_remote {remote branch} { - global HEAD commit_type file_states repo_config - - if {![lock_index update]} return - - # -- Our in memory state should match the repository. - # - repository_state curType curHEAD curMERGE_HEAD - if {$commit_type ne $curType || $HEAD ne $curHEAD} { - info_popup {Last scanned state does not match repository state. - -Another Git program has modified this repository -since the last scan. A rescan must be performed -before a pull operation can be started. - -The rescan will be automatically started now. -} - unlock_index - rescan {set ui_status_value {Ready.}} - return - } - - # -- No differences should exist before a pull. - # - if {[array size file_states] != 0} { - error_popup {Uncommitted but modified files are present. - -You should not perform a pull with unmodified -files in your working directory as Git will be -unable to recover from an incorrect merge. - -You should commit or revert all changes before -starting a pull operation. -} - unlock_index - return - } - - set w [new_console "pull $remote $branch" \ - "Pulling new changes from branch $branch in $remote"] - set cmd [list git pull] - if {$repo_config(gui.pullsummary) eq {false}} { - lappend cmd --no-summary - } - lappend cmd $remote - lappend cmd $branch - console_exec $w $cmd [list post_pull_remote $remote $branch] -} - -proc post_pull_remote {remote branch success} { - global HEAD PARENT MERGE_HEAD commit_type selected_commit_type - global ui_status_value - - unlock_index - if {$success} { - repository_state commit_type HEAD MERGE_HEAD - set PARENT $HEAD - set selected_commit_type new - set ui_status_value "Pulling $branch from $remote complete." - } else { - rescan [list set ui_status_value \ - "Conflicts detected while pulling $branch from $remote."] - } + console_exec $w $cmd console_done } proc push_to {remote} { - set w [new_console "push $remote" \ + set w [new_console \ + "push $remote" \ "Pushing changes to $remote"] set cmd [list git push] + lappend cmd -v lappend cmd $remote - console_exec $w $cmd + console_exec $w $cmd console_done } ###################################################################### @@ -1287,7 +1365,7 @@ proc display_file_helper {w path icon_name old_m new_m} { global file_lists if {$new_m eq {_}} { - set lno [lsearch -sorted $file_lists($w) $path] + set lno [lsearch -sorted -exact $file_lists($w) $path] if {$lno >= 0} { set file_lists($w) [lreplace $file_lists($w) $lno $lno] incr lno @@ -1298,7 +1376,7 @@ proc display_file_helper {w path icon_name old_m new_m} { } elseif {$old_m eq {_} && $new_m ne {_}} { lappend file_lists($w) $path set file_lists($w) [lsort -unique $file_lists($w)] - set lno [lsearch -sorted $file_lists($w) $path] + set lno [lsearch -sorted -exact $file_lists($w) $path] incr lno $w conf -state normal $w image create $lno.0 \ @@ -1425,6 +1503,7 @@ proc update_indexinfo {msg pathList after} { -blocking 0 \ -buffering full \ -buffersize 512 \ + -encoding binary \ -translation binary fileevent $fd writable [list \ write_update_indexinfo \ @@ -1465,7 +1544,7 @@ proc write_update_indexinfo {fd pathList totalCnt batch msg after} { set info [lindex $s 2] if {$info eq {}} continue - puts -nonewline $fd "$info\t$path\0" + puts -nonewline $fd "$info\t[encoding convertto $path]\0" display_file $path $new } @@ -1497,6 +1576,7 @@ proc update_index {msg pathList after} { -blocking 0 \ -buffering full \ -buffersize 512 \ + -encoding binary \ -translation binary fileevent $fd writable [list \ write_update_index \ @@ -1541,7 +1621,7 @@ proc write_update_index {fd pathList totalCnt batch msg after} { ?M {set new M_} ?? {continue} } - puts -nonewline $fd "$path\0" + puts -nonewline $fd "[encoding convertto $path]\0" display_file $path $new } @@ -1579,6 +1659,7 @@ proc checkout_index {msg pathList after} { -blocking 0 \ -buffering full \ -buffersize 512 \ + -encoding binary \ -translation binary fileevent $fd writable [list \ write_checkout_index \ @@ -1611,7 +1692,7 @@ proc write_checkout_index {fd pathList totalCnt batch msg after} { U? {continue} ?M - ?D { - puts -nonewline $fd "$path\0" + puts -nonewline $fd "[encoding convertto $path]\0" display_file $path ?_ } } @@ -1628,16 +1709,27 @@ proc write_checkout_index {fd pathList totalCnt batch msg after} { ## ## branch management +proc is_tracking_branch {name} { + global tracking_branches + + if {![catch {set info $tracking_branches($name)}]} { + return 1 + } + foreach t [array names tracking_branches] { + if {[string match {*/\*} $t] && [string match $t $name]} { + return 1 + } + } + return 0 +} + proc load_all_heads {} { - global all_heads tracking_branches + global all_heads set all_heads [list] - set cmd [list git for-each-ref] - lappend cmd --format=%(refname) - lappend cmd refs/heads - set fd [open "| $cmd" r] + set fd [open "| git for-each-ref --format=%(refname) refs/heads" r] while {[gets $fd line] > 0} { - if {![catch {set info $tracking_branches($line)}]} continue + if {[is_tracking_branch $line]} continue if {![regsub ^refs/heads/ $line {} name]} continue lappend all_heads $name } @@ -1665,7 +1757,9 @@ proc populate_branch_menu {} { } } - $m add separator + if {$all_heads ne {}} { + $m add separator + } foreach b $all_heads { $m add radiobutton \ -label $b \ @@ -1678,12 +1772,50 @@ proc populate_branch_menu {} { } } +proc all_tracking_branches {} { + global tracking_branches + + set all_trackings {} + set cmd {} + foreach name [array names tracking_branches] { + if {[regsub {/\*$} $name {} name]} { + lappend cmd $name + } else { + regsub ^refs/(heads|remotes)/ $name {} name + lappend all_trackings $name + } + } + + if {$cmd ne {}} { + set fd [open "| git for-each-ref --format=%(refname) $cmd" r] + while {[gets $fd name] > 0} { + regsub ^refs/(heads|remotes)/ $name {} name + lappend all_trackings $name + } + close $fd + } + + return [lsort -unique $all_trackings] +} + proc do_create_branch_action {w} { - global all_heads null_sha1 + global all_heads null_sha1 repo_config global create_branch_checkout create_branch_revtype global create_branch_head create_branch_trackinghead + global create_branch_name create_branch_revexp - set newbranch [string trim [$w.name.t get 0.0 end]] + set newbranch $create_branch_name + if {$newbranch eq {} + || $newbranch eq $repo_config(gui.newbranchtemplate)} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message "Please supply a branch name." + focus $w.desc.name_t + return + } if {![catch {exec git show-ref --verify -- "refs/heads/$newbranch"}]} { tk_messageBox \ -icon error \ @@ -1691,7 +1823,7 @@ proc do_create_branch_action {w} { -title [wm title $w] \ -parent $w \ -message "Branch '$newbranch' already exists." - focus $w.name.t + focus $w.desc.name_t return } if {[catch {exec git check-ref-format "heads/$newbranch"}]} { @@ -1701,7 +1833,7 @@ proc do_create_branch_action {w} { -title [wm title $w] \ -parent $w \ -message "We do not like '$newbranch' as a branch name." - focus $w.name.t + focus $w.desc.name_t return } @@ -1709,7 +1841,7 @@ proc do_create_branch_action {w} { switch -- $create_branch_revtype { head {set rev $create_branch_head} tracking {set rev $create_branch_trackinghead} - expression {set rev [string trim [$w.from.exp.t get 0.0 end]]} + expression {set rev $create_branch_revexp} } if {[catch {set cmt [exec git rev-parse --verify "${rev}^0"]}]} { tk_messageBox \ @@ -1745,15 +1877,26 @@ proc do_create_branch_action {w} { } } +proc radio_selector {varname value args} { + upvar #0 $varname var + set var $value +} + +trace add variable create_branch_head write \ + [list radio_selector create_branch_revtype head] +trace add variable create_branch_trackinghead write \ + [list radio_selector create_branch_revtype tracking] + +trace add variable delete_branch_head write \ + [list radio_selector delete_branch_checktype head] +trace add variable delete_branch_trackinghead write \ + [list radio_selector delete_branch_checktype tracking] + proc do_create_branch {} { - global all_heads current_branch tracking_branches + global all_heads current_branch repo_config global create_branch_checkout create_branch_revtype global create_branch_head create_branch_trackinghead - - set create_branch_checkout 1 - set create_branch_revtype head - set create_branch_head $current_branch - set create_branch_trackinghead {} + global create_branch_name create_branch_revexp set w .branch_editor toplevel $w @@ -1775,85 +1918,69 @@ proc do_create_branch {} { pack $w.buttons.cancel -side right -padx 5 pack $w.buttons -side bottom -fill x -pady 10 -padx 10 - labelframe $w.name \ + labelframe $w.desc \ -text {Branch Description} \ -font font_ui - label $w.name.l -text {Name:} -font font_ui - text $w.name.t \ + label $w.desc.name_l -text {Name:} -font font_ui + entry $w.desc.name_t \ -borderwidth 1 \ -relief sunken \ - -height 1 \ -width 40 \ - -font font_ui - bind $w.name.t "focus $w.postActions.checkout;break" - bind $w.name.t "focus $w.from.exp.t;break" - bind $w.name.t "do_create_branch_action $w;break" - bind $w.name.t { - if {{%K} ne {BackSpace} - && {%K} ne {Tab} - && {%K} ne {Escape} - && {%K} ne {Return}} { - if {%k <= 32} break - if {[string first %A {~^:?*[}] >= 0} break + -textvariable create_branch_name \ + -font font_ui \ + -validate key \ + -validatecommand { + if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0} + return 1 } - } - pack $w.name.l -side left -padx 5 - pack $w.name.t -side left -fill x -expand 1 - pack $w.name -anchor nw -fill x -pady 5 -padx 5 - - set all_trackings [list] - foreach b [array names tracking_branches] { - regsub ^refs/(heads|remotes)/ $b {} b - lappend all_trackings $b - } - set all_trackings [lsort -unique $all_trackings] - if {$all_trackings ne {}} { - set create_branch_trackinghead [lindex $all_trackings 0] - } + grid $w.desc.name_l $w.desc.name_t -sticky we -padx {0 5} + grid columnconfigure $w.desc 1 -weight 1 + pack $w.desc -anchor nw -fill x -pady 5 -padx 5 labelframe $w.from \ -text {Starting Revision} \ -font font_ui - frame $w.from.head - radiobutton $w.from.head.r \ + radiobutton $w.from.head_r \ -text {Local Branch:} \ -value head \ -variable create_branch_revtype \ -font font_ui - eval tk_optionMenu $w.from.head.m create_branch_head $all_heads - pack $w.from.head.r -side left - pack $w.from.head.m -side left - frame $w.from.tracking - radiobutton $w.from.tracking.r \ - -text {Tracking Branch:} \ - -value tracking \ - -variable create_branch_revtype \ - -font font_ui - eval tk_optionMenu $w.from.tracking.m \ - create_branch_trackinghead \ - $all_trackings - pack $w.from.tracking.r -side left - pack $w.from.tracking.m -side left - frame $w.from.exp - radiobutton $w.from.exp.r \ + eval tk_optionMenu $w.from.head_m create_branch_head $all_heads + grid $w.from.head_r $w.from.head_m -sticky w + set all_trackings [all_tracking_branches] + if {$all_trackings ne {}} { + set create_branch_trackinghead [lindex $all_trackings 0] + radiobutton $w.from.tracking_r \ + -text {Tracking Branch:} \ + -value tracking \ + -variable create_branch_revtype \ + -font font_ui + eval tk_optionMenu $w.from.tracking_m \ + create_branch_trackinghead \ + $all_trackings + grid $w.from.tracking_r $w.from.tracking_m -sticky w + } + radiobutton $w.from.exp_r \ -text {Revision Expression:} \ -value expression \ -variable create_branch_revtype \ -font font_ui - text $w.from.exp.t \ + entry $w.from.exp_t \ -borderwidth 1 \ -relief sunken \ - -height 1 \ -width 50 \ - -font font_ui - bind $w.from.exp.t "focus $w.name.t;break" - bind $w.from.exp.t "focus $w.postActions.checkout;break" - bind $w.from.exp.t "do_create_branch_action $w;break" - pack $w.from.exp.r -side left - pack $w.from.exp.t -side left -fill x -expand 1 - pack $w.from.head -padx 5 -fill x -expand 1 - pack $w.from.tracking -padx 5 -fill x -expand 1 - pack $w.from.exp -padx 5 -fill x -expand 1 + -textvariable create_branch_revexp \ + -font font_ui \ + -validate key \ + -validatecommand { + if {%d == 1 && [regexp {\s} %S]} {return 0} + if {%d == 1 && [string length %S] > 0} { + set create_branch_revtype expression + } + return 1 + } + grid $w.from.exp_r $w.from.exp_t -sticky we -padx {0 5} + grid columnconfigure $w.from 1 -weight 1 pack $w.from -anchor nw -fill x -pady 5 -padx 5 labelframe $w.postActions \ @@ -1866,7 +1993,17 @@ proc do_create_branch {} { pack $w.postActions.checkout -anchor nw pack $w.postActions -anchor nw -fill x -pady 5 -padx 5 - bind $w "grab $w; focus $w.name.t" + set create_branch_checkout 1 + set create_branch_head $current_branch + set create_branch_revtype head + set create_branch_name $repo_config(gui.newbranchtemplate) + set create_branch_revexp {} + + bind $w " + grab $w + $w.desc.name_t icursor end + focus $w.desc.name_t + " bind $w "destroy $w" bind $w "do_create_branch_action $w;break" wm title $w "[appname] ([reponame]): Create Branch" @@ -1875,16 +2012,34 @@ proc do_create_branch {} { proc do_delete_branch_action {w} { global all_heads - global delete_branch_checkhead delete_branch_head + global delete_branch_checktype delete_branch_head delete_branch_trackinghead + + set check_rev {} + switch -- $delete_branch_checktype { + head {set check_rev $delete_branch_head} + tracking {set check_rev $delete_branch_trackinghead} + always {set check_rev {:none}} + } + if {$check_rev eq {:none}} { + set check_cmt {} + } elseif {[catch {set check_cmt [exec git rev-parse --verify "${check_rev}^0"]}]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message "Invalid check revision: $check_rev" + return + } set to_delete [list] set not_merged [list] foreach i [$w.list.l curselection] { set b [$w.list.l get $i] if {[catch {set o [exec git rev-parse --verify $b]}]} continue - if {$delete_branch_checkhead} { - if {$b eq $delete_branch_head} continue - if {[catch {set m [exec git merge-base $o $delete_branch_head]}]} continue + if {$check_cmt ne {}} { + if {$b eq $check_rev} continue + if {[catch {set m [exec git merge-base $o $check_cmt]}]} continue if {$o ne $m} { lappend not_merged $b continue @@ -1893,7 +2048,7 @@ proc do_delete_branch_action {w} { lappend to_delete [list $b $o] } if {$not_merged ne {}} { - set msg "The following branches are not completely merged into $delete_branch_head: + set msg "The following branches are not completely merged into $check_rev: - [join $not_merged "\n - "]" tk_messageBox \ @@ -1904,7 +2059,7 @@ proc do_delete_branch_action {w} { -message $msg } if {$to_delete eq {}} return - if {!$delete_branch_checkhead} { + if {$delete_branch_checktype eq {always}} { set msg {Recovering deleted branches is difficult. Delete the selected branches?} @@ -1925,7 +2080,7 @@ Delete the selected branches?} if {[catch {exec git update-ref -d "refs/heads/$b" $o} err]} { append failed " - $b: $err\n" } else { - set x [lsearch -sorted $all_heads $b] + set x [lsearch -sorted -exact $all_heads $b] if {$x >= 0} { set all_heads [lreplace $all_heads $x $x] } @@ -1948,10 +2103,7 @@ Delete the selected branches?} proc do_delete_branch {} { global all_heads tracking_branches current_branch - global delete_branch_checkhead delete_branch_head - - set delete_branch_checkhead 1 - set delete_branch_head $current_branch + global delete_branch_checktype delete_branch_head delete_branch_trackinghead set w .branch_editor toplevel $w @@ -1988,44 +2140,52 @@ proc do_delete_branch {} { pack $w.list.l -fill both -pady 5 -padx 5 pack $w.list -fill both -pady 5 -padx 5 - set all_trackings [list] - foreach b [array names tracking_branches] { - regsub ^refs/(heads|remotes)/ $b {} b - lappend all_trackings $b - } - labelframe $w.validate \ - -text {Only Delete If} \ + -text {Delete Only If} \ + -font font_ui + radiobutton $w.validate.head_r \ + -text {Merged Into Local Branch:} \ + -value head \ + -variable delete_branch_checktype \ -font font_ui - frame $w.validate.head - checkbutton $w.validate.head.r \ - -text {Already Merged Into:} \ - -variable delete_branch_checkhead \ + eval tk_optionMenu $w.validate.head_m delete_branch_head $all_heads + grid $w.validate.head_r $w.validate.head_m -sticky w + set all_trackings [all_tracking_branches] + if {$all_trackings ne {}} { + set delete_branch_trackinghead [lindex $all_trackings 0] + radiobutton $w.validate.tracking_r \ + -text {Merged Into Tracking Branch:} \ + -value tracking \ + -variable delete_branch_checktype \ + -font font_ui + eval tk_optionMenu $w.validate.tracking_m \ + delete_branch_trackinghead \ + $all_trackings + grid $w.validate.tracking_r $w.validate.tracking_m -sticky w + } + radiobutton $w.validate.always_r \ + -text {Always (Do not perform merge checks)} \ + -value always \ + -variable delete_branch_checktype \ -font font_ui - eval tk_optionMenu $w.validate.head.m delete_branch_head \ - $all_heads \ - [lsort -unique $all_trackings] - pack $w.validate.head.r -side left - pack $w.validate.head.m -side left - pack $w.validate.head -padx 5 -fill x -expand 1 + grid $w.validate.always_r -columnspan 2 -sticky w + grid columnconfigure $w.validate 1 -weight 1 pack $w.validate -anchor nw -fill x -pady 5 -padx 5 + set delete_branch_head $current_branch + set delete_branch_checktype head + bind $w "grab $w; focus $w" bind $w "destroy $w" wm title $w "[appname] ([reponame]): Delete Branch" tkwait window $w } -proc switch_branch {b} { - global HEAD commit_type file_states current_branch - global selected_commit_type ui_comm +proc switch_branch {new_branch} { + global HEAD commit_type current_branch repo_config if {![lock_index switch]} return - # -- Backup the selected branch (repository_state resets it) - # - set new_branch $current_branch - # -- Our in memory state should match the repository. # repository_state curType curHEAD curMERGE_HEAD @@ -2046,171 +2206,638 @@ The rescan will be automatically started now. return } - # -- Toss the message buffer if we are in amend mode. + # -- Don't do a pointless switch. # - if {[string match amend* $curType]} { + if {$current_branch eq $new_branch} { + unlock_index + return + } + + if {$repo_config(gui.trustmtime) eq {true}} { + switch_branch_stage2 {} $new_branch + } else { + set ui_status_value {Refreshing file status...} + set cmd [list git update-index] + lappend cmd -q + lappend cmd --unmerged + lappend cmd --ignore-missing + lappend cmd --refresh + set fd_rf [open "| $cmd" r] + fconfigure $fd_rf -blocking 0 -translation binary + fileevent $fd_rf readable \ + [list switch_branch_stage2 $fd_rf $new_branch] + } +} + +proc switch_branch_stage2 {fd_rf new_branch} { + global ui_status_value HEAD + + if {$fd_rf ne {}} { + read $fd_rf + if {![eof $fd_rf]} return + close $fd_rf + } + + set ui_status_value "Updating working directory to '$new_branch'..." + set cmd [list git read-tree] + lappend cmd -m + lappend cmd -u + lappend cmd --exclude-per-directory=.gitignore + lappend cmd $HEAD + lappend cmd $new_branch + set fd_rt [open "| $cmd" r] + fconfigure $fd_rt -blocking 0 -translation binary + fileevent $fd_rt readable \ + [list switch_branch_readtree_wait $fd_rt $new_branch] +} + +proc switch_branch_readtree_wait {fd_rt new_branch} { + global selected_commit_type commit_type HEAD MERGE_HEAD PARENT + global current_branch + global ui_comm ui_status_value + + # -- We never get interesting output on stdout; only stderr. + # + read $fd_rt + fconfigure $fd_rt -blocking 1 + if {![eof $fd_rt]} { + fconfigure $fd_rt -blocking 0 + return + } + + # -- The working directory wasn't in sync with the index and + # we'd have to overwrite something to make the switch. A + # merge is required. + # + if {[catch {close $fd_rt} err]} { + regsub {^fatal: } $err {} err + warn_popup "File level merge required. + +$err + +Staying on branch '$current_branch'." + set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)." + unlock_index + return + } + + # -- Update the symbolic ref. Core git doesn't even check for failure + # here, it Just Works(tm). If it doesn't we are in some really ugly + # state that is difficult to recover from within git-gui. + # + if {[catch {exec git symbolic-ref HEAD "refs/heads/$new_branch"} err]} { + error_popup "Failed to set current branch. + +This working directory is only partially switched. +We successfully updated your files, but failed to +update an internal Git file. + +This should not have occurred. [appname] will now +close and give up. + +$err" + do_quit + return + } + + # -- Update our repository state. If we were previously in amend mode + # we need to toss the current buffer and do a full rescan to update + # our file lists. If we weren't in amend mode our file lists are + # accurate and we can avoid the rescan. + # + unlock_index + set selected_commit_type new + if {[string match amend* $commit_type]} { $ui_comm delete 0.0 end $ui_comm edit reset $ui_comm edit modified false + rescan {set ui_status_value "Checked out branch '$current_branch'."} + } else { + repository_state commit_type HEAD MERGE_HEAD + set PARENT $HEAD + set ui_status_value "Checked out branch '$current_branch'." } +} - set selected_commit_type new - set current_branch $new_branch +###################################################################### +## +## remote management + +proc load_all_remotes {} { + global repo_config + global all_remotes tracking_branches + + set all_remotes [list] + array unset tracking_branches + + set rm_dir [gitdir remotes] + if {[file isdirectory $rm_dir]} { + set all_remotes [glob \ + -types f \ + -tails \ + -nocomplain \ + -directory $rm_dir *] + + foreach name $all_remotes { + catch { + set fd [open [file join $rm_dir $name] r] + while {[gets $fd line] >= 0} { + if {![regexp {^Pull:[ ]*([^:]+):(.+)$} \ + $line line src dst]} continue + if {![regexp ^refs/ $dst]} { + set dst "refs/heads/$dst" + } + set tracking_branches($dst) [list $name $src] + } + close $fd + } + } + } + + foreach line [array names repo_config remote.*.url] { + if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue + lappend all_remotes $name + + if {[catch {set fl $repo_config(remote.$name.fetch)}]} { + set fl {} + } + foreach line $fl { + if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue + if {![regexp ^refs/ $dst]} { + set dst "refs/heads/$dst" + } + set tracking_branches($dst) [list $name $src] + } + } + + set all_remotes [lsort -unique $all_remotes] +} + +proc populate_fetch_menu {} { + global all_remotes repo_config + + set m .mbar.fetch + foreach r $all_remotes { + set enable 0 + if {![catch {set a $repo_config(remote.$r.url)}]} { + if {![catch {set a $repo_config(remote.$r.fetch)}]} { + set enable 1 + } + } else { + catch { + set fd [open [gitdir remotes $r] r] + while {[gets $fd n] >= 0} { + if {[regexp {^Pull:[ \t]*([^:]+):} $n]} { + set enable 1 + break + } + } + close $fd + } + } + + if {$enable} { + $m add command \ + -label "Fetch from $r..." \ + -command [list fetch_from $r] \ + -font font_ui + } + } +} + +proc populate_push_menu {} { + global all_remotes repo_config + + set m .mbar.push + set fast_count 0 + foreach r $all_remotes { + set enable 0 + if {![catch {set a $repo_config(remote.$r.url)}]} { + if {![catch {set a $repo_config(remote.$r.push)}]} { + set enable 1 + } + } else { + catch { + set fd [open [gitdir remotes $r] r] + while {[gets $fd n] >= 0} { + if {[regexp {^Push:[ \t]*([^:]+):} $n]} { + set enable 1 + break + } + } + close $fd + } + } + + if {$enable} { + if {!$fast_count} { + $m add separator + } + $m add command \ + -label "Push to $r..." \ + -command [list push_to $r] \ + -font font_ui + incr fast_count + } + } +} + +proc start_push_anywhere_action {w} { + global push_urltype push_remote push_url push_thin push_tags + + set r_url {} + switch -- $push_urltype { + remote {set r_url $push_remote} + url {set r_url $push_url} + } + if {$r_url eq {}} return + + set cmd [list git push] + lappend cmd -v + if {$push_thin} { + lappend cmd --thin + } + if {$push_tags} { + lappend cmd --tags + } + lappend cmd $r_url + set cnt 0 + foreach i [$w.source.l curselection] { + set b [$w.source.l get $i] + lappend cmd "refs/heads/$b:refs/heads/$b" + incr cnt + } + if {$cnt == 0} { + return + } elseif {$cnt == 1} { + set unit branch + } else { + set unit branches + } + + set cons [new_console "push $r_url" "Pushing $cnt $unit to $r_url"] + console_exec $cons $cmd console_done + destroy $w +} + +trace add variable push_remote write \ + [list radio_selector push_urltype remote] + +proc do_push_anywhere {} { + global all_heads all_remotes current_branch + global push_urltype push_remote push_url push_thin push_tags + + set w .push_setup + toplevel $w + wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + + label $w.header -text {Push Branches} -font font_uibold + pack $w.header -side top -fill x + + frame $w.buttons + button $w.buttons.create -text Push \ + -font font_ui \ + -command [list start_push_anywhere_action $w] + pack $w.buttons.create -side right + button $w.buttons.cancel -text {Cancel} \ + -font font_ui \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + labelframe $w.source \ + -text {Source Branches} \ + -font font_ui + listbox $w.source.l \ + -height 10 \ + -width 50 \ + -selectmode extended \ + -font font_ui + foreach h $all_heads { + $w.source.l insert end $h + if {$h eq $current_branch} { + $w.source.l select set end + } + } + pack $w.source.l -fill both -pady 5 -padx 5 + pack $w.source -fill both -pady 5 -padx 5 + + labelframe $w.dest \ + -text {Destination Repository} \ + -font font_ui + if {$all_remotes ne {}} { + radiobutton $w.dest.remote_r \ + -text {Remote:} \ + -value remote \ + -variable push_urltype \ + -font font_ui + eval tk_optionMenu $w.dest.remote_m push_remote $all_remotes + grid $w.dest.remote_r $w.dest.remote_m -sticky w + if {[lsearch -sorted -exact $all_remotes origin] != -1} { + set push_remote origin + } else { + set push_remote [lindex $all_remotes 0] + } + set push_urltype remote + } else { + set push_urltype url + } + radiobutton $w.dest.url_r \ + -text {Arbitrary URL:} \ + -value url \ + -variable push_urltype \ + -font font_ui + entry $w.dest.url_t \ + -borderwidth 1 \ + -relief sunken \ + -width 50 \ + -textvariable push_url \ + -font font_ui \ + -validate key \ + -validatecommand { + if {%d == 1 && [regexp {\s} %S]} {return 0} + if {%d == 1 && [string length %S] > 0} { + set push_urltype url + } + return 1 + } + grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5} + grid columnconfigure $w.dest 1 -weight 1 + pack $w.dest -anchor nw -fill x -pady 5 -padx 5 + + labelframe $w.options \ + -text {Transfer Options} \ + -font font_ui + checkbutton $w.options.thin \ + -text {Use thin pack (for slow network connections)} \ + -variable push_thin \ + -font font_ui + grid $w.options.thin -columnspan 2 -sticky w + checkbutton $w.options.tags \ + -text {Include tags} \ + -variable push_tags \ + -font font_ui + grid $w.options.tags -columnspan 2 -sticky w + grid columnconfigure $w.options 1 -weight 1 + pack $w.options -anchor nw -fill x -pady 5 -padx 5 + + set push_url {} + set push_thin 0 + set push_tags 0 + + bind $w "grab $w" + bind $w "destroy $w" + wm title $w "[appname] ([reponame]): Push" + tkwait window $w +} + +###################################################################### +## +## merge + +proc can_merge {} { + global HEAD commit_type file_states + + if {[string match amend* $commit_type]} { + info_popup {Cannot merge while amending. + +You must finish amending this commit before +starting any type of merge. +} + return 0 + } + + if {[committer_ident] eq {}} {return 0} + if {![lock_index merge]} {return 0} + + # -- Our in memory state should match the repository. + # + repository_state curType curHEAD curMERGE_HEAD + if {$commit_type ne $curType || $HEAD ne $curHEAD} { + info_popup {Last scanned state does not match repository state. + +Another Git program has modified this repository +since the last scan. A rescan must be performed +before a merge can be performed. + +The rescan will be automatically started now. +} + unlock_index + rescan {set ui_status_value {Ready.}} + return 0 + } + + foreach path [array names file_states] { + switch -glob -- [lindex $file_states($path) 0] { + U? { + error_popup "You are in the middle of a conflicted merge. + +File [short_path $path] has merge conflicts. + +You must resolve them, add the file, and commit to +complete the current merge. Only then can you +begin another merge. +" + unlock_index + return 0 + } + } + } + + return 1 +} + +proc visualize_local_merge {w} { + set revs {} + foreach i [$w.source.l curselection] { + lappend revs [$w.source.l get $i] + } + if {$revs eq {}} return + lappend revs --not HEAD + do_gitk $revs +} + +proc start_local_merge_action {w} { + global HEAD + + set cmd [list git merge] + set names {} + set revcnt 0 + foreach i [$w.source.l curselection] { + set b [$w.source.l get $i] + lappend cmd $b + lappend names $b + incr revcnt + } + + if {$revcnt == 0} { + return + } elseif {$revcnt == 1} { + set unit branch + } elseif {$revcnt <= 15} { + set unit branches + } else { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message "Too many branches selected. + +You have requested to merge $revcnt branches +in an octopus merge. This exceeds Git's +internal limit of 15 branches per merge. + +Please select fewer branches. To merge more +than 15 branches, merge the branches in batches. +" + return + } + + set cons [new_console "Merge" "Merging [join $names {, }]"] + console_exec $cons $cmd finish_merge + bind $w {} + destroy $w +} +proc finish_merge {w ok} { + console_done $w $ok + if {$ok} { + set msg {Merge completed successfully.} + } else { + set msg {Merge failed. Conflict resolution is required.} + } unlock_index - error "NOT FINISHED" + rescan [list set ui_status_value $msg] } -###################################################################### -## -## remote management +proc do_local_merge {} { + global current_branch -proc load_all_remotes {} { - global repo_config - global all_remotes tracking_branches + if {![can_merge]} return - set all_remotes [list] - array unset tracking_branches + set w .merge_setup + toplevel $w + wm geometry $w "+[winfo rootx .]+[winfo rooty .]" - set rm_dir [gitdir remotes] - if {[file isdirectory $rm_dir]} { - set all_remotes [glob \ - -types f \ - -tails \ - -nocomplain \ - -directory $rm_dir *] + label $w.header \ + -text "Merge Into $current_branch" \ + -font font_uibold + pack $w.header -side top -fill x - foreach name $all_remotes { - catch { - set fd [open [file join $rm_dir $name] r] - while {[gets $fd line] >= 0} { - if {![regexp {^Pull:[ ]*([^:]+):(.+)$} \ - $line line src dst]} continue - if {![regexp ^refs/ $dst]} { - set dst "refs/heads/$dst" - } - set tracking_branches($dst) [list $name $src] - } - close $fd - } - } + frame $w.buttons + button $w.buttons.visualize -text Visualize \ + -font font_ui \ + -command [list visualize_local_merge $w] + pack $w.buttons.visualize -side left + button $w.buttons.create -text Merge \ + -font font_ui \ + -command [list start_local_merge_action $w] + pack $w.buttons.create -side right + button $w.buttons.cancel -text {Cancel} \ + -font font_ui \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + labelframe $w.source \ + -text {Source Branches} \ + -font font_ui + listbox $w.source.l \ + -height 10 \ + -width 25 \ + -selectmode extended \ + -yscrollcommand [list $w.source.sby set] \ + -font font_ui + scrollbar $w.source.sby -command [list $w.source.l yview] + pack $w.source.sby -side right -fill y + pack $w.source.l -side left -fill both -expand 1 + pack $w.source -fill both -expand 1 -pady 5 -padx 5 + + set cmd [list git for-each-ref] + lappend cmd {--format=%(objectname) %(refname)} + lappend cmd refs/heads + lappend cmd refs/remotes + set fr_fd [open "| $cmd" r] + fconfigure $fr_fd -translation binary + while {[gets $fr_fd line] > 0} { + set line [split $line { }] + set sha1([lindex $line 0]) [lindex $line 1] } + close $fr_fd - foreach line [array names repo_config remote.*.url] { - if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue - lappend all_remotes $name + set to_show {} + set fr_fd [open "| git rev-list --all --not HEAD"] + while {[gets $fr_fd line] > 0} { + if {[catch {set ref $sha1($line)}]} continue + regsub ^refs/(heads|remotes)/ $ref {} ref + lappend to_show $ref + } + close $fr_fd - if {[catch {set fl $repo_config(remote.$name.fetch)}]} { - set fl {} - } - foreach line $fl { - if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue - if {![regexp ^refs/ $dst]} { - set dst "refs/heads/$dst" - } - set tracking_branches($dst) [list $name $src] - } + foreach ref [lsort -unique $to_show] { + $w.source.l insert end $ref } - set all_remotes [lsort -unique $all_remotes] + bind $w "grab $w" + bind $w "unlock_index;destroy $w" + bind $w unlock_index + wm title $w "[appname] ([reponame]): Merge" + tkwait window $w } -proc populate_fetch_menu {m} { - global all_remotes repo_config +proc do_reset_hard {} { + global HEAD commit_type file_states - foreach r $all_remotes { - set enable 0 - if {![catch {set a $repo_config(remote.$r.url)}]} { - if {![catch {set a $repo_config(remote.$r.fetch)}]} { - set enable 1 - } - } else { - catch { - set fd [open [gitdir remotes $r] r] - while {[gets $fd n] >= 0} { - if {[regexp {^Pull:[ \t]*([^:]+):} $n]} { - set enable 1 - break - } - } - close $fd - } - } + if {[string match amend* $commit_type]} { + info_popup {Cannot abort while amending. - if {$enable} { - $m add command \ - -label "Fetch from $r..." \ - -command [list fetch_from $r] \ - -font font_ui - } - } +You must finish amending this commit. } + return + } -proc populate_push_menu {m} { - global all_remotes repo_config + if {![lock_index abort]} return - foreach r $all_remotes { - set enable 0 - if {![catch {set a $repo_config(remote.$r.url)}]} { - if {![catch {set a $repo_config(remote.$r.push)}]} { - set enable 1 - } - } else { - catch { - set fd [open [gitdir remotes $r] r] - while {[gets $fd n] >= 0} { - if {[regexp {^Push:[ \t]*([^:]+):} $n]} { - set enable 1 - break - } - } - close $fd - } - } + if {[string match *merge* $commit_type]} { + set op merge + } else { + set op commit + } - if {$enable} { - $m add command \ - -label "Push to $r..." \ - -command [list push_to $r] \ - -font font_ui - } + if {[ask_popup "Abort $op? + +Aborting the current $op will cause +*ALL* uncommitted changes to be lost. + +Continue with aborting the current $op?"] eq {yes}} { + set fd [open "| git read-tree --reset -u HEAD" r] + fconfigure $fd -blocking 0 -translation binary + fileevent $fd readable [list reset_hard_wait $fd] + set ui_status_value {Aborting... please wait...} + } else { + unlock_index } } -proc populate_pull_menu {m} { - global repo_config all_remotes disable_on_lock +proc reset_hard_wait {fd} { + global ui_comm - foreach remote $all_remotes { - set rb_list [list] - if {[array get repo_config remote.$remote.url] ne {}} { - if {[array get repo_config remote.$remote.fetch] ne {}} { - foreach line $repo_config(remote.$remote.fetch) { - if {[regexp {^([^:]+):} $line line rb]} { - lappend rb_list $rb - } - } - } - } else { - catch { - set fd [open [gitdir remotes $remote] r] - while {[gets $fd line] >= 0} { - if {[regexp {^Pull:[ \t]*([^:]+):} $line line rb]} { - lappend rb_list $rb - } - } - close $fd - } - } + read $fd + if {[eof $fd]} { + close $fd + unlock_index - foreach rb $rb_list { - regsub ^refs/heads/ $rb {} rb_short - $m add command \ - -label "Branch $rb_short from $remote..." \ - -command [list pull_remote $remote $rb] \ - -font font_ui - lappend disable_on_lock \ - [list $m entryconf [$m index last] -state] - } + $ui_comm delete 0.0 end + $ui_comm edit modified false + + catch {file delete [gitdir MERGE_HEAD]} + catch {file delete [gitdir rr-cache MERGE_RR]} + catch {file delete [gitdir SQUASH_MSG]} + catch {file delete [gitdir MERGE_MSG]} + catch {file delete [gitdir GITGUI_MSG]} + + rescan {set ui_status_value {Abort completed. Ready.}} } } @@ -2454,7 +3081,7 @@ proc console_init {w} { -command "tk_textCopy $w.m.t" $w.ctxm add command -label "Select All" \ -font font_ui \ - -command "$w.m.t tag add sel 0.0 end" + -command "focus $w.m.t;$w.m.t tag add sel 0.0 end" $w.ctxm add command -label "Copy All" \ -font font_ui \ -command " @@ -2477,7 +3104,7 @@ proc console_init {w} { return $w } -proc console_exec {w cmd {after {}}} { +proc console_exec {w cmd after} { # -- Windows tosses the enviroment when we exec our child. # But most users need that so we have to relogin. :-( # @@ -2496,7 +3123,7 @@ proc console_exec {w cmd {after {}}} { } proc console_read {w fd after} { - global console_cr console_data + global console_cr set buf [read $fd] if {$buf ne {}} { @@ -2530,25 +3157,72 @@ proc console_read {w fd after} { fconfigure $fd -blocking 1 if {[eof $fd]} { if {[catch {close $fd}]} { - if {![winfo exists $w]} {console_init $w} - $w.m.s conf -background red -text {Error: Command Failed} - $w.ok conf -state normal set ok 0 - } elseif {[winfo exists $w]} { - $w.m.s conf -background green -text {Success} - $w.ok conf -state normal + } else { set ok 1 } - array unset console_cr $w - array unset console_data $w - if {$after ne {}} { - uplevel #0 $after $ok - } + uplevel #0 $after $w $ok return } fconfigure $fd -blocking 0 } +proc console_chain {cmdlist w {ok 1}} { + if {$ok} { + if {[llength $cmdlist] == 0} { + console_done $w $ok + return + } + + set cmd [lindex $cmdlist 0] + set cmdlist [lrange $cmdlist 1 end] + + if {[lindex $cmd 0] eq {console_exec}} { + console_exec $w \ + [lindex $cmd 1] \ + [list console_chain $cmdlist] + } else { + uplevel #0 $cmd $cmdlist $w $ok + } + } else { + console_done $w $ok + } +} + +proc console_done {args} { + global console_cr console_data + + switch -- [llength $args] { + 2 { + set w [lindex $args 0] + set ok [lindex $args 1] + } + 3 { + set w [lindex $args 1] + set ok [lindex $args 2] + } + default { + error "wrong number of args: console_done ?ignored? w ok" + } + } + + if {$ok} { + if {[winfo exists $w]} { + $w.m.s conf -background green -text {Success} + $w.ok conf -state normal + } + } else { + if {![winfo exists $w]} { + console_init $w + } + $w.m.s conf -background red -text {Error: Command Failed} + $w.ok conf -state normal + } + + array unset console_cr $w + array unset console_data $w +} + ###################################################################### ## ## ui commands @@ -2580,9 +3254,82 @@ proc do_gitk {revs} { } } +proc do_stats {} { + set fd [open "| git count-objects -v" r] + while {[gets $fd line] > 0} { + if {[regexp {^([^:]+): (\d+)$} $line _ name value]} { + set stats($name) $value + } + } + close $fd + + set packed_sz 0 + foreach p [glob -directory [gitdir objects pack] \ + -type f \ + -nocomplain -- *] { + incr packed_sz [file size $p] + } + if {$packed_sz > 0} { + set stats(size-pack) [expr {$packed_sz / 1024}] + } + + set w .stats_view + toplevel $w + wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + + label $w.header -text {Database Statistics} \ + -font font_uibold + pack $w.header -side top -fill x + + frame $w.buttons -border 1 + button $w.buttons.close -text Close \ + -font font_ui \ + -command [list destroy $w] + button $w.buttons.gc -text {Compress Database} \ + -font font_ui \ + -command "destroy $w;do_gc" + pack $w.buttons.close -side right + pack $w.buttons.gc -side left + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + frame $w.stat -borderwidth 1 -relief solid + foreach s { + {count {Number of loose objects}} + {size {Disk space used by loose objects} { KiB}} + {in-pack {Number of packed objects}} + {packs {Number of packs}} + {size-pack {Disk space used by packed objects} { KiB}} + {prune-packable {Packed objects waiting for pruning}} + {garbage {Garbage files}} + } { + set name [lindex $s 0] + set label [lindex $s 1] + if {[catch {set value $stats($name)}]} continue + if {[llength $s] > 2} { + set value "$value[lindex $s 2]" + } + + label $w.stat.l_$name -text "$label:" -anchor w -font font_ui + label $w.stat.v_$name -text $value -anchor w -font font_ui + grid $w.stat.l_$name $w.stat.v_$name -sticky we -padx {0 5} + } + pack $w.stat -pady 10 -padx 10 + + bind $w "grab $w; focus $w" + bind $w [list destroy $w] + bind $w [list destroy $w] + wm title $w "[appname] ([reponame]): Database Statistics" + tkwait window $w +} + proc do_gc {} { set w [new_console {gc} {Compressing the object database}] - console_exec $w {git gc} + console_chain { + {console_exec {git pack-refs --prune}} + {console_exec {git reflog expire --all}} + {console_exec {git repack -a -d -l}} + {console_exec {git rerere gc}} + } $w } proc do_fsck_objects {} { @@ -2592,7 +3339,7 @@ proc do_fsck_objects {} { lappend cmd --full lappend cmd --cache lappend cmd --strict - console_exec $w $cmd + console_exec $w $cmd console_done } set is_quitting 0 @@ -2607,12 +3354,13 @@ proc do_quit {} { # set save [gitdir GITGUI_MSG] set msg [string trim [$ui_comm get 0.0 end]] - if {![string match amend* $commit_type] - && [$ui_comm edit modified] + regsub -all -line {[ \r\t]+$} $msg {} msg + if {(![string match amend* $commit_type] + || [$ui_comm edit modified]) && $msg ne {}} { catch { set fd [open $save w] - puts $fd [string trim [$ui_comm get 0.0 end]] + puts -nonewline $fd $msg close $fd } } else { @@ -2958,11 +3706,9 @@ proc do_options {} { pack $w.buttons -side bottom -fill x -pady 10 -padx 10 labelframe $w.repo -text "[reponame] Repository" \ - -font font_ui \ - -relief raised -borderwidth 2 + -font font_ui labelframe $w.global -text {Global (All Repositories)} \ - -font font_ui \ - -relief raised -borderwidth 2 + -font font_ui pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5 pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5 @@ -2970,6 +3716,7 @@ proc do_options {} { {b pullsummary {Show Pull Summary}} {b trustmtime {Trust File Modification Timestamps}} {i diffcontext {Number of Diff Context Lines}} + {t newbranchtemplate {New Branch Name Template}} } { set type [lindex $option 0] set name [lindex $option 1] @@ -2993,7 +3740,23 @@ proc do_options {} { -from 1 -to 99 -increment 1 \ -width 3 \ -font font_ui - pack $w.$f.$name.v -side right -anchor e + bind $w.$f.$name.v {%W selection range 0 end} + pack $w.$f.$name.v -side right -anchor e -padx 5 + pack $w.$f.$name -side top -anchor w -fill x + } + t { + frame $w.$f.$name + label $w.$f.$name.l -text "$text:" -font font_ui + entry $w.$f.$name.v \ + -borderwidth 1 \ + -relief sunken \ + -width 20 \ + -textvariable ${f}_config_new(gui.$name) \ + -font font_ui + pack $w.$f.$name.l -side left -anchor w + pack $w.$f.$name.v -side left -anchor w \ + -fill x -expand 1 \ + -padx 5 pack $w.$f.$name -side top -anchor w -fill x } } @@ -3022,6 +3785,7 @@ proc do_options {} { -from 2 -to 80 -increment 1 \ -width 3 \ -font font_ui + bind $w.global.$name.size {%W selection range 0 end} pack $w.global.$name.size -side right -anchor e pack $w.global.$name.family -side right -anchor e pack $w.global.$name -side top -anchor w -fill x @@ -3327,6 +4091,7 @@ proc apply_config {} { set default_config(gui.trustmtime) false set default_config(gui.pullsummary) true set default_config(gui.diffcontext) 5 +set default_config(gui.newbranchtemplate) {} set default_config(gui.fontui) [font configure font_ui] set default_config(gui.fontdiff) [font configure font_diff] set font_descs { @@ -3350,8 +4115,8 @@ if {!$single_commit} { } .mbar add cascade -label Commit -menu .mbar.commit if {!$single_commit} { + .mbar add cascade -label Merge -menu .mbar.merge .mbar add cascade -label Fetch -menu .mbar.fetch - .mbar add cascade -label Pull -menu .mbar.pull .mbar add cascade -label Push -menu .mbar.push } . configure -menu .mbar @@ -3363,15 +4128,17 @@ menu .mbar.repository -label {Visualize Current Branch} \ -command {do_gitk {}} \ -font font_ui -if {![is_MacOSX]} { - .mbar.repository add command \ - -label {Visualize All Branches} \ - -command {do_gitk {--all}} \ - -font font_ui -} +.mbar.repository add command \ + -label {Visualize All Branches} \ + -command {do_gitk {--all}} \ + -font font_ui .mbar.repository add separator if {!$single_commit} { + .mbar.repository add command -label {Database Statistics} \ + -command do_stats \ + -font font_ui + .mbar.repository add command -label {Compress Database} \ -command do_gc \ -font font_ui @@ -3523,14 +4290,6 @@ lappend disable_on_lock \ lappend disable_on_lock \ [list .mbar.commit entryconf [.mbar.commit index last] -state] -# -- Transport menus -# -if {!$single_commit} { - menu .mbar.fetch - menu .mbar.pull - menu .mbar.push -} - if {[is_MacOSX]} { # -- Apple Menu (Mac OS X only) # @@ -3611,46 +4370,76 @@ pack .branch.l1 -side left pack .branch.cb -side left -fill x pack .branch -side top -fill x +if {!$single_commit} { + menu .mbar.merge + .mbar.merge add command -label {Local Merge...} \ + -command do_local_merge \ + -font font_ui + lappend disable_on_lock \ + [list .mbar.merge entryconf [.mbar.merge index last] -state] + .mbar.merge add command -label {Abort Merge...} \ + -command do_reset_hard \ + -font font_ui + lappend disable_on_lock \ + [list .mbar.merge entryconf [.mbar.merge index last] -state] + + + menu .mbar.fetch + + menu .mbar.push + .mbar.push add command -label {Push...} \ + -command do_push_anywhere \ + -font font_ui +} + # -- Main Window Layout # panedwindow .vpane -orient vertical panedwindow .vpane.files -orient horizontal -.vpane add .vpane.files -sticky nsew -height 100 -width 400 +.vpane add .vpane.files -sticky nsew -height 100 -width 200 pack .vpane -anchor n -side top -fill both -expand 1 # -- Index File List # -frame .vpane.files.index -height 100 -width 400 +frame .vpane.files.index -height 100 -width 200 label .vpane.files.index.title -text {Changes To Be Committed} \ -background green \ -font font_ui text $ui_index -background white -borderwidth 0 \ - -width 40 -height 10 \ + -width 20 -height 10 \ + -wrap none \ -font font_ui \ -cursor $cursor_ptr \ - -yscrollcommand {.vpane.files.index.sb set} \ + -xscrollcommand {.vpane.files.index.sx set} \ + -yscrollcommand {.vpane.files.index.sy set} \ -state disabled -scrollbar .vpane.files.index.sb -command [list $ui_index yview] +scrollbar .vpane.files.index.sx -orient h -command [list $ui_index xview] +scrollbar .vpane.files.index.sy -orient v -command [list $ui_index yview] pack .vpane.files.index.title -side top -fill x -pack .vpane.files.index.sb -side right -fill y +pack .vpane.files.index.sx -side bottom -fill x +pack .vpane.files.index.sy -side right -fill y pack $ui_index -side left -fill both -expand 1 .vpane.files add .vpane.files.index -sticky nsew # -- Working Directory File List # -frame .vpane.files.workdir -height 100 -width 100 +frame .vpane.files.workdir -height 100 -width 200 label .vpane.files.workdir.title -text {Changed But Not Updated} \ -background red \ -font font_ui text $ui_workdir -background white -borderwidth 0 \ - -width 40 -height 10 \ + -width 20 -height 10 \ + -wrap none \ -font font_ui \ -cursor $cursor_ptr \ - -yscrollcommand {.vpane.files.workdir.sb set} \ + -xscrollcommand {.vpane.files.workdir.sx set} \ + -yscrollcommand {.vpane.files.workdir.sy set} \ -state disabled -scrollbar .vpane.files.workdir.sb -command [list $ui_workdir yview] +scrollbar .vpane.files.workdir.sx -orient h -command [list $ui_workdir xview] +scrollbar .vpane.files.workdir.sy -orient v -command [list $ui_workdir yview] pack .vpane.files.workdir.title -side top -fill x -pack .vpane.files.workdir.sb -side right -fill y +pack .vpane.files.workdir.sx -side bottom -fill x +pack .vpane.files.workdir.sy -side right -fill y pack $ui_workdir -side left -fill both -expand 1 .vpane.files add .vpane.files.workdir -sticky nsew @@ -3669,7 +4458,7 @@ frame .vpane.lower.commarea frame .vpane.lower.diff -relief sunken -borderwidth 1 pack .vpane.lower.commarea -side top -fill x pack .vpane.lower.diff -side bottom -fill both -expand 1 -.vpane add .vpane.lower -stick nsew +.vpane add .vpane.lower -sticky nsew # -- Commit Area Buttons # @@ -3789,7 +4578,7 @@ $ctxm add separator $ctxm add command \ -label {Select All} \ -font font_ui \ - -command {$ui_comm tag add sel 0.0 end} + -command {focus $ui_comm;$ui_comm tag add sel 0.0 end} $ctxm add command \ -label {Copy All} \ -font font_ui \ @@ -3808,6 +4597,7 @@ bind_button3 $ui_comm "tk_popup $ctxm %X %Y" # -- Diff Header # set current_diff_path {} +set current_diff_side {} set diff_actions [list] proc trace_current_diff_path {varname args} { global current_diff_path diff_actions file_states @@ -3888,24 +4678,25 @@ pack $ui_diff -side left -fill both -expand 1 pack .vpane.lower.diff.header -side top -fill x pack .vpane.lower.diff.body -side bottom -fill both -expand 1 -$ui_diff tag conf d_@ -font font_diffbold -$ui_diff tag conf d_+ -foreground blue +$ui_diff tag conf d_cr -elide true +$ui_diff tag conf d_@ -foreground blue -font font_diffbold +$ui_diff tag conf d_+ -foreground {#00a000} $ui_diff tag conf d_- -foreground red -$ui_diff tag conf d_++ -foreground blue +$ui_diff tag conf d_++ -foreground {#00a000} $ui_diff tag conf d_-- -foreground red $ui_diff tag conf d_+s \ - -foreground blue \ - -background azure2 + -foreground {#00a000} \ + -background {#e2effa} $ui_diff tag conf d_-s \ -foreground red \ - -background azure2 + -background {#e2effa} $ui_diff tag conf d_s+ \ - -foreground blue \ - -background {light goldenrod yellow} + -foreground {#00a000} \ + -background ivory1 $ui_diff tag conf d_s- \ -foreground red \ - -background {light goldenrod yellow} + -background ivory1 $ui_diff tag conf d<<<<<<< \ -foreground orange \ @@ -3917,6 +4708,8 @@ $ui_diff tag conf d>>>>>>> \ -foreground orange \ -font font_diffbold +$ui_diff tag raise sel + # -- Diff Body Context Menu # set ctxm .vpane.lower.diff.body.ctxm @@ -3925,6 +4718,7 @@ $ctxm add command \ -label {Refresh} \ -font font_ui \ -command reshow_diff +lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] $ctxm add command \ -label {Copy} \ -font font_ui \ @@ -3933,7 +4727,7 @@ lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] $ctxm add command \ -label {Select All} \ -font font_ui \ - -command {$ui_diff tag add sel 0.0 end} + -command {focus $ui_diff;$ui_diff tag add sel 0.0 end} lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] $ctxm add command \ -label {Copy All} \ @@ -3945,6 +4739,13 @@ $ctxm add command \ } lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] $ctxm add separator +$ctxm add command \ + -label {Apply/Reverse Hunk} \ + -font font_ui \ + -command {apply_hunk $cursorX $cursorY} +set ui_diff_applyhunk [$ctxm index last] +lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state] +$ctxm add separator $ctxm add command \ -label {Decrease Font Size} \ -font font_ui \ @@ -3976,7 +4777,17 @@ $ctxm add separator $ctxm add command -label {Options...} \ -font font_ui \ -command do_options -bind_button3 $ui_diff "tk_popup $ctxm %X %Y" +bind_button3 $ui_diff " + set cursorX %x + set cursorY %y + if {\$ui_index eq \$current_diff_side} { + $ctxm entryconf $ui_diff_applyhunk -label {Unstage Hunk From Commit} + } else { + $ctxm entryconf $ui_diff_applyhunk -label {Stage Hunk For Commit} + } + tk_popup $ctxm %X %Y +" +unset ui_diff_applyhunk # -- Status Bar # @@ -4029,6 +4840,7 @@ bind $ui_diff {catch {%W yview scroll -1 units};break} bind $ui_diff {catch {%W yview scroll 1 units};break} bind $ui_diff {catch {%W xview scroll -1 units};break} bind $ui_diff {catch {%W xview scroll 1 units};break} +bind $ui_diff {focus %W} if {!$single_commit} { bind . <$M1B-Key-n> do_create_branch @@ -4132,9 +4944,8 @@ if {!$single_commit} { load_all_heads populate_branch_menu - populate_fetch_menu .mbar.fetch - populate_pull_menu .mbar.pull - populate_push_menu .mbar.push + populate_fetch_menu + populate_push_menu } # -- Only suggest a gc run if we are going to stay running.