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