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