git-gui: Correctly handle UTF-8 encoded commit messages
[git] / git-gui.sh
1 #!/bin/sh
2 # Tcl ignores the next line -*- tcl -*- \
3 exec wish "$0" -- "$@"
4
5 set appvers {@@GITGUI_VERSION@@}
6 set copyright {
7 Copyright © 2006, 2007 Shawn Pearce, et. al.
8
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2 of the License, or
12 (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA}
22
23 ######################################################################
24 ##
25 ## read only globals
26
27 set _appname [lindex [file split $argv0] end]
28 set _gitdir {}
29 set _gitexec {}
30 set _reponame {}
31 set _iscygwin {}
32
33 proc appname {} {
34         global _appname
35         return $_appname
36 }
37
38 proc gitdir {args} {
39         global _gitdir
40         if {$args eq {}} {
41                 return $_gitdir
42         }
43         return [eval [concat [list file join $_gitdir] $args]]
44 }
45
46 proc gitexec {args} {
47         global _gitexec
48         if {$_gitexec eq {}} {
49                 if {[catch {set _gitexec [git --exec-path]} err]} {
50                         error "Git not installed?\n\n$err"
51                 }
52         }
53         if {$args eq {}} {
54                 return $_gitexec
55         }
56         return [eval [concat [list file join $_gitexec] $args]]
57 }
58
59 proc reponame {} {
60         global _reponame
61         return $_reponame
62 }
63
64 proc is_MacOSX {} {
65         global tcl_platform tk_library
66         if {[tk windowingsystem] eq {aqua}} {
67                 return 1
68         }
69         return 0
70 }
71
72 proc is_Windows {} {
73         global tcl_platform
74         if {$tcl_platform(platform) eq {windows}} {
75                 return 1
76         }
77         return 0
78 }
79
80 proc is_Cygwin {} {
81         global tcl_platform _iscygwin
82         if {$_iscygwin eq {}} {
83                 if {$tcl_platform(platform) eq {windows}} {
84                         if {[catch {set p [exec cygpath --windir]} err]} {
85                                 set _iscygwin 0
86                         } else {
87                                 set _iscygwin 1
88                         }
89                 } else {
90                         set _iscygwin 0
91                 }
92         }
93         return $_iscygwin
94 }
95
96 proc is_enabled {option} {
97         global enabled_options
98         if {[catch {set on $enabled_options($option)}]} {return 0}
99         return $on
100 }
101
102 proc enable_option {option} {
103         global enabled_options
104         set enabled_options($option) 1
105 }
106
107 proc disable_option {option} {
108         global enabled_options
109         set enabled_options($option) 0
110 }
111
112 ######################################################################
113 ##
114 ## config
115
116 proc is_many_config {name} {
117         switch -glob -- $name {
118         remote.*.fetch -
119         remote.*.push
120                 {return 1}
121         *
122                 {return 0}
123         }
124 }
125
126 proc is_config_true {name} {
127         global repo_config
128         if {[catch {set v $repo_config($name)}]} {
129                 return 0
130         } elseif {$v eq {true} || $v eq {1} || $v eq {yes}} {
131                 return 1
132         } else {
133                 return 0
134         }
135 }
136
137 proc load_config {include_global} {
138         global repo_config global_config default_config
139
140         array unset global_config
141         if {$include_global} {
142                 catch {
143                         set fd_rc [open "| git config --global --list" r]
144                         while {[gets $fd_rc line] >= 0} {
145                                 if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
146                                         if {[is_many_config $name]} {
147                                                 lappend global_config($name) $value
148                                         } else {
149                                                 set global_config($name) $value
150                                         }
151                                 }
152                         }
153                         close $fd_rc
154                 }
155         }
156
157         array unset repo_config
158         catch {
159                 set fd_rc [open "| git config --list" r]
160                 while {[gets $fd_rc line] >= 0} {
161                         if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
162                                 if {[is_many_config $name]} {
163                                         lappend repo_config($name) $value
164                                 } else {
165                                         set repo_config($name) $value
166                                 }
167                         }
168                 }
169                 close $fd_rc
170         }
171
172         foreach name [array names default_config] {
173                 if {[catch {set v $global_config($name)}]} {
174                         set global_config($name) $default_config($name)
175                 }
176                 if {[catch {set v $repo_config($name)}]} {
177                         set repo_config($name) $default_config($name)
178                 }
179         }
180 }
181
182 proc save_config {} {
183         global default_config font_descs
184         global repo_config global_config
185         global repo_config_new global_config_new
186
187         foreach option $font_descs {
188                 set name [lindex $option 0]
189                 set font [lindex $option 1]
190                 font configure $font \
191                         -family $global_config_new(gui.$font^^family) \
192                         -size $global_config_new(gui.$font^^size)
193                 font configure ${font}bold \
194                         -family $global_config_new(gui.$font^^family) \
195                         -size $global_config_new(gui.$font^^size)
196                 set global_config_new(gui.$name) [font configure $font]
197                 unset global_config_new(gui.$font^^family)
198                 unset global_config_new(gui.$font^^size)
199         }
200
201         foreach name [array names default_config] {
202                 set value $global_config_new($name)
203                 if {$value ne $global_config($name)} {
204                         if {$value eq $default_config($name)} {
205                                 catch {git config --global --unset $name}
206                         } else {
207                                 regsub -all "\[{}\]" $value {"} value
208                                 git config --global $name $value
209                         }
210                         set global_config($name) $value
211                         if {$value eq $repo_config($name)} {
212                                 catch {git config --unset $name}
213                                 set repo_config($name) $value
214                         }
215                 }
216         }
217
218         foreach name [array names default_config] {
219                 set value $repo_config_new($name)
220                 if {$value ne $repo_config($name)} {
221                         if {$value eq $global_config($name)} {
222                                 catch {git config --unset $name}
223                         } else {
224                                 regsub -all "\[{}\]" $value {"} value
225                                 git config $name $value
226                         }
227                         set repo_config($name) $value
228                 }
229         }
230 }
231
232 ######################################################################
233 ##
234 ## handy utils
235
236 proc git {args} {
237         return [eval exec git $args]
238 }
239
240 proc error_popup {msg} {
241         set title [appname]
242         if {[reponame] ne {}} {
243                 append title " ([reponame])"
244         }
245         option add *Dialog.msg.font font_ui
246         option add *Button.font font_ui
247         set cmd [list tk_messageBox \
248                 -icon error \
249                 -type ok \
250                 -title "$title: error" \
251                 -message $msg]
252         if {[winfo ismapped .]} {
253                 lappend cmd -parent .
254         }
255         eval $cmd
256 }
257
258 proc warn_popup {msg} {
259         set title [appname]
260         if {[reponame] ne {}} {
261                 append title " ([reponame])"
262         }
263         option add *Dialog.msg.font font_ui
264         option add *Button.font font_ui
265         set cmd [list tk_messageBox \
266                 -icon warning \
267                 -type ok \
268                 -title "$title: warning" \
269                 -message $msg]
270         if {[winfo ismapped .]} {
271                 lappend cmd -parent .
272         }
273         eval $cmd
274 }
275
276 proc info_popup {msg {parent .}} {
277         set title [appname]
278         if {[reponame] ne {}} {
279                 append title " ([reponame])"
280         }
281         option add *Dialog.msg.font font_ui
282         option add *Button.font font_ui
283         tk_messageBox \
284                 -parent $parent \
285                 -icon info \
286                 -type ok \
287                 -title $title \
288                 -message $msg
289 }
290
291 proc ask_popup {msg} {
292         set title [appname]
293         if {[reponame] ne {}} {
294                 append title " ([reponame])"
295         }
296         option add *Dialog.msg.font font_ui
297         option add *Button.font font_ui
298         return [tk_messageBox \
299                 -parent . \
300                 -icon question \
301                 -type yesno \
302                 -title $title \
303                 -message $msg]
304 }
305
306 ######################################################################
307 ##
308 ## version check
309
310 if {{--version} eq $argv || {version} eq $argv} {
311         puts "git-gui version $appvers"
312         exit
313 }
314
315 set req_maj 1
316 set req_min 5
317
318 if {[catch {set v [git --version]} err]} {
319         catch {wm withdraw .}
320         error_popup "Cannot determine Git version:
321
322 $err
323
324 [appname] requires Git $req_maj.$req_min or later."
325         exit 1
326 }
327 if {[regexp {^git version (\d+)\.(\d+)} $v _junk act_maj act_min]} {
328         if {$act_maj < $req_maj
329                 || ($act_maj == $req_maj && $act_min < $req_min)} {
330                 catch {wm withdraw .}
331                 error_popup "[appname] requires Git $req_maj.$req_min or later.
332
333 You are using $v."
334                 exit 1
335         }
336 } else {
337         catch {wm withdraw .}
338         error_popup "Cannot parse Git version string:\n\n$v"
339         exit 1
340 }
341 unset -nocomplain v _junk act_maj act_min req_maj req_min
342
343 ######################################################################
344 ##
345 ## repository setup
346
347 if {   [catch {set _gitdir $env(GIT_DIR)}]
348         && [catch {set _gitdir [git rev-parse --git-dir]} err]} {
349         catch {wm withdraw .}
350         error_popup "Cannot find the git directory:\n\n$err"
351         exit 1
352 }
353 if {![file isdirectory $_gitdir] && [is_Cygwin]} {
354         catch {set _gitdir [exec cygpath --unix $_gitdir]}
355 }
356 if {![file isdirectory $_gitdir]} {
357         catch {wm withdraw .}
358         error_popup "Git directory not found:\n\n$_gitdir"
359         exit 1
360 }
361 if {[lindex [file split $_gitdir] end] ne {.git}} {
362         catch {wm withdraw .}
363         error_popup "Cannot use funny .git directory:\n\n$_gitdir"
364         exit 1
365 }
366 if {[catch {cd [file dirname $_gitdir]} err]} {
367         catch {wm withdraw .}
368         error_popup "No working directory [file dirname $_gitdir]:\n\n$err"
369         exit 1
370 }
371 set _reponame [lindex [file split \
372         [file normalize [file dirname $_gitdir]]] \
373         end]
374
375 ######################################################################
376 ##
377 ## global init
378
379 set current_diff_path {}
380 set current_diff_side {}
381 set diff_actions [list]
382 set ui_status_value {Initializing...}
383
384 set HEAD {}
385 set PARENT {}
386 set MERGE_HEAD [list]
387 set commit_type {}
388 set empty_tree {}
389 set current_branch {}
390 set current_diff_path {}
391 set selected_commit_type new
392
393 ######################################################################
394 ##
395 ## task management
396
397 set rescan_active 0
398 set diff_active 0
399 set last_clicked {}
400
401 set disable_on_lock [list]
402 set index_lock_type none
403
404 proc lock_index {type} {
405         global index_lock_type disable_on_lock
406
407         if {$index_lock_type eq {none}} {
408                 set index_lock_type $type
409                 foreach w $disable_on_lock {
410                         uplevel #0 $w disabled
411                 }
412                 return 1
413         } elseif {$index_lock_type eq "begin-$type"} {
414                 set index_lock_type $type
415                 return 1
416         }
417         return 0
418 }
419
420 proc unlock_index {} {
421         global index_lock_type disable_on_lock
422
423         set index_lock_type none
424         foreach w $disable_on_lock {
425                 uplevel #0 $w normal
426         }
427 }
428
429 ######################################################################
430 ##
431 ## status
432
433 proc repository_state {ctvar hdvar mhvar} {
434         global current_branch
435         upvar $ctvar ct $hdvar hd $mhvar mh
436
437         set mh [list]
438
439         if {[catch {set current_branch [git symbolic-ref HEAD]}]} {
440                 set current_branch {}
441         } else {
442                 regsub ^refs/((heads|tags|remotes)/)? \
443                         $current_branch \
444                         {} \
445                         current_branch
446         }
447
448         if {[catch {set hd [git rev-parse --verify HEAD]}]} {
449                 set hd {}
450                 set ct initial
451                 return
452         }
453
454         set merge_head [gitdir MERGE_HEAD]
455         if {[file exists $merge_head]} {
456                 set ct merge
457                 set fd_mh [open $merge_head r]
458                 while {[gets $fd_mh line] >= 0} {
459                         lappend mh $line
460                 }
461                 close $fd_mh
462                 return
463         }
464
465         set ct normal
466 }
467
468 proc PARENT {} {
469         global PARENT empty_tree
470
471         set p [lindex $PARENT 0]
472         if {$p ne {}} {
473                 return $p
474         }
475         if {$empty_tree eq {}} {
476                 set empty_tree [git mktree << {}]
477         }
478         return $empty_tree
479 }
480
481 proc rescan {after {honor_trustmtime 1}} {
482         global HEAD PARENT MERGE_HEAD commit_type
483         global ui_index ui_workdir ui_status_value ui_comm
484         global rescan_active file_states
485         global repo_config
486
487         if {$rescan_active > 0 || ![lock_index read]} return
488
489         repository_state newType newHEAD newMERGE_HEAD
490         if {[string match amend* $commit_type]
491                 && $newType eq {normal}
492                 && $newHEAD eq $HEAD} {
493         } else {
494                 set HEAD $newHEAD
495                 set PARENT $newHEAD
496                 set MERGE_HEAD $newMERGE_HEAD
497                 set commit_type $newType
498         }
499
500         array unset file_states
501
502         if {![$ui_comm edit modified]
503                 || [string trim [$ui_comm get 0.0 end]] eq {}} {
504                 if {[load_message GITGUI_MSG]} {
505                 } elseif {[load_message MERGE_MSG]} {
506                 } elseif {[load_message SQUASH_MSG]} {
507                 }
508                 $ui_comm edit reset
509                 $ui_comm edit modified false
510         }
511
512         if {[is_enabled branch]} {
513                 load_all_heads
514                 populate_branch_menu
515         }
516
517         if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} {
518                 rescan_stage2 {} $after
519         } else {
520                 set rescan_active 1
521                 set ui_status_value {Refreshing file status...}
522                 set cmd [list git update-index]
523                 lappend cmd -q
524                 lappend cmd --unmerged
525                 lappend cmd --ignore-missing
526                 lappend cmd --refresh
527                 set fd_rf [open "| $cmd" r]
528                 fconfigure $fd_rf -blocking 0 -translation binary
529                 fileevent $fd_rf readable \
530                         [list rescan_stage2 $fd_rf $after]
531         }
532 }
533
534 proc rescan_stage2 {fd after} {
535         global ui_status_value
536         global rescan_active buf_rdi buf_rdf buf_rlo
537
538         if {$fd ne {}} {
539                 read $fd
540                 if {![eof $fd]} return
541                 close $fd
542         }
543
544         set ls_others [list | git ls-files --others -z \
545                 --exclude-per-directory=.gitignore]
546         set info_exclude [gitdir info exclude]
547         if {[file readable $info_exclude]} {
548                 lappend ls_others "--exclude-from=$info_exclude"
549         }
550
551         set buf_rdi {}
552         set buf_rdf {}
553         set buf_rlo {}
554
555         set rescan_active 3
556         set ui_status_value {Scanning for modified files ...}
557         set fd_di [open "| git diff-index --cached -z [PARENT]" r]
558         set fd_df [open "| git diff-files -z" r]
559         set fd_lo [open $ls_others r]
560
561         fconfigure $fd_di -blocking 0 -translation binary -encoding binary
562         fconfigure $fd_df -blocking 0 -translation binary -encoding binary
563         fconfigure $fd_lo -blocking 0 -translation binary -encoding binary
564         fileevent $fd_di readable [list read_diff_index $fd_di $after]
565         fileevent $fd_df readable [list read_diff_files $fd_df $after]
566         fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
567 }
568
569 proc load_message {file} {
570         global ui_comm
571
572         set f [gitdir $file]
573         if {[file isfile $f]} {
574                 if {[catch {set fd [open $f r]}]} {
575                         return 0
576                 }
577                 set content [string trim [read $fd]]
578                 close $fd
579                 regsub -all -line {[ \r\t]+$} $content {} content
580                 $ui_comm delete 0.0 end
581                 $ui_comm insert end $content
582                 return 1
583         }
584         return 0
585 }
586
587 proc read_diff_index {fd after} {
588         global buf_rdi
589
590         append buf_rdi [read $fd]
591         set c 0
592         set n [string length $buf_rdi]
593         while {$c < $n} {
594                 set z1 [string first "\0" $buf_rdi $c]
595                 if {$z1 == -1} break
596                 incr z1
597                 set z2 [string first "\0" $buf_rdi $z1]
598                 if {$z2 == -1} break
599
600                 incr c
601                 set i [split [string range $buf_rdi $c [expr {$z1 - 2}]] { }]
602                 set p [string range $buf_rdi $z1 [expr {$z2 - 1}]]
603                 merge_state \
604                         [encoding convertfrom $p] \
605                         [lindex $i 4]? \
606                         [list [lindex $i 0] [lindex $i 2]] \
607                         [list]
608                 set c $z2
609                 incr c
610         }
611         if {$c < $n} {
612                 set buf_rdi [string range $buf_rdi $c end]
613         } else {
614                 set buf_rdi {}
615         }
616
617         rescan_done $fd buf_rdi $after
618 }
619
620 proc read_diff_files {fd after} {
621         global buf_rdf
622
623         append buf_rdf [read $fd]
624         set c 0
625         set n [string length $buf_rdf]
626         while {$c < $n} {
627                 set z1 [string first "\0" $buf_rdf $c]
628                 if {$z1 == -1} break
629                 incr z1
630                 set z2 [string first "\0" $buf_rdf $z1]
631                 if {$z2 == -1} break
632
633                 incr c
634                 set i [split [string range $buf_rdf $c [expr {$z1 - 2}]] { }]
635                 set p [string range $buf_rdf $z1 [expr {$z2 - 1}]]
636                 merge_state \
637                         [encoding convertfrom $p] \
638                         ?[lindex $i 4] \
639                         [list] \
640                         [list [lindex $i 0] [lindex $i 2]]
641                 set c $z2
642                 incr c
643         }
644         if {$c < $n} {
645                 set buf_rdf [string range $buf_rdf $c end]
646         } else {
647                 set buf_rdf {}
648         }
649
650         rescan_done $fd buf_rdf $after
651 }
652
653 proc read_ls_others {fd after} {
654         global buf_rlo
655
656         append buf_rlo [read $fd]
657         set pck [split $buf_rlo "\0"]
658         set buf_rlo [lindex $pck end]
659         foreach p [lrange $pck 0 end-1] {
660                 merge_state [encoding convertfrom $p] ?O
661         }
662         rescan_done $fd buf_rlo $after
663 }
664
665 proc rescan_done {fd buf after} {
666         global rescan_active
667         global file_states repo_config
668         upvar $buf to_clear
669
670         if {![eof $fd]} return
671         set to_clear {}
672         close $fd
673         if {[incr rescan_active -1] > 0} return
674
675         prune_selection
676         unlock_index
677         display_all_files
678         reshow_diff
679         uplevel #0 $after
680 }
681
682 proc prune_selection {} {
683         global file_states selected_paths
684
685         foreach path [array names selected_paths] {
686                 if {[catch {set still_here $file_states($path)}]} {
687                         unset selected_paths($path)
688                 }
689         }
690 }
691
692 ######################################################################
693 ##
694 ## diff
695
696 proc clear_diff {} {
697         global ui_diff current_diff_path current_diff_header
698         global ui_index ui_workdir
699
700         $ui_diff conf -state normal
701         $ui_diff delete 0.0 end
702         $ui_diff conf -state disabled
703
704         set current_diff_path {}
705         set current_diff_header {}
706
707         $ui_index tag remove in_diff 0.0 end
708         $ui_workdir tag remove in_diff 0.0 end
709 }
710
711 proc reshow_diff {} {
712         global ui_status_value file_states file_lists
713         global current_diff_path current_diff_side
714
715         set p $current_diff_path
716         if {$p eq {}} {
717                 # No diff is being shown.
718         } elseif {$current_diff_side eq {}
719                 || [catch {set s $file_states($p)}]
720                 || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
721                 clear_diff
722         } else {
723                 show_diff $p $current_diff_side
724         }
725 }
726
727 proc handle_empty_diff {} {
728         global current_diff_path file_states file_lists
729
730         set path $current_diff_path
731         set s $file_states($path)
732         if {[lindex $s 0] ne {_M}} return
733
734         info_popup "No differences detected.
735
736 [short_path $path] has no changes.
737
738 The modification date of this file was updated by another application, but the content within the file was not changed.
739
740 A rescan will be automatically started to find other files which may have the same state."
741
742         clear_diff
743         display_file $path __
744         rescan {set ui_status_value {Ready.}} 0
745 }
746
747 proc show_diff {path w {lno {}}} {
748         global file_states file_lists
749         global is_3way_diff diff_active repo_config
750         global ui_diff ui_status_value ui_index ui_workdir
751         global current_diff_path current_diff_side current_diff_header
752
753         if {$diff_active || ![lock_index read]} return
754
755         clear_diff
756         if {$lno == {}} {
757                 set lno [lsearch -sorted -exact $file_lists($w) $path]
758                 if {$lno >= 0} {
759                         incr lno
760                 }
761         }
762         if {$lno >= 1} {
763                 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
764         }
765
766         set s $file_states($path)
767         set m [lindex $s 0]
768         set is_3way_diff 0
769         set diff_active 1
770         set current_diff_path $path
771         set current_diff_side $w
772         set current_diff_header {}
773         set ui_status_value "Loading diff of [escape_path $path]..."
774
775         # - Git won't give us the diff, there's nothing to compare to!
776         #
777         if {$m eq {_O}} {
778                 set max_sz [expr {128 * 1024}]
779                 if {[catch {
780                                 set fd [open $path r]
781                                 set content [read $fd $max_sz]
782                                 close $fd
783                                 set sz [file size $path]
784                         } err ]} {
785                         set diff_active 0
786                         unlock_index
787                         set ui_status_value "Unable to display [escape_path $path]"
788                         error_popup "Error loading file:\n\n$err"
789                         return
790                 }
791                 $ui_diff conf -state normal
792                 if {![catch {set type [exec file $path]}]} {
793                         set n [string length $path]
794                         if {[string equal -length $n $path $type]} {
795                                 set type [string range $type $n end]
796                                 regsub {^:?\s*} $type {} type
797                         }
798                         $ui_diff insert end "* $type\n" d_@
799                 }
800                 if {[string first "\0" $content] != -1} {
801                         $ui_diff insert end \
802                                 "* Binary file (not showing content)." \
803                                 d_@
804                 } else {
805                         if {$sz > $max_sz} {
806                                 $ui_diff insert end \
807 "* Untracked file is $sz bytes.
808 * Showing only first $max_sz bytes.
809 " d_@
810                         }
811                         $ui_diff insert end $content
812                         if {$sz > $max_sz} {
813                                 $ui_diff insert end "
814 * Untracked file clipped here by [appname].
815 * To see the entire file, use an external editor.
816 " d_@
817                         }
818                 }
819                 $ui_diff conf -state disabled
820                 set diff_active 0
821                 unlock_index
822                 set ui_status_value {Ready.}
823                 return
824         }
825
826         set cmd [list | git]
827         if {$w eq $ui_index} {
828                 lappend cmd diff-index
829                 lappend cmd --cached
830         } elseif {$w eq $ui_workdir} {
831                 if {[string index $m 0] eq {U}} {
832                         lappend cmd diff
833                 } else {
834                         lappend cmd diff-files
835                 }
836         }
837
838         lappend cmd -p
839         lappend cmd --no-color
840         if {$repo_config(gui.diffcontext) > 0} {
841                 lappend cmd "-U$repo_config(gui.diffcontext)"
842         }
843         if {$w eq $ui_index} {
844                 lappend cmd [PARENT]
845         }
846         lappend cmd --
847         lappend cmd $path
848
849         if {[catch {set fd [open $cmd r]} err]} {
850                 set diff_active 0
851                 unlock_index
852                 set ui_status_value "Unable to display [escape_path $path]"
853                 error_popup "Error loading diff:\n\n$err"
854                 return
855         }
856
857         fconfigure $fd \
858                 -blocking 0 \
859                 -encoding binary \
860                 -translation binary
861         fileevent $fd readable [list read_diff $fd]
862 }
863
864 proc read_diff {fd} {
865         global ui_diff ui_status_value diff_active
866         global is_3way_diff current_diff_header
867
868         $ui_diff conf -state normal
869         while {[gets $fd line] >= 0} {
870                 # -- Cleanup uninteresting diff header lines.
871                 #
872                 if {   [string match {diff --git *}      $line]
873                         || [string match {diff --cc *}       $line]
874                         || [string match {diff --combined *} $line]
875                         || [string match {--- *}             $line]
876                         || [string match {+++ *}             $line]} {
877                         append current_diff_header $line "\n"
878                         continue
879                 }
880                 if {[string match {index *} $line]} continue
881                 if {$line eq {deleted file mode 120000}} {
882                         set line "deleted symlink"
883                 }
884
885                 # -- Automatically detect if this is a 3 way diff.
886                 #
887                 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
888
889                 if {[string match {mode *} $line]
890                         || [string match {new file *} $line]
891                         || [string match {deleted file *} $line]
892                         || [string match {Binary files * and * differ} $line]
893                         || $line eq {\ No newline at end of file}
894                         || [regexp {^\* Unmerged path } $line]} {
895                         set tags {}
896                 } elseif {$is_3way_diff} {
897                         set op [string range $line 0 1]
898                         switch -- $op {
899                         {  } {set tags {}}
900                         {@@} {set tags d_@}
901                         { +} {set tags d_s+}
902                         { -} {set tags d_s-}
903                         {+ } {set tags d_+s}
904                         {- } {set tags d_-s}
905                         {--} {set tags d_--}
906                         {++} {
907                                 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
908                                         set line [string replace $line 0 1 {  }]
909                                         set tags d$op
910                                 } else {
911                                         set tags d_++
912                                 }
913                         }
914                         default {
915                                 puts "error: Unhandled 3 way diff marker: {$op}"
916                                 set tags {}
917                         }
918                         }
919                 } else {
920                         set op [string index $line 0]
921                         switch -- $op {
922                         { } {set tags {}}
923                         {@} {set tags d_@}
924                         {-} {set tags d_-}
925                         {+} {
926                                 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
927                                         set line [string replace $line 0 0 { }]
928                                         set tags d$op
929                                 } else {
930                                         set tags d_+
931                                 }
932                         }
933                         default {
934                                 puts "error: Unhandled 2 way diff marker: {$op}"
935                                 set tags {}
936                         }
937                         }
938                 }
939                 $ui_diff insert end $line $tags
940                 if {[string index $line end] eq "\r"} {
941                         $ui_diff tag add d_cr {end - 2c}
942                 }
943                 $ui_diff insert end "\n" $tags
944         }
945         $ui_diff conf -state disabled
946
947         if {[eof $fd]} {
948                 close $fd
949                 set diff_active 0
950                 unlock_index
951                 set ui_status_value {Ready.}
952
953                 if {[$ui_diff index end] eq {2.0}} {
954                         handle_empty_diff
955                 }
956         }
957 }
958
959 proc apply_hunk {x y} {
960         global current_diff_path current_diff_header current_diff_side
961         global ui_diff ui_index file_states
962
963         if {$current_diff_path eq {} || $current_diff_header eq {}} return
964         if {![lock_index apply_hunk]} return
965
966         set apply_cmd {git apply --cached --whitespace=nowarn}
967         set mi [lindex $file_states($current_diff_path) 0]
968         if {$current_diff_side eq $ui_index} {
969                 set mode unstage
970                 lappend apply_cmd --reverse
971                 if {[string index $mi 0] ne {M}} {
972                         unlock_index
973                         return
974                 }
975         } else {
976                 set mode stage
977                 if {[string index $mi 1] ne {M}} {
978                         unlock_index
979                         return
980                 }
981         }
982
983         set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
984         set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
985         if {$s_lno eq {}} {
986                 unlock_index
987                 return
988         }
989
990         set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
991         if {$e_lno eq {}} {
992                 set e_lno end
993         }
994
995         if {[catch {
996                 set p [open "| $apply_cmd" w]
997                 fconfigure $p -translation binary -encoding binary
998                 puts -nonewline $p $current_diff_header
999                 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
1000                 close $p} err]} {
1001                 error_popup "Failed to $mode selected hunk.\n\n$err"
1002                 unlock_index
1003                 return
1004         }
1005
1006         $ui_diff conf -state normal
1007         $ui_diff delete $s_lno $e_lno
1008         $ui_diff conf -state disabled
1009
1010         if {[$ui_diff get 1.0 end] eq "\n"} {
1011                 set o _
1012         } else {
1013                 set o ?
1014         }
1015
1016         if {$current_diff_side eq $ui_index} {
1017                 set mi ${o}M
1018         } elseif {[string index $mi 0] eq {_}} {
1019                 set mi M$o
1020         } else {
1021                 set mi ?$o
1022         }
1023         unlock_index
1024         display_file $current_diff_path $mi
1025         if {$o eq {_}} {
1026                 clear_diff
1027         }
1028 }
1029
1030 ######################################################################
1031 ##
1032 ## commit
1033
1034 proc load_last_commit {} {
1035         global HEAD PARENT MERGE_HEAD commit_type ui_comm
1036         global repo_config
1037
1038         if {[llength $PARENT] == 0} {
1039                 error_popup {There is nothing to amend.
1040
1041 You are about to create the initial commit.  There is no commit before this to amend.
1042 }
1043                 return
1044         }
1045
1046         repository_state curType curHEAD curMERGE_HEAD
1047         if {$curType eq {merge}} {
1048                 error_popup {Cannot amend while merging.
1049
1050 You are currently in the middle of a merge that has not been fully completed.  You cannot amend the prior commit unless you first abort the current merge activity.
1051 }
1052                 return
1053         }
1054
1055         set msg {}
1056         set parents [list]
1057         if {[catch {
1058                         set fd [open "| git cat-file commit $curHEAD" r]
1059                         fconfigure $fd -encoding binary -translation lf
1060                         if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
1061                                 set enc utf-8
1062                         }
1063                         while {[gets $fd line] > 0} {
1064                                 if {[string match {parent *} $line]} {
1065                                         lappend parents [string range $line 7 end]
1066                                 } elseif {[string match {encoding *} $line]} {
1067                                         set enc [string tolower [string range $line 9 end]]
1068                                 }
1069                         }
1070                         set msg [encoding convertfrom $enc [read $fd]]
1071                         set msg [string trim $msg]
1072                         close $fd
1073                 } err]} {
1074                 error_popup "Error loading commit data for amend:\n\n$err"
1075                 return
1076         }
1077
1078         set HEAD $curHEAD
1079         set PARENT $parents
1080         set MERGE_HEAD [list]
1081         switch -- [llength $parents] {
1082         0       {set commit_type amend-initial}
1083         1       {set commit_type amend}
1084         default {set commit_type amend-merge}
1085         }
1086
1087         $ui_comm delete 0.0 end
1088         $ui_comm insert end $msg
1089         $ui_comm edit reset
1090         $ui_comm edit modified false
1091         rescan {set ui_status_value {Ready.}}
1092 }
1093
1094 proc create_new_commit {} {
1095         global commit_type ui_comm
1096
1097         set commit_type normal
1098         $ui_comm delete 0.0 end
1099         $ui_comm edit reset
1100         $ui_comm edit modified false
1101         rescan {set ui_status_value {Ready.}}
1102 }
1103
1104 set GIT_COMMITTER_IDENT {}
1105
1106 proc committer_ident {} {
1107         global GIT_COMMITTER_IDENT
1108
1109         if {$GIT_COMMITTER_IDENT eq {}} {
1110                 if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} {
1111                         error_popup "Unable to obtain your identity:\n\n$err"
1112                         return {}
1113                 }
1114                 if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
1115                         $me me GIT_COMMITTER_IDENT]} {
1116                         error_popup "Invalid GIT_COMMITTER_IDENT:\n\n$me"
1117                         return {}
1118                 }
1119         }
1120
1121         return $GIT_COMMITTER_IDENT
1122 }
1123
1124 proc commit_tree {} {
1125         global HEAD commit_type file_states ui_comm repo_config
1126         global ui_status_value pch_error
1127
1128         if {[committer_ident] eq {}} return
1129         if {![lock_index update]} return
1130
1131         # -- Our in memory state should match the repository.
1132         #
1133         repository_state curType curHEAD curMERGE_HEAD
1134         if {[string match amend* $commit_type]
1135                 && $curType eq {normal}
1136                 && $curHEAD eq $HEAD} {
1137         } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
1138                 info_popup {Last scanned state does not match repository state.
1139
1140 Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created.
1141
1142 The rescan will be automatically started now.
1143 }
1144                 unlock_index
1145                 rescan {set ui_status_value {Ready.}}
1146                 return
1147         }
1148
1149         # -- At least one file should differ in the index.
1150         #
1151         set files_ready 0
1152         foreach path [array names file_states] {
1153                 switch -glob -- [lindex $file_states($path) 0] {
1154                 _? {continue}
1155                 A? -
1156                 D? -
1157                 M? {set files_ready 1}
1158                 U? {
1159                         error_popup "Unmerged files cannot be committed.
1160
1161 File [short_path $path] has merge conflicts.  You must resolve them and add the file before committing.
1162 "
1163                         unlock_index
1164                         return
1165                 }
1166                 default {
1167                         error_popup "Unknown file state [lindex $s 0] detected.
1168
1169 File [short_path $path] cannot be committed by this program.
1170 "
1171                 }
1172                 }
1173         }
1174         if {!$files_ready && ![string match *merge $curType]} {
1175                 info_popup {No changes to commit.
1176
1177 You must add at least 1 file before you can commit.
1178 }
1179                 unlock_index
1180                 return
1181         }
1182
1183         # -- A message is required.
1184         #
1185         set msg [string trim [$ui_comm get 1.0 end]]
1186         regsub -all -line {[ \t\r]+$} $msg {} msg
1187         if {$msg eq {}} {
1188                 error_popup {Please supply a commit message.
1189
1190 A good commit message has the following format:
1191
1192 - First line: Describe in one sentance what you did.
1193 - Second line: Blank
1194 - Remaining lines: Describe why this change is good.
1195 }
1196                 unlock_index
1197                 return
1198         }
1199
1200         # -- Run the pre-commit hook.
1201         #
1202         set pchook [gitdir hooks pre-commit]
1203
1204         # On Cygwin [file executable] might lie so we need to ask
1205         # the shell if the hook is executable.  Yes that's annoying.
1206         #
1207         if {[is_Cygwin] && [file isfile $pchook]} {
1208                 set pchook [list sh -c [concat \
1209                         "if test -x \"$pchook\";" \
1210                         "then exec \"$pchook\" 2>&1;" \
1211                         "fi"]]
1212         } elseif {[file executable $pchook]} {
1213                 set pchook [list $pchook |& cat]
1214         } else {
1215                 commit_writetree $curHEAD $msg
1216                 return
1217         }
1218
1219         set ui_status_value {Calling pre-commit hook...}
1220         set pch_error {}
1221         set fd_ph [open "| $pchook" r]
1222         fconfigure $fd_ph -blocking 0 -translation binary
1223         fileevent $fd_ph readable \
1224                 [list commit_prehook_wait $fd_ph $curHEAD $msg]
1225 }
1226
1227 proc commit_prehook_wait {fd_ph curHEAD msg} {
1228         global pch_error ui_status_value
1229
1230         append pch_error [read $fd_ph]
1231         fconfigure $fd_ph -blocking 1
1232         if {[eof $fd_ph]} {
1233                 if {[catch {close $fd_ph}]} {
1234                         set ui_status_value {Commit declined by pre-commit hook.}
1235                         hook_failed_popup pre-commit $pch_error
1236                         unlock_index
1237                 } else {
1238                         commit_writetree $curHEAD $msg
1239                 }
1240                 set pch_error {}
1241                 return
1242         }
1243         fconfigure $fd_ph -blocking 0
1244 }
1245
1246 proc commit_writetree {curHEAD msg} {
1247         global ui_status_value
1248
1249         set ui_status_value {Committing changes...}
1250         set fd_wt [open "| git write-tree" r]
1251         fileevent $fd_wt readable \
1252                 [list commit_committree $fd_wt $curHEAD $msg]
1253 }
1254
1255 proc commit_committree {fd_wt curHEAD msg} {
1256         global HEAD PARENT MERGE_HEAD commit_type
1257         global all_heads current_branch
1258         global ui_status_value ui_comm selected_commit_type
1259         global file_states selected_paths rescan_active
1260         global repo_config
1261
1262         gets $fd_wt tree_id
1263         if {$tree_id eq {} || [catch {close $fd_wt} err]} {
1264                 error_popup "write-tree failed:\n\n$err"
1265                 set ui_status_value {Commit failed.}
1266                 unlock_index
1267                 return
1268         }
1269
1270         # -- Verify this wasn't an empty change.
1271         #
1272         if {$commit_type eq {normal}} {
1273                 set old_tree [git rev-parse "$PARENT^{tree}"]
1274                 if {$tree_id eq $old_tree} {
1275                         info_popup {No changes to commit.
1276
1277 No files were modified by this commit and it was not a merge commit.
1278
1279 A rescan will be automatically started now.
1280 }
1281                         unlock_index
1282                         rescan {set ui_status_value {No changes to commit.}}
1283                         return
1284                 }
1285         }
1286
1287         # -- Build the message.
1288         #
1289         set msg_p [gitdir COMMIT_EDITMSG]
1290         set msg_wt [open $msg_p w]
1291         if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
1292                 set enc utf-8
1293         }
1294         fconfigure $msg_wt -encoding binary -translation binary
1295         puts -nonewline $msg_wt [encoding convertto $enc $msg]
1296         close $msg_wt
1297
1298         # -- Create the commit.
1299         #
1300         set cmd [list git commit-tree $tree_id]
1301         foreach p [concat $PARENT $MERGE_HEAD] {
1302                 lappend cmd -p $p
1303         }
1304         lappend cmd <$msg_p
1305         if {[catch {set cmt_id [eval exec $cmd]} err]} {
1306                 error_popup "commit-tree failed:\n\n$err"
1307                 set ui_status_value {Commit failed.}
1308                 unlock_index
1309                 return
1310         }
1311
1312         # -- Update the HEAD ref.
1313         #
1314         set reflogm commit
1315         if {$commit_type ne {normal}} {
1316                 append reflogm " ($commit_type)"
1317         }
1318         set i [string first "\n" $msg]
1319         if {$i >= 0} {
1320                 append reflogm {: } [string range $msg 0 [expr {$i - 1}]]
1321         } else {
1322                 append reflogm {: } $msg
1323         }
1324         set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD]
1325         if {[catch {eval exec $cmd} err]} {
1326                 error_popup "update-ref failed:\n\n$err"
1327                 set ui_status_value {Commit failed.}
1328                 unlock_index
1329                 return
1330         }
1331
1332         # -- Cleanup after ourselves.
1333         #
1334         catch {file delete $msg_p}
1335         catch {file delete [gitdir MERGE_HEAD]}
1336         catch {file delete [gitdir MERGE_MSG]}
1337         catch {file delete [gitdir SQUASH_MSG]}
1338         catch {file delete [gitdir GITGUI_MSG]}
1339
1340         # -- Let rerere do its thing.
1341         #
1342         if {[file isdirectory [gitdir rr-cache]]} {
1343                 catch {git rerere}
1344         }
1345
1346         # -- Run the post-commit hook.
1347         #
1348         set pchook [gitdir hooks post-commit]
1349         if {[is_Cygwin] && [file isfile $pchook]} {
1350                 set pchook [list sh -c [concat \
1351                         "if test -x \"$pchook\";" \
1352                         "then exec \"$pchook\";" \
1353                         "fi"]]
1354         } elseif {![file executable $pchook]} {
1355                 set pchook {}
1356         }
1357         if {$pchook ne {}} {
1358                 catch {exec $pchook &}
1359         }
1360
1361         $ui_comm delete 0.0 end
1362         $ui_comm edit reset
1363         $ui_comm edit modified false
1364
1365         if {[is_enabled singlecommit]} do_quit
1366
1367         # -- Make sure our current branch exists.
1368         #
1369         if {$commit_type eq {initial}} {
1370                 lappend all_heads $current_branch
1371                 set all_heads [lsort -unique $all_heads]
1372                 populate_branch_menu
1373         }
1374
1375         # -- Update in memory status
1376         #
1377         set selected_commit_type new
1378         set commit_type normal
1379         set HEAD $cmt_id
1380         set PARENT $cmt_id
1381         set MERGE_HEAD [list]
1382
1383         foreach path [array names file_states] {
1384                 set s $file_states($path)
1385                 set m [lindex $s 0]
1386                 switch -glob -- $m {
1387                 _O -
1388                 _M -
1389                 _D {continue}
1390                 __ -
1391                 A_ -
1392                 M_ -
1393                 D_ {
1394                         unset file_states($path)
1395                         catch {unset selected_paths($path)}
1396                 }
1397                 DO {
1398                         set file_states($path) [list _O [lindex $s 1] {} {}]
1399                 }
1400                 AM -
1401                 AD -
1402                 MM -
1403                 MD {
1404                         set file_states($path) [list \
1405                                 _[string index $m 1] \
1406                                 [lindex $s 1] \
1407                                 [lindex $s 3] \
1408                                 {}]
1409                 }
1410                 }
1411         }
1412
1413         display_all_files
1414         unlock_index
1415         reshow_diff
1416         set ui_status_value \
1417                 "Changes committed as [string range $cmt_id 0 7]."
1418 }
1419
1420 ######################################################################
1421 ##
1422 ## fetch push
1423
1424 proc fetch_from {remote} {
1425         set w [new_console \
1426                 "fetch $remote" \
1427                 "Fetching new changes from $remote"]
1428         set cmd [list git fetch]
1429         lappend cmd $remote
1430         console_exec $w $cmd console_done
1431 }
1432
1433 proc push_to {remote} {
1434         set w [new_console \
1435                 "push $remote" \
1436                 "Pushing changes to $remote"]
1437         set cmd [list git push]
1438         lappend cmd -v
1439         lappend cmd $remote
1440         console_exec $w $cmd console_done
1441 }
1442
1443 ######################################################################
1444 ##
1445 ## ui helpers
1446
1447 proc mapicon {w state path} {
1448         global all_icons
1449
1450         if {[catch {set r $all_icons($state$w)}]} {
1451                 puts "error: no icon for $w state={$state} $path"
1452                 return file_plain
1453         }
1454         return $r
1455 }
1456
1457 proc mapdesc {state path} {
1458         global all_descs
1459
1460         if {[catch {set r $all_descs($state)}]} {
1461                 puts "error: no desc for state={$state} $path"
1462                 return $state
1463         }
1464         return $r
1465 }
1466
1467 proc escape_path {path} {
1468         regsub -all {\\} $path "\\\\" path
1469         regsub -all "\n" $path "\\n" path
1470         return $path
1471 }
1472
1473 proc short_path {path} {
1474         return [escape_path [lindex [file split $path] end]]
1475 }
1476
1477 set next_icon_id 0
1478 set null_sha1 [string repeat 0 40]
1479
1480 proc merge_state {path new_state {head_info {}} {index_info {}}} {
1481         global file_states next_icon_id null_sha1
1482
1483         set s0 [string index $new_state 0]
1484         set s1 [string index $new_state 1]
1485
1486         if {[catch {set info $file_states($path)}]} {
1487                 set state __
1488                 set icon n[incr next_icon_id]
1489         } else {
1490                 set state [lindex $info 0]
1491                 set icon [lindex $info 1]
1492                 if {$head_info eq {}}  {set head_info  [lindex $info 2]}
1493                 if {$index_info eq {}} {set index_info [lindex $info 3]}
1494         }
1495
1496         if     {$s0 eq {?}} {set s0 [string index $state 0]} \
1497         elseif {$s0 eq {_}} {set s0 _}
1498
1499         if     {$s1 eq {?}} {set s1 [string index $state 1]} \
1500         elseif {$s1 eq {_}} {set s1 _}
1501
1502         if {$s0 eq {A} && $s1 eq {_} && $head_info eq {}} {
1503                 set head_info [list 0 $null_sha1]
1504         } elseif {$s0 ne {_} && [string index $state 0] eq {_}
1505                 && $head_info eq {}} {
1506                 set head_info $index_info
1507         }
1508
1509         set file_states($path) [list $s0$s1 $icon \
1510                 $head_info $index_info \
1511                 ]
1512         return $state
1513 }
1514
1515 proc display_file_helper {w path icon_name old_m new_m} {
1516         global file_lists
1517
1518         if {$new_m eq {_}} {
1519                 set lno [lsearch -sorted -exact $file_lists($w) $path]
1520                 if {$lno >= 0} {
1521                         set file_lists($w) [lreplace $file_lists($w) $lno $lno]
1522                         incr lno
1523                         $w conf -state normal
1524                         $w delete $lno.0 [expr {$lno + 1}].0
1525                         $w conf -state disabled
1526                 }
1527         } elseif {$old_m eq {_} && $new_m ne {_}} {
1528                 lappend file_lists($w) $path
1529                 set file_lists($w) [lsort -unique $file_lists($w)]
1530                 set lno [lsearch -sorted -exact $file_lists($w) $path]
1531                 incr lno
1532                 $w conf -state normal
1533                 $w image create $lno.0 \
1534                         -align center -padx 5 -pady 1 \
1535                         -name $icon_name \
1536                         -image [mapicon $w $new_m $path]
1537                 $w insert $lno.1 "[escape_path $path]\n"
1538                 $w conf -state disabled
1539         } elseif {$old_m ne $new_m} {
1540                 $w conf -state normal
1541                 $w image conf $icon_name -image [mapicon $w $new_m $path]
1542                 $w conf -state disabled
1543         }
1544 }
1545
1546 proc display_file {path state} {
1547         global file_states selected_paths
1548         global ui_index ui_workdir
1549
1550         set old_m [merge_state $path $state]
1551         set s $file_states($path)
1552         set new_m [lindex $s 0]
1553         set icon_name [lindex $s 1]
1554
1555         set o [string index $old_m 0]
1556         set n [string index $new_m 0]
1557         if {$o eq {U}} {
1558                 set o _
1559         }
1560         if {$n eq {U}} {
1561                 set n _
1562         }
1563         display_file_helper     $ui_index $path $icon_name $o $n
1564
1565         if {[string index $old_m 0] eq {U}} {
1566                 set o U
1567         } else {
1568                 set o [string index $old_m 1]
1569         }
1570         if {[string index $new_m 0] eq {U}} {
1571                 set n U
1572         } else {
1573                 set n [string index $new_m 1]
1574         }
1575         display_file_helper     $ui_workdir $path $icon_name $o $n
1576
1577         if {$new_m eq {__}} {
1578                 unset file_states($path)
1579                 catch {unset selected_paths($path)}
1580         }
1581 }
1582
1583 proc display_all_files_helper {w path icon_name m} {
1584         global file_lists
1585
1586         lappend file_lists($w) $path
1587         set lno [expr {[lindex [split [$w index end] .] 0] - 1}]
1588         $w image create end \
1589                 -align center -padx 5 -pady 1 \
1590                 -name $icon_name \
1591                 -image [mapicon $w $m $path]
1592         $w insert end "[escape_path $path]\n"
1593 }
1594
1595 proc display_all_files {} {
1596         global ui_index ui_workdir
1597         global file_states file_lists
1598         global last_clicked
1599
1600         $ui_index conf -state normal
1601         $ui_workdir conf -state normal
1602
1603         $ui_index delete 0.0 end
1604         $ui_workdir delete 0.0 end
1605         set last_clicked {}
1606
1607         set file_lists($ui_index) [list]
1608         set file_lists($ui_workdir) [list]
1609
1610         foreach path [lsort [array names file_states]] {
1611                 set s $file_states($path)
1612                 set m [lindex $s 0]
1613                 set icon_name [lindex $s 1]
1614
1615                 set s [string index $m 0]
1616                 if {$s ne {U} && $s ne {_}} {
1617                         display_all_files_helper $ui_index $path \
1618                                 $icon_name $s
1619                 }
1620
1621                 if {[string index $m 0] eq {U}} {
1622                         set s U
1623                 } else {
1624                         set s [string index $m 1]
1625                 }
1626                 if {$s ne {_}} {
1627                         display_all_files_helper $ui_workdir $path \
1628                                 $icon_name $s
1629                 }
1630         }
1631
1632         $ui_index conf -state disabled
1633         $ui_workdir conf -state disabled
1634 }
1635
1636 proc update_indexinfo {msg pathList after} {
1637         global update_index_cp ui_status_value
1638
1639         if {![lock_index update]} return
1640
1641         set update_index_cp 0
1642         set pathList [lsort $pathList]
1643         set totalCnt [llength $pathList]
1644         set batch [expr {int($totalCnt * .01) + 1}]
1645         if {$batch > 25} {set batch 25}
1646
1647         set ui_status_value [format \
1648                 "$msg... %i/%i files (%.2f%%)" \
1649                 $update_index_cp \
1650                 $totalCnt \
1651                 0.0]
1652         set fd [open "| git update-index -z --index-info" w]
1653         fconfigure $fd \
1654                 -blocking 0 \
1655                 -buffering full \
1656                 -buffersize 512 \
1657                 -encoding binary \
1658                 -translation binary
1659         fileevent $fd writable [list \
1660                 write_update_indexinfo \
1661                 $fd \
1662                 $pathList \
1663                 $totalCnt \
1664                 $batch \
1665                 $msg \
1666                 $after \
1667                 ]
1668 }
1669
1670 proc write_update_indexinfo {fd pathList totalCnt batch msg after} {
1671         global update_index_cp ui_status_value
1672         global file_states current_diff_path
1673
1674         if {$update_index_cp >= $totalCnt} {
1675                 close $fd
1676                 unlock_index
1677                 uplevel #0 $after
1678                 return
1679         }
1680
1681         for {set i $batch} \
1682                 {$update_index_cp < $totalCnt && $i > 0} \
1683                 {incr i -1} {
1684                 set path [lindex $pathList $update_index_cp]
1685                 incr update_index_cp
1686
1687                 set s $file_states($path)
1688                 switch -glob -- [lindex $s 0] {
1689                 A? {set new _O}
1690                 M? {set new _M}
1691                 D_ {set new _D}
1692                 D? {set new _?}
1693                 ?? {continue}
1694                 }
1695                 set info [lindex $s 2]
1696                 if {$info eq {}} continue
1697
1698                 puts -nonewline $fd "$info\t[encoding convertto $path]\0"
1699                 display_file $path $new
1700         }
1701
1702         set ui_status_value [format \
1703                 "$msg... %i/%i files (%.2f%%)" \
1704                 $update_index_cp \
1705                 $totalCnt \
1706                 [expr {100.0 * $update_index_cp / $totalCnt}]]
1707 }
1708
1709 proc update_index {msg pathList after} {
1710         global update_index_cp ui_status_value
1711
1712         if {![lock_index update]} return
1713
1714         set update_index_cp 0
1715         set pathList [lsort $pathList]
1716         set totalCnt [llength $pathList]
1717         set batch [expr {int($totalCnt * .01) + 1}]
1718         if {$batch > 25} {set batch 25}
1719
1720         set ui_status_value [format \
1721                 "$msg... %i/%i files (%.2f%%)" \
1722                 $update_index_cp \
1723                 $totalCnt \
1724                 0.0]
1725         set fd [open "| git update-index --add --remove -z --stdin" w]
1726         fconfigure $fd \
1727                 -blocking 0 \
1728                 -buffering full \
1729                 -buffersize 512 \
1730                 -encoding binary \
1731                 -translation binary
1732         fileevent $fd writable [list \
1733                 write_update_index \
1734                 $fd \
1735                 $pathList \
1736                 $totalCnt \
1737                 $batch \
1738                 $msg \
1739                 $after \
1740                 ]
1741 }
1742
1743 proc write_update_index {fd pathList totalCnt batch msg after} {
1744         global update_index_cp ui_status_value
1745         global file_states current_diff_path
1746
1747         if {$update_index_cp >= $totalCnt} {
1748                 close $fd
1749                 unlock_index
1750                 uplevel #0 $after
1751                 return
1752         }
1753
1754         for {set i $batch} \
1755                 {$update_index_cp < $totalCnt && $i > 0} \
1756                 {incr i -1} {
1757                 set path [lindex $pathList $update_index_cp]
1758                 incr update_index_cp
1759
1760                 switch -glob -- [lindex $file_states($path) 0] {
1761                 AD {set new __}
1762                 ?D {set new D_}
1763                 _O -
1764                 AM {set new A_}
1765                 U? {
1766                         if {[file exists $path]} {
1767                                 set new M_
1768                         } else {
1769                                 set new D_
1770                         }
1771                 }
1772                 ?M {set new M_}
1773                 ?? {continue}
1774                 }
1775                 puts -nonewline $fd "[encoding convertto $path]\0"
1776                 display_file $path $new
1777         }
1778
1779         set ui_status_value [format \
1780                 "$msg... %i/%i files (%.2f%%)" \
1781                 $update_index_cp \
1782                 $totalCnt \
1783                 [expr {100.0 * $update_index_cp / $totalCnt}]]
1784 }
1785
1786 proc checkout_index {msg pathList after} {
1787         global update_index_cp ui_status_value
1788
1789         if {![lock_index update]} return
1790
1791         set update_index_cp 0
1792         set pathList [lsort $pathList]
1793         set totalCnt [llength $pathList]
1794         set batch [expr {int($totalCnt * .01) + 1}]
1795         if {$batch > 25} {set batch 25}
1796
1797         set ui_status_value [format \
1798                 "$msg... %i/%i files (%.2f%%)" \
1799                 $update_index_cp \
1800                 $totalCnt \
1801                 0.0]
1802         set cmd [list git checkout-index]
1803         lappend cmd --index
1804         lappend cmd --quiet
1805         lappend cmd --force
1806         lappend cmd -z
1807         lappend cmd --stdin
1808         set fd [open "| $cmd " w]
1809         fconfigure $fd \
1810                 -blocking 0 \
1811                 -buffering full \
1812                 -buffersize 512 \
1813                 -encoding binary \
1814                 -translation binary
1815         fileevent $fd writable [list \
1816                 write_checkout_index \
1817                 $fd \
1818                 $pathList \
1819                 $totalCnt \
1820                 $batch \
1821                 $msg \
1822                 $after \
1823                 ]
1824 }
1825
1826 proc write_checkout_index {fd pathList totalCnt batch msg after} {
1827         global update_index_cp ui_status_value
1828         global file_states current_diff_path
1829
1830         if {$update_index_cp >= $totalCnt} {
1831                 close $fd
1832                 unlock_index
1833                 uplevel #0 $after
1834                 return
1835         }
1836
1837         for {set i $batch} \
1838                 {$update_index_cp < $totalCnt && $i > 0} \
1839                 {incr i -1} {
1840                 set path [lindex $pathList $update_index_cp]
1841                 incr update_index_cp
1842                 switch -glob -- [lindex $file_states($path) 0] {
1843                 U? {continue}
1844                 ?M -
1845                 ?D {
1846                         puts -nonewline $fd "[encoding convertto $path]\0"
1847                         display_file $path ?_
1848                 }
1849                 }
1850         }
1851
1852         set ui_status_value [format \
1853                 "$msg... %i/%i files (%.2f%%)" \
1854                 $update_index_cp \
1855                 $totalCnt \
1856                 [expr {100.0 * $update_index_cp / $totalCnt}]]
1857 }
1858
1859 ######################################################################
1860 ##
1861 ## branch management
1862
1863 proc is_tracking_branch {name} {
1864         global tracking_branches
1865
1866         if {![catch {set info $tracking_branches($name)}]} {
1867                 return 1
1868         }
1869         foreach t [array names tracking_branches] {
1870                 if {[string match {*/\*} $t] && [string match $t $name]} {
1871                         return 1
1872                 }
1873         }
1874         return 0
1875 }
1876
1877 proc load_all_heads {} {
1878         global all_heads
1879
1880         set all_heads [list]
1881         set fd [open "| git for-each-ref --format=%(refname) refs/heads" r]
1882         while {[gets $fd line] > 0} {
1883                 if {[is_tracking_branch $line]} continue
1884                 if {![regsub ^refs/heads/ $line {} name]} continue
1885                 lappend all_heads $name
1886         }
1887         close $fd
1888
1889         set all_heads [lsort $all_heads]
1890 }
1891
1892 proc populate_branch_menu {} {
1893         global all_heads disable_on_lock
1894
1895         set m .mbar.branch
1896         set last [$m index last]
1897         for {set i 0} {$i <= $last} {incr i} {
1898                 if {[$m type $i] eq {separator}} {
1899                         $m delete $i last
1900                         set new_dol [list]
1901                         foreach a $disable_on_lock {
1902                                 if {[lindex $a 0] ne $m || [lindex $a 2] < $i} {
1903                                         lappend new_dol $a
1904                                 }
1905                         }
1906                         set disable_on_lock $new_dol
1907                         break
1908                 }
1909         }
1910
1911         if {$all_heads ne {}} {
1912                 $m add separator
1913         }
1914         foreach b $all_heads {
1915                 $m add radiobutton \
1916                         -label $b \
1917                         -command [list switch_branch $b] \
1918                         -variable current_branch \
1919                         -value $b \
1920                         -font font_ui
1921                 lappend disable_on_lock \
1922                         [list $m entryconf [$m index last] -state]
1923         }
1924 }
1925
1926 proc all_tracking_branches {} {
1927         global tracking_branches
1928
1929         set all_trackings {}
1930         set cmd {}
1931         foreach name [array names tracking_branches] {
1932                 if {[regsub {/\*$} $name {} name]} {
1933                         lappend cmd $name
1934                 } else {
1935                         regsub ^refs/(heads|remotes)/ $name {} name
1936                         lappend all_trackings $name
1937                 }
1938         }
1939
1940         if {$cmd ne {}} {
1941                 set fd [open "| git for-each-ref --format=%(refname) $cmd" r]
1942                 while {[gets $fd name] > 0} {
1943                         regsub ^refs/(heads|remotes)/ $name {} name
1944                         lappend all_trackings $name
1945                 }
1946                 close $fd
1947         }
1948
1949         return [lsort -unique $all_trackings]
1950 }
1951
1952 proc load_all_tags {} {
1953         set all_tags [list]
1954         set fd [open "| git for-each-ref --format=%(refname) refs/tags" r]
1955         while {[gets $fd line] > 0} {
1956                 if {![regsub ^refs/tags/ $line {} name]} continue
1957                 lappend all_tags $name
1958         }
1959         close $fd
1960
1961         return [lsort $all_tags]
1962 }
1963
1964 proc do_create_branch_action {w} {
1965         global all_heads null_sha1 repo_config
1966         global create_branch_checkout create_branch_revtype
1967         global create_branch_head create_branch_trackinghead
1968         global create_branch_name create_branch_revexp
1969         global create_branch_tag
1970
1971         set newbranch $create_branch_name
1972         if {$newbranch eq {}
1973                 || $newbranch eq $repo_config(gui.newbranchtemplate)} {
1974                 tk_messageBox \
1975                         -icon error \
1976                         -type ok \
1977                         -title [wm title $w] \
1978                         -parent $w \
1979                         -message "Please supply a branch name."
1980                 focus $w.desc.name_t
1981                 return
1982         }
1983         if {![catch {git show-ref --verify -- "refs/heads/$newbranch"}]} {
1984                 tk_messageBox \
1985                         -icon error \
1986                         -type ok \
1987                         -title [wm title $w] \
1988                         -parent $w \
1989                         -message "Branch '$newbranch' already exists."
1990                 focus $w.desc.name_t
1991                 return
1992         }
1993         if {[catch {git check-ref-format "heads/$newbranch"}]} {
1994                 tk_messageBox \
1995                         -icon error \
1996                         -type ok \
1997                         -title [wm title $w] \
1998                         -parent $w \
1999                         -message "We do not like '$newbranch' as a branch name."
2000                 focus $w.desc.name_t
2001                 return
2002         }
2003
2004         set rev {}
2005         switch -- $create_branch_revtype {
2006         head {set rev $create_branch_head}
2007         tracking {set rev $create_branch_trackinghead}
2008         tag {set rev $create_branch_tag}
2009         expression {set rev $create_branch_revexp}
2010         }
2011         if {[catch {set cmt [git rev-parse --verify "${rev}^0"]}]} {
2012                 tk_messageBox \
2013                         -icon error \
2014                         -type ok \
2015                         -title [wm title $w] \
2016                         -parent $w \
2017                         -message "Invalid starting revision: $rev"
2018                 return
2019         }
2020         set cmd [list git update-ref]
2021         lappend cmd -m
2022         lappend cmd "branch: Created from $rev"
2023         lappend cmd "refs/heads/$newbranch"
2024         lappend cmd $cmt
2025         lappend cmd $null_sha1
2026         if {[catch {eval exec $cmd} err]} {
2027                 tk_messageBox \
2028                         -icon error \
2029                         -type ok \
2030                         -title [wm title $w] \
2031                         -parent $w \
2032                         -message "Failed to create '$newbranch'.\n\n$err"
2033                 return
2034         }
2035
2036         lappend all_heads $newbranch
2037         set all_heads [lsort $all_heads]
2038         populate_branch_menu
2039         destroy $w
2040         if {$create_branch_checkout} {
2041                 switch_branch $newbranch
2042         }
2043 }
2044
2045 proc radio_selector {varname value args} {
2046         upvar #0 $varname var
2047         set var $value
2048 }
2049
2050 trace add variable create_branch_head write \
2051         [list radio_selector create_branch_revtype head]
2052 trace add variable create_branch_trackinghead write \
2053         [list radio_selector create_branch_revtype tracking]
2054 trace add variable create_branch_tag write \
2055         [list radio_selector create_branch_revtype tag]
2056
2057 trace add variable delete_branch_head write \
2058         [list radio_selector delete_branch_checktype head]
2059 trace add variable delete_branch_trackinghead write \
2060         [list radio_selector delete_branch_checktype tracking]
2061
2062 proc do_create_branch {} {
2063         global all_heads current_branch repo_config
2064         global create_branch_checkout create_branch_revtype
2065         global create_branch_head create_branch_trackinghead
2066         global create_branch_name create_branch_revexp
2067         global create_branch_tag
2068
2069         set w .branch_editor
2070         toplevel $w
2071         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2072
2073         label $w.header -text {Create New Branch} \
2074                 -font font_uibold
2075         pack $w.header -side top -fill x
2076
2077         frame $w.buttons
2078         button $w.buttons.create -text Create \
2079                 -font font_ui \
2080                 -default active \
2081                 -command [list do_create_branch_action $w]
2082         pack $w.buttons.create -side right
2083         button $w.buttons.cancel -text {Cancel} \
2084                 -font font_ui \
2085                 -command [list destroy $w]
2086         pack $w.buttons.cancel -side right -padx 5
2087         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2088
2089         labelframe $w.desc \
2090                 -text {Branch Description} \
2091                 -font font_ui
2092         label $w.desc.name_l -text {Name:} -font font_ui
2093         entry $w.desc.name_t \
2094                 -borderwidth 1 \
2095                 -relief sunken \
2096                 -width 40 \
2097                 -textvariable create_branch_name \
2098                 -font font_ui \
2099                 -validate key \
2100                 -validatecommand {
2101                         if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0}
2102                         return 1
2103                 }
2104         grid $w.desc.name_l $w.desc.name_t -sticky we -padx {0 5}
2105         grid columnconfigure $w.desc 1 -weight 1
2106         pack $w.desc -anchor nw -fill x -pady 5 -padx 5
2107
2108         labelframe $w.from \
2109                 -text {Starting Revision} \
2110                 -font font_ui
2111         radiobutton $w.from.head_r \
2112                 -text {Local Branch:} \
2113                 -value head \
2114                 -variable create_branch_revtype \
2115                 -font font_ui
2116         set lbranchm [eval tk_optionMenu $w.from.head_m create_branch_head \
2117                 $all_heads]
2118         $lbranchm configure -font font_ui
2119         $w.from.head_m configure -font font_ui
2120         grid $w.from.head_r $w.from.head_m -sticky w
2121         set all_trackings [all_tracking_branches]
2122         if {$all_trackings ne {}} {
2123                 set create_branch_trackinghead [lindex $all_trackings 0]
2124                 radiobutton $w.from.tracking_r \
2125                         -text {Tracking Branch:} \
2126                         -value tracking \
2127                         -variable create_branch_revtype \
2128                         -font font_ui
2129                 set tbranchm [eval tk_optionMenu $w.from.tracking_m \
2130                         create_branch_trackinghead \
2131                         $all_trackings]
2132                 $tbranchm configure -font font_ui
2133                 $w.from.tracking_m configure -font font_ui
2134                 grid $w.from.tracking_r $w.from.tracking_m -sticky w
2135         }
2136         set all_tags [load_all_tags]
2137         if {$all_tags ne {}} {
2138                 set create_branch_tag [lindex $all_tags 0]
2139                 radiobutton $w.from.tag_r \
2140                         -text {Tag:} \
2141                         -value tag \
2142                         -variable create_branch_revtype \
2143                         -font font_ui
2144                 set tagsm [eval tk_optionMenu $w.from.tag_m \
2145                         create_branch_tag \
2146                         $all_tags]
2147                 $tagsm configure -font font_ui
2148                 $w.from.tag_m configure -font font_ui
2149                 grid $w.from.tag_r $w.from.tag_m -sticky w
2150         }
2151         radiobutton $w.from.exp_r \
2152                 -text {Revision Expression:} \
2153                 -value expression \
2154                 -variable create_branch_revtype \
2155                 -font font_ui
2156         entry $w.from.exp_t \
2157                 -borderwidth 1 \
2158                 -relief sunken \
2159                 -width 50 \
2160                 -textvariable create_branch_revexp \
2161                 -font font_ui \
2162                 -validate key \
2163                 -validatecommand {
2164                         if {%d == 1 && [regexp {\s} %S]} {return 0}
2165                         if {%d == 1 && [string length %S] > 0} {
2166                                 set create_branch_revtype expression
2167                         }
2168                         return 1
2169                 }
2170         grid $w.from.exp_r $w.from.exp_t -sticky we -padx {0 5}
2171         grid columnconfigure $w.from 1 -weight 1
2172         pack $w.from -anchor nw -fill x -pady 5 -padx 5
2173
2174         labelframe $w.postActions \
2175                 -text {Post Creation Actions} \
2176                 -font font_ui
2177         checkbutton $w.postActions.checkout \
2178                 -text {Checkout after creation} \
2179                 -variable create_branch_checkout \
2180                 -font font_ui
2181         pack $w.postActions.checkout -anchor nw
2182         pack $w.postActions -anchor nw -fill x -pady 5 -padx 5
2183
2184         set create_branch_checkout 1
2185         set create_branch_head $current_branch
2186         set create_branch_revtype head
2187         set create_branch_name $repo_config(gui.newbranchtemplate)
2188         set create_branch_revexp {}
2189
2190         bind $w <Visibility> "
2191                 grab $w
2192                 $w.desc.name_t icursor end
2193                 focus $w.desc.name_t
2194         "
2195         bind $w <Key-Escape> "destroy $w"
2196         bind $w <Key-Return> "do_create_branch_action $w;break"
2197         wm title $w "[appname] ([reponame]): Create Branch"
2198         tkwait window $w
2199 }
2200
2201 proc do_delete_branch_action {w} {
2202         global all_heads
2203         global delete_branch_checktype delete_branch_head delete_branch_trackinghead
2204
2205         set check_rev {}
2206         switch -- $delete_branch_checktype {
2207         head {set check_rev $delete_branch_head}
2208         tracking {set check_rev $delete_branch_trackinghead}
2209         always {set check_rev {:none}}
2210         }
2211         if {$check_rev eq {:none}} {
2212                 set check_cmt {}
2213         } elseif {[catch {set check_cmt [git rev-parse --verify "${check_rev}^0"]}]} {
2214                 tk_messageBox \
2215                         -icon error \
2216                         -type ok \
2217                         -title [wm title $w] \
2218                         -parent $w \
2219                         -message "Invalid check revision: $check_rev"
2220                 return
2221         }
2222
2223         set to_delete [list]
2224         set not_merged [list]
2225         foreach i [$w.list.l curselection] {
2226                 set b [$w.list.l get $i]
2227                 if {[catch {set o [git rev-parse --verify $b]}]} continue
2228                 if {$check_cmt ne {}} {
2229                         if {$b eq $check_rev} continue
2230                         if {[catch {set m [git merge-base $o $check_cmt]}]} continue
2231                         if {$o ne $m} {
2232                                 lappend not_merged $b
2233                                 continue
2234                         }
2235                 }
2236                 lappend to_delete [list $b $o]
2237         }
2238         if {$not_merged ne {}} {
2239                 set msg "The following branches are not completely merged into $check_rev:
2240
2241  - [join $not_merged "\n - "]"
2242                 tk_messageBox \
2243                         -icon info \
2244                         -type ok \
2245                         -title [wm title $w] \
2246                         -parent $w \
2247                         -message $msg
2248         }
2249         if {$to_delete eq {}} return
2250         if {$delete_branch_checktype eq {always}} {
2251                 set msg {Recovering deleted branches is difficult.
2252
2253 Delete the selected branches?}
2254                 if {[tk_messageBox \
2255                         -icon warning \
2256                         -type yesno \
2257                         -title [wm title $w] \
2258                         -parent $w \
2259                         -message $msg] ne yes} {
2260                         return
2261                 }
2262         }
2263
2264         set failed {}
2265         foreach i $to_delete {
2266                 set b [lindex $i 0]
2267                 set o [lindex $i 1]
2268                 if {[catch {git update-ref -d "refs/heads/$b" $o} err]} {
2269                         append failed " - $b: $err\n"
2270                 } else {
2271                         set x [lsearch -sorted -exact $all_heads $b]
2272                         if {$x >= 0} {
2273                                 set all_heads [lreplace $all_heads $x $x]
2274                         }
2275                 }
2276         }
2277
2278         if {$failed ne {}} {
2279                 tk_messageBox \
2280                         -icon error \
2281                         -type ok \
2282                         -title [wm title $w] \
2283                         -parent $w \
2284                         -message "Failed to delete branches:\n$failed"
2285         }
2286
2287         set all_heads [lsort $all_heads]
2288         populate_branch_menu
2289         destroy $w
2290 }
2291
2292 proc do_delete_branch {} {
2293         global all_heads tracking_branches current_branch
2294         global delete_branch_checktype delete_branch_head delete_branch_trackinghead
2295
2296         set w .branch_editor
2297         toplevel $w
2298         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2299
2300         label $w.header -text {Delete Local Branch} \
2301                 -font font_uibold
2302         pack $w.header -side top -fill x
2303
2304         frame $w.buttons
2305         button $w.buttons.create -text Delete \
2306                 -font font_ui \
2307                 -command [list do_delete_branch_action $w]
2308         pack $w.buttons.create -side right
2309         button $w.buttons.cancel -text {Cancel} \
2310                 -font font_ui \
2311                 -command [list destroy $w]
2312         pack $w.buttons.cancel -side right -padx 5
2313         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2314
2315         labelframe $w.list \
2316                 -text {Local Branches} \
2317                 -font font_ui
2318         listbox $w.list.l \
2319                 -height 10 \
2320                 -width 70 \
2321                 -selectmode extended \
2322                 -yscrollcommand [list $w.list.sby set] \
2323                 -font font_ui
2324         foreach h $all_heads {
2325                 if {$h ne $current_branch} {
2326                         $w.list.l insert end $h
2327                 }
2328         }
2329         scrollbar $w.list.sby -command [list $w.list.l yview]
2330         pack $w.list.sby -side right -fill y
2331         pack $w.list.l -side left -fill both -expand 1
2332         pack $w.list -fill both -expand 1 -pady 5 -padx 5
2333
2334         labelframe $w.validate \
2335                 -text {Delete Only If} \
2336                 -font font_ui
2337         radiobutton $w.validate.head_r \
2338                 -text {Merged Into Local Branch:} \
2339                 -value head \
2340                 -variable delete_branch_checktype \
2341                 -font font_ui
2342         set mergedlocalm [eval tk_optionMenu $w.validate.head_m \
2343                 delete_branch_head \
2344                 $all_heads]
2345         $mergedlocalm configure -font font_ui
2346         $w.validate.head_m configure -font font_ui
2347         grid $w.validate.head_r $w.validate.head_m -sticky w
2348         set all_trackings [all_tracking_branches]
2349         if {$all_trackings ne {}} {
2350                 set delete_branch_trackinghead [lindex $all_trackings 0]
2351                 radiobutton $w.validate.tracking_r \
2352                         -text {Merged Into Tracking Branch:} \
2353                         -value tracking \
2354                         -variable delete_branch_checktype \
2355                         -font font_ui
2356                 set mergedtrackm [eval tk_optionMenu $w.validate.tracking_m \
2357                         delete_branch_trackinghead \
2358                         $all_trackings]
2359                 $mergedtrackm configure -font font_ui
2360                 $w.validate.tracking_m configure -font font_ui
2361                 grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
2362         }
2363         radiobutton $w.validate.always_r \
2364                 -text {Always (Do not perform merge checks)} \
2365                 -value always \
2366                 -variable delete_branch_checktype \
2367                 -font font_ui
2368         grid $w.validate.always_r -columnspan 2 -sticky w
2369         grid columnconfigure $w.validate 1 -weight 1
2370         pack $w.validate -anchor nw -fill x -pady 5 -padx 5
2371
2372         set delete_branch_head $current_branch
2373         set delete_branch_checktype head
2374
2375         bind $w <Visibility> "grab $w; focus $w"
2376         bind $w <Key-Escape> "destroy $w"
2377         wm title $w "[appname] ([reponame]): Delete Branch"
2378         tkwait window $w
2379 }
2380
2381 proc switch_branch {new_branch} {
2382         global HEAD commit_type current_branch repo_config
2383
2384         if {![lock_index switch]} return
2385
2386         # -- Our in memory state should match the repository.
2387         #
2388         repository_state curType curHEAD curMERGE_HEAD
2389         if {[string match amend* $commit_type]
2390                 && $curType eq {normal}
2391                 && $curHEAD eq $HEAD} {
2392         } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
2393                 info_popup {Last scanned state does not match repository state.
2394
2395 Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
2396
2397 The rescan will be automatically started now.
2398 }
2399                 unlock_index
2400                 rescan {set ui_status_value {Ready.}}
2401                 return
2402         }
2403
2404         # -- Don't do a pointless switch.
2405         #
2406         if {$current_branch eq $new_branch} {
2407                 unlock_index
2408                 return
2409         }
2410
2411         if {$repo_config(gui.trustmtime) eq {true}} {
2412                 switch_branch_stage2 {} $new_branch
2413         } else {
2414                 set ui_status_value {Refreshing file status...}
2415                 set cmd [list git update-index]
2416                 lappend cmd -q
2417                 lappend cmd --unmerged
2418                 lappend cmd --ignore-missing
2419                 lappend cmd --refresh
2420                 set fd_rf [open "| $cmd" r]
2421                 fconfigure $fd_rf -blocking 0 -translation binary
2422                 fileevent $fd_rf readable \
2423                         [list switch_branch_stage2 $fd_rf $new_branch]
2424         }
2425 }
2426
2427 proc switch_branch_stage2 {fd_rf new_branch} {
2428         global ui_status_value HEAD
2429
2430         if {$fd_rf ne {}} {
2431                 read $fd_rf
2432                 if {![eof $fd_rf]} return
2433                 close $fd_rf
2434         }
2435
2436         set ui_status_value "Updating working directory to '$new_branch'..."
2437         set cmd [list git read-tree]
2438         lappend cmd -m
2439         lappend cmd -u
2440         lappend cmd --exclude-per-directory=.gitignore
2441         lappend cmd $HEAD
2442         lappend cmd $new_branch
2443         set fd_rt [open "| $cmd" r]
2444         fconfigure $fd_rt -blocking 0 -translation binary
2445         fileevent $fd_rt readable \
2446                 [list switch_branch_readtree_wait $fd_rt $new_branch]
2447 }
2448
2449 proc switch_branch_readtree_wait {fd_rt new_branch} {
2450         global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
2451         global current_branch
2452         global ui_comm ui_status_value
2453
2454         # -- We never get interesting output on stdout; only stderr.
2455         #
2456         read $fd_rt
2457         fconfigure $fd_rt -blocking 1
2458         if {![eof $fd_rt]} {
2459                 fconfigure $fd_rt -blocking 0
2460                 return
2461         }
2462
2463         # -- The working directory wasn't in sync with the index and
2464         #    we'd have to overwrite something to make the switch. A
2465         #    merge is required.
2466         #
2467         if {[catch {close $fd_rt} err]} {
2468                 regsub {^fatal: } $err {} err
2469                 warn_popup "File level merge required.
2470
2471 $err
2472
2473 Staying on branch '$current_branch'."
2474                 set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
2475                 unlock_index
2476                 return
2477         }
2478
2479         # -- Update the symbolic ref.  Core git doesn't even check for failure
2480         #    here, it Just Works(tm).  If it doesn't we are in some really ugly
2481         #    state that is difficult to recover from within git-gui.
2482         #
2483         if {[catch {git symbolic-ref HEAD "refs/heads/$new_branch"} err]} {
2484                 error_popup "Failed to set current branch.
2485
2486 This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
2487
2488 This should not have occurred.  [appname] will now close and give up.
2489
2490 $err"
2491                 do_quit
2492                 return
2493         }
2494
2495         # -- Update our repository state.  If we were previously in amend mode
2496         #    we need to toss the current buffer and do a full rescan to update
2497         #    our file lists.  If we weren't in amend mode our file lists are
2498         #    accurate and we can avoid the rescan.
2499         #
2500         unlock_index
2501         set selected_commit_type new
2502         if {[string match amend* $commit_type]} {
2503                 $ui_comm delete 0.0 end
2504                 $ui_comm edit reset
2505                 $ui_comm edit modified false
2506                 rescan {set ui_status_value "Checked out branch '$current_branch'."}
2507         } else {
2508                 repository_state commit_type HEAD MERGE_HEAD
2509                 set PARENT $HEAD
2510                 set ui_status_value "Checked out branch '$current_branch'."
2511         }
2512 }
2513
2514 ######################################################################
2515 ##
2516 ## remote management
2517
2518 proc load_all_remotes {} {
2519         global repo_config
2520         global all_remotes tracking_branches
2521
2522         set all_remotes [list]
2523         array unset tracking_branches
2524
2525         set rm_dir [gitdir remotes]
2526         if {[file isdirectory $rm_dir]} {
2527                 set all_remotes [glob \
2528                         -types f \
2529                         -tails \
2530                         -nocomplain \
2531                         -directory $rm_dir *]
2532
2533                 foreach name $all_remotes {
2534                         catch {
2535                                 set fd [open [file join $rm_dir $name] r]
2536                                 while {[gets $fd line] >= 0} {
2537                                         if {![regexp {^Pull:[   ]*([^:]+):(.+)$} \
2538                                                 $line line src dst]} continue
2539                                         if {![regexp ^refs/ $dst]} {
2540                                                 set dst "refs/heads/$dst"
2541                                         }
2542                                         set tracking_branches($dst) [list $name $src]
2543                                 }
2544                                 close $fd
2545                         }
2546                 }
2547         }
2548
2549         foreach line [array names repo_config remote.*.url] {
2550                 if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue
2551                 lappend all_remotes $name
2552
2553                 if {[catch {set fl $repo_config(remote.$name.fetch)}]} {
2554                         set fl {}
2555                 }
2556                 foreach line $fl {
2557                         if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue
2558                         if {![regexp ^refs/ $dst]} {
2559                                 set dst "refs/heads/$dst"
2560                         }
2561                         set tracking_branches($dst) [list $name $src]
2562                 }
2563         }
2564
2565         set all_remotes [lsort -unique $all_remotes]
2566 }
2567
2568 proc populate_fetch_menu {} {
2569         global all_remotes repo_config
2570
2571         set m .mbar.fetch
2572         foreach r $all_remotes {
2573                 set enable 0
2574                 if {![catch {set a $repo_config(remote.$r.url)}]} {
2575                         if {![catch {set a $repo_config(remote.$r.fetch)}]} {
2576                                 set enable 1
2577                         }
2578                 } else {
2579                         catch {
2580                                 set fd [open [gitdir remotes $r] r]
2581                                 while {[gets $fd n] >= 0} {
2582                                         if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
2583                                                 set enable 1
2584                                                 break
2585                                         }
2586                                 }
2587                                 close $fd
2588                         }
2589                 }
2590
2591                 if {$enable} {
2592                         $m add command \
2593                                 -label "Fetch from $r..." \
2594                                 -command [list fetch_from $r] \
2595                                 -font font_ui
2596                 }
2597         }
2598 }
2599
2600 proc populate_push_menu {} {
2601         global all_remotes repo_config
2602
2603         set m .mbar.push
2604         set fast_count 0
2605         foreach r $all_remotes {
2606                 set enable 0
2607                 if {![catch {set a $repo_config(remote.$r.url)}]} {
2608                         if {![catch {set a $repo_config(remote.$r.push)}]} {
2609                                 set enable 1
2610                         }
2611                 } else {
2612                         catch {
2613                                 set fd [open [gitdir remotes $r] r]
2614                                 while {[gets $fd n] >= 0} {
2615                                         if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
2616                                                 set enable 1
2617                                                 break
2618                                         }
2619                                 }
2620                                 close $fd
2621                         }
2622                 }
2623
2624                 if {$enable} {
2625                         if {!$fast_count} {
2626                                 $m add separator
2627                         }
2628                         $m add command \
2629                                 -label "Push to $r..." \
2630                                 -command [list push_to $r] \
2631                                 -font font_ui
2632                         incr fast_count
2633                 }
2634         }
2635 }
2636
2637 proc start_push_anywhere_action {w} {
2638         global push_urltype push_remote push_url push_thin push_tags
2639
2640         set r_url {}
2641         switch -- $push_urltype {
2642         remote {set r_url $push_remote}
2643         url {set r_url $push_url}
2644         }
2645         if {$r_url eq {}} return
2646
2647         set cmd [list git push]
2648         lappend cmd -v
2649         if {$push_thin} {
2650                 lappend cmd --thin
2651         }
2652         if {$push_tags} {
2653                 lappend cmd --tags
2654         }
2655         lappend cmd $r_url
2656         set cnt 0
2657         foreach i [$w.source.l curselection] {
2658                 set b [$w.source.l get $i]
2659                 lappend cmd "refs/heads/$b:refs/heads/$b"
2660                 incr cnt
2661         }
2662         if {$cnt == 0} {
2663                 return
2664         } elseif {$cnt == 1} {
2665                 set unit branch
2666         } else {
2667                 set unit branches
2668         }
2669
2670         set cons [new_console "push $r_url" "Pushing $cnt $unit to $r_url"]
2671         console_exec $cons $cmd console_done
2672         destroy $w
2673 }
2674
2675 trace add variable push_remote write \
2676         [list radio_selector push_urltype remote]
2677
2678 proc do_push_anywhere {} {
2679         global all_heads all_remotes current_branch
2680         global push_urltype push_remote push_url push_thin push_tags
2681
2682         set w .push_setup
2683         toplevel $w
2684         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2685
2686         label $w.header -text {Push Branches} -font font_uibold
2687         pack $w.header -side top -fill x
2688
2689         frame $w.buttons
2690         button $w.buttons.create -text Push \
2691                 -font font_ui \
2692                 -default active \
2693                 -command [list start_push_anywhere_action $w]
2694         pack $w.buttons.create -side right
2695         button $w.buttons.cancel -text {Cancel} \
2696                 -font font_ui \
2697                 -default normal \
2698                 -command [list destroy $w]
2699         pack $w.buttons.cancel -side right -padx 5
2700         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2701
2702         labelframe $w.source \
2703                 -text {Source Branches} \
2704                 -font font_ui
2705         listbox $w.source.l \
2706                 -height 10 \
2707                 -width 70 \
2708                 -selectmode extended \
2709                 -yscrollcommand [list $w.source.sby set] \
2710                 -font font_ui
2711         foreach h $all_heads {
2712                 $w.source.l insert end $h
2713                 if {$h eq $current_branch} {
2714                         $w.source.l select set end
2715                 }
2716         }
2717         scrollbar $w.source.sby -command [list $w.source.l yview]
2718         pack $w.source.sby -side right -fill y
2719         pack $w.source.l -side left -fill both -expand 1
2720         pack $w.source -fill both -expand 1 -pady 5 -padx 5
2721
2722         labelframe $w.dest \
2723                 -text {Destination Repository} \
2724                 -font font_ui
2725         if {$all_remotes ne {}} {
2726                 radiobutton $w.dest.remote_r \
2727                         -text {Remote:} \
2728                         -value remote \
2729                         -variable push_urltype \
2730                         -font font_ui
2731                 set remmenu [eval tk_optionMenu $w.dest.remote_m push_remote \
2732                         $all_remotes]
2733                 $remmenu configure -font font_ui
2734                 $w.dest.remote_m configure -font font_ui
2735                 grid $w.dest.remote_r $w.dest.remote_m -sticky w
2736                 if {[lsearch -sorted -exact $all_remotes origin] != -1} {
2737                         set push_remote origin
2738                 } else {
2739                         set push_remote [lindex $all_remotes 0]
2740                 }
2741                 set push_urltype remote
2742         } else {
2743                 set push_urltype url
2744         }
2745         radiobutton $w.dest.url_r \
2746                 -text {Arbitrary URL:} \
2747                 -value url \
2748                 -variable push_urltype \
2749                 -font font_ui
2750         entry $w.dest.url_t \
2751                 -borderwidth 1 \
2752                 -relief sunken \
2753                 -width 50 \
2754                 -textvariable push_url \
2755                 -font font_ui \
2756                 -validate key \
2757                 -validatecommand {
2758                         if {%d == 1 && [regexp {\s} %S]} {return 0}
2759                         if {%d == 1 && [string length %S] > 0} {
2760                                 set push_urltype url
2761                         }
2762                         return 1
2763                 }
2764         grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5}
2765         grid columnconfigure $w.dest 1 -weight 1
2766         pack $w.dest -anchor nw -fill x -pady 5 -padx 5
2767
2768         labelframe $w.options \
2769                 -text {Transfer Options} \
2770                 -font font_ui
2771         checkbutton $w.options.thin \
2772                 -text {Use thin pack (for slow network connections)} \
2773                 -variable push_thin \
2774                 -font font_ui
2775         grid $w.options.thin -columnspan 2 -sticky w
2776         checkbutton $w.options.tags \
2777                 -text {Include tags} \
2778                 -variable push_tags \
2779                 -font font_ui
2780         grid $w.options.tags -columnspan 2 -sticky w
2781         grid columnconfigure $w.options 1 -weight 1
2782         pack $w.options -anchor nw -fill x -pady 5 -padx 5
2783
2784         set push_url {}
2785         set push_thin 0
2786         set push_tags 0
2787
2788         bind $w <Visibility> "grab $w; focus $w.buttons.create"
2789         bind $w <Key-Escape> "destroy $w"
2790         bind $w <Key-Return> [list start_push_anywhere_action $w]
2791         wm title $w "[appname] ([reponame]): Push"
2792         tkwait window $w
2793 }
2794
2795 ######################################################################
2796 ##
2797 ## merge
2798
2799 proc can_merge {} {
2800         global HEAD commit_type file_states
2801
2802         if {[string match amend* $commit_type]} {
2803                 info_popup {Cannot merge while amending.
2804
2805 You must finish amending this commit before starting any type of merge.
2806 }
2807                 return 0
2808         }
2809
2810         if {[committer_ident] eq {}} {return 0}
2811         if {![lock_index merge]} {return 0}
2812
2813         # -- Our in memory state should match the repository.
2814         #
2815         repository_state curType curHEAD curMERGE_HEAD
2816         if {$commit_type ne $curType || $HEAD ne $curHEAD} {
2817                 info_popup {Last scanned state does not match repository state.
2818
2819 Another Git program has modified this repository since the last scan.  A rescan must be performed before a merge can be performed.
2820
2821 The rescan will be automatically started now.
2822 }
2823                 unlock_index
2824                 rescan {set ui_status_value {Ready.}}
2825                 return 0
2826         }
2827
2828         foreach path [array names file_states] {
2829                 switch -glob -- [lindex $file_states($path) 0] {
2830                 _O {
2831                         continue; # and pray it works!
2832                 }
2833                 U? {
2834                         error_popup "You are in the middle of a conflicted merge.
2835
2836 File [short_path $path] has merge conflicts.
2837
2838 You must resolve them, add the file, and commit to complete the current merge.  Only then can you begin another merge.
2839 "
2840                         unlock_index
2841                         return 0
2842                 }
2843                 ?? {
2844                         error_popup "You are in the middle of a change.
2845
2846 File [short_path $path] is modified.
2847
2848 You should complete the current commit before starting a merge.  Doing so will help you abort a failed merge, should the need arise.
2849 "
2850                         unlock_index
2851                         return 0
2852                 }
2853                 }
2854         }
2855
2856         return 1
2857 }
2858
2859 proc visualize_local_merge {w} {
2860         set revs {}
2861         foreach i [$w.source.l curselection] {
2862                 lappend revs [$w.source.l get $i]
2863         }
2864         if {$revs eq {}} return
2865         lappend revs --not HEAD
2866         do_gitk $revs
2867 }
2868
2869 proc start_local_merge_action {w} {
2870         global HEAD ui_status_value current_branch
2871
2872         set cmd [list git merge]
2873         set names {}
2874         set revcnt 0
2875         foreach i [$w.source.l curselection] {
2876                 set b [$w.source.l get $i]
2877                 lappend cmd $b
2878                 lappend names $b
2879                 incr revcnt
2880         }
2881
2882         if {$revcnt == 0} {
2883                 return
2884         } elseif {$revcnt == 1} {
2885                 set unit branch
2886         } elseif {$revcnt <= 15} {
2887                 set unit branches
2888         } else {
2889                 tk_messageBox \
2890                         -icon error \
2891                         -type ok \
2892                         -title [wm title $w] \
2893                         -parent $w \
2894                         -message "Too many branches selected.
2895
2896 You have requested to merge $revcnt branches
2897 in an octopus merge.  This exceeds Git's
2898 internal limit of 15 branches per merge.
2899
2900 Please select fewer branches.  To merge more
2901 than 15 branches, merge the branches in batches.
2902 "
2903                 return
2904         }
2905
2906         set msg "Merging $current_branch, [join $names {, }]"
2907         set ui_status_value "$msg..."
2908         set cons [new_console "Merge" $msg]
2909         console_exec $cons $cmd [list finish_merge $revcnt]
2910         bind $w <Destroy> {}
2911         destroy $w
2912 }
2913
2914 proc finish_merge {revcnt w ok} {
2915         console_done $w $ok
2916         if {$ok} {
2917                 set msg {Merge completed successfully.}
2918         } else {
2919                 if {$revcnt != 1} {
2920                         info_popup "Octopus merge failed.
2921
2922 Your merge of $revcnt branches has failed.
2923
2924 There are file-level conflicts between the branches which must be resolved manually.
2925
2926 The working directory will now be reset.
2927
2928 You can attempt this merge again by merging only one branch at a time." $w
2929
2930                         set fd [open "| git read-tree --reset -u HEAD" r]
2931                         fconfigure $fd -blocking 0 -translation binary
2932                         fileevent $fd readable [list reset_hard_wait $fd]
2933                         set ui_status_value {Aborting... please wait...}
2934                         return
2935                 }
2936
2937                 set msg {Merge failed.  Conflict resolution is required.}
2938         }
2939         unlock_index
2940         rescan [list set ui_status_value $msg]
2941 }
2942
2943 proc do_local_merge {} {
2944         global current_branch
2945
2946         if {![can_merge]} return
2947
2948         set w .merge_setup
2949         toplevel $w
2950         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2951
2952         label $w.header \
2953                 -text "Merge Into $current_branch" \
2954                 -font font_uibold
2955         pack $w.header -side top -fill x
2956
2957         frame $w.buttons
2958         button $w.buttons.visualize -text Visualize \
2959                 -font font_ui \
2960                 -command [list visualize_local_merge $w]
2961         pack $w.buttons.visualize -side left
2962         button $w.buttons.create -text Merge \
2963                 -font font_ui \
2964                 -command [list start_local_merge_action $w]
2965         pack $w.buttons.create -side right
2966         button $w.buttons.cancel -text {Cancel} \
2967                 -font font_ui \
2968                 -command [list destroy $w]
2969         pack $w.buttons.cancel -side right -padx 5
2970         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2971
2972         labelframe $w.source \
2973                 -text {Source Branches} \
2974                 -font font_ui
2975         listbox $w.source.l \
2976                 -height 10 \
2977                 -width 70 \
2978                 -selectmode extended \
2979                 -yscrollcommand [list $w.source.sby set] \
2980                 -font font_ui
2981         scrollbar $w.source.sby -command [list $w.source.l yview]
2982         pack $w.source.sby -side right -fill y
2983         pack $w.source.l -side left -fill both -expand 1
2984         pack $w.source -fill both -expand 1 -pady 5 -padx 5
2985
2986         set cmd [list git for-each-ref]
2987         lappend cmd {--format=%(objectname) %(*objectname) %(refname)}
2988         lappend cmd refs/heads
2989         lappend cmd refs/remotes
2990         lappend cmd refs/tags
2991         set fr_fd [open "| $cmd" r]
2992         fconfigure $fr_fd -translation binary
2993         while {[gets $fr_fd line] > 0} {
2994                 set line [split $line { }]
2995                 set sha1([lindex $line 0]) [lindex $line 2]
2996                 set sha1([lindex $line 1]) [lindex $line 2]
2997         }
2998         close $fr_fd
2999
3000         set to_show {}
3001         set fr_fd [open "| git rev-list --all --not HEAD"]
3002         while {[gets $fr_fd line] > 0} {
3003                 if {[catch {set ref $sha1($line)}]} continue
3004                 regsub ^refs/(heads|remotes|tags)/ $ref {} ref
3005                 lappend to_show $ref
3006         }
3007         close $fr_fd
3008
3009         foreach ref [lsort -unique $to_show] {
3010                 $w.source.l insert end $ref
3011         }
3012
3013         bind $w <Visibility> "grab $w"
3014         bind $w <Key-Escape> "unlock_index;destroy $w"
3015         bind $w <Destroy> unlock_index
3016         wm title $w "[appname] ([reponame]): Merge"
3017         tkwait window $w
3018 }
3019
3020 proc do_reset_hard {} {
3021         global HEAD commit_type file_states
3022
3023         if {[string match amend* $commit_type]} {
3024                 info_popup {Cannot abort while amending.
3025
3026 You must finish amending this commit.
3027 }
3028                 return
3029         }
3030
3031         if {![lock_index abort]} return
3032
3033         if {[string match *merge* $commit_type]} {
3034                 set op merge
3035         } else {
3036                 set op commit
3037         }
3038
3039         if {[ask_popup "Abort $op?
3040
3041 Aborting the current $op will cause *ALL* uncommitted changes to be lost.
3042
3043 Continue with aborting the current $op?"] eq {yes}} {
3044                 set fd [open "| git read-tree --reset -u HEAD" r]
3045                 fconfigure $fd -blocking 0 -translation binary
3046                 fileevent $fd readable [list reset_hard_wait $fd]
3047                 set ui_status_value {Aborting... please wait...}
3048         } else {
3049                 unlock_index
3050         }
3051 }
3052
3053 proc reset_hard_wait {fd} {
3054         global ui_comm
3055
3056         read $fd
3057         if {[eof $fd]} {
3058                 close $fd
3059                 unlock_index
3060
3061                 $ui_comm delete 0.0 end
3062                 $ui_comm edit modified false
3063
3064                 catch {file delete [gitdir MERGE_HEAD]}
3065                 catch {file delete [gitdir rr-cache MERGE_RR]}
3066                 catch {file delete [gitdir SQUASH_MSG]}
3067                 catch {file delete [gitdir MERGE_MSG]}
3068                 catch {file delete [gitdir GITGUI_MSG]}
3069
3070                 rescan {set ui_status_value {Abort completed.  Ready.}}
3071         }
3072 }
3073
3074 ######################################################################
3075 ##
3076 ## browser
3077
3078 set next_browser_id 0
3079
3080 proc new_browser {commit} {
3081         global next_browser_id cursor_ptr M1B
3082         global browser_commit browser_status browser_stack browser_path browser_busy
3083
3084         if {[winfo ismapped .]} {
3085                 set w .browser[incr next_browser_id]
3086                 set tl $w
3087                 toplevel $w
3088         } else {
3089                 set w {}
3090                 set tl .
3091         }
3092         set w_list $w.list.l
3093         set browser_commit($w_list) $commit
3094         set browser_status($w_list) {Starting...}
3095         set browser_stack($w_list) {}
3096         set browser_path($w_list) $browser_commit($w_list):
3097         set browser_busy($w_list) 1
3098
3099         label $w.path -textvariable browser_path($w_list) \
3100                 -anchor w \
3101                 -justify left \
3102                 -borderwidth 1 \
3103                 -relief sunken \
3104                 -font font_uibold
3105         pack $w.path -anchor w -side top -fill x
3106
3107         frame $w.list
3108         text $w_list -background white -borderwidth 0 \
3109                 -cursor $cursor_ptr \
3110                 -state disabled \
3111                 -wrap none \
3112                 -height 20 \
3113                 -width 70 \
3114                 -xscrollcommand [list $w.list.sbx set] \
3115                 -yscrollcommand [list $w.list.sby set] \
3116                 -font font_ui
3117         $w_list tag conf in_sel \
3118                 -background [$w_list cget -foreground] \
3119                 -foreground [$w_list cget -background]
3120         scrollbar $w.list.sbx -orient h -command [list $w_list xview]
3121         scrollbar $w.list.sby -orient v -command [list $w_list yview]
3122         pack $w.list.sbx -side bottom -fill x
3123         pack $w.list.sby -side right -fill y
3124         pack $w_list -side left -fill both -expand 1
3125         pack $w.list -side top -fill both -expand 1
3126
3127         label $w.status -textvariable browser_status($w_list) \
3128                 -anchor w \
3129                 -justify left \
3130                 -borderwidth 1 \
3131                 -relief sunken \
3132                 -font font_ui
3133         pack $w.status -anchor w -side bottom -fill x
3134
3135         bind $w_list <Button-1>        "browser_click 0 $w_list @%x,%y;break"
3136         bind $w_list <Double-Button-1> "browser_click 1 $w_list @%x,%y;break"
3137         bind $w_list <$M1B-Up>         "browser_parent $w_list;break"
3138         bind $w_list <$M1B-Left>       "browser_parent $w_list;break"
3139         bind $w_list <Up>              "browser_move -1 $w_list;break"
3140         bind $w_list <Down>            "browser_move 1 $w_list;break"
3141         bind $w_list <$M1B-Right>      "browser_enter $w_list;break"
3142         bind $w_list <Return>          "browser_enter $w_list;break"
3143         bind $w_list <Prior>           "browser_page -1 $w_list;break"
3144         bind $w_list <Next>            "browser_page 1 $w_list;break"
3145         bind $w_list <Left>            break
3146         bind $w_list <Right>           break
3147
3148         bind $tl <Visibility> "focus $w"
3149         bind $tl <Destroy> "
3150                 array unset browser_buffer $w_list
3151                 array unset browser_files $w_list
3152                 array unset browser_status $w_list
3153                 array unset browser_stack $w_list
3154                 array unset browser_path $w_list
3155                 array unset browser_commit $w_list
3156                 array unset browser_busy $w_list
3157         "
3158         wm title $tl "[appname] ([reponame]): File Browser"
3159         ls_tree $w_list $browser_commit($w_list) {}
3160 }
3161
3162 proc browser_move {dir w} {
3163         global browser_files browser_busy
3164
3165         if {$browser_busy($w)} return
3166         set lno [lindex [split [$w index in_sel.first] .] 0]
3167         incr lno $dir
3168         if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} {
3169                 $w tag remove in_sel 0.0 end
3170                 $w tag add in_sel $lno.0 [expr {$lno + 1}].0
3171                 $w see $lno.0
3172         }
3173 }
3174
3175 proc browser_page {dir w} {
3176         global browser_files browser_busy
3177
3178         if {$browser_busy($w)} return
3179         $w yview scroll $dir pages
3180         set lno [expr {int(
3181                   [lindex [$w yview] 0]
3182                 * [llength $browser_files($w)]
3183                 + 1)}]
3184         if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} {
3185                 $w tag remove in_sel 0.0 end
3186                 $w tag add in_sel $lno.0 [expr {$lno + 1}].0
3187                 $w see $lno.0
3188         }
3189 }
3190
3191 proc browser_parent {w} {
3192         global browser_files browser_status browser_path
3193         global browser_stack browser_busy
3194
3195         if {$browser_busy($w)} return
3196         set info [lindex $browser_files($w) 0]
3197         if {[lindex $info 0] eq {parent}} {
3198                 set parent [lindex $browser_stack($w) end-1]
3199                 set browser_stack($w) [lrange $browser_stack($w) 0 end-2]
3200                 if {$browser_stack($w) eq {}} {
3201                         regsub {:.*$} $browser_path($w) {:} browser_path($w)
3202                 } else {
3203                         regsub {/[^/]+$} $browser_path($w) {} browser_path($w)
3204                 }
3205                 set browser_status($w) "Loading $browser_path($w)..."
3206                 ls_tree $w [lindex $parent 0] [lindex $parent 1]
3207         }
3208 }
3209
3210 proc browser_enter {w} {
3211         global browser_files browser_status browser_path
3212         global browser_commit browser_stack browser_busy
3213
3214         if {$browser_busy($w)} return
3215         set lno [lindex [split [$w index in_sel.first] .] 0]
3216         set info [lindex $browser_files($w) [expr {$lno - 1}]]
3217         if {$info ne {}} {
3218                 switch -- [lindex $info 0] {
3219                 parent {
3220                         browser_parent $w
3221                 }
3222                 tree {
3223                         set name [lindex $info 2]
3224                         set escn [escape_path $name]
3225                         set browser_status($w) "Loading $escn..."
3226                         append browser_path($w) $escn
3227                         ls_tree $w [lindex $info 1] $name
3228                 }
3229                 blob {
3230                         set name [lindex $info 2]
3231                         set p {}
3232                         foreach n $browser_stack($w) {
3233                                 append p [lindex $n 1]
3234                         }
3235                         append p $name
3236                         show_blame $browser_commit($w) $p
3237                 }
3238                 }
3239         }
3240 }
3241
3242 proc browser_click {was_double_click w pos} {
3243         global browser_files browser_busy
3244
3245         if {$browser_busy($w)} return
3246         set lno [lindex [split [$w index $pos] .] 0]
3247         focus $w
3248
3249         if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} {
3250                 $w tag remove in_sel 0.0 end
3251                 $w tag add in_sel $lno.0 [expr {$lno + 1}].0
3252                 if {$was_double_click} {
3253                         browser_enter $w
3254                 }
3255         }
3256 }
3257
3258 proc ls_tree {w tree_id name} {
3259         global browser_buffer browser_files browser_stack browser_busy
3260
3261         set browser_buffer($w) {}
3262         set browser_files($w) {}
3263         set browser_busy($w) 1
3264
3265         $w conf -state normal
3266         $w tag remove in_sel 0.0 end
3267         $w delete 0.0 end
3268         if {$browser_stack($w) ne {}} {
3269                 $w image create end \
3270                         -align center -padx 5 -pady 1 \
3271                         -name icon0 \
3272                         -image file_uplevel
3273                 $w insert end {[Up To Parent]}
3274                 lappend browser_files($w) parent
3275         }
3276         lappend browser_stack($w) [list $tree_id $name]
3277         $w conf -state disabled
3278
3279         set cmd [list git ls-tree -z $tree_id]
3280         set fd [open "| $cmd" r]
3281         fconfigure $fd -blocking 0 -translation binary -encoding binary
3282         fileevent $fd readable [list read_ls_tree $fd $w]
3283 }
3284
3285 proc read_ls_tree {fd w} {
3286         global browser_buffer browser_files browser_status browser_busy
3287
3288         if {![winfo exists $w]} {
3289                 catch {close $fd}
3290                 return
3291         }
3292
3293         append browser_buffer($w) [read $fd]
3294         set pck [split $browser_buffer($w) "\0"]
3295         set browser_buffer($w) [lindex $pck end]
3296
3297         set n [llength $browser_files($w)]
3298         $w conf -state normal
3299         foreach p [lrange $pck 0 end-1] {
3300                 set info [split $p "\t"]
3301                 set path [lindex $info 1]
3302                 set info [split [lindex $info 0] { }]
3303                 set type [lindex $info 1]
3304                 set object [lindex $info 2]
3305
3306                 switch -- $type {
3307                 blob {
3308                         set image file_mod
3309                 }
3310                 tree {
3311                         set image file_dir
3312                         append path /
3313                 }
3314                 default {
3315                         set image file_question
3316                 }
3317                 }
3318
3319                 if {$n > 0} {$w insert end "\n"}
3320                 $w image create end \
3321                         -align center -padx 5 -pady 1 \
3322                         -name icon[incr n] \
3323                         -image $image
3324                 $w insert end [escape_path $path]
3325                 lappend browser_files($w) [list $type $object $path]
3326         }
3327         $w conf -state disabled
3328
3329         if {[eof $fd]} {
3330                 close $fd
3331                 set browser_status($w) Ready.
3332                 set browser_busy($w) 0
3333                 array unset browser_buffer $w
3334                 if {$n > 0} {
3335                         $w tag add in_sel 1.0 2.0
3336                         focus -force $w
3337                 }
3338         }
3339 }
3340
3341 proc show_blame {commit path} {
3342         global next_browser_id blame_status blame_data
3343
3344         if {[winfo ismapped .]} {
3345                 set w .browser[incr next_browser_id]
3346                 set tl $w
3347                 toplevel $w
3348         } else {
3349                 set w {}
3350                 set tl .
3351         }
3352         set blame_status($w) {Loading current file content...}
3353
3354         label $w.path -text "$commit:$path" \
3355                 -anchor w \
3356                 -justify left \
3357                 -borderwidth 1 \
3358                 -relief sunken \
3359                 -font font_uibold
3360         pack $w.path -side top -fill x
3361
3362         frame $w.out
3363         text $w.out.loaded_t \
3364                 -background white -borderwidth 0 \
3365                 -state disabled \
3366                 -wrap none \
3367                 -height 40 \
3368                 -width 1 \
3369                 -font font_diff
3370         $w.out.loaded_t tag conf annotated -background grey
3371
3372         text $w.out.linenumber_t \
3373                 -background white -borderwidth 0 \
3374                 -state disabled \
3375                 -wrap none \
3376                 -height 40 \
3377                 -width 5 \
3378                 -font font_diff
3379         $w.out.linenumber_t tag conf linenumber -justify right
3380
3381         text $w.out.file_t \
3382                 -background white -borderwidth 0 \
3383                 -state disabled \
3384                 -wrap none \
3385                 -height 40 \
3386                 -width 80 \
3387                 -xscrollcommand [list $w.out.sbx set] \
3388                 -font font_diff
3389
3390         scrollbar $w.out.sbx -orient h -command [list $w.out.file_t xview]
3391         scrollbar $w.out.sby -orient v \
3392                 -command [list scrollbar2many [list \
3393                 $w.out.loaded_t \
3394                 $w.out.linenumber_t \
3395                 $w.out.file_t \
3396                 ] yview]
3397         grid \
3398                 $w.out.linenumber_t \
3399                 $w.out.loaded_t \
3400                 $w.out.file_t \
3401                 $w.out.sby \
3402                 -sticky nsew
3403         grid conf $w.out.sbx -column 2 -sticky we
3404         grid columnconfigure $w.out 2 -weight 1
3405         grid rowconfigure $w.out 0 -weight 1
3406         pack $w.out -fill both -expand 1
3407
3408         label $w.status -textvariable blame_status($w) \
3409                 -anchor w \
3410                 -justify left \
3411                 -borderwidth 1 \
3412                 -relief sunken \
3413                 -font font_ui
3414         pack $w.status -side bottom -fill x
3415
3416         frame $w.cm
3417         text $w.cm.t \
3418                 -background white -borderwidth 0 \
3419                 -state disabled \
3420                 -wrap none \
3421                 -height 10 \
3422                 -width 80 \
3423                 -xscrollcommand [list $w.cm.sbx set] \
3424                 -yscrollcommand [list $w.cm.sby set] \
3425                 -font font_diff
3426         scrollbar $w.cm.sbx -orient h -command [list $w.cm.t xview]
3427         scrollbar $w.cm.sby -orient v -command [list $w.cm.t yview]
3428         pack $w.cm.sby -side right -fill y
3429         pack $w.cm.sbx -side bottom -fill x
3430         pack $w.cm.t -expand 1 -fill both
3431         pack $w.cm -side bottom -fill x
3432
3433         menu $w.ctxm -tearoff 0
3434         $w.ctxm add command -label "Copy Commit" \
3435                 -font font_ui \
3436                 -command "blame_copycommit $w \$cursorW @\$cursorX,\$cursorY"
3437
3438         foreach i [list \
3439                 $w.out.loaded_t \
3440                 $w.out.linenumber_t \
3441                 $w.out.file_t] {
3442                 $i tag conf in_sel \
3443                         -background [$i cget -foreground] \
3444                         -foreground [$i cget -background]
3445                 $i conf -yscrollcommand \
3446                         [list many2scrollbar [list \
3447                         $w.out.loaded_t \
3448                         $w.out.linenumber_t \
3449                         $w.out.file_t \
3450                         ] yview $w.out.sby]
3451                 bind $i <Button-1> "
3452                         blame_click {$w} \\
3453                                 $w.cm.t \\
3454                                 $w.out.linenumber_t \\
3455                                 $w.out.file_t \\
3456                                 $i @%x,%y
3457                         focus $i
3458                 "
3459                 bind_button3 $i "
3460                         set cursorX %x
3461                         set cursorY %y
3462                         set cursorW %W
3463                         tk_popup $w.ctxm %X %Y
3464                 "
3465         }
3466
3467         bind $w.cm.t <Button-1> "focus $w.cm.t"
3468         bind $tl <Visibility> "focus $tl"
3469         bind $tl <Destroy> "
3470                 array unset blame_status {$w}
3471                 array unset blame_data $w,*
3472         "
3473         wm title $tl "[appname] ([reponame]): File Viewer"
3474
3475         set blame_data($w,commit_count) 0
3476         set blame_data($w,commit_list) {}
3477         set blame_data($w,total_lines) 0
3478         set blame_data($w,blame_lines) 0
3479         set blame_data($w,highlight_commit) {}
3480         set blame_data($w,highlight_line) -1
3481
3482         set cmd [list git cat-file blob "$commit:$path"]
3483         set fd [open "| $cmd" r]
3484         fconfigure $fd -blocking 0 -translation lf -encoding binary
3485         fileevent $fd readable [list read_blame_catfile \
3486                 $fd $w $commit $path \
3487                 $w.cm.t $w.out.loaded_t $w.out.linenumber_t $w.out.file_t]
3488 }
3489
3490 proc read_blame_catfile {fd w commit path w_cmit w_load w_line w_file} {
3491         global blame_status blame_data
3492
3493         if {![winfo exists $w_file]} {
3494                 catch {close $fd}
3495                 return
3496         }
3497
3498         set n $blame_data($w,total_lines)
3499         $w_load conf -state normal
3500         $w_line conf -state normal
3501         $w_file conf -state normal
3502         while {[gets $fd line] >= 0} {
3503                 regsub "\r\$" $line {} line
3504                 incr n
3505                 $w_load insert end "\n"
3506                 $w_line insert end "$n\n" linenumber
3507                 $w_file insert end "$line\n"
3508         }
3509         $w_load conf -state disabled
3510         $w_line conf -state disabled
3511         $w_file conf -state disabled
3512         set blame_data($w,total_lines) $n
3513
3514         if {[eof $fd]} {
3515                 close $fd
3516                 blame_incremental_status $w
3517                 set cmd [list git blame -M -C --incremental]
3518                 lappend cmd $commit -- $path
3519                 set fd [open "| $cmd" r]
3520                 fconfigure $fd -blocking 0 -translation lf -encoding binary
3521                 fileevent $fd readable [list read_blame_incremental $fd $w \
3522                         $w_load $w_cmit $w_line $w_file]
3523         }
3524 }
3525
3526 proc read_blame_incremental {fd w w_load w_cmit w_line w_file} {
3527         global blame_status blame_data
3528
3529         if {![winfo exists $w_file]} {
3530                 catch {close $fd}
3531                 return
3532         }
3533
3534         while {[gets $fd line] >= 0} {
3535                 if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
3536                         cmit original_line final_line line_count]} {
3537                         set blame_data($w,commit) $cmit
3538                         set blame_data($w,original_line) $original_line
3539                         set blame_data($w,final_line) $final_line
3540                         set blame_data($w,line_count) $line_count
3541
3542                         if {[catch {set g $blame_data($w,$cmit,order)}]} {
3543                                 $w_line tag conf g$cmit
3544                                 $w_file tag conf g$cmit
3545                                 $w_line tag raise in_sel
3546                                 $w_file tag raise in_sel
3547                                 $w_file tag raise sel
3548                                 set blame_data($w,$cmit,order) $blame_data($w,commit_count)
3549                                 incr blame_data($w,commit_count)
3550                                 lappend blame_data($w,commit_list) $cmit
3551                         }
3552                 } elseif {[string match {filename *} $line]} {
3553                         set file [string range $line 9 end]
3554                         set n $blame_data($w,line_count)
3555                         set lno $blame_data($w,final_line)
3556                         set cmit $blame_data($w,commit)
3557
3558                         while {$n > 0} {
3559                                 if {[catch {set g g$blame_data($w,line$lno,commit)}]} {
3560                                         $w_load tag add annotated $lno.0 "$lno.0 lineend + 1c"
3561                                 } else {
3562                                         $w_line tag remove g$g $lno.0 "$lno.0 lineend + 1c"
3563                                         $w_file tag remove g$g $lno.0 "$lno.0 lineend + 1c"
3564                                 }
3565
3566                                 set blame_data($w,line$lno,commit) $cmit
3567                                 set blame_data($w,line$lno,file) $file
3568                                 $w_line tag add g$cmit $lno.0 "$lno.0 lineend + 1c"
3569                                 $w_file tag add g$cmit $lno.0 "$lno.0 lineend + 1c"
3570
3571                                 if {$blame_data($w,highlight_line) == -1} {
3572                                         if {[lindex [$w_file yview] 0] == 0} {
3573                                                 $w_file see $lno.0
3574                                                 blame_showcommit $w $w_cmit $w_line $w_file $lno
3575                                         }
3576                                 } elseif {$blame_data($w,highlight_line) == $lno} {
3577                                         blame_showcommit $w $w_cmit $w_line $w_file $lno
3578                                 }
3579
3580                                 incr n -1
3581                                 incr lno
3582                                 incr blame_data($w,blame_lines)
3583                         }
3584
3585                         set hc $blame_data($w,highlight_commit)
3586                         if {$hc ne {}
3587                                 && [expr {$blame_data($w,$hc,order) + 1}]
3588                                         == $blame_data($w,$cmit,order)} {
3589                                 blame_showcommit $w $w_cmit $w_line $w_file \
3590                                         $blame_data($w,highlight_line)
3591                         }
3592                 } elseif {[regexp {^([a-z-]+) (.*)$} $line line header data]} {
3593                         set blame_data($w,$blame_data($w,commit),$header) $data
3594                 }
3595         }
3596
3597         if {[eof $fd]} {
3598                 close $fd
3599                 set blame_status($w) {Annotation complete.}
3600         } else {
3601                 blame_incremental_status $w
3602         }
3603 }
3604
3605 proc blame_incremental_status {w} {
3606         global blame_status blame_data
3607
3608         set have  $blame_data($w,blame_lines)
3609         set total $blame_data($w,total_lines)
3610         set pdone 0
3611         if {$total} {set pdone [expr {100 * $have / $total}]}
3612
3613         set blame_status($w) [format \
3614                 "Loading annotations... %i of %i lines annotated (%2i%%)" \
3615                 $have $total $pdone]
3616 }
3617
3618 proc blame_click {w w_cmit w_line w_file cur_w pos} {
3619         set lno [lindex [split [$cur_w index $pos] .] 0]
3620         if {$lno eq {}} return
3621
3622         $w_line tag remove in_sel 0.0 end
3623         $w_file tag remove in_sel 0.0 end
3624         $w_line tag add in_sel $lno.0 "$lno.0 + 1 line"
3625         $w_file tag add in_sel $lno.0 "$lno.0 + 1 line"
3626
3627         blame_showcommit $w $w_cmit $w_line $w_file $lno
3628 }
3629
3630 set blame_colors {
3631         #ff4040
3632         #ff40ff
3633         #4040ff
3634 }
3635
3636 proc blame_showcommit {w w_cmit w_line w_file lno} {
3637         global blame_colors blame_data repo_config
3638
3639         set cmit $blame_data($w,highlight_commit)
3640         if {$cmit ne {}} {
3641                 set idx $blame_data($w,$cmit,order)
3642                 set i 0
3643                 foreach c $blame_colors {
3644                         set h [lindex $blame_data($w,commit_list) [expr {$idx - 1 + $i}]]
3645                         $w_line tag conf g$h -background white
3646                         $w_file tag conf g$h -background white
3647                         incr i
3648                 }
3649         }
3650
3651         $w_cmit conf -state normal
3652         $w_cmit delete 0.0 end
3653         if {[catch {set cmit $blame_data($w,line$lno,commit)}]} {
3654                 set cmit {}
3655                 $w_cmit insert end "Loading annotation..."
3656         } else {
3657                 set idx $blame_data($w,$cmit,order)
3658                 set i 0
3659                 foreach c $blame_colors {
3660                         set h [lindex $blame_data($w,commit_list) [expr {$idx - 1 + $i}]]
3661                         $w_line tag conf g$h -background $c
3662                         $w_file tag conf g$h -background $c
3663                         incr i
3664                 }
3665
3666                 set author_name {}
3667                 set author_email {}
3668                 set author_time {}
3669                 catch {set author_name $blame_data($w,$cmit,author)}
3670                 catch {set author_email $blame_data($w,$cmit,author-mail)}
3671                 catch {set author_time [clock format $blame_data($w,$cmit,author-time)]}
3672
3673                 set committer_name {}
3674                 set committer_email {}
3675                 set committer_time {}
3676                 catch {set committer_name $blame_data($w,$cmit,committer)}
3677                 catch {set committer_email $blame_data($w,$cmit,committer-mail)}
3678                 catch {set committer_time [clock format $blame_data($w,$cmit,committer-time)]}
3679
3680                 if {[catch {set msg $blame_data($w,$cmit,message)}]} {
3681                         set msg {}
3682                         catch {
3683                                 set fd [open "| git cat-file commit $cmit" r]
3684                                 fconfigure $fd -encoding binary -translation lf
3685                                 if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
3686                                         set enc utf-8
3687                                 }
3688                                 while {[gets $fd line] > 0} {
3689                                         if {[string match {encoding *} $line]} {
3690                                                 set enc [string tolower [string range $line 9 end]]
3691                                         }
3692                                 }
3693                                 set msg [encoding convertfrom $enc [read $fd]]
3694                                 set msg [string trim $msg]
3695                                 close $fd
3696
3697                                 set author_name [encoding convertfrom $enc $author_name]
3698                                 set committer_name [encoding convertfrom $enc $committer_name]
3699
3700                                 set blame_data($w,$cmit,author) $author_name
3701                                 set blame_data($w,$cmit,committer) $committer_name
3702                         }
3703                         set blame_data($w,$cmit,message) $msg
3704                 }
3705
3706                 $w_cmit insert end "commit $cmit\n"
3707                 $w_cmit insert end "Author: $author_name $author_email $author_time\n"
3708                 $w_cmit insert end "Committer: $committer_name $committer_email $committer_time\n"
3709                 $w_cmit insert end "Original File: [escape_path $blame_data($w,line$lno,file)]\n"
3710                 $w_cmit insert end "\n"
3711                 $w_cmit insert end $msg
3712         }
3713         $w_cmit conf -state disabled
3714
3715         set blame_data($w,highlight_line) $lno
3716         set blame_data($w,highlight_commit) $cmit
3717 }
3718
3719 proc blame_copycommit {w i pos} {
3720         global blame_data
3721         set lno [lindex [split [$i index $pos] .] 0]
3722         if {![catch {set commit $blame_data($w,line$lno,commit)}]} {
3723                 clipboard clear
3724                 clipboard append \
3725                         -format STRING \
3726                         -type STRING \
3727                         -- $commit
3728         }
3729 }
3730
3731 ######################################################################
3732 ##
3733 ## icons
3734
3735 set filemask {
3736 #define mask_width 14
3737 #define mask_height 15
3738 static unsigned char mask_bits[] = {
3739    0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
3740    0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
3741    0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
3742 }
3743
3744 image create bitmap file_plain -background white -foreground black -data {
3745 #define plain_width 14
3746 #define plain_height 15
3747 static unsigned char plain_bits[] = {
3748    0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
3749    0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
3750    0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3751 } -maskdata $filemask
3752
3753 image create bitmap file_mod -background white -foreground blue -data {
3754 #define mod_width 14
3755 #define mod_height 15
3756 static unsigned char mod_bits[] = {
3757    0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
3758    0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
3759    0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
3760 } -maskdata $filemask
3761
3762 image create bitmap file_fulltick -background white -foreground "#007000" -data {
3763 #define file_fulltick_width 14
3764 #define file_fulltick_height 15
3765 static unsigned char file_fulltick_bits[] = {
3766    0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
3767    0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
3768    0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3769 } -maskdata $filemask
3770
3771 image create bitmap file_parttick -background white -foreground "#005050" -data {
3772 #define parttick_width 14
3773 #define parttick_height 15
3774 static unsigned char parttick_bits[] = {
3775    0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
3776    0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
3777    0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3778 } -maskdata $filemask
3779
3780 image create bitmap file_question -background white -foreground black -data {
3781 #define file_question_width 14
3782 #define file_question_height 15
3783 static unsigned char file_question_bits[] = {
3784    0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
3785    0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
3786    0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3787 } -maskdata $filemask
3788
3789 image create bitmap file_removed -background white -foreground red -data {
3790 #define file_removed_width 14
3791 #define file_removed_height 15
3792 static unsigned char file_removed_bits[] = {
3793    0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
3794    0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
3795    0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
3796 } -maskdata $filemask
3797
3798 image create bitmap file_merge -background white -foreground blue -data {
3799 #define file_merge_width 14
3800 #define file_merge_height 15
3801 static unsigned char file_merge_bits[] = {
3802    0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
3803    0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
3804    0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
3805 } -maskdata $filemask
3806
3807 set file_dir_data {
3808 #define file_width 18
3809 #define file_height 18
3810 static unsigned char file_bits[] = {
3811   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x03, 0x00,
3812   0x0c, 0x03, 0x00, 0x04, 0xfe, 0x00, 0x06, 0x80, 0x00, 0xff, 0x9f, 0x00,
3813   0x03, 0x98, 0x00, 0x02, 0x90, 0x00, 0x06, 0xb0, 0x00, 0x04, 0xa0, 0x00,
3814   0x0c, 0xe0, 0x00, 0x08, 0xc0, 0x00, 0xf8, 0xff, 0x00, 0x00, 0x00, 0x00,
3815   0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
3816 }
3817 image create bitmap file_dir -background white -foreground blue \
3818         -data $file_dir_data -maskdata $file_dir_data
3819 unset file_dir_data
3820
3821 set file_uplevel_data {
3822 #define up_width 15
3823 #define up_height 15
3824 static unsigned char up_bits[] = {
3825   0x80, 0x00, 0xc0, 0x01, 0xe0, 0x03, 0xf0, 0x07, 0xf8, 0x0f, 0xfc, 0x1f,
3826   0xfe, 0x3f, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01,
3827   0xc0, 0x01, 0xc0, 0x01, 0x00, 0x00};
3828 }
3829 image create bitmap file_uplevel -background white -foreground red \
3830         -data $file_uplevel_data -maskdata $file_uplevel_data
3831 unset file_uplevel_data
3832
3833 set ui_index .vpane.files.index.list
3834 set ui_workdir .vpane.files.workdir.list
3835
3836 set all_icons(_$ui_index)   file_plain
3837 set all_icons(A$ui_index)   file_fulltick
3838 set all_icons(M$ui_index)   file_fulltick
3839 set all_icons(D$ui_index)   file_removed
3840 set all_icons(U$ui_index)   file_merge
3841
3842 set all_icons(_$ui_workdir) file_plain
3843 set all_icons(M$ui_workdir) file_mod
3844 set all_icons(D$ui_workdir) file_question
3845 set all_icons(U$ui_workdir) file_merge
3846 set all_icons(O$ui_workdir) file_plain
3847
3848 set max_status_desc 0
3849 foreach i {
3850                 {__ "Unmodified"}
3851
3852                 {_M "Modified, not staged"}
3853                 {M_ "Staged for commit"}
3854                 {MM "Portions staged for commit"}
3855                 {MD "Staged for commit, missing"}
3856
3857                 {_O "Untracked, not staged"}
3858                 {A_ "Staged for commit"}
3859                 {AM "Portions staged for commit"}
3860                 {AD "Staged for commit, missing"}
3861
3862                 {_D "Missing"}
3863                 {D_ "Staged for removal"}
3864                 {DO "Staged for removal, still present"}
3865
3866                 {U_ "Requires merge resolution"}
3867                 {UU "Requires merge resolution"}
3868                 {UM "Requires merge resolution"}
3869                 {UD "Requires merge resolution"}
3870         } {
3871         if {$max_status_desc < [string length [lindex $i 1]]} {
3872                 set max_status_desc [string length [lindex $i 1]]
3873         }
3874         set all_descs([lindex $i 0]) [lindex $i 1]
3875 }
3876 unset i
3877
3878 ######################################################################
3879 ##
3880 ## util
3881
3882 proc bind_button3 {w cmd} {
3883         bind $w <Any-Button-3> $cmd
3884         if {[is_MacOSX]} {
3885                 bind $w <Control-Button-1> $cmd
3886         }
3887 }
3888
3889 proc scrollbar2many {list mode args} {
3890         foreach w $list {eval $w $mode $args}
3891 }
3892
3893 proc many2scrollbar {list mode sb top bottom} {
3894         $sb set $top $bottom
3895         foreach w $list {$w $mode moveto $top}
3896 }
3897
3898 proc incr_font_size {font {amt 1}} {
3899         set sz [font configure $font -size]
3900         incr sz $amt
3901         font configure $font -size $sz
3902         font configure ${font}bold -size $sz
3903 }
3904
3905 proc hook_failed_popup {hook msg} {
3906         set w .hookfail
3907         toplevel $w
3908
3909         frame $w.m
3910         label $w.m.l1 -text "$hook hook failed:" \
3911                 -anchor w \
3912                 -justify left \
3913                 -font font_uibold
3914         text $w.m.t \
3915                 -background white -borderwidth 1 \
3916                 -relief sunken \
3917                 -width 80 -height 10 \
3918                 -font font_diff \
3919                 -yscrollcommand [list $w.m.sby set]
3920         label $w.m.l2 \
3921                 -text {You must correct the above errors before committing.} \
3922                 -anchor w \
3923                 -justify left \
3924                 -font font_uibold
3925         scrollbar $w.m.sby -command [list $w.m.t yview]
3926         pack $w.m.l1 -side top -fill x
3927         pack $w.m.l2 -side bottom -fill x
3928         pack $w.m.sby -side right -fill y
3929         pack $w.m.t -side left -fill both -expand 1
3930         pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
3931
3932         $w.m.t insert 1.0 $msg
3933         $w.m.t conf -state disabled
3934
3935         button $w.ok -text OK \
3936                 -width 15 \
3937                 -font font_ui \
3938                 -command "destroy $w"
3939         pack $w.ok -side bottom -anchor e -pady 10 -padx 10
3940
3941         bind $w <Visibility> "grab $w; focus $w"
3942         bind $w <Key-Return> "destroy $w"
3943         wm title $w "[appname] ([reponame]): error"
3944         tkwait window $w
3945 }
3946
3947 set next_console_id 0
3948
3949 proc new_console {short_title long_title} {
3950         global next_console_id console_data
3951         set w .console[incr next_console_id]
3952         set console_data($w) [list $short_title $long_title]
3953         return [console_init $w]
3954 }
3955
3956 proc console_init {w} {
3957         global console_cr console_data M1B
3958
3959         set console_cr($w) 1.0
3960         toplevel $w
3961         frame $w.m
3962         label $w.m.l1 -text "[lindex $console_data($w) 1]:" \
3963                 -anchor w \
3964                 -justify left \
3965                 -font font_uibold
3966         text $w.m.t \
3967                 -background white -borderwidth 1 \
3968                 -relief sunken \
3969                 -width 80 -height 10 \
3970                 -font font_diff \
3971                 -state disabled \
3972                 -yscrollcommand [list $w.m.sby set]
3973         label $w.m.s -text {Working... please wait...} \
3974                 -anchor w \
3975                 -justify left \
3976                 -font font_uibold
3977         scrollbar $w.m.sby -command [list $w.m.t yview]
3978         pack $w.m.l1 -side top -fill x
3979         pack $w.m.s -side bottom -fill x
3980         pack $w.m.sby -side right -fill y
3981         pack $w.m.t -side left -fill both -expand 1
3982         pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
3983
3984         menu $w.ctxm -tearoff 0
3985         $w.ctxm add command -label "Copy" \
3986                 -font font_ui \
3987                 -command "tk_textCopy $w.m.t"
3988         $w.ctxm add command -label "Select All" \
3989                 -font font_ui \
3990                 -command "focus $w.m.t;$w.m.t tag add sel 0.0 end"
3991         $w.ctxm add command -label "Copy All" \
3992                 -font font_ui \
3993                 -command "
3994                         $w.m.t tag add sel 0.0 end
3995                         tk_textCopy $w.m.t
3996                         $w.m.t tag remove sel 0.0 end
3997                 "
3998
3999         button $w.ok -text {Close} \
4000                 -font font_ui \
4001                 -state disabled \
4002                 -command "destroy $w"
4003         pack $w.ok -side bottom -anchor e -pady 10 -padx 10
4004
4005         bind_button3 $w.m.t "tk_popup $w.ctxm %X %Y"
4006         bind $w.m.t <$M1B-Key-a> "$w.m.t tag add sel 0.0 end;break"
4007         bind $w.m.t <$M1B-Key-A> "$w.m.t tag add sel 0.0 end;break"
4008         bind $w <Visibility> "focus $w"
4009         wm title $w "[appname] ([reponame]): [lindex $console_data($w) 0]"
4010         return $w
4011 }
4012
4013 proc console_exec {w cmd after} {
4014         # -- Cygwin's Tcl tosses the enviroment when we exec our child.
4015         #    But most users need that so we have to relogin. :-(
4016         #
4017         if {[is_Cygwin]} {
4018                 set cmd [list sh --login -c "cd \"[pwd]\" && [join $cmd { }]"]
4019         }
4020
4021         # -- Tcl won't let us redirect both stdout and stderr to
4022         #    the same pipe.  So pass it through cat...
4023         #
4024         set cmd [concat | $cmd |& cat]
4025
4026         set fd_f [open $cmd r]
4027         fconfigure $fd_f -blocking 0 -translation binary
4028         fileevent $fd_f readable [list console_read $w $fd_f $after]
4029 }
4030
4031 proc console_read {w fd after} {
4032         global console_cr
4033
4034         set buf [read $fd]
4035         if {$buf ne {}} {
4036                 if {![winfo exists $w]} {console_init $w}
4037                 $w.m.t conf -state normal
4038                 set c 0
4039                 set n [string length $buf]
4040                 while {$c < $n} {
4041                         set cr [string first "\r" $buf $c]
4042                         set lf [string first "\n" $buf $c]
4043                         if {$cr < 0} {set cr [expr {$n + 1}]}
4044                         if {$lf < 0} {set lf [expr {$n + 1}]}
4045
4046                         if {$lf < $cr} {
4047                                 $w.m.t insert end [string range $buf $c $lf]
4048                                 set console_cr($w) [$w.m.t index {end -1c}]
4049                                 set c $lf
4050                                 incr c
4051                         } else {
4052                                 $w.m.t delete $console_cr($w) end
4053                                 $w.m.t insert end "\n"
4054                                 $w.m.t insert end [string range $buf $c $cr]
4055                                 set c $cr
4056                                 incr c
4057                         }
4058                 }
4059                 $w.m.t conf -state disabled
4060                 $w.m.t see end
4061         }
4062
4063         fconfigure $fd -blocking 1
4064         if {[eof $fd]} {
4065                 if {[catch {close $fd}]} {
4066                         set ok 0
4067                 } else {
4068                         set ok 1
4069                 }
4070                 uplevel #0 $after $w $ok
4071                 return
4072         }
4073         fconfigure $fd -blocking 0
4074 }
4075
4076 proc console_chain {cmdlist w {ok 1}} {
4077         if {$ok} {
4078                 if {[llength $cmdlist] == 0} {
4079                         console_done $w $ok
4080                         return
4081                 }
4082
4083                 set cmd [lindex $cmdlist 0]
4084                 set cmdlist [lrange $cmdlist 1 end]
4085
4086                 if {[lindex $cmd 0] eq {console_exec}} {
4087                         console_exec $w \
4088                                 [lindex $cmd 1] \
4089                                 [list console_chain $cmdlist]
4090                 } else {
4091                         uplevel #0 $cmd $cmdlist $w $ok
4092                 }
4093         } else {
4094                 console_done $w $ok
4095         }
4096 }
4097
4098 proc console_done {args} {
4099         global console_cr console_data
4100
4101         switch -- [llength $args] {
4102         2 {
4103                 set w [lindex $args 0]
4104                 set ok [lindex $args 1]
4105         }
4106         3 {
4107                 set w [lindex $args 1]
4108                 set ok [lindex $args 2]
4109         }
4110         default {
4111                 error "wrong number of args: console_done ?ignored? w ok"
4112         }
4113         }
4114
4115         if {$ok} {
4116                 if {[winfo exists $w]} {
4117                         $w.m.s conf -background green -text {Success}
4118                         $w.ok conf -state normal
4119                         focus $w.ok
4120                 }
4121         } else {
4122                 if {![winfo exists $w]} {
4123                         console_init $w
4124                 }
4125                 $w.m.s conf -background red -text {Error: Command Failed}
4126                 $w.ok conf -state normal
4127                 focus $w.ok
4128         }
4129
4130         array unset console_cr $w
4131         array unset console_data $w
4132 }
4133
4134 ######################################################################
4135 ##
4136 ## ui commands
4137
4138 set starting_gitk_msg {Starting gitk... please wait...}
4139
4140 proc do_gitk {revs} {
4141         global env ui_status_value starting_gitk_msg
4142
4143         # -- Always start gitk through whatever we were loaded with.  This
4144         #    lets us bypass using shell process on Windows systems.
4145         #
4146         set cmd [info nameofexecutable]
4147         lappend cmd [gitexec gitk]
4148         if {$revs ne {}} {
4149                 append cmd { }
4150                 append cmd $revs
4151         }
4152
4153         if {[catch {eval exec $cmd &} err]} {
4154                 error_popup "Failed to start gitk:\n\n$err"
4155         } else {
4156                 set ui_status_value $starting_gitk_msg
4157                 after 10000 {
4158                         if {$ui_status_value eq $starting_gitk_msg} {
4159                                 set ui_status_value {Ready.}
4160                         }
4161                 }
4162         }
4163 }
4164
4165 proc do_stats {} {
4166         set fd [open "| git count-objects -v" r]
4167         while {[gets $fd line] > 0} {
4168                 if {[regexp {^([^:]+): (\d+)$} $line _ name value]} {
4169                         set stats($name) $value
4170                 }
4171         }
4172         close $fd
4173
4174         set packed_sz 0
4175         foreach p [glob -directory [gitdir objects pack] \
4176                 -type f \
4177                 -nocomplain -- *] {
4178                 incr packed_sz [file size $p]
4179         }
4180         if {$packed_sz > 0} {
4181                 set stats(size-pack) [expr {$packed_sz / 1024}]
4182         }
4183
4184         set w .stats_view
4185         toplevel $w
4186         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
4187
4188         label $w.header -text {Database Statistics} \
4189                 -font font_uibold
4190         pack $w.header -side top -fill x
4191
4192         frame $w.buttons -border 1
4193         button $w.buttons.close -text Close \
4194                 -font font_ui \
4195                 -default active \
4196                 -command [list destroy $w]
4197         button $w.buttons.gc -text {Compress Database} \
4198                 -font font_ui \
4199                 -default normal \
4200                 -command "destroy $w;do_gc"
4201         pack $w.buttons.close -side right
4202         pack $w.buttons.gc -side left
4203         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
4204
4205         frame $w.stat -borderwidth 1 -relief solid
4206         foreach s {
4207                 {count           {Number of loose objects}}
4208                 {size            {Disk space used by loose objects} { KiB}}
4209                 {in-pack         {Number of packed objects}}
4210                 {packs           {Number of packs}}
4211                 {size-pack       {Disk space used by packed objects} { KiB}}
4212                 {prune-packable  {Packed objects waiting for pruning}}
4213                 {garbage         {Garbage files}}
4214                 } {
4215                 set name [lindex $s 0]
4216                 set label [lindex $s 1]
4217                 if {[catch {set value $stats($name)}]} continue
4218                 if {[llength $s] > 2} {
4219                         set value "$value[lindex $s 2]"
4220                 }
4221
4222                 label $w.stat.l_$name -text "$label:" -anchor w -font font_ui
4223                 label $w.stat.v_$name -text $value -anchor w -font font_ui
4224                 grid $w.stat.l_$name $w.stat.v_$name -sticky we -padx {0 5}
4225         }
4226         pack $w.stat -pady 10 -padx 10
4227
4228         bind $w <Visibility> "grab $w; focus $w.buttons.close"
4229         bind $w <Key-Escape> [list destroy $w]
4230         bind $w <Key-Return> [list destroy $w]
4231         wm title $w "[appname] ([reponame]): Database Statistics"
4232         tkwait window $w
4233 }
4234
4235 proc do_gc {} {
4236         set w [new_console {gc} {Compressing the object database}]
4237         console_chain {
4238                 {console_exec {git pack-refs --prune}}
4239                 {console_exec {git reflog expire --all}}
4240                 {console_exec {git repack -a -d -l}}
4241                 {console_exec {git rerere gc}}
4242         } $w
4243 }
4244
4245 proc do_fsck_objects {} {
4246         set w [new_console {fsck-objects} \
4247                 {Verifying the object database with fsck-objects}]
4248         set cmd [list git fsck-objects]
4249         lappend cmd --full
4250         lappend cmd --cache
4251         lappend cmd --strict
4252         console_exec $w $cmd console_done
4253 }
4254
4255 set is_quitting 0
4256
4257 proc do_quit {} {
4258         global ui_comm is_quitting repo_config commit_type
4259
4260         if {$is_quitting} return
4261         set is_quitting 1
4262
4263         if {[winfo exists $ui_comm]} {
4264                 # -- Stash our current commit buffer.
4265                 #
4266                 set save [gitdir GITGUI_MSG]
4267                 set msg [string trim [$ui_comm get 0.0 end]]
4268                 regsub -all -line {[ \r\t]+$} $msg {} msg
4269                 if {(![string match amend* $commit_type]
4270                         || [$ui_comm edit modified])
4271                         && $msg ne {}} {
4272                         catch {
4273                                 set fd [open $save w]
4274                                 puts -nonewline $fd $msg
4275                                 close $fd
4276                         }
4277                 } else {
4278                         catch {file delete $save}
4279                 }
4280
4281                 # -- Stash our current window geometry into this repository.
4282                 #
4283                 set cfg_geometry [list]
4284                 lappend cfg_geometry [wm geometry .]
4285                 lappend cfg_geometry [lindex [.vpane sash coord 0] 1]
4286                 lappend cfg_geometry [lindex [.vpane.files sash coord 0] 0]
4287                 if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
4288                         set rc_geometry {}
4289                 }
4290                 if {$cfg_geometry ne $rc_geometry} {
4291                         catch {git config gui.geometry $cfg_geometry}
4292                 }
4293         }
4294
4295         destroy .
4296 }
4297
4298 proc do_rescan {} {
4299         rescan {set ui_status_value {Ready.}}
4300 }
4301
4302 proc unstage_helper {txt paths} {
4303         global file_states current_diff_path
4304
4305         if {![lock_index begin-update]} return
4306
4307         set pathList [list]
4308         set after {}
4309         foreach path $paths {
4310                 switch -glob -- [lindex $file_states($path) 0] {
4311                 A? -
4312                 M? -
4313                 D? {
4314                         lappend pathList $path
4315                         if {$path eq $current_diff_path} {
4316                                 set after {reshow_diff;}
4317                         }
4318                 }
4319                 }
4320         }
4321         if {$pathList eq {}} {
4322                 unlock_index
4323         } else {
4324                 update_indexinfo \
4325                         $txt \
4326                         $pathList \
4327                         [concat $after {set ui_status_value {Ready.}}]
4328         }
4329 }
4330
4331 proc do_unstage_selection {} {
4332         global current_diff_path selected_paths
4333
4334         if {[array size selected_paths] > 0} {
4335                 unstage_helper \
4336                         {Unstaging selected files from commit} \
4337                         [array names selected_paths]
4338         } elseif {$current_diff_path ne {}} {
4339                 unstage_helper \
4340                         "Unstaging [short_path $current_diff_path] from commit" \
4341                         [list $current_diff_path]
4342         }
4343 }
4344
4345 proc add_helper {txt paths} {
4346         global file_states current_diff_path
4347
4348         if {![lock_index begin-update]} return
4349
4350         set pathList [list]
4351         set after {}
4352         foreach path $paths {
4353                 switch -glob -- [lindex $file_states($path) 0] {
4354                 _O -
4355                 ?M -
4356                 ?D -
4357                 U? {
4358                         lappend pathList $path
4359                         if {$path eq $current_diff_path} {
4360                                 set after {reshow_diff;}
4361                         }
4362                 }
4363                 }
4364         }
4365         if {$pathList eq {}} {
4366                 unlock_index
4367         } else {
4368                 update_index \
4369                         $txt \
4370                         $pathList \
4371                         [concat $after {set ui_status_value {Ready to commit.}}]
4372         }
4373 }
4374
4375 proc do_add_selection {} {
4376         global current_diff_path selected_paths
4377
4378         if {[array size selected_paths] > 0} {
4379                 add_helper \
4380                         {Adding selected files} \
4381                         [array names selected_paths]
4382         } elseif {$current_diff_path ne {}} {
4383                 add_helper \
4384                         "Adding [short_path $current_diff_path]" \
4385                         [list $current_diff_path]
4386         }
4387 }
4388
4389 proc do_add_all {} {
4390         global file_states
4391
4392         set paths [list]
4393         foreach path [array names file_states] {
4394                 switch -glob -- [lindex $file_states($path) 0] {
4395                 U? {continue}
4396                 ?M -
4397                 ?D {lappend paths $path}
4398                 }
4399         }
4400         add_helper {Adding all changed files} $paths
4401 }
4402
4403 proc revert_helper {txt paths} {
4404         global file_states current_diff_path
4405
4406         if {![lock_index begin-update]} return
4407
4408         set pathList [list]
4409         set after {}
4410         foreach path $paths {
4411                 switch -glob -- [lindex $file_states($path) 0] {
4412                 U? {continue}
4413                 ?M -
4414                 ?D {
4415                         lappend pathList $path
4416                         if {$path eq $current_diff_path} {
4417                                 set after {reshow_diff;}
4418                         }
4419                 }
4420                 }
4421         }
4422
4423         set n [llength $pathList]
4424         if {$n == 0} {
4425                 unlock_index
4426                 return
4427         } elseif {$n == 1} {
4428                 set s "[short_path [lindex $pathList]]"
4429         } else {
4430                 set s "these $n files"
4431         }
4432
4433         set reply [tk_dialog \
4434                 .confirm_revert \
4435                 "[appname] ([reponame])" \
4436                 "Revert changes in $s?
4437
4438 Any unadded changes will be permanently lost by the revert." \
4439                 question \
4440                 1 \
4441                 {Do Nothing} \
4442                 {Revert Changes} \
4443                 ]
4444         if {$reply == 1} {
4445                 checkout_index \
4446                         $txt \
4447                         $pathList \
4448                         [concat $after {set ui_status_value {Ready.}}]
4449         } else {
4450                 unlock_index
4451         }
4452 }
4453
4454 proc do_revert_selection {} {
4455         global current_diff_path selected_paths
4456
4457         if {[array size selected_paths] > 0} {
4458                 revert_helper \
4459                         {Reverting selected files} \
4460                         [array names selected_paths]
4461         } elseif {$current_diff_path ne {}} {
4462                 revert_helper \
4463                         "Reverting [short_path $current_diff_path]" \
4464                         [list $current_diff_path]
4465         }
4466 }
4467
4468 proc do_signoff {} {
4469         global ui_comm
4470
4471         set me [committer_ident]
4472         if {$me eq {}} return
4473
4474         set sob "Signed-off-by: $me"
4475         set last [$ui_comm get {end -1c linestart} {end -1c}]
4476         if {$last ne $sob} {
4477                 $ui_comm edit separator
4478                 if {$last ne {}
4479                         && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
4480                         $ui_comm insert end "\n"
4481                 }
4482                 $ui_comm insert end "\n$sob"
4483                 $ui_comm edit separator
4484                 $ui_comm see end
4485         }
4486 }
4487
4488 proc do_select_commit_type {} {
4489         global commit_type selected_commit_type
4490
4491         if {$selected_commit_type eq {new}
4492                 && [string match amend* $commit_type]} {
4493                 create_new_commit
4494         } elseif {$selected_commit_type eq {amend}
4495                 && ![string match amend* $commit_type]} {
4496                 load_last_commit
4497
4498                 # The amend request was rejected...
4499                 #
4500                 if {![string match amend* $commit_type]} {
4501                         set selected_commit_type new
4502                 }
4503         }
4504 }
4505
4506 proc do_commit {} {
4507         commit_tree
4508 }
4509
4510 proc do_about {} {
4511         global appvers copyright
4512         global tcl_patchLevel tk_patchLevel
4513
4514         set w .about_dialog
4515         toplevel $w
4516         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
4517
4518         label $w.header -text "About [appname]" \
4519                 -font font_uibold
4520         pack $w.header -side top -fill x
4521
4522         frame $w.buttons
4523         button $w.buttons.close -text {Close} \
4524                 -font font_ui \
4525                 -default active \
4526                 -command [list destroy $w]
4527         pack $w.buttons.close -side right
4528         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
4529
4530         label $w.desc \
4531                 -text "git-gui - a graphical user interface for Git.
4532 $copyright" \
4533                 -padx 5 -pady 5 \
4534                 -justify left \
4535                 -anchor w \
4536                 -borderwidth 1 \
4537                 -relief solid \
4538                 -font font_ui
4539         pack $w.desc -side top -fill x -padx 5 -pady 5
4540
4541         set v {}
4542         append v "git-gui version $appvers\n"
4543         append v "[git version]\n"
4544         append v "\n"
4545         if {$tcl_patchLevel eq $tk_patchLevel} {
4546                 append v "Tcl/Tk version $tcl_patchLevel"
4547         } else {
4548                 append v "Tcl version $tcl_patchLevel"
4549                 append v ", Tk version $tk_patchLevel"
4550         }
4551
4552         label $w.vers \
4553                 -text $v \
4554                 -padx 5 -pady 5 \
4555                 -justify left \
4556                 -anchor w \
4557                 -borderwidth 1 \
4558                 -relief solid \
4559                 -font font_ui
4560         pack $w.vers -side top -fill x -padx 5 -pady 5
4561
4562         menu $w.ctxm -tearoff 0
4563         $w.ctxm add command \
4564                 -label {Copy} \
4565                 -font font_ui \
4566                 -command "
4567                 clipboard clear
4568                 clipboard append -format STRING -type STRING -- \[$w.vers cget -text\]
4569         "
4570
4571         bind $w <Visibility> "grab $w; focus $w.buttons.close"
4572         bind $w <Key-Escape> "destroy $w"
4573         bind $w <Key-Return> "destroy $w"
4574         bind_button3 $w.vers "tk_popup $w.ctxm %X %Y; grab $w; focus $w"
4575         wm title $w "About [appname]"
4576         tkwait window $w
4577 }
4578
4579 proc do_options {} {
4580         global repo_config global_config font_descs
4581         global repo_config_new global_config_new
4582
4583         array unset repo_config_new
4584         array unset global_config_new
4585         foreach name [array names repo_config] {
4586                 set repo_config_new($name) $repo_config($name)
4587         }
4588         load_config 1
4589         foreach name [array names repo_config] {
4590                 switch -- $name {
4591                 gui.diffcontext {continue}
4592                 }
4593                 set repo_config_new($name) $repo_config($name)
4594         }
4595         foreach name [array names global_config] {
4596                 set global_config_new($name) $global_config($name)
4597         }
4598
4599         set w .options_editor
4600         toplevel $w
4601         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
4602
4603         label $w.header -text "Options" \
4604                 -font font_uibold
4605         pack $w.header -side top -fill x
4606
4607         frame $w.buttons
4608         button $w.buttons.restore -text {Restore Defaults} \
4609                 -font font_ui \
4610                 -default normal \
4611                 -command do_restore_defaults
4612         pack $w.buttons.restore -side left
4613         button $w.buttons.save -text Save \
4614                 -font font_ui \
4615                 -default active \
4616                 -command [list do_save_config $w]
4617         pack $w.buttons.save -side right
4618         button $w.buttons.cancel -text {Cancel} \
4619                 -font font_ui \
4620                 -default normal \
4621                 -command [list destroy $w]
4622         pack $w.buttons.cancel -side right -padx 5
4623         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
4624
4625         labelframe $w.repo -text "[reponame] Repository" \
4626                 -font font_ui
4627         labelframe $w.global -text {Global (All Repositories)} \
4628                 -font font_ui
4629         pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
4630         pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
4631
4632         set optid 0
4633         foreach option {
4634                 {t user.name {User Name}}
4635                 {t user.email {Email Address}}
4636
4637                 {b merge.summary {Summarize Merge Commits}}
4638                 {i-1..5 merge.verbosity {Merge Verbosity}}
4639
4640                 {b gui.trustmtime  {Trust File Modification Timestamps}}
4641                 {i-1..99 gui.diffcontext {Number of Diff Context Lines}}
4642                 {t gui.newbranchtemplate {New Branch Name Template}}
4643                 } {
4644                 set type [lindex $option 0]
4645                 set name [lindex $option 1]
4646                 set text [lindex $option 2]
4647                 incr optid
4648                 foreach f {repo global} {
4649                         switch -glob -- $type {
4650                         b {
4651                                 checkbutton $w.$f.$optid -text $text \
4652                                         -variable ${f}_config_new($name) \
4653                                         -onvalue true \
4654                                         -offvalue false \
4655                                         -font font_ui
4656                                 pack $w.$f.$optid -side top -anchor w
4657                         }
4658                         i-* {
4659                                 regexp -- {-(\d+)\.\.(\d+)$} $type _junk min max
4660                                 frame $w.$f.$optid
4661                                 label $w.$f.$optid.l -text "$text:" -font font_ui
4662                                 pack $w.$f.$optid.l -side left -anchor w -fill x
4663                                 spinbox $w.$f.$optid.v \
4664                                         -textvariable ${f}_config_new($name) \
4665                                         -from $min \
4666                                         -to $max \
4667                                         -increment 1 \
4668                                         -width [expr {1 + [string length $max]}] \
4669                                         -font font_ui
4670                                 bind $w.$f.$optid.v <FocusIn> {%W selection range 0 end}
4671                                 pack $w.$f.$optid.v -side right -anchor e -padx 5
4672                                 pack $w.$f.$optid -side top -anchor w -fill x
4673                         }
4674                         t {
4675                                 frame $w.$f.$optid
4676                                 label $w.$f.$optid.l -text "$text:" -font font_ui
4677                                 entry $w.$f.$optid.v \
4678                                         -borderwidth 1 \
4679                                         -relief sunken \
4680                                         -width 20 \
4681                                         -textvariable ${f}_config_new($name) \
4682                                         -font font_ui
4683                                 pack $w.$f.$optid.l -side left -anchor w
4684                                 pack $w.$f.$optid.v -side left -anchor w \
4685                                         -fill x -expand 1 \
4686                                         -padx 5
4687                                 pack $w.$f.$optid -side top -anchor w -fill x
4688                         }
4689                         }
4690                 }
4691         }
4692
4693         set all_fonts [lsort [font families]]
4694         foreach option $font_descs {
4695                 set name [lindex $option 0]
4696                 set font [lindex $option 1]
4697                 set text [lindex $option 2]
4698
4699                 set global_config_new(gui.$font^^family) \
4700                         [font configure $font -family]
4701                 set global_config_new(gui.$font^^size) \
4702                         [font configure $font -size]
4703
4704                 frame $w.global.$name
4705                 label $w.global.$name.l -text "$text:" -font font_ui
4706                 pack $w.global.$name.l -side left -anchor w -fill x
4707                 set fontmenu [eval tk_optionMenu $w.global.$name.family \
4708                         global_config_new(gui.$font^^family) \
4709                         $all_fonts]
4710                 $w.global.$name.family configure -font font_ui
4711                 $fontmenu configure -font font_ui
4712                 spinbox $w.global.$name.size \
4713                         -textvariable global_config_new(gui.$font^^size) \
4714                         -from 2 -to 80 -increment 1 \
4715                         -width 3 \
4716                         -font font_ui
4717                 bind $w.global.$name.size <FocusIn> {%W selection range 0 end}
4718                 pack $w.global.$name.size -side right -anchor e
4719                 pack $w.global.$name.family -side right -anchor e
4720                 pack $w.global.$name -side top -anchor w -fill x
4721         }
4722
4723         bind $w <Visibility> "grab $w; focus $w.buttons.save"
4724         bind $w <Key-Escape> "destroy $w"
4725         bind $w <Key-Return> [list do_save_config $w]
4726         wm title $w "[appname] ([reponame]): Options"
4727         tkwait window $w
4728 }
4729
4730 proc do_restore_defaults {} {
4731         global font_descs default_config repo_config
4732         global repo_config_new global_config_new
4733
4734         foreach name [array names default_config] {
4735                 set repo_config_new($name) $default_config($name)
4736                 set global_config_new($name) $default_config($name)
4737         }
4738
4739         foreach option $font_descs {
4740                 set name [lindex $option 0]
4741                 set repo_config(gui.$name) $default_config(gui.$name)
4742         }
4743         apply_config
4744
4745         foreach option $font_descs {
4746                 set name [lindex $option 0]
4747                 set font [lindex $option 1]
4748                 set global_config_new(gui.$font^^family) \
4749                         [font configure $font -family]
4750                 set global_config_new(gui.$font^^size) \
4751                         [font configure $font -size]
4752         }
4753 }
4754
4755 proc do_save_config {w} {
4756         if {[catch {save_config} err]} {
4757                 error_popup "Failed to completely save options:\n\n$err"
4758         }
4759         reshow_diff
4760         destroy $w
4761 }
4762
4763 proc do_windows_shortcut {} {
4764         global argv0
4765
4766         set fn [tk_getSaveFile \
4767                 -parent . \
4768                 -title "[appname] ([reponame]): Create Desktop Icon" \
4769                 -initialfile "Git [reponame].bat"]
4770         if {$fn != {}} {
4771                 if {[catch {
4772                                 set fd [open $fn w]
4773                                 puts $fd "@ECHO Entering [reponame]"
4774                                 puts $fd "@ECHO Starting git-gui... please wait..."
4775                                 puts $fd "@SET PATH=[file normalize [gitexec]];%PATH%"
4776                                 puts $fd "@SET GIT_DIR=[file normalize [gitdir]]"
4777                                 puts -nonewline $fd "@\"[info nameofexecutable]\""
4778                                 puts $fd " \"[file normalize $argv0]\""
4779                                 close $fd
4780                         } err]} {
4781                         error_popup "Cannot write script:\n\n$err"
4782                 }
4783         }
4784 }
4785
4786 proc do_cygwin_shortcut {} {
4787         global argv0
4788
4789         if {[catch {
4790                 set desktop [exec cygpath \
4791                         --windows \
4792                         --absolute \
4793                         --long-name \
4794                         --desktop]
4795                 }]} {
4796                         set desktop .
4797         }
4798         set fn [tk_getSaveFile \
4799                 -parent . \
4800                 -title "[appname] ([reponame]): Create Desktop Icon" \
4801                 -initialdir $desktop \
4802                 -initialfile "Git [reponame].bat"]
4803         if {$fn != {}} {
4804                 if {[catch {
4805                                 set fd [open $fn w]
4806                                 set sh [exec cygpath \
4807                                         --windows \
4808                                         --absolute \
4809                                         /bin/sh]
4810                                 set me [exec cygpath \
4811                                         --unix \
4812                                         --absolute \
4813                                         $argv0]
4814                                 set gd [exec cygpath \
4815                                         --unix \
4816                                         --absolute \
4817                                         [gitdir]]
4818                                 set gw [exec cygpath \
4819                                         --windows \
4820                                         --absolute \
4821                                         [file dirname [gitdir]]]
4822                                 regsub -all ' $me "'\\''" me
4823                                 regsub -all ' $gd "'\\''" gd
4824                                 puts $fd "@ECHO Entering $gw"
4825                                 puts $fd "@ECHO Starting git-gui... please wait..."
4826                                 puts -nonewline $fd "@\"$sh\" --login -c \""
4827                                 puts -nonewline $fd "GIT_DIR='$gd'"
4828                                 puts -nonewline $fd " '$me'"
4829                                 puts $fd "&\""
4830                                 close $fd
4831                         } err]} {
4832                         error_popup "Cannot write script:\n\n$err"
4833                 }
4834         }
4835 }
4836
4837 proc do_macosx_app {} {
4838         global argv0 env
4839
4840         set fn [tk_getSaveFile \
4841                 -parent . \
4842                 -title "[appname] ([reponame]): Create Desktop Icon" \
4843                 -initialdir [file join $env(HOME) Desktop] \
4844                 -initialfile "Git [reponame].app"]
4845         if {$fn != {}} {
4846                 if {[catch {
4847                                 set Contents [file join $fn Contents]
4848                                 set MacOS [file join $Contents MacOS]
4849                                 set exe [file join $MacOS git-gui]
4850
4851                                 file mkdir $MacOS
4852
4853                                 set fd [open [file join $Contents Info.plist] w]
4854                                 puts $fd {<?xml version="1.0" encoding="UTF-8"?>
4855 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4856 <plist version="1.0">
4857 <dict>
4858         <key>CFBundleDevelopmentRegion</key>
4859         <string>English</string>
4860         <key>CFBundleExecutable</key>
4861         <string>git-gui</string>
4862         <key>CFBundleIdentifier</key>
4863         <string>org.spearce.git-gui</string>
4864         <key>CFBundleInfoDictionaryVersion</key>
4865         <string>6.0</string>
4866         <key>CFBundlePackageType</key>
4867         <string>APPL</string>
4868         <key>CFBundleSignature</key>
4869         <string>????</string>
4870         <key>CFBundleVersion</key>
4871         <string>1.0</string>
4872         <key>NSPrincipalClass</key>
4873         <string>NSApplication</string>
4874 </dict>
4875 </plist>}
4876                                 close $fd
4877
4878                                 set fd [open $exe w]
4879                                 set gd [file normalize [gitdir]]
4880                                 set ep [file normalize [gitexec]]
4881                                 regsub -all ' $gd "'\\''" gd
4882                                 regsub -all ' $ep "'\\''" ep
4883                                 puts $fd "#!/bin/sh"
4884                                 foreach name [array names env] {
4885                                         if {[string match GIT_* $name]} {
4886                                                 regsub -all ' $env($name) "'\\''" v
4887                                                 puts $fd "export $name='$v'"
4888                                         }
4889                                 }
4890                                 puts $fd "export PATH='$ep':\$PATH"
4891                                 puts $fd "export GIT_DIR='$gd'"
4892                                 puts $fd "exec [file normalize $argv0]"
4893                                 close $fd
4894
4895                                 file attributes $exe -permissions u+x,g+x,o+x
4896                         } err]} {
4897                         error_popup "Cannot write icon:\n\n$err"
4898                 }
4899         }
4900 }
4901
4902 proc toggle_or_diff {w x y} {
4903         global file_states file_lists current_diff_path ui_index ui_workdir
4904         global last_clicked selected_paths
4905
4906         set pos [split [$w index @$x,$y] .]
4907         set lno [lindex $pos 0]
4908         set col [lindex $pos 1]
4909         set path [lindex $file_lists($w) [expr {$lno - 1}]]
4910         if {$path eq {}} {
4911                 set last_clicked {}
4912                 return
4913         }
4914
4915         set last_clicked [list $w $lno]
4916         array unset selected_paths
4917         $ui_index tag remove in_sel 0.0 end
4918         $ui_workdir tag remove in_sel 0.0 end
4919
4920         if {$col == 0} {
4921                 if {$current_diff_path eq $path} {
4922                         set after {reshow_diff;}
4923                 } else {
4924                         set after {}
4925                 }
4926                 if {$w eq $ui_index} {
4927                         update_indexinfo \
4928                                 "Unstaging [short_path $path] from commit" \
4929                                 [list $path] \
4930                                 [concat $after {set ui_status_value {Ready.}}]
4931                 } elseif {$w eq $ui_workdir} {
4932                         update_index \
4933                                 "Adding [short_path $path]" \
4934                                 [list $path] \
4935                                 [concat $after {set ui_status_value {Ready.}}]
4936                 }
4937         } else {
4938                 show_diff $path $w $lno
4939         }
4940 }
4941
4942 proc add_one_to_selection {w x y} {
4943         global file_lists last_clicked selected_paths
4944
4945         set lno [lindex [split [$w index @$x,$y] .] 0]
4946         set path [lindex $file_lists($w) [expr {$lno - 1}]]
4947         if {$path eq {}} {
4948                 set last_clicked {}
4949                 return
4950         }
4951
4952         if {$last_clicked ne {}
4953                 && [lindex $last_clicked 0] ne $w} {
4954                 array unset selected_paths
4955                 [lindex $last_clicked 0] tag remove in_sel 0.0 end
4956         }
4957
4958         set last_clicked [list $w $lno]
4959         if {[catch {set in_sel $selected_paths($path)}]} {
4960                 set in_sel 0
4961         }
4962         if {$in_sel} {
4963                 unset selected_paths($path)
4964                 $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
4965         } else {
4966                 set selected_paths($path) 1
4967                 $w tag add in_sel $lno.0 [expr {$lno + 1}].0
4968         }
4969 }
4970
4971 proc add_range_to_selection {w x y} {
4972         global file_lists last_clicked selected_paths
4973
4974         if {[lindex $last_clicked 0] ne $w} {
4975                 toggle_or_diff $w $x $y
4976                 return
4977         }
4978
4979         set lno [lindex [split [$w index @$x,$y] .] 0]
4980         set lc [lindex $last_clicked 1]
4981         if {$lc < $lno} {
4982                 set begin $lc
4983                 set end $lno
4984         } else {
4985                 set begin $lno
4986                 set end $lc
4987         }
4988
4989         foreach path [lrange $file_lists($w) \
4990                 [expr {$begin - 1}] \
4991                 [expr {$end - 1}]] {
4992                 set selected_paths($path) 1
4993         }
4994         $w tag add in_sel $begin.0 [expr {$end + 1}].0
4995 }
4996
4997 ######################################################################
4998 ##
4999 ## config defaults
5000
5001 set cursor_ptr arrow
5002 font create font_diff -family Courier -size 10
5003 font create font_ui
5004 catch {
5005         label .dummy
5006         eval font configure font_ui [font actual [.dummy cget -font]]
5007         destroy .dummy
5008 }
5009
5010 font create font_uibold
5011 font create font_diffbold
5012
5013 if {[is_Windows]} {
5014         set M1B Control
5015         set M1T Ctrl
5016 } elseif {[is_MacOSX]} {
5017         set M1B M1
5018         set M1T Cmd
5019 } else {
5020         set M1B M1
5021         set M1T M1
5022 }
5023
5024 proc apply_config {} {
5025         global repo_config font_descs
5026
5027         foreach option $font_descs {
5028                 set name [lindex $option 0]
5029                 set font [lindex $option 1]
5030                 if {[catch {
5031                         foreach {cn cv} $repo_config(gui.$name) {
5032                                 font configure $font $cn $cv
5033                         }
5034                         } err]} {
5035                         error_popup "Invalid font specified in gui.$name:\n\n$err"
5036                 }
5037                 foreach {cn cv} [font configure $font] {
5038                         font configure ${font}bold $cn $cv
5039                 }
5040                 font configure ${font}bold -weight bold
5041         }
5042 }
5043
5044 set default_config(merge.summary) false
5045 set default_config(merge.verbosity) 2
5046 set default_config(user.name) {}
5047 set default_config(user.email) {}
5048
5049 set default_config(gui.trustmtime) false
5050 set default_config(gui.diffcontext) 5
5051 set default_config(gui.newbranchtemplate) {}
5052 set default_config(gui.fontui) [font configure font_ui]
5053 set default_config(gui.fontdiff) [font configure font_diff]
5054 set font_descs {
5055         {fontui   font_ui   {Main Font}}
5056         {fontdiff font_diff {Diff/Console Font}}
5057 }
5058 load_config 0
5059 apply_config
5060
5061 ######################################################################
5062 ##
5063 ## feature option selection
5064
5065 if {[regexp {^git-(.+)$} [appname] _junk subcommand]} {
5066         unset _junk
5067 } else {
5068         set subcommand gui
5069 }
5070 if {$subcommand eq {gui.sh}} {
5071         set subcommand gui
5072 }
5073 if {$subcommand eq {gui} && [llength $argv] > 0} {
5074         set subcommand [lindex $argv 0]
5075         set argv [lrange $argv 1 end]
5076 }
5077
5078 enable_option multicommit
5079 enable_option branch
5080 enable_option transport
5081
5082 switch -- $subcommand {
5083 browser -
5084 blame {
5085         disable_option multicommit
5086         disable_option branch
5087         disable_option transport
5088 }
5089 citool {
5090         enable_option singlecommit
5091
5092         disable_option multicommit
5093         disable_option branch
5094         disable_option transport
5095 }
5096 }
5097
5098 ######################################################################
5099 ##
5100 ## ui construction
5101
5102 set ui_comm {}
5103
5104 # -- Menu Bar
5105 #
5106 menu .mbar -tearoff 0
5107 .mbar add cascade -label Repository -menu .mbar.repository -font font_ui
5108 .mbar add cascade -label Edit -menu .mbar.edit -font font_ui
5109 if {[is_enabled branch]} {
5110         .mbar add cascade -label Branch -menu .mbar.branch -font font_ui
5111 }
5112 if {[is_enabled multicommit] || [is_enabled singlecommit]} {
5113         .mbar add cascade -label Commit -menu .mbar.commit -font font_ui
5114 }
5115 if {[is_enabled transport]} {
5116         .mbar add cascade -label Merge -menu .mbar.merge -font font_ui
5117         .mbar add cascade -label Fetch -menu .mbar.fetch -font font_ui
5118         .mbar add cascade -label Push -menu .mbar.push -font font_ui
5119 }
5120 . configure -menu .mbar
5121
5122 # -- Repository Menu
5123 #
5124 menu .mbar.repository
5125
5126 .mbar.repository add command \
5127         -label {Browse Current Branch} \
5128         -command {new_browser $current_branch} \
5129         -font font_ui
5130 trace add variable current_branch write ".mbar.repository entryconf [.mbar.repository index last] -label \"Browse \$current_branch\" ;#"
5131 .mbar.repository add separator
5132
5133 .mbar.repository add command \
5134         -label {Visualize Current Branch} \
5135         -command {do_gitk $current_branch} \
5136         -font font_ui
5137 trace add variable current_branch write ".mbar.repository entryconf [.mbar.repository index last] -label \"Visualize \$current_branch\" ;#"
5138 .mbar.repository add command \
5139         -label {Visualize All Branches} \
5140         -command {do_gitk --all} \
5141         -font font_ui
5142 .mbar.repository add separator
5143
5144 if {[is_enabled multicommit]} {
5145         .mbar.repository add command -label {Database Statistics} \
5146                 -command do_stats \
5147                 -font font_ui
5148
5149         .mbar.repository add command -label {Compress Database} \
5150                 -command do_gc \
5151                 -font font_ui
5152
5153         .mbar.repository add command -label {Verify Database} \
5154                 -command do_fsck_objects \
5155                 -font font_ui
5156
5157         .mbar.repository add separator
5158
5159         if {[is_Cygwin]} {
5160                 .mbar.repository add command \
5161                         -label {Create Desktop Icon} \
5162                         -command do_cygwin_shortcut \
5163                         -font font_ui
5164         } elseif {[is_Windows]} {
5165                 .mbar.repository add command \
5166                         -label {Create Desktop Icon} \
5167                         -command do_windows_shortcut \
5168                         -font font_ui
5169         } elseif {[is_MacOSX]} {
5170                 .mbar.repository add command \
5171                         -label {Create Desktop Icon} \
5172                         -command do_macosx_app \
5173                         -font font_ui
5174         }
5175 }
5176
5177 .mbar.repository add command -label Quit \
5178         -command do_quit \
5179         -accelerator $M1T-Q \
5180         -font font_ui
5181
5182 # -- Edit Menu
5183 #
5184 menu .mbar.edit
5185 .mbar.edit add command -label Undo \
5186         -command {catch {[focus] edit undo}} \
5187         -accelerator $M1T-Z \
5188         -font font_ui
5189 .mbar.edit add command -label Redo \
5190         -command {catch {[focus] edit redo}} \
5191         -accelerator $M1T-Y \
5192         -font font_ui
5193 .mbar.edit add separator
5194 .mbar.edit add command -label Cut \
5195         -command {catch {tk_textCut [focus]}} \
5196         -accelerator $M1T-X \
5197         -font font_ui
5198 .mbar.edit add command -label Copy \
5199         -command {catch {tk_textCopy [focus]}} \
5200         -accelerator $M1T-C \
5201         -font font_ui
5202 .mbar.edit add command -label Paste \
5203         -command {catch {tk_textPaste [focus]; [focus] see insert}} \
5204         -accelerator $M1T-V \
5205         -font font_ui
5206 .mbar.edit add command -label Delete \
5207         -command {catch {[focus] delete sel.first sel.last}} \
5208         -accelerator Del \
5209         -font font_ui
5210 .mbar.edit add separator
5211 .mbar.edit add command -label {Select All} \
5212         -command {catch {[focus] tag add sel 0.0 end}} \
5213         -accelerator $M1T-A \
5214         -font font_ui
5215
5216 # -- Branch Menu
5217 #
5218 if {[is_enabled branch]} {
5219         menu .mbar.branch
5220
5221         .mbar.branch add command -label {Create...} \
5222                 -command do_create_branch \
5223                 -accelerator $M1T-N \
5224                 -font font_ui
5225         lappend disable_on_lock [list .mbar.branch entryconf \
5226                 [.mbar.branch index last] -state]
5227
5228         .mbar.branch add command -label {Delete...} \
5229                 -command do_delete_branch \
5230                 -font font_ui
5231         lappend disable_on_lock [list .mbar.branch entryconf \
5232                 [.mbar.branch index last] -state]
5233
5234         .mbar.branch add command -label {Reset...} \
5235                 -command do_reset_hard \
5236                 -font font_ui
5237         lappend disable_on_lock [list .mbar.branch entryconf \
5238                 [.mbar.branch index last] -state]
5239 }
5240
5241 # -- Commit Menu
5242 #
5243 if {[is_enabled multicommit] || [is_enabled singlecommit]} {
5244         menu .mbar.commit
5245
5246         .mbar.commit add radiobutton \
5247                 -label {New Commit} \
5248                 -command do_select_commit_type \
5249                 -variable selected_commit_type \
5250                 -value new \
5251                 -font font_ui
5252         lappend disable_on_lock \
5253                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
5254
5255         .mbar.commit add radiobutton \
5256                 -label {Amend Last Commit} \
5257                 -command do_select_commit_type \
5258                 -variable selected_commit_type \
5259                 -value amend \
5260                 -font font_ui
5261         lappend disable_on_lock \
5262                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
5263
5264         .mbar.commit add separator
5265
5266         .mbar.commit add command -label Rescan \
5267                 -command do_rescan \
5268                 -accelerator F5 \
5269                 -font font_ui
5270         lappend disable_on_lock \
5271                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
5272
5273         .mbar.commit add command -label {Add To Commit} \
5274                 -command do_add_selection \
5275                 -font font_ui
5276         lappend disable_on_lock \
5277                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
5278
5279         .mbar.commit add command -label {Add Existing To Commit} \
5280                 -command do_add_all \
5281                 -accelerator $M1T-I \
5282                 -font font_ui
5283         lappend disable_on_lock \
5284                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
5285
5286         .mbar.commit add command -label {Unstage From Commit} \
5287                 -command do_unstage_selection \
5288                 -font font_ui
5289         lappend disable_on_lock \
5290                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
5291
5292         .mbar.commit add command -label {Revert Changes} \
5293                 -command do_revert_selection \
5294                 -font font_ui
5295         lappend disable_on_lock \
5296                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
5297
5298         .mbar.commit add separator
5299
5300         .mbar.commit add command -label {Sign Off} \
5301                 -command do_signoff \
5302                 -accelerator $M1T-S \
5303                 -font font_ui
5304
5305         .mbar.commit add command -label Commit \
5306                 -command do_commit \
5307                 -accelerator $M1T-Return \
5308                 -font font_ui
5309         lappend disable_on_lock \
5310                 [list .mbar.commit entryconf [.mbar.commit index last] -state]
5311 }
5312
5313 # -- Merge Menu
5314 #
5315 if {[is_enabled branch]} {
5316         menu .mbar.merge
5317         .mbar.merge add command -label {Local Merge...} \
5318                 -command do_local_merge \
5319                 -font font_ui
5320         lappend disable_on_lock \
5321                 [list .mbar.merge entryconf [.mbar.merge index last] -state]
5322         .mbar.merge add command -label {Abort Merge...} \
5323                 -command do_reset_hard \
5324                 -font font_ui
5325         lappend disable_on_lock \
5326                 [list .mbar.merge entryconf [.mbar.merge index last] -state]
5327
5328 }
5329
5330 # -- Transport Menu
5331 #
5332 if {[is_enabled transport]} {
5333         menu .mbar.fetch
5334
5335         menu .mbar.push
5336         .mbar.push add command -label {Push...} \
5337                 -command do_push_anywhere \
5338                 -font font_ui
5339 }
5340
5341 if {[is_MacOSX]} {
5342         # -- Apple Menu (Mac OS X only)
5343         #
5344         .mbar add cascade -label Apple -menu .mbar.apple
5345         menu .mbar.apple
5346
5347         .mbar.apple add command -label "About [appname]" \
5348                 -command do_about \
5349                 -font font_ui
5350         .mbar.apple add command -label "Options..." \
5351                 -command do_options \
5352                 -font font_ui
5353 } else {
5354         # -- Edit Menu
5355         #
5356         .mbar.edit add separator
5357         .mbar.edit add command -label {Options...} \
5358                 -command do_options \
5359                 -font font_ui
5360
5361         # -- Tools Menu
5362         #
5363         if {[file exists /usr/local/miga/lib/gui-miga]
5364                 && [file exists .pvcsrc]} {
5365         proc do_miga {} {
5366                 global ui_status_value
5367                 if {![lock_index update]} return
5368                 set cmd [list sh --login -c "/usr/local/miga/lib/gui-miga \"[pwd]\""]
5369                 set miga_fd [open "|$cmd" r]
5370                 fconfigure $miga_fd -blocking 0
5371                 fileevent $miga_fd readable [list miga_done $miga_fd]
5372                 set ui_status_value {Running miga...}
5373         }
5374         proc miga_done {fd} {
5375                 read $fd 512
5376                 if {[eof $fd]} {
5377                         close $fd
5378                         unlock_index
5379                         rescan [list set ui_status_value {Ready.}]
5380                 }
5381         }
5382         .mbar add cascade -label Tools -menu .mbar.tools
5383         menu .mbar.tools
5384         .mbar.tools add command -label "Migrate" \
5385                 -command do_miga \
5386                 -font font_ui
5387         lappend disable_on_lock \
5388                 [list .mbar.tools entryconf [.mbar.tools index last] -state]
5389         }
5390 }
5391
5392 # -- Help Menu
5393 #
5394 .mbar add cascade -label Help -menu .mbar.help -font font_ui
5395 menu .mbar.help
5396
5397 if {![is_MacOSX]} {
5398         .mbar.help add command -label "About [appname]" \
5399                 -command do_about \
5400                 -font font_ui
5401 }
5402
5403 set browser {}
5404 catch {set browser $repo_config(instaweb.browser)}
5405 set doc_path [file dirname [gitexec]]
5406 set doc_path [file join $doc_path Documentation index.html]
5407
5408 if {[is_Cygwin]} {
5409         set doc_path [exec cygpath --mixed $doc_path]
5410 }
5411
5412 if {$browser eq {}} {
5413         if {[is_MacOSX]} {
5414                 set browser open
5415         } elseif {[is_Cygwin]} {
5416                 set program_files [file dirname [exec cygpath --windir]]
5417                 set program_files [file join $program_files {Program Files}]
5418                 set firefox [file join $program_files {Mozilla Firefox} firefox.exe]
5419                 set ie [file join $program_files {Internet Explorer} IEXPLORE.EXE]
5420                 if {[file exists $firefox]} {
5421                         set browser $firefox
5422                 } elseif {[file exists $ie]} {
5423                         set browser $ie
5424                 }
5425                 unset program_files firefox ie
5426         }
5427 }
5428
5429 if {[file isfile $doc_path]} {
5430         set doc_url "file:$doc_path"
5431 } else {
5432         set doc_url {http://www.kernel.org/pub/software/scm/git/docs/}
5433 }
5434
5435 if {$browser ne {}} {
5436         .mbar.help add command -label {Online Documentation} \
5437                 -command [list exec $browser $doc_url &] \
5438                 -font font_ui
5439 }
5440 unset browser doc_path doc_url
5441
5442 # -- Standard bindings
5443 #
5444 bind .   <Destroy> do_quit
5445 bind all <$M1B-Key-q> do_quit
5446 bind all <$M1B-Key-Q> do_quit
5447 bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
5448 bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
5449
5450 # -- Not a normal commit type invocation?  Do that instead!
5451 #
5452 switch -- $subcommand {
5453 browser {
5454         if {[llength $argv] != 1} {
5455                 puts stderr "usage: $argv0 browser commit"
5456                 exit 1
5457         }
5458         set current_branch [lindex $argv 0]
5459         new_browser $current_branch
5460         return
5461 }
5462 blame {
5463         if {[llength $argv] != 2} {
5464                 puts stderr "usage: $argv0 blame commit path"
5465                 exit 1
5466         }
5467         set current_branch [lindex $argv 0]
5468         show_blame $current_branch [lindex $argv 1]
5469         return
5470 }
5471 citool -
5472 gui {
5473         if {[llength $argv] != 0} {
5474                 puts -nonewline stderr "usage: $argv0"
5475                 if {$subcommand ne {gui} && [appname] ne "git-$subcommand"} {
5476                         puts -nonewline stderr " $subcommand"
5477                 }
5478                 puts stderr {}
5479                 exit 1
5480         }
5481         # fall through to setup UI for commits
5482 }
5483 default {
5484         puts stderr "usage: $argv0 \[{blame|browser|citool}\]"
5485         exit 1
5486 }
5487 }
5488
5489 # -- Branch Control
5490 #
5491 frame .branch \
5492         -borderwidth 1 \
5493         -relief sunken
5494 label .branch.l1 \
5495         -text {Current Branch:} \
5496         -anchor w \
5497         -justify left \
5498         -font font_ui
5499 label .branch.cb \
5500         -textvariable current_branch \
5501         -anchor w \
5502         -justify left \
5503         -font font_ui
5504 pack .branch.l1 -side left
5505 pack .branch.cb -side left -fill x
5506 pack .branch -side top -fill x
5507
5508 # -- Main Window Layout
5509 #
5510 panedwindow .vpane -orient vertical
5511 panedwindow .vpane.files -orient horizontal
5512 .vpane add .vpane.files -sticky nsew -height 100 -width 200
5513 pack .vpane -anchor n -side top -fill both -expand 1
5514
5515 # -- Index File List
5516 #
5517 frame .vpane.files.index -height 100 -width 200
5518 label .vpane.files.index.title -text {Changes To Be Committed} \
5519         -background green \
5520         -font font_ui
5521 text $ui_index -background white -borderwidth 0 \
5522         -width 20 -height 10 \
5523         -wrap none \
5524         -font font_ui \
5525         -cursor $cursor_ptr \
5526         -xscrollcommand {.vpane.files.index.sx set} \
5527         -yscrollcommand {.vpane.files.index.sy set} \
5528         -state disabled
5529 scrollbar .vpane.files.index.sx -orient h -command [list $ui_index xview]
5530 scrollbar .vpane.files.index.sy -orient v -command [list $ui_index yview]
5531 pack .vpane.files.index.title -side top -fill x
5532 pack .vpane.files.index.sx -side bottom -fill x
5533 pack .vpane.files.index.sy -side right -fill y
5534 pack $ui_index -side left -fill both -expand 1
5535 .vpane.files add .vpane.files.index -sticky nsew
5536
5537 # -- Working Directory File List
5538 #
5539 frame .vpane.files.workdir -height 100 -width 200
5540 label .vpane.files.workdir.title -text {Changed But Not Updated} \
5541         -background red \
5542         -font font_ui
5543 text $ui_workdir -background white -borderwidth 0 \
5544         -width 20 -height 10 \
5545         -wrap none \
5546         -font font_ui \
5547         -cursor $cursor_ptr \
5548         -xscrollcommand {.vpane.files.workdir.sx set} \
5549         -yscrollcommand {.vpane.files.workdir.sy set} \
5550         -state disabled
5551 scrollbar .vpane.files.workdir.sx -orient h -command [list $ui_workdir xview]
5552 scrollbar .vpane.files.workdir.sy -orient v -command [list $ui_workdir yview]
5553 pack .vpane.files.workdir.title -side top -fill x
5554 pack .vpane.files.workdir.sx -side bottom -fill x
5555 pack .vpane.files.workdir.sy -side right -fill y
5556 pack $ui_workdir -side left -fill both -expand 1
5557 .vpane.files add .vpane.files.workdir -sticky nsew
5558
5559 foreach i [list $ui_index $ui_workdir] {
5560         $i tag conf in_diff -font font_uibold
5561         $i tag conf in_sel \
5562                 -background [$i cget -foreground] \
5563                 -foreground [$i cget -background]
5564 }
5565 unset i
5566
5567 # -- Diff and Commit Area
5568 #
5569 frame .vpane.lower -height 300 -width 400
5570 frame .vpane.lower.commarea
5571 frame .vpane.lower.diff -relief sunken -borderwidth 1
5572 pack .vpane.lower.commarea -side top -fill x
5573 pack .vpane.lower.diff -side bottom -fill both -expand 1
5574 .vpane add .vpane.lower -sticky nsew
5575
5576 # -- Commit Area Buttons
5577 #
5578 frame .vpane.lower.commarea.buttons
5579 label .vpane.lower.commarea.buttons.l -text {} \
5580         -anchor w \
5581         -justify left \
5582         -font font_ui
5583 pack .vpane.lower.commarea.buttons.l -side top -fill x
5584 pack .vpane.lower.commarea.buttons -side left -fill y
5585
5586 button .vpane.lower.commarea.buttons.rescan -text {Rescan} \
5587         -command do_rescan \
5588         -font font_ui
5589 pack .vpane.lower.commarea.buttons.rescan -side top -fill x
5590 lappend disable_on_lock \
5591         {.vpane.lower.commarea.buttons.rescan conf -state}
5592
5593 button .vpane.lower.commarea.buttons.incall -text {Add Existing} \
5594         -command do_add_all \
5595         -font font_ui
5596 pack .vpane.lower.commarea.buttons.incall -side top -fill x
5597 lappend disable_on_lock \
5598         {.vpane.lower.commarea.buttons.incall conf -state}
5599
5600 button .vpane.lower.commarea.buttons.signoff -text {Sign Off} \
5601         -command do_signoff \
5602         -font font_ui
5603 pack .vpane.lower.commarea.buttons.signoff -side top -fill x
5604
5605 button .vpane.lower.commarea.buttons.commit -text {Commit} \
5606         -command do_commit \
5607         -font font_ui
5608 pack .vpane.lower.commarea.buttons.commit -side top -fill x
5609 lappend disable_on_lock \
5610         {.vpane.lower.commarea.buttons.commit conf -state}
5611
5612 # -- Commit Message Buffer
5613 #
5614 frame .vpane.lower.commarea.buffer
5615 frame .vpane.lower.commarea.buffer.header
5616 set ui_comm .vpane.lower.commarea.buffer.t
5617 set ui_coml .vpane.lower.commarea.buffer.header.l
5618 radiobutton .vpane.lower.commarea.buffer.header.new \
5619         -text {New Commit} \
5620         -command do_select_commit_type \
5621         -variable selected_commit_type \
5622         -value new \
5623         -font font_ui
5624 lappend disable_on_lock \
5625         [list .vpane.lower.commarea.buffer.header.new conf -state]
5626 radiobutton .vpane.lower.commarea.buffer.header.amend \
5627         -text {Amend Last Commit} \
5628         -command do_select_commit_type \
5629         -variable selected_commit_type \
5630         -value amend \
5631         -font font_ui
5632 lappend disable_on_lock \
5633         [list .vpane.lower.commarea.buffer.header.amend conf -state]
5634 label $ui_coml \
5635         -anchor w \
5636         -justify left \
5637         -font font_ui
5638 proc trace_commit_type {varname args} {
5639         global ui_coml commit_type
5640         switch -glob -- $commit_type {
5641         initial       {set txt {Initial Commit Message:}}
5642         amend         {set txt {Amended Commit Message:}}
5643         amend-initial {set txt {Amended Initial Commit Message:}}
5644         amend-merge   {set txt {Amended Merge Commit Message:}}
5645         merge         {set txt {Merge Commit Message:}}
5646         *             {set txt {Commit Message:}}
5647         }
5648         $ui_coml conf -text $txt
5649 }
5650 trace add variable commit_type write trace_commit_type
5651 pack $ui_coml -side left -fill x
5652 pack .vpane.lower.commarea.buffer.header.amend -side right
5653 pack .vpane.lower.commarea.buffer.header.new -side right
5654
5655 text $ui_comm -background white -borderwidth 1 \
5656         -undo true \
5657         -maxundo 20 \
5658         -autoseparators true \
5659         -relief sunken \
5660         -width 75 -height 9 -wrap none \
5661         -font font_diff \
5662         -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
5663 scrollbar .vpane.lower.commarea.buffer.sby \
5664         -command [list $ui_comm yview]
5665 pack .vpane.lower.commarea.buffer.header -side top -fill x
5666 pack .vpane.lower.commarea.buffer.sby -side right -fill y
5667 pack $ui_comm -side left -fill y
5668 pack .vpane.lower.commarea.buffer -side left -fill y
5669
5670 # -- Commit Message Buffer Context Menu
5671 #
5672 set ctxm .vpane.lower.commarea.buffer.ctxm
5673 menu $ctxm -tearoff 0
5674 $ctxm add command \
5675         -label {Cut} \
5676         -font font_ui \
5677         -command {tk_textCut $ui_comm}
5678 $ctxm add command \
5679         -label {Copy} \
5680         -font font_ui \
5681         -command {tk_textCopy $ui_comm}
5682 $ctxm add command \
5683         -label {Paste} \
5684         -font font_ui \
5685         -command {tk_textPaste $ui_comm}
5686 $ctxm add command \
5687         -label {Delete} \
5688         -font font_ui \
5689         -command {$ui_comm delete sel.first sel.last}
5690 $ctxm add separator
5691 $ctxm add command \
5692         -label {Select All} \
5693         -font font_ui \
5694         -command {focus $ui_comm;$ui_comm tag add sel 0.0 end}
5695 $ctxm add command \
5696         -label {Copy All} \
5697         -font font_ui \
5698         -command {
5699                 $ui_comm tag add sel 0.0 end
5700                 tk_textCopy $ui_comm
5701                 $ui_comm tag remove sel 0.0 end
5702         }
5703 $ctxm add separator
5704 $ctxm add command \
5705         -label {Sign Off} \
5706         -font font_ui \
5707         -command do_signoff
5708 bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
5709
5710 # -- Diff Header
5711 #
5712 proc trace_current_diff_path {varname args} {
5713         global current_diff_path diff_actions file_states
5714         if {$current_diff_path eq {}} {
5715                 set s {}
5716                 set f {}
5717                 set p {}
5718                 set o disabled
5719         } else {
5720                 set p $current_diff_path
5721                 set s [mapdesc [lindex $file_states($p) 0] $p]
5722                 set f {File:}
5723                 set p [escape_path $p]
5724                 set o normal
5725         }
5726
5727         .vpane.lower.diff.header.status configure -text $s
5728         .vpane.lower.diff.header.file configure -text $f
5729         .vpane.lower.diff.header.path configure -text $p
5730         foreach w $diff_actions {
5731                 uplevel #0 $w $o
5732         }
5733 }
5734 trace add variable current_diff_path write trace_current_diff_path
5735
5736 frame .vpane.lower.diff.header -background orange
5737 label .vpane.lower.diff.header.status \
5738         -background orange \
5739         -width $max_status_desc \
5740         -anchor w \
5741         -justify left \
5742         -font font_ui
5743 label .vpane.lower.diff.header.file \
5744         -background orange \
5745         -anchor w \
5746         -justify left \
5747         -font font_ui
5748 label .vpane.lower.diff.header.path \
5749         -background orange \
5750         -anchor w \
5751         -justify left \
5752         -font font_ui
5753 pack .vpane.lower.diff.header.status -side left
5754 pack .vpane.lower.diff.header.file -side left
5755 pack .vpane.lower.diff.header.path -fill x
5756 set ctxm .vpane.lower.diff.header.ctxm
5757 menu $ctxm -tearoff 0
5758 $ctxm add command \
5759         -label {Copy} \
5760         -font font_ui \
5761         -command {
5762                 clipboard clear
5763                 clipboard append \
5764                         -format STRING \
5765                         -type STRING \
5766                         -- $current_diff_path
5767         }
5768 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5769 bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
5770
5771 # -- Diff Body
5772 #
5773 frame .vpane.lower.diff.body
5774 set ui_diff .vpane.lower.diff.body.t
5775 text $ui_diff -background white -borderwidth 0 \
5776         -width 80 -height 15 -wrap none \
5777         -font font_diff \
5778         -xscrollcommand {.vpane.lower.diff.body.sbx set} \
5779         -yscrollcommand {.vpane.lower.diff.body.sby set} \
5780         -state disabled
5781 scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
5782         -command [list $ui_diff xview]
5783 scrollbar .vpane.lower.diff.body.sby -orient vertical \
5784         -command [list $ui_diff yview]
5785 pack .vpane.lower.diff.body.sbx -side bottom -fill x
5786 pack .vpane.lower.diff.body.sby -side right -fill y
5787 pack $ui_diff -side left -fill both -expand 1
5788 pack .vpane.lower.diff.header -side top -fill x
5789 pack .vpane.lower.diff.body -side bottom -fill both -expand 1
5790
5791 $ui_diff tag conf d_cr -elide true
5792 $ui_diff tag conf d_@ -foreground blue -font font_diffbold
5793 $ui_diff tag conf d_+ -foreground {#00a000}
5794 $ui_diff tag conf d_- -foreground red
5795
5796 $ui_diff tag conf d_++ -foreground {#00a000}
5797 $ui_diff tag conf d_-- -foreground red
5798 $ui_diff tag conf d_+s \
5799         -foreground {#00a000} \
5800         -background {#e2effa}
5801 $ui_diff tag conf d_-s \
5802         -foreground red \
5803         -background {#e2effa}
5804 $ui_diff tag conf d_s+ \
5805         -foreground {#00a000} \
5806         -background ivory1
5807 $ui_diff tag conf d_s- \
5808         -foreground red \
5809         -background ivory1
5810
5811 $ui_diff tag conf d<<<<<<< \
5812         -foreground orange \
5813         -font font_diffbold
5814 $ui_diff tag conf d======= \
5815         -foreground orange \
5816         -font font_diffbold
5817 $ui_diff tag conf d>>>>>>> \
5818         -foreground orange \
5819         -font font_diffbold
5820
5821 $ui_diff tag raise sel
5822
5823 # -- Diff Body Context Menu
5824 #
5825 set ctxm .vpane.lower.diff.body.ctxm
5826 menu $ctxm -tearoff 0
5827 $ctxm add command \
5828         -label {Refresh} \
5829         -font font_ui \
5830         -command reshow_diff
5831 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5832 $ctxm add command \
5833         -label {Copy} \
5834         -font font_ui \
5835         -command {tk_textCopy $ui_diff}
5836 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5837 $ctxm add command \
5838         -label {Select All} \
5839         -font font_ui \
5840         -command {focus $ui_diff;$ui_diff tag add sel 0.0 end}
5841 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5842 $ctxm add command \
5843         -label {Copy All} \
5844         -font font_ui \
5845         -command {
5846                 $ui_diff tag add sel 0.0 end
5847                 tk_textCopy $ui_diff
5848                 $ui_diff tag remove sel 0.0 end
5849         }
5850 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5851 $ctxm add separator
5852 $ctxm add command \
5853         -label {Apply/Reverse Hunk} \
5854         -font font_ui \
5855         -command {apply_hunk $cursorX $cursorY}
5856 set ui_diff_applyhunk [$ctxm index last]
5857 lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state]
5858 $ctxm add separator
5859 $ctxm add command \
5860         -label {Decrease Font Size} \
5861         -font font_ui \
5862         -command {incr_font_size font_diff -1}
5863 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5864 $ctxm add command \
5865         -label {Increase Font Size} \
5866         -font font_ui \
5867         -command {incr_font_size font_diff 1}
5868 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5869 $ctxm add separator
5870 $ctxm add command \
5871         -label {Show Less Context} \
5872         -font font_ui \
5873         -command {if {$repo_config(gui.diffcontext) >= 2} {
5874                 incr repo_config(gui.diffcontext) -1
5875                 reshow_diff
5876         }}
5877 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5878 $ctxm add command \
5879         -label {Show More Context} \
5880         -font font_ui \
5881         -command {
5882                 incr repo_config(gui.diffcontext)
5883                 reshow_diff
5884         }
5885 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5886 $ctxm add separator
5887 $ctxm add command -label {Options...} \
5888         -font font_ui \
5889         -command do_options
5890 bind_button3 $ui_diff "
5891         set cursorX %x
5892         set cursorY %y
5893         if {\$ui_index eq \$current_diff_side} {
5894                 $ctxm entryconf $ui_diff_applyhunk -label {Unstage Hunk From Commit}
5895         } else {
5896                 $ctxm entryconf $ui_diff_applyhunk -label {Stage Hunk For Commit}
5897         }
5898         tk_popup $ctxm %X %Y
5899 "
5900 unset ui_diff_applyhunk
5901
5902 # -- Status Bar
5903 #
5904 label .status -textvariable ui_status_value \
5905         -anchor w \
5906         -justify left \
5907         -borderwidth 1 \
5908         -relief sunken \
5909         -font font_ui
5910 pack .status -anchor w -side bottom -fill x
5911
5912 # -- Load geometry
5913 #
5914 catch {
5915 set gm $repo_config(gui.geometry)
5916 wm geometry . [lindex $gm 0]
5917 .vpane sash place 0 \
5918         [lindex [.vpane sash coord 0] 0] \
5919         [lindex $gm 1]
5920 .vpane.files sash place 0 \
5921         [lindex $gm 2] \
5922         [lindex [.vpane.files sash coord 0] 1]
5923 unset gm
5924 }
5925
5926 # -- Key Bindings
5927 #
5928 bind $ui_comm <$M1B-Key-Return> {do_commit;break}
5929 bind $ui_comm <$M1B-Key-i> {do_add_all;break}
5930 bind $ui_comm <$M1B-Key-I> {do_add_all;break}
5931 bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
5932 bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
5933 bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
5934 bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
5935 bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
5936 bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
5937 bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
5938 bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
5939
5940 bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
5941 bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
5942 bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
5943 bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
5944 bind $ui_diff <$M1B-Key-v> {break}
5945 bind $ui_diff <$M1B-Key-V> {break}
5946 bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
5947 bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
5948 bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
5949 bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
5950 bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
5951 bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
5952 bind $ui_diff <Button-1>   {focus %W}
5953
5954 if {[is_enabled branch]} {
5955         bind . <$M1B-Key-n> do_create_branch
5956         bind . <$M1B-Key-N> do_create_branch
5957 }
5958
5959 bind all <Key-F5> do_rescan
5960 bind all <$M1B-Key-r> do_rescan
5961 bind all <$M1B-Key-R> do_rescan
5962 bind .   <$M1B-Key-s> do_signoff
5963 bind .   <$M1B-Key-S> do_signoff
5964 bind .   <$M1B-Key-i> do_add_all
5965 bind .   <$M1B-Key-I> do_add_all
5966 bind .   <$M1B-Key-Return> do_commit
5967 foreach i [list $ui_index $ui_workdir] {
5968         bind $i <Button-1>       "toggle_or_diff         $i %x %y; break"
5969         bind $i <$M1B-Button-1>  "add_one_to_selection   $i %x %y; break"
5970         bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
5971 }
5972 unset i
5973
5974 set file_lists($ui_index) [list]
5975 set file_lists($ui_workdir) [list]
5976
5977 wm title . "[appname] ([reponame]) [file normalize [file dirname [gitdir]]]"
5978 focus -force $ui_comm
5979
5980 # -- Warn the user about environmental problems.  Cygwin's Tcl
5981 #    does *not* pass its env array onto any processes it spawns.
5982 #    This means that git processes get none of our environment.
5983 #
5984 if {[is_Cygwin]} {
5985         set ignored_env 0
5986         set suggest_user {}
5987         set msg "Possible environment issues exist.
5988
5989 The following environment variables are probably
5990 going to be ignored by any Git subprocess run
5991 by [appname]:
5992
5993 "
5994         foreach name [array names env] {
5995                 switch -regexp -- $name {
5996                 {^GIT_INDEX_FILE$} -
5997                 {^GIT_OBJECT_DIRECTORY$} -
5998                 {^GIT_ALTERNATE_OBJECT_DIRECTORIES$} -
5999                 {^GIT_DIFF_OPTS$} -
6000                 {^GIT_EXTERNAL_DIFF$} -
6001                 {^GIT_PAGER$} -
6002                 {^GIT_TRACE$} -
6003                 {^GIT_CONFIG$} -
6004                 {^GIT_CONFIG_LOCAL$} -
6005                 {^GIT_(AUTHOR|COMMITTER)_DATE$} {
6006                         append msg " - $name\n"
6007                         incr ignored_env
6008                 }
6009                 {^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$} {
6010                         append msg " - $name\n"
6011                         incr ignored_env
6012                         set suggest_user $name
6013                 }
6014                 }
6015         }
6016         if {$ignored_env > 0} {
6017                 append msg "
6018 This is due to a known issue with the
6019 Tcl binary distributed by Cygwin."
6020
6021                 if {$suggest_user ne {}} {
6022                         append msg "
6023
6024 A good replacement for $suggest_user
6025 is placing values for the user.name and
6026 user.email settings into your personal
6027 ~/.gitconfig file.
6028 "
6029                 }
6030                 warn_popup $msg
6031         }
6032         unset ignored_env msg suggest_user name
6033 }
6034
6035 # -- Only initialize complex UI if we are going to stay running.
6036 #
6037 if {[is_enabled transport]} {
6038         load_all_remotes
6039         load_all_heads
6040
6041         populate_branch_menu
6042         populate_fetch_menu
6043         populate_push_menu
6044 }
6045
6046 # -- Only suggest a gc run if we are going to stay running.
6047 #
6048 if {[is_enabled multicommit]} {
6049         set object_limit 2000
6050         if {[is_Windows]} {set object_limit 200}
6051         regexp {^([0-9]+) objects,} [git count-objects] _junk objects_current
6052         if {$objects_current >= $object_limit} {
6053                 if {[ask_popup \
6054                         "This repository currently has $objects_current loose objects.
6055
6056 To maintain optimal performance it is strongly recommended that you compress the database when more than $object_limit loose objects exist.
6057
6058 Compress the database now?"] eq yes} {
6059                         do_gc
6060                 }
6061         }
6062         unset object_limit _junk objects_current
6063 }
6064
6065 lock_index begin-read
6066 after 1 do_rescan