#!/bin/sh # Tcl ignores the next line -*- tcl -*- \ exec wish "$0" -- "$@" set appvers {@@GIT_VERSION@@} set copyright { Copyright © 2006, 2007 Shawn Pearce, Paul Mackerras. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA} ###################################################################### ## ## read only globals set _appname [lindex [file split $argv0] end] set _gitdir {} set _gitexec {} set _reponame {} set _iscygwin {} proc appname {} { global _appname return $_appname } proc gitdir {args} { global _gitdir if {$args eq {}} { return $_gitdir } 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 proc is_many_config {name} { switch -glob -- $name { remote.*.fetch - remote.*.push {return 1} * {return 0} } } 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] while {[gets $fd_rc line] >= 0} { if {[regexp {^([^=]+)=(.*)$} $line line name value]} { if {[is_many_config $name]} { lappend global_config($name) $value } else { set global_config($name) $value } } } close $fd_rc } } array unset repo_config catch { set fd_rc [open "| git repo-config --list" r] while {[gets $fd_rc line] >= 0} { if {[regexp {^([^=]+)=(.*)$} $line line name value]} { if {[is_many_config $name]} { lappend repo_config($name) $value } else { set repo_config($name) $value } } } close $fd_rc } foreach name [array names default_config] { if {[catch {set v $global_config($name)}]} { set global_config($name) $default_config($name) } if {[catch {set v $repo_config($name)}]} { set repo_config($name) $default_config($name) } } } proc save_config {} { global default_config font_descs global repo_config global_config global repo_config_new global_config_new foreach option $font_descs { set name [lindex $option 0] set font [lindex $option 1] font configure $font \ -family $global_config_new(gui.$font^^family) \ -size $global_config_new(gui.$font^^size) font configure ${font}bold \ -family $global_config_new(gui.$font^^family) \ -size $global_config_new(gui.$font^^size) set global_config_new(gui.$name) [font configure $font] unset global_config_new(gui.$font^^family) unset global_config_new(gui.$font^^size) } foreach name [array names default_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} } else { regsub -all "\[{}\]" $value {"} value exec git repo-config --global $name $value } set global_config($name) $value if {$value eq $repo_config($name)} { catch {exec git repo-config --unset $name} set repo_config($name) $value } } } foreach name [array names default_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} } else { regsub -all "\[{}\]" $value {"} value exec git repo-config $name $value } set repo_config($name) $value } } } proc error_popup {msg} { set title [appname] if {[reponame] ne {}} { append title " ([reponame])" } set cmd [list tk_messageBox \ -icon error \ -type ok \ -title "$title: error" \ -message $msg] if {[winfo ismapped .]} { lappend cmd -parent . } eval $cmd } proc warn_popup {msg} { set title [appname] if {[reponame] ne {}} { append title " ([reponame])" } set cmd [list tk_messageBox \ -icon warning \ -type ok \ -title "$title: warning" \ -message $msg] if {[winfo ismapped .]} { lappend cmd -parent . } eval $cmd } proc info_popup {msg {parent .}} { set title [appname] if {[reponame] ne {}} { append title " ([reponame])" } tk_messageBox \ -parent $parent \ -icon info \ -type ok \ -title $title \ -message $msg } proc ask_popup {msg} { set title [appname] if {[reponame] ne {}} { append title " ([reponame])" } return [tk_messageBox \ -parent . \ -icon question \ -type yesno \ -title $title \ -message $msg] } ###################################################################### ## ## repository setup if { [catch {set _gitdir $env(GIT_DIR)}] && [catch {set _gitdir [exec git rev-parse --git-dir]} err]} { catch {wm withdraw .} 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" exit 1 } if {[lindex [file split $_gitdir] end] ne {.git}} { catch {wm withdraw .} error_popup "Cannot use funny .git directory:\n\n$_gitdir" exit 1 } if {[catch {cd [file dirname $_gitdir]} err]} { catch {wm withdraw .} error_popup "No working directory [file dirname $_gitdir]:\n\n$err" exit 1 } set _reponame [lindex [file split \ [file normalize [file dirname $_gitdir]]] \ end] enable_option multicommit if {[appname] eq {git-citool}} { disable_option multicommit } ###################################################################### ## ## task management set rescan_active 0 set diff_active 0 set last_clicked {} set disable_on_lock [list] set index_lock_type none proc lock_index {type} { global index_lock_type disable_on_lock if {$index_lock_type eq {none}} { set index_lock_type $type foreach w $disable_on_lock { uplevel #0 $w disabled } return 1 } elseif {$index_lock_type eq "begin-$type"} { set index_lock_type $type return 1 } return 0 } proc unlock_index {} { global index_lock_type disable_on_lock set index_lock_type none foreach w $disable_on_lock { uplevel #0 $w normal } } ###################################################################### ## ## status proc repository_state {ctvar hdvar mhvar} { global current_branch upvar $ctvar ct $hdvar hd $mhvar mh set mh [list] if {[catch {set current_branch [exec git symbolic-ref HEAD]}]} { set current_branch {} } else { regsub ^refs/((heads|tags|remotes)/)? \ $current_branch \ {} \ current_branch } if {[catch {set hd [exec git rev-parse --verify HEAD]}]} { set hd {} set ct initial return } set merge_head [gitdir MERGE_HEAD] if {[file exists $merge_head]} { set ct merge set fd_mh [open $merge_head r] while {[gets $fd_mh line] >= 0} { lappend mh $line } close $fd_mh return } set ct normal } proc PARENT {} { global PARENT empty_tree set p [lindex $PARENT 0] if {$p ne {}} { return $p } if {$empty_tree eq {}} { set empty_tree [exec git mktree << {}] } return $empty_tree } 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 global repo_config if {$rescan_active > 0 || ![lock_index read]} return repository_state newType newHEAD newMERGE_HEAD if {[string match amend* $commit_type] && $newType eq {normal} && $newHEAD eq $HEAD} { } else { set HEAD $newHEAD set PARENT $newHEAD set MERGE_HEAD $newMERGE_HEAD set commit_type $newType } array unset file_states if {![$ui_comm edit modified] || [string trim [$ui_comm get 0.0 end]] eq {}} { if {[load_message GITGUI_MSG]} { } elseif {[load_message MERGE_MSG]} { } elseif {[load_message SQUASH_MSG]} { } $ui_comm edit reset $ui_comm edit modified false } if {[is_enabled multicommit]} { load_all_heads populate_branch_menu } if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} { rescan_stage2 {} $after } else { set rescan_active 1 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 rescan_stage2 $fd_rf $after] } } proc rescan_stage2 {fd after} { global ui_status_value global rescan_active buf_rdi buf_rdf buf_rlo if {$fd ne {}} { read $fd if {![eof $fd]} return close $fd } set ls_others [list | git ls-files --others -z \ --exclude-per-directory=.gitignore] set info_exclude [gitdir info exclude] if {[file readable $info_exclude]} { lappend ls_others "--exclude-from=$info_exclude" } set buf_rdi {} set buf_rdf {} set buf_rlo {} set rescan_active 3 set ui_status_value {Scanning for modified files ...} set fd_di [open "| git diff-index --cached -z [PARENT]" r] set fd_df [open "| git diff-files -z" r] set fd_lo [open $ls_others r] 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] } proc load_message {file} { global ui_comm set f [gitdir $file] if {[file isfile $f]} { if {[catch {set fd [open $f r]}]} { return 0 } 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 } return 0 } proc read_diff_index {fd after} { global buf_rdi append buf_rdi [read $fd] set c 0 set n [string length $buf_rdi] while {$c < $n} { set z1 [string first "\0" $buf_rdi $c] if {$z1 == -1} break incr z1 set z2 [string first "\0" $buf_rdi $z1] if {$z2 == -1} break 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 \ [encoding convertfrom $p] \ [lindex $i 4]? \ [list [lindex $i 0] [lindex $i 2]] \ [list] set c $z2 incr c } if {$c < $n} { set buf_rdi [string range $buf_rdi $c end] } else { set buf_rdi {} } rescan_done $fd buf_rdi $after } proc read_diff_files {fd after} { global buf_rdf append buf_rdf [read $fd] set c 0 set n [string length $buf_rdf] while {$c < $n} { set z1 [string first "\0" $buf_rdf $c] if {$z1 == -1} break incr z1 set z2 [string first "\0" $buf_rdf $z1] if {$z2 == -1} break 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 \ [encoding convertfrom $p] \ ?[lindex $i 4] \ [list] \ [list [lindex $i 0] [lindex $i 2]] set c $z2 incr c } if {$c < $n} { set buf_rdf [string range $buf_rdf $c end] } else { set buf_rdf {} } rescan_done $fd buf_rdf $after } proc read_ls_others {fd after} { global buf_rlo append buf_rlo [read $fd] set pck [split $buf_rlo "\0"] set buf_rlo [lindex $pck end] foreach p [lrange $pck 0 end-1] { merge_state [encoding convertfrom $p] ?O } rescan_done $fd buf_rlo $after } proc rescan_done {fd buf after} { global rescan_active global file_states repo_config upvar $buf to_clear if {![eof $fd]} return set to_clear {} close $fd if {[incr rescan_active -1] > 0} return prune_selection unlock_index display_all_files reshow_diff uplevel #0 $after } proc prune_selection {} { global file_states selected_paths foreach path [array names selected_paths] { if {[catch {set still_here $file_states($path)}]} { unset selected_paths($path) } } } ###################################################################### ## ## diff proc clear_diff {} { 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 } proc reshow_diff {} { global ui_status_value file_states file_lists global current_diff_path current_diff_side set p $current_diff_path if {$p eq {} || $current_diff_side eq {} || [catch {set s $file_states($p)}] || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} { clear_diff } else { show_diff $p $current_diff_side } } proc handle_empty_diff {} { global current_diff_path file_states file_lists set path $current_diff_path set s $file_states($path) if {[lindex $s 0] ne {_M}} return info_popup "No differences detected. [short_path $path] has no changes. The modification date of this file was updated 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 current_diff_header if {$diff_active || ![lock_index read]} return clear_diff if {$lno == {}} { set lno [lsearch -sorted -exact $file_lists($w) $path] if {$lno >= 0} { incr lno } } if {$lno >= 1} { $w tag add in_diff $lno.0 [expr {$lno + 1}].0 } set s $file_states($path) set m [lindex $s 0] set is_3way_diff 0 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 $max_sz] close $fd set sz [file size $path] } err ]} { set diff_active 0 unlock_index set ui_status_value "Unable to display [escape_path $path]" error_popup "Error loading file:\n\n$err" return } $ui_diff conf -state normal 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 set ui_status_value {Ready.} return } set cmd [list | git] if {$w eq $ui_index} { lappend cmd diff-index lappend cmd --cached } elseif {$w eq $ui_workdir} { if {[string index $m 0] eq {U}} { lappend cmd diff } else { lappend cmd diff-files } } lappend cmd -p lappend cmd --no-color if {$repo_config(gui.diffcontext) > 0} { lappend cmd "-U$repo_config(gui.diffcontext)" } if {$w eq $ui_index} { lappend cmd [PARENT] } lappend cmd -- lappend cmd $path if {[catch {set fd [open $cmd r]} err]} { set diff_active 0 unlock_index set ui_status_value "Unable to display [escape_path $path]" error_popup "Error loading diff:\n\n$err" return } 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 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] || [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" } # -- Automatically detect if this is a 3 way diff. # if {[string match {@@@ *} $line]} {set is_3way_diff 1} 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] switch -- $op { { } {set tags {}} {@@} {set tags d_@} { +} {set tags d_s+} { -} {set tags d_s-} {+ } {set tags d_+s} {- } {set tags d_-s} {--} {set tags d_--} {++} { if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} { set line [string replace $line 0 1 { }] set tags d$op } else { set tags d_++ } } default { puts "error: Unhandled 3 way diff marker: {$op}" set tags {} } } } else { set op [string index $line 0] switch -- $op { { } {set tags {}} {@} {set tags d_@} {-} {set tags d_-} {+} { if {[regexp {^\+([<>]{7} |={7})} $line _g op]} { set line [string replace $line 0 0 { }] set tags d$op } else { set tags d_+ } } default { puts "error: Unhandled 2 way diff marker: {$op}" set tags {} } } } $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 if {[eof $fd]} { close $fd set diff_active 0 unlock_index set ui_status_value {Ready.} 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. You are about to create the initial commit. There is no commit before this to amend. } return } repository_state curType curHEAD curMERGE_HEAD if {$curType eq {merge}} { error_popup {Cannot amend while merging. You are currently in the middle of a merge that has not been fully completed. You cannot amend the prior commit unless you first abort the current merge activity. } return } set msg {} 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]} { error_popup "Error loading commit data for amend:\n\n$err" return } set HEAD $curHEAD set PARENT $parents set MERGE_HEAD [list] switch -- [llength $parents] { 0 {set commit_type amend-initial} 1 {set commit_type amend} default {set commit_type amend-merge} } $ui_comm delete 0.0 end $ui_comm insert end $msg $ui_comm edit reset $ui_comm edit modified false rescan {set ui_status_value {Ready.}} } proc create_new_commit {} { global commit_type ui_comm set commit_type normal $ui_comm delete 0.0 end $ui_comm edit reset $ui_comm edit modified false rescan {set ui_status_value {Ready.}} } set GIT_COMMITTER_IDENT {} proc committer_ident {} { global GIT_COMMITTER_IDENT if {$GIT_COMMITTER_IDENT eq {}} { if {[catch {set me [exec git var GIT_COMMITTER_IDENT]} err]} { error_popup "Unable to obtain your identity:\n\n$err" return {} } if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \ $me me GIT_COMMITTER_IDENT]} { error_popup "Invalid GIT_COMMITTER_IDENT:\n\n$me" return {} } } return $GIT_COMMITTER_IDENT } proc commit_tree {} { global HEAD commit_type file_states ui_comm repo_config global ui_status_value pch_error if {[committer_ident] eq {}} return if {![lock_index update]} return # -- Our in memory state should match the repository. # repository_state curType curHEAD curMERGE_HEAD if {[string match amend* $commit_type] && $curType eq {normal} && $curHEAD eq $HEAD} { } elseif {$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 another commit can be created. The rescan will be automatically started now. } unlock_index rescan {set ui_status_value {Ready.}} return } # -- At least one file should differ in the index. # set files_ready 0 foreach path [array names file_states] { switch -glob -- [lindex $file_states($path) 0] { _? {continue} A? - D? - M? {set files_ready 1} U? { error_popup "Unmerged files cannot be committed. File [short_path $path] has merge conflicts. You must resolve them and add the file before committing. " unlock_index return } default { error_popup "Unknown file state [lindex $s 0] detected. File [short_path $path] cannot be committed by this program. " } } } if {!$files_ready} { info_popup {No changes to commit. You must add at least 1 file before you can commit. } unlock_index return } # -- 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. A good commit message has the following format: - First line: Describe in one sentance what you did. - Second line: Blank - Remaining lines: Describe why this change is good. } unlock_index return } # -- Run the pre-commit hook. # set pchook [gitdir hooks pre-commit] # On Cygwin [file executable] might lie so we need to ask # the shell if the hook is executable. Yes that's annoying. # if {[is_Cygwin] && [file isfile $pchook]} { set pchook [list sh -c [concat \ "if test -x \"$pchook\";" \ "then exec \"$pchook\" 2>&1;" \ "fi"]] } elseif {[file executable $pchook]} { set pchook [list $pchook |& cat] } else { commit_writetree $curHEAD $msg return } set ui_status_value {Calling pre-commit hook...} set pch_error {} set fd_ph [open "| $pchook" r] fconfigure $fd_ph -blocking 0 -translation binary fileevent $fd_ph readable \ [list commit_prehook_wait $fd_ph $curHEAD $msg] } proc commit_prehook_wait {fd_ph curHEAD msg} { global pch_error ui_status_value append pch_error [read $fd_ph] fconfigure $fd_ph -blocking 1 if {[eof $fd_ph]} { if {[catch {close $fd_ph}]} { set ui_status_value {Commit declined by pre-commit hook.} hook_failed_popup pre-commit $pch_error unlock_index } else { commit_writetree $curHEAD $msg } set pch_error {} return } fconfigure $fd_ph -blocking 0 } proc commit_writetree {curHEAD msg} { global ui_status_value set ui_status_value {Committing changes...} set fd_wt [open "| git write-tree" r] fileevent $fd_wt readable \ [list commit_committree $fd_wt $curHEAD $msg] } proc commit_committree {fd_wt curHEAD msg} { global HEAD PARENT MERGE_HEAD commit_type 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]} { error_popup "write-tree failed:\n\n$err" set ui_status_value {Commit failed.} unlock_index 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] set parents [concat $PARENT $MERGE_HEAD] if {[llength $parents] > 0} { foreach p $parents { lappend cmd -p $p } } else { # git commit-tree writes to stderr during initial commit. lappend cmd 2>/dev/null } 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.} unlock_index return } # -- Update the HEAD ref. # set reflogm commit if {$commit_type ne {normal}} { append reflogm " ($commit_type)" } set i [string first "\n" $msg] if {$i >= 0} { append reflogm {: } [string range $msg 0 [expr {$i - 1}]] } else { append reflogm {: } $msg } set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD] if {[catch {eval exec $cmd} err]} { error_popup "update-ref failed:\n\n$err" set ui_status_value {Commit failed.} unlock_index 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]} catch {file delete [gitdir GITGUI_MSG]} # -- Let rerere do its thing. # if {[file isdirectory [gitdir rr-cache]]} { catch {exec git rerere} } # -- Run the post-commit hook. # set pchook [gitdir hooks post-commit] if {[is_Cygwin] && [file isfile $pchook]} { set pchook [list sh -c [concat \ "if test -x \"$pchook\";" \ "then exec \"$pchook\";" \ "fi"]] } elseif {![file executable $pchook]} { set pchook {} } if {$pchook ne {}} { catch {exec $pchook &} } $ui_comm delete 0.0 end $ui_comm edit reset $ui_comm edit modified false if {![is_enabled multicommit]} do_quit # -- Update in memory status # set selected_commit_type new set commit_type normal set HEAD $cmt_id set PARENT $cmt_id set MERGE_HEAD [list] foreach path [array names file_states] { set s $file_states($path) set m [lindex $s 0] switch -glob -- $m { _O - _M - _D {continue} __ - A_ - M_ - D_ { unset file_states($path) catch {unset selected_paths($path)} } DO { set file_states($path) [list _O [lindex $s 1] {} {}] } AM - AD - MM - MD { set file_states($path) [list \ _[string index $m 1] \ [lindex $s 1] \ [lindex $s 3] \ {}] } } } display_all_files unlock_index reshow_diff set ui_status_value \ "Changes committed as [string range $cmt_id 0 7]." } ###################################################################### ## ## fetch push proc fetch_from {remote} { set w [new_console \ "fetch $remote" \ "Fetching new changes from $remote"] set cmd [list git fetch] lappend cmd $remote console_exec $w $cmd console_done } proc push_to {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_done } ###################################################################### ## ## ui helpers proc mapicon {w state path} { global all_icons if {[catch {set r $all_icons($state$w)}]} { puts "error: no icon for $w state={$state} $path" return file_plain } return $r } proc mapdesc {state path} { global all_descs if {[catch {set r $all_descs($state)}]} { puts "error: no desc for state={$state} $path" return $state } return $r } proc escape_path {path} { regsub -all {\\} $path "\\\\" path regsub -all "\n" $path "\\n" path return $path } proc short_path {path} { return [escape_path [lindex [file split $path] end]] } set next_icon_id 0 set null_sha1 [string repeat 0 40] proc merge_state {path new_state {head_info {}} {index_info {}}} { global file_states next_icon_id null_sha1 set s0 [string index $new_state 0] set s1 [string index $new_state 1] if {[catch {set info $file_states($path)}]} { set state __ set icon n[incr next_icon_id] } else { set state [lindex $info 0] set icon [lindex $info 1] if {$head_info eq {}} {set head_info [lindex $info 2]} if {$index_info eq {}} {set index_info [lindex $info 3]} } if {$s0 eq {?}} {set s0 [string index $state 0]} \ elseif {$s0 eq {_}} {set s0 _} if {$s1 eq {?}} {set s1 [string index $state 1]} \ elseif {$s1 eq {_}} {set s1 _} if {$s0 eq {A} && $s1 eq {_} && $head_info eq {}} { set head_info [list 0 $null_sha1] } elseif {$s0 ne {_} && [string index $state 0] eq {_} && $head_info eq {}} { set head_info $index_info } set file_states($path) [list $s0$s1 $icon \ $head_info $index_info \ ] return $state } proc display_file_helper {w path icon_name old_m new_m} { global file_lists if {$new_m eq {_}} { set lno [lsearch -sorted -exact $file_lists($w) $path] if {$lno >= 0} { set file_lists($w) [lreplace $file_lists($w) $lno $lno] incr lno $w conf -state normal $w delete $lno.0 [expr {$lno + 1}].0 $w conf -state disabled } } 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 -exact $file_lists($w) $path] incr lno $w conf -state normal $w image create $lno.0 \ -align center -padx 5 -pady 1 \ -name $icon_name \ -image [mapicon $w $new_m $path] $w insert $lno.1 "[escape_path $path]\n" $w conf -state disabled } elseif {$old_m ne $new_m} { $w conf -state normal $w image conf $icon_name -image [mapicon $w $new_m $path] $w conf -state disabled } } proc display_file {path state} { global file_states selected_paths global ui_index ui_workdir set old_m [merge_state $path $state] set s $file_states($path) set new_m [lindex $s 0] set icon_name [lindex $s 1] set o [string index $old_m 0] set n [string index $new_m 0] if {$o eq {U}} { set o _ } if {$n eq {U}} { set n _ } display_file_helper $ui_index $path $icon_name $o $n if {[string index $old_m 0] eq {U}} { set o U } else { set o [string index $old_m 1] } if {[string index $new_m 0] eq {U}} { set n U } else { set n [string index $new_m 1] } display_file_helper $ui_workdir $path $icon_name $o $n if {$new_m eq {__}} { unset file_states($path) catch {unset selected_paths($path)} } } proc display_all_files_helper {w path icon_name m} { global file_lists lappend file_lists($w) $path set lno [expr {[lindex [split [$w index end] .] 0] - 1}] $w image create end \ -align center -padx 5 -pady 1 \ -name $icon_name \ -image [mapicon $w $m $path] $w insert end "[escape_path $path]\n" } proc display_all_files {} { global ui_index ui_workdir global file_states file_lists global last_clicked $ui_index conf -state normal $ui_workdir conf -state normal $ui_index delete 0.0 end $ui_workdir delete 0.0 end set last_clicked {} set file_lists($ui_index) [list] set file_lists($ui_workdir) [list] foreach path [lsort [array names file_states]] { set s $file_states($path) set m [lindex $s 0] set icon_name [lindex $s 1] set s [string index $m 0] if {$s ne {U} && $s ne {_}} { display_all_files_helper $ui_index $path \ $icon_name $s } if {[string index $m 0] eq {U}} { set s U } else { set s [string index $m 1] } if {$s ne {_}} { display_all_files_helper $ui_workdir $path \ $icon_name $s } } $ui_index conf -state disabled $ui_workdir conf -state disabled } proc update_indexinfo {msg pathList after} { global update_index_cp ui_status_value if {![lock_index update]} return set update_index_cp 0 set pathList [lsort $pathList] set totalCnt [llength $pathList] set batch [expr {int($totalCnt * .01) + 1}] if {$batch > 25} {set batch 25} set ui_status_value [format \ "$msg... %i/%i files (%.2f%%)" \ $update_index_cp \ $totalCnt \ 0.0] set fd [open "| git update-index -z --index-info" w] fconfigure $fd \ -blocking 0 \ -buffering full \ -buffersize 512 \ -encoding binary \ -translation binary fileevent $fd writable [list \ write_update_indexinfo \ $fd \ $pathList \ $totalCnt \ $batch \ $msg \ $after \ ] } proc write_update_indexinfo {fd pathList totalCnt batch msg after} { global update_index_cp ui_status_value global file_states current_diff_path if {$update_index_cp >= $totalCnt} { close $fd unlock_index uplevel #0 $after return } for {set i $batch} \ {$update_index_cp < $totalCnt && $i > 0} \ {incr i -1} { set path [lindex $pathList $update_index_cp] incr update_index_cp set s $file_states($path) switch -glob -- [lindex $s 0] { A? {set new _O} M? {set new _M} D_ {set new _D} D? {set new _?} ?? {continue} } set info [lindex $s 2] if {$info eq {}} continue puts -nonewline $fd "$info\t[encoding convertto $path]\0" display_file $path $new } set ui_status_value [format \ "$msg... %i/%i files (%.2f%%)" \ $update_index_cp \ $totalCnt \ [expr {100.0 * $update_index_cp / $totalCnt}]] } proc update_index {msg pathList after} { global update_index_cp ui_status_value if {![lock_index update]} return set update_index_cp 0 set pathList [lsort $pathList] set totalCnt [llength $pathList] set batch [expr {int($totalCnt * .01) + 1}] if {$batch > 25} {set batch 25} set ui_status_value [format \ "$msg... %i/%i files (%.2f%%)" \ $update_index_cp \ $totalCnt \ 0.0] set fd [open "| git update-index --add --remove -z --stdin" w] fconfigure $fd \ -blocking 0 \ -buffering full \ -buffersize 512 \ -encoding binary \ -translation binary fileevent $fd writable [list \ write_update_index \ $fd \ $pathList \ $totalCnt \ $batch \ $msg \ $after \ ] } proc write_update_index {fd pathList totalCnt batch msg after} { global update_index_cp ui_status_value global file_states current_diff_path if {$update_index_cp >= $totalCnt} { close $fd unlock_index uplevel #0 $after return } for {set i $batch} \ {$update_index_cp < $totalCnt && $i > 0} \ {incr i -1} { set path [lindex $pathList $update_index_cp] incr update_index_cp switch -glob -- [lindex $file_states($path) 0] { AD {set new __} ?D {set new D_} _O - AM {set new A_} U? { if {[file exists $path]} { set new M_ } else { set new D_ } } ?M {set new M_} ?? {continue} } puts -nonewline $fd "[encoding convertto $path]\0" display_file $path $new } set ui_status_value [format \ "$msg... %i/%i files (%.2f%%)" \ $update_index_cp \ $totalCnt \ [expr {100.0 * $update_index_cp / $totalCnt}]] } proc checkout_index {msg pathList after} { global update_index_cp ui_status_value if {![lock_index update]} return set update_index_cp 0 set pathList [lsort $pathList] set totalCnt [llength $pathList] set batch [expr {int($totalCnt * .01) + 1}] if {$batch > 25} {set batch 25} set ui_status_value [format \ "$msg... %i/%i files (%.2f%%)" \ $update_index_cp \ $totalCnt \ 0.0] set cmd [list git checkout-index] lappend cmd --index lappend cmd --quiet lappend cmd --force lappend cmd -z lappend cmd --stdin set fd [open "| $cmd " w] fconfigure $fd \ -blocking 0 \ -buffering full \ -buffersize 512 \ -encoding binary \ -translation binary fileevent $fd writable [list \ write_checkout_index \ $fd \ $pathList \ $totalCnt \ $batch \ $msg \ $after \ ] } proc write_checkout_index {fd pathList totalCnt batch msg after} { global update_index_cp ui_status_value global file_states current_diff_path if {$update_index_cp >= $totalCnt} { close $fd unlock_index uplevel #0 $after return } for {set i $batch} \ {$update_index_cp < $totalCnt && $i > 0} \ {incr i -1} { set path [lindex $pathList $update_index_cp] incr update_index_cp switch -glob -- [lindex $file_states($path) 0] { U? {continue} ?M - ?D { puts -nonewline $fd "[encoding convertto $path]\0" display_file $path ?_ } } } set ui_status_value [format \ "$msg... %i/%i files (%.2f%%)" \ $update_index_cp \ $totalCnt \ [expr {100.0 * $update_index_cp / $totalCnt}]] } ###################################################################### ## ## 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 set all_heads [list] set fd [open "| git for-each-ref --format=%(refname) refs/heads" r] while {[gets $fd line] > 0} { if {[is_tracking_branch $line]} continue if {![regsub ^refs/heads/ $line {} name]} continue lappend all_heads $name } close $fd set all_heads [lsort $all_heads] } proc populate_branch_menu {} { global all_heads disable_on_lock set m .mbar.branch set last [$m index last] for {set i 0} {$i <= $last} {incr i} { if {[$m type $i] eq {separator}} { $m delete $i last set new_dol [list] foreach a $disable_on_lock { if {[lindex $a 0] ne $m || [lindex $a 2] < $i} { lappend new_dol $a } } set disable_on_lock $new_dol break } } if {$all_heads ne {}} { $m add separator } foreach b $all_heads { $m add radiobutton \ -label $b \ -command [list switch_branch $b] \ -variable current_branch \ -value $b \ -font font_ui lappend disable_on_lock \ [list $m entryconf [$m index last] -state] } } 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 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 $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 \ -type ok \ -title [wm title $w] \ -parent $w \ -message "Branch '$newbranch' already exists." focus $w.desc.name_t return } if {[catch {exec git check-ref-format "heads/$newbranch"}]} { tk_messageBox \ -icon error \ -type ok \ -title [wm title $w] \ -parent $w \ -message "We do not like '$newbranch' as a branch name." focus $w.desc.name_t return } set rev {} switch -- $create_branch_revtype { head {set rev $create_branch_head} tracking {set rev $create_branch_trackinghead} expression {set rev $create_branch_revexp} } if {[catch {set cmt [exec git rev-parse --verify "${rev}^0"]}]} { tk_messageBox \ -icon error \ -type ok \ -title [wm title $w] \ -parent $w \ -message "Invalid starting revision: $rev" return } set cmd [list git update-ref] lappend cmd -m lappend cmd "branch: Created from $rev" lappend cmd "refs/heads/$newbranch" lappend cmd $cmt lappend cmd $null_sha1 if {[catch {eval exec $cmd} err]} { tk_messageBox \ -icon error \ -type ok \ -title [wm title $w] \ -parent $w \ -message "Failed to create '$newbranch'.\n\n$err" return } lappend all_heads $newbranch set all_heads [lsort $all_heads] populate_branch_menu destroy $w if {$create_branch_checkout} { switch_branch $newbranch } } 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 repo_config global create_branch_checkout create_branch_revtype global create_branch_head create_branch_trackinghead global create_branch_name create_branch_revexp set w .branch_editor toplevel $w wm geometry $w "+[winfo rootx .]+[winfo rooty .]" label $w.header -text {Create New Branch} \ -font font_uibold pack $w.header -side top -fill x frame $w.buttons button $w.buttons.create -text Create \ -font font_ui \ -default active \ -command [list do_create_branch_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.desc \ -text {Branch Description} \ -font font_ui label $w.desc.name_l -text {Name:} -font font_ui entry $w.desc.name_t \ -borderwidth 1 \ -relief sunken \ -width 40 \ -textvariable create_branch_name \ -font font_ui \ -validate key \ -validatecommand { if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0} return 1 } 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 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 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 entry $w.from.exp_t \ -borderwidth 1 \ -relief sunken \ -width 50 \ -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 \ -text {Post Creation Actions} \ -font font_ui checkbutton $w.postActions.checkout \ -text {Checkout after creation} \ -variable create_branch_checkout \ -font font_ui pack $w.postActions.checkout -anchor nw pack $w.postActions -anchor nw -fill x -pady 5 -padx 5 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" tkwait window $w } proc do_delete_branch_action {w} { global all_heads 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 {$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 } } lappend to_delete [list $b $o] } if {$not_merged ne {}} { set msg "The following branches are not completely merged into $check_rev: - [join $not_merged "\n - "]" tk_messageBox \ -icon info \ -type ok \ -title [wm title $w] \ -parent $w \ -message $msg } if {$to_delete eq {}} return if {$delete_branch_checktype eq {always}} { set msg {Recovering deleted branches is difficult. Delete the selected branches?} if {[tk_messageBox \ -icon warning \ -type yesno \ -title [wm title $w] \ -parent $w \ -message $msg] ne yes} { return } } set failed {} foreach i $to_delete { set b [lindex $i 0] set o [lindex $i 1] if {[catch {exec git update-ref -d "refs/heads/$b" $o} err]} { append failed " - $b: $err\n" } else { set x [lsearch -sorted -exact $all_heads $b] if {$x >= 0} { set all_heads [lreplace $all_heads $x $x] } } } if {$failed ne {}} { tk_messageBox \ -icon error \ -type ok \ -title [wm title $w] \ -parent $w \ -message "Failed to delete branches:\n$failed" } set all_heads [lsort $all_heads] populate_branch_menu destroy $w } proc do_delete_branch {} { global all_heads tracking_branches current_branch global delete_branch_checktype delete_branch_head delete_branch_trackinghead set w .branch_editor toplevel $w wm geometry $w "+[winfo rootx .]+[winfo rooty .]" label $w.header -text {Delete Local Branch} \ -font font_uibold pack $w.header -side top -fill x frame $w.buttons button $w.buttons.create -text Delete \ -font font_ui \ -command [list do_delete_branch_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.list \ -text {Local Branches} \ -font font_ui listbox $w.list.l \ -height 10 \ -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 } } 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 {Delete Only If} \ -font font_ui radiobutton $w.validate.head_r \ -text {Merged Into Local Branch:} \ -value head \ -variable delete_branch_checktype \ -font font_ui 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 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 {new_branch} { global HEAD commit_type current_branch repo_config if {![lock_index switch]} return # -- Our in memory state should match the repository. # repository_state curType curHEAD curMERGE_HEAD if {[string match amend* $commit_type] && $curType eq {normal} && $curHEAD eq $HEAD} { } elseif {$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 the current branch can be changed. The rescan will be automatically started now. } unlock_index rescan {set ui_status_value {Ready.}} return } # -- Don't do a pointless switch. # if {$current_branch eq $new_branch} { unlock_index return } if {$repo_config(gui.trustmtime) eq {true}} { switch_branch_stage2 {} $new_branch } else { set ui_status_value {Refreshing file status...} set cmd [list git update-index] lappend cmd -q lappend cmd --unmerged lappend cmd --ignore-missing lappend cmd --refresh set fd_rf [open "| $cmd" r] fconfigure $fd_rf -blocking 0 -translation binary fileevent $fd_rf readable \ [list switch_branch_stage2 $fd_rf $new_branch] } } proc switch_branch_stage2 {fd_rf new_branch} { global ui_status_value HEAD if {$fd_rf ne {}} { read $fd_rf if {![eof $fd_rf]} return close $fd_rf } set ui_status_value "Updating working directory to '$new_branch'..." set cmd [list git read-tree] lappend cmd -m lappend cmd -u lappend cmd --exclude-per-directory=.gitignore lappend cmd $HEAD lappend cmd $new_branch set fd_rt [open "| $cmd" r] fconfigure $fd_rt -blocking 0 -translation binary fileevent $fd_rt readable \ [list switch_branch_readtree_wait $fd_rt $new_branch] } proc switch_branch_readtree_wait {fd_rt new_branch} { global selected_commit_type commit_type HEAD MERGE_HEAD PARENT global current_branch global ui_comm ui_status_value # -- We never get interesting output on stdout; only stderr. # read $fd_rt fconfigure $fd_rt -blocking 1 if {![eof $fd_rt]} { fconfigure $fd_rt -blocking 0 return } # -- The working directory wasn't in sync with the index and # we'd have to overwrite something to make the switch. A # merge is required. # if {[catch {close $fd_rt} err]} { regsub {^fatal: } $err {} err warn_popup "File level merge required. $err Staying on branch '$current_branch'." set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)." unlock_index return } # -- Update the symbolic ref. Core git doesn't even check for failure # here, it Just Works(tm). If it doesn't we are in some really ugly # state that is difficult to recover from within git-gui. # if {[catch {exec git symbolic-ref HEAD "refs/heads/$new_branch"} err]} { error_popup "Failed to set current branch. This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file. This should not have occurred. [appname] will now close and give up. $err" do_quit return } # -- Update our repository state. If we were previously in amend mode # we need to toss the current buffer and do a full rescan to update # our file lists. If we weren't in amend mode our file lists are # accurate and we can avoid the rescan. # unlock_index set selected_commit_type new if {[string match amend* $commit_type]} { $ui_comm delete 0.0 end $ui_comm edit reset $ui_comm edit modified false rescan {set ui_status_value "Checked out branch '$current_branch'."} } else { repository_state commit_type HEAD MERGE_HEAD set PARENT $HEAD set ui_status_value "Checked out branch '$current_branch'." } } ###################################################################### ## ## remote management proc load_all_remotes {} { global repo_config global all_remotes tracking_branches set all_remotes [list] array unset tracking_branches set rm_dir [gitdir remotes] if {[file isdirectory $rm_dir]} { set all_remotes [glob \ -types f \ -tails \ -nocomplain \ -directory $rm_dir *] foreach name $all_remotes { catch { set fd [open [file join $rm_dir $name] r] while {[gets $fd line] >= 0} { if {![regexp {^Pull:[ ]*([^:]+):(.+)$} \ $line line src dst]} continue if {![regexp ^refs/ $dst]} { set dst "refs/heads/$dst" } set tracking_branches($dst) [list $name $src] } close $fd } } } foreach line [array names repo_config remote.*.url] { if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue lappend all_remotes $name if {[catch {set fl $repo_config(remote.$name.fetch)}]} { set fl {} } foreach line $fl { if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue if {![regexp ^refs/ $dst]} { set dst "refs/heads/$dst" } set tracking_branches($dst) [list $name $src] } } set all_remotes [lsort -unique $all_remotes] } proc populate_fetch_menu {} { global all_remotes repo_config set m .mbar.fetch foreach r $all_remotes { set enable 0 if {![catch {set a $repo_config(remote.$r.url)}]} { if {![catch {set a $repo_config(remote.$r.fetch)}]} { set enable 1 } } else { catch { set fd [open [gitdir remotes $r] r] while {[gets $fd n] >= 0} { if {[regexp {^Pull:[ \t]*([^:]+):} $n]} { set enable 1 break } } close $fd } } if {$enable} { $m add command \ -label "Fetch from $r..." \ -command [list fetch_from $r] \ -font font_ui } } } proc populate_push_menu {} { global all_remotes repo_config set m .mbar.push set fast_count 0 foreach r $all_remotes { set enable 0 if {![catch {set a $repo_config(remote.$r.url)}]} { if {![catch {set a $repo_config(remote.$r.push)}]} { set enable 1 } } else { catch { set fd [open [gitdir remotes $r] r] while {[gets $fd n] >= 0} { if {[regexp {^Push:[ \t]*([^:]+):} $n]} { set enable 1 break } } close $fd } } if {$enable} { if {!$fast_count} { $m add separator } $m add command \ -label "Push to $r..." \ -command [list push_to $r] \ -font font_ui incr fast_count } } } proc start_push_anywhere_action {w} { global push_urltype push_remote push_url push_thin push_tags set r_url {} switch -- $push_urltype { remote {set r_url $push_remote} url {set r_url $push_url} } if {$r_url eq {}} return set cmd [list git push] lappend cmd -v if {$push_thin} { lappend cmd --thin } if {$push_tags} { lappend cmd --tags } lappend cmd $r_url set cnt 0 foreach i [$w.source.l curselection] { set b [$w.source.l get $i] lappend cmd "refs/heads/$b:refs/heads/$b" incr cnt } if {$cnt == 0} { return } elseif {$cnt == 1} { set unit branch } else { set unit branches } set cons [new_console "push $r_url" "Pushing $cnt $unit to $r_url"] console_exec $cons $cmd console_done destroy $w } trace add variable push_remote write \ [list radio_selector push_urltype remote] proc do_push_anywhere {} { global all_heads all_remotes current_branch global push_urltype push_remote push_url push_thin push_tags set w .push_setup toplevel $w wm geometry $w "+[winfo rootx .]+[winfo rooty .]" label $w.header -text {Push Branches} -font font_uibold pack $w.header -side top -fill x frame $w.buttons button $w.buttons.create -text Push \ -font font_ui \ -command [list start_push_anywhere_action $w] pack $w.buttons.create -side right button $w.buttons.cancel -text {Cancel} \ -font font_ui \ -command [list destroy $w] pack $w.buttons.cancel -side right -padx 5 pack $w.buttons -side bottom -fill x -pady 10 -padx 10 labelframe $w.source \ -text {Source Branches} \ -font font_ui listbox $w.source.l \ -height 10 \ -width 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 $