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