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