X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=git-gui.sh;h=ae22338be353e4ba588948a0778ea8b0b8a1a60e;hb=df6287ecd78206095796512bca80e87cc5a5e33b;hp=32c33672d6046b613f5a885ff614d7126a08c5e6;hpb=14efcc748597f8b00d362df26adf5c4b4b7777f7;p=git.git diff --git a/git-gui.sh b/git-gui.sh index 32c33672d..ae22338be 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -26,7 +26,9 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA} set _appname [lindex [file split $argv0] end] set _gitdir {} +set _gitexec {} set _reponame {} +set _iscygwin {} proc appname {} { global _appname @@ -41,11 +43,72 @@ proc gitdir {args} { return [eval [concat [list file join $_gitdir] $args]] } +proc gitexec {args} { + global _gitexec + if {$_gitexec eq {}} { + if {[catch {set _gitexec [exec git --exec-path]} err]} { + error "Git not installed?\n\n$err" + } + } + if {$args eq {}} { + return $_gitexec + } + return [eval [concat [list file join $_gitexec] $args]] +} + proc reponame {} { global _reponame return $_reponame } +proc is_MacOSX {} { + global tcl_platform tk_library + if {[tk windowingsystem] eq {aqua}} { + return 1 + } + return 0 +} + +proc is_Windows {} { + global tcl_platform + if {$tcl_platform(platform) eq {windows}} { + return 1 + } + return 0 +} + +proc is_Cygwin {} { + global tcl_platform _iscygwin + if {$_iscygwin eq {}} { + if {$tcl_platform(platform) eq {windows}} { + if {[catch {set p [exec cygpath --windir]} err]} { + set _iscygwin 0 + } else { + set _iscygwin 1 + } + } else { + set _iscygwin 0 + } + } + return $_iscygwin +} + +proc is_enabled {option} { + global enabled_options + if {[catch {set on $enabled_options($option)}]} {return 0} + return $on +} + +proc enable_option {option} { + global enabled_options + set enabled_options($option) 1 +} + +proc disable_option {option} { + global enabled_options + set enabled_options($option) 0 +} + ###################################################################### ## ## config @@ -60,13 +123,24 @@ proc is_many_config {name} { } } +proc is_config_true {name} { + global repo_config + if {[catch {set v $repo_config($name)}]} { + return 0 + } elseif {$v eq {true} || $v eq {1} || $v eq {yes}} { + return 1 + } else { + return 0 + } +} + proc load_config {include_global} { global repo_config global_config default_config array unset global_config if {$include_global} { catch { - set fd_rc [open "| git repo-config --global --list" r] + set fd_rc [open "| git config --global --list" r] while {[gets $fd_rc line] >= 0} { if {[regexp {^([^=]+)=(.*)$} $line line name value]} { if {[is_many_config $name]} { @@ -82,7 +156,7 @@ proc load_config {include_global} { array unset repo_config catch { - set fd_rc [open "| git repo-config --list" r] + set fd_rc [open "| git config --list" r] while {[gets $fd_rc line] >= 0} { if {[regexp {^([^=]+)=(.*)$} $line line name value]} { if {[is_many_config $name]} { @@ -128,14 +202,14 @@ proc save_config {} { set value $global_config_new($name) if {$value ne $global_config($name)} { if {$value eq $default_config($name)} { - catch {exec git repo-config --global --unset $name} + catch {exec git config --global --unset $name} } else { regsub -all "\[{}\]" $value {"} value - exec git repo-config --global $name $value + exec git config --global $name $value } set global_config($name) $value if {$value eq $repo_config($name)} { - catch {exec git repo-config --unset $name} + catch {exec git config --unset $name} set repo_config($name) $value } } @@ -145,10 +219,10 @@ proc save_config {} { set value $repo_config_new($name) if {$value ne $repo_config($name)} { if {$value eq $global_config($name)} { - catch {exec git repo-config --unset $name} + catch {exec git config --unset $name} } else { regsub -all "\[{}\]" $value {"} value - exec git repo-config $name $value + exec git config $name $value } set repo_config($name) $value } @@ -187,13 +261,13 @@ proc warn_popup {msg} { eval $cmd } -proc info_popup {msg} { +proc info_popup {msg {parent .}} { set title [appname] if {[reponame] ne {}} { append title " ([reponame])" } tk_messageBox \ - -parent . \ + -parent $parent \ -icon info \ -type ok \ -title $title \ @@ -223,6 +297,9 @@ if { [catch {set _gitdir $env(GIT_DIR)}] error_popup "Cannot find the git directory:\n\n$err" exit 1 } +if {![file isdirectory $_gitdir] && [is_Cygwin]} { + catch {set _gitdir [exec cygpath --unix $_gitdir]} +} if {![file isdirectory $_gitdir]} { catch {wm withdraw .} error_popup "Git directory not found:\n\n$_gitdir" @@ -230,7 +307,7 @@ if {![file isdirectory $_gitdir]} { } if {[lindex [file split $_gitdir] end] ne {.git}} { catch {wm withdraw .} - error_popup "Cannot use funny .git directory:\n\n$gitdir" + error_popup "Cannot use funny .git directory:\n\n$_gitdir" exit 1 } if {[catch {cd [file dirname $_gitdir]} err]} { @@ -242,11 +319,6 @@ set _reponame [lindex [file split \ [file normalize [file dirname $_gitdir]]] \ end] -set single_commit 0 -if {[appname] eq {git-citool}} { - set single_commit 1 -} - ###################################################################### ## ## task management @@ -335,7 +407,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 +438,12 @@ proc rescan {after} { $ui_comm edit modified false } - if {$repo_config(gui.trustmtime) eq {true}} { + if {[is_enabled branch]} { + load_all_heads + populate_branch_menu + } + + if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} { rescan_stage2 {} $after } else { set rescan_active 1 @@ -410,9 +487,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 +505,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 +528,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 +561,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 +586,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 +623,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 +645,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 +664,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 +700,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 +720,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 +785,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 +817,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 +868,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 +881,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 +991,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 +1060,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. # @@ -915,7 +1110,7 @@ File [short_path $path] cannot be committed by this program. } } if {!$files_ready} { - error_popup {No changes to commit. + info_popup {No changes to commit. You must add at least 1 file before you can commit. } @@ -926,6 +1121,7 @@ You must add 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. @@ -946,7 +1142,7 @@ A good commit message has the following format: # On Cygwin [file executable] might lie so we need to ask # the shell if the hook is executable. Yes that's annoying. # - if {[is_Windows] && [file isfile $pchook]} { + if {[is_Cygwin] && [file isfile $pchook]} { set pchook [list sh -c [concat \ "if test -x \"$pchook\";" \ "then exec \"$pchook\" 2>&1;" \ @@ -996,9 +1192,10 @@ proc commit_writetree {curHEAD msg} { proc commit_committree {fd_wt curHEAD msg} { global HEAD PARENT MERGE_HEAD commit_type - global single_commit + global 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 +1205,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 +1228,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 +1256,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]} @@ -1064,7 +1281,7 @@ proc commit_committree {fd_wt curHEAD msg} { # -- Run the post-commit hook. # set pchook [gitdir hooks post-commit] - if {[is_Windows] && [file isfile $pchook]} { + if {[is_Cygwin] && [file isfile $pchook]} { set pchook [list sh -c [concat \ "if test -x \"$pchook\";" \ "then exec \"$pchook\";" \ @@ -1080,7 +1297,7 @@ proc commit_committree {fd_wt curHEAD msg} { $ui_comm edit reset $ui_comm edit modified false - if {$single_commit} do_quit + if {[is_enabled singlecommit]} do_quit # -- Update in memory status # @@ -1129,87 +1346,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 } ###################################################################### @@ -1237,6 +1392,7 @@ proc mapdesc {state path} { } proc escape_path {path} { + regsub -all {\\} $path "\\\\" path regsub -all "\n" $path "\\n" path return $path } @@ -1287,7 +1443,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 +1454,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 +1581,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 +1622,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 +1654,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 +1699,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 +1737,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 +1770,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 +1787,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 +1835,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 +1850,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 +1901,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 +1911,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 +1919,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 +1955,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 +1996,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 +2071,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 +2090,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 +2126,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 +2137,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 +2158,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 +2181,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 @@ -1977,55 +2207,66 @@ proc do_delete_branch {} { -font font_ui listbox $w.list.l \ -height 10 \ - -width 50 \ + -width 70 \ -selectmode extended \ + -yscrollcommand [list $w.list.sby set] \ -font font_ui foreach h $all_heads { if {$h ne $current_branch} { $w.list.l insert end $h } } - 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 - } + scrollbar $w.list.sby -command [list $w.list.l yview] + pack $w.list.sby -side right -fill y + pack $w.list.l -side left -fill both -expand 1 + pack $w.list -fill both -expand 1 -pady 5 -padx 5 labelframe $w.validate \ - -text {Only Delete If} \ + -text {Delete Only If} \ -font font_ui - frame $w.validate.head - checkbutton $w.validate.head.r \ - -text {Already Merged Into:} \ - -variable delete_branch_checkhead \ + radiobutton $w.validate.head_r \ + -text {Merged Into Local Branch:} \ + -value head \ + -variable delete_branch_checktype \ + -font font_ui + 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,27 +2287,125 @@ 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]} { - $ui_comm delete 0.0 end - $ui_comm edit reset - $ui_comm edit modified false + if {$current_branch eq $new_branch} { + unlock_index + return } - set selected_commit_type new - set current_branch $new_branch - - unlock_index - error "NOT FINISHED" -} - -###################################################################### -## -## remote management - -proc load_all_remotes {} { - global repo_config + 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'." + } +} + +###################################################################### +## +## remote management + +proc load_all_remotes {} { + global repo_config global all_remotes tracking_branches set all_remotes [list] @@ -2115,9 +2454,10 @@ proc load_all_remotes {} { set all_remotes [lsort -unique $all_remotes] } -proc populate_fetch_menu {m} { +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)}]} { @@ -2146,9 +2486,11 @@ proc populate_fetch_menu {m} { } } -proc populate_push_menu {m} { +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)}]} { @@ -2169,48 +2511,1079 @@ proc populate_push_menu {m} { } 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 70 \ + -selectmode extended \ + -yscrollcommand [list $w.source.sby set] \ + -font font_ui + foreach h $all_heads { + $w.source.l insert end $h + if {$h eq $current_branch} { + $w.source.l select set end + } + } + 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 + + 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] { + _O { + continue; # and pray it works! + } + 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 + } + ?? { + error_popup "You are in the middle of a change. + +File [short_path $path] is modified. + +You should complete the current commit before +starting a merge. Doing so will help you abort +a failed merge, should the need arise. +" + 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 ui_status_value current_branch + + 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 msg "Merging $current_branch, [join $names {, }]" + set ui_status_value "$msg..." + set cons [new_console "Merge" $msg] + console_exec $cons $cmd [list finish_merge $revcnt] + bind $w {} + destroy $w +} + +proc finish_merge {revcnt w ok} { + console_done $w $ok + if {$ok} { + set msg {Merge completed successfully.} + } else { + if {$revcnt != 1} { + info_popup "Octopus merge failed. + +Your merge of $revcnt branches has failed. + +There are file-level conflicts between the +branches which must be resolved manually. + +The working directory will now be reset. + +You can attempt this merge again +by merging only one branch at a time." $w + + 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...} + return + } + + set msg {Merge failed. Conflict resolution is required.} + } + unlock_index + rescan [list set ui_status_value $msg] +} + +proc do_local_merge {} { + global current_branch + + if {![can_merge]} return + + set w .merge_setup + toplevel $w + wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + + label $w.header \ + -text "Merge Into $current_branch" \ + -font font_uibold + pack $w.header -side top -fill x + + 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 70 \ + -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 + + 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 + + foreach ref [lsort -unique $to_show] { + $w.source.l insert end $ref + } + + bind $w "grab $w" + bind $w "unlock_index;destroy $w" + bind $w unlock_index + wm title $w "[appname] ([reponame]): Merge" + tkwait window $w +} + +proc do_reset_hard {} { + global HEAD commit_type file_states + + if {[string match amend* $commit_type]} { + info_popup {Cannot abort while amending. + +You must finish amending this commit. +} + return + } + + if {![lock_index abort]} return + + if {[string match *merge* $commit_type]} { + set op merge + } else { + set op commit + } + + 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 reset_hard_wait {fd} { + global ui_comm + + read $fd + if {[eof $fd]} { + close $fd + unlock_index + + $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.}} + } +} + +###################################################################### +## +## browser + +set next_browser_id 0 + +proc new_browser {commit} { + global next_browser_id cursor_ptr M1B + global browser_commit browser_status browser_stack browser_path browser_busy + + set w .browser[incr next_browser_id] + set w_list $w.list.l + set browser_commit($w_list) $commit + set browser_status($w_list) {Starting...} + set browser_stack($w_list) {} + set browser_path($w_list) $browser_commit($w_list): + set browser_busy($w_list) 1 + + toplevel $w + label $w.path -textvariable browser_path($w_list) \ + -anchor w \ + -justify left \ + -borderwidth 1 \ + -relief sunken \ + -font font_uibold + pack $w.path -anchor w -side top -fill x + + frame $w.list + text $w_list -background white -borderwidth 0 \ + -cursor $cursor_ptr \ + -state disabled \ + -wrap none \ + -height 20 \ + -width 70 \ + -xscrollcommand [list $w.list.sbx set] \ + -yscrollcommand [list $w.list.sby set] \ + -font font_ui + $w_list tag conf in_sel \ + -background [$w_list cget -foreground] \ + -foreground [$w_list cget -background] + scrollbar $w.list.sbx -orient h -command [list $w_list xview] + scrollbar $w.list.sby -orient v -command [list $w_list yview] + pack $w.list.sbx -side bottom -fill x + pack $w.list.sby -side right -fill y + pack $w_list -side left -fill both -expand 1 + pack $w.list -side top -fill both -expand 1 + + label $w.status -textvariable browser_status($w_list) \ + -anchor w \ + -justify left \ + -borderwidth 1 \ + -relief sunken \ + -font font_ui + pack $w.status -anchor w -side bottom -fill x + + bind $w_list "browser_click 0 $w_list @%x,%y;break" + bind $w_list "browser_click 1 $w_list @%x,%y;break" + bind $w_list <$M1B-Up> "browser_parent $w_list;break" + bind $w_list <$M1B-Left> "browser_parent $w_list;break" + bind $w_list "browser_move -1 $w_list;break" + bind $w_list "browser_move 1 $w_list;break" + bind $w_list <$M1B-Right> "browser_enter $w_list;break" + bind $w_list "browser_enter $w_list;break" + bind $w_list "browser_page -1 $w_list;break" + bind $w_list "browser_page 1 $w_list;break" + bind $w_list break + bind $w_list break + + bind $w "focus $w" + bind $w " + array unset browser_buffer $w_list + array unset browser_files $w_list + array unset browser_status $w_list + array unset browser_stack $w_list + array unset browser_path $w_list + array unset browser_commit $w_list + array unset browser_busy $w_list + " + wm title $w "[appname] ([reponame]): File Browser" + ls_tree $w_list $browser_commit($w_list) {} +} + +proc browser_move {dir w} { + global browser_files browser_busy + + if {$browser_busy($w)} return + set lno [lindex [split [$w index in_sel.first] .] 0] + incr lno $dir + if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} { + $w tag remove in_sel 0.0 end + $w tag add in_sel $lno.0 [expr {$lno + 1}].0 + $w see $lno.0 + } +} + +proc browser_page {dir w} { + global browser_files browser_busy + + if {$browser_busy($w)} return + $w yview scroll $dir pages + set lno [expr {int( + [lindex [$w yview] 0] + * [llength $browser_files($w)] + + 1)}] + if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} { + $w tag remove in_sel 0.0 end + $w tag add in_sel $lno.0 [expr {$lno + 1}].0 + $w see $lno.0 + } +} + +proc browser_parent {w} { + global browser_files browser_status browser_path + global browser_stack browser_busy + + if {$browser_busy($w)} return + set info [lindex $browser_files($w) 0] + if {[lindex $info 0] eq {parent}} { + set parent [lindex $browser_stack($w) end-1] + set browser_stack($w) [lrange $browser_stack($w) 0 end-2] + if {$browser_stack($w) eq {}} { + regsub {:.*$} $browser_path($w) {:} browser_path($w) + } else { + regsub {/[^/]+$} $browser_path($w) {} browser_path($w) + } + set browser_status($w) "Loading $browser_path($w)..." + ls_tree $w [lindex $parent 0] [lindex $parent 1] + } +} + +proc browser_enter {w} { + global browser_files browser_status browser_path + global browser_commit browser_stack browser_busy + + if {$browser_busy($w)} return + set lno [lindex [split [$w index in_sel.first] .] 0] + set info [lindex $browser_files($w) [expr {$lno - 1}]] + if {$info ne {}} { + switch -- [lindex $info 0] { + parent { + browser_parent $w + } + tree { + set name [lindex $info 2] + set escn [escape_path $name] + set browser_status($w) "Loading $escn..." + append browser_path($w) $escn + ls_tree $w [lindex $info 1] $name + } + blob { + set name [lindex $info 2] + set p {} + foreach n $browser_stack($w) { + append p [lindex $n 1] + } + append p $name + show_blame $browser_commit($w) $p + } + } + } +} + +proc browser_click {was_double_click w pos} { + global browser_files browser_busy + + if {$browser_busy($w)} return + set lno [lindex [split [$w index $pos] .] 0] + focus $w + + if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} { + $w tag remove in_sel 0.0 end + $w tag add in_sel $lno.0 [expr {$lno + 1}].0 + if {$was_double_click} { + browser_enter $w + } + } +} + +proc ls_tree {w tree_id name} { + global browser_buffer browser_files browser_stack browser_busy + + set browser_buffer($w) {} + set browser_files($w) {} + set browser_busy($w) 1 + + $w conf -state normal + $w tag remove in_sel 0.0 end + $w delete 0.0 end + if {$browser_stack($w) ne {}} { + $w image create end \ + -align center -padx 5 -pady 1 \ + -name icon0 \ + -image file_uplevel + $w insert end {[Up To Parent]} + lappend browser_files($w) parent + } + lappend browser_stack($w) [list $tree_id $name] + $w conf -state disabled + + set cmd [list git ls-tree -z $tree_id] + set fd [open "| $cmd" r] + fconfigure $fd -blocking 0 -translation binary -encoding binary + fileevent $fd readable [list read_ls_tree $fd $w] +} + +proc read_ls_tree {fd w} { + global browser_buffer browser_files browser_status browser_busy + + if {![winfo exists $w]} { + catch {close $fd} + return + } + + append browser_buffer($w) [read $fd] + set pck [split $browser_buffer($w) "\0"] + set browser_buffer($w) [lindex $pck end] + + set n [llength $browser_files($w)] + $w conf -state normal + foreach p [lrange $pck 0 end-1] { + set info [split $p "\t"] + set path [lindex $info 1] + set info [split [lindex $info 0] { }] + set type [lindex $info 1] + set object [lindex $info 2] + + switch -- $type { + blob { + set image file_mod + } + tree { + set image file_dir + append path / + } + default { + set image file_question + } + } + + if {$n > 0} {$w insert end "\n"} + $w image create end \ + -align center -padx 5 -pady 1 \ + -name icon[incr n] \ + -image $image + $w insert end [escape_path $path] + lappend browser_files($w) [list $type $object $path] + } + $w conf -state disabled + + if {[eof $fd]} { + close $fd + set browser_status($w) Ready. + set browser_busy($w) 0 + array unset browser_buffer $w + if {$n > 0} { + $w tag add in_sel 1.0 2.0 + focus -force $w } } } -proc populate_pull_menu {m} { - global repo_config all_remotes disable_on_lock +proc show_blame {commit path} { + global next_browser_id blame_status blame_data + + if {[winfo ismapped .]} { + set w .browser[incr next_browser_id] + set tl $w + toplevel $w + } else { + set w {} + set tl . + } + set blame_status($w) {Loading current file content...} + set texts [list] + + label $w.path -text "$commit:$path" \ + -anchor w \ + -justify left \ + -borderwidth 1 \ + -relief sunken \ + -font font_uibold + pack $w.path -side top -fill x + + set hbg #e2effa + frame $w.out + label $w.out.commit_l -text Commit \ + -relief solid \ + -borderwidth 1 \ + -background $hbg \ + -font font_uibold + text $w.out.commit_t \ + -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 9 \ + -font font_diff + lappend texts $w.out.commit_t + + label $w.out.author_l -text Author \ + -relief solid \ + -borderwidth 1 \ + -background $hbg \ + -font font_uibold + text $w.out.author_t \ + -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 20 \ + -font font_diff + lappend texts $w.out.author_t + + label $w.out.date_l -text Date \ + -relief solid \ + -borderwidth 1 \ + -background $hbg \ + -font font_uibold + text $w.out.date_t \ + -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width [string length "yyyy-mm-dd hh:mm:ss"] \ + -font font_diff + lappend texts $w.out.date_t + + label $w.out.filename_l -text Filename \ + -relief solid \ + -borderwidth 1 \ + -background $hbg \ + -font font_uibold + text $w.out.filename_t \ + -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 20 \ + -font font_diff + lappend texts $w.out.filename_t + + label $w.out.origlinenumber_l -text {Orig Line} \ + -relief solid \ + -borderwidth 1 \ + -background $hbg \ + -font font_uibold + text $w.out.origlinenumber_t \ + -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 5 \ + -font font_diff + $w.out.origlinenumber_t tag conf linenumber -justify right + lappend texts $w.out.origlinenumber_t + + label $w.out.linenumber_l -text {Curr Line} \ + -relief solid \ + -borderwidth 1 \ + -background $hbg \ + -font font_uibold + text $w.out.linenumber_t \ + -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 5 \ + -font font_diff + $w.out.linenumber_t tag conf linenumber -justify right + lappend texts $w.out.linenumber_t + + label $w.out.file_l -text {File Content} \ + -relief solid \ + -borderwidth 1 \ + -background $hbg \ + -font font_uibold + text $w.out.file_t \ + -background white -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 80 \ + -xscrollcommand [list $w.out.sbx set] \ + -font font_diff + lappend texts $w.out.file_t + + scrollbar $w.out.sbx -orient h -command [list $w.out.file_t xview] + scrollbar $w.out.sby -orient v \ + -command [list scrollbar2many $texts yview] + set labels [list] + foreach i $texts { + regsub {_t$} $i _l l + lappend labels $l + } + set file_col [expr {[llength $texts] - 1}] + eval grid $labels -sticky we + eval grid $texts $w.out.sby -sticky nsew + grid conf $w.out.sbx -column $file_col -sticky we + grid columnconfigure $w.out $file_col -weight 1 + grid rowconfigure $w.out 1 -weight 1 + pack $w.out -fill both -expand 1 + + label $w.status -textvariable blame_status($w) \ + -anchor w \ + -justify left \ + -borderwidth 1 \ + -relief sunken \ + -font font_ui + pack $w.status -side bottom -fill x + + menu $w.ctxm -tearoff 0 + $w.ctxm add command -label "Copy Commit" \ + -font font_ui \ + -command "blame_copycommit $w \$cursorW @\$cursorX,\$cursorY" + + foreach i $texts { + $i tag conf in_sel \ + -background [$i cget -foreground] \ + -foreground [$i cget -background] + $i conf -yscrollcommand \ + [list many2scrollbar $texts yview $w.out.sby] + bind $i "blame_highlight $i @%x,%y $texts;break" + bind_button3 $i " + set cursorX %x + set cursorY %y + set cursorW %W + tk_popup $w.ctxm %X %Y + " + } + + set blame_data($w,colors) {} + + bind $tl "focus $tl" + bind $tl " + array unset blame_status {$w} + array unset blame_data $w,* + " + wm title $tl "[appname] ([reponame]): File Viewer" + + set blame_data($w,total_lines) 0 + set cmd [list git cat-file blob "$commit:$path"] + set fd [open "| $cmd" r] + fconfigure $fd -blocking 0 -translation lf -encoding binary + fileevent $fd readable [list read_blame_catfile \ + $fd $w $commit $path \ + $texts $w.out.linenumber_t $w.out.file_t] +} + +proc read_blame_catfile {fd w commit path texts w_lno w_file} { + global blame_status blame_data + + if {![winfo exists $w_file]} { + catch {close $fd} + return + } + + set n $blame_data($w,total_lines) + foreach i $texts {$i conf -state normal} + while {[gets $fd line] >= 0} { + regsub "\r\$" $line {} line + incr n + $w_lno insert end $n linenumber + $w_file insert end $line + foreach i $texts {$i insert end "\n"} + } + foreach i $texts {$i conf -state disabled} + set blame_data($w,total_lines) $n + + if {[eof $fd]} { + close $fd + set blame_status($w) {Loading annotations...} + set cmd [list git blame -M -C --incremental] + lappend cmd $commit -- $path + set fd [open "| $cmd" r] + fconfigure $fd -blocking 0 -translation lf -encoding binary + set handler [list read_blame_incremental $fd $w] + append handler " $texts" + fileevent $fd readable $handler + } +} + +proc read_blame_incremental {fd w + w_commit w_author w_date w_filename w_olno + w_lno w_file} { + global blame_status blame_data - 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 + if {![winfo exists $w_commit]} { + catch {close $fd} + return + } + + set all [list \ + $w_commit \ + $w_author \ + $w_date \ + $w_filename \ + $w_olno \ + $w_lno \ + $w_file] + + $w_commit conf -state normal + $w_author conf -state normal + $w_date conf -state normal + $w_filename conf -state normal + $w_olno conf -state normal + + while {[gets $fd line] >= 0} { + if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \ + cmit original_line final_line line_count]} { + set blame_data($w,commit) $cmit + set blame_data($w,original_line) $original_line + set blame_data($w,final_line) $final_line + set blame_data($w,line_count) $line_count + + if {[catch {set g $blame_data($w,$cmit,seen)}]} { + if {$blame_data($w,colors) eq {}} { + set blame_data($w,colors) { + yellow + red + pink + orange + green + grey } } + set c [lindex $blame_data($w,colors) 0] + set blame_data($w,colors) \ + [lrange $blame_data($w,colors) 1 end] + foreach t $all { + $t tag conf g$cmit -background $c + } + } else { + set blame_data($w,$cmit,seen) 1 } - } 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 + } elseif {[string match {filename *} $line]} { + set n $blame_data($w,line_count) + set lno $blame_data($w,final_line) + set ol $blame_data($w,original_line) + set file [string range $line 9 end] + set cmit $blame_data($w,commit) + set abbrev [string range $cmit 0 8] + + if {[catch {set author $blame_data($w,$cmit,author)} err]} { + set author {} + } + + if {[catch {set atime $blame_data($w,$cmit,author-time)}]} { + set atime {} + } else { + set atime [clock format $atime -format {%Y-%m-%d %T}] + } + + while {$n > 0} { + if {![catch {set g g$blame_data($w,line$lno,commit)}]} { + foreach t $all { + $t tag remove $g $lno.0 "$lno.0 lineend + 1c" } } - close $fd + + foreach t [list \ + $w_commit \ + $w_author \ + $w_date \ + $w_filename \ + $w_olno] { + $t delete $lno.0 "$lno.0 lineend" + } + + $w_commit insert $lno.0 $abbrev + $w_author insert $lno.0 $author + $w_date insert $lno.0 $atime + $w_filename insert $lno.0 $file + $w_olno insert $lno.0 $ol linenumber + + set g g$cmit + foreach t $all { + $t tag add $g $lno.0 "$lno.0 lineend + 1c" + } + + set blame_data($w,line$lno,commit) $cmit + + incr n -1 + incr lno + incr ol } + } elseif {[regexp {^([a-z-]+) (.*)$} $line line header data]} { + set blame_data($w,$blame_data($w,commit),$header) $data } + } - 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] - } + $w_commit conf -state disabled + $w_author conf -state disabled + $w_date conf -state disabled + $w_filename conf -state disabled + $w_olno conf -state disabled + + if {[eof $fd]} { + close $fd + set blame_status($w) {Annotation complete.} + } +} + +proc blame_highlight {w pos args} { + set lno [lindex [split [$w index $pos] .] 0] + foreach i $args { + $i tag remove in_sel 0.0 end + } + if {$lno eq {}} return + foreach i $args { + $i tag add in_sel $lno.0 "$lno.0 + 1 line" + } +} + +proc blame_copycommit {w i pos} { + global blame_data + set lno [lindex [split [$i index $pos] .] 0] + if {![catch {set commit $blame_data($w,line$lno,commit)}]} { + clipboard clear + clipboard append \ + -format STRING \ + -type STRING \ + -- $commit } } @@ -2290,6 +3663,32 @@ static unsigned char file_merge_bits[] = { 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f}; } -maskdata $filemask +set file_dir_data { +#define file_width 18 +#define file_height 18 +static unsigned char file_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x03, 0x00, + 0x0c, 0x03, 0x00, 0x04, 0xfe, 0x00, 0x06, 0x80, 0x00, 0xff, 0x9f, 0x00, + 0x03, 0x98, 0x00, 0x02, 0x90, 0x00, 0x06, 0xb0, 0x00, 0x04, 0xa0, 0x00, + 0x0c, 0xe0, 0x00, 0x08, 0xc0, 0x00, 0xf8, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +} +image create bitmap file_dir -background white -foreground blue \ + -data $file_dir_data -maskdata $file_dir_data +unset file_dir_data + +set file_uplevel_data { +#define up_width 15 +#define up_height 15 +static unsigned char up_bits[] = { + 0x80, 0x00, 0xc0, 0x01, 0xe0, 0x03, 0xf0, 0x07, 0xf8, 0x0f, 0xfc, 0x1f, + 0xfe, 0x3f, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, + 0xc0, 0x01, 0xc0, 0x01, 0x00, 0x00}; +} +image create bitmap file_uplevel -background white -foreground red \ + -data $file_uplevel_data -maskdata $file_uplevel_data +unset file_uplevel_data + set ui_index .vpane.files.index.list set ui_workdir .vpane.files.workdir.list @@ -2336,24 +3735,8 @@ foreach i { unset i ###################################################################### -## -## util - -proc is_MacOSX {} { - global tcl_platform tk_library - if {[tk windowingsystem] eq {aqua}} { - return 1 - } - return 0 -} - -proc is_Windows {} { - global tcl_platform - if {$tcl_platform(platform) eq {windows}} { - return 1 - } - return 0 -} +## +## util proc bind_button3 {w cmd} { bind $w $cmd @@ -2362,6 +3745,15 @@ proc bind_button3 {w cmd} { } } +proc scrollbar2many {list mode args} { + foreach w $list {eval $w $mode $args} +} + +proc many2scrollbar {list mode sb top bottom} { + $sb set $top $bottom + foreach w $list {$w $mode moveto $top} +} + proc incr_font_size {font {amt 1}} { set sz [font configure $font -size] incr sz $amt @@ -2454,7 +3846,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,11 +3869,11 @@ proc console_init {w} { return $w } -proc console_exec {w cmd {after {}}} { - # -- Windows tosses the enviroment when we exec our child. +proc console_exec {w cmd after} { + # -- Cygwin's Tcl tosses the enviroment when we exec our child. # But most users need that so we have to relogin. :-( # - if {[is_Windows]} { + if {[is_Cygwin]} { set cmd [list sh --login -c "cd \"[pwd]\" && [join $cmd { }]"] } @@ -2496,7 +3888,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 +3922,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 @@ -2556,19 +3995,27 @@ proc console_read {w fd after} { set starting_gitk_msg {Starting gitk... please wait...} proc do_gitk {revs} { - global ui_status_value starting_gitk_msg + global env ui_status_value starting_gitk_msg + + # -- On Windows gitk is severly broken, and right now it seems like + # nobody cares about fixing it. The only known workaround is to + # always delete ~/.gitk before starting the program. + # + if {[is_Windows]} { + catch {file delete [file join $env(HOME) .gitk]} + } - set cmd gitk + # -- Always start gitk through whatever we were loaded with. This + # lets us bypass using shell process on Windows systems. + # + set cmd [info nameofexecutable] + lappend cmd [gitexec gitk] if {$revs ne {}} { append cmd { } append cmd $revs } - if {[is_Windows]} { - set cmd "sh -c \"exec $cmd\"" - } - append cmd { &} - if {[catch {eval exec $cmd} err]} { + if {[catch {eval exec $cmd &} err]} { error_popup "Failed to start gitk:\n\n$err" } else { set ui_status_value $starting_gitk_msg @@ -2580,9 +4027,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 +4112,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 @@ -2603,33 +4123,36 @@ proc do_quit {} { if {$is_quitting} return set is_quitting 1 - # -- Stash our current commit buffer. - # - 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] - && $msg ne {}} { - catch { - set fd [open $save w] - puts $fd [string trim [$ui_comm get 0.0 end]] - close $fd + if {[winfo exists $ui_comm]} { + # -- Stash our current commit buffer. + # + set save [gitdir GITGUI_MSG] + set msg [string trim [$ui_comm get 0.0 end]] + 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 -nonewline $fd $msg + close $fd + } + } else { + catch {file delete $save} } - } else { - catch {file delete $save} - } - # -- Stash our current window geometry into this repository. - # - set cfg_geometry [list] - lappend cfg_geometry [wm geometry .] - lappend cfg_geometry [lindex [.vpane sash coord 0] 1] - lappend cfg_geometry [lindex [.vpane.files sash coord 0] 0] - if {[catch {set rc_geometry $repo_config(gui.geometry)}]} { - set rc_geometry {} - } - if {$cfg_geometry ne $rc_geometry} { - catch {exec git repo-config gui.geometry $cfg_geometry} + # -- Stash our current window geometry into this repository. + # + set cfg_geometry [list] + lappend cfg_geometry [wm geometry .] + lappend cfg_geometry [lindex [.vpane sash coord 0] 1] + lappend cfg_geometry [lindex [.vpane.files sash coord 0] 0] + if {[catch {set rc_geometry $repo_config(gui.geometry)}]} { + set rc_geometry {} + } + if {$cfg_geometry ne $rc_geometry} { + catch {exec git config gui.geometry $cfg_geometry} + } } destroy . @@ -2958,43 +4481,68 @@ 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 + set optid 0 foreach option { - {b pullsummary {Show Pull Summary}} - {b trustmtime {Trust File Modification Timestamps}} - {i diffcontext {Number of Diff Context Lines}} + {t user.name {User Name}} + {t user.email {Email Address}} + + {b merge.summary {Summarize Merge Commits}} + {i-1..5 merge.verbosity {Merge Verbosity}} + + {b gui.trustmtime {Trust File Modification Timestamps}} + {i-1..99 gui.diffcontext {Number of Diff Context Lines}} + {t gui.newbranchtemplate {New Branch Name Template}} } { set type [lindex $option 0] set name [lindex $option 1] set text [lindex $option 2] + incr optid foreach f {repo global} { - switch $type { + switch -glob -- $type { b { - checkbutton $w.$f.$name -text $text \ - -variable ${f}_config_new(gui.$name) \ + checkbutton $w.$f.$optid -text $text \ + -variable ${f}_config_new($name) \ -onvalue true \ -offvalue false \ -font font_ui - pack $w.$f.$name -side top -anchor w + pack $w.$f.$optid -side top -anchor w + } + i-* { + regexp -- {-(\d+)\.\.(\d+)$} $type _junk min max + frame $w.$f.$optid + label $w.$f.$optid.l -text "$text:" -font font_ui + pack $w.$f.$optid.l -side left -anchor w -fill x + spinbox $w.$f.$optid.v \ + -textvariable ${f}_config_new($name) \ + -from $min \ + -to $max \ + -increment 1 \ + -width [expr {1 + [string length $max]}] \ + -font font_ui + bind $w.$f.$optid.v {%W selection range 0 end} + pack $w.$f.$optid.v -side right -anchor e -padx 5 + pack $w.$f.$optid -side top -anchor w -fill x } - i { - frame $w.$f.$name - label $w.$f.$name.l -text "$text:" -font font_ui - pack $w.$f.$name.l -side left -anchor w -fill x - spinbox $w.$f.$name.v \ - -textvariable ${f}_config_new(gui.$name) \ - -from 1 -to 99 -increment 1 \ - -width 3 \ + t { + frame $w.$f.$optid + label $w.$f.$optid.l -text "$text:" -font font_ui + entry $w.$f.$optid.v \ + -borderwidth 1 \ + -relief sunken \ + -width 20 \ + -textvariable ${f}_config_new($name) \ -font font_ui - pack $w.$f.$name.v -side right -anchor e - pack $w.$f.$name -side top -anchor w -fill x + pack $w.$f.$optid.l -side left -anchor w + pack $w.$f.$optid.v -side left -anchor w \ + -fill x -expand 1 \ + -padx 5 + pack $w.$f.$optid -side top -anchor w -fill x } } } @@ -3022,6 +4570,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 @@ -3069,6 +4618,29 @@ proc do_save_config {w} { proc do_windows_shortcut {} { global argv0 + set fn [tk_getSaveFile \ + -parent . \ + -title "[appname] ([reponame]): Create Desktop Icon" \ + -initialfile "Git [reponame].bat"] + if {$fn != {}} { + if {[catch { + set fd [open $fn w] + puts $fd "@ECHO Entering [reponame]" + puts $fd "@ECHO Starting git-gui... please wait..." + puts $fd "@SET PATH=[file normalize [gitexec]];%PATH%" + puts $fd "@SET GIT_DIR=[file normalize [gitdir]]" + puts -nonewline $fd "@\"[info nameofexecutable]\"" + puts $fd " \"[file normalize $argv0]\"" + close $fd + } err]} { + error_popup "Cannot write script:\n\n$err" + } + } +} + +proc do_cygwin_shortcut {} { + global argv0 + if {[catch { set desktop [exec cygpath \ --windows \ @@ -3160,7 +4732,7 @@ proc do_macosx_app {} { set fd [open $exe w] set gd [file normalize [gitdir]] - set ep [file normalize [exec git --exec-path]] + set ep [file normalize [gitexec]] regsub -all ' $gd "'\\''" gd regsub -all ' $ep "'\\''" ep puts $fd "#!/bin/sh" @@ -3324,9 +4896,14 @@ proc apply_config {} { } } +set default_config(merge.summary) false +set default_config(merge.verbosity) 2 +set default_config(user.name) {} +set default_config(user.email) {} + 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 { @@ -3336,22 +4913,62 @@ set font_descs { load_config 0 apply_config +###################################################################### +## +## feature option selection + +if {[regexp {^git-(.+)$} [appname] _junk subcommand]} { + unset _junk +} else { + set subcommand gui +} +if {$subcommand eq {gui.sh}} { + set subcommand gui +} +if {$subcommand eq {gui} && [llength $argv] > 0} { + set subcommand [lindex $argv 0] + set argv [lrange $argv 1 end] +} + +enable_option multicommit +enable_option branch +enable_option transport + +switch -- $subcommand { +blame { + disable_option multicommit + disable_option branch + disable_option transport +} +citool { + enable_option singlecommit + + disable_option multicommit + disable_option branch + disable_option transport +} +} + ###################################################################### ## ## ui construction +set ui_comm {} + # -- Menu Bar # menu .mbar -tearoff 0 .mbar add cascade -label Repository -menu .mbar.repository .mbar add cascade -label Edit -menu .mbar.edit -if {!$single_commit} { +if {[is_enabled branch]} { .mbar add cascade -label Branch -menu .mbar.branch } -.mbar add cascade -label Commit -menu .mbar.commit -if {!$single_commit} { +if {[is_enabled multicommit] || [is_enabled singlecommit]} { + .mbar add cascade -label Commit -menu .mbar.commit +} +if {[is_enabled transport]} { + .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 @@ -3359,19 +4976,30 @@ if {!$single_commit} { # -- Repository Menu # menu .mbar.repository + +.mbar.repository add command \ + -label {Browse Current Branch} \ + -command {new_browser $current_branch} \ + -font font_ui +trace add variable current_branch write ".mbar.repository entryconf [.mbar.repository index last] -label \"Browse \$current_branch\" ;#" +.mbar.repository add separator + .mbar.repository add command \ -label {Visualize Current Branch} \ - -command {do_gitk {}} \ + -command {do_gitk $current_branch} \ + -font font_ui +trace add variable current_branch write ".mbar.repository entryconf [.mbar.repository index last] -label \"Visualize \$current_branch\" ;#" +.mbar.repository add command \ + -label {Visualize All Branches} \ + -command {do_gitk --all} \ -font font_ui -if {![is_MacOSX]} { - .mbar.repository add command \ - -label {Visualize All Branches} \ - -command {do_gitk {--all}} \ - -font font_ui -} .mbar.repository add separator -if {!$single_commit} { +if {[is_enabled multicommit]} { + .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 @@ -3382,7 +5010,12 @@ if {!$single_commit} { .mbar.repository add separator - if {[is_Windows]} { + if {[is_Cygwin]} { + .mbar.repository add command \ + -label {Create Desktop Icon} \ + -command do_cygwin_shortcut \ + -font font_ui + } elseif {[is_Windows]} { .mbar.repository add command \ -label {Create Desktop Icon} \ -command do_windows_shortcut \ @@ -3436,7 +5069,7 @@ menu .mbar.edit # -- Branch Menu # -if {!$single_commit} { +if {[is_enabled branch]} { menu .mbar.branch .mbar.branch add command -label {Create...} \ @@ -3455,80 +5088,74 @@ if {!$single_commit} { # -- Commit Menu # -menu .mbar.commit - -.mbar.commit add radiobutton \ - -label {New Commit} \ - -command do_select_commit_type \ - -variable selected_commit_type \ - -value new \ - -font font_ui -lappend disable_on_lock \ - [list .mbar.commit entryconf [.mbar.commit index last] -state] - -.mbar.commit add radiobutton \ - -label {Amend Last Commit} \ - -command do_select_commit_type \ - -variable selected_commit_type \ - -value amend \ - -font font_ui -lappend disable_on_lock \ - [list .mbar.commit entryconf [.mbar.commit index last] -state] +if {[is_enabled multicommit] || [is_enabled singlecommit]} { + menu .mbar.commit + + .mbar.commit add radiobutton \ + -label {New Commit} \ + -command do_select_commit_type \ + -variable selected_commit_type \ + -value new \ + -font font_ui + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] -.mbar.commit add separator + .mbar.commit add radiobutton \ + -label {Amend Last Commit} \ + -command do_select_commit_type \ + -variable selected_commit_type \ + -value amend \ + -font font_ui + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] -.mbar.commit add command -label Rescan \ - -command do_rescan \ - -accelerator F5 \ - -font font_ui -lappend disable_on_lock \ - [list .mbar.commit entryconf [.mbar.commit index last] -state] + .mbar.commit add separator -.mbar.commit add command -label {Add To Commit} \ - -command do_add_selection \ - -font font_ui -lappend disable_on_lock \ - [list .mbar.commit entryconf [.mbar.commit index last] -state] + .mbar.commit add command -label Rescan \ + -command do_rescan \ + -accelerator F5 \ + -font font_ui + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] -.mbar.commit add command -label {Add All To Commit} \ - -command do_add_all \ - -accelerator $M1T-I \ - -font font_ui -lappend disable_on_lock \ - [list .mbar.commit entryconf [.mbar.commit index last] -state] + .mbar.commit add command -label {Add To Commit} \ + -command do_add_selection \ + -font font_ui + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] -.mbar.commit add command -label {Unstage From Commit} \ - -command do_unstage_selection \ - -font font_ui -lappend disable_on_lock \ - [list .mbar.commit entryconf [.mbar.commit index last] -state] + .mbar.commit add command -label {Add Existing To Commit} \ + -command do_add_all \ + -accelerator $M1T-I \ + -font font_ui + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] -.mbar.commit add command -label {Revert Changes} \ - -command do_revert_selection \ - -font font_ui -lappend disable_on_lock \ - [list .mbar.commit entryconf [.mbar.commit index last] -state] + .mbar.commit add command -label {Unstage From Commit} \ + -command do_unstage_selection \ + -font font_ui + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] -.mbar.commit add separator + .mbar.commit add command -label {Revert Changes} \ + -command do_revert_selection \ + -font font_ui + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] -.mbar.commit add command -label {Sign Off} \ - -command do_signoff \ - -accelerator $M1T-S \ - -font font_ui + .mbar.commit add separator -.mbar.commit add command -label Commit \ - -command do_commit \ - -accelerator $M1T-Return \ - -font font_ui -lappend disable_on_lock \ - [list .mbar.commit entryconf [.mbar.commit index last] -state] + .mbar.commit add command -label {Sign Off} \ + -command do_signoff \ + -accelerator $M1T-S \ + -font font_ui -# -- Transport menus -# -if {!$single_commit} { - menu .mbar.fetch - menu .mbar.pull - menu .mbar.push + .mbar.commit add command -label Commit \ + -command do_commit \ + -accelerator $M1T-Return \ + -font font_ui + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] } if {[is_MacOSX]} { @@ -3580,17 +5207,95 @@ if {[is_MacOSX]} { lappend disable_on_lock \ [list .mbar.tools entryconf [.mbar.tools index last] -state] } +} - # -- Help Menu - # - .mbar add cascade -label Help -menu .mbar.help - menu .mbar.help +# -- Help Menu +# +.mbar add cascade -label Help -menu .mbar.help +menu .mbar.help +if {![is_MacOSX]} { .mbar.help add command -label "About [appname]" \ -command do_about \ -font font_ui } +set browser {} +catch {set browser $repo_config(instaweb.browser)} +set doc_path [file dirname [gitexec]] +set doc_path [file join $doc_path Documentation index.html] + +if {[is_Cygwin]} { + set doc_path [exec cygpath --windows $doc_path] +} + +if {$browser eq {}} { + if {[is_MacOSX]} { + set browser open + } elseif {[is_Cygwin]} { + set program_files [file dirname [exec cygpath --windir]] + set program_files [file join $program_files {Program Files}] + set firefox [file join $program_files {Mozilla Firefox} firefox.exe] + set ie [file join $program_files {Internet Explorer} IEXPLORE.EXE] + if {[file exists $firefox]} { + set browser $firefox + } elseif {[file exists $ie]} { + set browser $ie + } + unset program_files firefox ie + } +} + +if {[file isfile $doc_path]} { + set doc_url "file:$doc_path" +} else { + set doc_url {http://www.kernel.org/pub/software/scm/git/docs/} +} + +if {$browser ne {}} { + .mbar.help add command -label {Online Documentation} \ + -command [list exec $browser $doc_url &] \ + -font font_ui +} +unset browser doc_path doc_url + +# -- Standard bindings +# +bind . do_quit +bind all <$M1B-Key-q> do_quit +bind all <$M1B-Key-Q> do_quit +bind all <$M1B-Key-w> {destroy [winfo toplevel %W]} +bind all <$M1B-Key-W> {destroy [winfo toplevel %W]} + +# -- Not a normal commit type invocation? Do that instead! +# +switch -- $subcommand { +blame { + if {[llength $argv] != 2} { + puts stderr "usage: $argv0 blame commit path" + exit 1 + } + set current_branch [lindex $argv 0] + show_blame $current_branch [lindex $argv 1] + return +} +citool - +gui { + if {[llength $argv] != 0} { + puts -nonewline stderr "usage: $argv0" + if {$subcommand ne {gui} && [appname] ne "git-$subcommand"} { + puts -nonewline stderr " $subcommand" + } + puts stderr {} + exit 1 + } + # fall through to setup UI for commits +} +default { + puts stderr "usage: $argv0 \[{blame|citool}\]" + exit 1 +} +} # -- Branch Control # @@ -3611,46 +5316,76 @@ pack .branch.l1 -side left pack .branch.cb -side left -fill x pack .branch -side top -fill x +if {[is_enabled branch]} { + 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 +5404,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 # @@ -3688,7 +5423,7 @@ pack .vpane.lower.commarea.buttons.rescan -side top -fill x lappend disable_on_lock \ {.vpane.lower.commarea.buttons.rescan conf -state} -button .vpane.lower.commarea.buttons.incall -text {Add All} \ +button .vpane.lower.commarea.buttons.incall -text {Add Existing} \ -command do_add_all \ -font font_ui pack .vpane.lower.commarea.buttons.incall -side top -fill x @@ -3789,7 +5524,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 +5543,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 +5624,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 +5654,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 +5664,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 +5673,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 +5685,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 +5723,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,13 +5786,13 @@ 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} { +if {[is_enabled branch]} { bind . <$M1B-Key-n> do_create_branch bind . <$M1B-Key-N> do_create_branch } -bind . do_quit bind all do_rescan bind all <$M1B-Key-r> do_rescan bind all <$M1B-Key-R> do_rescan @@ -4044,10 +5801,6 @@ bind . <$M1B-Key-S> do_signoff bind . <$M1B-Key-i> do_add_all bind . <$M1B-Key-I> do_add_all bind . <$M1B-Key-Return> do_commit -bind all <$M1B-Key-q> do_quit -bind all <$M1B-Key-Q> do_quit -bind all <$M1B-Key-w> {destroy [winfo toplevel %W]} -bind all <$M1B-Key-W> {destroy [winfo toplevel %W]} foreach i [list $ui_index $ui_workdir] { bind $i "toggle_or_diff $i %x %y; break" bind $i <$M1B-Button-1> "add_one_to_selection $i %x %y; break" @@ -4074,7 +5827,7 @@ focus -force $ui_comm # does *not* pass its env array onto any processes it spawns. # This means that git processes get none of our environment. # -if {[is_Windows]} { +if {[is_Cygwin]} { set ignored_env 0 set suggest_user {} set msg "Possible environment issues exist. @@ -4127,19 +5880,18 @@ user.email settings into your personal # -- Only initialize complex UI if we are going to stay running. # -if {!$single_commit} { +if {[is_enabled transport]} { load_all_remotes 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. # -if {!$single_commit} { +if {[is_enabled multicommit]} { set object_limit 2000 if {[is_Windows]} {set object_limit 200} regexp {^([0-9]+) objects,} [exec git count-objects] _junk objects_current