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