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
10######################################################################
11##
12## task management
13
14set single_commit 0
15set status_active 0
16set diff_active 0
17set checkin_active 0
18set commit_active 0
19set update_index_fd {}
20
21set disable_on_lock [list]
22set index_lock_type none
23
24set HEAD {}
25set PARENT {}
26set commit_type {}
27
28proc lock_index {type} {
29 global index_lock_type disable_on_lock
30
31 if {$index_lock_type == {none}} {
32 set index_lock_type $type
33 foreach w $disable_on_lock {
34 uplevel #0 $w disabled
35 }
36 return 1
37 } elseif {$index_lock_type == {begin-update} && $type == {update}} {
38 set index_lock_type $type
39 return 1
40 }
41 return 0
42}
43
44proc unlock_index {} {
45 global index_lock_type disable_on_lock
46
47 set index_lock_type none
48 foreach w $disable_on_lock {
49 uplevel #0 $w normal
50 }
51}
52
53######################################################################
54##
55## status
56
57proc repository_state {hdvar ctvar} {
58 global gitdir
59 upvar $hdvar hd $ctvar ct
60
61 if {[catch {set hd [exec git rev-parse --verify HEAD]}]} {
62 set ct initial
63 } elseif {[file exists [file join $gitdir MERGE_HEAD]]} {
64 set ct merge
65 } else {
66 set ct normal
67 }
68}
69
70proc update_status {{final Ready.}} {
71 global HEAD PARENT commit_type
72 global ui_index ui_other ui_status_value ui_comm
73 global status_active file_states
74
75 if {$status_active || ![lock_index read]} return
76
77 repository_state new_HEAD new_type
78 if {$commit_type == {amend}
79 && $new_type == {normal}
80 && $new_HEAD == $HEAD} {
81 } else {
82 set HEAD $new_HEAD
83 set PARENT $new_HEAD
84 set commit_type $new_type
85 }
86
87 array unset file_states
88 foreach w [list $ui_index $ui_other] {
89 $w conf -state normal
90 $w delete 0.0 end
91 $w conf -state disabled
92 }
93
94 if {![$ui_comm edit modified]
95 || [string trim [$ui_comm get 0.0 end]] == {}} {
96 if {[load_message GITGUI_MSG]} {
97 } elseif {[load_message MERGE_MSG]} {
98 } elseif {[load_message SQUASH_MSG]} {
99 }
100 $ui_comm edit modified false
101 }
102
103 set status_active 1
104 set ui_status_value {Refreshing file status...}
105 set fd_rf [open "| git update-index -q --unmerged --refresh" r]
106 fconfigure $fd_rf -blocking 0 -translation binary
107 fileevent $fd_rf readable [list read_refresh $fd_rf $final]
108}
109
110proc read_refresh {fd final} {
111 global gitdir PARENT commit_type
112 global ui_index ui_other ui_status_value ui_comm
113 global status_active file_states
114
115 read $fd
116 if {![eof $fd]} return
117 close $fd
118
119 set ls_others [list | git ls-files --others -z \
120 --exclude-per-directory=.gitignore]
121 set info_exclude [file join $gitdir info exclude]
122 if {[file readable $info_exclude]} {
123 lappend ls_others "--exclude-from=$info_exclude"
124 }
125
126 set status_active 3
127 set ui_status_value {Scanning for modified files ...}
128 set fd_di [open "| git diff-index --cached -z $PARENT" r]
129 set fd_df [open "| git diff-files -z" r]
130 set fd_lo [open $ls_others r]
131
132 fconfigure $fd_di -blocking 0 -translation binary
133 fconfigure $fd_df -blocking 0 -translation binary
134 fconfigure $fd_lo -blocking 0 -translation binary
135 fileevent $fd_di readable [list read_diff_index $fd_di $final]
136 fileevent $fd_df readable [list read_diff_files $fd_df $final]
137 fileevent $fd_lo readable [list read_ls_others $fd_lo $final]
138}
139
140proc load_message {file} {
141 global gitdir ui_comm
142
143 set f [file join $gitdir $file]
144 if {[file isfile $f]} {
145 if {[catch {set fd [open $f r]}]} {
146 return 0
147 }
148 set content [string trim [read $fd]]
149 close $fd
150 $ui_comm delete 0.0 end
151 $ui_comm insert end $content
152 return 1
153 }
154 return 0
155}
156
157proc read_diff_index {fd final} {
158 global buf_rdi
159
160 append buf_rdi [read $fd]
161 set pck [split $buf_rdi "\0"]
162 set buf_rdi [lindex $pck end]
163 foreach {m p} [lrange $pck 0 end-1] {
164 if {$m != {} && $p != {}} {
165 display_file $p [string index $m end]_
166 }
167 }
168 status_eof $fd buf_rdi $final
169}
170
171proc read_diff_files {fd final} {
172 global buf_rdf
173
174 append buf_rdf [read $fd]
175 set pck [split $buf_rdf "\0"]
176 set buf_rdf [lindex $pck end]
177 foreach {m p} [lrange $pck 0 end-1] {
178 if {$m != {} && $p != {}} {
179 display_file $p _[string index $m end]
180 }
181 }
182 status_eof $fd buf_rdf $final
183}
184
185proc read_ls_others {fd final} {
186 global buf_rlo
187
188 append buf_rlo [read $fd]
189 set pck [split $buf_rlo "\0"]
190 set buf_rlo [lindex $pck end]
191 foreach p [lrange $pck 0 end-1] {
192 display_file $p _O
193 }
194 status_eof $fd buf_rlo $final
195}
196
197proc status_eof {fd buf final} {
198 global status_active $buf
199 global ui_fname_value ui_status_value file_states
200
201 if {[eof $fd]} {
202 set $buf {}
203 close $fd
204 if {[incr status_active -1] == 0} {
205 unlock_index
206
207 set ui_status_value $final
208 if {$ui_fname_value != {} && [array names file_states \
209 -exact $ui_fname_value] != {}} {
210 show_diff $ui_fname_value
211 } else {
212 clear_diff
213 }
214 }
215 }
216}
217
218######################################################################
219##
220## diff
221
222proc clear_diff {} {
223 global ui_diff ui_fname_value ui_fstatus_value
224
225 $ui_diff conf -state normal
226 $ui_diff delete 0.0 end
227 $ui_diff conf -state disabled
228 set ui_fname_value {}
229 set ui_fstatus_value {}
230}
231
232proc show_diff {path} {
233 global file_states PARENT diff_3way diff_active
234 global ui_diff ui_fname_value ui_fstatus_value ui_status_value
235
236 if {$diff_active || ![lock_index read]} return
237
238 clear_diff
239 set s $file_states($path)
240 set m [lindex $s 0]
241 set diff_3way 0
242 set diff_active 1
243 set ui_fname_value $path
244 set ui_fstatus_value [mapdesc $m $path]
245 set ui_status_value "Loading diff of $path..."
246
247 set cmd [list | git diff-index -p $PARENT -- $path]
248 switch $m {
249 AM {
250 }
251 MM {
252 set cmd [list | git diff-index -p -c $PARENT $path]
253 }
254 _O {
255 if {[catch {
256 set fd [open $path r]
257 set content [read $fd]
258 close $fd
259 } err ]} {
260 set diff_active 0
261 unlock_index
262 set ui_status_value "Unable to display $path"
263 error_popup "Error loading file:\n$err"
264 return
265 }
266 $ui_diff conf -state normal
267 $ui_diff insert end $content
268 $ui_diff conf -state disabled
269 return
270 }
271 }
272
273 if {[catch {set fd [open $cmd r]} err]} {
274 set diff_active 0
275 unlock_index
276 set ui_status_value "Unable to display $path"
277 error_popup "Error loading diff:\n$err"
278 return
279 }
280
281 fconfigure $fd -blocking 0 -translation auto
282 fileevent $fd readable [list read_diff $fd]
283}
284
285proc read_diff {fd} {
286 global ui_diff ui_status_value diff_3way diff_active
287
288 while {[gets $fd line] >= 0} {
289 if {[string match {diff --git *} $line]} continue
290 if {[string match {diff --combined *} $line]} continue
291 if {[string match {--- *} $line]} continue
292 if {[string match {+++ *} $line]} continue
293 if {[string match index* $line]} {
294 if {[string first , $line] >= 0} {
295 set diff_3way 1
296 }
297 }
298
299 $ui_diff conf -state normal
300 if {!$diff_3way} {
301 set x [string index $line 0]
302 switch -- $x {
303 "@" {set tags da}
304 "+" {set tags dp}
305 "-" {set tags dm}
306 default {set tags {}}
307 }
308 } else {
309 set x [string range $line 0 1]
310 switch -- $x {
311 default {set tags {}}
312 "@@" {set tags da}
313 "++" {set tags dp; set x " +"}
314 " +" {set tags {di bold}; set x "++"}
315 "+ " {set tags dni; set x "-+"}
316 "--" {set tags dm; set x " -"}
317 " -" {set tags {dm bold}; set x "--"}
318 "- " {set tags di; set x "+-"}
319 default {set tags {}}
320 }
321 set line [string replace $line 0 1 $x]
322 }
323 $ui_diff insert end $line $tags
324 $ui_diff insert end "\n"
325 $ui_diff conf -state disabled
326 }
327
328 if {[eof $fd]} {
329 close $fd
330 set diff_active 0
331 unlock_index
332 set ui_status_value {Ready.}
333 }
334}
335
336######################################################################
337##
338## commit
339
340proc load_last_commit {} {
341 global HEAD PARENT commit_type ui_comm
342
343 if {$commit_type == {amend}} return
344 if {$commit_type != {normal}} {
345 error_popup "Can't amend a $commit_type commit."
346 return
347 }
348
349 set msg {}
350 set parent {}
351 set parent_count 0
352 if {[catch {
353 set fd [open "| git cat-file commit $HEAD" r]
354 while {[gets $fd line] > 0} {
355 if {[string match {parent *} $line]} {
356 set parent [string range $line 7 end]
357 incr parent_count
358 }
359 }
360 set msg [string trim [read $fd]]
361 close $fd
362 } err]} {
363 error_popup "Error loading commit data for amend:\n$err"
364 return
365 }
366
367 if {$parent_count == 0} {
368 set commit_type amend
369 set HEAD {}
370 set PARENT {}
371 update_status
372 } elseif {$parent_count == 1} {
373 set commit_type amend
374 set PARENT $parent
375 $ui_comm delete 0.0 end
376 $ui_comm insert end $msg
377 $ui_comm edit modified false
378 update_status
379 } else {
380 error_popup {You can't amend a merge commit.}
381 return
382 }
383}
384
385proc commit_tree {} {
386 global tcl_platform HEAD gitdir commit_type file_states
387 global commit_active ui_status_value
388 global ui_comm
389
390 if {$commit_active || ![lock_index update]} return
391
392 # -- Our in memory state should match the repository.
393 #
394 repository_state curHEAD cur_type
395 if {$commit_type == {amend}
396 && $cur_type == {normal}
397 && $curHEAD == $HEAD} {
398 } elseif {$commit_type != $cur_type || $HEAD != $curHEAD} {
399 error_popup {Last scanned state does not match repository state.
400
401Its highly likely that another Git program modified the
402repository since our last scan. A rescan is required
403before committing.
404}
405 unlock_index
406 update_status
407 return
408 }
409
410 # -- At least one file should differ in the index.
411 #
412 set files_ready 0
413 foreach path [array names file_states] {
414 set s $file_states($path)
415 switch -glob -- [lindex $s 0] {
416 _* {continue}
417 A* -
418 D* -
419 M* {set files_ready 1; break}
420 U* {
421 error_popup "Unmerged files cannot be committed.
422
423File $path has merge conflicts.
424You must resolve them and check the file in before committing.
425"
426 unlock_index
427 return
428 }
429 default {
430 error_popup "Unknown file state [lindex $s 0] detected.
431
432File $path cannot be committed by this program.
433"
434 }
435 }
436 }
437 if {!$files_ready} {
438 error_popup {No checked-in files to commit.
439
440You must check-in at least 1 file before you can commit.
441}
442 unlock_index
443 return
444 }
445
446 # -- A message is required.
447 #
448 set msg [string trim [$ui_comm get 1.0 end]]
449 if {$msg == {}} {
450 error_popup {Please supply a commit message.
451
452A good commit message has the following format:
453
454- First line: Describe in one sentance what you did.
455- Second line: Blank
456- Remaining lines: Describe why this change is good.
457}
458 unlock_index
459 return
460 }
461
462 # -- Ask the pre-commit hook for the go-ahead.
463 #
464 set pchook [file join $gitdir hooks pre-commit]
465 if {$tcl_platform(platform) == {windows} && [file isfile $pchook]} {
466 set pchook [list sh -c \
467 "if test -x \"$pchook\"; then exec \"$pchook\"; fi"]
468 } elseif {[file executable $pchook]} {
469 set pchook [list $pchook]
470 } else {
471 set pchook {}
472 }
473 if {$pchook != {} && [catch {eval exec $pchook} err]} {
474 hook_failed_popup pre-commit $err
475 unlock_index
476 return
477 }
478
479 # -- Write the tree in the background.
480 #
481 set commit_active 1
482 set ui_status_value {Committing changes...}
483
484 set fd_wt [open "| git write-tree" r]
485 fileevent $fd_wt readable \
486 [list commit_stage2 $fd_wt $curHEAD $msg]
487}
488
489proc commit_stage2 {fd_wt curHEAD msg} {
490 global single_commit gitdir PARENT commit_type
491 global commit_active ui_status_value ui_comm
492
493 gets $fd_wt tree_id
494 close $fd_wt
495
496 if {$tree_id == {}} {
497 error_popup "write-tree failed"
498 set commit_active 0
499 set ui_status_value {Commit failed.}
500 unlock_index
501 return
502 }
503
504 # -- Create the commit.
505 #
506 set cmd [list git commit-tree $tree_id]
507 if {$PARENT != {}} {
508 lappend cmd -p $PARENT
509 }
510 if {$commit_type == {merge}} {
511 if {[catch {
512 set fd_mh [open [file join $gitdir MERGE_HEAD] r]
513 while {[gets $fd_mh merge_head] > 0} {
514 lappend -p $merge_head
515 }
516 close $fd_mh
517 } err]} {
518 error_popup "Loading MERGE_HEADs failed:\n$err"
519 set commit_active 0
520 set ui_status_value {Commit failed.}
521 unlock_index
522 return
523 }
524 }
525 if {$PARENT == {}} {
526 # git commit-tree writes to stderr during initial commit.
527 lappend cmd 2>/dev/null
528 }
529 lappend cmd << $msg
530 if {[catch {set cmt_id [eval exec $cmd]} err]} {
531 error_popup "commit-tree failed:\n$err"
532 set commit_active 0
533 set ui_status_value {Commit failed.}
534 unlock_index
535 return
536 }
537
538 # -- Update the HEAD ref.
539 #
540 set reflogm commit
541 if {$commit_type != {normal}} {
542 append reflogm " ($commit_type)"
543 }
544 set i [string first "\n" $msg]
545 if {$i >= 0} {
546 append reflogm {: } [string range $msg 0 [expr $i - 1]]
547 } else {
548 append reflogm {: } $msg
549 }
550 set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD]
551 if {[catch {eval exec $cmd} err]} {
552 error_popup "update-ref failed:\n$err"
553 set commit_active 0
554 set ui_status_value {Commit failed.}
555 unlock_index
556 return
557 }
558
559 # -- Cleanup after ourselves.
560 #
561 catch {file delete [file join $gitdir MERGE_HEAD]}
562 catch {file delete [file join $gitdir MERGE_MSG]}
563 catch {file delete [file join $gitdir SQUASH_MSG]}
564 catch {file delete [file join $gitdir GITGUI_MSG]}
565
566 # -- Let rerere do its thing.
567 #
568 if {[file isdirectory [file join $gitdir rr-cache]]} {
569 catch {exec git rerere}
570 }
571
572 $ui_comm delete 0.0 end
573 $ui_comm edit modified false
574
575 if {$single_commit} do_quit
576
577 set commit_type {}
578 set commit_active 0
579 unlock_index
580 update_status "Changes committed as $cmt_id."
581}
582
583######################################################################
584##
585## ui helpers
586
587proc mapcol {state path} {
588 global all_cols
589
590 if {[catch {set r $all_cols($state)}]} {
591 puts "error: no column for state={$state} $path"
592 return o
593 }
594 return $r
595}
596
597proc mapicon {state path} {
598 global all_icons
599
600 if {[catch {set r $all_icons($state)}]} {
601 puts "error: no icon for state={$state} $path"
602 return file_plain
603 }
604 return $r
605}
606
607proc mapdesc {state path} {
608 global all_descs
609
610 if {[catch {set r $all_descs($state)}]} {
611 puts "error: no desc for state={$state} $path"
612 return $state
613 }
614 return $r
615}
616
617proc bsearch {w path} {
618 set hi [expr [lindex [split [$w index end] .] 0] - 2]
619 if {$hi == 0} {
620 return -1
621 }
622 set lo 0
623 while {$lo < $hi} {
624 set mi [expr [expr $lo + $hi] / 2]
625 set ti [expr $mi + 1]
626 set cmp [string compare [$w get $ti.1 $ti.end] $path]
627 if {$cmp < 0} {
628 set lo $ti
629 } elseif {$cmp == 0} {
630 return $mi
631 } else {
632 set hi $mi
633 }
634 }
635 return -[expr $lo + 1]
636}
637
638proc merge_state {path state} {
639 global file_states
640
641 if {[array names file_states -exact $path] == {}} {
642 set o __
643 set s [list $o none none]
644 } else {
645 set s $file_states($path)
646 set o [lindex $s 0]
647 }
648
649 set m [lindex $s 0]
650 if {[string index $state 0] == "_"} {
651 set state [string index $m 0][string index $state 1]
652 } elseif {[string index $state 0] == "*"} {
653 set state _[string index $state 1]
654 }
655
656 if {[string index $state 1] == "_"} {
657 set state [string index $state 0][string index $m 1]
658 } elseif {[string index $state 1] == "*"} {
659 set state [string index $state 0]_
660 }
661
662 set file_states($path) [lreplace $s 0 0 $state]
663 return $o
664}
665
666proc display_file {path state} {
667 global ui_index ui_other file_states
668
669 set old_m [merge_state $path $state]
670 set s $file_states($path)
671 set m [lindex $s 0]
672
673 if {[mapcol $m $path] == "o"} {
674 set ii 1
675 set ai 2
676 set iw $ui_index
677 set aw $ui_other
678 } else {
679 set ii 2
680 set ai 1
681 set iw $ui_other
682 set aw $ui_index
683 }
684
685 set d [lindex $s $ii]
686 if {$d != "none"} {
687 set lno [bsearch $iw $path]
688 if {$lno >= 0} {
689 incr lno
690 $iw conf -state normal
691 $iw delete $lno.0 [expr $lno + 1].0
692 $iw conf -state disabled
693 set s [lreplace $s $ii $ii none]
694 }
695 }
696
697 set d [lindex $s $ai]
698 if {$d == "none"} {
699 set lno [expr abs([bsearch $aw $path] + 1) + 1]
700 $aw conf -state normal
701 set ico [$aw image create $lno.0 \
702 -align center -padx 5 -pady 1 \
703 -image [mapicon $m $path]]
704 $aw insert $lno.1 "$path\n"
705 $aw conf -state disabled
706 set file_states($path) [lreplace $s $ai $ai [list $ico]]
707 } elseif {[mapicon $m $path] != [mapicon $old_m $path]} {
708 set ico [lindex $d 0]
709 $aw image conf $ico -image [mapicon $m $path]
710 }
711}
712
713proc with_update_index {body} {
714 global update_index_fd
715
716 if {$update_index_fd == {}} {
717 if {![lock_index update]} return
718 set update_index_fd [open \
719 "| git update-index --add --remove -z --stdin" \
720 w]
721 fconfigure $update_index_fd -translation binary
722 uplevel 1 $body
723 close $update_index_fd
724 set update_index_fd {}
725 unlock_index
726 } else {
727 uplevel 1 $body
728 }
729}
730
731proc update_index {path} {
732 global update_index_fd
733
734 if {$update_index_fd == {}} {
735 error {not in with_update_index}
736 } else {
737 puts -nonewline $update_index_fd "$path\0"
738 }
739}
740
741proc toggle_mode {path} {
742 global file_states
743
744 set s $file_states($path)
745 set m [lindex $s 0]
746
747 switch -- $m {
748 AM -
749 _O {set new A*}
750 _M -
751 MM {set new M*}
752 _D {set new D*}
753 default {return}
754 }
755
756 with_update_index {update_index $path}
757 display_file $path $new
758}
759
760######################################################################
761##
762## icons
763
764set filemask {
765#define mask_width 14
766#define mask_height 15
767static unsigned char mask_bits[] = {
768 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
769 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
770 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
771}
772
773image create bitmap file_plain -background white -foreground black -data {
774#define plain_width 14
775#define plain_height 15
776static unsigned char plain_bits[] = {
777 0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
778 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
779 0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
780} -maskdata $filemask
781
782image create bitmap file_mod -background white -foreground blue -data {
783#define mod_width 14
784#define mod_height 15
785static unsigned char mod_bits[] = {
786 0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
787 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
788 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
789} -maskdata $filemask
790
791image create bitmap file_fulltick -background white -foreground "#007000" -data {
792#define file_fulltick_width 14
793#define file_fulltick_height 15
794static unsigned char file_fulltick_bits[] = {
795 0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
796 0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
797 0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
798} -maskdata $filemask
799
800image create bitmap file_parttick -background white -foreground "#005050" -data {
801#define parttick_width 14
802#define parttick_height 15
803static unsigned char parttick_bits[] = {
804 0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
805 0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
806 0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
807} -maskdata $filemask
808
809image create bitmap file_question -background white -foreground black -data {
810#define file_question_width 14
811#define file_question_height 15
812static unsigned char file_question_bits[] = {
813 0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
814 0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
815 0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
816} -maskdata $filemask
817
818image create bitmap file_removed -background white -foreground red -data {
819#define file_removed_width 14
820#define file_removed_height 15
821static unsigned char file_removed_bits[] = {
822 0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
823 0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
824 0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
825} -maskdata $filemask
826
827image create bitmap file_merge -background white -foreground blue -data {
828#define file_merge_width 14
829#define file_merge_height 15
830static unsigned char file_merge_bits[] = {
831 0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
832 0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
833 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
834} -maskdata $filemask
835
836set max_status_desc 0
837foreach i {
838 {__ i plain "Unmodified"}
839 {_M i mod "Modified"}
840 {M_ i fulltick "Checked in"}
841 {MM i parttick "Partially checked in"}
842
843 {_O o plain "Untracked"}
844 {A_ o fulltick "Added"}
845 {AM o parttick "Partially added"}
846
847 {_D i question "Missing"}
848 {D_ i removed "Removed"}
849 {DD i removed "Removed"}
850 {DO i removed "Removed (still exists)"}
851
852 {UM i merge "Merge conflicts"}
853 {U_ i merge "Merge conflicts"}
854 } {
855 if {$max_status_desc < [string length [lindex $i 3]]} {
856 set max_status_desc [string length [lindex $i 3]]
857 }
858 set all_cols([lindex $i 0]) [lindex $i 1]
859 set all_icons([lindex $i 0]) file_[lindex $i 2]
860 set all_descs([lindex $i 0]) [lindex $i 3]
861}
862unset filemask i
863
864######################################################################
865##
866## util
867
868proc error_popup {msg} {
869 set w .error
870 toplevel $w
871 wm transient $w .
872 show_msg $w $w $msg
873}
874
875proc show_msg {w top msg} {
876 global gitdir appname
877
878 message $w.m -text $msg -justify left -aspect 400
879 pack $w.m -side top -fill x -padx 5 -pady 10
880 button $w.ok -text OK \
881 -width 15 \
882 -command "destroy $top"
883 pack $w.ok -side bottom
884 bind $top <Visibility> "grab $top; focus $top"
885 bind $top <Key-Return> "destroy $top"
886 wm title $top "error: $appname ([file normalize [file dirname $gitdir]])"
887 tkwait window $top
888}
889
890proc hook_failed_popup {hook msg} {
891 global gitdir mainfont difffont appname
892
893 set w .hookfail
894 toplevel $w
895 wm transient $w .
896
897 frame $w.m
898 label $w.m.l1 -text "$hook hook failed:" \
899 -anchor w \
900 -justify left \
901 -font [concat $mainfont bold]
902 text $w.m.t \
903 -background white -borderwidth 1 \
904 -relief sunken \
905 -width 80 -height 10 \
906 -font $difffont \
907 -yscrollcommand [list $w.m.sby set]
908 label $w.m.l2 \
909 -text {You must correct the above errors before committing.} \
910 -anchor w \
911 -justify left \
912 -font [concat $mainfont bold]
913 scrollbar $w.m.sby -command [list $w.m.t yview]
914 pack $w.m.l1 -side top -fill x
915 pack $w.m.l2 -side bottom -fill x
916 pack $w.m.sby -side right -fill y
917 pack $w.m.t -side left -fill both -expand 1
918 pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
919
920 $w.m.t insert 1.0 $msg
921 $w.m.t conf -state disabled
922
923 button $w.ok -text OK \
924 -width 15 \
925 -command "destroy $w"
926 pack $w.ok -side bottom
927
928 bind $w <Visibility> "grab $w; focus $w"
929 bind $w <Key-Return> "destroy $w"
930 wm title $w "error: $appname ([file normalize [file dirname $gitdir]])"
931 tkwait window $w
932}
933
934######################################################################
935##
936## ui commands
937
938set starting_gitk_msg {Please wait... Starting gitk...}
939proc do_gitk {} {
940 global tcl_platform ui_status_value starting_gitk_msg
941
942 set ui_status_value $starting_gitk_msg
943 after 10000 {
944 if {$ui_status_value == $starting_gitk_msg} {
945 set ui_status_value {Ready.}
946 }
947 }
948
949 if {$tcl_platform(platform) == "windows"} {
950 exec sh -c gitk &
951 } else {
952 exec gitk &
953 }
954}
955
956proc do_quit {} {
957 global gitdir ui_comm
958
959 set save [file join $gitdir GITGUI_MSG]
960 set msg [string trim [$ui_comm get 0.0 end]]
961 if {[$ui_comm edit modified] && $msg != {}} {
962 catch {
963 set fd [open $save w]
964 puts $fd [string trim [$ui_comm get 0.0 end]]
965 close $fd
966 }
967 } elseif {$msg == {} && [file exists $save]} {
968 file delete $save
969 }
970
971 destroy .
972}
973
974proc do_rescan {} {
975 update_status
976}
977
978proc do_checkin_all {} {
979 global checkin_active ui_status_value
980
981 if {$checkin_active || ![lock_index begin-update]} return
982
983 set checkin_active 1
984 set ui_status_value {Checking in all files...}
985 after 1 {
986 with_update_index {
987 foreach path [array names file_states] {
988 set s $file_states($path)
989 set m [lindex $s 0]
990 switch -- $m {
991 AM -
992 MM -
993 _M -
994 _D {toggle_mode $path}
995 }
996 }
997 }
998 set checkin_active 0
999 set ui_status_value {Ready.}
1000 }
1001}
1002
1003proc do_signoff {} {
1004 global ui_comm
1005
1006 catch {
1007 set me [exec git var GIT_COMMITTER_IDENT]
1008 if {[regexp {(.*) [0-9]+ [-+0-9]+$} $me me name]} {
1009 set str "Signed-off-by: $name"
1010 if {[$ui_comm get {end -1c linestart} {end -1c}] != $str} {
1011 $ui_comm insert end "\n"
1012 $ui_comm insert end $str
1013 $ui_comm see end
1014 }
1015 }
1016 }
1017}
1018
1019proc do_amend_last {} {
1020 load_last_commit
1021}
1022
1023proc do_commit {} {
1024 commit_tree
1025}
1026
1027# shift == 1: left click
1028# 3: right click
1029proc click {w x y shift wx wy} {
1030 global ui_index ui_other
1031
1032 set pos [split [$w index @$x,$y] .]
1033 set lno [lindex $pos 0]
1034 set col [lindex $pos 1]
1035 set path [$w get $lno.1 $lno.end]
1036 if {$path == {}} return
1037
1038 if {$col > 0 && $shift == 1} {
1039 $ui_index tag remove in_diff 0.0 end
1040 $ui_other tag remove in_diff 0.0 end
1041 $w tag add in_diff $lno.0 [expr $lno + 1].0
1042 show_diff $path
1043 }
1044}
1045
1046proc unclick {w x y} {
1047 set pos [split [$w index @$x,$y] .]
1048 set lno [lindex $pos 0]
1049 set col [lindex $pos 1]
1050 set path [$w get $lno.1 $lno.end]
1051 if {$path == {}} return
1052
1053 if {$col == 0} {
1054 toggle_mode $path
1055 }
1056}
1057
1058######################################################################
1059##
1060## ui init
1061
1062set mainfont {Helvetica 10}
1063set difffont {Courier 10}
1064set maincursor [. cget -cursor]
1065
1066switch -- $tcl_platform(platform) {
1067windows {set M1B Control; set M1T Ctrl}
1068default {set M1B M1; set M1T M1}
1069}
1070
1071# -- Menu Bar
1072menu .mbar -tearoff 0
1073.mbar add cascade -label Project -menu .mbar.project
1074.mbar add cascade -label Commit -menu .mbar.commit
1075.mbar add cascade -label Fetch -menu .mbar.fetch
1076.mbar add cascade -label Pull -menu .mbar.pull
1077. configure -menu .mbar
1078
1079# -- Project Menu
1080menu .mbar.project
1081.mbar.project add command -label Visualize \
1082 -command do_gitk \
1083 -font $mainfont
1084.mbar.project add command -label Quit \
1085 -command do_quit \
1086 -accelerator $M1T-Q \
1087 -font $mainfont
1088
1089# -- Commit Menu
1090menu .mbar.commit
1091.mbar.commit add command -label Rescan \
1092 -command do_rescan \
1093 -accelerator F5 \
1094 -font $mainfont
1095lappend disable_on_lock \
1096 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1097.mbar.commit add command -label {Amend Last Commit} \
1098 -command do_amend_last \
1099 -font $mainfont
1100lappend disable_on_lock \
1101 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1102.mbar.commit add command -label {Check-in All Files} \
1103 -command do_checkin_all \
1104 -accelerator $M1T-U \
1105 -font $mainfont
1106lappend disable_on_lock \
1107 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1108.mbar.commit add command -label {Sign Off} \
1109 -command do_signoff \
1110 -accelerator $M1T-S \
1111 -font $mainfont
1112.mbar.commit add command -label Commit \
1113 -command do_commit \
1114 -accelerator $M1T-Return \
1115 -font $mainfont
1116lappend disable_on_lock \
1117 [list .mbar.commit entryconf [.mbar.commit index last] -state]
1118
1119# -- Fetch Menu
1120menu .mbar.fetch
1121
1122# -- Pull Menu
1123menu .mbar.pull
1124
1125# -- Main Window Layout
1126panedwindow .vpane -orient vertical
1127panedwindow .vpane.files -orient horizontal
1128.vpane add .vpane.files -sticky nsew -height 100 -width 400
1129pack .vpane -anchor n -side top -fill both -expand 1
1130
1131# -- Index File List
1132set ui_index .vpane.files.index.list
1133frame .vpane.files.index -height 100 -width 400
1134label .vpane.files.index.title -text {Modified Files} \
1135 -background green \
1136 -font $mainfont
1137text $ui_index -background white -borderwidth 0 \
1138 -width 40 -height 10 \
1139 -font $mainfont \
1140 -yscrollcommand {.vpane.files.index.sb set} \
1141 -cursor $maincursor \
1142 -state disabled
1143scrollbar .vpane.files.index.sb -command [list $ui_index yview]
1144pack .vpane.files.index.title -side top -fill x
1145pack .vpane.files.index.sb -side right -fill y
1146pack $ui_index -side left -fill both -expand 1
1147.vpane.files add .vpane.files.index -sticky nsew
1148
1149# -- Other (Add) File List
1150set ui_other .vpane.files.other.list
1151frame .vpane.files.other -height 100 -width 100
1152label .vpane.files.other.title -text {Untracked Files} \
1153 -background red \
1154 -font $mainfont
1155text $ui_other -background white -borderwidth 0 \
1156 -width 40 -height 10 \
1157 -font $mainfont \
1158 -yscrollcommand {.vpane.files.other.sb set} \
1159 -cursor $maincursor \
1160 -state disabled
1161scrollbar .vpane.files.other.sb -command [list $ui_other yview]
1162pack .vpane.files.other.title -side top -fill x
1163pack .vpane.files.other.sb -side right -fill y
1164pack $ui_other -side left -fill both -expand 1
1165.vpane.files add .vpane.files.other -sticky nsew
1166
1167$ui_index tag conf in_diff -font [concat $mainfont bold]
1168$ui_other tag conf in_diff -font [concat $mainfont bold]
1169
1170# -- Diff Header
1171set ui_fname_value {}
1172set ui_fstatus_value {}
1173frame .vpane.diff -height 200 -width 400
1174frame .vpane.diff.header
1175label .vpane.diff.header.l1 -text {File:} -font $mainfont
1176label .vpane.diff.header.l2 -textvariable ui_fname_value \
1177 -anchor w \
1178 -justify left \
1179 -font $mainfont
1180label .vpane.diff.header.l3 -text {Status:} -font $mainfont
1181label .vpane.diff.header.l4 -textvariable ui_fstatus_value \
1182 -width $max_status_desc \
1183 -anchor w \
1184 -justify left \
1185 -font $mainfont
1186pack .vpane.diff.header.l1 -side left
1187pack .vpane.diff.header.l2 -side left -fill x
1188pack .vpane.diff.header.l4 -side right
1189pack .vpane.diff.header.l3 -side right
1190
1191# -- Diff Body
1192frame .vpane.diff.body
1193set ui_diff .vpane.diff.body.t
1194text $ui_diff -background white -borderwidth 0 \
1195 -width 80 -height 15 -wrap none \
1196 -font $difffont \
1197 -xscrollcommand {.vpane.diff.body.sbx set} \
1198 -yscrollcommand {.vpane.diff.body.sby set} \
1199 -cursor $maincursor \
1200 -state disabled
1201scrollbar .vpane.diff.body.sbx -orient horizontal \
1202 -command [list $ui_diff xview]
1203scrollbar .vpane.diff.body.sby -orient vertical \
1204 -command [list $ui_diff yview]
1205pack .vpane.diff.body.sbx -side bottom -fill x
1206pack .vpane.diff.body.sby -side right -fill y
1207pack $ui_diff -side left -fill both -expand 1
1208pack .vpane.diff.header -side top -fill x
1209pack .vpane.diff.body -side bottom -fill both -expand 1
1210.vpane add .vpane.diff -stick nsew
1211
1212$ui_diff tag conf dm -foreground red
1213$ui_diff tag conf dp -foreground blue
1214$ui_diff tag conf da -font [concat $difffont bold]
1215$ui_diff tag conf di -foreground "#00a000"
1216$ui_diff tag conf dni -foreground "#a000a0"
1217$ui_diff tag conf bold -font [concat $difffont bold]
1218
1219# -- Commit Area
1220frame .vpane.commarea -height 170
1221.vpane add .vpane.commarea -stick nsew
1222
1223# -- Commit Area Buttons
1224frame .vpane.commarea.buttons
1225label .vpane.commarea.buttons.l -text {} \
1226 -anchor w \
1227 -justify left \
1228 -font $mainfont
1229pack .vpane.commarea.buttons.l -side top -fill x
1230pack .vpane.commarea.buttons -side left -fill y
1231
1232button .vpane.commarea.buttons.rescan -text {Rescan} \
1233 -command do_rescan \
1234 -font $mainfont
1235pack .vpane.commarea.buttons.rescan -side top -fill x
1236lappend disable_on_lock {.vpane.commarea.buttons.rescan conf -state}
1237
1238button .vpane.commarea.buttons.amend -text {Amend Last} \
1239 -command do_amend_last \
1240 -font $mainfont
1241pack .vpane.commarea.buttons.amend -side top -fill x
1242lappend disable_on_lock {.vpane.commarea.buttons.amend conf -state}
1243
1244button .vpane.commarea.buttons.ciall -text {Check-in All} \
1245 -command do_checkin_all \
1246 -font $mainfont
1247pack .vpane.commarea.buttons.ciall -side top -fill x
1248lappend disable_on_lock {.vpane.commarea.buttons.ciall conf -state}
1249
1250button .vpane.commarea.buttons.signoff -text {Sign Off} \
1251 -command do_signoff \
1252 -font $mainfont
1253pack .vpane.commarea.buttons.signoff -side top -fill x
1254
1255button .vpane.commarea.buttons.commit -text {Commit} \
1256 -command do_commit \
1257 -font $mainfont
1258pack .vpane.commarea.buttons.commit -side top -fill x
1259lappend disable_on_lock {.vpane.commarea.buttons.commit conf -state}
1260
1261# -- Commit Message Buffer
1262frame .vpane.commarea.buffer
1263set ui_comm .vpane.commarea.buffer.t
1264label .vpane.commarea.buffer.l -text {Commit Message:} \
1265 -anchor w \
1266 -justify left \
1267 -font $mainfont
1268text $ui_comm -background white -borderwidth 1 \
1269 -relief sunken \
1270 -width 75 -height 10 -wrap none \
1271 -font $difffont \
1272 -yscrollcommand {.vpane.commarea.buffer.sby set} \
1273 -cursor $maincursor
1274scrollbar .vpane.commarea.buffer.sby -command [list $ui_comm yview]
1275pack .vpane.commarea.buffer.l -side top -fill x
1276pack .vpane.commarea.buffer.sby -side right -fill y
1277pack $ui_comm -side left -fill y
1278pack .vpane.commarea.buffer -side left -fill y
1279
1280# -- Status Bar
1281set ui_status_value {Initializing...}
1282label .status -textvariable ui_status_value \
1283 -anchor w \
1284 -justify left \
1285 -borderwidth 1 \
1286 -relief sunken \
1287 -font $mainfont
1288pack .status -anchor w -side bottom -fill x
1289
1290# -- Key Bindings
1291bind $ui_comm <$M1B-Key-Return> {do_commit;break}
1292bind . <Destroy> do_quit
1293bind . <Key-F5> do_rescan
1294bind . <$M1B-Key-r> do_rescan
1295bind . <$M1B-Key-R> do_rescan
1296bind . <$M1B-Key-s> do_signoff
1297bind . <$M1B-Key-S> do_signoff
1298bind . <$M1B-Key-u> do_checkin_all
1299bind . <$M1B-Key-U> do_checkin_all
1300bind . <$M1B-Key-Return> do_commit
1301bind . <$M1B-Key-q> do_quit
1302bind . <$M1B-Key-Q> do_quit
1303foreach i [list $ui_index $ui_other] {
1304 bind $i <Button-1> {click %W %x %y 1 %X %Y; break}
1305 bind $i <Button-3> {click %W %x %y 3 %X %Y; break}
1306 bind $i <ButtonRelease-1> {unclick %W %x %y; break}
1307}
1308unset i M1B M1T
1309
1310######################################################################
1311##
1312## main
1313
1314if {[catch {set gitdir [exec git rev-parse --git-dir]} err]} {
1315 show_msg {} . "Cannot find the git directory: $err"
1316 exit 1
1317}
1318
1319set appname [lindex [file split $argv0] end]
1320if {$appname == {git-citool}} {
1321 set single_commit 1
1322}
1323
1324wm title . "$appname ([file normalize [file dirname $gitdir]])"
1325focus -force $ui_comm
1326update_status