Code

Merge branch 'maint'
[git.git] / git-gui.sh
1 #!/bin/sh
2 # Tcl ignores the next line -*- tcl -*- \
3  if test "z$*" = zversion \
4  || test "z$*" = z--version; \
5  then \
6         echo 'git-gui version @@GITGUI_VERSION@@'; \
7         exit; \
8  fi; \
9  argv0=$0; \
10  exec wish "$argv0" -- "$@"
12 set appvers {@@GITGUI_VERSION@@}
13 set copyright {
14 Copyright © 2006, 2007 Shawn Pearce, et. al.
16 This program is free software; you can redistribute it and/or modify
17 it under the terms of the GNU General Public License as published by
18 the Free Software Foundation; either version 2 of the License, or
19 (at your option) any later version.
21 This program is distributed in the hope that it will be useful,
22 but WITHOUT ANY WARRANTY; without even the implied warranty of
23 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24 GNU General Public License for more details.
26 You should have received a copy of the GNU General Public License
27 along with this program; if not, write to the Free Software
28 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA}
30 ######################################################################
31 ##
32 ## Tcl/Tk sanity check
34 if {[catch {package require Tcl 8.4} err]
35  || [catch {package require Tk  8.4} err]
36 } {
37         catch {wm withdraw .}
38         tk_messageBox \
39                 -icon error \
40                 -type ok \
41                 -title [mc "git-gui: fatal error"] \
42                 -message $err
43         exit 1
44 }
46 catch {rename send {}} ; # What an evil concept...
48 ######################################################################
49 ##
50 ## locate our library
52 set oguilib {@@GITGUI_LIBDIR@@}
53 set oguirel {@@GITGUI_RELATIVE@@}
54 if {$oguirel eq {1}} {
55         set oguilib [file dirname [file dirname [file normalize $argv0]]]
56         set oguilib [file join $oguilib share git-gui lib]
57         set oguimsg [file join $oguilib msgs]
58 } elseif {[string match @@* $oguirel]} {
59         set oguilib [file join [file dirname [file normalize $argv0]] lib]
60         set oguimsg [file join [file dirname [file normalize $argv0]] po]
61 } else {
62         set oguimsg [file join $oguilib msgs]
63 }
64 unset oguirel
66 ######################################################################
67 ##
68 ## enable verbose loading?
70 if {![catch {set _verbose $env(GITGUI_VERBOSE)}]} {
71         unset _verbose
72         rename auto_load real__auto_load
73         proc auto_load {name args} {
74                 puts stderr "auto_load $name"
75                 return [uplevel 1 real__auto_load $name $args]
76         }
77         rename source real__source
78         proc source {name} {
79                 puts stderr "source    $name"
80                 uplevel 1 real__source $name
81         }
82 }
84 ######################################################################
85 ##
86 ## Internationalization (i18n) through msgcat and gettext. See
87 ## http://www.gnu.org/software/gettext/manual/html_node/Tcl.html
89 package require msgcat
91 proc mc {fmt args} {
92         set fmt [::msgcat::mc $fmt]
93         set cmk [string first @@ $fmt]
94         if {$cmk > 0} {
95                 set fmt [string range $fmt 0 [expr {$cmk - 1}]]
96         }
97         return [eval [list format $fmt] $args]
98 }
100 proc strcat {args} {
101         return [join $args {}]
104 ::msgcat::mcload $oguimsg
105 unset oguimsg
107 ######################################################################
108 ##
109 ## read only globals
111 set _appname {Git Gui}
112 set _gitdir {}
113 set _gitexec {}
114 set _reponame {}
115 set _iscygwin {}
116 set _search_path {}
118 proc appname {} {
119         global _appname
120         return $_appname
123 proc gitdir {args} {
124         global _gitdir
125         if {$args eq {}} {
126                 return $_gitdir
127         }
128         return [eval [list file join $_gitdir] $args]
131 proc gitexec {args} {
132         global _gitexec
133         if {$_gitexec eq {}} {
134                 if {[catch {set _gitexec [git --exec-path]} err]} {
135                         error "Git not installed?\n\n$err"
136                 }
137                 if {[is_Cygwin]} {
138                         set _gitexec [exec cygpath \
139                                 --windows \
140                                 --absolute \
141                                 $_gitexec]
142                 } else {
143                         set _gitexec [file normalize $_gitexec]
144                 }
145         }
146         if {$args eq {}} {
147                 return $_gitexec
148         }
149         return [eval [list file join $_gitexec] $args]
152 proc reponame {} {
153         return $::_reponame
156 proc is_MacOSX {} {
157         if {[tk windowingsystem] eq {aqua}} {
158                 return 1
159         }
160         return 0
163 proc is_Windows {} {
164         if {$::tcl_platform(platform) eq {windows}} {
165                 return 1
166         }
167         return 0
170 proc is_Cygwin {} {
171         global _iscygwin
172         if {$_iscygwin eq {}} {
173                 if {$::tcl_platform(platform) eq {windows}} {
174                         if {[catch {set p [exec cygpath --windir]} err]} {
175                                 set _iscygwin 0
176                         } else {
177                                 set _iscygwin 1
178                         }
179                 } else {
180                         set _iscygwin 0
181                 }
182         }
183         return $_iscygwin
186 proc is_enabled {option} {
187         global enabled_options
188         if {[catch {set on $enabled_options($option)}]} {return 0}
189         return $on
192 proc enable_option {option} {
193         global enabled_options
194         set enabled_options($option) 1
197 proc disable_option {option} {
198         global enabled_options
199         set enabled_options($option) 0
202 ######################################################################
203 ##
204 ## config
206 proc is_many_config {name} {
207         switch -glob -- $name {
208         remote.*.fetch -
209         remote.*.push
210                 {return 1}
211         *
212                 {return 0}
213         }
216 proc is_config_true {name} {
217         global repo_config
218         if {[catch {set v $repo_config($name)}]} {
219                 return 0
220         } elseif {$v eq {true} || $v eq {1} || $v eq {yes}} {
221                 return 1
222         } else {
223                 return 0
224         }
227 proc get_config {name} {
228         global repo_config
229         if {[catch {set v $repo_config($name)}]} {
230                 return {}
231         } else {
232                 return $v
233         }
236 proc load_config {include_global} {
237         global repo_config global_config default_config
239         array unset global_config
240         if {$include_global} {
241                 catch {
242                         set fd_rc [git_read config --global --list]
243                         while {[gets $fd_rc line] >= 0} {
244                                 if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
245                                         if {[is_many_config $name]} {
246                                                 lappend global_config($name) $value
247                                         } else {
248                                                 set global_config($name) $value
249                                         }
250                                 }
251                         }
252                         close $fd_rc
253                 }
254         }
256         array unset repo_config
257         catch {
258                 set fd_rc [git_read config --list]
259                 while {[gets $fd_rc line] >= 0} {
260                         if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
261                                 if {[is_many_config $name]} {
262                                         lappend repo_config($name) $value
263                                 } else {
264                                         set repo_config($name) $value
265                                 }
266                         }
267                 }
268                 close $fd_rc
269         }
271         foreach name [array names default_config] {
272                 if {[catch {set v $global_config($name)}]} {
273                         set global_config($name) $default_config($name)
274                 }
275                 if {[catch {set v $repo_config($name)}]} {
276                         set repo_config($name) $default_config($name)
277                 }
278         }
281 ######################################################################
282 ##
283 ## handy utils
285 proc _git_cmd {name} {
286         global _git_cmd_path
288         if {[catch {set v $_git_cmd_path($name)}]} {
289                 switch -- $name {
290                   version   -
291                 --version   -
292                 --exec-path { return [list $::_git $name] }
293                 }
295                 set p [gitexec git-$name$::_search_exe]
296                 if {[file exists $p]} {
297                         set v [list $p]
298                 } elseif {[is_Windows] && [file exists [gitexec git-$name]]} {
299                         # Try to determine what sort of magic will make
300                         # git-$name go and do its thing, because native
301                         # Tcl on Windows doesn't know it.
302                         #
303                         set p [gitexec git-$name]
304                         set f [open $p r]
305                         set s [gets $f]
306                         close $f
308                         switch -glob -- [lindex $s 0] {
309                         #!*sh     { set i sh     }
310                         #!*perl   { set i perl   }
311                         #!*python { set i python }
312                         default   { error "git-$name is not supported: $s" }
313                         }
315                         upvar #0 _$i interp
316                         if {![info exists interp]} {
317                                 set interp [_which $i]
318                         }
319                         if {$interp eq {}} {
320                                 error "git-$name requires $i (not in PATH)"
321                         }
322                         set v [concat [list $interp] [lrange $s 1 end] [list $p]]
323                 } else {
324                         # Assume it is builtin to git somehow and we
325                         # aren't actually able to see a file for it.
326                         #
327                         set v [list $::_git $name]
328                 }
329                 set _git_cmd_path($name) $v
330         }
331         return $v
334 proc _which {what} {
335         global env _search_exe _search_path
337         if {$_search_path eq {}} {
338                 if {[is_Cygwin] && [regexp {^(/|\.:)} $env(PATH)]} {
339                         set _search_path [split [exec cygpath \
340                                 --windows \
341                                 --path \
342                                 --absolute \
343                                 $env(PATH)] {;}]
344                         set _search_exe .exe
345                 } elseif {[is_Windows]} {
346                         set _search_path [split $env(PATH) {;}]
347                         set _search_exe .exe
348                 } else {
349                         set _search_path [split $env(PATH) :]
350                         set _search_exe {}
351                 }
352         }
354         foreach p $_search_path {
355                 set p [file join $p $what$_search_exe]
356                 if {[file exists $p]} {
357                         return [file normalize $p]
358                 }
359         }
360         return {}
363 proc _lappend_nice {cmd_var} {
364         global _nice
365         upvar $cmd_var cmd
367         if {![info exists _nice]} {
368                 set _nice [_which nice]
369         }
370         if {$_nice ne {}} {
371                 lappend cmd $_nice
372         }
375 proc git {args} {
376         set opt [list exec]
378         while {1} {
379                 switch -- [lindex $args 0] {
380                 --nice {
381                         _lappend_nice opt
382                 }
384                 default {
385                         break
386                 }
388                 }
390                 set args [lrange $args 1 end]
391         }
393         set cmdp [_git_cmd [lindex $args 0]]
394         set args [lrange $args 1 end]
396         return [eval $opt $cmdp $args]
399 proc _open_stdout_stderr {cmd} {
400         if {[catch {
401                         set fd [open $cmd r]
402                 } err]} {
403                 if {   [lindex $cmd end] eq {2>@1}
404                     && $err eq {can not find channel named "1"}
405                         } {
406                         # Older versions of Tcl 8.4 don't have this 2>@1 IO
407                         # redirect operator.  Fallback to |& cat for those.
408                         # The command was not actually started, so its safe
409                         # to try to start it a second time.
410                         #
411                         set fd [open [concat \
412                                 [lrange $cmd 0 end-1] \
413                                 [list |& cat] \
414                                 ] r]
415                 } else {
416                         error $err
417                 }
418         }
419         fconfigure $fd -eofchar {}
420         return $fd
423 proc git_read {args} {
424         set opt [list |]
426         while {1} {
427                 switch -- [lindex $args 0] {
428                 --nice {
429                         _lappend_nice opt
430                 }
432                 --stderr {
433                         lappend args 2>@1
434                 }
436                 default {
437                         break
438                 }
440                 }
442                 set args [lrange $args 1 end]
443         }
445         set cmdp [_git_cmd [lindex $args 0]]
446         set args [lrange $args 1 end]
448         return [_open_stdout_stderr [concat $opt $cmdp $args]]
451 proc git_write {args} {
452         set opt [list |]
454         while {1} {
455                 switch -- [lindex $args 0] {
456                 --nice {
457                         _lappend_nice opt
458                 }
460                 default {
461                         break
462                 }
464                 }
466                 set args [lrange $args 1 end]
467         }
469         set cmdp [_git_cmd [lindex $args 0]]
470         set args [lrange $args 1 end]
472         return [open [concat $opt $cmdp $args] w]
475 proc sq {value} {
476         regsub -all ' $value "'\\''" value
477         return "'$value'"
480 proc load_current_branch {} {
481         global current_branch is_detached
483         set fd [open [gitdir HEAD] r]
484         if {[gets $fd ref] < 1} {
485                 set ref {}
486         }
487         close $fd
489         set pfx {ref: refs/heads/}
490         set len [string length $pfx]
491         if {[string equal -length $len $pfx $ref]} {
492                 # We're on a branch.  It might not exist.  But
493                 # HEAD looks good enough to be a branch.
494                 #
495                 set current_branch [string range $ref $len end]
496                 set is_detached 0
497         } else {
498                 # Assume this is a detached head.
499                 #
500                 set current_branch HEAD
501                 set is_detached 1
502         }
505 auto_load tk_optionMenu
506 rename tk_optionMenu real__tkOptionMenu
507 proc tk_optionMenu {w varName args} {
508         set m [eval real__tkOptionMenu $w $varName $args]
509         $m configure -font font_ui
510         $w configure -font font_ui
511         return $m
514 proc rmsel_tag {text} {
515         $text tag conf sel \
516                 -background [$text cget -background] \
517                 -foreground [$text cget -foreground] \
518                 -borderwidth 0
519         $text tag conf in_sel -background lightgray
520         bind $text <Motion> break
521         return $text
524 set root_exists 0
525 bind . <Visibility> {
526         bind . <Visibility> {}
527         set root_exists 1
530 if {[is_Windows]} {
531         wm iconbitmap . -default $oguilib/git-gui.ico
534 ######################################################################
535 ##
536 ## config defaults
538 set cursor_ptr arrow
539 font create font_diff -family Courier -size 10
540 font create font_ui
541 catch {
542         label .dummy
543         eval font configure font_ui [font actual [.dummy cget -font]]
544         destroy .dummy
547 font create font_uiitalic
548 font create font_uibold
549 font create font_diffbold
550 font create font_diffitalic
552 foreach class {Button Checkbutton Entry Label
553                 Labelframe Listbox Menu Message
554                 Radiobutton Spinbox Text} {
555         option add *$class.font font_ui
557 unset class
559 if {[is_Windows] || [is_MacOSX]} {
560         option add *Menu.tearOff 0
563 if {[is_MacOSX]} {
564         set M1B M1
565         set M1T Cmd
566 } else {
567         set M1B Control
568         set M1T Ctrl
571 proc bind_button3 {w cmd} {
572         bind $w <Any-Button-3> $cmd
573         if {[is_MacOSX]} {
574                 # Mac OS X sends Button-2 on right click through three-button mouse,
575                 # or through trackpad right-clicking (two-finger touch + click).
576                 bind $w <Any-Button-2> $cmd
577                 bind $w <Control-Button-1> $cmd
578         }
581 proc apply_config {} {
582         global repo_config font_descs
584         foreach option $font_descs {
585                 set name [lindex $option 0]
586                 set font [lindex $option 1]
587                 if {[catch {
588                         foreach {cn cv} $repo_config(gui.$name) {
589                                 font configure $font $cn $cv -weight normal
590                         }
591                         } err]} {
592                         error_popup [strcat [mc "Invalid font specified in %s:" "gui.$name"] "\n\n$err"]
593                 }
594                 foreach {cn cv} [font configure $font] {
595                         font configure ${font}bold $cn $cv
596                         font configure ${font}italic $cn $cv
597                 }
598                 font configure ${font}bold -weight bold
599                 font configure ${font}italic -slant italic
600         }
603 set default_config(merge.diffstat) true
604 set default_config(merge.summary) false
605 set default_config(merge.verbosity) 2
606 set default_config(user.name) {}
607 set default_config(user.email) {}
609 set default_config(gui.matchtrackingbranch) false
610 set default_config(gui.pruneduringfetch) false
611 set default_config(gui.trustmtime) false
612 set default_config(gui.diffcontext) 5
613 set default_config(gui.newbranchtemplate) {}
614 set default_config(gui.fontui) [font configure font_ui]
615 set default_config(gui.fontdiff) [font configure font_diff]
616 set font_descs {
617         {fontui   font_ui   {mc "Main Font"}}
618         {fontdiff font_diff {mc "Diff/Console Font"}}
621 ######################################################################
622 ##
623 ## find git
625 set _git  [_which git]
626 if {$_git eq {}} {
627         catch {wm withdraw .}
628         tk_messageBox \
629                 -icon error \
630                 -type ok \
631                 -title [mc "git-gui: fatal error"] \
632                 -message [mc "Cannot find git in PATH."]
633         exit 1
636 ######################################################################
637 ##
638 ## version check
640 if {[catch {set _git_version [git --version]} err]} {
641         catch {wm withdraw .}
642         tk_messageBox \
643                 -icon error \
644                 -type ok \
645                 -title [mc "git-gui: fatal error"] \
646                 -message "Cannot determine Git version:
648 $err
650 [appname] requires Git 1.5.0 or later."
651         exit 1
653 if {![regsub {^git version } $_git_version {} _git_version]} {
654         catch {wm withdraw .}
655         tk_messageBox \
656                 -icon error \
657                 -type ok \
658                 -title [mc "git-gui: fatal error"] \
659                 -message [strcat [mc "Cannot parse Git version string:"] "\n\n$_git_version"]
660         exit 1
663 set _real_git_version $_git_version
664 regsub -- {-dirty$} $_git_version {} _git_version
665 regsub {\.[0-9]+\.g[0-9a-f]+$} $_git_version {} _git_version
666 regsub {\.rc[0-9]+$} $_git_version {} _git_version
667 regsub {\.GIT$} $_git_version {} _git_version
669 if {![regexp {^[1-9]+(\.[0-9]+)+$} $_git_version]} {
670         catch {wm withdraw .}
671         if {[tk_messageBox \
672                 -icon warning \
673                 -type yesno \
674                 -default no \
675                 -title "[appname]: warning" \
676                  -message [mc "Git version cannot be determined.
678 %s claims it is version '%s'.
680 %s requires at least Git 1.5.0 or later.
682 Assume '%s' is version 1.5.0?
683 " $_git $_real_git_version [appname] $_real_git_version]] eq {yes}} {
684                 set _git_version 1.5.0
685         } else {
686                 exit 1
687         }
689 unset _real_git_version
691 proc git-version {args} {
692         global _git_version
694         switch [llength $args] {
695         0 {
696                 return $_git_version
697         }
699         2 {
700                 set op [lindex $args 0]
701                 set vr [lindex $args 1]
702                 set cm [package vcompare $_git_version $vr]
703                 return [expr $cm $op 0]
704         }
706         4 {
707                 set type [lindex $args 0]
708                 set name [lindex $args 1]
709                 set parm [lindex $args 2]
710                 set body [lindex $args 3]
712                 if {($type ne {proc} && $type ne {method})} {
713                         error "Invalid arguments to git-version"
714                 }
715                 if {[llength $body] < 2 || [lindex $body end-1] ne {default}} {
716                         error "Last arm of $type $name must be default"
717                 }
719                 foreach {op vr cb} [lrange $body 0 end-2] {
720                         if {[git-version $op $vr]} {
721                                 return [uplevel [list $type $name $parm $cb]]
722                         }
723                 }
725                 return [uplevel [list $type $name $parm [lindex $body end]]]
726         }
728         default {
729                 error "git-version >= x"
730         }
732         }
735 if {[git-version < 1.5]} {
736         catch {wm withdraw .}
737         tk_messageBox \
738                 -icon error \
739                 -type ok \
740                 -title [mc "git-gui: fatal error"] \
741                 -message "[appname] requires Git 1.5.0 or later.
743 You are using [git-version]:
745 [git --version]"
746         exit 1
749 ######################################################################
750 ##
751 ## configure our library
753 set idx [file join $oguilib tclIndex]
754 if {[catch {set fd [open $idx r]} err]} {
755         catch {wm withdraw .}
756         tk_messageBox \
757                 -icon error \
758                 -type ok \
759                 -title [mc "git-gui: fatal error"] \
760                 -message $err
761         exit 1
763 if {[gets $fd] eq {# Autogenerated by git-gui Makefile}} {
764         set idx [list]
765         while {[gets $fd n] >= 0} {
766                 if {$n ne {} && ![string match #* $n]} {
767                         lappend idx $n
768                 }
769         }
770 } else {
771         set idx {}
773 close $fd
775 if {$idx ne {}} {
776         set loaded [list]
777         foreach p $idx {
778                 if {[lsearch -exact $loaded $p] >= 0} continue
779                 source [file join $oguilib $p]
780                 lappend loaded $p
781         }
782         unset loaded p
783 } else {
784         set auto_path [concat [list $oguilib] $auto_path]
786 unset -nocomplain idx fd
788 ######################################################################
789 ##
790 ## feature option selection
792 if {[regexp {^git-(.+)$} [file tail $argv0] _junk subcommand]} {
793         unset _junk
794 } else {
795         set subcommand gui
797 if {$subcommand eq {gui.sh}} {
798         set subcommand gui
800 if {$subcommand eq {gui} && [llength $argv] > 0} {
801         set subcommand [lindex $argv 0]
802         set argv [lrange $argv 1 end]
805 enable_option multicommit
806 enable_option branch
807 enable_option transport
808 disable_option bare
810 switch -- $subcommand {
811 browser -
812 blame {
813         enable_option bare
815         disable_option multicommit
816         disable_option branch
817         disable_option transport
819 citool {
820         enable_option singlecommit
822         disable_option multicommit
823         disable_option branch
824         disable_option transport
828 ######################################################################
829 ##
830 ## repository setup
832 if {[catch {
833                 set _gitdir $env(GIT_DIR)
834                 set _prefix {}
835                 }]
836         && [catch {
837                 set _gitdir [git rev-parse --git-dir]
838                 set _prefix [git rev-parse --show-prefix]
839         } err]} {
840         load_config 1
841         apply_config
842         choose_repository::pick
844 if {![file isdirectory $_gitdir] && [is_Cygwin]} {
845         catch {set _gitdir [exec cygpath --windows $_gitdir]}
847 if {![file isdirectory $_gitdir]} {
848         catch {wm withdraw .}
849         error_popup [strcat [mc "Git directory not found:"] "\n\n$_gitdir"]
850         exit 1
852 if {$_prefix ne {}} {
853         regsub -all {[^/]+/} $_prefix ../ cdup
854         if {[catch {cd $cdup} err]} {
855                 catch {wm withdraw .}
856                 error_popup [strcat [mc "Cannot move to top of working directory:"] "\n\n$err"]
857                 exit 1
858         }
859         unset cdup
860 } elseif {![is_enabled bare]} {
861         if {[lindex [file split $_gitdir] end] ne {.git}} {
862                 catch {wm withdraw .}
863                 error_popup [strcat [mc "Cannot use funny .git directory:"] "\n\n$_gitdir"]
864                 exit 1
865         }
866         if {[catch {cd [file dirname $_gitdir]} err]} {
867                 catch {wm withdraw .}
868                 error_popup [strcat [mc "No working directory"] " [file dirname $_gitdir]:\n\n$err"]
869                 exit 1
870         }
872 set _reponame [file split [file normalize $_gitdir]]
873 if {[lindex $_reponame end] eq {.git}} {
874         set _reponame [lindex $_reponame end-1]
875 } else {
876         set _reponame [lindex $_reponame end]
879 ######################################################################
880 ##
881 ## global init
883 set current_diff_path {}
884 set current_diff_side {}
885 set diff_actions [list]
887 set HEAD {}
888 set PARENT {}
889 set MERGE_HEAD [list]
890 set commit_type {}
891 set empty_tree {}
892 set current_branch {}
893 set is_detached 0
894 set current_diff_path {}
895 set is_3way_diff 0
896 set selected_commit_type new
898 ######################################################################
899 ##
900 ## task management
902 set rescan_active 0
903 set diff_active 0
904 set last_clicked {}
906 set disable_on_lock [list]
907 set index_lock_type none
909 proc lock_index {type} {
910         global index_lock_type disable_on_lock
912         if {$index_lock_type eq {none}} {
913                 set index_lock_type $type
914                 foreach w $disable_on_lock {
915                         uplevel #0 $w disabled
916                 }
917                 return 1
918         } elseif {$index_lock_type eq "begin-$type"} {
919                 set index_lock_type $type
920                 return 1
921         }
922         return 0
925 proc unlock_index {} {
926         global index_lock_type disable_on_lock
928         set index_lock_type none
929         foreach w $disable_on_lock {
930                 uplevel #0 $w normal
931         }
934 ######################################################################
935 ##
936 ## status
938 proc repository_state {ctvar hdvar mhvar} {
939         global current_branch
940         upvar $ctvar ct $hdvar hd $mhvar mh
942         set mh [list]
944         load_current_branch
945         if {[catch {set hd [git rev-parse --verify HEAD]}]} {
946                 set hd {}
947                 set ct initial
948                 return
949         }
951         set merge_head [gitdir MERGE_HEAD]
952         if {[file exists $merge_head]} {
953                 set ct merge
954                 set fd_mh [open $merge_head r]
955                 while {[gets $fd_mh line] >= 0} {
956                         lappend mh $line
957                 }
958                 close $fd_mh
959                 return
960         }
962         set ct normal
965 proc PARENT {} {
966         global PARENT empty_tree
968         set p [lindex $PARENT 0]
969         if {$p ne {}} {
970                 return $p
971         }
972         if {$empty_tree eq {}} {
973                 set empty_tree [git mktree << {}]
974         }
975         return $empty_tree
978 proc rescan {after {honor_trustmtime 1}} {
979         global HEAD PARENT MERGE_HEAD commit_type
980         global ui_index ui_workdir ui_comm
981         global rescan_active file_states
982         global repo_config
984         if {$rescan_active > 0 || ![lock_index read]} return
986         repository_state newType newHEAD newMERGE_HEAD
987         if {[string match amend* $commit_type]
988                 && $newType eq {normal}
989                 && $newHEAD eq $HEAD} {
990         } else {
991                 set HEAD $newHEAD
992                 set PARENT $newHEAD
993                 set MERGE_HEAD $newMERGE_HEAD
994                 set commit_type $newType
995         }
997         array unset file_states
999         if {!$::GITGUI_BCK_exists &&
1000                 (![$ui_comm edit modified]
1001                 || [string trim [$ui_comm get 0.0 end]] eq {})} {
1002                 if {[string match amend* $commit_type]} {
1003                 } elseif {[load_message GITGUI_MSG]} {
1004                 } elseif {[load_message MERGE_MSG]} {
1005                 } elseif {[load_message SQUASH_MSG]} {
1006                 }
1007                 $ui_comm edit reset
1008                 $ui_comm edit modified false
1009         }
1011         if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} {
1012                 rescan_stage2 {} $after
1013         } else {
1014                 set rescan_active 1
1015                 ui_status [mc "Refreshing file status..."]
1016                 set fd_rf [git_read update-index \
1017                         -q \
1018                         --unmerged \
1019                         --ignore-missing \
1020                         --refresh \
1021                         ]
1022                 fconfigure $fd_rf -blocking 0 -translation binary
1023                 fileevent $fd_rf readable \
1024                         [list rescan_stage2 $fd_rf $after]
1025         }
1028 if {[is_Cygwin]} {
1029         set is_git_info_link {}
1030         set is_git_info_exclude {}
1031         proc have_info_exclude {} {
1032                 global is_git_info_link is_git_info_exclude
1034                 if {$is_git_info_link eq {}} {
1035                         set is_git_info_link [file isfile [gitdir info.lnk]]
1036                 }
1038                 if {$is_git_info_link} {
1039                         if {$is_git_info_exclude eq {}} {
1040                                 if {[catch {exec test -f [gitdir info exclude]}]} {
1041                                         set is_git_info_exclude 0
1042                                 } else {
1043                                         set is_git_info_exclude 1
1044                                 }
1045                         }
1046                         return $is_git_info_exclude
1047                 } else {
1048                         return [file readable [gitdir info exclude]]
1049                 }
1050         }
1051 } else {
1052         proc have_info_exclude {} {
1053                 return [file readable [gitdir info exclude]]
1054         }
1057 proc rescan_stage2 {fd after} {
1058         global rescan_active buf_rdi buf_rdf buf_rlo
1060         if {$fd ne {}} {
1061                 read $fd
1062                 if {![eof $fd]} return
1063                 close $fd
1064         }
1066         set ls_others [list --exclude-per-directory=.gitignore]
1067         if {[have_info_exclude]} {
1068                 lappend ls_others "--exclude-from=[gitdir info exclude]"
1069         }
1070         set user_exclude [get_config core.excludesfile]
1071         if {$user_exclude ne {} && [file readable $user_exclude]} {
1072                 lappend ls_others "--exclude-from=$user_exclude"
1073         }
1075         set buf_rdi {}
1076         set buf_rdf {}
1077         set buf_rlo {}
1079         set rescan_active 3
1080         ui_status [mc "Scanning for modified files ..."]
1081         set fd_di [git_read diff-index --cached -z [PARENT]]
1082         set fd_df [git_read diff-files -z]
1083         set fd_lo [eval git_read ls-files --others -z $ls_others]
1085         fconfigure $fd_di -blocking 0 -translation binary -encoding binary
1086         fconfigure $fd_df -blocking 0 -translation binary -encoding binary
1087         fconfigure $fd_lo -blocking 0 -translation binary -encoding binary
1088         fileevent $fd_di readable [list read_diff_index $fd_di $after]
1089         fileevent $fd_df readable [list read_diff_files $fd_df $after]
1090         fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
1093 proc load_message {file} {
1094         global ui_comm
1096         set f [gitdir $file]
1097         if {[file isfile $f]} {
1098                 if {[catch {set fd [open $f r]}]} {
1099                         return 0
1100                 }
1101                 fconfigure $fd -eofchar {}
1102                 set content [string trim [read $fd]]
1103                 close $fd
1104                 regsub -all -line {[ \r\t]+$} $content {} content
1105                 $ui_comm delete 0.0 end
1106                 $ui_comm insert end $content
1107                 return 1
1108         }
1109         return 0
1112 proc read_diff_index {fd after} {
1113         global buf_rdi
1115         append buf_rdi [read $fd]
1116         set c 0
1117         set n [string length $buf_rdi]
1118         while {$c < $n} {
1119                 set z1 [string first "\0" $buf_rdi $c]
1120                 if {$z1 == -1} break
1121                 incr z1
1122                 set z2 [string first "\0" $buf_rdi $z1]
1123                 if {$z2 == -1} break
1125                 incr c
1126                 set i [split [string range $buf_rdi $c [expr {$z1 - 2}]] { }]
1127                 set p [string range $buf_rdi $z1 [expr {$z2 - 1}]]
1128                 merge_state \
1129                         [encoding convertfrom $p] \
1130                         [lindex $i 4]? \
1131                         [list [lindex $i 0] [lindex $i 2]] \
1132                         [list]
1133                 set c $z2
1134                 incr c
1135         }
1136         if {$c < $n} {
1137                 set buf_rdi [string range $buf_rdi $c end]
1138         } else {
1139                 set buf_rdi {}
1140         }
1142         rescan_done $fd buf_rdi $after
1145 proc read_diff_files {fd after} {
1146         global buf_rdf
1148         append buf_rdf [read $fd]
1149         set c 0
1150         set n [string length $buf_rdf]
1151         while {$c < $n} {
1152                 set z1 [string first "\0" $buf_rdf $c]
1153                 if {$z1 == -1} break
1154                 incr z1
1155                 set z2 [string first "\0" $buf_rdf $z1]
1156                 if {$z2 == -1} break
1158                 incr c
1159                 set i [split [string range $buf_rdf $c [expr {$z1 - 2}]] { }]
1160                 set p [string range $buf_rdf $z1 [expr {$z2 - 1}]]
1161                 merge_state \
1162                         [encoding convertfrom $p] \
1163                         ?[lindex $i 4] \
1164                         [list] \
1165                         [list [lindex $i 0] [lindex $i 2]]
1166                 set c $z2
1167                 incr c
1168         }
1169         if {$c < $n} {
1170                 set buf_rdf [string range $buf_rdf $c end]
1171         } else {
1172                 set buf_rdf {}
1173         }
1175         rescan_done $fd buf_rdf $after
1178 proc read_ls_others {fd after} {
1179         global buf_rlo
1181         append buf_rlo [read $fd]
1182         set pck [split $buf_rlo "\0"]
1183         set buf_rlo [lindex $pck end]
1184         foreach p [lrange $pck 0 end-1] {
1185                 set p [encoding convertfrom $p]
1186                 if {[string index $p end] eq {/}} {
1187                         set p [string range $p 0 end-1]
1188                 }
1189                 merge_state $p ?O
1190         }
1191         rescan_done $fd buf_rlo $after
1194 proc rescan_done {fd buf after} {
1195         global rescan_active current_diff_path
1196         global file_states repo_config
1197         upvar $buf to_clear
1199         if {![eof $fd]} return
1200         set to_clear {}
1201         close $fd
1202         if {[incr rescan_active -1] > 0} return
1204         prune_selection
1205         unlock_index
1206         display_all_files
1207         if {$current_diff_path ne {}} reshow_diff
1208         uplevel #0 $after
1211 proc prune_selection {} {
1212         global file_states selected_paths
1214         foreach path [array names selected_paths] {
1215                 if {[catch {set still_here $file_states($path)}]} {
1216                         unset selected_paths($path)
1217                 }
1218         }
1221 ######################################################################
1222 ##
1223 ## ui helpers
1225 proc mapicon {w state path} {
1226         global all_icons
1228         if {[catch {set r $all_icons($state$w)}]} {
1229                 puts "error: no icon for $w state={$state} $path"
1230                 return file_plain
1231         }
1232         return $r
1235 proc mapdesc {state path} {
1236         global all_descs
1238         if {[catch {set r $all_descs($state)}]} {
1239                 puts "error: no desc for state={$state} $path"
1240                 return $state
1241         }
1242         return $r
1245 proc ui_status {msg} {
1246         global main_status
1247         if {[info exists main_status]} {
1248                 $main_status show $msg
1249         }
1252 proc ui_ready {{test {}}} {
1253         global main_status
1254         if {[info exists main_status]} {
1255                 $main_status show [mc "Ready."] $test
1256         }
1259 proc escape_path {path} {
1260         regsub -all {\\} $path "\\\\" path
1261         regsub -all "\n" $path "\\n" path
1262         return $path
1265 proc short_path {path} {
1266         return [escape_path [lindex [file split $path] end]]
1269 set next_icon_id 0
1270 set null_sha1 [string repeat 0 40]
1272 proc merge_state {path new_state {head_info {}} {index_info {}}} {
1273         global file_states next_icon_id null_sha1
1275         set s0 [string index $new_state 0]
1276         set s1 [string index $new_state 1]
1278         if {[catch {set info $file_states($path)}]} {
1279                 set state __
1280                 set icon n[incr next_icon_id]
1281         } else {
1282                 set state [lindex $info 0]
1283                 set icon [lindex $info 1]
1284                 if {$head_info eq {}}  {set head_info  [lindex $info 2]}
1285                 if {$index_info eq {}} {set index_info [lindex $info 3]}
1286         }
1288         if     {$s0 eq {?}} {set s0 [string index $state 0]} \
1289         elseif {$s0 eq {_}} {set s0 _}
1291         if     {$s1 eq {?}} {set s1 [string index $state 1]} \
1292         elseif {$s1 eq {_}} {set s1 _}
1294         if {$s0 eq {A} && $s1 eq {_} && $head_info eq {}} {
1295                 set head_info [list 0 $null_sha1]
1296         } elseif {$s0 ne {_} && [string index $state 0] eq {_}
1297                 && $head_info eq {}} {
1298                 set head_info $index_info
1299         }
1301         set file_states($path) [list $s0$s1 $icon \
1302                 $head_info $index_info \
1303                 ]
1304         return $state
1307 proc display_file_helper {w path icon_name old_m new_m} {
1308         global file_lists
1310         if {$new_m eq {_}} {
1311                 set lno [lsearch -sorted -exact $file_lists($w) $path]
1312                 if {$lno >= 0} {
1313                         set file_lists($w) [lreplace $file_lists($w) $lno $lno]
1314                         incr lno
1315                         $w conf -state normal
1316                         $w delete $lno.0 [expr {$lno + 1}].0
1317                         $w conf -state disabled
1318                 }
1319         } elseif {$old_m eq {_} && $new_m ne {_}} {
1320                 lappend file_lists($w) $path
1321                 set file_lists($w) [lsort -unique $file_lists($w)]
1322                 set lno [lsearch -sorted -exact $file_lists($w) $path]
1323                 incr lno
1324                 $w conf -state normal
1325                 $w image create $lno.0 \
1326                         -align center -padx 5 -pady 1 \
1327                         -name $icon_name \
1328                         -image [mapicon $w $new_m $path]
1329                 $w insert $lno.1 "[escape_path $path]\n"
1330                 $w conf -state disabled
1331         } elseif {$old_m ne $new_m} {
1332                 $w conf -state normal
1333                 $w image conf $icon_name -image [mapicon $w $new_m $path]
1334                 $w conf -state disabled
1335         }
1338 proc display_file {path state} {
1339         global file_states selected_paths
1340         global ui_index ui_workdir
1342         set old_m [merge_state $path $state]
1343         set s $file_states($path)
1344         set new_m [lindex $s 0]
1345         set icon_name [lindex $s 1]
1347         set o [string index $old_m 0]
1348         set n [string index $new_m 0]
1349         if {$o eq {U}} {
1350                 set o _
1351         }
1352         if {$n eq {U}} {
1353                 set n _
1354         }
1355         display_file_helper     $ui_index $path $icon_name $o $n
1357         if {[string index $old_m 0] eq {U}} {
1358                 set o U
1359         } else {
1360                 set o [string index $old_m 1]
1361         }
1362         if {[string index $new_m 0] eq {U}} {
1363                 set n U
1364         } else {
1365                 set n [string index $new_m 1]
1366         }
1367         display_file_helper     $ui_workdir $path $icon_name $o $n
1369         if {$new_m eq {__}} {
1370                 unset file_states($path)
1371                 catch {unset selected_paths($path)}
1372         }
1375 proc display_all_files_helper {w path icon_name m} {
1376         global file_lists
1378         lappend file_lists($w) $path
1379         set lno [expr {[lindex [split [$w index end] .] 0] - 1}]
1380         $w image create end \
1381                 -align center -padx 5 -pady 1 \
1382                 -name $icon_name \
1383                 -image [mapicon $w $m $path]
1384         $w insert end "[escape_path $path]\n"
1387 proc display_all_files {} {
1388         global ui_index ui_workdir
1389         global file_states file_lists
1390         global last_clicked
1392         $ui_index conf -state normal
1393         $ui_workdir conf -state normal
1395         $ui_index delete 0.0 end
1396         $ui_workdir delete 0.0 end
1397         set last_clicked {}
1399         set file_lists($ui_index) [list]
1400         set file_lists($ui_workdir) [list]
1402         foreach path [lsort [array names file_states]] {
1403                 set s $file_states($path)
1404                 set m [lindex $s 0]
1405                 set icon_name [lindex $s 1]
1407                 set s [string index $m 0]
1408                 if {$s ne {U} && $s ne {_}} {
1409                         display_all_files_helper $ui_index $path \
1410                                 $icon_name $s
1411                 }
1413                 if {[string index $m 0] eq {U}} {
1414                         set s U
1415                 } else {
1416                         set s [string index $m 1]
1417                 }
1418                 if {$s ne {_}} {
1419                         display_all_files_helper $ui_workdir $path \
1420                                 $icon_name $s
1421                 }
1422         }
1424         $ui_index conf -state disabled
1425         $ui_workdir conf -state disabled
1428 ######################################################################
1429 ##
1430 ## icons
1432 set filemask {
1433 #define mask_width 14
1434 #define mask_height 15
1435 static unsigned char mask_bits[] = {
1436    0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1437    0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1438    0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
1441 image create bitmap file_plain -background white -foreground black -data {
1442 #define plain_width 14
1443 #define plain_height 15
1444 static unsigned char plain_bits[] = {
1445    0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1446    0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
1447    0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1448 } -maskdata $filemask
1450 image create bitmap file_mod -background white -foreground blue -data {
1451 #define mod_width 14
1452 #define mod_height 15
1453 static unsigned char mod_bits[] = {
1454    0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1455    0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1456    0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1457 } -maskdata $filemask
1459 image create bitmap file_fulltick -background white -foreground "#007000" -data {
1460 #define file_fulltick_width 14
1461 #define file_fulltick_height 15
1462 static unsigned char file_fulltick_bits[] = {
1463    0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
1464    0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
1465    0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1466 } -maskdata $filemask
1468 image create bitmap file_parttick -background white -foreground "#005050" -data {
1469 #define parttick_width 14
1470 #define parttick_height 15
1471 static unsigned char parttick_bits[] = {
1472    0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1473    0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
1474    0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1475 } -maskdata $filemask
1477 image create bitmap file_question -background white -foreground black -data {
1478 #define file_question_width 14
1479 #define file_question_height 15
1480 static unsigned char file_question_bits[] = {
1481    0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
1482    0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
1483    0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1484 } -maskdata $filemask
1486 image create bitmap file_removed -background white -foreground red -data {
1487 #define file_removed_width 14
1488 #define file_removed_height 15
1489 static unsigned char file_removed_bits[] = {
1490    0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1491    0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
1492    0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
1493 } -maskdata $filemask
1495 image create bitmap file_merge -background white -foreground blue -data {
1496 #define file_merge_width 14
1497 #define file_merge_height 15
1498 static unsigned char file_merge_bits[] = {
1499    0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
1500    0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1501    0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1502 } -maskdata $filemask
1504 set ui_index .vpane.files.index.list
1505 set ui_workdir .vpane.files.workdir.list
1507 set all_icons(_$ui_index)   file_plain
1508 set all_icons(A$ui_index)   file_fulltick
1509 set all_icons(M$ui_index)   file_fulltick
1510 set all_icons(D$ui_index)   file_removed
1511 set all_icons(U$ui_index)   file_merge
1513 set all_icons(_$ui_workdir) file_plain
1514 set all_icons(M$ui_workdir) file_mod
1515 set all_icons(D$ui_workdir) file_question
1516 set all_icons(U$ui_workdir) file_merge
1517 set all_icons(O$ui_workdir) file_plain
1519 set max_status_desc 0
1520 foreach i {
1521                 {__ {mc "Unmodified"}}
1523                 {_M {mc "Modified, not staged"}}
1524                 {M_ {mc "Staged for commit"}}
1525                 {MM {mc "Portions staged for commit"}}
1526                 {MD {mc "Staged for commit, missing"}}
1528                 {_O {mc "Untracked, not staged"}}
1529                 {A_ {mc "Staged for commit"}}
1530                 {AM {mc "Portions staged for commit"}}
1531                 {AD {mc "Staged for commit, missing"}}
1533                 {_D {mc "Missing"}}
1534                 {D_ {mc "Staged for removal"}}
1535                 {DO {mc "Staged for removal, still present"}}
1537                 {U_ {mc "Requires merge resolution"}}
1538                 {UU {mc "Requires merge resolution"}}
1539                 {UM {mc "Requires merge resolution"}}
1540                 {UD {mc "Requires merge resolution"}}
1541         } {
1542         set text [eval [lindex $i 1]]
1543         if {$max_status_desc < [string length $text]} {
1544                 set max_status_desc [string length $text]
1545         }
1546         set all_descs([lindex $i 0]) $text
1548 unset i
1550 ######################################################################
1551 ##
1552 ## util
1554 proc scrollbar2many {list mode args} {
1555         foreach w $list {eval $w $mode $args}
1558 proc many2scrollbar {list mode sb top bottom} {
1559         $sb set $top $bottom
1560         foreach w $list {$w $mode moveto $top}
1563 proc incr_font_size {font {amt 1}} {
1564         set sz [font configure $font -size]
1565         incr sz $amt
1566         font configure $font -size $sz
1567         font configure ${font}bold -size $sz
1568         font configure ${font}italic -size $sz
1571 ######################################################################
1572 ##
1573 ## ui commands
1575 set starting_gitk_msg [mc "Starting gitk... please wait..."]
1577 proc do_gitk {revs} {
1578         # -- Always start gitk through whatever we were loaded with.  This
1579         #    lets us bypass using shell process on Windows systems.
1580         #
1581         set exe [file join [file dirname $::_git] gitk]
1582         set cmd [list [info nameofexecutable] $exe]
1583         if {! [file exists $exe]} {
1584                 error_popup [mc "Unable to start gitk:\n\n%s does not exist" $exe]
1585         } else {
1586                 global env
1588                 if {[info exists env(GIT_DIR)]} {
1589                         set old_GIT_DIR $env(GIT_DIR)
1590                 } else {
1591                         set old_GIT_DIR {}
1592                 }
1594                 set pwd [pwd]
1595                 cd [file dirname [gitdir]]
1596                 set env(GIT_DIR) [file tail [gitdir]]
1598                 eval exec $cmd $revs &
1600                 if {$old_GIT_DIR eq {}} {
1601                         unset env(GIT_DIR)
1602                 } else {
1603                         set env(GIT_DIR) $old_GIT_DIR
1604                 }
1605                 cd $pwd
1607                 ui_status $::starting_gitk_msg
1608                 after 10000 {
1609                         ui_ready $starting_gitk_msg
1610                 }
1611         }
1614 set is_quitting 0
1616 proc do_quit {} {
1617         global ui_comm is_quitting repo_config commit_type
1618         global GITGUI_BCK_exists GITGUI_BCK_i
1620         if {$is_quitting} return
1621         set is_quitting 1
1623         if {[winfo exists $ui_comm]} {
1624                 # -- Stash our current commit buffer.
1625                 #
1626                 set save [gitdir GITGUI_MSG]
1627                 if {$GITGUI_BCK_exists && ![$ui_comm edit modified]} {
1628                         file rename -force [gitdir GITGUI_BCK] $save
1629                         set GITGUI_BCK_exists 0
1630                 } else {
1631                         set msg [string trim [$ui_comm get 0.0 end]]
1632                         regsub -all -line {[ \r\t]+$} $msg {} msg
1633                         if {(![string match amend* $commit_type]
1634                                 || [$ui_comm edit modified])
1635                                 && $msg ne {}} {
1636                                 catch {
1637                                         set fd [open $save w]
1638                                         puts -nonewline $fd $msg
1639                                         close $fd
1640                                 }
1641                         } else {
1642                                 catch {file delete $save}
1643                         }
1644                 }
1646                 # -- Remove our editor backup, its not needed.
1647                 #
1648                 after cancel $GITGUI_BCK_i
1649                 if {$GITGUI_BCK_exists} {
1650                         catch {file delete [gitdir GITGUI_BCK]}
1651                 }
1653                 # -- Stash our current window geometry into this repository.
1654                 #
1655                 set cfg_geometry [list]
1656                 lappend cfg_geometry [wm geometry .]
1657                 lappend cfg_geometry [lindex [.vpane sash coord 0] 1]
1658                 lappend cfg_geometry [lindex [.vpane.files sash coord 0] 0]
1659                 if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
1660                         set rc_geometry {}
1661                 }
1662                 if {$cfg_geometry ne $rc_geometry} {
1663                         catch {git config gui.geometry $cfg_geometry}
1664                 }
1665         }
1667         destroy .
1670 proc do_rescan {} {
1671         rescan ui_ready
1674 proc do_commit {} {
1675         commit_tree
1678 proc toggle_or_diff {w x y} {
1679         global file_states file_lists current_diff_path ui_index ui_workdir
1680         global last_clicked selected_paths
1682         set pos [split [$w index @$x,$y] .]
1683         set lno [lindex $pos 0]
1684         set col [lindex $pos 1]
1685         set path [lindex $file_lists($w) [expr {$lno - 1}]]
1686         if {$path eq {}} {
1687                 set last_clicked {}
1688                 return
1689         }
1691         set last_clicked [list $w $lno]
1692         array unset selected_paths
1693         $ui_index tag remove in_sel 0.0 end
1694         $ui_workdir tag remove in_sel 0.0 end
1696         if {$col == 0} {
1697                 if {$current_diff_path eq $path} {
1698                         set after {reshow_diff;}
1699                 } else {
1700                         set after {}
1701                 }
1702                 if {$w eq $ui_index} {
1703                         update_indexinfo \
1704                                 "Unstaging [short_path $path] from commit" \
1705                                 [list $path] \
1706                                 [concat $after [list ui_ready]]
1707                 } elseif {$w eq $ui_workdir} {
1708                         update_index \
1709                                 "Adding [short_path $path]" \
1710                                 [list $path] \
1711                                 [concat $after [list ui_ready]]
1712                 }
1713         } else {
1714                 show_diff $path $w $lno
1715         }
1718 proc add_one_to_selection {w x y} {
1719         global file_lists last_clicked selected_paths
1721         set lno [lindex [split [$w index @$x,$y] .] 0]
1722         set path [lindex $file_lists($w) [expr {$lno - 1}]]
1723         if {$path eq {}} {
1724                 set last_clicked {}
1725                 return
1726         }
1728         if {$last_clicked ne {}
1729                 && [lindex $last_clicked 0] ne $w} {
1730                 array unset selected_paths
1731                 [lindex $last_clicked 0] tag remove in_sel 0.0 end
1732         }
1734         set last_clicked [list $w $lno]
1735         if {[catch {set in_sel $selected_paths($path)}]} {
1736                 set in_sel 0
1737         }
1738         if {$in_sel} {
1739                 unset selected_paths($path)
1740                 $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
1741         } else {
1742                 set selected_paths($path) 1
1743                 $w tag add in_sel $lno.0 [expr {$lno + 1}].0
1744         }
1747 proc add_range_to_selection {w x y} {
1748         global file_lists last_clicked selected_paths
1750         if {[lindex $last_clicked 0] ne $w} {
1751                 toggle_or_diff $w $x $y
1752                 return
1753         }
1755         set lno [lindex [split [$w index @$x,$y] .] 0]
1756         set lc [lindex $last_clicked 1]
1757         if {$lc < $lno} {
1758                 set begin $lc
1759                 set end $lno
1760         } else {
1761                 set begin $lno
1762                 set end $lc
1763         }
1765         foreach path [lrange $file_lists($w) \
1766                 [expr {$begin - 1}] \
1767                 [expr {$end - 1}]] {
1768                 set selected_paths($path) 1
1769         }
1770         $w tag add in_sel $begin.0 [expr {$end + 1}].0
1773 ######################################################################
1774 ##
1775 ## ui construction
1777 load_config 0
1778 apply_config
1779 set ui_comm {}
1781 # -- Menu Bar
1783 menu .mbar -tearoff 0
1784 .mbar add cascade -label [mc Repository] -menu .mbar.repository
1785 .mbar add cascade -label [mc Edit] -menu .mbar.edit
1786 if {[is_enabled branch]} {
1787         .mbar add cascade -label [mc Branch] -menu .mbar.branch
1789 if {[is_enabled multicommit] || [is_enabled singlecommit]} {
1790         .mbar add cascade -label [mc Commit@@noun] -menu .mbar.commit
1792 if {[is_enabled transport]} {
1793         .mbar add cascade -label [mc Merge] -menu .mbar.merge
1794         .mbar add cascade -label [mc Fetch] -menu .mbar.fetch
1795         .mbar add cascade -label [mc Push] -menu .mbar.push
1797 . configure -menu .mbar
1799 # -- Repository Menu
1801 menu .mbar.repository
1803 .mbar.repository add command \
1804         -label [mc "Browse Current Branch's Files"] \
1805         -command {browser::new $current_branch}
1806 set ui_browse_current [.mbar.repository index last]
1807 .mbar.repository add command \
1808         -label [mc "Browse Branch Files..."] \
1809         -command browser_open::dialog
1810 .mbar.repository add separator
1812 .mbar.repository add command \
1813         -label [mc "Visualize Current Branch's History"] \
1814         -command {do_gitk $current_branch}
1815 set ui_visualize_current [.mbar.repository index last]
1816 .mbar.repository add command \
1817         -label [mc "Visualize All Branch History"] \
1818         -command {do_gitk --all}
1819 .mbar.repository add separator
1821 proc current_branch_write {args} {
1822         global current_branch
1823         .mbar.repository entryconf $::ui_browse_current \
1824                 -label [mc "Browse %s's Files" $current_branch]
1825         .mbar.repository entryconf $::ui_visualize_current \
1826                 -label [mc "Visualize %s's History" $current_branch]
1828 trace add variable current_branch write current_branch_write
1830 if {[is_enabled multicommit]} {
1831         .mbar.repository add command -label [mc "Database Statistics"] \
1832                 -command do_stats
1834         .mbar.repository add command -label [mc "Compress Database"] \
1835                 -command do_gc
1837         .mbar.repository add command -label [mc "Verify Database"] \
1838                 -command do_fsck_objects
1840         .mbar.repository add separator
1842         if {[is_Cygwin]} {
1843                 .mbar.repository add command \
1844                         -label [mc "Create Desktop Icon"] \
1845                         -command do_cygwin_shortcut
1846         } elseif {[is_Windows]} {
1847                 .mbar.repository add command \
1848                         -label [mc "Create Desktop Icon"] \
1849                         -command do_windows_shortcut
1850         } elseif {[is_MacOSX]} {
1851                 .mbar.repository add command \
1852                         -label [mc "Create Desktop Icon"] \
1853                         -command do_macosx_app
1854         }
1857 .mbar.repository add command -label [mc Quit] \
1858         -command do_quit \
1859         -accelerator $M1T-Q
1861 # -- Edit Menu
1863 menu .mbar.edit
1864 .mbar.edit add command -label [mc Undo] \
1865         -command {catch {[focus] edit undo}} \
1866         -accelerator $M1T-Z
1867 .mbar.edit add command -label [mc Redo] \
1868         -command {catch {[focus] edit redo}} \
1869         -accelerator $M1T-Y
1870 .mbar.edit add separator
1871 .mbar.edit add command -label [mc Cut] \
1872         -command {catch {tk_textCut [focus]}} \
1873         -accelerator $M1T-X
1874 .mbar.edit add command -label [mc Copy] \
1875         -command {catch {tk_textCopy [focus]}} \
1876         -accelerator $M1T-C
1877 .mbar.edit add command -label [mc Paste] \
1878         -command {catch {tk_textPaste [focus]; [focus] see insert}} \
1879         -accelerator $M1T-V
1880 .mbar.edit add command -label [mc Delete] \
1881         -command {catch {[focus] delete sel.first sel.last}} \
1882         -accelerator Del
1883 .mbar.edit add separator
1884 .mbar.edit add command -label [mc "Select All"] \
1885         -command {catch {[focus] tag add sel 0.0 end}} \
1886         -accelerator $M1T-A
1888 # -- Branch Menu
1890 if {[is_enabled branch]} {
1891         menu .mbar.branch
1893         .mbar.branch add command -label [mc "Create..."] \
1894                 -command branch_create::dialog \
1895                 -accelerator $M1T-N
1896         lappend disable_on_lock [list .mbar.branch entryconf \
1897                 [.mbar.branch index last] -state]
1899         .mbar.branch add command -label [mc "Checkout..."] \
1900                 -command branch_checkout::dialog \
1901                 -accelerator $M1T-O
1902         lappend disable_on_lock [list .mbar.branch entryconf \
1903                 [.mbar.branch index last] -state]
1905         .mbar.branch add command -label [mc "Rename..."] \
1906                 -command branch_rename::dialog
1907         lappend disable_on_lock [list .mbar.branch entryconf \
1908                 [.mbar.branch index last] -state]
1910         .mbar.branch add command -label [mc "Delete..."] \
1911                 -command branch_delete::dialog
1912         lappend disable_on_lock [list .mbar.branch entryconf \
1913                 [.mbar.branch index last] -state]
1915         .mbar.branch add command -label [mc "Reset..."] \
1916                 -command merge::reset_hard
1917         lappend disable_on_lock [list .mbar.branch entryconf \
1918                 [.mbar.branch index last] -state]
1921 # -- Commit Menu
1923 if {[is_enabled multicommit] || [is_enabled singlecommit]} {
1924         menu .mbar.commit
1926         .mbar.commit add radiobutton \
1927                 -label [mc "New Commit"] \
1928                 -command do_select_commit_type \
1929                 -variable selected_commit_type \
1930                 -value new
1931         lappend disable_on_lock \
1932                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1934         .mbar.commit add radiobutton \
1935                 -label [mc "Amend Last Commit"] \
1936                 -command do_select_commit_type \
1937                 -variable selected_commit_type \
1938                 -value amend
1939         lappend disable_on_lock \
1940                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1942         .mbar.commit add separator
1944         .mbar.commit add command -label [mc Rescan] \
1945                 -command do_rescan \
1946                 -accelerator F5
1947         lappend disable_on_lock \
1948                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1950         .mbar.commit add command -label [mc "Stage To Commit"] \
1951                 -command do_add_selection
1952         lappend disable_on_lock \
1953                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1955         .mbar.commit add command -label [mc "Stage Changed Files To Commit"] \
1956                 -command do_add_all \
1957                 -accelerator $M1T-I
1958         lappend disable_on_lock \
1959                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1961         .mbar.commit add command -label [mc "Unstage From Commit"] \
1962                 -command do_unstage_selection
1963         lappend disable_on_lock \
1964                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1966         .mbar.commit add command -label [mc "Revert Changes"] \
1967                 -command do_revert_selection
1968         lappend disable_on_lock \
1969                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1971         .mbar.commit add separator
1973         .mbar.commit add command -label [mc "Sign Off"] \
1974                 -command do_signoff \
1975                 -accelerator $M1T-S
1977         .mbar.commit add command -label [mc Commit@@verb] \
1978                 -command do_commit \
1979                 -accelerator $M1T-Return
1980         lappend disable_on_lock \
1981                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1984 # -- Merge Menu
1986 if {[is_enabled branch]} {
1987         menu .mbar.merge
1988         .mbar.merge add command -label [mc "Local Merge..."] \
1989                 -command merge::dialog \
1990                 -accelerator $M1T-M
1991         lappend disable_on_lock \
1992                 [list .mbar.merge entryconf [.mbar.merge index last] -state]
1993         .mbar.merge add command -label [mc "Abort Merge..."] \
1994                 -command merge::reset_hard
1995         lappend disable_on_lock \
1996                 [list .mbar.merge entryconf [.mbar.merge index last] -state]
1999 # -- Transport Menu
2001 if {[is_enabled transport]} {
2002         menu .mbar.fetch
2004         menu .mbar.push
2005         .mbar.push add command -label [mc "Push..."] \
2006                 -command do_push_anywhere \
2007                 -accelerator $M1T-P
2008         .mbar.push add command -label [mc "Delete..."] \
2009                 -command remote_branch_delete::dialog
2012 if {[is_MacOSX]} {
2013         # -- Apple Menu (Mac OS X only)
2014         #
2015         .mbar add cascade -label [mc Apple] -menu .mbar.apple
2016         menu .mbar.apple
2018         .mbar.apple add command -label [mc "About %s" [appname]] \
2019                 -command do_about
2020         .mbar.apple add command -label [mc "Options..."] \
2021                 -command do_options
2022 } else {
2023         # -- Edit Menu
2024         #
2025         .mbar.edit add separator
2026         .mbar.edit add command -label [mc "Options..."] \
2027                 -command do_options
2030 # -- Help Menu
2032 .mbar add cascade -label [mc Help] -menu .mbar.help
2033 menu .mbar.help
2035 if {![is_MacOSX]} {
2036         .mbar.help add command -label [mc "About %s" [appname]] \
2037                 -command do_about
2040 set browser {}
2041 catch {set browser $repo_config(instaweb.browser)}
2042 set doc_path [file dirname [gitexec]]
2043 set doc_path [file join $doc_path Documentation index.html]
2045 if {[is_Cygwin]} {
2046         set doc_path [exec cygpath --mixed $doc_path]
2049 if {$browser eq {}} {
2050         if {[is_MacOSX]} {
2051                 set browser open
2052         } elseif {[is_Cygwin]} {
2053                 set program_files [file dirname [exec cygpath --windir]]
2054                 set program_files [file join $program_files {Program Files}]
2055                 set firefox [file join $program_files {Mozilla Firefox} firefox.exe]
2056                 set ie [file join $program_files {Internet Explorer} IEXPLORE.EXE]
2057                 if {[file exists $firefox]} {
2058                         set browser $firefox
2059                 } elseif {[file exists $ie]} {
2060                         set browser $ie
2061                 }
2062                 unset program_files firefox ie
2063         }
2066 if {[file isfile $doc_path]} {
2067         set doc_url "file:$doc_path"
2068 } else {
2069         set doc_url {http://www.kernel.org/pub/software/scm/git/docs/}
2072 if {$browser ne {}} {
2073         .mbar.help add command -label [mc "Online Documentation"] \
2074                 -command [list exec $browser $doc_url &]
2076 unset browser doc_path doc_url
2078 # -- Standard bindings
2080 wm protocol . WM_DELETE_WINDOW do_quit
2081 bind all <$M1B-Key-q> do_quit
2082 bind all <$M1B-Key-Q> do_quit
2083 bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
2084 bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
2086 set subcommand_args {}
2087 proc usage {} {
2088         puts stderr "usage: $::argv0 $::subcommand $::subcommand_args"
2089         exit 1
2092 # -- Not a normal commit type invocation?  Do that instead!
2094 switch -- $subcommand {
2095 browser -
2096 blame {
2097         set subcommand_args {rev? path}
2098         if {$argv eq {}} usage
2099         set head {}
2100         set path {}
2101         set is_path 0
2102         foreach a $argv {
2103                 if {$is_path || [file exists $_prefix$a]} {
2104                         if {$path ne {}} usage
2105                         set path $_prefix$a
2106                         break
2107                 } elseif {$a eq {--}} {
2108                         if {$path ne {}} {
2109                                 if {$head ne {}} usage
2110                                 set head $path
2111                                 set path {}
2112                         }
2113                         set is_path 1
2114                 } elseif {$head eq {}} {
2115                         if {$head ne {}} usage
2116                         set head $a
2117                         set is_path 1
2118                 } else {
2119                         usage
2120                 }
2121         }
2122         unset is_path
2124         if {$head ne {} && $path eq {}} {
2125                 set path $_prefix$head
2126                 set head {}
2127         }
2129         if {$head eq {}} {
2130                 load_current_branch
2131         } else {
2132                 if {[regexp {^[0-9a-f]{1,39}$} $head]} {
2133                         if {[catch {
2134                                         set head [git rev-parse --verify $head]
2135                                 } err]} {
2136                                 puts stderr $err
2137                                 exit 1
2138                         }
2139                 }
2140                 set current_branch $head
2141         }
2143         switch -- $subcommand {
2144         browser {
2145                 if {$head eq {}} {
2146                         if {$path ne {} && [file isdirectory $path]} {
2147                                 set head $current_branch
2148                         } else {
2149                                 set head $path
2150                                 set path {}
2151                         }
2152                 }
2153                 browser::new $head $path
2154         }
2155         blame   {
2156                 if {$head eq {} && ![file exists $path]} {
2157                         puts stderr [mc "fatal: cannot stat path %s: No such file or directory" $path]
2158                         exit 1
2159                 }
2160                 blame::new $head $path
2161         }
2162         }
2163         return
2165 citool -
2166 gui {
2167         if {[llength $argv] != 0} {
2168                 puts -nonewline stderr "usage: $argv0"
2169                 if {$subcommand ne {gui}
2170                         && [file tail $argv0] ne "git-$subcommand"} {
2171                         puts -nonewline stderr " $subcommand"
2172                 }
2173                 puts stderr {}
2174                 exit 1
2175         }
2176         # fall through to setup UI for commits
2178 default {
2179         puts stderr "usage: $argv0 \[{blame|browser|citool}\]"
2180         exit 1
2184 # -- Branch Control
2186 frame .branch \
2187         -borderwidth 1 \
2188         -relief sunken
2189 label .branch.l1 \
2190         -text [mc "Current Branch:"] \
2191         -anchor w \
2192         -justify left
2193 label .branch.cb \
2194         -textvariable current_branch \
2195         -anchor w \
2196         -justify left
2197 pack .branch.l1 -side left
2198 pack .branch.cb -side left -fill x
2199 pack .branch -side top -fill x
2201 # -- Main Window Layout
2203 panedwindow .vpane -orient vertical
2204 panedwindow .vpane.files -orient horizontal
2205 .vpane add .vpane.files -sticky nsew -height 100 -width 200
2206 pack .vpane -anchor n -side top -fill both -expand 1
2208 # -- Index File List
2210 frame .vpane.files.index -height 100 -width 200
2211 label .vpane.files.index.title -text [mc "Staged Changes (Will Be Committed)"] \
2212         -background lightgreen
2213 text $ui_index -background white -borderwidth 0 \
2214         -width 20 -height 10 \
2215         -wrap none \
2216         -cursor $cursor_ptr \
2217         -xscrollcommand {.vpane.files.index.sx set} \
2218         -yscrollcommand {.vpane.files.index.sy set} \
2219         -state disabled
2220 scrollbar .vpane.files.index.sx -orient h -command [list $ui_index xview]
2221 scrollbar .vpane.files.index.sy -orient v -command [list $ui_index yview]
2222 pack .vpane.files.index.title -side top -fill x
2223 pack .vpane.files.index.sx -side bottom -fill x
2224 pack .vpane.files.index.sy -side right -fill y
2225 pack $ui_index -side left -fill both -expand 1
2226 .vpane.files add .vpane.files.index -sticky nsew
2228 # -- Working Directory File List
2230 frame .vpane.files.workdir -height 100 -width 200
2231 label .vpane.files.workdir.title -text [mc "Unstaged Changes (Will Not Be Committed)"] \
2232         -background lightsalmon
2233 text $ui_workdir -background white -borderwidth 0 \
2234         -width 20 -height 10 \
2235         -wrap none \
2236         -cursor $cursor_ptr \
2237         -xscrollcommand {.vpane.files.workdir.sx set} \
2238         -yscrollcommand {.vpane.files.workdir.sy set} \
2239         -state disabled
2240 scrollbar .vpane.files.workdir.sx -orient h -command [list $ui_workdir xview]
2241 scrollbar .vpane.files.workdir.sy -orient v -command [list $ui_workdir yview]
2242 pack .vpane.files.workdir.title -side top -fill x
2243 pack .vpane.files.workdir.sx -side bottom -fill x
2244 pack .vpane.files.workdir.sy -side right -fill y
2245 pack $ui_workdir -side left -fill both -expand 1
2246 .vpane.files add .vpane.files.workdir -sticky nsew
2248 foreach i [list $ui_index $ui_workdir] {
2249         rmsel_tag $i
2250         $i tag conf in_diff -background [$i tag cget in_sel -background]
2252 unset i
2254 # -- Diff and Commit Area
2256 frame .vpane.lower -height 300 -width 400
2257 frame .vpane.lower.commarea
2258 frame .vpane.lower.diff -relief sunken -borderwidth 1
2259 pack .vpane.lower.commarea -side top -fill x
2260 pack .vpane.lower.diff -side bottom -fill both -expand 1
2261 .vpane add .vpane.lower -sticky nsew
2263 # -- Commit Area Buttons
2265 frame .vpane.lower.commarea.buttons
2266 label .vpane.lower.commarea.buttons.l -text {} \
2267         -anchor w \
2268         -justify left
2269 pack .vpane.lower.commarea.buttons.l -side top -fill x
2270 pack .vpane.lower.commarea.buttons -side left -fill y
2272 button .vpane.lower.commarea.buttons.rescan -text [mc Rescan] \
2273         -command do_rescan
2274 pack .vpane.lower.commarea.buttons.rescan -side top -fill x
2275 lappend disable_on_lock \
2276         {.vpane.lower.commarea.buttons.rescan conf -state}
2278 button .vpane.lower.commarea.buttons.incall -text [mc "Stage Changed"] \
2279         -command do_add_all
2280 pack .vpane.lower.commarea.buttons.incall -side top -fill x
2281 lappend disable_on_lock \
2282         {.vpane.lower.commarea.buttons.incall conf -state}
2284 button .vpane.lower.commarea.buttons.signoff -text [mc "Sign Off"] \
2285         -command do_signoff
2286 pack .vpane.lower.commarea.buttons.signoff -side top -fill x
2288 button .vpane.lower.commarea.buttons.commit -text [mc Commit@@verb] \
2289         -command do_commit
2290 pack .vpane.lower.commarea.buttons.commit -side top -fill x
2291 lappend disable_on_lock \
2292         {.vpane.lower.commarea.buttons.commit conf -state}
2294 button .vpane.lower.commarea.buttons.push -text [mc Push] \
2295         -command do_push_anywhere
2296 pack .vpane.lower.commarea.buttons.push -side top -fill x
2298 # -- Commit Message Buffer
2300 frame .vpane.lower.commarea.buffer
2301 frame .vpane.lower.commarea.buffer.header
2302 set ui_comm .vpane.lower.commarea.buffer.t
2303 set ui_coml .vpane.lower.commarea.buffer.header.l
2304 radiobutton .vpane.lower.commarea.buffer.header.new \
2305         -text [mc "New Commit"] \
2306         -command do_select_commit_type \
2307         -variable selected_commit_type \
2308         -value new
2309 lappend disable_on_lock \
2310         [list .vpane.lower.commarea.buffer.header.new conf -state]
2311 radiobutton .vpane.lower.commarea.buffer.header.amend \
2312         -text [mc "Amend Last Commit"] \
2313         -command do_select_commit_type \
2314         -variable selected_commit_type \
2315         -value amend
2316 lappend disable_on_lock \
2317         [list .vpane.lower.commarea.buffer.header.amend conf -state]
2318 label $ui_coml \
2319         -anchor w \
2320         -justify left
2321 proc trace_commit_type {varname args} {
2322         global ui_coml commit_type
2323         switch -glob -- $commit_type {
2324         initial       {set txt [mc "Initial Commit Message:"]}
2325         amend         {set txt [mc "Amended Commit Message:"]}
2326         amend-initial {set txt [mc "Amended Initial Commit Message:"]}
2327         amend-merge   {set txt [mc "Amended Merge Commit Message:"]}
2328         merge         {set txt [mc "Merge Commit Message:"]}
2329         *             {set txt [mc "Commit Message:"]}
2330         }
2331         $ui_coml conf -text $txt
2333 trace add variable commit_type write trace_commit_type
2334 pack $ui_coml -side left -fill x
2335 pack .vpane.lower.commarea.buffer.header.amend -side right
2336 pack .vpane.lower.commarea.buffer.header.new -side right
2338 text $ui_comm -background white -borderwidth 1 \
2339         -undo true \
2340         -maxundo 20 \
2341         -autoseparators true \
2342         -relief sunken \
2343         -width 75 -height 9 -wrap none \
2344         -font font_diff \
2345         -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
2346 scrollbar .vpane.lower.commarea.buffer.sby \
2347         -command [list $ui_comm yview]
2348 pack .vpane.lower.commarea.buffer.header -side top -fill x
2349 pack .vpane.lower.commarea.buffer.sby -side right -fill y
2350 pack $ui_comm -side left -fill y
2351 pack .vpane.lower.commarea.buffer -side left -fill y
2353 # -- Commit Message Buffer Context Menu
2355 set ctxm .vpane.lower.commarea.buffer.ctxm
2356 menu $ctxm -tearoff 0
2357 $ctxm add command \
2358         -label [mc Cut] \
2359         -command {tk_textCut $ui_comm}
2360 $ctxm add command \
2361         -label [mc Copy] \
2362         -command {tk_textCopy $ui_comm}
2363 $ctxm add command \
2364         -label [mc Paste] \
2365         -command {tk_textPaste $ui_comm}
2366 $ctxm add command \
2367         -label [mc Delete] \
2368         -command {$ui_comm delete sel.first sel.last}
2369 $ctxm add separator
2370 $ctxm add command \
2371         -label [mc "Select All"] \
2372         -command {focus $ui_comm;$ui_comm tag add sel 0.0 end}
2373 $ctxm add command \
2374         -label [mc "Copy All"] \
2375         -command {
2376                 $ui_comm tag add sel 0.0 end
2377                 tk_textCopy $ui_comm
2378                 $ui_comm tag remove sel 0.0 end
2379         }
2380 $ctxm add separator
2381 $ctxm add command \
2382         -label [mc "Sign Off"] \
2383         -command do_signoff
2384 bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
2386 # -- Diff Header
2388 proc trace_current_diff_path {varname args} {
2389         global current_diff_path diff_actions file_states
2390         if {$current_diff_path eq {}} {
2391                 set s {}
2392                 set f {}
2393                 set p {}
2394                 set o disabled
2395         } else {
2396                 set p $current_diff_path
2397                 set s [mapdesc [lindex $file_states($p) 0] $p]
2398                 set f [mc "File:"]
2399                 set p [escape_path $p]
2400                 set o normal
2401         }
2403         .vpane.lower.diff.header.status configure -text $s
2404         .vpane.lower.diff.header.file configure -text $f
2405         .vpane.lower.diff.header.path configure -text $p
2406         foreach w $diff_actions {
2407                 uplevel #0 $w $o
2408         }
2410 trace add variable current_diff_path write trace_current_diff_path
2412 frame .vpane.lower.diff.header -background gold
2413 label .vpane.lower.diff.header.status \
2414         -background gold \
2415         -width $max_status_desc \
2416         -anchor w \
2417         -justify left
2418 label .vpane.lower.diff.header.file \
2419         -background gold \
2420         -anchor w \
2421         -justify left
2422 label .vpane.lower.diff.header.path \
2423         -background gold \
2424         -anchor w \
2425         -justify left
2426 pack .vpane.lower.diff.header.status -side left
2427 pack .vpane.lower.diff.header.file -side left
2428 pack .vpane.lower.diff.header.path -fill x
2429 set ctxm .vpane.lower.diff.header.ctxm
2430 menu $ctxm -tearoff 0
2431 $ctxm add command \
2432         -label [mc Copy] \
2433         -command {
2434                 clipboard clear
2435                 clipboard append \
2436                         -format STRING \
2437                         -type STRING \
2438                         -- $current_diff_path
2439         }
2440 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2441 bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
2443 # -- Diff Body
2445 frame .vpane.lower.diff.body
2446 set ui_diff .vpane.lower.diff.body.t
2447 text $ui_diff -background white -borderwidth 0 \
2448         -width 80 -height 15 -wrap none \
2449         -font font_diff \
2450         -xscrollcommand {.vpane.lower.diff.body.sbx set} \
2451         -yscrollcommand {.vpane.lower.diff.body.sby set} \
2452         -state disabled
2453 scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
2454         -command [list $ui_diff xview]
2455 scrollbar .vpane.lower.diff.body.sby -orient vertical \
2456         -command [list $ui_diff yview]
2457 pack .vpane.lower.diff.body.sbx -side bottom -fill x
2458 pack .vpane.lower.diff.body.sby -side right -fill y
2459 pack $ui_diff -side left -fill both -expand 1
2460 pack .vpane.lower.diff.header -side top -fill x
2461 pack .vpane.lower.diff.body -side bottom -fill both -expand 1
2463 $ui_diff tag conf d_cr -elide true
2464 $ui_diff tag conf d_@ -foreground blue -font font_diffbold
2465 $ui_diff tag conf d_+ -foreground {#00a000}
2466 $ui_diff tag conf d_- -foreground red
2468 $ui_diff tag conf d_++ -foreground {#00a000}
2469 $ui_diff tag conf d_-- -foreground red
2470 $ui_diff tag conf d_+s \
2471         -foreground {#00a000} \
2472         -background {#e2effa}
2473 $ui_diff tag conf d_-s \
2474         -foreground red \
2475         -background {#e2effa}
2476 $ui_diff tag conf d_s+ \
2477         -foreground {#00a000} \
2478         -background ivory1
2479 $ui_diff tag conf d_s- \
2480         -foreground red \
2481         -background ivory1
2483 $ui_diff tag conf d<<<<<<< \
2484         -foreground orange \
2485         -font font_diffbold
2486 $ui_diff tag conf d======= \
2487         -foreground orange \
2488         -font font_diffbold
2489 $ui_diff tag conf d>>>>>>> \
2490         -foreground orange \
2491         -font font_diffbold
2493 $ui_diff tag raise sel
2495 # -- Diff Body Context Menu
2497 set ctxm .vpane.lower.diff.body.ctxm
2498 menu $ctxm -tearoff 0
2499 $ctxm add command \
2500         -label [mc Refresh] \
2501         -command reshow_diff
2502 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2503 $ctxm add command \
2504         -label [mc Copy] \
2505         -command {tk_textCopy $ui_diff}
2506 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2507 $ctxm add command \
2508         -label [mc "Select All"] \
2509         -command {focus $ui_diff;$ui_diff tag add sel 0.0 end}
2510 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2511 $ctxm add command \
2512         -label [mc "Copy All"] \
2513         -command {
2514                 $ui_diff tag add sel 0.0 end
2515                 tk_textCopy $ui_diff
2516                 $ui_diff tag remove sel 0.0 end
2517         }
2518 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2519 $ctxm add separator
2520 $ctxm add command \
2521         -label [mc "Apply/Reverse Hunk"] \
2522         -command {apply_hunk $cursorX $cursorY}
2523 set ui_diff_applyhunk [$ctxm index last]
2524 lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state]
2525 $ctxm add separator
2526 $ctxm add command \
2527         -label [mc "Decrease Font Size"] \
2528         -command {incr_font_size font_diff -1}
2529 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2530 $ctxm add command \
2531         -label [mc "Increase Font Size"] \
2532         -command {incr_font_size font_diff 1}
2533 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2534 $ctxm add separator
2535 $ctxm add command \
2536         -label [mc "Show Less Context"] \
2537         -command {if {$repo_config(gui.diffcontext) >= 1} {
2538                 incr repo_config(gui.diffcontext) -1
2539                 reshow_diff
2540         }}
2541 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2542 $ctxm add command \
2543         -label [mc "Show More Context"] \
2544         -command {if {$repo_config(gui.diffcontext) < 99} {
2545                 incr repo_config(gui.diffcontext)
2546                 reshow_diff
2547         }}
2548 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2549 $ctxm add separator
2550 $ctxm add command -label [mc "Options..."] \
2551         -command do_options
2552 proc popup_diff_menu {ctxm x y X Y} {
2553         global current_diff_path file_states
2554         set ::cursorX $x
2555         set ::cursorY $y
2556         if {$::ui_index eq $::current_diff_side} {
2557                 set l [mc "Unstage Hunk From Commit"]
2558         } else {
2559                 set l [mc "Stage Hunk For Commit"]
2560         }
2561         if {$::is_3way_diff
2562                 || $current_diff_path eq {}
2563                 || ![info exists file_states($current_diff_path)]
2564                 || {_O} eq [lindex $file_states($current_diff_path) 0]} {
2565                 set s disabled
2566         } else {
2567                 set s normal
2568         }
2569         $ctxm entryconf $::ui_diff_applyhunk -state $s -label $l
2570         tk_popup $ctxm $X $Y
2572 bind_button3 $ui_diff [list popup_diff_menu $ctxm %x %y %X %Y]
2574 # -- Status Bar
2576 set main_status [::status_bar::new .status]
2577 pack .status -anchor w -side bottom -fill x
2578 $main_status show [mc "Initializing..."]
2580 # -- Load geometry
2582 catch {
2583 set gm $repo_config(gui.geometry)
2584 wm geometry . [lindex $gm 0]
2585 .vpane sash place 0 \
2586         [lindex [.vpane sash coord 0] 0] \
2587         [lindex $gm 1]
2588 .vpane.files sash place 0 \
2589         [lindex $gm 2] \
2590         [lindex [.vpane.files sash coord 0] 1]
2591 unset gm
2594 # -- Key Bindings
2596 bind $ui_comm <$M1B-Key-Return> {do_commit;break}
2597 bind $ui_comm <$M1B-Key-i> {do_add_all;break}
2598 bind $ui_comm <$M1B-Key-I> {do_add_all;break}
2599 bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
2600 bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
2601 bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
2602 bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
2603 bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
2604 bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
2605 bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
2606 bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
2608 bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
2609 bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
2610 bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
2611 bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
2612 bind $ui_diff <$M1B-Key-v> {break}
2613 bind $ui_diff <$M1B-Key-V> {break}
2614 bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
2615 bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
2616 bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
2617 bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
2618 bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
2619 bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
2620 bind $ui_diff <Key-k>         {catch {%W yview scroll -1 units};break}
2621 bind $ui_diff <Key-j>         {catch {%W yview scroll  1 units};break}
2622 bind $ui_diff <Key-h>         {catch {%W xview scroll -1 units};break}
2623 bind $ui_diff <Key-l>         {catch {%W xview scroll  1 units};break}
2624 bind $ui_diff <Control-Key-b> {catch {%W yview scroll -1 pages};break}
2625 bind $ui_diff <Control-Key-f> {catch {%W yview scroll  1 pages};break}
2626 bind $ui_diff <Button-1>   {focus %W}
2628 if {[is_enabled branch]} {
2629         bind . <$M1B-Key-n> branch_create::dialog
2630         bind . <$M1B-Key-N> branch_create::dialog
2631         bind . <$M1B-Key-o> branch_checkout::dialog
2632         bind . <$M1B-Key-O> branch_checkout::dialog
2633         bind . <$M1B-Key-m> merge::dialog
2634         bind . <$M1B-Key-M> merge::dialog
2636 if {[is_enabled transport]} {
2637         bind . <$M1B-Key-p> do_push_anywhere
2638         bind . <$M1B-Key-P> do_push_anywhere
2641 bind .   <Key-F5>     do_rescan
2642 bind .   <$M1B-Key-r> do_rescan
2643 bind .   <$M1B-Key-R> do_rescan
2644 bind .   <$M1B-Key-s> do_signoff
2645 bind .   <$M1B-Key-S> do_signoff
2646 bind .   <$M1B-Key-i> do_add_all
2647 bind .   <$M1B-Key-I> do_add_all
2648 bind .   <$M1B-Key-Return> do_commit
2649 foreach i [list $ui_index $ui_workdir] {
2650         bind $i <Button-1>       "toggle_or_diff         $i %x %y; break"
2651         bind $i <$M1B-Button-1>  "add_one_to_selection   $i %x %y; break"
2652         bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
2654 unset i
2656 set file_lists($ui_index) [list]
2657 set file_lists($ui_workdir) [list]
2659 wm title . "[appname] ([reponame]) [file normalize [file dirname [gitdir]]]"
2660 focus -force $ui_comm
2662 # -- Warn the user about environmental problems.  Cygwin's Tcl
2663 #    does *not* pass its env array onto any processes it spawns.
2664 #    This means that git processes get none of our environment.
2666 if {[is_Cygwin]} {
2667         set ignored_env 0
2668         set suggest_user {}
2669         set msg [mc "Possible environment issues exist.
2671 The following environment variables are probably
2672 going to be ignored by any Git subprocess run
2673 by %s:
2675 " [appname]]
2676         foreach name [array names env] {
2677                 switch -regexp -- $name {
2678                 {^GIT_INDEX_FILE$} -
2679                 {^GIT_OBJECT_DIRECTORY$} -
2680                 {^GIT_ALTERNATE_OBJECT_DIRECTORIES$} -
2681                 {^GIT_DIFF_OPTS$} -
2682                 {^GIT_EXTERNAL_DIFF$} -
2683                 {^GIT_PAGER$} -
2684                 {^GIT_TRACE$} -
2685                 {^GIT_CONFIG$} -
2686                 {^GIT_CONFIG_LOCAL$} -
2687                 {^GIT_(AUTHOR|COMMITTER)_DATE$} {
2688                         append msg " - $name\n"
2689                         incr ignored_env
2690                 }
2691                 {^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$} {
2692                         append msg " - $name\n"
2693                         incr ignored_env
2694                         set suggest_user $name
2695                 }
2696                 }
2697         }
2698         if {$ignored_env > 0} {
2699                 append msg [mc "
2700 This is due to a known issue with the
2701 Tcl binary distributed by Cygwin."]
2703                 if {$suggest_user ne {}} {
2704                         append msg [mc "
2706 A good replacement for %s
2707 is placing values for the user.name and
2708 user.email settings into your personal
2709 ~/.gitconfig file.
2710 " $suggest_user]
2711                 }
2712                 warn_popup $msg
2713         }
2714         unset ignored_env msg suggest_user name
2717 # -- Only initialize complex UI if we are going to stay running.
2719 if {[is_enabled transport]} {
2720         load_all_remotes
2722         populate_fetch_menu
2723         populate_push_menu
2726 if {[winfo exists $ui_comm]} {
2727         set GITGUI_BCK_exists [load_message GITGUI_BCK]
2729         # -- If both our backup and message files exist use the
2730         #    newer of the two files to initialize the buffer.
2731         #
2732         if {$GITGUI_BCK_exists} {
2733                 set m [gitdir GITGUI_MSG]
2734                 if {[file isfile $m]} {
2735                         if {[file mtime [gitdir GITGUI_BCK]] > [file mtime $m]} {
2736                                 catch {file delete [gitdir GITGUI_MSG]}
2737                         } else {
2738                                 $ui_comm delete 0.0 end
2739                                 $ui_comm edit reset
2740                                 $ui_comm edit modified false
2741                                 catch {file delete [gitdir GITGUI_BCK]}
2742                                 set GITGUI_BCK_exists 0
2743                         }
2744                 }
2745                 unset m
2746         }
2748         proc backup_commit_buffer {} {
2749                 global ui_comm GITGUI_BCK_exists
2751                 set m [$ui_comm edit modified]
2752                 if {$m || $GITGUI_BCK_exists} {
2753                         set msg [string trim [$ui_comm get 0.0 end]]
2754                         regsub -all -line {[ \r\t]+$} $msg {} msg
2756                         if {$msg eq {}} {
2757                                 if {$GITGUI_BCK_exists} {
2758                                         catch {file delete [gitdir GITGUI_BCK]}
2759                                         set GITGUI_BCK_exists 0
2760                                 }
2761                         } elseif {$m} {
2762                                 catch {
2763                                         set fd [open [gitdir GITGUI_BCK] w]
2764                                         puts -nonewline $fd $msg
2765                                         close $fd
2766                                         set GITGUI_BCK_exists 1
2767                                 }
2768                         }
2770                         $ui_comm edit modified false
2771                 }
2773                 set ::GITGUI_BCK_i [after 2000 backup_commit_buffer]
2774         }
2776         backup_commit_buffer
2779 lock_index begin-read
2780 if {![winfo ismapped .]} {
2781         wm deiconify .
2783 after 1 do_rescan
2784 if {[is_enabled multicommit]} {
2785         after 1000 hint_gc