1#!/bin/sh
2# Tcl ignores the next line -*- tcl -*- \
3exec wish "$0" -- "$@"
4
5# Copyright (C) 2006 Shawn Pearce, Paul Mackerras. All rights reserved.
6# This program is free software; it may be used, copied, modified
7# and distributed under the terms of the GNU General Public Licence,
8# either version 2, or (at your option) any later version.
9
10set appname [lindex [file split $argv0] end]
11set gitdir {}
12
13######################################################################
14##
15## config
16
17proc is_many_config {name} {
18 switch -glob -- $name {
19 remote.*.fetch -
20 remote.*.push
21 {return 1}
22 *
23 {return 0}
24 }
25}
26
27proc load_config {include_global} {
28 global repo_config global_config default_config
29
30 array unset global_config
31 if {$include_global} {
32 catch {
33 set fd_rc [open "| git repo-config --global --list" r]
34 while {[gets $fd_rc line] >= 0} {
35 if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
36 if {[is_many_config $name]} {
37 lappend global_config($name) $value
38 } else {
39 set global_config($name) $value
40 }
41 }
42 }
43 close $fd_rc
44 }
45 }
46
47 array unset repo_config
48 catch {
49 set fd_rc [open "| git repo-config --list" r]
50 while {[gets $fd_rc line] >= 0} {
51 if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
52 if {[is_many_config $name]} {
53 lappend repo_config($name) $value
54 } else {
55 set repo_config($name) $value
56 }
57 }
58 }
59 close $fd_rc
60 }
61
62 foreach name [array names default_config] {
63 if {[catch {set v $global_config($name)}]} {
64 set global_config($name) $default_config($name)
65 }
66 if {[catch {set v $repo_config($name)}]} {
67 set repo_config($name) $default_config($name)
68 }
69 }
70}
71
72proc save_config {} {
73 global default_config font_descs
74 global repo_config global_config
75 global repo_config_new global_config_new
76
77 foreach option $font_descs {
78 set name [lindex $option 0]
79 set font [lindex $option 1]
80 font configure $font \
81 -family $global_config_new(gui.$font^^family) \
82 -size $global_config_new(gui.$font^^size)
83 font configure ${font}bold \
84 -family $global_config_new(gui.$font^^family) \
85 -size $global_config_new(gui.$font^^size)
86 set global_config_new(gui.$name) [font configure $font]
87 unset global_config_new(gui.$font^^family)
88 unset global_config_new(gui.$font^^size)
89 }
90
91 foreach name [array names default_config] {
92 set value $global_config_new($name)
93 if {$value ne $global_config($name)} {
94 if {$value eq $default_config($name)} {
95 catch {exec git repo-config --global --unset $name}
96 } else {
97 regsub -all "\[{}\]" $value {"} value
98 exec git repo-config --global $name $value
99 }
100 set global_config($name) $value
101 if {$value eq $repo_config($name)} {
102 catch {exec git repo-config --unset $name}
103 set repo_config($name) $value
104 }
105 }
106 }
107
108 foreach name [array names default_config] {
109 set value $repo_config_new($name)
110 if {$value ne $repo_config($name)} {
111 if {$value eq $global_config($name)} {
112 catch {exec git repo-config --unset $name}
113 } else {
114 regsub -all "\[{}\]" $value {"} value
115 exec git repo-config $name $value
116 }
117 set repo_config($name) $value
118 }
119 }
120}
121
122proc error_popup {msg} {
123 global gitdir appname
124
125 set title $appname
126 if {$gitdir ne {}} {
127 append title { (}
128 append title [lindex \
129 [file split [file normalize [file dirname $gitdir]]] \
130 end]
131 append title {)}
132 }
133 set cmd [list tk_messageBox \
134 -icon error \
135 -type ok \
136 -title "$title: error" \
137 -message $msg]
138 if {[winfo ismapped .]} {
139 lappend cmd -parent .
140 }
141 eval $cmd
142}
143
144proc info_popup {msg} {
145 global gitdir appname
146
147 set title $appname
148 if {$gitdir ne {}} {
149 append title { (}
150 append title [lindex \
151 [file split [file normalize [file dirname $gitdir]]] \
152 end]
153 append title {)}
154 }
155 tk_messageBox \
156 -parent . \
157 -icon error \
158 -type ok \
159 -title $title \
160 -message $msg
161}
162
163######################################################################
164##
165## repository setup
166
167if { [catch {set gitdir $env(GIT_DIR)}]
168 && [catch {set gitdir [exec git rev-parse --git-dir]} err]} {
169 catch {wm withdraw .}
170 error_popup "Cannot find the git directory:\n\n$err"
171 exit 1
172}
173if {![file isdirectory $gitdir]} {
174 catch {wm withdraw .}
175 error_popup "Git directory not found:\n\n$gitdir"
176 exit 1
177}
178if {[lindex [file split $gitdir] end] ne {.git}} {
179 catch {wm withdraw .}
180 error_popup "Cannot use funny .git directory:\n\n$gitdir"
181 exit 1
182}
183if {[catch {cd [file dirname $gitdir]} err]} {
184 catch {wm withdraw .}
185 error_popup "No working directory [file dirname $gitdir]:\n\n$err"
186 exit 1
187}
188
189set single_commit 0
190if {$appname eq {git-citool}} {
191 set single_commit 1
192}
193
194######################################################################
195##
196## task management
197
198set rescan_active 0
199set diff_active 0
200set last_clicked {}
201
202set disable_on_lock [list]
203set index_lock_type none
204
205proc lock_index {type} {
206 global index_lock_type disable_on_lock
207
208 if {$index_lock_type eq {none}} {
209 set index_lock_type $type
210 foreach w $disable_on_lock {
211 uplevel #0 $w disabled
212 }
213 return 1
214 } elseif {$index_lock_type eq "begin-$type"} {
215 set index_lock_type $type
216 return 1
217 }
218 return 0
219}
220
221proc unlock_index {} {
222 global index_lock_type disable_on_lock
223
224 set index_lock_type none
225 foreach w $disable_on_lock {
226 uplevel #0 $w normal
227 }
228}
229
230######################################################################
231##
232## status
233
234proc repository_state {hdvar ctvar} {
235 global gitdir
236 upvar $hdvar hd $ctvar ct
237
238 if {[catch {set hd [exec git rev-parse --verify HEAD]}]} {
239 set hd {}
240 set ct initial
241 } elseif {[file exists [file join $gitdir MERGE_HEAD]]} {
242 set ct merge
243 } else {
244 set ct normal
245 }
246}
247
248proc PARENT {} {
249 global PARENT empty_tree
250
251 if {$PARENT ne {}} {
252 return $PARENT
253 }
254 if {$empty_tree eq {}} {
255 set empty_tree [exec git mktree << {}]
256 }
257 return $empty_tree
258}
259
260proc rescan {after} {
261 global HEAD PARENT commit_type
262 global ui_index ui_other ui_status_value ui_comm
263 global rescan_active file_states
264 global repo_config
265
266 if {$rescan_active > 0 || ![lock_index read]} return
267
268 repository_state new_HEAD new_type
269 if {[string match amend* $commit_type]
270 && $new_type eq {normal}
271 && $new_HEAD eq $HEAD} {
272 } else {
273 set HEAD $new_HEAD
274 set PARENT $new_HEAD
275 set commit_type $new_type
276 }
277
278 array unset file_states
279
280 if {![$ui_comm edit modified]
281 || [string trim [$ui_comm get 0.0 end]] eq {}} {
282 if {[load_message GITGUI_MSG]} {
283 } elseif {[load_message MERGE_MSG]} {
284 } elseif {[load_message SQUASH_MSG]} {
285 }
286 $ui_comm edit modified false
287 $ui_comm edit reset
288 }
289
290 if {$repo_config(gui.trustmtime) eq {true}} {
291 rescan_stage2 {} $after
292 } else {
293 set rescan_active 1
294 set ui_status_value {Refreshing file status...}
295 set cmd [list git update-index]
296 lappend cmd -q
297 lappend cmd --unmerged
298 lappend cmd --ignore-missing
299 lappend cmd --refresh
300 set fd_rf [open "| $cmd" r]
301 fconfigure $fd_rf -blocking 0 -translation binary
302 fileevent $fd_rf readable \
303 [list rescan_stage2 $fd_rf $after]
304 }
305}
306
307proc rescan_stage2 {fd after} {
308 global gitdir ui_status_value
309 global rescan_active buf_rdi buf_rdf buf_rlo
310
311 if {$fd ne {}} {
312 read $fd
313 if {![eof $fd]} return
314 close $fd
315 }
316
317 set ls_others [list | git ls-files --others -z \
318 --exclude-per-directory=.gitignore]
319 set info_exclude [file join $gitdir info exclude]
320 if {[file readable $info_exclude]} {
321 lappend ls_others "--exclude-from=$info_exclude"
322 }
323
324 set buf_rdi {}
325 set buf_rdf {}
326 set buf_rlo {}
327
328 set rescan_active 3
329 set ui_status_value {Scanning for modified files ...}
330 set fd_di [open "| git diff-index --cached -z [PARENT]" r]
331 set fd_df [open "| git diff-files -z" r]
332 set fd_lo [open $ls_others r]
333
334 fconfigure $fd_di -blocking 0 -translation binary
335 fconfigure $fd_df -blocking 0 -translation binary
336 fconfigure $fd_lo -blocking 0 -translation binary
337 fileevent $fd_di readable [list read_diff_index $fd_di $after]
338 fileevent $fd_df readable [list read_diff_files $fd_df $after]
339 fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
340}
341
342proc load_message {file} {
343 global gitdir ui_comm
344
345 set f [file join $gitdir $file]
346 if {[file isfile $f]} {
347 if {[catch {set fd [open $f r]}]} {
348 return 0
349 }
350 set content [string trim [read $fd]]
351 close $fd
352 $ui_comm delete 0.0 end
353 $ui_comm insert end $content
354 return 1
355 }
356 return 0
357}
358
359proc read_diff_index {fd after} {
360 global buf_rdi
361
362 append buf_rdi [read $fd]
363 set c 0
364 set n [string length $buf_rdi]
365 while {$c < $n} {
366 set z1 [string first "\0" $buf_rdi $c]
367 if {$z1 == -1} break
368 incr z1
369 set z2 [string first "\0" $buf_rdi $z1]
370 if {$z2 == -1} break
371
372 set c $z2
373 incr z2 -1
374 display_file \
375 [string range $buf_rdi $z1 $z2] \
376 [string index $buf_rdi [expr {$z1 - 2}]]?
377 incr c
378 }
379 if {$c < $n} {
380 set buf_rdi [string range $buf_rdi $c end]
381 } else {
382 set buf_rdi {}
383 }
384
385 rescan_done $fd buf_rdi $after
386}
387
388proc read_diff_files {fd after} {
389 global buf_rdf
390
391 append buf_rdf [read $fd]
392 set c 0
393 set n [string length $buf_rdf]
394 while {$c < $n} {
395 set z1 [string first "\0" $buf_rdf $c]
396 if {$z1 == -1} break
397 incr z1
398 set z2 [string first "\0" $buf_rdf $z1]
399 if {$z2 == -1} break
400
401 set c $z2
402 incr z2 -1
403 display_file \
404 [string range $buf_rdf $z1 $z2] \
405 ?[string index $buf_rdf [expr {$z1 - 2}]]
406 incr c
407 }
408 if {$c < $n} {
409 set buf_rdf [string range $buf_rdf $c end]
410 } else {
411 set buf_rdf {}
412 }
413
414 rescan_done $fd buf_rdf $after
415}
416
417proc read_ls_others {fd after} {
418 global buf_rlo
419
420 append buf_rlo [read $fd]
421 set pck [split $buf_rlo "\0"]
422 set buf_rlo [lindex $pck end]
423 foreach p [lrange $pck 0 end-1] {
424 display_file $p ?O
425 }
426 rescan_done $fd buf_rlo $after
427}
428
429proc rescan_done {fd buf after} {
430 global rescan_active
431 global file_states repo_config
432 upvar $buf to_clear
433
434 if {![eof $fd]} return
435 set to_clear {}
436 close $fd
437 if {[incr rescan_active -1] > 0} return
438
439 prune_selection
440 unlock_index
441 display_all_files
442
443 if {$repo_config(gui.partialinclude) ne {true}} {
444 set pathList [list]
445 foreach path [array names file_states] {
446 switch -- [lindex $file_states($path) 0] {
447 AM -
448 MM {lappend pathList $path}
449 }
450 }
451 if {$pathList ne {}} {
452 update_index \
453 "Updating included files" \
454 $pathList \
455 [concat {reshow_diff;} $after]
456 return
457 }
458 }
459
460 reshow_diff
461 uplevel #0 $after
462}
463
464proc prune_selection {} {
465 global file_states selected_paths
466
467 foreach path [array names selected_paths] {
468 if {[catch {set still_here $file_states($path)}]} {
469 unset selected_paths($path)
470 }
471 }
472}
473
474######################################################################
475##
476## diff
477
478proc clear_diff {} {
479 global ui_diff current_diff ui_index ui_other
480
481 $ui_diff conf -state normal
482 $ui_diff delete 0.0 end
483 $ui_diff conf -state disabled
484
485 set current_diff {}
486
487 $ui_index tag remove in_diff 0.0 end
488 $ui_other tag remove in_diff 0.0 end
489}
490
491proc reshow_diff {} {
492 global current_diff ui_status_value file_states
493
494 if {$current_diff eq {}
495 || [catch {set s $file_states($current_diff)}]} {
496 clear_diff
497 } else {
498 show_diff $current_diff
499 }
500}
501
502proc handle_empty_diff {} {
503 global current_diff file_states file_lists
504
505 set path $current_diff
506 set s $file_states($path)
507 if {[lindex $s 0] ne {_M}} return
508
509 info_popup "No differences detected.
510
511[short_path $path] has no changes.
512
513The modification date of this file was updated
514by another application and you currently have
515the Trust File Modification Timestamps option
516enabled, so Git did not automatically detect
517that there are no content differences in this
518file.
519
520This file will now be removed from the modified
521files list, to prevent possible confusion.
522"
523 if {[catch {exec git update-index -- $path} err]} {
524 error_popup "Failed to refresh index:\n\n$err"
525 }
526
527 clear_diff
528 set old_w [mapcol [lindex $file_states($path) 0] $path]
529 set lno [lsearch -sorted $file_lists($old_w) $path]
530 if {$lno >= 0} {
531 set file_lists($old_w) \
532 [lreplace $file_lists($old_w) $lno $lno]
533 incr lno
534 $old_w conf -state normal
535 $old_w delete $lno.0 [expr {$lno + 1}].0
536 $old_w conf -state disabled
537 }
538}
539
540proc show_diff {path {w {}} {lno {}}} {
541 global file_states file_lists
542 global diff_3way diff_active repo_config
543 global ui_diff current_diff ui_status_value
544
545 if {$diff_active || ![lock_index read]} return
546
547 clear_diff
548 if {$w eq {} || $lno == {}} {
549 foreach w [array names file_lists] {
550 set lno [lsearch -sorted $file_lists($w) $path]
551 if {$lno >= 0} {
552 incr lno
553 break
554 }
555 }
556 }
557 if {$w ne {} && $lno >= 1} {
558 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
559 }
560
561 set s $file_states($path)
562 set m [lindex $s 0]
563 set diff_3way 0
564 set diff_active 1
565 set current_diff $path
566 set ui_status_value "Loading diff of [escape_path $path]..."
567
568 set cmd [list | git diff-index]
569 lappend cmd --no-color
570 if {$repo_config(gui.diffcontext) > 0} {
571 lappend cmd "-U$repo_config(gui.diffcontext)"
572 }
573 lappend cmd -p
574
575 switch $m {
576 MM {
577 lappend cmd -c
578 }
579 _O {
580 if {[catch {
581 set fd [open $path r]
582 set content [read $fd]
583 close $fd
584 } err ]} {
585 set diff_active 0
586 unlock_index
587 set ui_status_value "Unable to display [escape_path $path]"
588 error_popup "Error loading file:\n\n$err"
589 return
590 }
591 $ui_diff conf -state normal
592 $ui_diff insert end $content
593 $ui_diff conf -state disabled
594 set diff_active 0
595 unlock_index
596 set ui_status_value {Ready.}
597 return
598 }
599 }
600
601 lappend cmd [PARENT]
602 lappend cmd --
603 lappend cmd $path
604
605 if {[catch {set fd [open $cmd r]} err]} {
606 set diff_active 0
607 unlock_index
608 set ui_status_value "Unable to display [escape_path $path]"
609 error_popup "Error loading diff:\n\n$err"
610 return
611 }
612
613 fconfigure $fd -blocking 0 -translation auto
614 fileevent $fd readable [list read_diff $fd]
615}
616
617proc read_diff {fd} {
618 global ui_diff ui_status_value diff_3way diff_active
619 global repo_config
620
621 while {[gets $fd line] >= 0} {
622 if {[string match {diff --git *} $line]} continue
623 if {[string match {diff --combined *} $line]} continue
624 if {[string match {--- *} $line]} continue
625 if {[string match {+++ *} $line]} continue
626 if {[string match index* $line]} {
627 if {[string first , $line] >= 0} {
628 set diff_3way 1
629 }
630 }
631
632 $ui_diff conf -state normal
633 if {!$diff_3way} {
634 set x [string index $line 0]
635 switch -- $x {
636 "@" {set tags da}
637 "+" {set tags dp}
638 "-" {set tags dm}
639 default {set tags {}}
640 }
641 } else {
642 set x [string range $line 0 1]
643 switch -- $x {
644 default {set tags {}}
645 "@@" {set tags da}
646 "++" {set tags dp; set x " +"}
647 " +" {set tags {di bold}; set x "++"}
648 "+ " {set tags dni; set x "-+"}
649 "--" {set tags dm; set x " -"}
650 " -" {set tags {dm bold}; set x "--"}
651 "- " {set tags di; set x "+-"}
652 default {set tags {}}
653 }
654 set line [string replace $line 0 1 $x]
655 }
656 $ui_diff insert end $line $tags
657 $ui_diff insert end "\n"
658 $ui_diff conf -state disabled
659 }
660
661 if {[eof $fd]} {
662 close $fd
663 set diff_active 0
664 unlock_index
665 set ui_status_value {Ready.}
666
667 if {$repo_config(gui.trustmtime) eq {true}
668 && [$ui_diff index end] eq {2.0}} {
669 handle_empty_diff
670 }
671 }
672}
673
674######################################################################
675##
676## commit
677
678proc load_last_commit {} {
679 global HEAD PARENT commit_type ui_comm
680
681 if {[string match amend* $commit_type]} return
682 if {$commit_type ne {normal}} {
683 error_popup "Can't amend a $commit_type commit."
684 return
685 }
686
687 set msg {}
688 set parent {}
689 set parent_count 0
690 if {[catch {
691 set fd [open "| git cat-file commit $HEAD" r]
692 while {[gets $fd line] > 0} {
693 if {[string match {parent *} $line]} {
694 set parent [string range $line 7 end]
695 incr parent_count
696 }
697 }
698 set msg [string trim [read $fd]]
699 close $fd
700 } err]} {
701 error_popup "Error loading commit data for amend:\n\n$err"
702 return
703 }
704
705 if {$parent_count > 1} {
706 error_popup {Can't amend a merge commit.}
707 return
708 }
709
710 if {$parent_count == 0} {
711 set commit_type amend-initial
712 set PARENT {}
713 } elseif {$parent_count == 1} {
714 set commit_type amend
715 set PARENT $parent
716 }
717
718 $ui_comm delete 0.0 end
719 $ui_comm insert end $msg
720 $ui_comm edit modified false
721 $ui_comm edit reset
722 rescan {set ui_status_value {Ready.}}
723}
724
725proc create_new_commit {} {
726 global commit_type ui_comm
727
728 set commit_type normal
729 $ui_comm delete 0.0 end
730 $ui_comm edit modified false
731 $ui_comm edit reset
732 rescan {set ui_status_value {Ready.}}
733}
734
735proc commit_tree {} {
736 global HEAD commit_type file_states ui_comm repo_config
737
738 if {![lock_index update]} return
739
740 # -- Our in memory state should match the repository.
741 #
742 repository_state curHEAD cur_type
743 if {[string match amend* $commit_type]
744 && $cur_type eq {normal}
745 && $curHEAD eq $HEAD} {
746 } elseif {$commit_type ne $cur_type || $HEAD ne $curHEAD} {
747 error_popup {Last scanned state does not match repository state.
748
749Its highly likely that another Git program modified the
750repository since the last scan. A rescan is required
751before committing.
752
753A rescan will be automatically started now.
754}
755 unlock_index
756 rescan {set ui_status_value {Ready.}}
757 return
758 }
759
760 # -- At least one file should differ in the index.
761 #
762 set files_ready 0
763 foreach path [array names file_states] {
764 switch -glob -- [lindex $file_states($path) 0] {
765 _? {continue}
766 A? -
767 D? -
768 M? {set files_ready 1; break}
769 U? {
770 error_popup "Unmerged files cannot be committed.
771
772File [short_path $path] has merge conflicts.
773You must resolve them and include the file before committing.
774"
775 unlock_index
776 return
777 }
778 default {
779 error_popup "Unknown file state [lindex $s 0] detected.
780
781File [short_path $path] cannot be committed by this program.
782"
783 }
784 }
785 }
786 if {!$files_ready} {
787 error_popup {No included files to commit.
788
789You must include at least 1 file before you can commit.
790}
791 unlock_index
792 return
793 }
794
795 # -- A message is required.
796 #
797 set msg [string trim [$ui_comm get 1.0 end]]
798 if {$msg eq {}} {
799 error_popup {Please supply a commit message.
800
801A good commit message has the following format:
802
803- First line: Describe in one sentance what you did.
804- Second line: Blank
805- Remaining lines: Describe why this change is good.
806}
807 unlock_index
808 return
809 }
810
811 # -- Update included files if partialincludes are off.
812 #
813 if {$repo_config(gui.partialinclude) ne {true}} {
814 set pathList [list]
815 foreach path [array names file_states] {
816 switch -glob -- [lindex $file_states($path) 0] {
817 A? -
818 M? {lappend pathList $path}
819 }
820 }
821 if {$pathList ne {}} {
822 unlock_index
823 update_index \
824 "Updating included files" \
825 $pathList \
826 [concat {lock_index update;} \
827 [list commit_prehook $curHEAD $msg]]
828 return
829 }
830 }
831
832 commit_prehook $curHEAD $msg
833}
834
835proc commit_prehook {curHEAD msg} {
836 global tcl_platform gitdir ui_status_value pch_error
837
838 # On Cygwin [file executable] might lie so we need to ask
839 # the shell if the hook is executable. Yes that's annoying.
840
841 set pchook [file join $gitdir hooks pre-commit]
842 if {$tcl_platform(platform) eq {windows}
843 && [file isfile $pchook]} {
844 set pchook [list sh -c [concat \
845 "if test -x \"$pchook\";" \
846 "then exec \"$pchook\" 2>&1;" \
847 "fi"]]
848 } elseif {[file executable $pchook]} {
849 set pchook [list $pchook |& cat]
850 } else {
851 commit_writetree $curHEAD $msg
852 return
853 }
854
855 set ui_status_value {Calling pre-commit hook...}
856 set pch_error {}
857 set fd_ph [open "| $pchook" r]
858 fconfigure $fd_ph -blocking 0 -translation binary
859 fileevent $fd_ph readable \
860 [list commit_prehook_wait $fd_ph $curHEAD $msg]
861}
862
863proc commit_prehook_wait {fd_ph curHEAD msg} {
864 global pch_error ui_status_value
865
866 append pch_error [read $fd_ph]
867 fconfigure $fd_ph -blocking 1
868 if {[eof $fd_ph]} {
869 if {[catch {close $fd_ph}]} {
870 set ui_status_value {Commit declined by pre-commit hook.}
871 hook_failed_popup pre-commit $pch_error
872 unlock_index
873 } else {
874 commit_writetree $curHEAD $msg
875 }
876 set pch_error {}
877 return
878 }
879 fconfigure $fd_ph -blocking 0
880}
881
882proc commit_writetree {curHEAD msg} {
883 global ui_status_value
884
885 set ui_status_value {Committing changes...}
886 set fd_wt [open "| git write-tree" r]
887 fileevent $fd_wt readable \
888 [list commit_committree $fd_wt $curHEAD $msg]
889}
890
891proc commit_committree {fd_wt curHEAD msg} {
892 global single_commit gitdir HEAD PARENT commit_type tcl_platform
893 global ui_status_value ui_comm selected_commit_type
894 global file_states selected_paths
895
896 gets $fd_wt tree_id
897 if {$tree_id eq {} || [catch {close $fd_wt} err]} {
898 error_popup "write-tree failed:\n\n$err"
899 set ui_status_value {Commit failed.}
900 unlock_index
901 return
902 }
903
904 # -- Create the commit.
905 #
906 set cmd [list git commit-tree $tree_id]
907 if {$PARENT ne {}} {
908 lappend cmd -p $PARENT
909 }
910 if {$commit_type eq {merge}} {
911 if {[catch {
912 set fd_mh [open [file join $gitdir MERGE_HEAD] r]
913 while {[gets $fd_mh merge_head] >= 0} {
914 lappend cmd -p $merge_head
915 }
916 close $fd_mh
917 } err]} {
918 error_popup "Loading MERGE_HEAD failed:\n\n$err"
919 set ui_status_value {Commit failed.}
920 unlock_index
921 return
922 }
923 }
924 if {$PARENT eq {}} {
925 # git commit-tree writes to stderr during initial commit.
926 lappend cmd 2>/dev/null
927 }
928 lappend cmd << $msg
929 if {[catch {set cmt_id [eval exec $cmd]} err]} {
930 error_popup "commit-tree failed:\n\n$err"
931 set ui_status_value {Commit failed.}
932 unlock_index
933 return
934 }
935
936 # -- Update the HEAD ref.
937 #
938 set reflogm commit
939 if {$commit_type ne {normal}} {
940 append reflogm " ($commit_type)"
941 }
942 set i [string first "\n" $msg]
943 if {$i >= 0} {
944 append reflogm {: } [string range $msg 0 [expr {$i - 1}]]
945 } else {
946 append reflogm {: } $msg
947 }
948 set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD]
949 if {[catch {eval exec $cmd} err]} {
950 error_popup "update-ref failed:\n\n$err"
951 set ui_status_value {Commit failed.}
952 unlock_index
953 return
954 }
955
956 # -- Cleanup after ourselves.
957 #
958 catch {file delete [file join $gitdir MERGE_HEAD]}
959 catch {file delete [file join $gitdir MERGE_MSG]}
960 catch {file delete [file join $gitdir SQUASH_MSG]}
961 catch {file delete [file join $gitdir GITGUI_MSG]}
962
963 # -- Let rerere do its thing.
964 #
965 if {[file isdirectory [file join $gitdir rr-cache]]} {
966 catch {exec git rerere}
967 }
968
969 # -- Run the post-commit hook.
970 #
971 set pchook [file join $gitdir hooks post-commit]
972 if {$tcl_platform(platform) eq {windows} && [file isfile $pchook]} {
973 set pchook [list sh -c [concat \
974 "if test -x \"$pchook\";" \
975 "then exec \"$pchook\";" \
976 "fi"]]
977 } elseif {![file executable $pchook]} {
978 set pchook {}
979 }
980 if {$pchook ne {}} {
981 catch {exec $pchook &}
982 }
983
984 $ui_comm delete 0.0 end
985 $ui_comm edit modified false
986 $ui_comm edit reset
987
988 if {$single_commit} do_quit
989
990 # -- Update status without invoking any git commands.
991 #
992 set commit_type normal
993 set selected_commit_type new
994 set HEAD $cmt_id
995 set PARENT $cmt_id
996
997 foreach path [array names file_states] {
998 set s $file_states($path)
999 set m [lindex $s 0]
1000 switch -glob -- $m {
1001 A? -
1002 M? -
1003 D? {set m _[string index $m 1]}
1004 }
1005
1006 if {$m eq {__}} {
1007 unset file_states($path)
1008 catch {unset selected_paths($path)}
1009 } else {
1010 lset file_states($path) 0 $m
1011 }
1012 }
1013
1014 display_all_files
1015 unlock_index
1016 reshow_diff
1017 set ui_status_value \
1018 "Changes committed as [string range $cmt_id 0 7]."
1019}
1020
1021######################################################################
1022##
1023## fetch pull push
1024
1025proc fetch_from {remote} {
1026 set w [new_console "fetch $remote" \
1027 "Fetching new changes from $remote"]
1028 set cmd [list git fetch]
1029 lappend cmd $remote
1030 console_exec $w $cmd
1031}
1032
1033proc pull_remote {remote branch} {
1034 global HEAD commit_type file_states repo_config
1035
1036 if {![lock_index update]} return
1037
1038 # -- Our in memory state should match the repository.
1039 #
1040 repository_state curHEAD cur_type
1041 if {$commit_type ne $cur_type || $HEAD ne $curHEAD} {
1042 error_popup {Last scanned state does not match repository state.
1043
1044Its highly likely that another Git program modified the
1045repository since our last scan. A rescan is required
1046before a pull can be started.
1047}
1048 unlock_index
1049 rescan {set ui_status_value {Ready.}}
1050 return
1051 }
1052
1053 # -- No differences should exist before a pull.
1054 #
1055 if {[array size file_states] != 0} {
1056 error_popup {Uncommitted but modified files are present.
1057
1058You should not perform a pull with unmodified files in your working
1059directory as Git would be unable to recover from an incorrect merge.
1060
1061Commit or throw away all changes before starting a pull operation.
1062}
1063 unlock_index
1064 return
1065 }
1066
1067 set w [new_console "pull $remote $branch" \
1068 "Pulling new changes from branch $branch in $remote"]
1069 set cmd [list git pull]
1070 if {$repo_config(gui.pullsummary) eq {false}} {
1071 lappend cmd --no-summary
1072 }
1073 lappend cmd $remote
1074 lappend cmd $branch
1075 console_exec $w $cmd [list post_pull_remote $remote $branch]
1076}
1077
1078proc post_pull_remote {remote branch success} {
1079 global HEAD PARENT commit_type selected_commit_type
1080 global ui_status_value
1081
1082 unlock_index
1083 if {$success} {
1084 repository_state HEAD commit_type
1085 set PARENT $HEAD
1086 set selected_commit_type new
1087 set $ui_status_value "Pulling $branch from $remote complete."
1088 } else {
1089 set m "Conflicts detected while pulling $branch from $remote."
1090 rescan "set ui_status_value {$m}"
1091 }
1092}
1093
1094proc push_to {remote} {
1095 set w [new_console "push $remote" \
1096 "Pushing changes to $remote"]
1097 set cmd [list git push]
1098 lappend cmd $remote
1099 console_exec $w $cmd
1100}
1101
1102######################################################################
1103##
1104## ui helpers
1105
1106proc mapcol {state path} {
1107 global all_cols ui_other
1108
1109 if {[catch {set r $all_cols($state)}]} {
1110 puts "error: no column for state={$state} $path"
1111 return $ui_other
1112 }
1113 return $r
1114}
1115
1116proc mapicon {state path} {
1117 global all_icons
1118
1119 if {[catch {set r $all_icons($state)}]} {
1120 puts "error: no icon for state={$state} $path"
1121 return file_plain
1122 }
1123 return $r
1124}
1125
1126proc mapdesc {state path} {
1127 global all_descs
1128
1129 if {[catch {set r $all_descs($state)}]} {
1130 puts "error: no desc for state={$state} $path"
1131 return $state
1132 }
1133 return $r
1134}
1135
1136proc escape_path {path} {
1137 regsub -all "\n" $path "\\n" path
1138 return $path
1139}
1140
1141proc short_path {path} {
1142 return [escape_path [lindex [file split $path] end]]
1143}
1144
1145set next_icon_id 0
1146
1147proc merge_state {path new_state} {
1148 global file_states next_icon_id
1149
1150 set s0 [string index $new_state 0]
1151 set s1 [string index $new_state 1]
1152
1153 if {[catch {set info $file_states($path)}]} {
1154 set state __
1155 set icon n[incr next_icon_id]
1156 } else {
1157 set state [lindex $info 0]
1158 set icon [lindex $info 1]
1159 }
1160
1161 if {$s0 eq {?}} {
1162 set s0 [string index $state 0]
1163 } elseif {$s0 eq {_}} {
1164 set s0 _
1165 }
1166
1167 if {$s1 eq {?}} {
1168 set s1 [string index $state 1]
1169 } elseif {$s1 eq {_}} {
1170 set s1 _
1171 }
1172
1173 set file_states($path) [list $s0$s1 $icon]
1174 return $state
1175}
1176
1177proc display_file {path state} {
1178 global file_states file_lists selected_paths rescan_active
1179
1180 set old_m [merge_state $path $state]
1181 if {$rescan_active > 0} return
1182
1183 set s $file_states($path)
1184 set new_m [lindex $s 0]
1185 set new_w [mapcol $new_m $path]
1186 set old_w [mapcol $old_m $path]
1187 set new_icon [mapicon $new_m $path]
1188
1189 if {$new_w ne $old_w} {
1190 set lno [lsearch -sorted $file_lists($old_w) $path]
1191 if {$lno >= 0} {
1192 incr lno
1193 $old_w conf -state normal
1194 $old_w delete $lno.0 [expr {$lno + 1}].0
1195 $old_w conf -state disabled
1196 }
1197
1198 lappend file_lists($new_w) $path
1199 set file_lists($new_w) [lsort $file_lists($new_w)]
1200 set lno [lsearch -sorted $file_lists($new_w) $path]
1201 incr lno
1202 $new_w conf -state normal
1203 $new_w image create $lno.0 \
1204 -align center -padx 5 -pady 1 \
1205 -name [lindex $s 1] \
1206 -image $new_icon
1207 $new_w insert $lno.1 "[escape_path $path]\n"
1208 if {[catch {set in_sel $selected_paths($path)}]} {
1209 set in_sel 0
1210 }
1211 if {$in_sel} {
1212 $new_w tag add in_sel $lno.0 [expr {$lno + 1}].0
1213 }
1214 $new_w conf -state disabled
1215 } elseif {$new_icon ne [mapicon $old_m $path]} {
1216 $new_w conf -state normal
1217 $new_w image conf [lindex $s 1] -image $new_icon
1218 $new_w conf -state disabled
1219 }
1220}
1221
1222proc display_all_files {} {
1223 global ui_index ui_other
1224 global file_states file_lists
1225 global last_clicked selected_paths
1226
1227 $ui_index conf -state normal
1228 $ui_other conf -state normal
1229
1230 $ui_index delete 0.0 end
1231 $ui_other delete 0.0 end
1232 set last_clicked {}
1233
1234 set file_lists($ui_index) [list]
1235 set file_lists($ui_other) [list]
1236
1237 foreach path [lsort [array names file_states]] {
1238 set s $file_states($path)
1239 set m [lindex $s 0]
1240 set w [mapcol $m $path]
1241 lappend file_lists($w) $path
1242 set lno [expr {[lindex [split [$w index end] .] 0] - 1}]
1243 $w image create end \
1244 -align center -padx 5 -pady 1 \
1245 -name [lindex $s 1] \
1246 -image [mapicon $m $path]
1247 $w insert end "[escape_path $path]\n"
1248 if {[catch {set in_sel $selected_paths($path)}]} {
1249 set in_sel 0
1250 }
1251 if {$in_sel} {
1252 $w tag add in_sel $lno.0 [expr {$lno + 1}].0
1253 }
1254 }
1255
1256 $ui_index conf -state disabled
1257 $ui_other conf -state disabled
1258}
1259
1260proc update_index {msg pathList after} {
1261 global update_index_cp ui_status_value
1262
1263 if {![lock_index update]} return
1264
1265 set update_index_cp 0
1266 set pathList [lsort $pathList]
1267 set totalCnt [llength $pathList]
1268 set batch [expr {int($totalCnt * .01) + 1}]
1269 if {$batch > 25} {set batch 25}
1270
1271 set ui_status_value [format \
1272 "$msg... %i/%i files (%.2f%%)" \
1273 $update_index_cp \
1274 $totalCnt \
1275 0.0]
1276 set fd [open "| git update-index --add --remove -z --stdin" w]
1277 fconfigure $fd \
1278 -blocking 0 \
1279 -buffering full \
1280 -buffersize 512 \
1281 -translation binary
1282 fileevent $fd writable [list \
1283 write_update_index \
1284 $fd \
1285 $pathList \
1286 $totalCnt \
1287 $batch \
1288 $msg \
1289 $after \
1290 ]
1291}
1292
1293proc write_update_index {fd pathList totalCnt batch msg after} {
1294 global update_index_cp ui_status_value
1295 global file_states current_diff
1296
1297 if {$update_index_cp >= $totalCnt} {
1298 close $fd
1299 unlock_index
1300 uplevel #0 $after
1301 return
1302 }
1303
1304 for {set i $batch} \
1305 {$update_index_cp < $totalCnt && $i > 0} \
1306 {incr i -1} {
1307 set path [lindex $pathList $update_index_cp]
1308 incr update_index_cp
1309
1310 switch -glob -- [lindex $file_states($path) 0] {
1311 AD -
1312 MD -
1313 _D {set new D_}
1314
1315 _M -
1316 MM -
1317 M_ {set new M_}
1318
1319 _O -
1320 AM -
1321 A_ {set new A_}
1322
1323 ?? {continue}
1324 }
1325
1326 puts -nonewline $fd $path
1327 puts -nonewline $fd "\0"
1328 display_file $path $new
1329 }
1330
1331 set ui_status_value [format \
1332 "$msg... %i/%i files (%.2f%%)" \
1333 $update_index_cp \
1334 $totalCnt \
1335 [expr {100.0 * $update_index_cp / $totalCnt}]]
1336}
1337
1338######################################################################
1339##
1340## remote management
1341
1342proc load_all_remotes {} {
1343 global gitdir all_remotes repo_config
1344
1345 set all_remotes [list]
1346 set rm_dir [file join $gitdir remotes]
1347 if {[file isdirectory $rm_dir]} {
1348 set all_remotes [concat $all_remotes [glob \
1349 -types f \
1350 -tails \
1351 -nocomplain \
1352 -directory $rm_dir *]]
1353 }
1354
1355 foreach line [array names repo_config remote.*.url] {
1356 if {[regexp ^remote\.(.*)\.url\$ $line line name]} {
1357 lappend all_remotes $name
1358 }
1359 }
1360
1361 set all_remotes [lsort -unique $all_remotes]
1362}
1363
1364proc populate_fetch_menu {m} {
1365 global gitdir all_remotes repo_config
1366
1367 foreach r $all_remotes {
1368 set enable 0
1369 if {![catch {set a $repo_config(remote.$r.url)}]} {
1370 if {![catch {set a $repo_config(remote.$r.fetch)}]} {
1371 set enable 1
1372 }
1373 } else {
1374 catch {
1375 set fd [open [file join $gitdir remotes $r] r]
1376 while {[gets $fd n] >= 0} {
1377 if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
1378 set enable 1
1379 break
1380 }
1381 }
1382 close $fd
1383 }
1384 }
1385
1386 if {$enable} {
1387 $m add command \
1388 -label "Fetch from $r..." \
1389 -command [list fetch_from $r] \
1390 -font font_ui
1391 }
1392 }
1393}
1394
1395proc populate_push_menu {m} {
1396 global gitdir all_remotes repo_config
1397
1398 foreach r $all_remotes {
1399 set enable 0
1400 if {![catch {set a $repo_config(remote.$r.url)}]} {
1401 if {![catch {set a $repo_config(remote.$r.push)}]} {
1402 set enable 1
1403 }
1404 } else {
1405 catch {
1406 set fd [open [file join $gitdir remotes $r] r]
1407 while {[gets $fd n] >= 0} {
1408 if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
1409 set enable 1
1410 break
1411 }
1412 }
1413 close $fd
1414 }
1415 }
1416
1417 if {$enable} {
1418 $m add command \
1419 -label "Push to $r..." \
1420 -command [list push_to $r] \
1421 -font font_ui
1422 }
1423 }
1424}
1425
1426proc populate_pull_menu {m} {
1427 global gitdir repo_config all_remotes disable_on_lock
1428
1429 foreach remote $all_remotes {
1430 set rb {}
1431 if {[array get repo_config remote.$remote.url] ne {}} {
1432 if {[array get repo_config remote.$remote.fetch] ne {}} {
1433 regexp {^([^:]+):} \
1434 [lindex $repo_config(remote.$remote.fetch) 0] \
1435 line rb
1436 }
1437 } else {
1438 catch {
1439 set fd [open [file join $gitdir remotes $remote] r]
1440 while {[gets $fd line] >= 0} {
1441 if {[regexp {^Pull:[ \t]*([^:]+):} $line line rb]} {
1442 break
1443 }
1444 }
1445 close $fd
1446 }
1447 }
1448
1449 set rb_short $rb
1450 regsub ^refs/heads/ $rb {} rb_short
1451 if {$rb_short ne {}} {
1452 $m add command \
1453 -label "Branch $rb_short from $remote..." \
1454 -command [list pull_remote $remote $rb] \
1455 -font font_ui
1456 lappend disable_on_lock \
1457 [list $m entryconf [$m index last] -state]
1458 }
1459 }
1460}
1461
1462######################################################################
1463##
1464## icons
1465
1466set filemask {
1467#define mask_width 14
1468#define mask_height 15
1469static unsigned char mask_bits[] = {
1470 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1471 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1472 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
1473}
1474
1475image create bitmap file_plain -background white -foreground black -data {
1476#define plain_width 14
1477#define plain_height 15
1478static unsigned char plain_bits[] = {
1479 0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1480 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
1481 0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1482} -maskdata $filemask
1483
1484image create bitmap file_mod -background white -foreground blue -data {
1485#define mod_width 14
1486#define mod_height 15
1487static unsigned char mod_bits[] = {
1488 0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1489 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1490 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1491} -maskdata $filemask
1492
1493image create bitmap file_fulltick -background white -foreground "#007000" -data {
1494#define file_fulltick_width 14
1495#define file_fulltick_height 15
1496static unsigned char file_fulltick_bits[] = {
1497 0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
1498 0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
1499 0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1500} -maskdata $filemask
1501
1502image create bitmap file_parttick -background white -foreground "#005050" -data {
1503#define parttick_width 14
1504#define parttick_height 15
1505static unsigned char parttick_bits[] = {
1506 0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1507 0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
1508 0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1509} -maskdata $filemask
1510
1511image create bitmap file_question -background white -foreground black -data {
1512#define file_question_width 14
1513#define file_question_height 15
1514static unsigned char file_question_bits[] = {
1515 0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
1516 0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
1517 0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1518} -maskdata $filemask
1519
1520image create bitmap file_removed -background white -foreground red -data {
1521#define file_removed_width 14
1522#define file_removed_height 15
1523static unsigned char file_removed_bits[] = {
1524 0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1525 0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
1526 0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
1527} -maskdata $filemask
1528
1529image create bitmap file_merge -background white -foreground blue -data {
1530#define file_merge_width 14
1531#define file_merge_height 15
1532static unsigned char file_merge_bits[] = {
1533 0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
1534 0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1535 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1536} -maskdata $filemask
1537
1538set ui_index .vpane.files.index.list
1539set ui_other .vpane.files.other.list
1540set max_status_desc 0
1541foreach i {
1542 {__ i plain "Unmodified"}
1543 {_M i mod "Modified"}
1544 {M_ i fulltick "Included in commit"}
1545 {MM i parttick "Partially included"}
1546
1547 {_O o plain "Untracked"}
1548 {A_ o fulltick "Added by commit"}
1549 {AM o parttick "Partially added"}
1550 {AD o question "Added (but now gone)"}
1551
1552 {_D i question "Missing"}
1553 {D_ i removed "Removed by commit"}
1554 {DD i removed "Removed by commit"}
1555 {DO i removed "Removed (still exists)"}
1556
1557 {UM i merge "Merge conflicts"}
1558 {U_ i merge "Merge conflicts"}
1559 } {
1560 if {$max_status_desc < [string length [lindex $i 3]]} {
1561 set max_status_desc [string length [lindex $i 3]]
1562 }
1563 if {[lindex $i 1] eq {i}} {
1564 set all_cols([lindex $i 0]) $ui_index
1565 } else {
1566 set all_cols([lindex $i 0]) $ui_other
1567 }
1568 set all_icons([lindex $i 0]) file_[lindex $i 2]
1569 set all_descs([lindex $i 0]) [lindex $i 3]
1570}
1571unset filemask i
1572
1573######################################################################
1574##
1575## util
1576
1577proc is_MacOSX {} {
1578 global tcl_platform tk_library
1579 if {$tcl_platform(platform) eq {unix}
1580 && $tcl_platform(os) eq {Darwin}
1581 && [string match /Library/Frameworks/* $tk_library]} {
1582 return 1
1583 }
1584 return 0
1585}
1586
1587proc bind_button3 {w cmd} {
1588 bind $w <Any-Button-3> $cmd
1589 if {[is_MacOSX]} {
1590 bind $w <Control-Button-1> $cmd
1591 }
1592}
1593
1594proc incr_font_size {font {amt 1}} {
1595 set sz [font configure $font -size]
1596 incr sz $amt
1597 font configure $font -size $sz
1598 font configure ${font}bold -size $sz
1599}
1600
1601proc hook_failed_popup {hook msg} {
1602 global gitdir appname
1603
1604 set w .hookfail
1605 toplevel $w
1606
1607 frame $w.m
1608 label $w.m.l1 -text "$hook hook failed:" \
1609 -anchor w \
1610 -justify left \
1611 -font font_uibold
1612 text $w.m.t \
1613 -background white -borderwidth 1 \
1614 -relief sunken \
1615 -width 80 -height 10 \
1616 -font font_diff \
1617 -yscrollcommand [list $w.m.sby set]
1618 label $w.m.l2 \
1619 -text {You must correct the above errors before committing.} \
1620 -anchor w \
1621 -justify left \
1622 -font font_uibold
1623 scrollbar $w.m.sby -command [list $w.m.t yview]
1624 pack $w.m.l1 -side top -fill x
1625 pack $w.m.l2 -side bottom -fill x
1626 pack $w.m.sby -side right -fill y
1627 pack $w.m.t -side left -fill both -expand 1
1628 pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
1629
1630 $w.m.t insert 1.0 $msg
1631 $w.m.t conf -state disabled
1632
1633 button $w.ok -text OK \
1634 -width 15 \
1635 -font font_ui \
1636 -command "destroy $w"
1637 pack $w.ok -side bottom -anchor e -pady 10 -padx 10
1638
1639 bind $w <Visibility> "grab $w; focus $w"
1640 bind $w <Key-Return> "destroy $w"
1641 wm title $w "$appname ([lindex [file split \
1642 [file normalize [file dirname $gitdir]]] \
1643 end]): error"
1644 tkwait window $w
1645}
1646
1647set next_console_id 0
1648
1649proc new_console {short_title long_title} {
1650 global next_console_id console_data
1651 set w .console[incr next_console_id]
1652 set console_data($w) [list $short_title $long_title]
1653 return [console_init $w]
1654}
1655
1656proc console_init {w} {
1657 global console_cr console_data
1658 global gitdir appname M1B
1659
1660 set console_cr($w) 1.0
1661 toplevel $w
1662 frame $w.m
1663 label $w.m.l1 -text "[lindex $console_data($w) 1]:" \
1664 -anchor w \
1665 -justify left \
1666 -font font_uibold
1667 text $w.m.t \
1668 -background white -borderwidth 1 \
1669 -relief sunken \
1670 -width 80 -height 10 \
1671 -font font_diff \
1672 -state disabled \
1673 -yscrollcommand [list $w.m.sby set]
1674 label $w.m.s -text {Working... please wait...} \
1675 -anchor w \
1676 -justify left \
1677 -font font_uibold
1678 scrollbar $w.m.sby -command [list $w.m.t yview]
1679 pack $w.m.l1 -side top -fill x
1680 pack $w.m.s -side bottom -fill x
1681 pack $w.m.sby -side right -fill y
1682 pack $w.m.t -side left -fill both -expand 1
1683 pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
1684
1685 menu $w.ctxm -tearoff 0
1686 $w.ctxm add command -label "Copy" \
1687 -font font_ui \
1688 -command "tk_textCopy $w.m.t"
1689 $w.ctxm add command -label "Select All" \
1690 -font font_ui \
1691 -command "$w.m.t tag add sel 0.0 end"
1692 $w.ctxm add command -label "Copy All" \
1693 -font font_ui \
1694 -command "
1695 $w.m.t tag add sel 0.0 end
1696 tk_textCopy $w.m.t
1697 $w.m.t tag remove sel 0.0 end
1698 "
1699
1700 button $w.ok -text {Close} \
1701 -font font_ui \
1702 -state disabled \
1703 -command "destroy $w"
1704 pack $w.ok -side bottom -anchor e -pady 10 -padx 10
1705
1706 bind_button3 $w.m.t "tk_popup $w.ctxm %X %Y"
1707 bind $w.m.t <$M1B-Key-a> "$w.m.t tag add sel 0.0 end;break"
1708 bind $w.m.t <$M1B-Key-A> "$w.m.t tag add sel 0.0 end;break"
1709 bind $w <Visibility> "focus $w"
1710 wm title $w "$appname ([lindex [file split \
1711 [file normalize [file dirname $gitdir]]] \
1712 end]): [lindex $console_data($w) 0]"
1713 return $w
1714}
1715
1716proc console_exec {w cmd {after {}}} {
1717 global tcl_platform
1718
1719 # -- Windows tosses the enviroment when we exec our child.
1720 # But most users need that so we have to relogin. :-(
1721 #
1722 if {$tcl_platform(platform) eq {windows}} {
1723 set cmd [list sh --login -c "cd \"[pwd]\" && [join $cmd { }]"]
1724 }
1725
1726 # -- Tcl won't let us redirect both stdout and stderr to
1727 # the same pipe. So pass it through cat...
1728 #
1729 set cmd [concat | $cmd |& cat]
1730
1731 set fd_f [open $cmd r]
1732 fconfigure $fd_f -blocking 0 -translation binary
1733 fileevent $fd_f readable [list console_read $w $fd_f $after]
1734}
1735
1736proc console_read {w fd after} {
1737 global console_cr console_data
1738
1739 set buf [read $fd]
1740 if {$buf ne {}} {
1741 if {![winfo exists $w]} {console_init $w}
1742 $w.m.t conf -state normal
1743 set c 0
1744 set n [string length $buf]
1745 while {$c < $n} {
1746 set cr [string first "\r" $buf $c]
1747 set lf [string first "\n" $buf $c]
1748 if {$cr < 0} {set cr [expr {$n + 1}]}
1749 if {$lf < 0} {set lf [expr {$n + 1}]}
1750
1751 if {$lf < $cr} {
1752 $w.m.t insert end [string range $buf $c $lf]
1753 set console_cr($w) [$w.m.t index {end -1c}]
1754 set c $lf
1755 incr c
1756 } else {
1757 $w.m.t delete $console_cr($w) end
1758 $w.m.t insert end "\n"
1759 $w.m.t insert end [string range $buf $c $cr]
1760 set c $cr
1761 incr c
1762 }
1763 }
1764 $w.m.t conf -state disabled
1765 $w.m.t see end
1766 }
1767
1768 fconfigure $fd -blocking 1
1769 if {[eof $fd]} {
1770 if {[catch {close $fd}]} {
1771 if {![winfo exists $w]} {console_init $w}
1772 $w.m.s conf -background red -text {Error: Command Failed}
1773 $w.ok conf -state normal
1774 set ok 0
1775 } elseif {[winfo exists $w]} {
1776 $w.m.s conf -background green -text {Success}
1777 $w.ok conf -state normal
1778 set ok 1
1779 }
1780 array unset console_cr $w
1781 array unset console_data $w
1782 if {$after ne {}} {
1783 uplevel #0 $after $ok
1784 }
1785 return
1786 }
1787 fconfigure $fd -blocking 0
1788}
1789
1790######################################################################
1791##
1792## ui commands
1793
1794set starting_gitk_msg {Please wait... Starting gitk...}
1795
1796proc do_gitk {} {
1797 global tcl_platform ui_status_value starting_gitk_msg
1798
1799 set ui_status_value $starting_gitk_msg
1800 after 10000 {
1801 if {$ui_status_value eq $starting_gitk_msg} {
1802 set ui_status_value {Ready.}
1803 }
1804 }
1805
1806 if {$tcl_platform(platform) eq {windows}} {
1807 exec sh -c gitk &
1808 } else {
1809 exec gitk &
1810 }
1811}
1812
1813proc do_repack {} {
1814 set w [new_console "repack" "Repacking the object database"]
1815 set cmd [list git repack]
1816 lappend cmd -a
1817 lappend cmd -d
1818 console_exec $w $cmd
1819}
1820
1821set is_quitting 0
1822
1823proc do_quit {} {
1824 global gitdir ui_comm is_quitting repo_config
1825
1826 if {$is_quitting} return
1827 set is_quitting 1
1828
1829 # -- Stash our current commit buffer.
1830 #
1831 set save [file join $gitdir GITGUI_MSG]
1832 set msg [string trim [$ui_comm get 0.0 end]]
1833 if {[$ui_comm edit modified] && $msg ne {}} {
1834 catch {
1835 set fd [open $save w]
1836 puts $fd [string trim [$ui_comm get 0.0 end]]
1837 close $fd
1838 }
1839 } elseif {$msg eq {} && [file exists $save]} {
1840 file delete $save
1841 }
1842
1843 # -- Stash our current window geometry into this repository.
1844 #
1845 set cfg_geometry [list]
1846 lappend cfg_geometry [wm geometry .]
1847 lappend cfg_geometry [lindex [.vpane sash coord 0] 1]
1848 lappend cfg_geometry [lindex [.vpane.files sash coord 0] 0]
1849 if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
1850 set rc_geometry {}
1851 }
1852 if {$cfg_geometry ne $rc_geometry} {
1853 catch {exec git repo-config gui.geometry $cfg_geometry}
1854 }
1855
1856 destroy .
1857}
1858
1859proc do_rescan {} {
1860 rescan {set ui_status_value {Ready.}}
1861}
1862
1863proc include_helper {txt paths} {
1864 global file_states current_diff
1865
1866 if {![lock_index begin-update]} return
1867
1868 set pathList [list]
1869 set after {}
1870 foreach path $paths {
1871 switch -- [lindex $file_states($path) 0] {
1872 AM -
1873 MM -
1874 _M -
1875 _D {
1876 lappend pathList $path
1877 if {$path eq $current_diff} {
1878 set after {reshow_diff;}
1879 }
1880 }
1881 }
1882 }
1883 if {$pathList eq {}} {
1884 unlock_index
1885 } else {
1886 update_index \
1887 $txt \
1888 $pathList \
1889 [concat $after {set ui_status_value {Ready to commit.}}]
1890 }
1891}
1892
1893proc do_include_selection {} {
1894 global current_diff selected_paths
1895
1896 if {[array size selected_paths] > 0} {
1897 include_helper \
1898 {Including selected files} \
1899 [array names selected_paths]
1900 } elseif {$current_diff ne {}} {
1901 include_helper \
1902 "Including [short_path $current_diff]" \
1903 [list $current_diff]
1904 }
1905}
1906
1907proc do_include_all {} {
1908 global file_states
1909 include_helper \
1910 {Including all modified files} \
1911 [array names file_states]
1912}
1913
1914set GIT_COMMITTER_IDENT {}
1915
1916proc do_signoff {} {
1917 global ui_comm GIT_COMMITTER_IDENT
1918
1919 if {$GIT_COMMITTER_IDENT eq {}} {
1920 if {[catch {set me [exec git var GIT_COMMITTER_IDENT]} err]} {
1921 error_popup "Unable to obtain your identity:\n\n$err"
1922 return
1923 }
1924 if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
1925 $me me GIT_COMMITTER_IDENT]} {
1926 error_popup "Invalid GIT_COMMITTER_IDENT:\n\n$me"
1927 return
1928 }
1929 }
1930
1931 set sob "Signed-off-by: $GIT_COMMITTER_IDENT"
1932 set last [$ui_comm get {end -1c linestart} {end -1c}]
1933 if {$last ne $sob} {
1934 $ui_comm edit separator
1935 if {$last ne {}
1936 && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
1937 $ui_comm insert end "\n"
1938 }
1939 $ui_comm insert end "\n$sob"
1940 $ui_comm edit separator
1941 $ui_comm see end
1942 }
1943}
1944
1945proc do_select_commit_type {} {
1946 global commit_type selected_commit_type
1947
1948 if {$selected_commit_type eq {new}
1949 && [string match amend* $commit_type]} {
1950 create_new_commit
1951 } elseif {$selected_commit_type eq {amend}
1952 && ![string match amend* $commit_type]} {
1953 load_last_commit
1954
1955 # The amend request was rejected...
1956 #
1957 if {![string match amend* $commit_type]} {
1958 set commit_type new
1959 }
1960 }
1961}
1962
1963proc do_commit {} {
1964 commit_tree
1965}
1966
1967proc do_options {} {
1968 global appname gitdir font_descs
1969 global repo_config global_config
1970 global repo_config_new global_config_new
1971
1972 array unset repo_config_new
1973 array unset global_config_new
1974 foreach name [array names repo_config] {
1975 set repo_config_new($name) $repo_config($name)
1976 }
1977 load_config 1
1978 foreach name [array names repo_config] {
1979 switch -- $name {
1980 gui.diffcontext {continue}
1981 }
1982 set repo_config_new($name) $repo_config($name)
1983 }
1984 foreach name [array names global_config] {
1985 set global_config_new($name) $global_config($name)
1986 }
1987 set reponame [lindex [file split \
1988 [file normalize [file dirname $gitdir]]] \
1989 end]
1990
1991 set w .options_editor
1992 toplevel $w
1993 wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
1994
1995 label $w.header -text "$appname Options" \
1996 -font font_uibold
1997 pack $w.header -side top -fill x
1998
1999 frame $w.buttons
2000 button $w.buttons.restore -text {Restore Defaults} \
2001 -font font_ui \
2002 -command do_restore_defaults
2003 pack $w.buttons.restore -side left
2004 button $w.buttons.save -text Save \
2005 -font font_ui \
2006 -command [list do_save_config $w]
2007 pack $w.buttons.save -side right
2008 button $w.buttons.cancel -text {Cancel} \
2009 -font font_ui \
2010 -command [list destroy $w]
2011 pack $w.buttons.cancel -side right
2012 pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2013
2014 labelframe $w.repo -text "$reponame Repository" \
2015 -font font_ui \
2016 -relief raised -borderwidth 2
2017 labelframe $w.global -text {Global (All Repositories)} \
2018 -font font_ui \
2019 -relief raised -borderwidth 2
2020 pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
2021 pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
2022
2023 foreach option {
2024 {b partialinclude {Allow Partially Included Files}}
2025 {b pullsummary {Show Pull Summary}}
2026 {b trustmtime {Trust File Modification Timestamps}}
2027 {i diffcontext {Number of Diff Context Lines}}
2028 } {
2029 set type [lindex $option 0]
2030 set name [lindex $option 1]
2031 set text [lindex $option 2]
2032 foreach f {repo global} {
2033 switch $type {
2034 b {
2035 checkbutton $w.$f.$name -text $text \
2036 -variable ${f}_config_new(gui.$name) \
2037 -onvalue true \
2038 -offvalue false \
2039 -font font_ui
2040 pack $w.$f.$name -side top -anchor w
2041 }
2042 i {
2043 frame $w.$f.$name
2044 label $w.$f.$name.l -text "$text:" -font font_ui
2045 pack $w.$f.$name.l -side left -anchor w -fill x
2046 spinbox $w.$f.$name.v \
2047 -textvariable ${f}_config_new(gui.$name) \
2048 -from 1 -to 99 -increment 1 \
2049 -width 3 \
2050 -font font_ui
2051 pack $w.$f.$name.v -side right -anchor e
2052 pack $w.$f.$name -side top -anchor w -fill x
2053 }
2054 }
2055 }
2056 }
2057
2058 set all_fonts [lsort [font families]]
2059 foreach option $font_descs {
2060 set name [lindex $option 0]
2061 set font [lindex $option 1]
2062 set text [lindex $option 2]
2063
2064 set global_config_new(gui.$font^^family) \
2065 [font configure $font -family]
2066 set global_config_new(gui.$font^^size) \
2067 [font configure $font -size]
2068
2069 frame $w.global.$name
2070 label $w.global.$name.l -text "$text:" -font font_ui
2071 pack $w.global.$name.l -side left -anchor w -fill x
2072 eval tk_optionMenu $w.global.$name.family \
2073 global_config_new(gui.$font^^family) \
2074 $all_fonts
2075 spinbox $w.global.$name.size \
2076 -textvariable global_config_new(gui.$font^^size) \
2077 -from 2 -to 80 -increment 1 \
2078 -width 3 \
2079 -font font_ui
2080 pack $w.global.$name.size -side right -anchor e
2081 pack $w.global.$name.family -side right -anchor e
2082 pack $w.global.$name -side top -anchor w -fill x
2083 }
2084
2085 bind $w <Visibility> "grab $w; focus $w"
2086 bind $w <Key-Escape> "destroy $w"
2087 wm title $w "$appname ($reponame): Options"
2088 tkwait window $w
2089}
2090
2091proc do_restore_defaults {} {
2092 global font_descs default_config repo_config
2093 global repo_config_new global_config_new
2094
2095 foreach name [array names default_config] {
2096 set repo_config_new($name) $default_config($name)
2097 set global_config_new($name) $default_config($name)
2098 }
2099
2100 foreach option $font_descs {
2101 set name [lindex $option 0]
2102 set repo_config(gui.$name) $default_config(gui.$name)
2103 }
2104 apply_config
2105
2106 foreach option $font_descs {
2107 set name [lindex $option 0]
2108 set font [lindex $option 1]
2109 set global_config_new(gui.$font^^family) \
2110 [font configure $font -family]
2111 set global_config_new(gui.$font^^size) \
2112 [font configure $font -size]
2113 }
2114}
2115
2116proc do_save_config {w} {
2117 if {[catch {save_config} err]} {
2118 error_popup "Failed to completely save options:\n\n$err"
2119 }
2120 reshow_diff
2121 destroy $w
2122}
2123
2124proc do_windows_shortcut {} {
2125 global gitdir appname argv0
2126
2127 set reponame [lindex [file split \
2128 [file normalize [file dirname $gitdir]]] \
2129 end]
2130
2131 if {[catch {
2132 set desktop [exec cygpath \
2133 --windows \
2134 --absolute \
2135 --long-name \
2136 --desktop]
2137 }]} {
2138 set desktop .
2139 }
2140 set fn [tk_getSaveFile \
2141 -parent . \
2142 -title "$appname ($reponame): Create Desktop Icon" \
2143 -initialdir $desktop \
2144 -initialfile "Git $reponame.bat"]
2145 if {$fn != {}} {
2146 if {[catch {
2147 set fd [open $fn w]
2148 set sh [exec cygpath \
2149 --windows \
2150 --absolute \
2151 --long-name \
2152 /bin/sh]
2153 set me [exec cygpath \
2154 --unix \
2155 --absolute \
2156 $argv0]
2157 set gd [exec cygpath \
2158 --unix \
2159 --absolute \
2160 $gitdir]
2161 regsub -all ' $me "'\\''" me
2162 regsub -all ' $gd "'\\''" gd
2163 puts -nonewline $fd "\"$sh\" --login -c \""
2164 puts -nonewline $fd "GIT_DIR='$gd'"
2165 puts -nonewline $fd " '$me'"
2166 puts $fd "&\""
2167 close $fd
2168 } err]} {
2169 error_popup "Cannot write script:\n\n$err"
2170 }
2171 }
2172}
2173
2174proc do_macosx_app {} {
2175 global gitdir appname argv0 env
2176
2177 set reponame [lindex [file split \
2178 [file normalize [file dirname $gitdir]]] \
2179 end]
2180
2181 set fn [tk_getSaveFile \
2182 -parent . \
2183 -title "$appname ($reponame): Create Desktop Icon" \
2184 -initialdir [file join $env(HOME) Desktop] \
2185 -initialfile "Git $reponame.app"]
2186 if {$fn != {}} {
2187 if {[catch {
2188 set Contents [file join $fn Contents]
2189 set MacOS [file join $Contents MacOS]
2190 set exe [file join $MacOS git-gui]
2191
2192 file mkdir $MacOS
2193
2194 set fd [open [file join $Contents PkgInfo] w]
2195 puts -nonewline $fd {APPL????}
2196 close $fd
2197
2198 set fd [open [file join $Contents Info.plist] w]
2199 puts $fd {<?xml version="1.0" encoding="UTF-8"?>
2200<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2201<plist version="1.0">
2202<dict>
2203 <key>CFBundleDevelopmentRegion</key>
2204 <string>English</string>
2205 <key>CFBundleExecutable</key>
2206 <string>git-gui</string>
2207 <key>CFBundleIdentifier</key>
2208 <string>org.spearce.git-gui</string>
2209 <key>CFBundleInfoDictionaryVersion</key>
2210 <string>6.0</string>
2211 <key>CFBundlePackageType</key>
2212 <string>APPL</string>
2213 <key>CFBundleSignature</key>
2214 <string>????</string>
2215 <key>CFBundleVersion</key>
2216 <string>1.0</string>
2217 <key>NSPrincipalClass</key>
2218 <string>NSApplication</string>
2219</dict>
2220</plist>}
2221 close $fd
2222
2223 set fd [open $exe w]
2224 set gd [file normalize $gitdir]
2225 set ep [file normalize [exec git --exec-path]]
2226 regsub -all ' $gd "'\\''" gd
2227 regsub -all ' $ep "'\\''" ep
2228 puts $fd "#!/bin/sh"
2229 foreach name [array names env] {
2230 if {[string match GIT_* $name]} {
2231 regsub -all ' $env($name) "'\\''" v
2232 puts $fd "export $name='$v'"
2233 }
2234 }
2235 puts $fd "export PATH='$ep':\$PATH"
2236 puts $fd "export GIT_DIR='$gd'"
2237 puts $fd "exec [file normalize $argv0]"
2238 close $fd
2239
2240 file attributes $exe -permissions u+x,g+x,o+x
2241 } err]} {
2242 error_popup "Cannot write icon:\n\n$err"
2243 }
2244 }
2245}
2246
2247proc toggle_or_diff {w x y} {
2248 global file_lists current_diff ui_index ui_other
2249 global last_clicked selected_paths
2250
2251 set pos [split [$w index @$x,$y] .]
2252 set lno [lindex $pos 0]
2253 set col [lindex $pos 1]
2254 set path [lindex $file_lists($w) [expr {$lno - 1}]]
2255 if {$path eq {}} {
2256 set last_clicked {}
2257 return
2258 }
2259
2260 set last_clicked [list $w $lno]
2261 array unset selected_paths
2262 $ui_index tag remove in_sel 0.0 end
2263 $ui_other tag remove in_sel 0.0 end
2264
2265 if {$col == 0} {
2266 if {$current_diff eq $path} {
2267 set after {reshow_diff;}
2268 } else {
2269 set after {}
2270 }
2271 update_index \
2272 "Including [short_path $path]" \
2273 [list $path] \
2274 [concat $after {set ui_status_value {Ready.}}]
2275 } else {
2276 show_diff $path $w $lno
2277 }
2278}
2279
2280proc add_one_to_selection {w x y} {
2281 global file_lists
2282 global last_clicked selected_paths
2283
2284 set pos [split [$w index @$x,$y] .]
2285 set lno [lindex $pos 0]
2286 set col [lindex $pos 1]
2287 set path [lindex $file_lists($w) [expr {$lno - 1}]]
2288 if {$path eq {}} {
2289 set last_clicked {}
2290 return
2291 }
2292
2293 set last_clicked [list $w $lno]
2294 if {[catch {set in_sel $selected_paths($path)}]} {
2295 set in_sel 0
2296 }
2297 if {$in_sel} {
2298 unset selected_paths($path)
2299 $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
2300 } else {
2301 set selected_paths($path) 1
2302 $w tag add in_sel $lno.0 [expr {$lno + 1}].0
2303 }
2304}
2305
2306proc add_range_to_selection {w x y} {
2307 global file_lists
2308 global last_clicked selected_paths
2309
2310 if {[lindex $last_clicked 0] ne $w} {
2311 toggle_or_diff $w $x $y
2312 return
2313 }
2314
2315 set pos [split [$w index @$x,$y] .]
2316 set lno [lindex $pos 0]
2317 set lc [lindex $last_clicked 1]
2318 if {$lc < $lno} {
2319 set begin $lc
2320 set end $lno
2321 } else {
2322 set begin $lno
2323 set end $lc
2324 }
2325
2326 foreach path [lrange $file_lists($w) \
2327 [expr {$begin - 1}] \
2328 [expr {$end - 1}]] {
2329 set selected_paths($path) 1
2330 }
2331 $w tag add in_sel $begin.0 [expr {$end + 1}].0
2332}
2333
2334######################################################################
2335##
2336## config defaults
2337
2338set cursor_ptr arrow
2339font create font_diff -family Courier -size 10
2340font create font_ui
2341catch {
2342 label .dummy
2343 eval font configure font_ui [font actual [.dummy cget -font]]
2344 destroy .dummy
2345}
2346
2347font create font_uibold
2348font create font_diffbold
2349
2350set M1B M1
2351set M1T M1
2352if {$tcl_platform(platform) eq {windows}} {
2353 set M1B Control
2354 set M1T Ctrl
2355} elseif {[is_MacOSX]} {
2356 set M1B M1
2357 set M1T Cmd
2358}
2359
2360proc apply_config {} {
2361 global repo_config font_descs
2362
2363 foreach option $font_descs {
2364 set name [lindex $option 0]
2365 set font [lindex $option 1]
2366 if {[catch {
2367 foreach {cn cv} $repo_config(gui.$name) {
2368 font configure $font $cn $cv
2369 }
2370 } err]} {
2371 error_popup "Invalid font specified in gui.$name:\n\n$err"
2372 }
2373 foreach {cn cv} [font configure $font] {
2374 font configure ${font}bold $cn $cv
2375 }
2376 font configure ${font}bold -weight bold
2377 }
2378}
2379
2380set default_config(gui.trustmtime) false
2381set default_config(gui.pullsummary) true
2382set default_config(gui.partialinclude) false
2383set default_config(gui.diffcontext) 5
2384set default_config(gui.fontui) [font configure font_ui]
2385set default_config(gui.fontdiff) [font configure font_diff]
2386set font_descs {
2387 {fontui font_ui {Main Font}}
2388 {fontdiff font_diff {Diff/Console Font}}
2389}
2390load_config 0
2391apply_config
2392
2393######################################################################
2394##
2395## ui construction
2396
2397# -- Menu Bar
2398#
2399menu .mbar -tearoff 0
2400.mbar add cascade -label Project -menu .mbar.project
2401.mbar add cascade -label Edit -menu .mbar.edit
2402.mbar add cascade -label Commit -menu .mbar.commit
2403if {!$single_commit} {
2404 .mbar add cascade -label Fetch -menu .mbar.fetch
2405 .mbar add cascade -label Pull -menu .mbar.pull
2406 .mbar add cascade -label Push -menu .mbar.push
2407}
2408. configure -menu .mbar
2409
2410# -- Project Menu
2411#
2412menu .mbar.project
2413.mbar.project add command -label Visualize \
2414 -command do_gitk \
2415 -font font_ui
2416if {!$single_commit} {
2417 .mbar.project add command -label {Repack Database} \
2418 -command do_repack \
2419 -font font_ui
2420
2421 if {$tcl_platform(platform) eq {windows}} {
2422 .mbar.project add command \
2423 -label {Create Desktop Icon} \
2424 -command do_windows_shortcut \
2425 -font font_ui
2426 } elseif {[is_MacOSX]} {
2427 .mbar.project add command \
2428 -label {Create Desktop Icon} \
2429 -command do_macosx_app \
2430 -font font_ui
2431 }
2432}
2433.mbar.project add command -label Quit \
2434 -command do_quit \
2435 -accelerator $M1T-Q \
2436 -font font_ui
2437
2438# -- Edit Menu
2439#
2440menu .mbar.edit
2441.mbar.edit add command -label Undo \
2442 -command {catch {[focus] edit undo}} \
2443 -accelerator $M1T-Z \
2444 -font font_ui
2445.mbar.edit add command -label Redo \
2446 -command {catch {[focus] edit redo}} \
2447 -accelerator $M1T-Y \
2448 -font font_ui
2449.mbar.edit add separator
2450.mbar.edit add command -label Cut \
2451 -command {catch {tk_textCut [focus]}} \
2452 -accelerator $M1T-X \
2453 -font font_ui
2454.mbar.edit add command -label Copy \
2455 -command {catch {tk_textCopy [focus]}} \
2456 -accelerator $M1T-C \
2457 -font font_ui
2458.mbar.edit add command -label Paste \
2459 -command {catch {tk_textPaste [focus]; [focus] see insert}} \
2460 -accelerator $M1T-V \
2461 -font font_ui
2462.mbar.edit add command -label Delete \
2463 -command {catch {[focus] delete sel.first sel.last}} \
2464 -accelerator Del \
2465 -font font_ui
2466.mbar.edit add separator
2467.mbar.edit add command -label {Select All} \
2468 -command {catch {[focus] tag add sel 0.0 end}} \
2469 -accelerator $M1T-A \
2470 -font font_ui
2471.mbar.edit add separator
2472.mbar.edit add command -label {Options...} \
2473 -command do_options \
2474 -font font_ui
2475
2476# -- Commit Menu
2477#
2478menu .mbar.commit
2479
2480.mbar.commit add radiobutton \
2481 -label {New Commit} \
2482 -command do_select_commit_type \
2483 -variable selected_commit_type \
2484 -value new \
2485 -font font_ui
2486lappend disable_on_lock \
2487 [list .mbar.commit entryconf [.mbar.commit index last] -state]
2488
2489.mbar.commit add radiobutton \
2490 -label {Amend Last Commit} \
2491 -command do_select_commit_type \
2492 -variable selected_commit_type \
2493 -value amend \
2494 -font font_ui
2495lappend disable_on_lock \
2496 [list .mbar.commit entryconf [.mbar.commit index last] -state]
2497
2498.mbar.commit add separator
2499
2500.mbar.commit add command -label Rescan \
2501 -command do_rescan \
2502 -accelerator F5 \
2503 -font font_ui
2504lappend disable_on_lock \
2505 [list .mbar.commit entryconf [.mbar.commit index last] -state]
2506
2507.mbar.commit add command -label {Include Selected Files} \
2508 -command do_include_selection \
2509 -font font_ui
2510lappend disable_on_lock \
2511 [list .mbar.commit entryconf [.mbar.commit index last] -state]
2512
2513.mbar.commit add command -label {Include All Files} \
2514 -command do_include_all \
2515 -accelerator $M1T-I \
2516 -font font_ui
2517lappend disable_on_lock \
2518 [list .mbar.commit entryconf [.mbar.commit index last] -state]
2519
2520.mbar.commit add command -label {Sign Off} \
2521 -command do_signoff \
2522 -accelerator $M1T-S \
2523 -font font_ui
2524
2525.mbar.commit add command -label Commit \
2526 -command do_commit \
2527 -accelerator $M1T-Return \
2528 -font font_ui
2529lappend disable_on_lock \
2530 [list .mbar.commit entryconf [.mbar.commit index last] -state]
2531
2532# -- Transport menus
2533#
2534if {!$single_commit} {
2535 menu .mbar.fetch
2536 menu .mbar.pull
2537 menu .mbar.push
2538}
2539
2540# -- Main Window Layout
2541#
2542panedwindow .vpane -orient vertical
2543panedwindow .vpane.files -orient horizontal
2544.vpane add .vpane.files -sticky nsew -height 100 -width 400
2545pack .vpane -anchor n -side top -fill both -expand 1
2546
2547# -- Index File List
2548#
2549frame .vpane.files.index -height 100 -width 400
2550label .vpane.files.index.title -text {Modified Files} \
2551 -background green \
2552 -font font_ui
2553text $ui_index -background white -borderwidth 0 \
2554 -width 40 -height 10 \
2555 -font font_ui \
2556 -cursor $cursor_ptr \
2557 -yscrollcommand {.vpane.files.index.sb set} \
2558 -state disabled
2559scrollbar .vpane.files.index.sb -command [list $ui_index yview]
2560pack .vpane.files.index.title -side top -fill x
2561pack .vpane.files.index.sb -side right -fill y
2562pack $ui_index -side left -fill both -expand 1
2563.vpane.files add .vpane.files.index -sticky nsew
2564
2565# -- Other (Add) File List
2566#
2567frame .vpane.files.other -height 100 -width 100
2568label .vpane.files.other.title -text {Untracked Files} \
2569 -background red \
2570 -font font_ui
2571text $ui_other -background white -borderwidth 0 \
2572 -width 40 -height 10 \
2573 -font font_ui \
2574 -cursor $cursor_ptr \
2575 -yscrollcommand {.vpane.files.other.sb set} \
2576 -state disabled
2577scrollbar .vpane.files.other.sb -command [list $ui_other yview]
2578pack .vpane.files.other.title -side top -fill x
2579pack .vpane.files.other.sb -side right -fill y
2580pack $ui_other -side left -fill both -expand 1
2581.vpane.files add .vpane.files.other -sticky nsew
2582
2583foreach i [list $ui_index $ui_other] {
2584 $i tag conf in_diff -font font_uibold
2585 $i tag conf in_sel \
2586 -background [$i cget -foreground] \
2587 -foreground [$i cget -background]
2588}
2589unset i
2590
2591# -- Diff and Commit Area
2592#
2593frame .vpane.lower -height 300 -width 400
2594frame .vpane.lower.commarea
2595frame .vpane.lower.diff -relief sunken -borderwidth 1
2596pack .vpane.lower.commarea -side top -fill x
2597pack .vpane.lower.diff -side bottom -fill both -expand 1
2598.vpane add .vpane.lower -stick nsew
2599
2600# -- Commit Area Buttons
2601#
2602frame .vpane.lower.commarea.buttons
2603label .vpane.lower.commarea.buttons.l -text {} \
2604 -anchor w \
2605 -justify left \
2606 -font font_ui
2607pack .vpane.lower.commarea.buttons.l -side top -fill x
2608pack .vpane.lower.commarea.buttons -side left -fill y
2609
2610button .vpane.lower.commarea.buttons.rescan -text {Rescan} \
2611 -command do_rescan \
2612 -font font_ui
2613pack .vpane.lower.commarea.buttons.rescan -side top -fill x
2614lappend disable_on_lock \
2615 {.vpane.lower.commarea.buttons.rescan conf -state}
2616
2617button .vpane.lower.commarea.buttons.incall -text {Include All} \
2618 -command do_include_all \
2619 -font font_ui
2620pack .vpane.lower.commarea.buttons.incall -side top -fill x
2621lappend disable_on_lock \
2622 {.vpane.lower.commarea.buttons.incall conf -state}
2623
2624button .vpane.lower.commarea.buttons.signoff -text {Sign Off} \
2625 -command do_signoff \
2626 -font font_ui
2627pack .vpane.lower.commarea.buttons.signoff -side top -fill x
2628
2629button .vpane.lower.commarea.buttons.commit -text {Commit} \
2630 -command do_commit \
2631 -font font_ui
2632pack .vpane.lower.commarea.buttons.commit -side top -fill x
2633lappend disable_on_lock \
2634 {.vpane.lower.commarea.buttons.commit conf -state}
2635
2636# -- Commit Message Buffer
2637#
2638frame .vpane.lower.commarea.buffer
2639frame .vpane.lower.commarea.buffer.header
2640set ui_comm .vpane.lower.commarea.buffer.t
2641set ui_coml .vpane.lower.commarea.buffer.header.l
2642radiobutton .vpane.lower.commarea.buffer.header.new \
2643 -text {New Commit} \
2644 -command do_select_commit_type \
2645 -variable selected_commit_type \
2646 -value new \
2647 -font font_ui
2648lappend disable_on_lock \
2649 [list .vpane.lower.commarea.buffer.header.new conf -state]
2650radiobutton .vpane.lower.commarea.buffer.header.amend \
2651 -text {Amend Last Commit} \
2652 -command do_select_commit_type \
2653 -variable selected_commit_type \
2654 -value amend \
2655 -font font_ui
2656lappend disable_on_lock \
2657 [list .vpane.lower.commarea.buffer.header.amend conf -state]
2658label $ui_coml \
2659 -anchor w \
2660 -justify left \
2661 -font font_ui
2662proc trace_commit_type {varname args} {
2663 global ui_coml commit_type
2664 switch -glob -- $commit_type {
2665 initial {set txt {Initial Commit Message:}}
2666 amend {set txt {Amended Commit Message:}}
2667 amend-initial {set txt {Amended Initial Commit Message:}}
2668 merge {set txt {Merge Commit Message:}}
2669 * {set txt {Commit Message:}}
2670 }
2671 $ui_coml conf -text $txt
2672}
2673trace add variable commit_type write trace_commit_type
2674pack $ui_coml -side left -fill x
2675pack .vpane.lower.commarea.buffer.header.amend -side right
2676pack .vpane.lower.commarea.buffer.header.new -side right
2677
2678text $ui_comm -background white -borderwidth 1 \
2679 -undo true \
2680 -maxundo 20 \
2681 -autoseparators true \
2682 -relief sunken \
2683 -width 75 -height 9 -wrap none \
2684 -font font_diff \
2685 -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
2686scrollbar .vpane.lower.commarea.buffer.sby \
2687 -command [list $ui_comm yview]
2688pack .vpane.lower.commarea.buffer.header -side top -fill x
2689pack .vpane.lower.commarea.buffer.sby -side right -fill y
2690pack $ui_comm -side left -fill y
2691pack .vpane.lower.commarea.buffer -side left -fill y
2692
2693# -- Commit Message Buffer Context Menu
2694#
2695set ctxm .vpane.lower.commarea.buffer.ctxm
2696menu $ctxm -tearoff 0
2697$ctxm add command \
2698 -label {Cut} \
2699 -font font_ui \
2700 -command {tk_textCut $ui_comm}
2701$ctxm add command \
2702 -label {Copy} \
2703 -font font_ui \
2704 -command {tk_textCopy $ui_comm}
2705$ctxm add command \
2706 -label {Paste} \
2707 -font font_ui \
2708 -command {tk_textPaste $ui_comm}
2709$ctxm add command \
2710 -label {Delete} \
2711 -font font_ui \
2712 -command {$ui_comm delete sel.first sel.last}
2713$ctxm add separator
2714$ctxm add command \
2715 -label {Select All} \
2716 -font font_ui \
2717 -command {$ui_comm tag add sel 0.0 end}
2718$ctxm add command \
2719 -label {Copy All} \
2720 -font font_ui \
2721 -command {
2722 $ui_comm tag add sel 0.0 end
2723 tk_textCopy $ui_comm
2724 $ui_comm tag remove sel 0.0 end
2725 }
2726$ctxm add separator
2727$ctxm add command \
2728 -label {Sign Off} \
2729 -font font_ui \
2730 -command do_signoff
2731bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
2732
2733# -- Diff Header
2734#
2735set current_diff {}
2736set diff_actions [list]
2737proc trace_current_diff {varname args} {
2738 global current_diff diff_actions file_states
2739 if {$current_diff eq {}} {
2740 set s {}
2741 set f {}
2742 set p {}
2743 set o disabled
2744 } else {
2745 set p $current_diff
2746 set s [mapdesc [lindex $file_states($p) 0] $p]
2747 set f {File:}
2748 set p [escape_path $p]
2749 set o normal
2750 }
2751
2752 .vpane.lower.diff.header.status configure -text $s
2753 .vpane.lower.diff.header.file configure -text $f
2754 .vpane.lower.diff.header.path configure -text $p
2755 foreach w $diff_actions {
2756 uplevel #0 $w $o
2757 }
2758}
2759trace add variable current_diff write trace_current_diff
2760
2761frame .vpane.lower.diff.header -background orange
2762label .vpane.lower.diff.header.status \
2763 -background orange \
2764 -width $max_status_desc \
2765 -anchor w \
2766 -justify left \
2767 -font font_ui
2768label .vpane.lower.diff.header.file \
2769 -background orange \
2770 -anchor w \
2771 -justify left \
2772 -font font_ui
2773label .vpane.lower.diff.header.path \
2774 -background orange \
2775 -anchor w \
2776 -justify left \
2777 -font font_ui
2778pack .vpane.lower.diff.header.status -side left
2779pack .vpane.lower.diff.header.file -side left
2780pack .vpane.lower.diff.header.path -fill x
2781set ctxm .vpane.lower.diff.header.ctxm
2782menu $ctxm -tearoff 0
2783$ctxm add command \
2784 -label {Copy} \
2785 -font font_ui \
2786 -command {
2787 clipboard clear
2788 clipboard append \
2789 -format STRING \
2790 -type STRING \
2791 -- $current_diff
2792 }
2793lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2794bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
2795
2796# -- Diff Body
2797#
2798frame .vpane.lower.diff.body
2799set ui_diff .vpane.lower.diff.body.t
2800text $ui_diff -background white -borderwidth 0 \
2801 -width 80 -height 15 -wrap none \
2802 -font font_diff \
2803 -xscrollcommand {.vpane.lower.diff.body.sbx set} \
2804 -yscrollcommand {.vpane.lower.diff.body.sby set} \
2805 -state disabled
2806scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
2807 -command [list $ui_diff xview]
2808scrollbar .vpane.lower.diff.body.sby -orient vertical \
2809 -command [list $ui_diff yview]
2810pack .vpane.lower.diff.body.sbx -side bottom -fill x
2811pack .vpane.lower.diff.body.sby -side right -fill y
2812pack $ui_diff -side left -fill both -expand 1
2813pack .vpane.lower.diff.header -side top -fill x
2814pack .vpane.lower.diff.body -side bottom -fill both -expand 1
2815
2816$ui_diff tag conf dm -foreground red
2817$ui_diff tag conf dp -foreground blue
2818$ui_diff tag conf di -foreground {#00a000}
2819$ui_diff tag conf dni -foreground {#a000a0}
2820$ui_diff tag conf da -font font_diffbold
2821$ui_diff tag conf bold -font font_diffbold
2822
2823# -- Diff Body Context Menu
2824#
2825set ctxm .vpane.lower.diff.body.ctxm
2826menu $ctxm -tearoff 0
2827$ctxm add command \
2828 -label {Copy} \
2829 -font font_ui \
2830 -command {tk_textCopy $ui_diff}
2831lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2832$ctxm add command \
2833 -label {Select All} \
2834 -font font_ui \
2835 -command {$ui_diff tag add sel 0.0 end}
2836lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2837$ctxm add command \
2838 -label {Copy All} \
2839 -font font_ui \
2840 -command {
2841 $ui_diff tag add sel 0.0 end
2842 tk_textCopy $ui_diff
2843 $ui_diff tag remove sel 0.0 end
2844 }
2845lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2846$ctxm add separator
2847$ctxm add command \
2848 -label {Decrease Font Size} \
2849 -font font_ui \
2850 -command {incr_font_size font_diff -1}
2851lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2852$ctxm add command \
2853 -label {Increase Font Size} \
2854 -font font_ui \
2855 -command {incr_font_size font_diff 1}
2856lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2857$ctxm add separator
2858$ctxm add command \
2859 -label {Show Less Context} \
2860 -font font_ui \
2861 -command {if {$repo_config(gui.diffcontext) >= 2} {
2862 incr repo_config(gui.diffcontext) -1
2863 reshow_diff
2864 }}
2865lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2866$ctxm add command \
2867 -label {Show More Context} \
2868 -font font_ui \
2869 -command {
2870 incr repo_config(gui.diffcontext)
2871 reshow_diff
2872 }
2873lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2874$ctxm add separator
2875$ctxm add command -label {Options...} \
2876 -font font_ui \
2877 -command do_options
2878bind_button3 $ui_diff "tk_popup $ctxm %X %Y"
2879
2880# -- Status Bar
2881#
2882set ui_status_value {Initializing...}
2883label .status -textvariable ui_status_value \
2884 -anchor w \
2885 -justify left \
2886 -borderwidth 1 \
2887 -relief sunken \
2888 -font font_ui
2889pack .status -anchor w -side bottom -fill x
2890
2891# -- Load geometry
2892#
2893catch {
2894set gm $repo_config(gui.geometry)
2895wm geometry . [lindex $gm 0]
2896.vpane sash place 0 \
2897 [lindex [.vpane sash coord 0] 0] \
2898 [lindex $gm 1]
2899.vpane.files sash place 0 \
2900 [lindex $gm 2] \
2901 [lindex [.vpane.files sash coord 0] 1]
2902unset gm
2903}
2904
2905# -- Key Bindings
2906#
2907bind $ui_comm <$M1B-Key-Return> {do_commit;break}
2908bind $ui_comm <$M1B-Key-i> {do_include_all;break}
2909bind $ui_comm <$M1B-Key-I> {do_include_all;break}
2910bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
2911bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
2912bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
2913bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
2914bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
2915bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
2916bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
2917bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
2918
2919bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
2920bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
2921bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
2922bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
2923bind $ui_diff <$M1B-Key-v> {break}
2924bind $ui_diff <$M1B-Key-V> {break}
2925bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
2926bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
2927bind $ui_diff <Key-Up> {catch {%W yview scroll -1 units};break}
2928bind $ui_diff <Key-Down> {catch {%W yview scroll 1 units};break}
2929bind $ui_diff <Key-Left> {catch {%W xview scroll -1 units};break}
2930bind $ui_diff <Key-Right> {catch {%W xview scroll 1 units};break}
2931
2932bind . <Destroy> do_quit
2933bind all <Key-F5> do_rescan
2934bind all <$M1B-Key-r> do_rescan
2935bind all <$M1B-Key-R> do_rescan
2936bind . <$M1B-Key-s> do_signoff
2937bind . <$M1B-Key-S> do_signoff
2938bind . <$M1B-Key-i> do_include_all
2939bind . <$M1B-Key-I> do_include_all
2940bind . <$M1B-Key-Return> do_commit
2941bind all <$M1B-Key-q> do_quit
2942bind all <$M1B-Key-Q> do_quit
2943bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
2944bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
2945foreach i [list $ui_index $ui_other] {
2946 bind $i <Button-1> "toggle_or_diff $i %x %y; break"
2947 bind $i <$M1B-Button-1> "add_one_to_selection $i %x %y; break"
2948 bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
2949}
2950unset i
2951
2952set file_lists($ui_index) [list]
2953set file_lists($ui_other) [list]
2954
2955set HEAD {}
2956set PARENT {}
2957set commit_type {}
2958set empty_tree {}
2959set current_diff {}
2960set selected_commit_type new
2961
2962wm title . "$appname ([file normalize [file dirname $gitdir]])"
2963focus -force $ui_comm
2964if {!$single_commit} {
2965 load_all_remotes
2966 populate_fetch_menu .mbar.fetch
2967 populate_pull_menu .mbar.pull
2968 populate_push_menu .mbar.push
2969}
2970lock_index begin-read
2971after 1 do_rescan