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