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