Code

212a093118e42d3e31f0e5c25df2eb5655b88a47
[git.git] / git-gui.sh
1 #!/bin/sh
2 # Tcl ignores the next line -*- tcl -*- \
3 exec wish "$0" -- "$@"
5 set copyright {
6 Copyright © 2006, 2007 Shawn Pearce, Paul Mackerras.
8 All rights reserved.
10 This program is free software; it may be used, copied, modified
11 and distributed under the terms of the GNU General Public Licence,
12 either version 2, or (at your option) any later version.}
14 set appvers {@@GITGUI_VERSION@@}
15 set appname [lindex [file split $argv0] end]
16 set gitdir {}
18 ######################################################################
19 ##
20 ## config
22 proc is_many_config {name} {
23         switch -glob -- $name {
24         remote.*.fetch -
25         remote.*.push
26                 {return 1}
27         *
28                 {return 0}
29         }
30 }
32 proc load_config {include_global} {
33         global repo_config global_config default_config
35         array unset global_config
36         if {$include_global} {
37                 catch {
38                         set fd_rc [open "| git repo-config --global --list" r]
39                         while {[gets $fd_rc line] >= 0} {
40                                 if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
41                                         if {[is_many_config $name]} {
42                                                 lappend global_config($name) $value
43                                         } else {
44                                                 set global_config($name) $value
45                                         }
46                                 }
47                         }
48                         close $fd_rc
49                 }
50         }
52         array unset repo_config
53         catch {
54                 set fd_rc [open "| git repo-config --list" r]
55                 while {[gets $fd_rc line] >= 0} {
56                         if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
57                                 if {[is_many_config $name]} {
58                                         lappend repo_config($name) $value
59                                 } else {
60                                         set repo_config($name) $value
61                                 }
62                         }
63                 }
64                 close $fd_rc
65         }
67         foreach name [array names default_config] {
68                 if {[catch {set v $global_config($name)}]} {
69                         set global_config($name) $default_config($name)
70                 }
71                 if {[catch {set v $repo_config($name)}]} {
72                         set repo_config($name) $default_config($name)
73                 }
74         }
75 }
77 proc save_config {} {
78         global default_config font_descs
79         global repo_config global_config
80         global repo_config_new global_config_new
82         foreach option $font_descs {
83                 set name [lindex $option 0]
84                 set font [lindex $option 1]
85                 font configure $font \
86                         -family $global_config_new(gui.$font^^family) \
87                         -size $global_config_new(gui.$font^^size)
88                 font configure ${font}bold \
89                         -family $global_config_new(gui.$font^^family) \
90                         -size $global_config_new(gui.$font^^size)
91                 set global_config_new(gui.$name) [font configure $font]
92                 unset global_config_new(gui.$font^^family)
93                 unset global_config_new(gui.$font^^size)
94         }
96         foreach name [array names default_config] {
97                 set value $global_config_new($name)
98                 if {$value ne $global_config($name)} {
99                         if {$value eq $default_config($name)} {
100                                 catch {exec git repo-config --global --unset $name}
101                         } else {
102                                 regsub -all "\[{}\]" $value {"} value
103                                 exec git repo-config --global $name $value
104                         }
105                         set global_config($name) $value
106                         if {$value eq $repo_config($name)} {
107                                 catch {exec git repo-config --unset $name}
108                                 set repo_config($name) $value
109                         }
110                 }
111         }
113         foreach name [array names default_config] {
114                 set value $repo_config_new($name)
115                 if {$value ne $repo_config($name)} {
116                         if {$value eq $global_config($name)} {
117                                 catch {exec git repo-config --unset $name}
118                         } else {
119                                 regsub -all "\[{}\]" $value {"} value
120                                 exec git repo-config $name $value
121                         }
122                         set repo_config($name) $value
123                 }
124         }
127 proc error_popup {msg} {
128         global gitdir appname
130         set title $appname
131         if {$gitdir ne {}} {
132                 append title { (}
133                 append title [lindex \
134                         [file split [file normalize [file dirname $gitdir]]] \
135                         end]
136                 append title {)}
137         }
138         set cmd [list tk_messageBox \
139                 -icon error \
140                 -type ok \
141                 -title "$title: error" \
142                 -message $msg]
143         if {[winfo ismapped .]} {
144                 lappend cmd -parent .
145         }
146         eval $cmd
149 proc warn_popup {msg} {
150         global gitdir appname
152         set title $appname
153         if {$gitdir ne {}} {
154                 append title { (}
155                 append title [lindex \
156                         [file split [file normalize [file dirname $gitdir]]] \
157                         end]
158                 append title {)}
159         }
160         set cmd [list tk_messageBox \
161                 -icon warning \
162                 -type ok \
163                 -title "$title: warning" \
164                 -message $msg]
165         if {[winfo ismapped .]} {
166                 lappend cmd -parent .
167         }
168         eval $cmd
171 proc info_popup {msg} {
172         global gitdir appname
174         set title $appname
175         if {$gitdir ne {}} {
176                 append title { (}
177                 append title [lindex \
178                         [file split [file normalize [file dirname $gitdir]]] \
179                         end]
180                 append title {)}
181         }
182         tk_messageBox \
183                 -parent . \
184                 -icon info \
185                 -type ok \
186                 -title $title \
187                 -message $msg
190 ######################################################################
191 ##
192 ## repository setup
194 if {   [catch {set gitdir $env(GIT_DIR)}]
195         && [catch {set gitdir [exec git rev-parse --git-dir]} err]} {
196         catch {wm withdraw .}
197         error_popup "Cannot find the git directory:\n\n$err"
198         exit 1
200 if {![file isdirectory $gitdir]} {
201         catch {wm withdraw .}
202         error_popup "Git directory not found:\n\n$gitdir"
203         exit 1
205 if {[lindex [file split $gitdir] end] ne {.git}} {
206         catch {wm withdraw .}
207         error_popup "Cannot use funny .git directory:\n\n$gitdir"
208         exit 1
210 if {[catch {cd [file dirname $gitdir]} err]} {
211         catch {wm withdraw .}
212         error_popup "No working directory [file dirname $gitdir]:\n\n$err"
213         exit 1
216 set single_commit 0
217 if {$appname eq {git-citool}} {
218         set single_commit 1
221 ######################################################################
222 ##
223 ## task management
225 set rescan_active 0
226 set diff_active 0
227 set last_clicked {}
229 set disable_on_lock [list]
230 set index_lock_type none
232 proc lock_index {type} {
233         global index_lock_type disable_on_lock
235         if {$index_lock_type eq {none}} {
236                 set index_lock_type $type
237                 foreach w $disable_on_lock {
238                         uplevel #0 $w disabled
239                 }
240                 return 1
241         } elseif {$index_lock_type eq "begin-$type"} {
242                 set index_lock_type $type
243                 return 1
244         }
245         return 0
248 proc unlock_index {} {
249         global index_lock_type disable_on_lock
251         set index_lock_type none
252         foreach w $disable_on_lock {
253                 uplevel #0 $w normal
254         }
257 ######################################################################
258 ##
259 ## status
261 proc repository_state {ctvar hdvar mhvar} {
262         global gitdir current_branch
263         upvar $ctvar ct $hdvar hd $mhvar mh
265         set mh [list]
267         if {[catch {set current_branch [exec git symbolic-ref HEAD]}]} {
268                 set current_branch {}
269         } else {
270                 regsub ^refs/((heads|tags|remotes)/)? \
271                         $current_branch \
272                         {} \
273                         current_branch
274         }
276         if {[catch {set hd [exec git rev-parse --verify HEAD]}]} {
277                 set hd {}
278                 set ct initial
279                 return
280         }
282         set merge_head [file join $gitdir MERGE_HEAD]
283         if {[file exists $merge_head]} {
284                 set ct merge
285                 set fd_mh [open $merge_head r]
286                 while {[gets $fd_mh line] >= 0} {
287                         lappend mh $line
288                 }
289                 close $fd_mh
290                 return
291         }
293         set ct normal
296 proc PARENT {} {
297         global PARENT empty_tree
299         set p [lindex $PARENT 0]
300         if {$p ne {}} {
301                 return $p
302         }
303         if {$empty_tree eq {}} {
304                 set empty_tree [exec git mktree << {}]
305         }
306         return $empty_tree
309 proc rescan {after} {
310         global HEAD PARENT MERGE_HEAD commit_type
311         global ui_index ui_other ui_status_value ui_comm
312         global rescan_active file_states
313         global repo_config
315         if {$rescan_active > 0 || ![lock_index read]} return
317         repository_state newType newHEAD newMERGE_HEAD
318         if {[string match amend* $commit_type]
319                 && $newType eq {normal}
320                 && $newHEAD eq $HEAD} {
321         } else {
322                 set HEAD $newHEAD
323                 set PARENT $newHEAD
324                 set MERGE_HEAD $newMERGE_HEAD
325                 set commit_type $newType
326         }
328         array unset file_states
330         if {![$ui_comm edit modified]
331                 || [string trim [$ui_comm get 0.0 end]] eq {}} {
332                 if {[load_message GITGUI_MSG]} {
333                 } elseif {[load_message MERGE_MSG]} {
334                 } elseif {[load_message SQUASH_MSG]} {
335                 }
336                 $ui_comm edit reset
337                 $ui_comm edit modified false
338         }
340         if {$repo_config(gui.trustmtime) eq {true}} {
341                 rescan_stage2 {} $after
342         } else {
343                 set rescan_active 1
344                 set ui_status_value {Refreshing file status...}
345                 set cmd [list git update-index]
346                 lappend cmd -q
347                 lappend cmd --unmerged
348                 lappend cmd --ignore-missing
349                 lappend cmd --refresh
350                 set fd_rf [open "| $cmd" r]
351                 fconfigure $fd_rf -blocking 0 -translation binary
352                 fileevent $fd_rf readable \
353                         [list rescan_stage2 $fd_rf $after]
354         }
357 proc rescan_stage2 {fd after} {
358         global gitdir ui_status_value
359         global rescan_active buf_rdi buf_rdf buf_rlo
361         if {$fd ne {}} {
362                 read $fd
363                 if {![eof $fd]} return
364                 close $fd
365         }
367         set ls_others [list | git ls-files --others -z \
368                 --exclude-per-directory=.gitignore]
369         set info_exclude [file join $gitdir info exclude]
370         if {[file readable $info_exclude]} {
371                 lappend ls_others "--exclude-from=$info_exclude"
372         }
374         set buf_rdi {}
375         set buf_rdf {}
376         set buf_rlo {}
378         set rescan_active 3
379         set ui_status_value {Scanning for modified files ...}
380         set fd_di [open "| git diff-index --cached -z [PARENT]" r]
381         set fd_df [open "| git diff-files -z" r]
382         set fd_lo [open $ls_others r]
384         fconfigure $fd_di -blocking 0 -translation binary
385         fconfigure $fd_df -blocking 0 -translation binary
386         fconfigure $fd_lo -blocking 0 -translation binary
387         fileevent $fd_di readable [list read_diff_index $fd_di $after]
388         fileevent $fd_df readable [list read_diff_files $fd_df $after]
389         fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
392 proc load_message {file} {
393         global gitdir ui_comm
395         set f [file join $gitdir $file]
396         if {[file isfile $f]} {
397                 if {[catch {set fd [open $f r]}]} {
398                         return 0
399                 }
400                 set content [string trim [read $fd]]
401                 close $fd
402                 $ui_comm delete 0.0 end
403                 $ui_comm insert end $content
404                 return 1
405         }
406         return 0
409 proc read_diff_index {fd after} {
410         global buf_rdi
412         append buf_rdi [read $fd]
413         set c 0
414         set n [string length $buf_rdi]
415         while {$c < $n} {
416                 set z1 [string first "\0" $buf_rdi $c]
417                 if {$z1 == -1} break
418                 incr z1
419                 set z2 [string first "\0" $buf_rdi $z1]
420                 if {$z2 == -1} break
422                 incr c
423                 set i [split [string range $buf_rdi $c [expr {$z1 - 2}]] { }]
424                 merge_state \
425                         [string range $buf_rdi $z1 [expr {$z2 - 1}]] \
426                         [lindex $i 4]? \
427                         [list [lindex $i 0] [lindex $i 2]] \
428                         [list]
429                 set c $z2
430                 incr c
431         }
432         if {$c < $n} {
433                 set buf_rdi [string range $buf_rdi $c end]
434         } else {
435                 set buf_rdi {}
436         }
438         rescan_done $fd buf_rdi $after
441 proc read_diff_files {fd after} {
442         global buf_rdf
444         append buf_rdf [read $fd]
445         set c 0
446         set n [string length $buf_rdf]
447         while {$c < $n} {
448                 set z1 [string first "\0" $buf_rdf $c]
449                 if {$z1 == -1} break
450                 incr z1
451                 set z2 [string first "\0" $buf_rdf $z1]
452                 if {$z2 == -1} break
454                 incr c
455                 set i [split [string range $buf_rdf $c [expr {$z1 - 2}]] { }]
456                 merge_state \
457                         [string range $buf_rdf $z1 [expr {$z2 - 1}]] \
458                         ?[lindex $i 4] \
459                         [list] \
460                         [list [lindex $i 0] [lindex $i 2]]
461                 set c $z2
462                 incr c
463         }
464         if {$c < $n} {
465                 set buf_rdf [string range $buf_rdf $c end]
466         } else {
467                 set buf_rdf {}
468         }
470         rescan_done $fd buf_rdf $after
473 proc read_ls_others {fd after} {
474         global buf_rlo
476         append buf_rlo [read $fd]
477         set pck [split $buf_rlo "\0"]
478         set buf_rlo [lindex $pck end]
479         foreach p [lrange $pck 0 end-1] {
480                 merge_state $p ?O
481         }
482         rescan_done $fd buf_rlo $after
485 proc rescan_done {fd buf after} {
486         global rescan_active
487         global file_states repo_config
488         upvar $buf to_clear
490         if {![eof $fd]} return
491         set to_clear {}
492         close $fd
493         if {[incr rescan_active -1] > 0} return
495         prune_selection
496         unlock_index
497         display_all_files
499         if {$repo_config(gui.partialinclude) ne {true}} {
500                 set pathList [list]
501                 foreach path [array names file_states] {
502                         switch -- [lindex $file_states($path) 0] {
503                         A? -
504                         M? {lappend pathList $path}
505                         }
506                 }
507                 if {$pathList ne {}} {
508                         update_index \
509                                 "Updating included files" \
510                                 $pathList \
511                                 [concat {reshow_diff;} $after]
512                         return
513                 }
514         }
516         reshow_diff
517         uplevel #0 $after
520 proc prune_selection {} {
521         global file_states selected_paths
523         foreach path [array names selected_paths] {
524                 if {[catch {set still_here $file_states($path)}]} {
525                         unset selected_paths($path)
526                 }
527         }
530 ######################################################################
531 ##
532 ## diff
534 proc clear_diff {} {
535         global ui_diff current_diff ui_index ui_other
537         $ui_diff conf -state normal
538         $ui_diff delete 0.0 end
539         $ui_diff conf -state disabled
541         set current_diff {}
543         $ui_index tag remove in_diff 0.0 end
544         $ui_other tag remove in_diff 0.0 end
547 proc reshow_diff {} {
548         global current_diff ui_status_value file_states
550         if {$current_diff eq {}
551                 || [catch {set s $file_states($current_diff)}]} {
552                 clear_diff
553         } else {
554                 show_diff $current_diff
555         }
558 proc handle_empty_diff {} {
559         global current_diff file_states file_lists
561         set path $current_diff
562         set s $file_states($path)
563         if {[lindex $s 0] ne {_M}} return
565         info_popup "No differences detected.
567 [short_path $path] has no changes.
569 The modification date of this file was updated
570 by another application and you currently have
571 the Trust File Modification Timestamps option
572 enabled, so Git did not automatically detect
573 that there are no content differences in this
574 file.
576 This file will now be removed from the modified
577 files list, to prevent possible confusion.
579         if {[catch {exec git update-index -- $path} err]} {
580                 error_popup "Failed to refresh index:\n\n$err"
581         }
583         clear_diff
584         set old_w [mapcol [lindex $file_states($path) 0] $path]
585         set lno [lsearch -sorted $file_lists($old_w) $path]
586         if {$lno >= 0} {
587                 set file_lists($old_w) \
588                         [lreplace $file_lists($old_w) $lno $lno]
589                 incr lno
590                 $old_w conf -state normal
591                 $old_w delete $lno.0 [expr {$lno + 1}].0
592                 $old_w conf -state disabled
593         }
596 proc show_diff {path {w {}} {lno {}}} {
597         global file_states file_lists
598         global is_3way_diff diff_active repo_config
599         global ui_diff current_diff ui_status_value
601         if {$diff_active || ![lock_index read]} return
603         clear_diff
604         if {$w eq {} || $lno == {}} {
605                 foreach w [array names file_lists] {
606                         set lno [lsearch -sorted $file_lists($w) $path]
607                         if {$lno >= 0} {
608                                 incr lno
609                                 break
610                         }
611                 }
612         }
613         if {$w ne {} && $lno >= 1} {
614                 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
615         }
617         set s $file_states($path)
618         set m [lindex $s 0]
619         set is_3way_diff 0
620         set diff_active 1
621         set current_diff $path
622         set ui_status_value "Loading diff of [escape_path $path]..."
624         set cmd [list | git diff-index]
625         lappend cmd --no-color
626         if {$repo_config(gui.diffcontext) > 0} {
627                 lappend cmd "-U$repo_config(gui.diffcontext)"
628         }
629         lappend cmd -p
631         switch $m {
632         MM {
633                 lappend cmd -c
634         }
635         _O {
636                 if {[catch {
637                                 set fd [open $path r]
638                                 set content [read $fd]
639                                 close $fd
640                         } err ]} {
641                         set diff_active 0
642                         unlock_index
643                         set ui_status_value "Unable to display [escape_path $path]"
644                         error_popup "Error loading file:\n\n$err"
645                         return
646                 }
647                 $ui_diff conf -state normal
648                 $ui_diff insert end $content
649                 $ui_diff conf -state disabled
650                 set diff_active 0
651                 unlock_index
652                 set ui_status_value {Ready.}
653                 return
654         }
655         }
657         lappend cmd [PARENT]
658         lappend cmd --
659         lappend cmd $path
661         if {[catch {set fd [open $cmd r]} err]} {
662                 set diff_active 0
663                 unlock_index
664                 set ui_status_value "Unable to display [escape_path $path]"
665                 error_popup "Error loading diff:\n\n$err"
666                 return
667         }
669         fconfigure $fd -blocking 0 -translation auto
670         fileevent $fd readable [list read_diff $fd]
673 proc read_diff {fd} {
674         global ui_diff ui_status_value is_3way_diff diff_active
675         global repo_config
677         $ui_diff conf -state normal
678         while {[gets $fd line] >= 0} {
679                 # -- Cleanup uninteresting diff header lines.
680                 #
681                 if {[string match {diff --git *}      $line]} continue
682                 if {[string match {diff --combined *} $line]} continue
683                 if {[string match {--- *}             $line]} continue
684                 if {[string match {+++ *}             $line]} continue
685                 if {$line eq {deleted file mode 120000}} {
686                         set line "deleted symlink"
687                 }
689                 # -- Automatically detect if this is a 3 way diff.
690                 #
691                 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
693                 # -- Reformat a 3 way diff, 'cause its too weird.
694                 #
695                 if {$is_3way_diff} {
696                         set op [string range $line 0 1]
697                         switch -- $op {
698                         {@@} {set tags d_@}
699                         {++} {set tags d_+ ; set op { +}}
700                         {--} {set tags d_- ; set op { -}}
701                         { +} {set tags d_++; set op {++}}
702                         { -} {set tags d_--; set op {--}}
703                         {+ } {set tags d_-+; set op {-+}}
704                         {- } {set tags d_+-; set op {+-}}
705                         default {set tags {}}
706                         }
707                         set line [string replace $line 0 1 $op]
708                 } else {
709                         switch -- [string index $line 0] {
710                         @ {set tags d_@}
711                         + {set tags d_+}
712                         - {set tags d_-}
713                         default {set tags {}}
714                         }
715                 }
716                 $ui_diff insert end $line $tags
717                 $ui_diff insert end "\n" $tags
718         }
719         $ui_diff conf -state disabled
721         if {[eof $fd]} {
722                 close $fd
723                 set diff_active 0
724                 unlock_index
725                 set ui_status_value {Ready.}
727                 if {$repo_config(gui.trustmtime) eq {true}
728                         && [$ui_diff index end] eq {2.0}} {
729                         handle_empty_diff
730                 }
731         }
734 ######################################################################
735 ##
736 ## commit
738 proc load_last_commit {} {
739         global HEAD PARENT MERGE_HEAD commit_type ui_comm
741         if {[llength $PARENT] == 0} {
742                 error_popup {There is nothing to amend.
744 You are about to create the initial commit.
745 There is no commit before this to amend.
747                 return
748         }
750         repository_state curType curHEAD curMERGE_HEAD
751         if {$curType eq {merge}} {
752                 error_popup {Cannot amend while merging.
754 You are currently in the middle of a merge that
755 has not been fully completed.  You cannot amend
756 the prior commit unless you first abort the
757 current merge activity.
759                 return
760         }
762         set msg {}
763         set parents [list]
764         if {[catch {
765                         set fd [open "| git cat-file commit $curHEAD" r]
766                         while {[gets $fd line] > 0} {
767                                 if {[string match {parent *} $line]} {
768                                         lappend parents [string range $line 7 end]
769                                 }
770                         }
771                         set msg [string trim [read $fd]]
772                         close $fd
773                 } err]} {
774                 error_popup "Error loading commit data for amend:\n\n$err"
775                 return
776         }
778         set HEAD $curHEAD
779         set PARENT $parents
780         set MERGE_HEAD [list]
781         switch -- [llength $parents] {
782         0       {set commit_type amend-initial}
783         1       {set commit_type amend}
784         default {set commit_type amend-merge}
785         }
787         $ui_comm delete 0.0 end
788         $ui_comm insert end $msg
789         $ui_comm edit reset
790         $ui_comm edit modified false
791         rescan {set ui_status_value {Ready.}}
794 proc create_new_commit {} {
795         global commit_type ui_comm
797         set commit_type normal
798         $ui_comm delete 0.0 end
799         $ui_comm edit reset
800         $ui_comm edit modified false
801         rescan {set ui_status_value {Ready.}}
804 set GIT_COMMITTER_IDENT {}
806 proc committer_ident {} {
807         global GIT_COMMITTER_IDENT
809         if {$GIT_COMMITTER_IDENT eq {}} {
810                 if {[catch {set me [exec git var GIT_COMMITTER_IDENT]} err]} {
811                         error_popup "Unable to obtain your identity:\n\n$err"
812                         return {}
813                 }
814                 if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
815                         $me me GIT_COMMITTER_IDENT]} {
816                         error_popup "Invalid GIT_COMMITTER_IDENT:\n\n$me"
817                         return {}
818                 }
819         }
821         return $GIT_COMMITTER_IDENT
824 proc commit_tree {} {
825         global HEAD commit_type file_states ui_comm repo_config
827         if {![lock_index update]} return
828         if {[committer_ident] eq {}} return
830         # -- Our in memory state should match the repository.
831         #
832         repository_state curType curHEAD curMERGE_HEAD
833         if {[string match amend* $commit_type]
834                 && $curType eq {normal}
835                 && $curHEAD eq $HEAD} {
836         } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
837                 info_popup {Last scanned state does not match repository state.
839 Another Git program has modified this repository
840 since the last scan.  A rescan must be performed
841 before another commit can be created.
843 The rescan will be automatically started now.
845                 unlock_index
846                 rescan {set ui_status_value {Ready.}}
847                 return
848         }
850         # -- At least one file should differ in the index.
851         #
852         set files_ready 0
853         foreach path [array names file_states] {
854                 switch -glob -- [lindex $file_states($path) 0] {
855                 _? {continue}
856                 A? -
857                 D? -
858                 M? {set files_ready 1; break}
859                 U? {
860                         error_popup "Unmerged files cannot be committed.
862 File [short_path $path] has merge conflicts.
863 You must resolve them and include the file before committing.
865                         unlock_index
866                         return
867                 }
868                 default {
869                         error_popup "Unknown file state [lindex $s 0] detected.
871 File [short_path $path] cannot be committed by this program.
873                 }
874                 }
875         }
876         if {!$files_ready} {
877                 error_popup {No included files to commit.
879 You must include at least 1 file before you can commit.
881                 unlock_index
882                 return
883         }
885         # -- A message is required.
886         #
887         set msg [string trim [$ui_comm get 1.0 end]]
888         if {$msg eq {}} {
889                 error_popup {Please supply a commit message.
891 A good commit message has the following format:
893 - First line: Describe in one sentance what you did.
894 - Second line: Blank
895 - Remaining lines: Describe why this change is good.
897                 unlock_index
898                 return
899         }
901         # -- Update included files if partialincludes are off.
902         #
903         if {$repo_config(gui.partialinclude) ne {true}} {
904                 set pathList [list]
905                 foreach path [array names file_states] {
906                         switch -glob -- [lindex $file_states($path) 0] {
907                         A? -
908                         M? {lappend pathList $path}
909                         }
910                 }
911                 if {$pathList ne {}} {
912                         unlock_index
913                         update_index \
914                                 "Updating included files" \
915                                 $pathList \
916                                 [concat {lock_index update;} \
917                                         [list commit_prehook $curHEAD $msg]]
918                         return
919                 }
920         }
922         commit_prehook $curHEAD $msg
925 proc commit_prehook {curHEAD msg} {
926         global gitdir ui_status_value pch_error
928         set pchook [file join $gitdir hooks pre-commit]
930         # On Cygwin [file executable] might lie so we need to ask
931         # the shell if the hook is executable.  Yes that's annoying.
932         #
933         if {[is_Windows] && [file isfile $pchook]} {
934                 set pchook [list sh -c [concat \
935                         "if test -x \"$pchook\";" \
936                         "then exec \"$pchook\" 2>&1;" \
937                         "fi"]]
938         } elseif {[file executable $pchook]} {
939                 set pchook [list $pchook |& cat]
940         } else {
941                 commit_writetree $curHEAD $msg
942                 return
943         }
945         set ui_status_value {Calling pre-commit hook...}
946         set pch_error {}
947         set fd_ph [open "| $pchook" r]
948         fconfigure $fd_ph -blocking 0 -translation binary
949         fileevent $fd_ph readable \
950                 [list commit_prehook_wait $fd_ph $curHEAD $msg]
953 proc commit_prehook_wait {fd_ph curHEAD msg} {
954         global pch_error ui_status_value
956         append pch_error [read $fd_ph]
957         fconfigure $fd_ph -blocking 1
958         if {[eof $fd_ph]} {
959                 if {[catch {close $fd_ph}]} {
960                         set ui_status_value {Commit declined by pre-commit hook.}
961                         hook_failed_popup pre-commit $pch_error
962                         unlock_index
963                 } else {
964                         commit_writetree $curHEAD $msg
965                 }
966                 set pch_error {}
967                 return
968         }
969         fconfigure $fd_ph -blocking 0
972 proc commit_writetree {curHEAD msg} {
973         global ui_status_value
975         set ui_status_value {Committing changes...}
976         set fd_wt [open "| git write-tree" r]
977         fileevent $fd_wt readable \
978                 [list commit_committree $fd_wt $curHEAD $msg]
981 proc commit_committree {fd_wt curHEAD msg} {
982         global HEAD PARENT MERGE_HEAD commit_type
983         global single_commit gitdir
984         global ui_status_value ui_comm selected_commit_type
985         global file_states selected_paths rescan_active
987         gets $fd_wt tree_id
988         if {$tree_id eq {} || [catch {close $fd_wt} err]} {
989                 error_popup "write-tree failed:\n\n$err"
990                 set ui_status_value {Commit failed.}
991                 unlock_index
992                 return
993         }
995         # -- Create the commit.
996         #
997         set cmd [list git commit-tree $tree_id]
998         set parents [concat $PARENT $MERGE_HEAD]
999         if {[llength $parents] > 0} {
1000                 foreach p $parents {
1001                         lappend cmd -p $p
1002                 }
1003         } else {
1004                 # git commit-tree writes to stderr during initial commit.
1005                 lappend cmd 2>/dev/null
1006         }
1007         lappend cmd << $msg
1008         if {[catch {set cmt_id [eval exec $cmd]} err]} {
1009                 error_popup "commit-tree failed:\n\n$err"
1010                 set ui_status_value {Commit failed.}
1011                 unlock_index
1012                 return
1013         }
1015         # -- Update the HEAD ref.
1016         #
1017         set reflogm commit
1018         if {$commit_type ne {normal}} {
1019                 append reflogm " ($commit_type)"
1020         }
1021         set i [string first "\n" $msg]
1022         if {$i >= 0} {
1023                 append reflogm {: } [string range $msg 0 [expr {$i - 1}]]
1024         } else {
1025                 append reflogm {: } $msg
1026         }
1027         set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD]
1028         if {[catch {eval exec $cmd} err]} {
1029                 error_popup "update-ref failed:\n\n$err"
1030                 set ui_status_value {Commit failed.}
1031                 unlock_index
1032                 return
1033         }
1035         # -- Cleanup after ourselves.
1036         #
1037         catch {file delete [file join $gitdir MERGE_HEAD]}
1038         catch {file delete [file join $gitdir MERGE_MSG]}
1039         catch {file delete [file join $gitdir SQUASH_MSG]}
1040         catch {file delete [file join $gitdir GITGUI_MSG]}
1042         # -- Let rerere do its thing.
1043         #
1044         if {[file isdirectory [file join $gitdir rr-cache]]} {
1045                 catch {exec git rerere}
1046         }
1048         # -- Run the post-commit hook.
1049         #
1050         set pchook [file join $gitdir hooks post-commit]
1051         if {[is_Windows] && [file isfile $pchook]} {
1052                 set pchook [list sh -c [concat \
1053                         "if test -x \"$pchook\";" \
1054                         "then exec \"$pchook\";" \
1055                         "fi"]]
1056         } elseif {![file executable $pchook]} {
1057                 set pchook {}
1058         }
1059         if {$pchook ne {}} {
1060                 catch {exec $pchook &}
1061         }
1063         $ui_comm delete 0.0 end
1064         $ui_comm edit reset
1065         $ui_comm edit modified false
1067         if {$single_commit} do_quit
1069         # -- Update in memory status
1070         #
1071         set selected_commit_type new
1072         set commit_type normal
1073         set HEAD $cmt_id
1074         set PARENT $cmt_id
1075         set MERGE_HEAD [list]
1077         foreach path [array names file_states] {
1078                 set s $file_states($path)
1079                 set m [lindex $s 0]
1080                 switch -glob -- $m {
1081                 _O -
1082                 _M -
1083                 _D {continue}
1084                 __ -
1085                 A_ -
1086                 M_ -
1087                 DD {
1088                         unset file_states($path)
1089                         catch {unset selected_paths($path)}
1090                 }
1091                 DO {
1092                         set file_states($path) [list _O [lindex $s 1] {} {}]
1093                 }
1094                 AM -
1095                 AD -
1096                 MM -
1097                 MD -
1098                 DM {
1099                         set file_states($path) [list \
1100                                 _[string index $m 1] \
1101                                 [lindex $s 1] \
1102                                 [lindex $s 3] \
1103                                 {}]
1104                 }
1105                 }
1106         }
1108         display_all_files
1109         unlock_index
1110         reshow_diff
1111         set ui_status_value \
1112                 "Changes committed as [string range $cmt_id 0 7]."
1115 ######################################################################
1116 ##
1117 ## fetch pull push
1119 proc fetch_from {remote} {
1120         set w [new_console "fetch $remote" \
1121                 "Fetching new changes from $remote"]
1122         set cmd [list git fetch]
1123         lappend cmd $remote
1124         console_exec $w $cmd
1127 proc pull_remote {remote branch} {
1128         global HEAD commit_type file_states repo_config
1130         if {![lock_index update]} return
1132         # -- Our in memory state should match the repository.
1133         #
1134         repository_state curType curHEAD curMERGE_HEAD
1135         if {$commit_type ne $curType || $HEAD ne $curHEAD} {
1136                 info_popup {Last scanned state does not match repository state.
1138 Another Git program has modified this repository
1139 since the last scan.  A rescan must be performed
1140 before a pull operation can be started.
1142 The rescan will be automatically started now.
1144                 unlock_index
1145                 rescan {set ui_status_value {Ready.}}
1146                 return
1147         }
1149         # -- No differences should exist before a pull.
1150         #
1151         if {[array size file_states] != 0} {
1152                 error_popup {Uncommitted but modified files are present.
1154 You should not perform a pull with unmodified
1155 files in your working directory as Git will be
1156 unable to recover from an incorrect merge.
1158 You should commit or revert all changes before
1159 starting a pull operation.
1161                 unlock_index
1162                 return
1163         }
1165         set w [new_console "pull $remote $branch" \
1166                 "Pulling new changes from branch $branch in $remote"]
1167         set cmd [list git pull]
1168         if {$repo_config(gui.pullsummary) eq {false}} {
1169                 lappend cmd --no-summary
1170         }
1171         lappend cmd $remote
1172         lappend cmd $branch
1173         console_exec $w $cmd [list post_pull_remote $remote $branch]
1176 proc post_pull_remote {remote branch success} {
1177         global HEAD PARENT MERGE_HEAD commit_type selected_commit_type
1178         global ui_status_value
1180         unlock_index
1181         if {$success} {
1182                 repository_state commit_type HEAD MERGE_HEAD
1183                 set PARENT $HEAD
1184                 set selected_commit_type new
1185                 set ui_status_value "Pulling $branch from $remote complete."
1186         } else {
1187                 rescan [list set ui_status_value \
1188                         "Conflicts detected while pulling $branch from $remote."]
1189         }
1192 proc push_to {remote} {
1193         set w [new_console "push $remote" \
1194                 "Pushing changes to $remote"]
1195         set cmd [list git push]
1196         lappend cmd $remote
1197         console_exec $w $cmd
1200 ######################################################################
1201 ##
1202 ## ui helpers
1204 proc mapcol {state path} {
1205         global all_cols ui_other
1207         if {[catch {set r $all_cols($state)}]} {
1208                 puts "error: no column for state={$state} $path"
1209                 return $ui_other
1210         }
1211         return $r
1214 proc mapicon {state path} {
1215         global all_icons
1217         if {[catch {set r $all_icons($state)}]} {
1218                 puts "error: no icon for state={$state} $path"
1219                 return file_plain
1220         }
1221         return $r
1224 proc mapdesc {state path} {
1225         global all_descs
1227         if {[catch {set r $all_descs($state)}]} {
1228                 puts "error: no desc for state={$state} $path"
1229                 return $state
1230         }
1231         return $r
1234 proc escape_path {path} {
1235         regsub -all "\n" $path "\\n" path
1236         return $path
1239 proc short_path {path} {
1240         return [escape_path [lindex [file split $path] end]]
1243 set next_icon_id 0
1244 set null_sha1 [string repeat 0 40]
1246 proc merge_state {path new_state {head_info {}} {index_info {}}} {
1247         global file_states next_icon_id null_sha1
1249         set s0 [string index $new_state 0]
1250         set s1 [string index $new_state 1]
1252         if {[catch {set info $file_states($path)}]} {
1253                 set state __
1254                 set icon n[incr next_icon_id]
1255         } else {
1256                 set state [lindex $info 0]
1257                 set icon [lindex $info 1]
1258                 if {$head_info eq {}}  {set head_info  [lindex $info 2]}
1259                 if {$index_info eq {}} {set index_info [lindex $info 3]}
1260         }
1262         if     {$s0 eq {?}} {set s0 [string index $state 0]} \
1263         elseif {$s0 eq {_}} {set s0 _}
1265         if     {$s1 eq {?}} {set s1 [string index $state 1]} \
1266         elseif {$s1 eq {_}} {set s1 _}
1268         if {$s0 eq {A} && $s1 eq {_} && $head_info eq {}} {
1269                 set head_info [list 0 $null_sha1]
1270         } elseif {$s0 ne {_} && [string index $state 0] eq {_}
1271                 && $head_info eq {}} {
1272                 set head_info $index_info
1273         }
1275         set file_states($path) [list $s0$s1 $icon \
1276                 $head_info $index_info \
1277                 ]
1278         return $state
1281 proc display_file {path state} {
1282         global file_states file_lists selected_paths
1284         set old_m [merge_state $path $state]
1285         set s $file_states($path)
1286         set new_m [lindex $s 0]
1287         set new_w [mapcol $new_m $path] 
1288         set old_w [mapcol $old_m $path]
1289         set new_icon [mapicon $new_m $path]
1291         if {$new_m eq {__}} {
1292                 set lno [lsearch -sorted $file_lists($old_w) $path]
1293                 if {$lno >= 0} {
1294                         set file_lists($old_w) \
1295                                 [lreplace $file_lists($old_w) $lno $lno]
1296                         incr lno
1297                         $old_w conf -state normal
1298                         $old_w delete $lno.0 [expr {$lno + 1}].0
1299                         $old_w conf -state disabled
1300                 }
1301                 unset file_states($path)
1302                 catch {unset selected_paths($path)}
1303                 return
1304         }
1306         if {$new_w ne $old_w} {
1307                 set lno [lsearch -sorted $file_lists($old_w) $path]
1308                 if {$lno >= 0} {
1309                         set file_lists($old_w) \
1310                                 [lreplace $file_lists($old_w) $lno $lno]
1311                         incr lno
1312                         $old_w conf -state normal
1313                         $old_w delete $lno.0 [expr {$lno + 1}].0
1314                         $old_w conf -state disabled
1315                 }
1317                 lappend file_lists($new_w) $path
1318                 set file_lists($new_w) [lsort $file_lists($new_w)]
1319                 set lno [lsearch -sorted $file_lists($new_w) $path]
1320                 incr lno
1321                 $new_w conf -state normal
1322                 $new_w image create $lno.0 \
1323                         -align center -padx 5 -pady 1 \
1324                         -name [lindex $s 1] \
1325                         -image $new_icon
1326                 $new_w insert $lno.1 "[escape_path $path]\n"
1327                 if {[catch {set in_sel $selected_paths($path)}]} {
1328                         set in_sel 0
1329                 }
1330                 if {$in_sel} {
1331                         $new_w tag add in_sel $lno.0 [expr {$lno + 1}].0
1332                 }
1333                 $new_w conf -state disabled
1334         } elseif {$new_icon ne [mapicon $old_m $path]} {
1335                 $new_w conf -state normal
1336                 $new_w image conf [lindex $s 1] -image $new_icon
1337                 $new_w conf -state disabled
1338         }
1341 proc display_all_files {} {
1342         global ui_index ui_other
1343         global file_states file_lists
1344         global last_clicked selected_paths
1346         $ui_index conf -state normal
1347         $ui_other conf -state normal
1349         $ui_index delete 0.0 end
1350         $ui_other delete 0.0 end
1351         set last_clicked {}
1353         set file_lists($ui_index) [list]
1354         set file_lists($ui_other) [list]
1356         foreach path [lsort [array names file_states]] {
1357                 set s $file_states($path)
1358                 set m [lindex $s 0]
1359                 set w [mapcol $m $path]
1360                 lappend file_lists($w) $path
1361                 set lno [expr {[lindex [split [$w index end] .] 0] - 1}]
1362                 $w image create end \
1363                         -align center -padx 5 -pady 1 \
1364                         -name [lindex $s 1] \
1365                         -image [mapicon $m $path]
1366                 $w insert end "[escape_path $path]\n"
1367                 if {[catch {set in_sel $selected_paths($path)}]} {
1368                         set in_sel 0
1369                 }
1370                 if {$in_sel} {
1371                         $w tag add in_sel $lno.0 [expr {$lno + 1}].0
1372                 }
1373         }
1375         $ui_index conf -state disabled
1376         $ui_other conf -state disabled
1379 proc update_indexinfo {msg pathList after} {
1380         global update_index_cp ui_status_value
1382         if {![lock_index update]} return
1384         set update_index_cp 0
1385         set pathList [lsort $pathList]
1386         set totalCnt [llength $pathList]
1387         set batch [expr {int($totalCnt * .01) + 1}]
1388         if {$batch > 25} {set batch 25}
1390         set ui_status_value [format \
1391                 "$msg... %i/%i files (%.2f%%)" \
1392                 $update_index_cp \
1393                 $totalCnt \
1394                 0.0]
1395         set fd [open "| git update-index -z --index-info" w]
1396         fconfigure $fd \
1397                 -blocking 0 \
1398                 -buffering full \
1399                 -buffersize 512 \
1400                 -translation binary
1401         fileevent $fd writable [list \
1402                 write_update_indexinfo \
1403                 $fd \
1404                 $pathList \
1405                 $totalCnt \
1406                 $batch \
1407                 $msg \
1408                 $after \
1409                 ]
1412 proc write_update_indexinfo {fd pathList totalCnt batch msg after} {
1413         global update_index_cp ui_status_value
1414         global file_states current_diff
1416         if {$update_index_cp >= $totalCnt} {
1417                 close $fd
1418                 unlock_index
1419                 uplevel #0 $after
1420                 return
1421         }
1423         for {set i $batch} \
1424                 {$update_index_cp < $totalCnt && $i > 0} \
1425                 {incr i -1} {
1426                 set path [lindex $pathList $update_index_cp]
1427                 incr update_index_cp
1429                 set s $file_states($path)
1430                 switch -glob -- [lindex $s 0] {
1431                 A? {set new _O}
1432                 M? {set new _M}
1433                 D_ {set new _D}
1434                 D? {set new _?}
1435                 ?? {continue}
1436                 }
1437                 set info [lindex $s 2]
1438                 if {$info eq {}} continue
1440                 puts -nonewline $fd $info
1441                 puts -nonewline $fd "\t"
1442                 puts -nonewline $fd $path
1443                 puts -nonewline $fd "\0"
1444                 display_file $path $new
1445         }
1447         set ui_status_value [format \
1448                 "$msg... %i/%i files (%.2f%%)" \
1449                 $update_index_cp \
1450                 $totalCnt \
1451                 [expr {100.0 * $update_index_cp / $totalCnt}]]
1454 proc update_index {msg pathList after} {
1455         global update_index_cp ui_status_value
1457         if {![lock_index update]} return
1459         set update_index_cp 0
1460         set pathList [lsort $pathList]
1461         set totalCnt [llength $pathList]
1462         set batch [expr {int($totalCnt * .01) + 1}]
1463         if {$batch > 25} {set batch 25}
1465         set ui_status_value [format \
1466                 "$msg... %i/%i files (%.2f%%)" \
1467                 $update_index_cp \
1468                 $totalCnt \
1469                 0.0]
1470         set fd [open "| git update-index --add --remove -z --stdin" w]
1471         fconfigure $fd \
1472                 -blocking 0 \
1473                 -buffering full \
1474                 -buffersize 512 \
1475                 -translation binary
1476         fileevent $fd writable [list \
1477                 write_update_index \
1478                 $fd \
1479                 $pathList \
1480                 $totalCnt \
1481                 $batch \
1482                 $msg \
1483                 $after \
1484                 ]
1487 proc write_update_index {fd pathList totalCnt batch msg after} {
1488         global update_index_cp ui_status_value
1489         global file_states current_diff
1491         if {$update_index_cp >= $totalCnt} {
1492                 close $fd
1493                 unlock_index
1494                 uplevel #0 $after
1495                 return
1496         }
1498         for {set i $batch} \
1499                 {$update_index_cp < $totalCnt && $i > 0} \
1500                 {incr i -1} {
1501                 set path [lindex $pathList $update_index_cp]
1502                 incr update_index_cp
1504                 switch -glob -- [lindex $file_states($path) 0] {
1505                 AD -
1506                 MD -
1507                 UD -
1508                 _D {set new DD}
1510                 _M -
1511                 MM -
1512                 UM -
1513                 U_ -
1514                 M_ {set new M_}
1516                 _O -
1517                 AM -
1518                 A_ {set new A_}
1520                 ?? {continue}
1521                 }
1523                 puts -nonewline $fd $path
1524                 puts -nonewline $fd "\0"
1525                 display_file $path $new
1526         }
1528         set ui_status_value [format \
1529                 "$msg... %i/%i files (%.2f%%)" \
1530                 $update_index_cp \
1531                 $totalCnt \
1532                 [expr {100.0 * $update_index_cp / $totalCnt}]]
1535 proc checkout_index {msg pathList after} {
1536         global update_index_cp ui_status_value
1538         if {![lock_index update]} return
1540         set update_index_cp 0
1541         set pathList [lsort $pathList]
1542         set totalCnt [llength $pathList]
1543         set batch [expr {int($totalCnt * .01) + 1}]
1544         if {$batch > 25} {set batch 25}
1546         set ui_status_value [format \
1547                 "$msg... %i/%i files (%.2f%%)" \
1548                 $update_index_cp \
1549                 $totalCnt \
1550                 0.0]
1551         set cmd [list git checkout-index]
1552         lappend cmd --index
1553         lappend cmd --quiet
1554         lappend cmd --force
1555         lappend cmd -z
1556         lappend cmd --stdin
1557         set fd [open "| $cmd " w]
1558         fconfigure $fd \
1559                 -blocking 0 \
1560                 -buffering full \
1561                 -buffersize 512 \
1562                 -translation binary
1563         fileevent $fd writable [list \
1564                 write_checkout_index \
1565                 $fd \
1566                 $pathList \
1567                 $totalCnt \
1568                 $batch \
1569                 $msg \
1570                 $after \
1571                 ]
1574 proc write_checkout_index {fd pathList totalCnt batch msg after} {
1575         global update_index_cp ui_status_value
1576         global file_states current_diff
1578         if {$update_index_cp >= $totalCnt} {
1579                 close $fd
1580                 unlock_index
1581                 uplevel #0 $after
1582                 return
1583         }
1585         for {set i $batch} \
1586                 {$update_index_cp < $totalCnt && $i > 0} \
1587                 {incr i -1} {
1588                 set path [lindex $pathList $update_index_cp]
1589                 incr update_index_cp
1591                 switch -glob -- [lindex $file_states($path) 0] {
1592                 AM -
1593                 AD {set new A_}
1594                 MM -
1595                 MD {set new M_}
1596                 _M -
1597                 _D {set new __}
1598                 ?? {continue}
1599                 }
1601                 puts -nonewline $fd $path
1602                 puts -nonewline $fd "\0"
1603                 display_file $path $new
1604         }
1606         set ui_status_value [format \
1607                 "$msg... %i/%i files (%.2f%%)" \
1608                 $update_index_cp \
1609                 $totalCnt \
1610                 [expr {100.0 * $update_index_cp / $totalCnt}]]
1613 ######################################################################
1614 ##
1615 ## branch management
1617 proc load_all_heads {} {
1618         global all_heads tracking_branches
1620         set all_heads [list]
1621         set cmd [list git for-each-ref]
1622         lappend cmd --format=%(refname)
1623         lappend cmd refs/heads
1624         set fd [open "| $cmd" r]
1625         while {[gets $fd line] > 0} {
1626                 if {![catch {set info $tracking_branches($line)}]} continue
1627                 if {![regsub ^refs/heads/ $line {} name]} continue
1628                 lappend all_heads $name
1629         }
1630         close $fd
1632         set all_heads [lsort $all_heads]
1635 proc populate_branch_menu {m} {
1636         global all_heads disable_on_lock
1638         $m add separator
1639         foreach b $all_heads {
1640                 $m add radiobutton \
1641                         -label $b \
1642                         -command [list switch_branch $b] \
1643                         -variable current_branch \
1644                         -value $b \
1645                         -font font_ui
1646                 lappend disable_on_lock \
1647                         [list $m entryconf [$m index last] -state]
1648         }
1651 proc do_create_branch {} {
1652         error "NOT IMPLEMENTED"
1655 proc do_delete_branch {} {
1656         error "NOT IMPLEMENTED"
1659 proc switch_branch {b} {
1660         global HEAD commit_type file_states current_branch
1661         global selected_commit_type ui_comm
1663         if {![lock_index switch]} return
1665         # -- Backup the selected branch (repository_state resets it)
1666         #
1667         set new_branch $current_branch
1669         # -- Our in memory state should match the repository.
1670         #
1671         repository_state curType curHEAD curMERGE_HEAD
1672         if {[string match amend* $commit_type]
1673                 && $curType eq {normal}
1674                 && $curHEAD eq $HEAD} {
1675         } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
1676                 info_popup {Last scanned state does not match repository state.
1678 Another Git program has modified this repository
1679 since the last scan.  A rescan must be performed
1680 before the current branch can be changed.
1682 The rescan will be automatically started now.
1684                 unlock_index
1685                 rescan {set ui_status_value {Ready.}}
1686                 return
1687         }
1689         # -- Toss the message buffer if we are in amend mode.
1690         #
1691         if {[string match amend* $curType]} {
1692                 $ui_comm delete 0.0 end
1693                 $ui_comm edit reset
1694                 $ui_comm edit modified false
1695         }
1697         set selected_commit_type new
1698         set current_branch $new_branch
1700         unlock_index
1701         error "NOT FINISHED"
1704 ######################################################################
1705 ##
1706 ## remote management
1708 proc load_all_remotes {} {
1709         global gitdir repo_config
1710         global all_remotes tracking_branches
1712         set all_remotes [list]
1713         array unset tracking_branches
1715         set rm_dir [file join $gitdir remotes]
1716         if {[file isdirectory $rm_dir]} {
1717                 set all_remotes [glob \
1718                         -types f \
1719                         -tails \
1720                         -nocomplain \
1721                         -directory $rm_dir *]
1723                 foreach name $all_remotes {
1724                         catch {
1725                                 set fd [open [file join $rm_dir $name] r]
1726                                 while {[gets $fd line] >= 0} {
1727                                         if {![regexp {^Pull:[   ]*([^:]+):(.+)$} \
1728                                                 $line line src dst]} continue
1729                                         if {![regexp ^refs/ $dst]} {
1730                                                 set dst "refs/heads/$dst"
1731                                         }
1732                                         set tracking_branches($dst) [list $name $src]
1733                                 }
1734                                 close $fd
1735                         }
1736                 }
1737         }
1739         foreach line [array names repo_config remote.*.url] {
1740                 if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue
1741                 lappend all_remotes $name
1743                 if {[catch {set fl $repo_config(remote.$name.fetch)}]} {
1744                         set fl {}
1745                 }
1746                 foreach line $fl {
1747                         if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue
1748                         if {![regexp ^refs/ $dst]} {
1749                                 set dst "refs/heads/$dst"
1750                         }
1751                         set tracking_branches($dst) [list $name $src]
1752                 }
1753         }
1755         set all_remotes [lsort -unique $all_remotes]
1758 proc populate_fetch_menu {m} {
1759         global gitdir all_remotes repo_config
1761         foreach r $all_remotes {
1762                 set enable 0
1763                 if {![catch {set a $repo_config(remote.$r.url)}]} {
1764                         if {![catch {set a $repo_config(remote.$r.fetch)}]} {
1765                                 set enable 1
1766                         }
1767                 } else {
1768                         catch {
1769                                 set fd [open [file join $gitdir remotes $r] r]
1770                                 while {[gets $fd n] >= 0} {
1771                                         if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
1772                                                 set enable 1
1773                                                 break
1774                                         }
1775                                 }
1776                                 close $fd
1777                         }
1778                 }
1780                 if {$enable} {
1781                         $m add command \
1782                                 -label "Fetch from $r..." \
1783                                 -command [list fetch_from $r] \
1784                                 -font font_ui
1785                 }
1786         }
1789 proc populate_push_menu {m} {
1790         global gitdir all_remotes repo_config
1792         foreach r $all_remotes {
1793                 set enable 0
1794                 if {![catch {set a $repo_config(remote.$r.url)}]} {
1795                         if {![catch {set a $repo_config(remote.$r.push)}]} {
1796                                 set enable 1
1797                         }
1798                 } else {
1799                         catch {
1800                                 set fd [open [file join $gitdir remotes $r] r]
1801                                 while {[gets $fd n] >= 0} {
1802                                         if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
1803                                                 set enable 1
1804                                                 break
1805                                         }
1806                                 }
1807                                 close $fd
1808                         }
1809                 }
1811                 if {$enable} {
1812                         $m add command \
1813                                 -label "Push to $r..." \
1814                                 -command [list push_to $r] \
1815                                 -font font_ui
1816                 }
1817         }
1820 proc populate_pull_menu {m} {
1821         global gitdir repo_config all_remotes disable_on_lock
1823         foreach remote $all_remotes {
1824                 set rb_list [list]
1825                 if {[array get repo_config remote.$remote.url] ne {}} {
1826                         if {[array get repo_config remote.$remote.fetch] ne {}} {
1827                                 foreach line $repo_config(remote.$remote.fetch) {
1828                                         if {[regexp {^([^:]+):} $line line rb]} {
1829                                                 lappend rb_list $rb
1830                                         }
1831                                 }
1832                         }
1833                 } else {
1834                         catch {
1835                                 set fd [open [file join $gitdir remotes $remote] r]
1836                                 while {[gets $fd line] >= 0} {
1837                                         if {[regexp {^Pull:[ \t]*([^:]+):} $line line rb]} {
1838                                                 lappend rb_list $rb
1839                                         }
1840                                 }
1841                                 close $fd
1842                         }
1843                 }
1845                 foreach rb $rb_list {
1846                         regsub ^refs/heads/ $rb {} rb_short
1847                         $m add command \
1848                                 -label "Branch $rb_short from $remote..." \
1849                                 -command [list pull_remote $remote $rb] \
1850                                 -font font_ui
1851                         lappend disable_on_lock \
1852                                 [list $m entryconf [$m index last] -state]
1853                 }
1854         }
1857 ######################################################################
1858 ##
1859 ## icons
1861 set filemask {
1862 #define mask_width 14
1863 #define mask_height 15
1864 static unsigned char mask_bits[] = {
1865    0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1866    0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1867    0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
1870 image create bitmap file_plain -background white -foreground black -data {
1871 #define plain_width 14
1872 #define plain_height 15
1873 static unsigned char plain_bits[] = {
1874    0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1875    0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
1876    0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1877 } -maskdata $filemask
1879 image create bitmap file_mod -background white -foreground blue -data {
1880 #define mod_width 14
1881 #define mod_height 15
1882 static unsigned char mod_bits[] = {
1883    0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1884    0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1885    0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1886 } -maskdata $filemask
1888 image create bitmap file_fulltick -background white -foreground "#007000" -data {
1889 #define file_fulltick_width 14
1890 #define file_fulltick_height 15
1891 static unsigned char file_fulltick_bits[] = {
1892    0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
1893    0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
1894    0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1895 } -maskdata $filemask
1897 image create bitmap file_parttick -background white -foreground "#005050" -data {
1898 #define parttick_width 14
1899 #define parttick_height 15
1900 static unsigned char parttick_bits[] = {
1901    0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1902    0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
1903    0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1904 } -maskdata $filemask
1906 image create bitmap file_question -background white -foreground black -data {
1907 #define file_question_width 14
1908 #define file_question_height 15
1909 static unsigned char file_question_bits[] = {
1910    0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
1911    0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
1912    0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1913 } -maskdata $filemask
1915 image create bitmap file_removed -background white -foreground red -data {
1916 #define file_removed_width 14
1917 #define file_removed_height 15
1918 static unsigned char file_removed_bits[] = {
1919    0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1920    0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
1921    0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
1922 } -maskdata $filemask
1924 image create bitmap file_merge -background white -foreground blue -data {
1925 #define file_merge_width 14
1926 #define file_merge_height 15
1927 static unsigned char file_merge_bits[] = {
1928    0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
1929    0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1930    0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1931 } -maskdata $filemask
1933 set ui_index .vpane.files.index.list
1934 set ui_other .vpane.files.other.list
1935 set max_status_desc 0
1936 foreach i {
1937                 {__ i plain    "Unmodified"}
1938                 {_M i mod      "Modified"}
1939                 {M_ i fulltick "Added to commit"}
1940                 {MM i parttick "Partially included"}
1941                 {MD i question "Added (but gone)"}
1943                 {_O o plain    "Untracked"}
1944                 {A_ o fulltick "Added by commit"}
1945                 {AM o parttick "Partially added"}
1946                 {AD o question "Added (but gone)"}
1948                 {_D i question "Missing"}
1949                 {DD i removed  "Removed by commit"}
1950                 {D_ i removed  "Removed by commit"}
1951                 {DO i removed  "Removed (still exists)"}
1952                 {DM i removed  "Removed (but modified)"}
1954                 {UD i merge    "Merge conflicts"}
1955                 {UM i merge    "Merge conflicts"}
1956                 {U_ i merge    "Merge conflicts"}
1957         } {
1958         if {$max_status_desc < [string length [lindex $i 3]]} {
1959                 set max_status_desc [string length [lindex $i 3]]
1960         }
1961         if {[lindex $i 1] eq {i}} {
1962                 set all_cols([lindex $i 0]) $ui_index
1963         } else {
1964                 set all_cols([lindex $i 0]) $ui_other
1965         }
1966         set all_icons([lindex $i 0]) file_[lindex $i 2]
1967         set all_descs([lindex $i 0]) [lindex $i 3]
1969 unset filemask i
1971 ######################################################################
1972 ##
1973 ## util
1975 proc is_MacOSX {} {
1976         global tcl_platform tk_library
1977         if {[tk windowingsystem] eq {aqua}} {
1978                 return 1
1979         }
1980         return 0
1983 proc is_Windows {} {
1984         global tcl_platform
1985         if {$tcl_platform(platform) eq {windows}} {
1986                 return 1
1987         }
1988         return 0
1991 proc bind_button3 {w cmd} {
1992         bind $w <Any-Button-3> $cmd
1993         if {[is_MacOSX]} {
1994                 bind $w <Control-Button-1> $cmd
1995         }
1998 proc incr_font_size {font {amt 1}} {
1999         set sz [font configure $font -size]
2000         incr sz $amt
2001         font configure $font -size $sz
2002         font configure ${font}bold -size $sz
2005 proc hook_failed_popup {hook msg} {
2006         global gitdir appname
2008         set w .hookfail
2009         toplevel $w
2011         frame $w.m
2012         label $w.m.l1 -text "$hook hook failed:" \
2013                 -anchor w \
2014                 -justify left \
2015                 -font font_uibold
2016         text $w.m.t \
2017                 -background white -borderwidth 1 \
2018                 -relief sunken \
2019                 -width 80 -height 10 \
2020                 -font font_diff \
2021                 -yscrollcommand [list $w.m.sby set]
2022         label $w.m.l2 \
2023                 -text {You must correct the above errors before committing.} \
2024                 -anchor w \
2025                 -justify left \
2026                 -font font_uibold
2027         scrollbar $w.m.sby -command [list $w.m.t yview]
2028         pack $w.m.l1 -side top -fill x
2029         pack $w.m.l2 -side bottom -fill x
2030         pack $w.m.sby -side right -fill y
2031         pack $w.m.t -side left -fill both -expand 1
2032         pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
2034         $w.m.t insert 1.0 $msg
2035         $w.m.t conf -state disabled
2037         button $w.ok -text OK \
2038                 -width 15 \
2039                 -font font_ui \
2040                 -command "destroy $w"
2041         pack $w.ok -side bottom -anchor e -pady 10 -padx 10
2043         bind $w <Visibility> "grab $w; focus $w"
2044         bind $w <Key-Return> "destroy $w"
2045         wm title $w "$appname ([lindex [file split \
2046                 [file normalize [file dirname $gitdir]]] \
2047                 end]): error"
2048         tkwait window $w
2051 set next_console_id 0
2053 proc new_console {short_title long_title} {
2054         global next_console_id console_data
2055         set w .console[incr next_console_id]
2056         set console_data($w) [list $short_title $long_title]
2057         return [console_init $w]
2060 proc console_init {w} {
2061         global console_cr console_data
2062         global gitdir appname M1B
2064         set console_cr($w) 1.0
2065         toplevel $w
2066         frame $w.m
2067         label $w.m.l1 -text "[lindex $console_data($w) 1]:" \
2068                 -anchor w \
2069                 -justify left \
2070                 -font font_uibold
2071         text $w.m.t \
2072                 -background white -borderwidth 1 \
2073                 -relief sunken \
2074                 -width 80 -height 10 \
2075                 -font font_diff \
2076                 -state disabled \
2077                 -yscrollcommand [list $w.m.sby set]
2078         label $w.m.s -text {Working... please wait...} \
2079                 -anchor w \
2080                 -justify left \
2081                 -font font_uibold
2082         scrollbar $w.m.sby -command [list $w.m.t yview]
2083         pack $w.m.l1 -side top -fill x
2084         pack $w.m.s -side bottom -fill x
2085         pack $w.m.sby -side right -fill y
2086         pack $w.m.t -side left -fill both -expand 1
2087         pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
2089         menu $w.ctxm -tearoff 0
2090         $w.ctxm add command -label "Copy" \
2091                 -font font_ui \
2092                 -command "tk_textCopy $w.m.t"
2093         $w.ctxm add command -label "Select All" \
2094                 -font font_ui \
2095                 -command "$w.m.t tag add sel 0.0 end"
2096         $w.ctxm add command -label "Copy All" \
2097                 -font font_ui \
2098                 -command "
2099                         $w.m.t tag add sel 0.0 end
2100                         tk_textCopy $w.m.t
2101                         $w.m.t tag remove sel 0.0 end
2102                 "
2104         button $w.ok -text {Close} \
2105                 -font font_ui \
2106                 -state disabled \
2107                 -command "destroy $w"
2108         pack $w.ok -side bottom -anchor e -pady 10 -padx 10
2110         bind_button3 $w.m.t "tk_popup $w.ctxm %X %Y"
2111         bind $w.m.t <$M1B-Key-a> "$w.m.t tag add sel 0.0 end;break"
2112         bind $w.m.t <$M1B-Key-A> "$w.m.t tag add sel 0.0 end;break"
2113         bind $w <Visibility> "focus $w"
2114         wm title $w "$appname ([lindex [file split \
2115                 [file normalize [file dirname $gitdir]]] \
2116                 end]): [lindex $console_data($w) 0]"
2117         return $w
2120 proc console_exec {w cmd {after {}}} {
2121         # -- Windows tosses the enviroment when we exec our child.
2122         #    But most users need that so we have to relogin. :-(
2123         #
2124         if {[is_Windows]} {
2125                 set cmd [list sh --login -c "cd \"[pwd]\" && [join $cmd { }]"]
2126         }
2128         # -- Tcl won't let us redirect both stdout and stderr to
2129         #    the same pipe.  So pass it through cat...
2130         #
2131         set cmd [concat | $cmd |& cat]
2133         set fd_f [open $cmd r]
2134         fconfigure $fd_f -blocking 0 -translation binary
2135         fileevent $fd_f readable [list console_read $w $fd_f $after]
2138 proc console_read {w fd after} {
2139         global console_cr console_data
2141         set buf [read $fd]
2142         if {$buf ne {}} {
2143                 if {![winfo exists $w]} {console_init $w}
2144                 $w.m.t conf -state normal
2145                 set c 0
2146                 set n [string length $buf]
2147                 while {$c < $n} {
2148                         set cr [string first "\r" $buf $c]
2149                         set lf [string first "\n" $buf $c]
2150                         if {$cr < 0} {set cr [expr {$n + 1}]}
2151                         if {$lf < 0} {set lf [expr {$n + 1}]}
2153                         if {$lf < $cr} {
2154                                 $w.m.t insert end [string range $buf $c $lf]
2155                                 set console_cr($w) [$w.m.t index {end -1c}]
2156                                 set c $lf
2157                                 incr c
2158                         } else {
2159                                 $w.m.t delete $console_cr($w) end
2160                                 $w.m.t insert end "\n"
2161                                 $w.m.t insert end [string range $buf $c $cr]
2162                                 set c $cr
2163                                 incr c
2164                         }
2165                 }
2166                 $w.m.t conf -state disabled
2167                 $w.m.t see end
2168         }
2170         fconfigure $fd -blocking 1
2171         if {[eof $fd]} {
2172                 if {[catch {close $fd}]} {
2173                         if {![winfo exists $w]} {console_init $w}
2174                         $w.m.s conf -background red -text {Error: Command Failed}
2175                         $w.ok conf -state normal
2176                         set ok 0
2177                 } elseif {[winfo exists $w]} {
2178                         $w.m.s conf -background green -text {Success}
2179                         $w.ok conf -state normal
2180                         set ok 1
2181                 }
2182                 array unset console_cr $w
2183                 array unset console_data $w
2184                 if {$after ne {}} {
2185                         uplevel #0 $after $ok
2186                 }
2187                 return
2188         }
2189         fconfigure $fd -blocking 0
2192 ######################################################################
2193 ##
2194 ## ui commands
2196 set starting_gitk_msg {Please wait... Starting gitk...}
2198 proc do_gitk {revs} {
2199         global ui_status_value starting_gitk_msg
2201         set cmd gitk
2202         if {$revs ne {}} {
2203                 append cmd { }
2204                 append cmd $revs
2205         }
2206         if {[is_Windows]} {
2207                 set cmd "sh -c \"exec $cmd\""
2208         }
2209         append cmd { &}
2211         if {[catch {eval exec $cmd} err]} {
2212                 error_popup "Failed to start gitk:\n\n$err"
2213         } else {
2214                 set ui_status_value $starting_gitk_msg
2215                 after 10000 {
2216                         if {$ui_status_value eq $starting_gitk_msg} {
2217                                 set ui_status_value {Ready.}
2218                         }
2219                 }
2220         }
2223 proc do_gc {} {
2224         set w [new_console {gc} {Compressing the object database}]
2225         console_exec $w {git gc}
2228 proc do_fsck_objects {} {
2229         set w [new_console {fsck-objects} \
2230                 {Verifying the object database with fsck-objects}]
2231         set cmd [list git fsck-objects]
2232         lappend cmd --full
2233         lappend cmd --cache
2234         lappend cmd --strict
2235         console_exec $w $cmd
2238 set is_quitting 0
2240 proc do_quit {} {
2241         global gitdir ui_comm is_quitting repo_config commit_type
2243         if {$is_quitting} return
2244         set is_quitting 1
2246         # -- Stash our current commit buffer.
2247         #
2248         set save [file join $gitdir GITGUI_MSG]
2249         set msg [string trim [$ui_comm get 0.0 end]]
2250         if {![string match amend* $commit_type]
2251                 && [$ui_comm edit modified]
2252                 && $msg ne {}} {
2253                 catch {
2254                         set fd [open $save w]
2255                         puts $fd [string trim [$ui_comm get 0.0 end]]
2256                         close $fd
2257                 }
2258         } else {
2259                 catch {file delete $save}
2260         }
2262         # -- Stash our current window geometry into this repository.
2263         #
2264         set cfg_geometry [list]
2265         lappend cfg_geometry [wm geometry .]
2266         lappend cfg_geometry [lindex [.vpane sash coord 0] 1]
2267         lappend cfg_geometry [lindex [.vpane.files sash coord 0] 0]
2268         if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
2269                 set rc_geometry {}
2270         }
2271         if {$cfg_geometry ne $rc_geometry} {
2272                 catch {exec git repo-config gui.geometry $cfg_geometry}
2273         }
2275         destroy .
2278 proc do_rescan {} {
2279         rescan {set ui_status_value {Ready.}}
2282 proc remove_helper {txt paths} {
2283         global file_states current_diff
2285         if {![lock_index begin-update]} return
2287         set pathList [list]
2288         set after {}
2289         foreach path $paths {
2290                 switch -glob -- [lindex $file_states($path) 0] {
2291                 A? -
2292                 M? -
2293                 D? {
2294                         lappend pathList $path
2295                         if {$path eq $current_diff} {
2296                                 set after {reshow_diff;}
2297                         }
2298                 }
2299                 }
2300         }
2301         if {$pathList eq {}} {
2302                 unlock_index
2303         } else {
2304                 update_indexinfo \
2305                         $txt \
2306                         $pathList \
2307                         [concat $after {set ui_status_value {Ready.}}]
2308         }
2311 proc do_remove_selection {} {
2312         global current_diff selected_paths
2314         if {[array size selected_paths] > 0} {
2315                 remove_helper \
2316                         {Removing selected files from commit} \
2317                         [array names selected_paths]
2318         } elseif {$current_diff ne {}} {
2319                 remove_helper \
2320                         "Removing [short_path $current_diff] from commit" \
2321                         [list $current_diff]
2322         }
2325 proc include_helper {txt paths} {
2326         global file_states current_diff
2328         if {![lock_index begin-update]} return
2330         set pathList [list]
2331         set after {}
2332         foreach path $paths {
2333                 switch -glob -- [lindex $file_states($path) 0] {
2334                 AM -
2335                 AD -
2336                 MM -
2337                 MD -
2338                 U? -
2339                 _M -
2340                 _D -
2341                 _O {
2342                         lappend pathList $path
2343                         if {$path eq $current_diff} {
2344                                 set after {reshow_diff;}
2345                         }
2346                 }
2347                 }
2348         }
2349         if {$pathList eq {}} {
2350                 unlock_index
2351         } else {
2352                 update_index \
2353                         $txt \
2354                         $pathList \
2355                         [concat $after {set ui_status_value {Ready to commit.}}]
2356         }
2359 proc do_include_selection {} {
2360         global current_diff selected_paths
2362         if {[array size selected_paths] > 0} {
2363                 include_helper \
2364                         {Adding selected files} \
2365                         [array names selected_paths]
2366         } elseif {$current_diff ne {}} {
2367                 include_helper \
2368                         "Adding [short_path $current_diff]" \
2369                         [list $current_diff]
2370         }
2373 proc do_include_all {} {
2374         global file_states
2376         set paths [list]
2377         foreach path [array names file_states] {
2378                 switch -- [lindex $file_states($path) 0] {
2379                 AM -
2380                 AD -
2381                 MM -
2382                 MD -
2383                 _M -
2384                 _D {lappend paths $path}
2385                 }
2386         }
2387         include_helper \
2388                 {Adding all modified files} \
2389                 $paths
2392 proc revert_helper {txt paths} {
2393         global gitdir appname
2394         global file_states current_diff
2396         if {![lock_index begin-update]} return
2398         set pathList [list]
2399         set after {}
2400         foreach path $paths {
2401                 switch -glob -- [lindex $file_states($path) 0] {
2402                 AM -
2403                 AD -
2404                 MM -
2405                 MD -
2406                 _M -
2407                 _D {
2408                         lappend pathList $path
2409                         if {$path eq $current_diff} {
2410                                 set after {reshow_diff;}
2411                         }
2412                 }
2413                 }
2414         }
2416         set n [llength $pathList]
2417         if {$n == 0} {
2418                 unlock_index
2419                 return
2420         } elseif {$n == 1} {
2421                 set s "[short_path [lindex $pathList]]"
2422         } else {
2423                 set s "these $n files"
2424         }
2426         set reponame [lindex [file split \
2427                 [file normalize [file dirname $gitdir]]] \
2428                 end]
2430         set reply [tk_dialog \
2431                 .confirm_revert \
2432                 "$appname ($reponame)" \
2433                 "Revert changes in $s?
2435 Any unadded changes will be permanently lost by the revert." \
2436                 question \
2437                 1 \
2438                 {Do Nothing} \
2439                 {Revert Changes} \
2440                 ]
2441         if {$reply == 1} {
2442                 checkout_index \
2443                         $txt \
2444                         $pathList \
2445                         [concat $after {set ui_status_value {Ready.}}]
2446         } else {
2447                 unlock_index
2448         }
2451 proc do_revert_selection {} {
2452         global current_diff selected_paths
2454         if {[array size selected_paths] > 0} {
2455                 revert_helper \
2456                         {Reverting selected files} \
2457                         [array names selected_paths]
2458         } elseif {$current_diff ne {}} {
2459                 revert_helper \
2460                         "Reverting [short_path $current_diff]" \
2461                         [list $current_diff]
2462         }
2465 proc do_signoff {} {
2466         global ui_comm
2468         set me [committer_ident]
2469         if {$me eq {}} return
2471         set sob "Signed-off-by: $me"
2472         set last [$ui_comm get {end -1c linestart} {end -1c}]
2473         if {$last ne $sob} {
2474                 $ui_comm edit separator
2475                 if {$last ne {}
2476                         && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
2477                         $ui_comm insert end "\n"
2478                 }
2479                 $ui_comm insert end "\n$sob"
2480                 $ui_comm edit separator
2481                 $ui_comm see end
2482         }
2485 proc do_select_commit_type {} {
2486         global commit_type selected_commit_type
2488         if {$selected_commit_type eq {new}
2489                 && [string match amend* $commit_type]} {
2490                 create_new_commit
2491         } elseif {$selected_commit_type eq {amend}
2492                 && ![string match amend* $commit_type]} {
2493                 load_last_commit
2495                 # The amend request was rejected...
2496                 #
2497                 if {![string match amend* $commit_type]} {
2498                         set selected_commit_type new
2499                 }
2500         }
2503 proc do_commit {} {
2504         commit_tree
2507 proc do_about {} {
2508         global appname appvers copyright
2509         global tcl_patchLevel tk_patchLevel
2511         set w .about_dialog
2512         toplevel $w
2513         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2515         label $w.header -text "About $appname" \
2516                 -font font_uibold
2517         pack $w.header -side top -fill x
2519         frame $w.buttons
2520         button $w.buttons.close -text {Close} \
2521                 -font font_ui \
2522                 -command [list destroy $w]
2523         pack $w.buttons.close -side right
2524         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2526         label $w.desc \
2527                 -text "$appname - a commit creation tool for Git.
2528 $copyright" \
2529                 -padx 5 -pady 5 \
2530                 -justify left \
2531                 -anchor w \
2532                 -borderwidth 1 \
2533                 -relief solid \
2534                 -font font_ui
2535         pack $w.desc -side top -fill x -padx 5 -pady 5
2537         set v {}
2538         append v "$appname version $appvers\n\n"
2539         append v "[exec git --version]\n\n"
2540         if {$tcl_patchLevel eq $tk_patchLevel} {
2541                 append v "Tcl/Tk version $tcl_patchLevel"
2542         } else {
2543                 append v "Tcl version $tcl_patchLevel"
2544                 append v ", Tk version $tk_patchLevel"
2545         }
2547         label $w.vers \
2548                 -text $v \
2549                 -padx 5 -pady 5 \
2550                 -justify left \
2551                 -anchor w \
2552                 -borderwidth 1 \
2553                 -relief solid \
2554                 -font font_ui
2555         pack $w.vers -side top -fill x -padx 5 -pady 5
2557         bind $w <Visibility> "grab $w; focus $w"
2558         bind $w <Key-Escape> "destroy $w"
2559         wm title $w "About $appname"
2560         tkwait window $w
2563 proc do_options {} {
2564         global appname gitdir font_descs
2565         global repo_config global_config
2566         global repo_config_new global_config_new
2568         array unset repo_config_new
2569         array unset global_config_new
2570         foreach name [array names repo_config] {
2571                 set repo_config_new($name) $repo_config($name)
2572         }
2573         load_config 1
2574         foreach name [array names repo_config] {
2575                 switch -- $name {
2576                 gui.diffcontext {continue}
2577                 }
2578                 set repo_config_new($name) $repo_config($name)
2579         }
2580         foreach name [array names global_config] {
2581                 set global_config_new($name) $global_config($name)
2582         }
2583         set reponame [lindex [file split \
2584                 [file normalize [file dirname $gitdir]]] \
2585                 end]
2587         set w .options_editor
2588         toplevel $w
2589         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2591         label $w.header -text "$appname Options" \
2592                 -font font_uibold
2593         pack $w.header -side top -fill x
2595         frame $w.buttons
2596         button $w.buttons.restore -text {Restore Defaults} \
2597                 -font font_ui \
2598                 -command do_restore_defaults
2599         pack $w.buttons.restore -side left
2600         button $w.buttons.save -text Save \
2601                 -font font_ui \
2602                 -command [list do_save_config $w]
2603         pack $w.buttons.save -side right
2604         button $w.buttons.cancel -text {Cancel} \
2605                 -font font_ui \
2606                 -command [list destroy $w]
2607         pack $w.buttons.cancel -side right
2608         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2610         labelframe $w.repo -text "$reponame Repository" \
2611                 -font font_ui \
2612                 -relief raised -borderwidth 2
2613         labelframe $w.global -text {Global (All Repositories)} \
2614                 -font font_ui \
2615                 -relief raised -borderwidth 2
2616         pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
2617         pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
2619         foreach option {
2620                 {b partialinclude {Allow Partially Added Files}}
2621                 {b pullsummary {Show Pull Summary}}
2622                 {b trustmtime  {Trust File Modification Timestamps}}
2623                 {i diffcontext {Number of Diff Context Lines}}
2624                 } {
2625                 set type [lindex $option 0]
2626                 set name [lindex $option 1]
2627                 set text [lindex $option 2]
2628                 foreach f {repo global} {
2629                         switch $type {
2630                         b {
2631                                 checkbutton $w.$f.$name -text $text \
2632                                         -variable ${f}_config_new(gui.$name) \
2633                                         -onvalue true \
2634                                         -offvalue false \
2635                                         -font font_ui
2636                                 pack $w.$f.$name -side top -anchor w
2637                         }
2638                         i {
2639                                 frame $w.$f.$name
2640                                 label $w.$f.$name.l -text "$text:" -font font_ui
2641                                 pack $w.$f.$name.l -side left -anchor w -fill x
2642                                 spinbox $w.$f.$name.v \
2643                                         -textvariable ${f}_config_new(gui.$name) \
2644                                         -from 1 -to 99 -increment 1 \
2645                                         -width 3 \
2646                                         -font font_ui
2647                                 pack $w.$f.$name.v -side right -anchor e
2648                                 pack $w.$f.$name -side top -anchor w -fill x
2649                         }
2650                         }
2651                 }
2652         }
2654         set all_fonts [lsort [font families]]
2655         foreach option $font_descs {
2656                 set name [lindex $option 0]
2657                 set font [lindex $option 1]
2658                 set text [lindex $option 2]
2660                 set global_config_new(gui.$font^^family) \
2661                         [font configure $font -family]
2662                 set global_config_new(gui.$font^^size) \
2663                         [font configure $font -size]
2665                 frame $w.global.$name
2666                 label $w.global.$name.l -text "$text:" -font font_ui
2667                 pack $w.global.$name.l -side left -anchor w -fill x
2668                 eval tk_optionMenu $w.global.$name.family \
2669                         global_config_new(gui.$font^^family) \
2670                         $all_fonts
2671                 spinbox $w.global.$name.size \
2672                         -textvariable global_config_new(gui.$font^^size) \
2673                         -from 2 -to 80 -increment 1 \
2674                         -width 3 \
2675                         -font font_ui
2676                 pack $w.global.$name.size -side right -anchor e
2677                 pack $w.global.$name.family -side right -anchor e
2678                 pack $w.global.$name -side top -anchor w -fill x
2679         }
2681         bind $w <Visibility> "grab $w; focus $w"
2682         bind $w <Key-Escape> "destroy $w"
2683         wm title $w "$appname ($reponame): Options"
2684         tkwait window $w
2687 proc do_restore_defaults {} {
2688         global font_descs default_config repo_config
2689         global repo_config_new global_config_new
2691         foreach name [array names default_config] {
2692                 set repo_config_new($name) $default_config($name)
2693                 set global_config_new($name) $default_config($name)
2694         }
2696         foreach option $font_descs {
2697                 set name [lindex $option 0]
2698                 set repo_config(gui.$name) $default_config(gui.$name)
2699         }
2700         apply_config
2702         foreach option $font_descs {
2703                 set name [lindex $option 0]
2704                 set font [lindex $option 1]
2705                 set global_config_new(gui.$font^^family) \
2706                         [font configure $font -family]
2707                 set global_config_new(gui.$font^^size) \
2708                         [font configure $font -size]
2709         }
2712 proc do_save_config {w} {
2713         if {[catch {save_config} err]} {
2714                 error_popup "Failed to completely save options:\n\n$err"
2715         }
2716         reshow_diff
2717         destroy $w
2720 proc do_windows_shortcut {} {
2721         global gitdir appname argv0
2723         set reponame [lindex [file split \
2724                 [file normalize [file dirname $gitdir]]] \
2725                 end]
2727         if {[catch {
2728                 set desktop [exec cygpath \
2729                         --windows \
2730                         --absolute \
2731                         --long-name \
2732                         --desktop]
2733                 }]} {
2734                         set desktop .
2735         }
2736         set fn [tk_getSaveFile \
2737                 -parent . \
2738                 -title "$appname ($reponame): Create Desktop Icon" \
2739                 -initialdir $desktop \
2740                 -initialfile "Git $reponame.bat"]
2741         if {$fn != {}} {
2742                 if {[catch {
2743                                 set fd [open $fn w]
2744                                 set sh [exec cygpath \
2745                                         --windows \
2746                                         --absolute \
2747                                         /bin/sh]
2748                                 set me [exec cygpath \
2749                                         --unix \
2750                                         --absolute \
2751                                         $argv0]
2752                                 set gd [exec cygpath \
2753                                         --unix \
2754                                         --absolute \
2755                                         $gitdir]
2756                                 regsub -all ' $me "'\\''" me
2757                                 regsub -all ' $gd "'\\''" gd
2758                                 puts $fd "@ECHO Starting git-gui... Please wait..."
2759                                 puts -nonewline $fd "@\"$sh\" --login -c \""
2760                                 puts -nonewline $fd "GIT_DIR='$gd'"
2761                                 puts -nonewline $fd " '$me'"
2762                                 puts $fd "&\""
2763                                 close $fd
2764                         } err]} {
2765                         error_popup "Cannot write script:\n\n$err"
2766                 }
2767         }
2770 proc do_macosx_app {} {
2771         global gitdir appname argv0 env
2773         set reponame [lindex [file split \
2774                 [file normalize [file dirname $gitdir]]] \
2775                 end]
2777         set fn [tk_getSaveFile \
2778                 -parent . \
2779                 -title "$appname ($reponame): Create Desktop Icon" \
2780                 -initialdir [file join $env(HOME) Desktop] \
2781                 -initialfile "Git $reponame.app"]
2782         if {$fn != {}} {
2783                 if {[catch {
2784                                 set Contents [file join $fn Contents]
2785                                 set MacOS [file join $Contents MacOS]
2786                                 set exe [file join $MacOS git-gui]
2788                                 file mkdir $MacOS
2790                                 set fd [open [file join $Contents Info.plist] w]
2791                                 puts $fd {<?xml version="1.0" encoding="UTF-8"?>
2792 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2793 <plist version="1.0">
2794 <dict>
2795         <key>CFBundleDevelopmentRegion</key>
2796         <string>English</string>
2797         <key>CFBundleExecutable</key>
2798         <string>git-gui</string>
2799         <key>CFBundleIdentifier</key>
2800         <string>org.spearce.git-gui</string>
2801         <key>CFBundleInfoDictionaryVersion</key>
2802         <string>6.0</string>
2803         <key>CFBundlePackageType</key>
2804         <string>APPL</string>
2805         <key>CFBundleSignature</key>
2806         <string>????</string>
2807         <key>CFBundleVersion</key>
2808         <string>1.0</string>
2809         <key>NSPrincipalClass</key>
2810         <string>NSApplication</string>
2811 </dict>
2812 </plist>}
2813                                 close $fd
2815                                 set fd [open $exe w]
2816                                 set gd [file normalize $gitdir]
2817                                 set ep [file normalize [exec git --exec-path]]
2818                                 regsub -all ' $gd "'\\''" gd
2819                                 regsub -all ' $ep "'\\''" ep
2820                                 puts $fd "#!/bin/sh"
2821                                 foreach name [array names env] {
2822                                         if {[string match GIT_* $name]} {
2823                                                 regsub -all ' $env($name) "'\\''" v
2824                                                 puts $fd "export $name='$v'"
2825                                         }
2826                                 }
2827                                 puts $fd "export PATH='$ep':\$PATH"
2828                                 puts $fd "export GIT_DIR='$gd'"
2829                                 puts $fd "exec [file normalize $argv0]"
2830                                 close $fd
2832                                 file attributes $exe -permissions u+x,g+x,o+x
2833                         } err]} {
2834                         error_popup "Cannot write icon:\n\n$err"
2835                 }
2836         }
2839 proc toggle_or_diff {w x y} {
2840         global file_states file_lists current_diff ui_index ui_other
2841         global last_clicked selected_paths
2843         set pos [split [$w index @$x,$y] .]
2844         set lno [lindex $pos 0]
2845         set col [lindex $pos 1]
2846         set path [lindex $file_lists($w) [expr {$lno - 1}]]
2847         if {$path eq {}} {
2848                 set last_clicked {}
2849                 return
2850         }
2852         set last_clicked [list $w $lno]
2853         array unset selected_paths
2854         $ui_index tag remove in_sel 0.0 end
2855         $ui_other tag remove in_sel 0.0 end
2857         if {$col == 0} {
2858                 if {$current_diff eq $path} {
2859                         set after {reshow_diff;}
2860                 } else {
2861                         set after {}
2862                 }
2863                 switch -glob -- [lindex $file_states($path) 0] {
2864                 A_ -
2865                 M_ -
2866                 DD -
2867                 DO -
2868                 DM {
2869                         update_indexinfo \
2870                                 "Removing [short_path $path] from commit" \
2871                                 [list $path] \
2872                                 [concat $after {set ui_status_value {Ready.}}]
2873                 }
2874                 ?? {
2875                         update_index \
2876                                 "Adding [short_path $path]" \
2877                                 [list $path] \
2878                                 [concat $after {set ui_status_value {Ready.}}]
2879                 }
2880                 }
2881         } else {
2882                 show_diff $path $w $lno
2883         }
2886 proc add_one_to_selection {w x y} {
2887         global file_lists
2888         global last_clicked selected_paths
2890         set pos [split [$w index @$x,$y] .]
2891         set lno [lindex $pos 0]
2892         set col [lindex $pos 1]
2893         set path [lindex $file_lists($w) [expr {$lno - 1}]]
2894         if {$path eq {}} {
2895                 set last_clicked {}
2896                 return
2897         }
2899         set last_clicked [list $w $lno]
2900         if {[catch {set in_sel $selected_paths($path)}]} {
2901                 set in_sel 0
2902         }
2903         if {$in_sel} {
2904                 unset selected_paths($path)
2905                 $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
2906         } else {
2907                 set selected_paths($path) 1
2908                 $w tag add in_sel $lno.0 [expr {$lno + 1}].0
2909         }
2912 proc add_range_to_selection {w x y} {
2913         global file_lists
2914         global last_clicked selected_paths
2916         if {[lindex $last_clicked 0] ne $w} {
2917                 toggle_or_diff $w $x $y
2918                 return
2919         }
2921         set pos [split [$w index @$x,$y] .]
2922         set lno [lindex $pos 0]
2923         set lc [lindex $last_clicked 1]
2924         if {$lc < $lno} {
2925                 set begin $lc
2926                 set end $lno
2927         } else {
2928                 set begin $lno
2929                 set end $lc
2930         }
2932         foreach path [lrange $file_lists($w) \
2933                 [expr {$begin - 1}] \
2934                 [expr {$end - 1}]] {
2935                 set selected_paths($path) 1
2936         }
2937         $w tag add in_sel $begin.0 [expr {$end + 1}].0
2940 ######################################################################
2941 ##
2942 ## config defaults
2944 set cursor_ptr arrow
2945 font create font_diff -family Courier -size 10
2946 font create font_ui
2947 catch {
2948         label .dummy
2949         eval font configure font_ui [font actual [.dummy cget -font]]
2950         destroy .dummy
2953 font create font_uibold
2954 font create font_diffbold
2956 if {[is_Windows]} {
2957         set M1B Control
2958         set M1T Ctrl
2959 } elseif {[is_MacOSX]} {
2960         set M1B M1
2961         set M1T Cmd
2962 } else {
2963         set M1B M1
2964         set M1T M1
2967 proc apply_config {} {
2968         global repo_config font_descs
2970         foreach option $font_descs {
2971                 set name [lindex $option 0]
2972                 set font [lindex $option 1]
2973                 if {[catch {
2974                         foreach {cn cv} $repo_config(gui.$name) {
2975                                 font configure $font $cn $cv
2976                         }
2977                         } err]} {
2978                         error_popup "Invalid font specified in gui.$name:\n\n$err"
2979                 }
2980                 foreach {cn cv} [font configure $font] {
2981                         font configure ${font}bold $cn $cv
2982                 }
2983                 font configure ${font}bold -weight bold
2984         }
2987 set default_config(gui.trustmtime) false
2988 set default_config(gui.pullsummary) true
2989 set default_config(gui.partialinclude) false
2990 set default_config(gui.diffcontext) 5
2991 set default_config(gui.fontui) [font configure font_ui]
2992 set default_config(gui.fontdiff) [font configure font_diff]
2993 set font_descs {
2994         {fontui   font_ui   {Main Font}}
2995         {fontdiff font_diff {Diff/Console Font}}
2997 load_config 0
2998 apply_config
3000 ######################################################################
3001 ##
3002 ## ui construction
3004 # -- Menu Bar
3006 menu .mbar -tearoff 0
3007 .mbar add cascade -label Repository -menu .mbar.repository
3008 .mbar add cascade -label Edit -menu .mbar.edit
3009 if {!$single_commit} {
3010         .mbar add cascade -label Branch -menu .mbar.branch
3012 .mbar add cascade -label Commit -menu .mbar.commit
3013 if {!$single_commit} {
3014         .mbar add cascade -label Fetch -menu .mbar.fetch
3015         .mbar add cascade -label Pull -menu .mbar.pull
3016         .mbar add cascade -label Push -menu .mbar.push
3018 . configure -menu .mbar
3020 # -- Repository Menu
3022 menu .mbar.repository
3023 .mbar.repository add command \
3024         -label {Visualize Current Branch} \
3025         -command {do_gitk {}} \
3026         -font font_ui
3027 if {![is_MacOSX]} {
3028         .mbar.repository add command \
3029                 -label {Visualize All Branches} \
3030                 -command {do_gitk {--all}} \
3031                 -font font_ui
3033 .mbar.repository add separator
3035 if {!$single_commit} {
3036         .mbar.repository add command -label {Compress Database} \
3037                 -command do_gc \
3038                 -font font_ui
3040         .mbar.repository add command -label {Verify Database} \
3041                 -command do_fsck_objects \
3042                 -font font_ui
3044         .mbar.repository add separator
3046         if {[is_Windows]} {
3047                 .mbar.repository add command \
3048                         -label {Create Desktop Icon} \
3049                         -command do_windows_shortcut \
3050                         -font font_ui
3051         } elseif {[is_MacOSX]} {
3052                 .mbar.repository add command \
3053                         -label {Create Desktop Icon} \
3054                         -command do_macosx_app \
3055                         -font font_ui
3056         }
3059 .mbar.repository add command -label Quit \
3060         -command do_quit \
3061         -accelerator $M1T-Q \
3062         -font font_ui
3064 # -- Edit Menu
3066 menu .mbar.edit
3067 .mbar.edit add command -label Undo \
3068         -command {catch {[focus] edit undo}} \
3069         -accelerator $M1T-Z \
3070         -font font_ui
3071 .mbar.edit add command -label Redo \
3072         -command {catch {[focus] edit redo}} \
3073         -accelerator $M1T-Y \
3074         -font font_ui
3075 .mbar.edit add separator
3076 .mbar.edit add command -label Cut \
3077         -command {catch {tk_textCut [focus]}} \
3078         -accelerator $M1T-X \
3079         -font font_ui
3080 .mbar.edit add command -label Copy \
3081         -command {catch {tk_textCopy [focus]}} \
3082         -accelerator $M1T-C \
3083         -font font_ui
3084 .mbar.edit add command -label Paste \
3085         -command {catch {tk_textPaste [focus]; [focus] see insert}} \
3086         -accelerator $M1T-V \
3087         -font font_ui
3088 .mbar.edit add command -label Delete \
3089         -command {catch {[focus] delete sel.first sel.last}} \
3090         -accelerator Del \
3091         -font font_ui
3092 .mbar.edit add separator
3093 .mbar.edit add command -label {Select All} \
3094         -command {catch {[focus] tag add sel 0.0 end}} \
3095         -accelerator $M1T-A \
3096         -font font_ui
3098 # -- Branch Menu
3100 if {!$single_commit} {
3101         menu .mbar.branch
3103         .mbar.branch add command -label {Create...} \
3104                 -command do_create_branch \
3105                 -font font_ui
3106         lappend disable_on_lock [list .mbar.branch entryconf \
3107                 [.mbar.branch index last] -state]
3109         .mbar.branch add command -label {Delete...} \
3110                 -command do_delete_branch \
3111                 -font font_ui
3112         lappend disable_on_lock [list .mbar.branch entryconf \
3113                 [.mbar.branch index last] -state]
3116 # -- Commit Menu
3118 menu .mbar.commit
3120 .mbar.commit add radiobutton \
3121         -label {New Commit} \
3122         -command do_select_commit_type \
3123         -variable selected_commit_type \
3124         -value new \
3125         -font font_ui
3126 lappend disable_on_lock \
3127         [list .mbar.commit entryconf [.mbar.commit index last] -state]
3129 .mbar.commit add radiobutton \
3130         -label {Amend Last Commit} \
3131         -command do_select_commit_type \
3132         -variable selected_commit_type \
3133         -value amend \
3134         -font font_ui
3135 lappend disable_on_lock \
3136         [list .mbar.commit entryconf [.mbar.commit index last] -state]
3138 .mbar.commit add separator
3140 .mbar.commit add command -label Rescan \
3141         -command do_rescan \
3142         -accelerator F5 \
3143         -font font_ui
3144 lappend disable_on_lock \
3145         [list .mbar.commit entryconf [.mbar.commit index last] -state]
3147 .mbar.commit add command -label {Add To Commit} \
3148         -command do_include_selection \
3149         -font font_ui
3150 lappend disable_on_lock \
3151         [list .mbar.commit entryconf [.mbar.commit index last] -state]
3153 .mbar.commit add command -label {Add All To Commit} \
3154         -command do_include_all \
3155         -accelerator $M1T-I \
3156         -font font_ui
3157 lappend disable_on_lock \
3158         [list .mbar.commit entryconf [.mbar.commit index last] -state]
3160 .mbar.commit add command -label {Remove From Commit} \
3161         -command do_remove_selection \
3162         -font font_ui
3163 lappend disable_on_lock \
3164         [list .mbar.commit entryconf [.mbar.commit index last] -state]
3166 .mbar.commit add command -label {Revert Changes} \
3167         -command do_revert_selection \
3168         -font font_ui
3169 lappend disable_on_lock \
3170         [list .mbar.commit entryconf [.mbar.commit index last] -state]
3172 .mbar.commit add separator
3174 .mbar.commit add command -label {Sign Off} \
3175         -command do_signoff \
3176         -accelerator $M1T-S \
3177         -font font_ui
3179 .mbar.commit add command -label Commit \
3180         -command do_commit \
3181         -accelerator $M1T-Return \
3182         -font font_ui
3183 lappend disable_on_lock \
3184         [list .mbar.commit entryconf [.mbar.commit index last] -state]
3186 # -- Transport menus
3188 if {!$single_commit} {
3189         menu .mbar.fetch
3190         menu .mbar.pull
3191         menu .mbar.push
3194 if {[is_MacOSX]} {
3195         # -- Apple Menu (Mac OS X only)
3196         #
3197         .mbar add cascade -label Apple -menu .mbar.apple
3198         menu .mbar.apple
3200         .mbar.apple add command -label "About $appname" \
3201                 -command do_about \
3202                 -font font_ui
3203         .mbar.apple add command -label "$appname Options..." \
3204                 -command do_options \
3205                 -font font_ui
3206 } else {
3207         # -- Edit Menu
3208         #
3209         .mbar.edit add separator
3210         .mbar.edit add command -label {Options...} \
3211                 -command do_options \
3212                 -font font_ui
3214         # -- Tools Menu
3215         #
3216         if {[file exists /usr/local/miga/lib/gui-miga]} {
3217         proc do_miga {} {
3218                 global gitdir ui_status_value
3219                 if {![lock_index update]} return
3220                 set cmd [list sh --login -c "/usr/local/miga/lib/gui-miga \"[pwd]\""]
3221                 set miga_fd [open "|$cmd" r]
3222                 fconfigure $miga_fd -blocking 0
3223                 fileevent $miga_fd readable [list miga_done $miga_fd]
3224                 set ui_status_value {Running miga...}
3225         }
3226         proc miga_done {fd} {
3227                 read $fd 512
3228                 if {[eof $fd]} {
3229                         close $fd
3230                         unlock_index
3231                         rescan [list set ui_status_value {Ready.}]
3232                 }
3233         }
3234         .mbar add cascade -label Tools -menu .mbar.tools
3235         menu .mbar.tools
3236         .mbar.tools add command -label "Migrate" \
3237                 -command do_miga \
3238                 -font font_ui
3239         lappend disable_on_lock \
3240                 [list .mbar.tools entryconf [.mbar.tools index last] -state]
3241         }
3243         # -- Help Menu
3244         #
3245         .mbar add cascade -label Help -menu .mbar.help
3246         menu .mbar.help
3248         .mbar.help add command -label "About $appname" \
3249                 -command do_about \
3250                 -font font_ui
3254 # -- Branch Control
3256 frame .branch \
3257         -borderwidth 1 \
3258         -relief sunken
3259 label .branch.l1 \
3260         -text {Current Branch:} \
3261         -anchor w \
3262         -justify left \
3263         -font font_ui
3264 label .branch.cb \
3265         -textvariable current_branch \
3266         -anchor w \
3267         -justify left \
3268         -font font_ui
3269 pack .branch.l1 -side left
3270 pack .branch.cb -side left -fill x
3271 pack .branch -side top -fill x
3273 # -- Main Window Layout
3275 panedwindow .vpane -orient vertical
3276 panedwindow .vpane.files -orient horizontal
3277 .vpane add .vpane.files -sticky nsew -height 100 -width 400
3278 pack .vpane -anchor n -side top -fill both -expand 1
3280 # -- Index File List
3282 frame .vpane.files.index -height 100 -width 400
3283 label .vpane.files.index.title -text {Modified Files} \
3284         -background green \
3285         -font font_ui
3286 text $ui_index -background white -borderwidth 0 \
3287         -width 40 -height 10 \
3288         -font font_ui \
3289         -cursor $cursor_ptr \
3290         -yscrollcommand {.vpane.files.index.sb set} \
3291         -state disabled
3292 scrollbar .vpane.files.index.sb -command [list $ui_index yview]
3293 pack .vpane.files.index.title -side top -fill x
3294 pack .vpane.files.index.sb -side right -fill y
3295 pack $ui_index -side left -fill both -expand 1
3296 .vpane.files add .vpane.files.index -sticky nsew
3298 # -- Other (Add) File List
3300 frame .vpane.files.other -height 100 -width 100
3301 label .vpane.files.other.title -text {Untracked Files} \
3302         -background red \
3303         -font font_ui
3304 text $ui_other -background white -borderwidth 0 \
3305         -width 40 -height 10 \
3306         -font font_ui \
3307         -cursor $cursor_ptr \
3308         -yscrollcommand {.vpane.files.other.sb set} \
3309         -state disabled
3310 scrollbar .vpane.files.other.sb -command [list $ui_other yview]
3311 pack .vpane.files.other.title -side top -fill x
3312 pack .vpane.files.other.sb -side right -fill y
3313 pack $ui_other -side left -fill both -expand 1
3314 .vpane.files add .vpane.files.other -sticky nsew
3316 foreach i [list $ui_index $ui_other] {
3317         $i tag conf in_diff -font font_uibold
3318         $i tag conf in_sel \
3319                 -background [$i cget -foreground] \
3320                 -foreground [$i cget -background]
3322 unset i
3324 # -- Diff and Commit Area
3326 frame .vpane.lower -height 300 -width 400
3327 frame .vpane.lower.commarea
3328 frame .vpane.lower.diff -relief sunken -borderwidth 1
3329 pack .vpane.lower.commarea -side top -fill x
3330 pack .vpane.lower.diff -side bottom -fill both -expand 1
3331 .vpane add .vpane.lower -stick nsew
3333 # -- Commit Area Buttons
3335 frame .vpane.lower.commarea.buttons
3336 label .vpane.lower.commarea.buttons.l -text {} \
3337         -anchor w \
3338         -justify left \
3339         -font font_ui
3340 pack .vpane.lower.commarea.buttons.l -side top -fill x
3341 pack .vpane.lower.commarea.buttons -side left -fill y
3343 button .vpane.lower.commarea.buttons.rescan -text {Rescan} \
3344         -command do_rescan \
3345         -font font_ui
3346 pack .vpane.lower.commarea.buttons.rescan -side top -fill x
3347 lappend disable_on_lock \
3348         {.vpane.lower.commarea.buttons.rescan conf -state}
3350 button .vpane.lower.commarea.buttons.incall -text {Add All} \
3351         -command do_include_all \
3352         -font font_ui
3353 pack .vpane.lower.commarea.buttons.incall -side top -fill x
3354 lappend disable_on_lock \
3355         {.vpane.lower.commarea.buttons.incall conf -state}
3357 button .vpane.lower.commarea.buttons.signoff -text {Sign Off} \
3358         -command do_signoff \
3359         -font font_ui
3360 pack .vpane.lower.commarea.buttons.signoff -side top -fill x
3362 button .vpane.lower.commarea.buttons.commit -text {Commit} \
3363         -command do_commit \
3364         -font font_ui
3365 pack .vpane.lower.commarea.buttons.commit -side top -fill x
3366 lappend disable_on_lock \
3367         {.vpane.lower.commarea.buttons.commit conf -state}
3369 # -- Commit Message Buffer
3371 frame .vpane.lower.commarea.buffer
3372 frame .vpane.lower.commarea.buffer.header
3373 set ui_comm .vpane.lower.commarea.buffer.t
3374 set ui_coml .vpane.lower.commarea.buffer.header.l
3375 radiobutton .vpane.lower.commarea.buffer.header.new \
3376         -text {New Commit} \
3377         -command do_select_commit_type \
3378         -variable selected_commit_type \
3379         -value new \
3380         -font font_ui
3381 lappend disable_on_lock \
3382         [list .vpane.lower.commarea.buffer.header.new conf -state]
3383 radiobutton .vpane.lower.commarea.buffer.header.amend \
3384         -text {Amend Last Commit} \
3385         -command do_select_commit_type \
3386         -variable selected_commit_type \
3387         -value amend \
3388         -font font_ui
3389 lappend disable_on_lock \
3390         [list .vpane.lower.commarea.buffer.header.amend conf -state]
3391 label $ui_coml \
3392         -anchor w \
3393         -justify left \
3394         -font font_ui
3395 proc trace_commit_type {varname args} {
3396         global ui_coml commit_type
3397         switch -glob -- $commit_type {
3398         initial       {set txt {Initial Commit Message:}}
3399         amend         {set txt {Amended Commit Message:}}
3400         amend-initial {set txt {Amended Initial Commit Message:}}
3401         amend-merge   {set txt {Amended Merge Commit Message:}}
3402         merge         {set txt {Merge Commit Message:}}
3403         *             {set txt {Commit Message:}}
3404         }
3405         $ui_coml conf -text $txt
3407 trace add variable commit_type write trace_commit_type
3408 pack $ui_coml -side left -fill x
3409 pack .vpane.lower.commarea.buffer.header.amend -side right
3410 pack .vpane.lower.commarea.buffer.header.new -side right
3412 text $ui_comm -background white -borderwidth 1 \
3413         -undo true \
3414         -maxundo 20 \
3415         -autoseparators true \
3416         -relief sunken \
3417         -width 75 -height 9 -wrap none \
3418         -font font_diff \
3419         -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
3420 scrollbar .vpane.lower.commarea.buffer.sby \
3421         -command [list $ui_comm yview]
3422 pack .vpane.lower.commarea.buffer.header -side top -fill x
3423 pack .vpane.lower.commarea.buffer.sby -side right -fill y
3424 pack $ui_comm -side left -fill y
3425 pack .vpane.lower.commarea.buffer -side left -fill y
3427 # -- Commit Message Buffer Context Menu
3429 set ctxm .vpane.lower.commarea.buffer.ctxm
3430 menu $ctxm -tearoff 0
3431 $ctxm add command \
3432         -label {Cut} \
3433         -font font_ui \
3434         -command {tk_textCut $ui_comm}
3435 $ctxm add command \
3436         -label {Copy} \
3437         -font font_ui \
3438         -command {tk_textCopy $ui_comm}
3439 $ctxm add command \
3440         -label {Paste} \
3441         -font font_ui \
3442         -command {tk_textPaste $ui_comm}
3443 $ctxm add command \
3444         -label {Delete} \
3445         -font font_ui \
3446         -command {$ui_comm delete sel.first sel.last}
3447 $ctxm add separator
3448 $ctxm add command \
3449         -label {Select All} \
3450         -font font_ui \
3451         -command {$ui_comm tag add sel 0.0 end}
3452 $ctxm add command \
3453         -label {Copy All} \
3454         -font font_ui \
3455         -command {
3456                 $ui_comm tag add sel 0.0 end
3457                 tk_textCopy $ui_comm
3458                 $ui_comm tag remove sel 0.0 end
3459         }
3460 $ctxm add separator
3461 $ctxm add command \
3462         -label {Sign Off} \
3463         -font font_ui \
3464         -command do_signoff
3465 bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
3467 # -- Diff Header
3469 set current_diff {}
3470 set diff_actions [list]
3471 proc trace_current_diff {varname args} {
3472         global current_diff diff_actions file_states
3473         if {$current_diff eq {}} {
3474                 set s {}
3475                 set f {}
3476                 set p {}
3477                 set o disabled
3478         } else {
3479                 set p $current_diff
3480                 set s [mapdesc [lindex $file_states($p) 0] $p]
3481                 set f {File:}
3482                 set p [escape_path $p]
3483                 set o normal
3484         }
3486         .vpane.lower.diff.header.status configure -text $s
3487         .vpane.lower.diff.header.file configure -text $f
3488         .vpane.lower.diff.header.path configure -text $p
3489         foreach w $diff_actions {
3490                 uplevel #0 $w $o
3491         }
3493 trace add variable current_diff write trace_current_diff
3495 frame .vpane.lower.diff.header -background orange
3496 label .vpane.lower.diff.header.status \
3497         -background orange \
3498         -width $max_status_desc \
3499         -anchor w \
3500         -justify left \
3501         -font font_ui
3502 label .vpane.lower.diff.header.file \
3503         -background orange \
3504         -anchor w \
3505         -justify left \
3506         -font font_ui
3507 label .vpane.lower.diff.header.path \
3508         -background orange \
3509         -anchor w \
3510         -justify left \
3511         -font font_ui
3512 pack .vpane.lower.diff.header.status -side left
3513 pack .vpane.lower.diff.header.file -side left
3514 pack .vpane.lower.diff.header.path -fill x
3515 set ctxm .vpane.lower.diff.header.ctxm
3516 menu $ctxm -tearoff 0
3517 $ctxm add command \
3518         -label {Copy} \
3519         -font font_ui \
3520         -command {
3521                 clipboard clear
3522                 clipboard append \
3523                         -format STRING \
3524                         -type STRING \
3525                         -- $current_diff
3526         }
3527 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3528 bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
3530 # -- Diff Body
3532 frame .vpane.lower.diff.body
3533 set ui_diff .vpane.lower.diff.body.t
3534 text $ui_diff -background white -borderwidth 0 \
3535         -width 80 -height 15 -wrap none \
3536         -font font_diff \
3537         -xscrollcommand {.vpane.lower.diff.body.sbx set} \
3538         -yscrollcommand {.vpane.lower.diff.body.sby set} \
3539         -state disabled
3540 scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
3541         -command [list $ui_diff xview]
3542 scrollbar .vpane.lower.diff.body.sby -orient vertical \
3543         -command [list $ui_diff yview]
3544 pack .vpane.lower.diff.body.sbx -side bottom -fill x
3545 pack .vpane.lower.diff.body.sby -side right -fill y
3546 pack $ui_diff -side left -fill both -expand 1
3547 pack .vpane.lower.diff.header -side top -fill x
3548 pack .vpane.lower.diff.body -side bottom -fill both -expand 1
3550 $ui_diff tag conf d_@ -font font_diffbold
3551 $ui_diff tag conf d_+  -foreground blue
3552 $ui_diff tag conf d_-  -foreground red
3553 $ui_diff tag conf d_++ -foreground {#00a000}
3554 $ui_diff tag conf d_-- -foreground {#a000a0}
3555 $ui_diff tag conf d_+- \
3556         -foreground red \
3557         -background {light goldenrod yellow}
3558 $ui_diff tag conf d_-+ \
3559         -foreground blue \
3560         -background azure2
3562 # -- Diff Body Context Menu
3564 set ctxm .vpane.lower.diff.body.ctxm
3565 menu $ctxm -tearoff 0
3566 $ctxm add command \
3567         -label {Copy} \
3568         -font font_ui \
3569         -command {tk_textCopy $ui_diff}
3570 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3571 $ctxm add command \
3572         -label {Select All} \
3573         -font font_ui \
3574         -command {$ui_diff tag add sel 0.0 end}
3575 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3576 $ctxm add command \
3577         -label {Copy All} \
3578         -font font_ui \
3579         -command {
3580                 $ui_diff tag add sel 0.0 end
3581                 tk_textCopy $ui_diff
3582                 $ui_diff tag remove sel 0.0 end
3583         }
3584 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3585 $ctxm add separator
3586 $ctxm add command \
3587         -label {Decrease Font Size} \
3588         -font font_ui \
3589         -command {incr_font_size font_diff -1}
3590 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3591 $ctxm add command \
3592         -label {Increase Font Size} \
3593         -font font_ui \
3594         -command {incr_font_size font_diff 1}
3595 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3596 $ctxm add separator
3597 $ctxm add command \
3598         -label {Show Less Context} \
3599         -font font_ui \
3600         -command {if {$repo_config(gui.diffcontext) >= 2} {
3601                 incr repo_config(gui.diffcontext) -1
3602                 reshow_diff
3603         }}
3604 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3605 $ctxm add command \
3606         -label {Show More Context} \
3607         -font font_ui \
3608         -command {
3609                 incr repo_config(gui.diffcontext)
3610                 reshow_diff
3611         }
3612 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3613 $ctxm add separator
3614 $ctxm add command -label {Options...} \
3615         -font font_ui \
3616         -command do_options
3617 bind_button3 $ui_diff "tk_popup $ctxm %X %Y"
3619 # -- Status Bar
3621 set ui_status_value {Initializing...}
3622 label .status -textvariable ui_status_value \
3623         -anchor w \
3624         -justify left \
3625         -borderwidth 1 \
3626         -relief sunken \
3627         -font font_ui
3628 pack .status -anchor w -side bottom -fill x
3630 # -- Load geometry
3632 catch {
3633 set gm $repo_config(gui.geometry)
3634 wm geometry . [lindex $gm 0]
3635 .vpane sash place 0 \
3636         [lindex [.vpane sash coord 0] 0] \
3637         [lindex $gm 1]
3638 .vpane.files sash place 0 \
3639         [lindex $gm 2] \
3640         [lindex [.vpane.files sash coord 0] 1]
3641 unset gm
3644 # -- Key Bindings
3646 bind $ui_comm <$M1B-Key-Return> {do_commit;break}
3647 bind $ui_comm <$M1B-Key-i> {do_include_all;break}
3648 bind $ui_comm <$M1B-Key-I> {do_include_all;break}
3649 bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
3650 bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
3651 bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
3652 bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
3653 bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
3654 bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
3655 bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
3656 bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
3658 bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
3659 bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
3660 bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
3661 bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
3662 bind $ui_diff <$M1B-Key-v> {break}
3663 bind $ui_diff <$M1B-Key-V> {break}
3664 bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
3665 bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
3666 bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
3667 bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
3668 bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
3669 bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
3671 bind .   <Destroy> do_quit
3672 bind all <Key-F5> do_rescan
3673 bind all <$M1B-Key-r> do_rescan
3674 bind all <$M1B-Key-R> do_rescan
3675 bind .   <$M1B-Key-s> do_signoff
3676 bind .   <$M1B-Key-S> do_signoff
3677 bind .   <$M1B-Key-i> do_include_all
3678 bind .   <$M1B-Key-I> do_include_all
3679 bind .   <$M1B-Key-Return> do_commit
3680 bind all <$M1B-Key-q> do_quit
3681 bind all <$M1B-Key-Q> do_quit
3682 bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
3683 bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
3684 foreach i [list $ui_index $ui_other] {
3685         bind $i <Button-1>       "toggle_or_diff         $i %x %y; break"
3686         bind $i <$M1B-Button-1>  "add_one_to_selection   $i %x %y; break"
3687         bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
3689 unset i
3691 set file_lists($ui_index) [list]
3692 set file_lists($ui_other) [list]
3694 set HEAD {}
3695 set PARENT {}
3696 set MERGE_HEAD [list]
3697 set commit_type {}
3698 set empty_tree {}
3699 set current_branch {}
3700 set current_diff {}
3701 set selected_commit_type new
3703 wm title . "$appname ([file normalize [file dirname $gitdir]])"
3704 focus -force $ui_comm
3706 # -- Warn the user about environmental problems.  Cygwin's Tcl
3707 #    does *not* pass its env array onto any processes it spawns.
3708 #    This means that git processes get none of our environment.
3710 if {[is_Windows]} {
3711         set ignored_env 0
3712         set suggest_user {}
3713         set msg "Possible environment issues exist.
3715 The following environment variables are probably
3716 going to be ignored by any Git subprocess run
3717 by $appname:
3720         foreach name [array names env] {
3721                 switch -regexp -- $name {
3722                 {^GIT_INDEX_FILE$} -
3723                 {^GIT_OBJECT_DIRECTORY$} -
3724                 {^GIT_ALTERNATE_OBJECT_DIRECTORIES$} -
3725                 {^GIT_DIFF_OPTS$} -
3726                 {^GIT_EXTERNAL_DIFF$} -
3727                 {^GIT_PAGER$} -
3728                 {^GIT_TRACE$} -
3729                 {^GIT_CONFIG$} -
3730                 {^GIT_CONFIG_LOCAL$} -
3731                 {^GIT_(AUTHOR|COMMITTER)_DATE$} {
3732                         append msg " - $name\n"
3733                         incr ignored_env
3734                 }
3735                 {^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$} {
3736                         append msg " - $name\n"
3737                         incr ignored_env
3738                         set suggest_user $name
3739                 }
3740                 }
3741         }
3742         if {$ignored_env > 0} {
3743                 append msg "
3744 This is due to a known issue with the
3745 Tcl binary distributed by Cygwin."
3747                 if {$suggest_user ne {}} {
3748                         append msg "
3750 A good replacement for $suggest_user
3751 is placing values for the user.name and
3752 user.email settings into your personal
3753 ~/.gitconfig file.
3755                 }
3756                 warn_popup $msg
3757         }
3758         unset ignored_env msg suggest_user name
3761 # -- Only initialize complex UI if we are going to stay running.
3763 if {!$single_commit} {
3764         load_all_remotes
3765         load_all_heads
3767         populate_branch_menu .mbar.branch
3768         populate_fetch_menu .mbar.fetch
3769         populate_pull_menu .mbar.pull
3770         populate_push_menu .mbar.push
3773 lock_index begin-read
3774 after 1 do_rescan