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