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