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 load_repo_config {} {
18 global repo_config
19 global cfg_trust_mtime
20
21 array unset repo_config
22 catch {
23 set fd_rc [open "| git repo-config --list" r]
24 while {[gets $fd_rc line] >= 0} {
25 if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
26 lappend repo_config($name) $value
27 }
28 }
29 close $fd_rc
30 }
31
32 if {[catch {set cfg_trust_mtime \
33 [lindex $repo_config(gui.trustmtime) 0]
34 }]} {
35 set cfg_trust_mtime false
36 }
37}
38
39proc save_my_config {} {
40 global repo_config
41 global cfg_trust_mtime
42
43 if {[catch {set rc_trustMTime $repo_config(gui.trustmtime)}]} {
44 set rc_trustMTime [list false]
45 }
46 if {$cfg_trust_mtime != [lindex $rc_trustMTime 0]} {
47 exec git repo-config gui.trustMTime $cfg_trust_mtime
48 set repo_config(gui.trustmtime) [list $cfg_trust_mtime]
49 }
50
51 set cfg_geometry [list \
52 [wm geometry .] \
53 [.vpane sash coord 0] \
54 [.vpane.files sash coord 0] \
55 ]
56 if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
57 set rc_geometry [list [list]]
58 }
59 if {$cfg_geometry != [lindex $rc_geometry 0]} {
60 exec git repo-config gui.geometry $cfg_geometry
61 set repo_config(gui.geometry) [list $cfg_geometry]
62 }
63}
64
65proc error_popup {msg} {
66 global gitdir appname
67
68 set title $appname
69 if {$gitdir != {}} {
70 append title { (}
71 append title [lindex \
72 [file split [file normalize [file dirname $gitdir]]] \
73 end]
74 append title {)}
75 }
76 tk_messageBox \
77 -parent . \
78 -icon error \
79 -type ok \
80 -title "$title: error" \
81 -message $msg
82}
83
84######################################################################
85##
86## repository setup
87
88if { [catch {set cdup [exec git rev-parse --show-cdup]} err]
89 || [catch {set gitdir [exec git rev-parse --git-dir]} err]} {
90 catch {wm withdraw .}
91 error_popup "Cannot find the git directory:\n\n$err"
92 exit 1
93}
94if {$cdup != ""} {
95 cd $cdup
96}
97unset cdup
98
99if {$appname == {git-citool}} {
100 set single_commit 1
101}
102
103load_repo_config
104
105######################################################################
106##
107## task management
108
109set single_commit 0
110set status_active 0
111set diff_active 0
112set update_active 0
113set commit_active 0
114set update_index_fd {}
115
116set disable_on_lock [list]
117set index_lock_type none
118
119set HEAD {}
120set PARENT {}
121set commit_type {}
122
123proc lock_index {type} {
124 global index_lock_type disable_on_lock
125
126 if {$index_lock_type == {none}} {
127 set index_lock_type $type
128 foreach w $disable_on_lock {
129 uplevel #0 $w disabled
130 }
131 return 1
132 } elseif {$index_lock_type == {begin-update} && $type == {update}} {
133 set index_lock_type $type
134 return 1
135 }
136 return 0
137}
138
139proc unlock_index {} {
140 global index_lock_type disable_on_lock
141
142 set index_lock_type none
143 foreach w $disable_on_lock {
144 uplevel #0 $w normal
145 }
146}
147
148######################################################################
149##
150## status
151
152proc repository_state {hdvar ctvar} {
153 global gitdir
154 upvar $hdvar hd $ctvar ct
155
156 if {[catch {set hd [exec git rev-parse --verify HEAD]}]} {
157 set ct initial
158 } elseif {[file exists [file join $gitdir MERGE_HEAD]]} {
159 set ct merge
160 } else {
161 set ct normal
162 }
163}
164
165proc update_status {{final Ready.}} {
166 global HEAD PARENT commit_type
167 global ui_index ui_other ui_status_value ui_comm
168 global status_active file_states
169 global cfg_trust_mtime
170
171 if {$status_active || ![lock_index read]} return
172
173 repository_state new_HEAD new_type
174 if {$commit_type == {amend}
175 && $new_type == {normal}
176 && $new_HEAD == $HEAD} {
177 } else {
178 set HEAD $new_HEAD
179 set PARENT $new_HEAD
180 set commit_type $new_type
181 }
182
183 array unset file_states
184
185 if {![$ui_comm edit modified]
186 || [string trim [$ui_comm get 0.0 end]] == {}} {
187 if {[load_message GITGUI_MSG]} {
188 } elseif {[load_message MERGE_MSG]} {
189 } elseif {[load_message SQUASH_MSG]} {
190 }
191 $ui_comm edit modified false
192 $ui_comm edit reset
193 }
194
195 if {$cfg_trust_mtime == {true}} {
196 update_status_stage2 {} $final
197 } else {
198 set status_active 1
199 set ui_status_value {Refreshing file status...}
200 set fd_rf [open "| git update-index -q --unmerged --refresh" r]
201 fconfigure $fd_rf -blocking 0 -translation binary
202 fileevent $fd_rf readable [list update_status_stage2 $fd_rf $final]
203 }
204}
205
206proc update_status_stage2 {fd final} {
207 global gitdir PARENT commit_type
208 global ui_index ui_other ui_status_value ui_comm
209 global status_active
210 global buf_rdi buf_rdf buf_rlo
211
212 if {$fd != {}} {
213 read $fd
214 if {![eof $fd]} return
215 close $fd
216 }
217
218 set ls_others [list | git ls-files --others -z \
219 --exclude-per-directory=.gitignore]
220 set info_exclude [file join $gitdir info exclude]
221 if {[file readable $info_exclude]} {
222 lappend ls_others "--exclude-from=$info_exclude"
223 }
224
225 set buf_rdi {}
226 set buf_rdf {}
227 set buf_rlo {}
228
229 set status_active 3
230 set ui_status_value {Scanning for modified files ...}
231 set fd_di [open "| git diff-index --cached -z $PARENT" r]
232 set fd_df [open "| git diff-files -z" r]
233 set fd_lo [open $ls_others r]
234
235 fconfigure $fd_di -blocking 0 -translation binary
236 fconfigure $fd_df -blocking 0 -translation binary
237 fconfigure $fd_lo -blocking 0 -translation binary
238 fileevent $fd_di readable [list read_diff_index $fd_di $final]
239 fileevent $fd_df readable [list read_diff_files $fd_df $final]
240 fileevent $fd_lo readable [list read_ls_others $fd_lo $final]
241}
242
243proc load_message {file} {
244 global gitdir ui_comm
245
246 set f [file join $gitdir $file]
247 if {[file isfile $f]} {
248 if {[catch {set fd [open $f r]}]} {
249 return 0
250 }
251 set content [string trim [read $fd]]
252 close $fd
253 $ui_comm delete 0.0 end
254 $ui_comm insert end $content
255 return 1
256 }
257 return 0
258}
259
260proc read_diff_index {fd final} {
261 global buf_rdi
262
263 append buf_rdi [read $fd]
264 set c 0
265 set n [string length $buf_rdi]
266 while {$c < $n} {
267 set z1 [string first "\0" $buf_rdi $c]
268 if {$z1 == -1} break
269 incr z1
270 set z2 [string first "\0" $buf_rdi $z1]
271 if {$z2 == -1} break
272
273 set c $z2
274 incr z2 -1
275 display_file \
276 [string range $buf_rdi $z1 $z2] \
277 [string index $buf_rdi [expr $z1 - 2]]_
278 incr c
279 }
280 if {$c < $n} {
281 set buf_rdi [string range $buf_rdi $c end]
282 } else {
283 set buf_rdi {}
284 }
285
286 status_eof $fd buf_rdi $final
287}
288
289proc read_diff_files {fd final} {
290 global buf_rdf
291
292 append buf_rdf [read $fd]
293 set c 0
294 set n [string length $buf_rdf]
295 while {$c < $n} {
296 set z1 [string first "\0" $buf_rdf $c]
297 if {$z1 == -1} break
298 incr z1
299 set z2 [string first "\0" $buf_rdf $z1]
300 if {$z2 == -1} break
301
302 set c $z2
303 incr z2 -1
304 display_file \
305 [string range $buf_rdf $z1 $z2] \
306 _[string index $buf_rdf [expr $z1 - 2]]
307 incr c
308 }
309 if {$c < $n} {
310 set buf_rdf [string range $buf_rdf $c end]
311 } else {
312 set buf_rdf {}
313 }
314
315 status_eof $fd buf_rdf $final
316}
317
318proc read_ls_others {fd final} {
319 global buf_rlo
320
321 append buf_rlo [read $fd]
322 set pck [split $buf_rlo "\0"]
323 set buf_rlo [lindex $pck end]
324 foreach p [lrange $pck 0 end-1] {
325 display_file $p _O
326 }
327 status_eof $fd buf_rlo $final
328}
329
330proc status_eof {fd buf final} {
331 global status_active ui_status_value
332 upvar $buf to_clear
333
334 if {[eof $fd]} {
335 set to_clear {}
336 close $fd
337
338 if {[incr status_active -1] == 0} {
339 display_all_files
340 unlock_index
341 reshow_diff
342 set ui_status_value $final
343 }
344 }
345}
346
347######################################################################
348##
349## diff
350
351proc clear_diff {} {
352 global ui_diff ui_fname_value ui_fstatus_value ui_index ui_other
353
354 $ui_diff conf -state normal
355 $ui_diff delete 0.0 end
356 $ui_diff conf -state disabled
357
358 set ui_fname_value {}
359 set ui_fstatus_value {}
360
361 $ui_index tag remove in_diff 0.0 end
362 $ui_other tag remove in_diff 0.0 end
363}
364
365proc reshow_diff {} {
366 global ui_fname_value ui_status_value file_states
367
368 if {$ui_fname_value == {}
369 || [catch {set s $file_states($ui_fname_value)}]} {
370 clear_diff
371 } else {
372 show_diff $ui_fname_value
373 }
374}
375
376proc show_diff {path {w {}} {lno {}}} {
377 global file_states file_lists
378 global PARENT diff_3way diff_active
379 global ui_diff ui_fname_value ui_fstatus_value ui_status_value
380
381 if {$diff_active || ![lock_index read]} return
382
383 clear_diff
384 if {$w == {} || $lno == {}} {
385 foreach w [array names file_lists] {
386 set lno [lsearch -sorted $file_lists($w) $path]
387 if {$lno >= 0} {
388 incr lno
389 break
390 }
391 }
392 }
393 if {$w != {} && $lno >= 1} {
394 $w tag add in_diff $lno.0 [expr $lno + 1].0
395 }
396
397 set s $file_states($path)
398 set m [lindex $s 0]
399 set diff_3way 0
400 set diff_active 1
401 set ui_fname_value [escape_path $path]
402 set ui_fstatus_value [mapdesc $m $path]
403 set ui_status_value "Loading diff of [escape_path $path]..."
404
405 set cmd [list | git diff-index -p $PARENT -- $path]
406 switch $m {
407 AM {
408 }
409 MM {
410 set cmd [list | git diff-index -p -c $PARENT $path]
411 }
412 _O {
413 if {[catch {
414 set fd [open $path r]
415 set content [read $fd]
416 close $fd
417 } err ]} {
418 set diff_active 0
419 unlock_index
420 set ui_status_value "Unable to display [escape_path $path]"
421 error_popup "Error loading file:\n\n$err"
422 return
423 }
424 $ui_diff conf -state normal
425 $ui_diff insert end $content
426 $ui_diff conf -state disabled
427 set diff_active 0
428 unlock_index
429 set ui_status_value {Ready.}
430 return
431 }
432 }
433
434 if {[catch {set fd [open $cmd r]} err]} {
435 set diff_active 0
436 unlock_index
437 set ui_status_value "Unable to display [escape_path $path]"
438 error_popup "Error loading diff:\n\n$err"
439 return
440 }
441
442 fconfigure $fd -blocking 0 -translation auto
443 fileevent $fd readable [list read_diff $fd]
444}
445
446proc read_diff {fd} {
447 global ui_diff ui_status_value diff_3way diff_active
448
449 while {[gets $fd line] >= 0} {
450 if {[string match {diff --git *} $line]} continue
451 if {[string match {diff --combined *} $line]} continue
452 if {[string match {--- *} $line]} continue
453 if {[string match {+++ *} $line]} continue
454 if {[string match index* $line]} {
455 if {[string first , $line] >= 0} {
456 set diff_3way 1
457 }
458 }
459
460 $ui_diff conf -state normal
461 if {!$diff_3way} {
462 set x [string index $line 0]
463 switch -- $x {
464 "@" {set tags da}
465 "+" {set tags dp}
466 "-" {set tags dm}
467 default {set tags {}}
468 }
469 } else {
470 set x [string range $line 0 1]
471 switch -- $x {
472 default {set tags {}}
473 "@@" {set tags da}
474 "++" {set tags dp; set x " +"}
475 " +" {set tags {di bold}; set x "++"}
476 "+ " {set tags dni; set x "-+"}
477 "--" {set tags dm; set x " -"}
478 " -" {set tags {dm bold}; set x "--"}
479 "- " {set tags di; set x "+-"}
480 default {set tags {}}
481 }
482 set line [string replace $line 0 1 $x]
483 }
484 $ui_diff insert end $line $tags
485 $ui_diff insert end "\n"
486 $ui_diff conf -state disabled
487 }
488
489 if {[eof $fd]} {
490 close $fd
491 set diff_active 0
492 unlock_index
493 set ui_status_value {Ready.}
494 }
495}
496
497######################################################################
498##
499## commit
500
501proc load_last_commit {} {
502 global HEAD PARENT commit_type ui_comm
503
504 if {$commit_type == {amend}} return
505 if {$commit_type != {normal}} {
506 error_popup "Can't amend a $commit_type commit."
507 return
508 }
509
510 set msg {}
511 set parent {}
512 set parent_count 0
513 if {[catch {
514 set fd [open "| git cat-file commit $HEAD" r]
515 while {[gets $fd line] > 0} {
516 if {[string match {parent *} $line]} {
517 set parent [string range $line 7 end]
518 incr parent_count
519 }
520 }
521 set msg [string trim [read $fd]]
522 close $fd
523 } err]} {
524 error_popup "Error loading commit data for amend:\n\n$err"
525 return
526 }
527
528 if {$parent_count == 0} {
529 set commit_type amend
530 set HEAD {}
531 set PARENT {}
532 update_status
533 } elseif {$parent_count == 1} {
534 set commit_type amend
535 set PARENT $parent
536 $ui_comm delete 0.0 end
537 $ui_comm insert end $msg
538 $ui_comm edit modified false
539 $ui_comm edit reset
540 update_status
541 } else {
542 error_popup {You can't amend a merge commit.}
543 return
544 }
545}
546
547proc commit_tree {} {
548 global tcl_platform HEAD gitdir commit_type file_states
549 global commit_active ui_status_value
550 global ui_comm
551
552 if {$commit_active || ![lock_index update]} return
553
554 # -- Our in memory state should match the repository.
555 #
556 repository_state curHEAD cur_type
557 if {$commit_type == {amend}
558 && $cur_type == {normal}
559 && $curHEAD == $HEAD} {
560 } elseif {$commit_type != $cur_type || $HEAD != $curHEAD} {
561 error_popup {Last scanned state does not match repository state.
562
563Its highly likely that another Git program modified the
564repository since our last scan. A rescan is required
565before committing.
566}
567 unlock_index
568 update_status
569 return
570 }
571
572 # -- At least one file should differ in the index.
573 #
574 set files_ready 0
575 foreach path [array names file_states] {
576 set s $file_states($path)
577 switch -glob -- [lindex $s 0] {
578 _? {continue}
579 A? -
580 D? -
581 M? {set files_ready 1; break}
582 U? {
583 error_popup "Unmerged files cannot be committed.
584
585File [escape_path $path] has merge conflicts.
586You must resolve them and include the file before committing.
587"
588 unlock_index
589 return
590 }
591 default {
592 error_popup "Unknown file state [lindex $s 0] detected.
593
594File [escape_path $path] cannot be committed by this program.
595"
596 }
597 }
598 }
599 if {!$files_ready} {
600 error_popup {No included files to commit.
601
602You must include at least 1 file before you can commit.
603}
604 unlock_index
605 return
606 }
607
608 # -- A message is required.
609 #
610 set msg [string trim [$ui_comm get 1.0 end]]
611 if {$msg == {}} {
612 error_popup {Please supply a commit message.
613
614A good commit message has the following format:
615
616- First line: Describe in one sentance what you did.
617- Second line: Blank
618- Remaining lines: Describe why this change is good.
619}
620 unlock_index
621 return
622 }
623
624 # -- Ask the pre-commit hook for the go-ahead.
625 #
626 set pchook [file join $gitdir hooks pre-commit]
627 if {$tcl_platform(platform) == {windows} && [file isfile $pchook]} {
628 set pchook [list sh -c \
629 "if test -x \"$pchook\"; then exec \"$pchook\"; fi"]
630 } elseif {[file executable $pchook]} {
631 set pchook [list $pchook]
632 } else {
633 set pchook {}
634 }
635 if {$pchook != {} && [catch {eval exec $pchook} err]} {
636 hook_failed_popup pre-commit $err
637 unlock_index
638 return
639 }
640
641 # -- Write the tree in the background.
642 #
643 set commit_active 1
644 set ui_status_value {Committing changes...}
645
646 set fd_wt [open "| git write-tree" r]
647 fileevent $fd_wt readable [list commit_stage2 $fd_wt $curHEAD $msg]
648}
649
650proc commit_stage2 {fd_wt curHEAD msg} {
651 global single_commit gitdir HEAD PARENT commit_type
652 global commit_active ui_status_value ui_comm
653 global file_states
654
655 gets $fd_wt tree_id
656 if {$tree_id == {} || [catch {close $fd_wt} err]} {
657 error_popup "write-tree failed:\n\n$err"
658 set commit_active 0
659 set ui_status_value {Commit failed.}
660 unlock_index
661 return
662 }
663
664 # -- Create the commit.
665 #
666 set cmd [list git commit-tree $tree_id]
667 if {$PARENT != {}} {
668 lappend cmd -p $PARENT
669 }
670 if {$commit_type == {merge}} {
671 if {[catch {
672 set fd_mh [open [file join $gitdir MERGE_HEAD] r]
673 while {[gets $fd_mh merge_head] >= 0} {
674 lappend cmd -p $merge_head
675 }
676 close $fd_mh
677 } err]} {
678 error_popup "Loading MERGE_HEAD failed:\n\n$err"
679 set commit_active 0
680 set ui_status_value {Commit failed.}
681 unlock_index
682 return
683 }
684 }
685 if {$PARENT == {}} {
686 # git commit-tree writes to stderr during initial commit.
687 lappend cmd 2>/dev/null
688 }
689 lappend cmd << $msg
690 if {[catch {set cmt_id [eval exec $cmd]} err]} {
691 error_popup "commit-tree failed:\n\n$err"
692 set commit_active 0
693 set ui_status_value {Commit failed.}
694 unlock_index
695 return
696 }
697
698 # -- Update the HEAD ref.
699 #
700 set reflogm commit
701 if {$commit_type != {normal}} {
702 append reflogm " ($commit_type)"
703 }
704 set i [string first "\n" $msg]
705 if {$i >= 0} {
706 append reflogm {: } [string range $msg 0 [expr $i - 1]]
707 } else {
708 append reflogm {: } $msg
709 }
710 set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD]
711 if {[catch {eval exec $cmd} err]} {
712 error_popup "update-ref failed:\n\n$err"
713 set commit_active 0
714 set ui_status_value {Commit failed.}
715 unlock_index
716 return
717 }
718
719 # -- Cleanup after ourselves.
720 #
721 catch {file delete [file join $gitdir MERGE_HEAD]}
722 catch {file delete [file join $gitdir MERGE_MSG]}
723 catch {file delete [file join $gitdir SQUASH_MSG]}
724 catch {file delete [file join $gitdir GITGUI_MSG]}
725
726 # -- Let rerere do its thing.
727 #
728 if {[file isdirectory [file join $gitdir rr-cache]]} {
729 catch {exec git rerere}
730 }
731
732 $ui_comm delete 0.0 end
733 $ui_comm edit modified false
734 $ui_comm edit reset
735
736 if {$single_commit} do_quit
737
738 # -- Update status without invoking any git commands.
739 #
740 set commit_active 0
741 set commit_type normal
742 set HEAD $cmt_id
743 set PARENT $cmt_id
744
745 foreach path [array names file_states] {
746 set s $file_states($path)
747 set m [lindex $s 0]
748 switch -glob -- $m {
749 A? -
750 M? -
751 D? {set m _[string index $m 1]}
752 }
753
754 if {$m == {__}} {
755 unset file_states($path)
756 } else {
757 lset file_states($path) 0 $m
758 }
759 }
760
761 display_all_files
762 unlock_index
763 reshow_diff
764 set ui_status_value \
765 "Changes committed as [string range $cmt_id 0 7]."
766}
767
768######################################################################
769##
770## fetch pull push
771
772proc fetch_from {remote} {
773 set w [new_console "fetch $remote" \
774 "Fetching new changes from $remote"]
775 set cmd [list git fetch]
776 lappend cmd $remote
777 console_exec $w $cmd
778}
779
780proc pull_remote {remote branch} {
781 global HEAD commit_type
782 global file_states
783
784 if {![lock_index update]} return
785
786 # -- Our in memory state should match the repository.
787 #
788 repository_state curHEAD cur_type
789 if {$commit_type != $cur_type || $HEAD != $curHEAD} {
790 error_popup {Last scanned state does not match repository state.
791
792Its highly likely that another Git program modified the
793repository since our last scan. A rescan is required
794before a pull can be started.
795}
796 unlock_index
797 update_status
798 return
799 }
800
801 # -- No differences should exist before a pull.
802 #
803 if {[array size file_states] != 0} {
804 error_popup {Uncommitted but modified files are present.
805
806You should not perform a pull with unmodified files in your working
807directory as Git would be unable to recover from an incorrect merge.
808
809Commit or throw away all changes before starting a pull operation.
810}
811 unlock_index
812 return
813 }
814
815 set w [new_console "pull $remote $branch" \
816 "Pulling new changes from branch $branch in $remote"]
817 set cmd [list git pull]
818 lappend cmd $remote
819 lappend cmd $branch
820 console_exec $w $cmd [list post_pull_remote $remote $branch]
821}
822
823proc post_pull_remote {remote branch success} {
824 global HEAD PARENT commit_type
825 global ui_status_value
826
827 unlock_index
828 if {$success} {
829 repository_state HEAD commit_type
830 set PARENT $HEAD
831 set $ui_status_value {Ready.}
832 } else {
833 update_status "Conflicts detected while pulling $branch from $remote."
834 }
835}
836
837proc push_to {remote} {
838 set w [new_console "push $remote" \
839 "Pushing changes to $remote"]
840 set cmd [list git push]
841 lappend cmd $remote
842 console_exec $w $cmd
843}
844
845######################################################################
846##
847## ui helpers
848
849proc mapcol {state path} {
850 global all_cols ui_other
851
852 if {[catch {set r $all_cols($state)}]} {
853 puts "error: no column for state={$state} $path"
854 return $ui_other
855 }
856 return $r
857}
858
859proc mapicon {state path} {
860 global all_icons
861
862 if {[catch {set r $all_icons($state)}]} {
863 puts "error: no icon for state={$state} $path"
864 return file_plain
865 }
866 return $r
867}
868
869proc mapdesc {state path} {
870 global all_descs
871
872 if {[catch {set r $all_descs($state)}]} {
873 puts "error: no desc for state={$state} $path"
874 return $state
875 }
876 return $r
877}
878
879proc escape_path {path} {
880 regsub -all "\n" $path "\\n" path
881 return $path
882}
883
884set next_icon_id 0
885
886proc merge_state {path new_state} {
887 global file_states next_icon_id
888
889 set s0 [string index $new_state 0]
890 set s1 [string index $new_state 1]
891
892 if {[catch {set info $file_states($path)}]} {
893 set state __
894 set icon n[incr next_icon_id]
895 } else {
896 set state [lindex $info 0]
897 set icon [lindex $info 1]
898 }
899
900 if {$s0 == {_}} {
901 set s0 [string index $state 0]
902 } elseif {$s0 == {*}} {
903 set s0 _
904 }
905
906 if {$s1 == {_}} {
907 set s1 [string index $state 1]
908 } elseif {$s1 == {*}} {
909 set s1 _
910 }
911
912 set file_states($path) [list $s0$s1 $icon]
913 return $state
914}
915
916proc display_file {path state} {
917 global ui_index ui_other
918 global file_states file_lists status_active
919
920 set old_m [merge_state $path $state]
921 if {$status_active} return
922
923 set s $file_states($path)
924 set new_m [lindex $s 0]
925 set new_w [mapcol $new_m $path]
926 set old_w [mapcol $old_m $path]
927 set new_icon [mapicon $new_m $path]
928
929 if {$new_w != $old_w} {
930 set lno [lsearch -sorted $file_lists($old_w) $path]
931 if {$lno >= 0} {
932 incr lno
933 $old_w conf -state normal
934 $old_w delete $lno.0 [expr $lno + 1].0
935 $old_w conf -state disabled
936 }
937
938 lappend file_lists($new_w) $path
939 set file_lists($new_w) [lsort $file_lists($new_w)]
940 set lno [lsearch -sorted $file_lists($new_w) $path]
941 incr lno
942 $new_w conf -state normal
943 $new_w image create $lno.0 \
944 -align center -padx 5 -pady 1 \
945 -name [lindex $s 1] \
946 -image $new_icon
947 $new_w insert $lno.1 "[escape_path $path]\n"
948 $new_w conf -state disabled
949 } elseif {$new_icon != [mapicon $old_m $path]} {
950 $new_w conf -state normal
951 $new_w image conf [lindex $s 1] -image $new_icon
952 $new_w conf -state disabled
953 }
954}
955
956proc display_all_files {} {
957 global ui_index ui_other file_states file_lists
958
959 $ui_index conf -state normal
960 $ui_other conf -state normal
961
962 $ui_index delete 0.0 end
963 $ui_other delete 0.0 end
964
965 array unset file_lists
966 foreach path [lsort [array names file_states]] {
967 set s $file_states($path)
968 set m [lindex $s 0]
969 set w [mapcol $m $path]
970 lappend file_lists($w) $path
971 $w image create end \
972 -align center -padx 5 -pady 1 \
973 -name [lindex $s 1] \
974 -image [mapicon $m $path]
975 $w insert end "[escape_path $path]\n"
976 }
977
978 $ui_index conf -state disabled
979 $ui_other conf -state disabled
980}
981
982proc with_update_index {body} {
983 global update_index_fd
984
985 if {$update_index_fd == {}} {
986 if {![lock_index update]} return
987 set update_index_fd [open \
988 "| git update-index --add --remove -z --stdin" \
989 w]
990 fconfigure $update_index_fd -translation binary
991 uplevel 1 $body
992 close $update_index_fd
993 set update_index_fd {}
994 unlock_index
995 } else {
996 uplevel 1 $body
997 }
998}
999
1000proc update_index {path} {
1001 global update_index_fd
1002
1003 if {$update_index_fd == {}} {
1004 error {not in with_update_index}
1005 } else {
1006 puts -nonewline $update_index_fd "$path\0"
1007 }
1008}
1009
1010proc toggle_mode {path} {
1011 global file_states ui_fname_value
1012
1013 set s $file_states($path)
1014 set m [lindex $s 0]
1015
1016 switch -- $m {
1017 AM -
1018 _O {set new A*}
1019 _M -
1020 MM {set new M*}
1021 AD -
1022 _D {set new D*}
1023 default {return}
1024 }
1025
1026 with_update_index {update_index $path}
1027 display_file $path $new
1028 if {$ui_fname_value == $path} {
1029 show_diff $path
1030 }
1031}
1032
1033######################################################################
1034##
1035## remote management
1036
1037proc load_all_remotes {} {
1038 global gitdir all_remotes repo_config
1039
1040 set all_remotes [list]
1041 set rm_dir [file join $gitdir remotes]
1042 if {[file isdirectory $rm_dir]} {
1043 set all_remotes [concat $all_remotes [glob \
1044 -types f \
1045 -tails \
1046 -nocomplain \
1047 -directory $rm_dir *]]
1048 }
1049
1050 foreach line [array names repo_config remote.*.url] {
1051 if {[regexp ^remote\.(.*)\.url\$ $line line name]} {
1052 lappend all_remotes $name
1053 }
1054 }
1055
1056 set all_remotes [lsort -unique $all_remotes]
1057}
1058
1059proc populate_remote_menu {m pfx op} {
1060 global all_remotes font_ui
1061
1062 foreach remote $all_remotes {
1063 $m add command -label "$pfx $remote..." \
1064 -command [list $op $remote] \
1065 -font $font_ui
1066 }
1067}
1068
1069proc populate_pull_menu {m} {
1070 global gitdir repo_config all_remotes font_ui disable_on_lock
1071
1072 foreach remote $all_remotes {
1073 set rb {}
1074 if {[array get repo_config remote.$remote.url] != {}} {
1075 if {[array get repo_config remote.$remote.fetch] != {}} {
1076 regexp {^([^:]+):} \
1077 [lindex $repo_config(remote.$remote.fetch) 0] \
1078 line rb
1079 }
1080 } else {
1081 catch {
1082 set fd [open [file join $gitdir remotes $remote] r]
1083 while {[gets $fd line] >= 0} {
1084 if {[regexp {^Pull:[ \t]*([^:]+):} $line line rb]} {
1085 break
1086 }
1087 }
1088 close $fd
1089 }
1090 }
1091
1092 set rb_short $rb
1093 regsub ^refs/heads/ $rb {} rb_short
1094 if {$rb_short != {}} {
1095 $m add command \
1096 -label "Branch $rb_short from $remote..." \
1097 -command [list pull_remote $remote $rb] \
1098 -font $font_ui
1099 lappend disable_on_lock \
1100 [list $m entryconf [$m index last] -state]
1101 }
1102 }
1103}
1104
1105######################################################################
1106##
1107## icons
1108
1109set filemask {
1110#define mask_width 14
1111#define mask_height 15
1112static unsigned char mask_bits[] = {
1113 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1114 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1115 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
1116}
1117
1118image create bitmap file_plain -background white -foreground black -data {
1119#define plain_width 14
1120#define plain_height 15
1121static unsigned char plain_bits[] = {
1122 0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1123 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
1124 0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1125} -maskdata $filemask
1126
1127image create bitmap file_mod -background white -foreground blue -data {
1128#define mod_width 14
1129#define mod_height 15
1130static unsigned char mod_bits[] = {
1131 0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1132 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1133 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1134} -maskdata $filemask
1135
1136image create bitmap file_fulltick -background white -foreground "#007000" -data {
1137#define file_fulltick_width 14
1138#define file_fulltick_height 15
1139static unsigned char file_fulltick_bits[] = {
1140 0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
1141 0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
1142 0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1143} -maskdata $filemask
1144
1145image create bitmap file_parttick -background white -foreground "#005050" -data {
1146#define parttick_width 14
1147#define parttick_height 15
1148static unsigned char parttick_bits[] = {
1149 0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1150 0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
1151 0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1152} -maskdata $filemask
1153
1154image create bitmap file_question -background white -foreground black -data {
1155#define file_question_width 14
1156#define file_question_height 15
1157static unsigned char file_question_bits[] = {
1158 0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
1159 0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
1160 0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1161} -maskdata $filemask
1162
1163image create bitmap file_removed -background white -foreground red -data {
1164#define file_removed_width 14
1165#define file_removed_height 15
1166static unsigned char file_removed_bits[] = {
1167 0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1168 0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
1169 0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
1170} -maskdata $filemask
1171
1172image create bitmap file_merge -background white -foreground blue -data {
1173#define file_merge_width 14
1174#define file_merge_height 15
1175static unsigned char file_merge_bits[] = {
1176 0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
1177 0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1178 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1179} -maskdata $filemask
1180
1181set ui_index .vpane.files.index.list
1182set ui_other .vpane.files.other.list
1183set max_status_desc 0
1184foreach i {
1185 {__ i plain "Unmodified"}
1186 {_M i mod "Modified"}
1187 {M_ i fulltick "Checked in"}
1188 {MM i parttick "Partially included"}
1189
1190 {_O o plain "Untracked"}
1191 {A_ o fulltick "Added"}
1192 {AM o parttick "Partially added"}
1193 {AD o question "Added (but now gone)"}
1194
1195 {_D i question "Missing"}
1196 {D_ i removed "Removed"}
1197 {DD i removed "Removed"}
1198 {DO i removed "Removed (still exists)"}
1199
1200 {UM i merge "Merge conflicts"}
1201 {U_ i merge "Merge conflicts"}
1202 } {
1203 if {$max_status_desc < [string length [lindex $i 3]]} {
1204 set max_status_desc [string length [lindex $i 3]]
1205 }
1206 if {[lindex $i 1] == {i}} {
1207 set all_cols([lindex $i 0]) $ui_index
1208 } else {
1209 set all_cols([lindex $i 0]) $ui_other
1210 }
1211 set all_icons([lindex $i 0]) file_[lindex $i 2]
1212 set all_descs([lindex $i 0]) [lindex $i 3]
1213}
1214unset filemask i
1215
1216######################################################################
1217##
1218## util
1219
1220proc hook_failed_popup {hook msg} {
1221 global gitdir font_ui font_diff appname
1222
1223 set w .hookfail
1224 toplevel $w
1225 wm transient $w .
1226
1227 frame $w.m
1228 label $w.m.l1 -text "$hook hook failed:" \
1229 -anchor w \
1230 -justify left \
1231 -font [concat $font_ui bold]
1232 text $w.m.t \
1233 -background white -borderwidth 1 \
1234 -relief sunken \
1235 -width 80 -height 10 \
1236 -font $font_diff \
1237 -yscrollcommand [list $w.m.sby set]
1238 label $w.m.l2 \
1239 -text {You must correct the above errors before committing.} \
1240 -anchor w \
1241 -justify left \
1242 -font [concat $font_ui bold]
1243 scrollbar $w.m.sby -command [list $w.m.t yview]
1244 pack $w.m.l1 -side top -fill x
1245 pack $w.m.l2 -side bottom -fill x
1246 pack $w.m.sby -side right -fill y
1247 pack $w.m.t -side left -fill both -expand 1
1248 pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
1249
1250 $w.m.t insert 1.0 $msg
1251 $w.m.t conf -state disabled
1252
1253 button $w.ok -text OK \
1254 -width 15 \
1255 -font $font_ui \
1256 -command "destroy $w"
1257 pack $w.ok -side bottom
1258
1259 bind $w <Visibility> "grab $w; focus $w"
1260 bind $w <Key-Return> "destroy $w"
1261 wm title $w "$appname ([lindex [file split \
1262 [file normalize [file dirname $gitdir]]] \
1263 end]): error"
1264 tkwait window $w
1265}
1266
1267set next_console_id 0
1268
1269proc new_console {short_title long_title} {
1270 global next_console_id console_data
1271 set w .console[incr next_console_id]
1272 set console_data($w) [list $short_title $long_title]
1273 return [console_init $w]
1274}
1275
1276proc console_init {w} {
1277 global console_cr console_data
1278 global gitdir appname font_ui font_diff
1279
1280 set console_cr($w) 1.0
1281 toplevel $w
1282 frame $w.m
1283 label $w.m.l1 -text "[lindex $console_data($w) 1]:" \
1284 -anchor w \
1285 -justify left \
1286 -font [concat $font_ui bold]
1287 text $w.m.t \
1288 -background white -borderwidth 1 \
1289 -relief sunken \
1290 -width 80 -height 10 \
1291 -font $font_diff \
1292 -state disabled \
1293 -yscrollcommand [list $w.m.sby set]
1294 label $w.m.s -anchor w \
1295 -justify left \
1296 -font [concat $font_ui bold]
1297 scrollbar $w.m.sby -command [list $w.m.t yview]
1298 pack $w.m.l1 -side top -fill x
1299 pack $w.m.s -side bottom -fill x
1300 pack $w.m.sby -side right -fill y
1301 pack $w.m.t -side left -fill both -expand 1
1302 pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
1303
1304 button $w.ok -text {Running...} \
1305 -width 15 \
1306 -font $font_ui \
1307 -state disabled \
1308 -command "destroy $w"
1309 pack $w.ok -side bottom
1310
1311 bind $w <Visibility> "focus $w"
1312 wm title $w "$appname ([lindex [file split \
1313 [file normalize [file dirname $gitdir]]] \
1314 end]): [lindex $console_data($w) 0]"
1315 return $w
1316}
1317
1318proc console_exec {w cmd {after {}}} {
1319 global tcl_platform
1320
1321 # -- Windows tosses the enviroment when we exec our child.
1322 # But most users need that so we have to relogin. :-(
1323 #
1324 if {$tcl_platform(platform) == {windows}} {
1325 set cmd [list sh --login -c "cd \"[pwd]\" && [join $cmd { }]"]
1326 }
1327
1328 # -- Tcl won't let us redirect both stdout and stderr to
1329 # the same pipe. So pass it through cat...
1330 #
1331 set cmd [concat | $cmd |& cat]
1332
1333 set fd_f [open $cmd r]
1334 fconfigure $fd_f -blocking 0 -translation binary
1335 fileevent $fd_f readable [list console_read $w $fd_f $after]
1336}
1337
1338proc console_read {w fd after} {
1339 global console_cr console_data
1340
1341 set buf [read $fd]
1342 if {$buf != {}} {
1343 if {![winfo exists $w]} {console_init $w}
1344 $w.m.t conf -state normal
1345 set c 0
1346 set n [string length $buf]
1347 while {$c < $n} {
1348 set cr [string first "\r" $buf $c]
1349 set lf [string first "\n" $buf $c]
1350 if {$cr < 0} {set cr [expr $n + 1]}
1351 if {$lf < 0} {set lf [expr $n + 1]}
1352
1353 if {$lf < $cr} {
1354 $w.m.t insert end [string range $buf $c $lf]
1355 set console_cr($w) [$w.m.t index {end -1c}]
1356 set c $lf
1357 incr c
1358 } else {
1359 $w.m.t delete $console_cr($w) end
1360 $w.m.t insert end "\n"
1361 $w.m.t insert end [string range $buf $c $cr]
1362 set c $cr
1363 incr c
1364 }
1365 }
1366 $w.m.t conf -state disabled
1367 $w.m.t see end
1368 }
1369
1370 fconfigure $fd -blocking 1
1371 if {[eof $fd]} {
1372 if {[catch {close $fd}]} {
1373 if {![winfo exists $w]} {console_init $w}
1374 $w.m.s conf -background red -text {Error: Command Failed}
1375 $w.ok conf -text Close
1376 $w.ok conf -state normal
1377 set ok 0
1378 } elseif {[winfo exists $w]} {
1379 $w.m.s conf -background green -text {Success}
1380 $w.ok conf -text Close
1381 $w.ok conf -state normal
1382 set ok 1
1383 }
1384 array unset console_cr $w
1385 array unset console_data $w
1386 if {$after != {}} {
1387 uplevel #0 $after $ok
1388 }
1389 return
1390 }
1391 fconfigure $fd -blocking 0
1392}
1393
1394######################################################################
1395##
1396## ui commands
1397
1398set starting_gitk_msg {Please wait... Starting gitk...}
1399
1400proc do_gitk {} {
1401 global tcl_platform ui_status_value starting_gitk_msg
1402
1403 set ui_status_value $starting_gitk_msg
1404 after 10000 {
1405 if {$ui_status_value == $starting_gitk_msg} {
1406 set ui_status_value {Ready.}
1407 }
1408 }
1409
1410 if {$tcl_platform(platform) == {windows}} {
1411 exec sh -c gitk &
1412 } else {
1413 exec gitk &
1414 }
1415}
1416
1417proc do_repack {} {
1418 set w [new_console "repack" "Repacking the object database"]
1419 set cmd [list git repack]
1420 lappend cmd -a
1421 lappend cmd -d
1422 console_exec $w $cmd
1423}
1424
1425proc do_quit {} {
1426 global gitdir ui_comm
1427
1428 set save [file join $gitdir GITGUI_MSG]
1429 set msg [string trim [$ui_comm get 0.0 end]]
1430 if {[$ui_comm edit modified] && $msg != {}} {
1431 catch {
1432 set fd [open $save w]
1433 puts $fd [string trim [$ui_comm get 0.0 end]]
1434 close $fd
1435 }
1436 } elseif {$msg == {} && [file exists $save]} {
1437 file delete $save
1438 }
1439
1440 save_my_config
1441 destroy .
1442}
1443
1444proc do_rescan {} {
1445 update_status
1446}
1447
1448proc do_include_all {} {
1449 global update_active ui_status_value
1450
1451 if {$update_active || ![lock_index begin-update]} return
1452
1453 set update_active 1
1454 set ui_status_value {Including all modified files...}
1455 after 1 {
1456 with_update_index {
1457 foreach path [array names file_states] {
1458 set s $file_states($path)
1459 set m [lindex $s 0]
1460 switch -- $m {
1461 AM -
1462 MM -
1463 _M -
1464 _D {toggle_mode $path}
1465 }
1466 }
1467 }
1468 set update_active 0
1469 set ui_status_value {Ready.}
1470 }
1471}
1472
1473set GIT_COMMITTER_IDENT {}
1474
1475proc do_signoff {} {
1476 global ui_comm GIT_COMMITTER_IDENT
1477
1478 if {$GIT_COMMITTER_IDENT == {}} {
1479 if {[catch {set me [exec git var GIT_COMMITTER_IDENT]} err]} {
1480 error_popup "Unable to obtain your identity:\n\n$err"
1481 return
1482 }
1483 if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
1484 $me me GIT_COMMITTER_IDENT]} {
1485 error_popup "Invalid GIT_COMMITTER_IDENT:\n\n$me"
1486 return
1487 }
1488 }
1489
1490 set str "Signed-off-by: $GIT_COMMITTER_IDENT"
1491 if {[$ui_comm get {end -1c linestart} {end -1c}] != $str} {
1492 $ui_comm edit separator
1493 $ui_comm insert end "\n$str"
1494 $ui_comm edit separator
1495 $ui_comm see end
1496 }
1497}
1498
1499proc do_amend_last {} {
1500 load_last_commit
1501}
1502
1503proc do_commit {} {
1504 commit_tree
1505}
1506
1507# shift == 1: left click
1508# 3: right click
1509proc click {w x y shift wx wy} {
1510 global ui_index ui_other file_lists
1511
1512 set pos [split [$w index @$x,$y] .]
1513 set lno [lindex $pos 0]
1514 set col [lindex $pos 1]
1515 set path [lindex $file_lists($w) [expr $lno - 1]]
1516 if {$path == {}} return
1517
1518 if {$col > 0 && $shift == 1} {
1519 show_diff $path $w $lno
1520 }
1521}
1522
1523proc unclick {w x y} {
1524 global file_lists
1525
1526 set pos [split [$w index @$x,$y] .]
1527 set lno [lindex $pos 0]
1528 set col [lindex $pos 1]
1529 set path [lindex $file_lists($w) [expr $lno - 1]]
1530 if {$path == {}} return
1531
1532 if {$col == 0} {
1533 toggle_mode $path
1534 }
1535}
1536
1537######################################################################
1538##
1539## ui init
1540
1541set font_ui {Helvetica 10}
1542set font_diff {Courier 10}
1543set maincursor [. cget -cursor]
1544
1545switch -glob -- "$tcl_platform(platform),$tcl_platform(os)" {
1546windows,* {set M1B Control; set M1T Ctrl}
1547unix,Darwin {set M1B M1; set M1T Cmd}
1548default {set M1B M1; set M1T M1}
1549}
1550
1551# -- Menu Bar
1552menu .mbar -tearoff 0
1553.mbar add cascade -label Project -menu .mbar.project
1554.mbar add cascade -label Edit -menu .mbar.edit
1555.mbar add cascade -label Commit -menu .mbar.commit
1556.mbar add cascade -label Fetch -menu .mbar.fetch
1557.mbar add cascade -label Pull -menu .mbar.pull
1558.mbar add cascade -label Push -menu .mbar.push
1559.mbar add cascade -label Options -menu .mbar.options
1560. configure -menu .mbar
1561
1562# -- Project Menu
1563menu .mbar.project
1564.mbar.project add command -label Visualize \
1565 -command do_gitk \
1566 -font $font_ui
1567.mbar.project add command -label {Repack Database} \
1568 -command do_repack \
1569 -font $font_ui
1570.mbar.project add command -label Quit \
1571 -command do_quit \
1572 -accelerator $M1T-Q \
1573 -font $font_ui
1574
1575# -- Edit Menu
1576#
1577menu .mbar.edit
1578.mbar.edit add command -label Undo \
1579 -command {catch {[focus] edit undo}} \
1580 -accelerator $M1T-Z \
1581 -font $font_ui
1582.mbar.edit add command -label Redo \
1583 -command {catch {[focus] edit redo}} \
1584 -accelerator $M1T-Y \
1585 -font $font_ui
1586.mbar.edit add separator
1587.mbar.edit add command -label Cut \
1588 -command {catch {tk_textCut [focus]}} \
1589 -accelerator $M1T-X \
1590 -font $font_ui
1591.mbar.edit add command -label Copy \
1592 -command {catch {tk_textCopy [focus]}} \
1593 -accelerator $M1T-C \
1594 -font $font_ui
1595.mbar.edit add command -label Paste \
1596 -command {catch {tk_textPaste [focus]; [focus] see insert}} \
1597 -accelerator $M1T-V \
1598 -font $font_ui
1599.mbar.edit add command -label Delete \
1600 -command {catch {[focus] delete sel.first sel.last}} \
1601 -accelerator Del \
1602 -font $font_ui
1603.mbar.edit add separator
1604.mbar.edit add command -label {Select All} \
1605 -command {catch {[focus] tag add sel 0.0 end}} \
1606 -accelerator $M1T-A \
1607 -font $font_ui
1608
1609# -- Commit Menu
1610menu .mbar.commit
1611.mbar.commit add command -label Rescan \
1612 -command do_rescan \
1613 -accelerator F5 \
1614 -font $font_ui
1615lappend disable_on_lock \
1616 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1617.mbar.commit add command -label {Amend Last Commit} \
1618 -command do_amend_last \
1619 -font $font_ui
1620lappend disable_on_lock \
1621 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1622.mbar.commit add command -label {Include All Files} \
1623 -command do_include_all \
1624 -accelerator $M1T-I \
1625 -font $font_ui
1626lappend disable_on_lock \
1627 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1628.mbar.commit add command -label {Sign Off} \
1629 -command do_signoff \
1630 -accelerator $M1T-S \
1631 -font $font_ui
1632.mbar.commit add command -label Commit \
1633 -command do_commit \
1634 -accelerator $M1T-Return \
1635 -font $font_ui
1636lappend disable_on_lock \
1637 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1638
1639# -- Fetch Menu
1640menu .mbar.fetch
1641
1642# -- Pull Menu
1643menu .mbar.pull
1644
1645# -- Push Menu
1646menu .mbar.push
1647
1648# -- Options Menu
1649menu .mbar.options
1650.mbar.options add checkbutton \
1651 -label {Trust File Modification Timestamps} \
1652 -offvalue false \
1653 -onvalue true \
1654 -variable cfg_trust_mtime
1655
1656# -- Main Window Layout
1657panedwindow .vpane -orient vertical
1658panedwindow .vpane.files -orient horizontal
1659.vpane add .vpane.files -sticky nsew -height 100 -width 400
1660pack .vpane -anchor n -side top -fill both -expand 1
1661
1662# -- Index File List
1663frame .vpane.files.index -height 100 -width 400
1664label .vpane.files.index.title -text {Modified Files} \
1665 -background green \
1666 -font $font_ui
1667text $ui_index -background white -borderwidth 0 \
1668 -width 40 -height 10 \
1669 -font $font_ui \
1670 -yscrollcommand {.vpane.files.index.sb set} \
1671 -cursor $maincursor \
1672 -state disabled
1673scrollbar .vpane.files.index.sb -command [list $ui_index yview]
1674pack .vpane.files.index.title -side top -fill x
1675pack .vpane.files.index.sb -side right -fill y
1676pack $ui_index -side left -fill both -expand 1
1677.vpane.files add .vpane.files.index -sticky nsew
1678
1679# -- Other (Add) File List
1680frame .vpane.files.other -height 100 -width 100
1681label .vpane.files.other.title -text {Untracked Files} \
1682 -background red \
1683 -font $font_ui
1684text $ui_other -background white -borderwidth 0 \
1685 -width 40 -height 10 \
1686 -font $font_ui \
1687 -yscrollcommand {.vpane.files.other.sb set} \
1688 -cursor $maincursor \
1689 -state disabled
1690scrollbar .vpane.files.other.sb -command [list $ui_other yview]
1691pack .vpane.files.other.title -side top -fill x
1692pack .vpane.files.other.sb -side right -fill y
1693pack $ui_other -side left -fill both -expand 1
1694.vpane.files add .vpane.files.other -sticky nsew
1695
1696$ui_index tag conf in_diff -font [concat $font_ui bold]
1697$ui_other tag conf in_diff -font [concat $font_ui bold]
1698
1699# -- Diff and Commit Area
1700frame .vpane.lower -height 400 -width 400
1701frame .vpane.lower.commarea
1702frame .vpane.lower.diff -relief sunken -borderwidth 1
1703pack .vpane.lower.commarea -side top -fill x
1704pack .vpane.lower.diff -side bottom -fill both -expand 1
1705.vpane add .vpane.lower -stick nsew
1706
1707# -- Commit Area Buttons
1708frame .vpane.lower.commarea.buttons
1709label .vpane.lower.commarea.buttons.l -text {} \
1710 -anchor w \
1711 -justify left \
1712 -font $font_ui
1713pack .vpane.lower.commarea.buttons.l -side top -fill x
1714pack .vpane.lower.commarea.buttons -side left -fill y
1715
1716button .vpane.lower.commarea.buttons.rescan -text {Rescan} \
1717 -command do_rescan \
1718 -font $font_ui
1719pack .vpane.lower.commarea.buttons.rescan -side top -fill x
1720lappend disable_on_lock {.vpane.lower.commarea.buttons.rescan conf -state}
1721
1722button .vpane.lower.commarea.buttons.amend -text {Amend Last} \
1723 -command do_amend_last \
1724 -font $font_ui
1725pack .vpane.lower.commarea.buttons.amend -side top -fill x
1726lappend disable_on_lock {.vpane.lower.commarea.buttons.amend conf -state}
1727
1728button .vpane.lower.commarea.buttons.incall -text {Include All} \
1729 -command do_include_all \
1730 -font $font_ui
1731pack .vpane.lower.commarea.buttons.incall -side top -fill x
1732lappend disable_on_lock {.vpane.lower.commarea.buttons.incall conf -state}
1733
1734button .vpane.lower.commarea.buttons.signoff -text {Sign Off} \
1735 -command do_signoff \
1736 -font $font_ui
1737pack .vpane.lower.commarea.buttons.signoff -side top -fill x
1738
1739button .vpane.lower.commarea.buttons.commit -text {Commit} \
1740 -command do_commit \
1741 -font $font_ui
1742pack .vpane.lower.commarea.buttons.commit -side top -fill x
1743lappend disable_on_lock {.vpane.lower.commarea.buttons.commit conf -state}
1744
1745# -- Commit Message Buffer
1746frame .vpane.lower.commarea.buffer
1747set ui_comm .vpane.lower.commarea.buffer.t
1748set ui_coml .vpane.lower.commarea.buffer.l
1749label $ui_coml -text {Commit Message:} \
1750 -anchor w \
1751 -justify left \
1752 -font $font_ui
1753trace add variable commit_type write {uplevel #0 {
1754 switch -glob $commit_type \
1755 initial {$ui_coml conf -text {Initial Commit Message:}} \
1756 amend {$ui_coml conf -text {Amended Commit Message:}} \
1757 merge {$ui_coml conf -text {Merge Commit Message:}} \
1758 * {$ui_coml conf -text {Commit Message:}}
1759}}
1760text $ui_comm -background white -borderwidth 1 \
1761 -undo true \
1762 -maxundo 20 \
1763 -autoseparators true \
1764 -relief sunken \
1765 -width 75 -height 9 -wrap none \
1766 -font $font_diff \
1767 -yscrollcommand {.vpane.lower.commarea.buffer.sby set} \
1768 -cursor $maincursor
1769scrollbar .vpane.lower.commarea.buffer.sby -command [list $ui_comm yview]
1770pack $ui_coml -side top -fill x
1771pack .vpane.lower.commarea.buffer.sby -side right -fill y
1772pack $ui_comm -side left -fill y
1773pack .vpane.lower.commarea.buffer -side left -fill y
1774
1775# -- Diff Header
1776set ui_fname_value {}
1777set ui_fstatus_value {}
1778frame .vpane.lower.diff.header -background orange
1779label .vpane.lower.diff.header.l1 -text {File:} \
1780 -background orange \
1781 -font $font_ui
1782label .vpane.lower.diff.header.l2 -textvariable ui_fname_value \
1783 -background orange \
1784 -anchor w \
1785 -justify left \
1786 -font $font_ui
1787label .vpane.lower.diff.header.l3 -text {Status:} \
1788 -background orange \
1789 -font $font_ui
1790label .vpane.lower.diff.header.l4 -textvariable ui_fstatus_value \
1791 -background orange \
1792 -width $max_status_desc \
1793 -anchor w \
1794 -justify left \
1795 -font $font_ui
1796pack .vpane.lower.diff.header.l1 -side left
1797pack .vpane.lower.diff.header.l2 -side left -fill x
1798pack .vpane.lower.diff.header.l4 -side right
1799pack .vpane.lower.diff.header.l3 -side right
1800
1801# -- Diff Body
1802frame .vpane.lower.diff.body
1803set ui_diff .vpane.lower.diff.body.t
1804text $ui_diff -background white -borderwidth 0 \
1805 -width 80 -height 15 -wrap none \
1806 -font $font_diff \
1807 -xscrollcommand {.vpane.lower.diff.body.sbx set} \
1808 -yscrollcommand {.vpane.lower.diff.body.sby set} \
1809 -cursor $maincursor \
1810 -state disabled
1811scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
1812 -command [list $ui_diff xview]
1813scrollbar .vpane.lower.diff.body.sby -orient vertical \
1814 -command [list $ui_diff yview]
1815pack .vpane.lower.diff.body.sbx -side bottom -fill x
1816pack .vpane.lower.diff.body.sby -side right -fill y
1817pack $ui_diff -side left -fill both -expand 1
1818pack .vpane.lower.diff.header -side top -fill x
1819pack .vpane.lower.diff.body -side bottom -fill both -expand 1
1820
1821$ui_diff tag conf dm -foreground red
1822$ui_diff tag conf dp -foreground blue
1823$ui_diff tag conf da -font [concat $font_diff bold]
1824$ui_diff tag conf di -foreground "#00a000"
1825$ui_diff tag conf dni -foreground "#a000a0"
1826$ui_diff tag conf bold -font [concat $font_diff bold]
1827
1828# -- Status Bar
1829set ui_status_value {Initializing...}
1830label .status -textvariable ui_status_value \
1831 -anchor w \
1832 -justify left \
1833 -borderwidth 1 \
1834 -relief sunken \
1835 -font $font_ui
1836pack .status -anchor w -side bottom -fill x
1837
1838# -- Load geometry
1839catch {
1840wm geometry . [lindex $repo_config(gui.geometry) 0 0]
1841eval .vpane sash place 0 [lindex $repo_config(gui.geometry) 0 1]
1842eval .vpane.files sash place 0 [lindex $repo_config(gui.geometry) 0 2]
1843}
1844
1845# -- Key Bindings
1846bind $ui_comm <$M1B-Key-Return> {do_commit;break}
1847bind $ui_comm <$M1B-Key-i> {do_include_all;break}
1848bind $ui_comm <$M1B-Key-I> {do_include_all;break}
1849bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
1850bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
1851bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
1852bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
1853bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
1854bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
1855bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
1856bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
1857
1858bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
1859bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
1860bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
1861bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
1862bind $ui_diff <$M1B-Key-v> {break}
1863bind $ui_diff <$M1B-Key-V> {break}
1864bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
1865bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
1866bind $ui_diff <Key-Up> {catch {%W yview scroll -1 units};break}
1867bind $ui_diff <Key-Down> {catch {%W yview scroll 1 units};break}
1868bind $ui_diff <Key-Left> {catch {%W xview scroll -1 units};break}
1869bind $ui_diff <Key-Right> {catch {%W xview scroll 1 units};break}
1870
1871bind . <Destroy> do_quit
1872bind all <Key-F5> do_rescan
1873bind all <$M1B-Key-r> do_rescan
1874bind all <$M1B-Key-R> do_rescan
1875bind . <$M1B-Key-s> do_signoff
1876bind . <$M1B-Key-S> do_signoff
1877bind . <$M1B-Key-i> do_include_all
1878bind . <$M1B-Key-I> do_include_all
1879bind . <$M1B-Key-Return> do_commit
1880bind all <$M1B-Key-q> do_quit
1881bind all <$M1B-Key-Q> do_quit
1882bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
1883bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
1884foreach i [list $ui_index $ui_other] {
1885 bind $i <Button-1> {click %W %x %y 1 %X %Y; break}
1886 bind $i <Button-3> {click %W %x %y 3 %X %Y; break}
1887 bind $i <ButtonRelease-1> {unclick %W %x %y; break}
1888}
1889unset i M1B M1T
1890
1891wm title . "$appname ([file normalize [file dirname $gitdir]])"
1892focus -force $ui_comm
1893load_all_remotes
1894populate_remote_menu .mbar.fetch From fetch_from
1895populate_remote_menu .mbar.push To push_to
1896populate_pull_menu .mbar.pull
1897update_status