212a093118e42d3e31f0e5c25df2eb5655b88a47
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 appvers 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 {}
2538 append v "$appname version $appvers\n\n"
2539 append v "[exec git --version]\n\n"
2540 if {$tcl_patchLevel eq $tk_patchLevel} {
2541 append v "Tcl/Tk version $tcl_patchLevel"
2542 } else {
2543 append v "Tcl version $tcl_patchLevel"
2544 append v ", Tk version $tk_patchLevel"
2545 }
2547 label $w.vers \
2548 -text $v \
2549 -padx 5 -pady 5 \
2550 -justify left \
2551 -anchor w \
2552 -borderwidth 1 \
2553 -relief solid \
2554 -font font_ui
2555 pack $w.vers -side top -fill x -padx 5 -pady 5
2557 bind $w <Visibility> "grab $w; focus $w"
2558 bind $w <Key-Escape> "destroy $w"
2559 wm title $w "About $appname"
2560 tkwait window $w
2561 }
2563 proc do_options {} {
2564 global appname gitdir font_descs
2565 global repo_config global_config
2566 global repo_config_new global_config_new
2568 array unset repo_config_new
2569 array unset global_config_new
2570 foreach name [array names repo_config] {
2571 set repo_config_new($name) $repo_config($name)
2572 }
2573 load_config 1
2574 foreach name [array names repo_config] {
2575 switch -- $name {
2576 gui.diffcontext {continue}
2577 }
2578 set repo_config_new($name) $repo_config($name)
2579 }
2580 foreach name [array names global_config] {
2581 set global_config_new($name) $global_config($name)
2582 }
2583 set reponame [lindex [file split \
2584 [file normalize [file dirname $gitdir]]] \
2585 end]
2587 set w .options_editor
2588 toplevel $w
2589 wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2591 label $w.header -text "$appname Options" \
2592 -font font_uibold
2593 pack $w.header -side top -fill x
2595 frame $w.buttons
2596 button $w.buttons.restore -text {Restore Defaults} \
2597 -font font_ui \
2598 -command do_restore_defaults
2599 pack $w.buttons.restore -side left
2600 button $w.buttons.save -text Save \
2601 -font font_ui \
2602 -command [list do_save_config $w]
2603 pack $w.buttons.save -side right
2604 button $w.buttons.cancel -text {Cancel} \
2605 -font font_ui \
2606 -command [list destroy $w]
2607 pack $w.buttons.cancel -side right
2608 pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2610 labelframe $w.repo -text "$reponame Repository" \
2611 -font font_ui \
2612 -relief raised -borderwidth 2
2613 labelframe $w.global -text {Global (All Repositories)} \
2614 -font font_ui \
2615 -relief raised -borderwidth 2
2616 pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
2617 pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
2619 foreach option {
2620 {b partialinclude {Allow Partially Added Files}}
2621 {b pullsummary {Show Pull Summary}}
2622 {b trustmtime {Trust File Modification Timestamps}}
2623 {i diffcontext {Number of Diff Context Lines}}
2624 } {
2625 set type [lindex $option 0]
2626 set name [lindex $option 1]
2627 set text [lindex $option 2]
2628 foreach f {repo global} {
2629 switch $type {
2630 b {
2631 checkbutton $w.$f.$name -text $text \
2632 -variable ${f}_config_new(gui.$name) \
2633 -onvalue true \
2634 -offvalue false \
2635 -font font_ui
2636 pack $w.$f.$name -side top -anchor w
2637 }
2638 i {
2639 frame $w.$f.$name
2640 label $w.$f.$name.l -text "$text:" -font font_ui
2641 pack $w.$f.$name.l -side left -anchor w -fill x
2642 spinbox $w.$f.$name.v \
2643 -textvariable ${f}_config_new(gui.$name) \
2644 -from 1 -to 99 -increment 1 \
2645 -width 3 \
2646 -font font_ui
2647 pack $w.$f.$name.v -side right -anchor e
2648 pack $w.$f.$name -side top -anchor w -fill x
2649 }
2650 }
2651 }
2652 }
2654 set all_fonts [lsort [font families]]
2655 foreach option $font_descs {
2656 set name [lindex $option 0]
2657 set font [lindex $option 1]
2658 set text [lindex $option 2]
2660 set global_config_new(gui.$font^^family) \
2661 [font configure $font -family]
2662 set global_config_new(gui.$font^^size) \
2663 [font configure $font -size]
2665 frame $w.global.$name
2666 label $w.global.$name.l -text "$text:" -font font_ui
2667 pack $w.global.$name.l -side left -anchor w -fill x
2668 eval tk_optionMenu $w.global.$name.family \
2669 global_config_new(gui.$font^^family) \
2670 $all_fonts
2671 spinbox $w.global.$name.size \
2672 -textvariable global_config_new(gui.$font^^size) \
2673 -from 2 -to 80 -increment 1 \
2674 -width 3 \
2675 -font font_ui
2676 pack $w.global.$name.size -side right -anchor e
2677 pack $w.global.$name.family -side right -anchor e
2678 pack $w.global.$name -side top -anchor w -fill x
2679 }
2681 bind $w <Visibility> "grab $w; focus $w"
2682 bind $w <Key-Escape> "destroy $w"
2683 wm title $w "$appname ($reponame): Options"
2684 tkwait window $w
2685 }
2687 proc do_restore_defaults {} {
2688 global font_descs default_config repo_config
2689 global repo_config_new global_config_new
2691 foreach name [array names default_config] {
2692 set repo_config_new($name) $default_config($name)
2693 set global_config_new($name) $default_config($name)
2694 }
2696 foreach option $font_descs {
2697 set name [lindex $option 0]
2698 set repo_config(gui.$name) $default_config(gui.$name)
2699 }
2700 apply_config
2702 foreach option $font_descs {
2703 set name [lindex $option 0]
2704 set font [lindex $option 1]
2705 set global_config_new(gui.$font^^family) \
2706 [font configure $font -family]
2707 set global_config_new(gui.$font^^size) \
2708 [font configure $font -size]
2709 }
2710 }
2712 proc do_save_config {w} {
2713 if {[catch {save_config} err]} {
2714 error_popup "Failed to completely save options:\n\n$err"
2715 }
2716 reshow_diff
2717 destroy $w
2718 }
2720 proc do_windows_shortcut {} {
2721 global gitdir appname argv0
2723 set reponame [lindex [file split \
2724 [file normalize [file dirname $gitdir]]] \
2725 end]
2727 if {[catch {
2728 set desktop [exec cygpath \
2729 --windows \
2730 --absolute \
2731 --long-name \
2732 --desktop]
2733 }]} {
2734 set desktop .
2735 }
2736 set fn [tk_getSaveFile \
2737 -parent . \
2738 -title "$appname ($reponame): Create Desktop Icon" \
2739 -initialdir $desktop \
2740 -initialfile "Git $reponame.bat"]
2741 if {$fn != {}} {
2742 if {[catch {
2743 set fd [open $fn w]
2744 set sh [exec cygpath \
2745 --windows \
2746 --absolute \
2747 /bin/sh]
2748 set me [exec cygpath \
2749 --unix \
2750 --absolute \
2751 $argv0]
2752 set gd [exec cygpath \
2753 --unix \
2754 --absolute \
2755 $gitdir]
2756 regsub -all ' $me "'\\''" me
2757 regsub -all ' $gd "'\\''" gd
2758 puts $fd "@ECHO Starting git-gui... Please wait..."
2759 puts -nonewline $fd "@\"$sh\" --login -c \""
2760 puts -nonewline $fd "GIT_DIR='$gd'"
2761 puts -nonewline $fd " '$me'"
2762 puts $fd "&\""
2763 close $fd
2764 } err]} {
2765 error_popup "Cannot write script:\n\n$err"
2766 }
2767 }
2768 }
2770 proc do_macosx_app {} {
2771 global gitdir appname argv0 env
2773 set reponame [lindex [file split \
2774 [file normalize [file dirname $gitdir]]] \
2775 end]
2777 set fn [tk_getSaveFile \
2778 -parent . \
2779 -title "$appname ($reponame): Create Desktop Icon" \
2780 -initialdir [file join $env(HOME) Desktop] \
2781 -initialfile "Git $reponame.app"]
2782 if {$fn != {}} {
2783 if {[catch {
2784 set Contents [file join $fn Contents]
2785 set MacOS [file join $Contents MacOS]
2786 set exe [file join $MacOS git-gui]
2788 file mkdir $MacOS
2790 set fd [open [file join $Contents Info.plist] w]
2791 puts $fd {<?xml version="1.0" encoding="UTF-8"?>
2792 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2793 <plist version="1.0">
2794 <dict>
2795 <key>CFBundleDevelopmentRegion</key>
2796 <string>English</string>
2797 <key>CFBundleExecutable</key>
2798 <string>git-gui</string>
2799 <key>CFBundleIdentifier</key>
2800 <string>org.spearce.git-gui</string>
2801 <key>CFBundleInfoDictionaryVersion</key>
2802 <string>6.0</string>
2803 <key>CFBundlePackageType</key>
2804 <string>APPL</string>
2805 <key>CFBundleSignature</key>
2806 <string>????</string>
2807 <key>CFBundleVersion</key>
2808 <string>1.0</string>
2809 <key>NSPrincipalClass</key>
2810 <string>NSApplication</string>
2811 </dict>
2812 </plist>}
2813 close $fd
2815 set fd [open $exe w]
2816 set gd [file normalize $gitdir]
2817 set ep [file normalize [exec git --exec-path]]
2818 regsub -all ' $gd "'\\''" gd
2819 regsub -all ' $ep "'\\''" ep
2820 puts $fd "#!/bin/sh"
2821 foreach name [array names env] {
2822 if {[string match GIT_* $name]} {
2823 regsub -all ' $env($name) "'\\''" v
2824 puts $fd "export $name='$v'"
2825 }
2826 }
2827 puts $fd "export PATH='$ep':\$PATH"
2828 puts $fd "export GIT_DIR='$gd'"
2829 puts $fd "exec [file normalize $argv0]"
2830 close $fd
2832 file attributes $exe -permissions u+x,g+x,o+x
2833 } err]} {
2834 error_popup "Cannot write icon:\n\n$err"
2835 }
2836 }
2837 }
2839 proc toggle_or_diff {w x y} {
2840 global file_states file_lists current_diff ui_index ui_other
2841 global last_clicked selected_paths
2843 set pos [split [$w index @$x,$y] .]
2844 set lno [lindex $pos 0]
2845 set col [lindex $pos 1]
2846 set path [lindex $file_lists($w) [expr {$lno - 1}]]
2847 if {$path eq {}} {
2848 set last_clicked {}
2849 return
2850 }
2852 set last_clicked [list $w $lno]
2853 array unset selected_paths
2854 $ui_index tag remove in_sel 0.0 end
2855 $ui_other tag remove in_sel 0.0 end
2857 if {$col == 0} {
2858 if {$current_diff eq $path} {
2859 set after {reshow_diff;}
2860 } else {
2861 set after {}
2862 }
2863 switch -glob -- [lindex $file_states($path) 0] {
2864 A_ -
2865 M_ -
2866 DD -
2867 DO -
2868 DM {
2869 update_indexinfo \
2870 "Removing [short_path $path] from commit" \
2871 [list $path] \
2872 [concat $after {set ui_status_value {Ready.}}]
2873 }
2874 ?? {
2875 update_index \
2876 "Adding [short_path $path]" \
2877 [list $path] \
2878 [concat $after {set ui_status_value {Ready.}}]
2879 }
2880 }
2881 } else {
2882 show_diff $path $w $lno
2883 }
2884 }
2886 proc add_one_to_selection {w x y} {
2887 global file_lists
2888 global last_clicked selected_paths
2890 set pos [split [$w index @$x,$y] .]
2891 set lno [lindex $pos 0]
2892 set col [lindex $pos 1]
2893 set path [lindex $file_lists($w) [expr {$lno - 1}]]
2894 if {$path eq {}} {
2895 set last_clicked {}
2896 return
2897 }
2899 set last_clicked [list $w $lno]
2900 if {[catch {set in_sel $selected_paths($path)}]} {
2901 set in_sel 0
2902 }
2903 if {$in_sel} {
2904 unset selected_paths($path)
2905 $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
2906 } else {
2907 set selected_paths($path) 1
2908 $w tag add in_sel $lno.0 [expr {$lno + 1}].0
2909 }
2910 }
2912 proc add_range_to_selection {w x y} {
2913 global file_lists
2914 global last_clicked selected_paths
2916 if {[lindex $last_clicked 0] ne $w} {
2917 toggle_or_diff $w $x $y
2918 return
2919 }
2921 set pos [split [$w index @$x,$y] .]
2922 set lno [lindex $pos 0]
2923 set lc [lindex $last_clicked 1]
2924 if {$lc < $lno} {
2925 set begin $lc
2926 set end $lno
2927 } else {
2928 set begin $lno
2929 set end $lc
2930 }
2932 foreach path [lrange $file_lists($w) \
2933 [expr {$begin - 1}] \
2934 [expr {$end - 1}]] {
2935 set selected_paths($path) 1
2936 }
2937 $w tag add in_sel $begin.0 [expr {$end + 1}].0
2938 }
2940 ######################################################################
2941 ##
2942 ## config defaults
2944 set cursor_ptr arrow
2945 font create font_diff -family Courier -size 10
2946 font create font_ui
2947 catch {
2948 label .dummy
2949 eval font configure font_ui [font actual [.dummy cget -font]]
2950 destroy .dummy
2951 }
2953 font create font_uibold
2954 font create font_diffbold
2956 if {[is_Windows]} {
2957 set M1B Control
2958 set M1T Ctrl
2959 } elseif {[is_MacOSX]} {
2960 set M1B M1
2961 set M1T Cmd
2962 } else {
2963 set M1B M1
2964 set M1T M1
2965 }
2967 proc apply_config {} {
2968 global repo_config font_descs
2970 foreach option $font_descs {
2971 set name [lindex $option 0]
2972 set font [lindex $option 1]
2973 if {[catch {
2974 foreach {cn cv} $repo_config(gui.$name) {
2975 font configure $font $cn $cv
2976 }
2977 } err]} {
2978 error_popup "Invalid font specified in gui.$name:\n\n$err"
2979 }
2980 foreach {cn cv} [font configure $font] {
2981 font configure ${font}bold $cn $cv
2982 }
2983 font configure ${font}bold -weight bold
2984 }
2985 }
2987 set default_config(gui.trustmtime) false
2988 set default_config(gui.pullsummary) true
2989 set default_config(gui.partialinclude) false
2990 set default_config(gui.diffcontext) 5
2991 set default_config(gui.fontui) [font configure font_ui]
2992 set default_config(gui.fontdiff) [font configure font_diff]
2993 set font_descs {
2994 {fontui font_ui {Main Font}}
2995 {fontdiff font_diff {Diff/Console Font}}
2996 }
2997 load_config 0
2998 apply_config
3000 ######################################################################
3001 ##
3002 ## ui construction
3004 # -- Menu Bar
3005 #
3006 menu .mbar -tearoff 0
3007 .mbar add cascade -label Repository -menu .mbar.repository
3008 .mbar add cascade -label Edit -menu .mbar.edit
3009 if {!$single_commit} {
3010 .mbar add cascade -label Branch -menu .mbar.branch
3011 }
3012 .mbar add cascade -label Commit -menu .mbar.commit
3013 if {!$single_commit} {
3014 .mbar add cascade -label Fetch -menu .mbar.fetch
3015 .mbar add cascade -label Pull -menu .mbar.pull
3016 .mbar add cascade -label Push -menu .mbar.push
3017 }
3018 . configure -menu .mbar
3020 # -- Repository Menu
3021 #
3022 menu .mbar.repository
3023 .mbar.repository add command \
3024 -label {Visualize Current Branch} \
3025 -command {do_gitk {}} \
3026 -font font_ui
3027 if {![is_MacOSX]} {
3028 .mbar.repository add command \
3029 -label {Visualize All Branches} \
3030 -command {do_gitk {--all}} \
3031 -font font_ui
3032 }
3033 .mbar.repository add separator
3035 if {!$single_commit} {
3036 .mbar.repository add command -label {Compress Database} \
3037 -command do_gc \
3038 -font font_ui
3040 .mbar.repository add command -label {Verify Database} \
3041 -command do_fsck_objects \
3042 -font font_ui
3044 .mbar.repository add separator
3046 if {[is_Windows]} {
3047 .mbar.repository add command \
3048 -label {Create Desktop Icon} \
3049 -command do_windows_shortcut \
3050 -font font_ui
3051 } elseif {[is_MacOSX]} {
3052 .mbar.repository add command \
3053 -label {Create Desktop Icon} \
3054 -command do_macosx_app \
3055 -font font_ui
3056 }
3057 }
3059 .mbar.repository add command -label Quit \
3060 -command do_quit \
3061 -accelerator $M1T-Q \
3062 -font font_ui
3064 # -- Edit Menu
3065 #
3066 menu .mbar.edit
3067 .mbar.edit add command -label Undo \
3068 -command {catch {[focus] edit undo}} \
3069 -accelerator $M1T-Z \
3070 -font font_ui
3071 .mbar.edit add command -label Redo \
3072 -command {catch {[focus] edit redo}} \
3073 -accelerator $M1T-Y \
3074 -font font_ui
3075 .mbar.edit add separator
3076 .mbar.edit add command -label Cut \
3077 -command {catch {tk_textCut [focus]}} \
3078 -accelerator $M1T-X \
3079 -font font_ui
3080 .mbar.edit add command -label Copy \
3081 -command {catch {tk_textCopy [focus]}} \
3082 -accelerator $M1T-C \
3083 -font font_ui
3084 .mbar.edit add command -label Paste \
3085 -command {catch {tk_textPaste [focus]; [focus] see insert}} \
3086 -accelerator $M1T-V \
3087 -font font_ui
3088 .mbar.edit add command -label Delete \
3089 -command {catch {[focus] delete sel.first sel.last}} \
3090 -accelerator Del \
3091 -font font_ui
3092 .mbar.edit add separator
3093 .mbar.edit add command -label {Select All} \
3094 -command {catch {[focus] tag add sel 0.0 end}} \
3095 -accelerator $M1T-A \
3096 -font font_ui
3098 # -- Branch Menu
3099 #
3100 if {!$single_commit} {
3101 menu .mbar.branch
3103 .mbar.branch add command -label {Create...} \
3104 -command do_create_branch \
3105 -font font_ui
3106 lappend disable_on_lock [list .mbar.branch entryconf \
3107 [.mbar.branch index last] -state]
3109 .mbar.branch add command -label {Delete...} \
3110 -command do_delete_branch \
3111 -font font_ui
3112 lappend disable_on_lock [list .mbar.branch entryconf \
3113 [.mbar.branch index last] -state]
3114 }
3116 # -- Commit Menu
3117 #
3118 menu .mbar.commit
3120 .mbar.commit add radiobutton \
3121 -label {New Commit} \
3122 -command do_select_commit_type \
3123 -variable selected_commit_type \
3124 -value new \
3125 -font font_ui
3126 lappend disable_on_lock \
3127 [list .mbar.commit entryconf [.mbar.commit index last] -state]
3129 .mbar.commit add radiobutton \
3130 -label {Amend Last Commit} \
3131 -command do_select_commit_type \
3132 -variable selected_commit_type \
3133 -value amend \
3134 -font font_ui
3135 lappend disable_on_lock \
3136 [list .mbar.commit entryconf [.mbar.commit index last] -state]
3138 .mbar.commit add separator
3140 .mbar.commit add command -label Rescan \
3141 -command do_rescan \
3142 -accelerator F5 \
3143 -font font_ui
3144 lappend disable_on_lock \
3145 [list .mbar.commit entryconf [.mbar.commit index last] -state]
3147 .mbar.commit add command -label {Add To Commit} \
3148 -command do_include_selection \
3149 -font font_ui
3150 lappend disable_on_lock \
3151 [list .mbar.commit entryconf [.mbar.commit index last] -state]
3153 .mbar.commit add command -label {Add All To Commit} \
3154 -command do_include_all \
3155 -accelerator $M1T-I \
3156 -font font_ui
3157 lappend disable_on_lock \
3158 [list .mbar.commit entryconf [.mbar.commit index last] -state]
3160 .mbar.commit add command -label {Remove From Commit} \
3161 -command do_remove_selection \
3162 -font font_ui
3163 lappend disable_on_lock \
3164 [list .mbar.commit entryconf [.mbar.commit index last] -state]
3166 .mbar.commit add command -label {Revert Changes} \
3167 -command do_revert_selection \
3168 -font font_ui
3169 lappend disable_on_lock \
3170 [list .mbar.commit entryconf [.mbar.commit index last] -state]
3172 .mbar.commit add separator
3174 .mbar.commit add command -label {Sign Off} \
3175 -command do_signoff \
3176 -accelerator $M1T-S \
3177 -font font_ui
3179 .mbar.commit add command -label Commit \
3180 -command do_commit \
3181 -accelerator $M1T-Return \
3182 -font font_ui
3183 lappend disable_on_lock \
3184 [list .mbar.commit entryconf [.mbar.commit index last] -state]
3186 # -- Transport menus
3187 #
3188 if {!$single_commit} {
3189 menu .mbar.fetch
3190 menu .mbar.pull
3191 menu .mbar.push
3192 }
3194 if {[is_MacOSX]} {
3195 # -- Apple Menu (Mac OS X only)
3196 #
3197 .mbar add cascade -label Apple -menu .mbar.apple
3198 menu .mbar.apple
3200 .mbar.apple add command -label "About $appname" \
3201 -command do_about \
3202 -font font_ui
3203 .mbar.apple add command -label "$appname Options..." \
3204 -command do_options \
3205 -font font_ui
3206 } else {
3207 # -- Edit Menu
3208 #
3209 .mbar.edit add separator
3210 .mbar.edit add command -label {Options...} \
3211 -command do_options \
3212 -font font_ui
3214 # -- Tools Menu
3215 #
3216 if {[file exists /usr/local/miga/lib/gui-miga]} {
3217 proc do_miga {} {
3218 global gitdir ui_status_value
3219 if {![lock_index update]} return
3220 set cmd [list sh --login -c "/usr/local/miga/lib/gui-miga \"[pwd]\""]
3221 set miga_fd [open "|$cmd" r]
3222 fconfigure $miga_fd -blocking 0
3223 fileevent $miga_fd readable [list miga_done $miga_fd]
3224 set ui_status_value {Running miga...}
3225 }
3226 proc miga_done {fd} {
3227 read $fd 512
3228 if {[eof $fd]} {
3229 close $fd
3230 unlock_index
3231 rescan [list set ui_status_value {Ready.}]
3232 }
3233 }
3234 .mbar add cascade -label Tools -menu .mbar.tools
3235 menu .mbar.tools
3236 .mbar.tools add command -label "Migrate" \
3237 -command do_miga \
3238 -font font_ui
3239 lappend disable_on_lock \
3240 [list .mbar.tools entryconf [.mbar.tools index last] -state]
3241 }
3243 # -- Help Menu
3244 #
3245 .mbar add cascade -label Help -menu .mbar.help
3246 menu .mbar.help
3248 .mbar.help add command -label "About $appname" \
3249 -command do_about \
3250 -font font_ui
3251 }
3254 # -- Branch Control
3255 #
3256 frame .branch \
3257 -borderwidth 1 \
3258 -relief sunken
3259 label .branch.l1 \
3260 -text {Current Branch:} \
3261 -anchor w \
3262 -justify left \
3263 -font font_ui
3264 label .branch.cb \
3265 -textvariable current_branch \
3266 -anchor w \
3267 -justify left \
3268 -font font_ui
3269 pack .branch.l1 -side left
3270 pack .branch.cb -side left -fill x
3271 pack .branch -side top -fill x
3273 # -- Main Window Layout
3274 #
3275 panedwindow .vpane -orient vertical
3276 panedwindow .vpane.files -orient horizontal
3277 .vpane add .vpane.files -sticky nsew -height 100 -width 400
3278 pack .vpane -anchor n -side top -fill both -expand 1
3280 # -- Index File List
3281 #
3282 frame .vpane.files.index -height 100 -width 400
3283 label .vpane.files.index.title -text {Modified Files} \
3284 -background green \
3285 -font font_ui
3286 text $ui_index -background white -borderwidth 0 \
3287 -width 40 -height 10 \
3288 -font font_ui \
3289 -cursor $cursor_ptr \
3290 -yscrollcommand {.vpane.files.index.sb set} \
3291 -state disabled
3292 scrollbar .vpane.files.index.sb -command [list $ui_index yview]
3293 pack .vpane.files.index.title -side top -fill x
3294 pack .vpane.files.index.sb -side right -fill y
3295 pack $ui_index -side left -fill both -expand 1
3296 .vpane.files add .vpane.files.index -sticky nsew
3298 # -- Other (Add) File List
3299 #
3300 frame .vpane.files.other -height 100 -width 100
3301 label .vpane.files.other.title -text {Untracked Files} \
3302 -background red \
3303 -font font_ui
3304 text $ui_other -background white -borderwidth 0 \
3305 -width 40 -height 10 \
3306 -font font_ui \
3307 -cursor $cursor_ptr \
3308 -yscrollcommand {.vpane.files.other.sb set} \
3309 -state disabled
3310 scrollbar .vpane.files.other.sb -command [list $ui_other yview]
3311 pack .vpane.files.other.title -side top -fill x
3312 pack .vpane.files.other.sb -side right -fill y
3313 pack $ui_other -side left -fill both -expand 1
3314 .vpane.files add .vpane.files.other -sticky nsew
3316 foreach i [list $ui_index $ui_other] {
3317 $i tag conf in_diff -font font_uibold
3318 $i tag conf in_sel \
3319 -background [$i cget -foreground] \
3320 -foreground [$i cget -background]
3321 }
3322 unset i
3324 # -- Diff and Commit Area
3325 #
3326 frame .vpane.lower -height 300 -width 400
3327 frame .vpane.lower.commarea
3328 frame .vpane.lower.diff -relief sunken -borderwidth 1
3329 pack .vpane.lower.commarea -side top -fill x
3330 pack .vpane.lower.diff -side bottom -fill both -expand 1
3331 .vpane add .vpane.lower -stick nsew
3333 # -- Commit Area Buttons
3334 #
3335 frame .vpane.lower.commarea.buttons
3336 label .vpane.lower.commarea.buttons.l -text {} \
3337 -anchor w \
3338 -justify left \
3339 -font font_ui
3340 pack .vpane.lower.commarea.buttons.l -side top -fill x
3341 pack .vpane.lower.commarea.buttons -side left -fill y
3343 button .vpane.lower.commarea.buttons.rescan -text {Rescan} \
3344 -command do_rescan \
3345 -font font_ui
3346 pack .vpane.lower.commarea.buttons.rescan -side top -fill x
3347 lappend disable_on_lock \
3348 {.vpane.lower.commarea.buttons.rescan conf -state}
3350 button .vpane.lower.commarea.buttons.incall -text {Add All} \
3351 -command do_include_all \
3352 -font font_ui
3353 pack .vpane.lower.commarea.buttons.incall -side top -fill x
3354 lappend disable_on_lock \
3355 {.vpane.lower.commarea.buttons.incall conf -state}
3357 button .vpane.lower.commarea.buttons.signoff -text {Sign Off} \
3358 -command do_signoff \
3359 -font font_ui
3360 pack .vpane.lower.commarea.buttons.signoff -side top -fill x
3362 button .vpane.lower.commarea.buttons.commit -text {Commit} \
3363 -command do_commit \
3364 -font font_ui
3365 pack .vpane.lower.commarea.buttons.commit -side top -fill x
3366 lappend disable_on_lock \
3367 {.vpane.lower.commarea.buttons.commit conf -state}
3369 # -- Commit Message Buffer
3370 #
3371 frame .vpane.lower.commarea.buffer
3372 frame .vpane.lower.commarea.buffer.header
3373 set ui_comm .vpane.lower.commarea.buffer.t
3374 set ui_coml .vpane.lower.commarea.buffer.header.l
3375 radiobutton .vpane.lower.commarea.buffer.header.new \
3376 -text {New Commit} \
3377 -command do_select_commit_type \
3378 -variable selected_commit_type \
3379 -value new \
3380 -font font_ui
3381 lappend disable_on_lock \
3382 [list .vpane.lower.commarea.buffer.header.new conf -state]
3383 radiobutton .vpane.lower.commarea.buffer.header.amend \
3384 -text {Amend Last Commit} \
3385 -command do_select_commit_type \
3386 -variable selected_commit_type \
3387 -value amend \
3388 -font font_ui
3389 lappend disable_on_lock \
3390 [list .vpane.lower.commarea.buffer.header.amend conf -state]
3391 label $ui_coml \
3392 -anchor w \
3393 -justify left \
3394 -font font_ui
3395 proc trace_commit_type {varname args} {
3396 global ui_coml commit_type
3397 switch -glob -- $commit_type {
3398 initial {set txt {Initial Commit Message:}}
3399 amend {set txt {Amended Commit Message:}}
3400 amend-initial {set txt {Amended Initial Commit Message:}}
3401 amend-merge {set txt {Amended Merge Commit Message:}}
3402 merge {set txt {Merge Commit Message:}}
3403 * {set txt {Commit Message:}}
3404 }
3405 $ui_coml conf -text $txt
3406 }
3407 trace add variable commit_type write trace_commit_type
3408 pack $ui_coml -side left -fill x
3409 pack .vpane.lower.commarea.buffer.header.amend -side right
3410 pack .vpane.lower.commarea.buffer.header.new -side right
3412 text $ui_comm -background white -borderwidth 1 \
3413 -undo true \
3414 -maxundo 20 \
3415 -autoseparators true \
3416 -relief sunken \
3417 -width 75 -height 9 -wrap none \
3418 -font font_diff \
3419 -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
3420 scrollbar .vpane.lower.commarea.buffer.sby \
3421 -command [list $ui_comm yview]
3422 pack .vpane.lower.commarea.buffer.header -side top -fill x
3423 pack .vpane.lower.commarea.buffer.sby -side right -fill y
3424 pack $ui_comm -side left -fill y
3425 pack .vpane.lower.commarea.buffer -side left -fill y
3427 # -- Commit Message Buffer Context Menu
3428 #
3429 set ctxm .vpane.lower.commarea.buffer.ctxm
3430 menu $ctxm -tearoff 0
3431 $ctxm add command \
3432 -label {Cut} \
3433 -font font_ui \
3434 -command {tk_textCut $ui_comm}
3435 $ctxm add command \
3436 -label {Copy} \
3437 -font font_ui \
3438 -command {tk_textCopy $ui_comm}
3439 $ctxm add command \
3440 -label {Paste} \
3441 -font font_ui \
3442 -command {tk_textPaste $ui_comm}
3443 $ctxm add command \
3444 -label {Delete} \
3445 -font font_ui \
3446 -command {$ui_comm delete sel.first sel.last}
3447 $ctxm add separator
3448 $ctxm add command \
3449 -label {Select All} \
3450 -font font_ui \
3451 -command {$ui_comm tag add sel 0.0 end}
3452 $ctxm add command \
3453 -label {Copy All} \
3454 -font font_ui \
3455 -command {
3456 $ui_comm tag add sel 0.0 end
3457 tk_textCopy $ui_comm
3458 $ui_comm tag remove sel 0.0 end
3459 }
3460 $ctxm add separator
3461 $ctxm add command \
3462 -label {Sign Off} \
3463 -font font_ui \
3464 -command do_signoff
3465 bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
3467 # -- Diff Header
3468 #
3469 set current_diff {}
3470 set diff_actions [list]
3471 proc trace_current_diff {varname args} {
3472 global current_diff diff_actions file_states
3473 if {$current_diff eq {}} {
3474 set s {}
3475 set f {}
3476 set p {}
3477 set o disabled
3478 } else {
3479 set p $current_diff
3480 set s [mapdesc [lindex $file_states($p) 0] $p]
3481 set f {File:}
3482 set p [escape_path $p]
3483 set o normal
3484 }
3486 .vpane.lower.diff.header.status configure -text $s
3487 .vpane.lower.diff.header.file configure -text $f
3488 .vpane.lower.diff.header.path configure -text $p
3489 foreach w $diff_actions {
3490 uplevel #0 $w $o
3491 }
3492 }
3493 trace add variable current_diff write trace_current_diff
3495 frame .vpane.lower.diff.header -background orange
3496 label .vpane.lower.diff.header.status \
3497 -background orange \
3498 -width $max_status_desc \
3499 -anchor w \
3500 -justify left \
3501 -font font_ui
3502 label .vpane.lower.diff.header.file \
3503 -background orange \
3504 -anchor w \
3505 -justify left \
3506 -font font_ui
3507 label .vpane.lower.diff.header.path \
3508 -background orange \
3509 -anchor w \
3510 -justify left \
3511 -font font_ui
3512 pack .vpane.lower.diff.header.status -side left
3513 pack .vpane.lower.diff.header.file -side left
3514 pack .vpane.lower.diff.header.path -fill x
3515 set ctxm .vpane.lower.diff.header.ctxm
3516 menu $ctxm -tearoff 0
3517 $ctxm add command \
3518 -label {Copy} \
3519 -font font_ui \
3520 -command {
3521 clipboard clear
3522 clipboard append \
3523 -format STRING \
3524 -type STRING \
3525 -- $current_diff
3526 }
3527 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3528 bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
3530 # -- Diff Body
3531 #
3532 frame .vpane.lower.diff.body
3533 set ui_diff .vpane.lower.diff.body.t
3534 text $ui_diff -background white -borderwidth 0 \
3535 -width 80 -height 15 -wrap none \
3536 -font font_diff \
3537 -xscrollcommand {.vpane.lower.diff.body.sbx set} \
3538 -yscrollcommand {.vpane.lower.diff.body.sby set} \
3539 -state disabled
3540 scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
3541 -command [list $ui_diff xview]
3542 scrollbar .vpane.lower.diff.body.sby -orient vertical \
3543 -command [list $ui_diff yview]
3544 pack .vpane.lower.diff.body.sbx -side bottom -fill x
3545 pack .vpane.lower.diff.body.sby -side right -fill y
3546 pack $ui_diff -side left -fill both -expand 1
3547 pack .vpane.lower.diff.header -side top -fill x
3548 pack .vpane.lower.diff.body -side bottom -fill both -expand 1
3550 $ui_diff tag conf d_@ -font font_diffbold
3551 $ui_diff tag conf d_+ -foreground blue
3552 $ui_diff tag conf d_- -foreground red
3553 $ui_diff tag conf d_++ -foreground {#00a000}
3554 $ui_diff tag conf d_-- -foreground {#a000a0}
3555 $ui_diff tag conf d_+- \
3556 -foreground red \
3557 -background {light goldenrod yellow}
3558 $ui_diff tag conf d_-+ \
3559 -foreground blue \
3560 -background azure2
3562 # -- Diff Body Context Menu
3563 #
3564 set ctxm .vpane.lower.diff.body.ctxm
3565 menu $ctxm -tearoff 0
3566 $ctxm add command \
3567 -label {Copy} \
3568 -font font_ui \
3569 -command {tk_textCopy $ui_diff}
3570 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3571 $ctxm add command \
3572 -label {Select All} \
3573 -font font_ui \
3574 -command {$ui_diff tag add sel 0.0 end}
3575 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3576 $ctxm add command \
3577 -label {Copy All} \
3578 -font font_ui \
3579 -command {
3580 $ui_diff tag add sel 0.0 end
3581 tk_textCopy $ui_diff
3582 $ui_diff tag remove sel 0.0 end
3583 }
3584 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3585 $ctxm add separator
3586 $ctxm add command \
3587 -label {Decrease Font Size} \
3588 -font font_ui \
3589 -command {incr_font_size font_diff -1}
3590 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3591 $ctxm add command \
3592 -label {Increase Font Size} \
3593 -font font_ui \
3594 -command {incr_font_size font_diff 1}
3595 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3596 $ctxm add separator
3597 $ctxm add command \
3598 -label {Show Less Context} \
3599 -font font_ui \
3600 -command {if {$repo_config(gui.diffcontext) >= 2} {
3601 incr repo_config(gui.diffcontext) -1
3602 reshow_diff
3603 }}
3604 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3605 $ctxm add command \
3606 -label {Show More Context} \
3607 -font font_ui \
3608 -command {
3609 incr repo_config(gui.diffcontext)
3610 reshow_diff
3611 }
3612 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
3613 $ctxm add separator
3614 $ctxm add command -label {Options...} \
3615 -font font_ui \
3616 -command do_options
3617 bind_button3 $ui_diff "tk_popup $ctxm %X %Y"
3619 # -- Status Bar
3620 #
3621 set ui_status_value {Initializing...}
3622 label .status -textvariable ui_status_value \
3623 -anchor w \
3624 -justify left \
3625 -borderwidth 1 \
3626 -relief sunken \
3627 -font font_ui
3628 pack .status -anchor w -side bottom -fill x
3630 # -- Load geometry
3631 #
3632 catch {
3633 set gm $repo_config(gui.geometry)
3634 wm geometry . [lindex $gm 0]
3635 .vpane sash place 0 \
3636 [lindex [.vpane sash coord 0] 0] \
3637 [lindex $gm 1]
3638 .vpane.files sash place 0 \
3639 [lindex $gm 2] \
3640 [lindex [.vpane.files sash coord 0] 1]
3641 unset gm
3642 }
3644 # -- Key Bindings
3645 #
3646 bind $ui_comm <$M1B-Key-Return> {do_commit;break}
3647 bind $ui_comm <$M1B-Key-i> {do_include_all;break}
3648 bind $ui_comm <$M1B-Key-I> {do_include_all;break}
3649 bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
3650 bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
3651 bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
3652 bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
3653 bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
3654 bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
3655 bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
3656 bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
3658 bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
3659 bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
3660 bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
3661 bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
3662 bind $ui_diff <$M1B-Key-v> {break}
3663 bind $ui_diff <$M1B-Key-V> {break}
3664 bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
3665 bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
3666 bind $ui_diff <Key-Up> {catch {%W yview scroll -1 units};break}
3667 bind $ui_diff <Key-Down> {catch {%W yview scroll 1 units};break}
3668 bind $ui_diff <Key-Left> {catch {%W xview scroll -1 units};break}
3669 bind $ui_diff <Key-Right> {catch {%W xview scroll 1 units};break}
3671 bind . <Destroy> do_quit
3672 bind all <Key-F5> do_rescan
3673 bind all <$M1B-Key-r> do_rescan
3674 bind all <$M1B-Key-R> do_rescan
3675 bind . <$M1B-Key-s> do_signoff
3676 bind . <$M1B-Key-S> do_signoff
3677 bind . <$M1B-Key-i> do_include_all
3678 bind . <$M1B-Key-I> do_include_all
3679 bind . <$M1B-Key-Return> do_commit
3680 bind all <$M1B-Key-q> do_quit
3681 bind all <$M1B-Key-Q> do_quit
3682 bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
3683 bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
3684 foreach i [list $ui_index $ui_other] {
3685 bind $i <Button-1> "toggle_or_diff $i %x %y; break"
3686 bind $i <$M1B-Button-1> "add_one_to_selection $i %x %y; break"
3687 bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
3688 }
3689 unset i
3691 set file_lists($ui_index) [list]
3692 set file_lists($ui_other) [list]
3694 set HEAD {}
3695 set PARENT {}
3696 set MERGE_HEAD [list]
3697 set commit_type {}
3698 set empty_tree {}
3699 set current_branch {}
3700 set current_diff {}
3701 set selected_commit_type new
3703 wm title . "$appname ([file normalize [file dirname $gitdir]])"
3704 focus -force $ui_comm
3706 # -- Warn the user about environmental problems. Cygwin's Tcl
3707 # does *not* pass its env array onto any processes it spawns.
3708 # This means that git processes get none of our environment.
3709 #
3710 if {[is_Windows]} {
3711 set ignored_env 0
3712 set suggest_user {}
3713 set msg "Possible environment issues exist.
3715 The following environment variables are probably
3716 going to be ignored by any Git subprocess run
3717 by $appname:
3719 "
3720 foreach name [array names env] {
3721 switch -regexp -- $name {
3722 {^GIT_INDEX_FILE$} -
3723 {^GIT_OBJECT_DIRECTORY$} -
3724 {^GIT_ALTERNATE_OBJECT_DIRECTORIES$} -
3725 {^GIT_DIFF_OPTS$} -
3726 {^GIT_EXTERNAL_DIFF$} -
3727 {^GIT_PAGER$} -
3728 {^GIT_TRACE$} -
3729 {^GIT_CONFIG$} -
3730 {^GIT_CONFIG_LOCAL$} -
3731 {^GIT_(AUTHOR|COMMITTER)_DATE$} {
3732 append msg " - $name\n"
3733 incr ignored_env
3734 }
3735 {^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$} {
3736 append msg " - $name\n"
3737 incr ignored_env
3738 set suggest_user $name
3739 }
3740 }
3741 }
3742 if {$ignored_env > 0} {
3743 append msg "
3744 This is due to a known issue with the
3745 Tcl binary distributed by Cygwin."
3747 if {$suggest_user ne {}} {
3748 append msg "
3750 A good replacement for $suggest_user
3751 is placing values for the user.name and
3752 user.email settings into your personal
3753 ~/.gitconfig file.
3754 "
3755 }
3756 warn_popup $msg
3757 }
3758 unset ignored_env msg suggest_user name
3759 }
3761 # -- Only initialize complex UI if we are going to stay running.
3762 #
3763 if {!$single_commit} {
3764 load_all_remotes
3765 load_all_heads
3767 populate_branch_menu .mbar.branch
3768 populate_fetch_menu .mbar.fetch
3769 populate_pull_menu .mbar.pull
3770 populate_push_menu .mbar.push
3771 }
3773 lock_index begin-read
3774 after 1 do_rescan