gitk: Add a way to mark a commit, plus a "find descendant" command
[git] / gitk
1 #!/bin/sh
2 # Tcl ignores the next line -*- tcl -*- \
3 exec wish "$0" -- "$@"
4
5 # Copyright © 2005-2008 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 proc gitdir {} {
11     global env
12     if {[info exists env(GIT_DIR)]} {
13         return $env(GIT_DIR)
14     } else {
15         return [exec git rev-parse --git-dir]
16     }
17 }
18
19 # A simple scheduler for compute-intensive stuff.
20 # The aim is to make sure that event handlers for GUI actions can
21 # run at least every 50-100 ms.  Unfortunately fileevent handlers are
22 # run before X event handlers, so reading from a fast source can
23 # make the GUI completely unresponsive.
24 proc run args {
25     global isonrunq runq currunq
26
27     set script $args
28     if {[info exists isonrunq($script)]} return
29     if {$runq eq {} && ![info exists currunq]} {
30         after idle dorunq
31     }
32     lappend runq [list {} $script]
33     set isonrunq($script) 1
34 }
35
36 proc filerun {fd script} {
37     fileevent $fd readable [list filereadable $fd $script]
38 }
39
40 proc filereadable {fd script} {
41     global runq currunq
42
43     fileevent $fd readable {}
44     if {$runq eq {} && ![info exists currunq]} {
45         after idle dorunq
46     }
47     lappend runq [list $fd $script]
48 }
49
50 proc nukefile {fd} {
51     global runq
52
53     for {set i 0} {$i < [llength $runq]} {} {
54         if {[lindex $runq $i 0] eq $fd} {
55             set runq [lreplace $runq $i $i]
56         } else {
57             incr i
58         }
59     }
60 }
61
62 proc dorunq {} {
63     global isonrunq runq currunq
64
65     set tstart [clock clicks -milliseconds]
66     set t0 $tstart
67     while {[llength $runq] > 0} {
68         set fd [lindex $runq 0 0]
69         set script [lindex $runq 0 1]
70         set currunq [lindex $runq 0]
71         set runq [lrange $runq 1 end]
72         set repeat [eval $script]
73         unset currunq
74         set t1 [clock clicks -milliseconds]
75         set t [expr {$t1 - $t0}]
76         if {$repeat ne {} && $repeat} {
77             if {$fd eq {} || $repeat == 2} {
78                 # script returns 1 if it wants to be readded
79                 # file readers return 2 if they could do more straight away
80                 lappend runq [list $fd $script]
81             } else {
82                 fileevent $fd readable [list filereadable $fd $script]
83             }
84         } elseif {$fd eq {}} {
85             unset isonrunq($script)
86         }
87         set t0 $t1
88         if {$t1 - $tstart >= 80} break
89     }
90     if {$runq ne {}} {
91         after idle dorunq
92     }
93 }
94
95 proc reg_instance {fd} {
96     global commfd leftover loginstance
97
98     set i [incr loginstance]
99     set commfd($i) $fd
100     set leftover($i) {}
101     return $i
102 }
103
104 proc unmerged_files {files} {
105     global nr_unmerged
106
107     # find the list of unmerged files
108     set mlist {}
109     set nr_unmerged 0
110     if {[catch {
111         set fd [open "| git ls-files -u" r]
112     } err]} {
113         show_error {} . "[mc "Couldn't get list of unmerged files:"] $err"
114         exit 1
115     }
116     while {[gets $fd line] >= 0} {
117         set i [string first "\t" $line]
118         if {$i < 0} continue
119         set fname [string range $line [expr {$i+1}] end]
120         if {[lsearch -exact $mlist $fname] >= 0} continue
121         incr nr_unmerged
122         if {$files eq {} || [path_filter $files $fname]} {
123             lappend mlist $fname
124         }
125     }
126     catch {close $fd}
127     return $mlist
128 }
129
130 proc parseviewargs {n arglist} {
131     global vdatemode vmergeonly vflags vdflags vrevs vfiltered vorigargs
132
133     set vdatemode($n) 0
134     set vmergeonly($n) 0
135     set glflags {}
136     set diffargs {}
137     set nextisval 0
138     set revargs {}
139     set origargs $arglist
140     set allknown 1
141     set filtered 0
142     set i -1
143     foreach arg $arglist {
144         incr i
145         if {$nextisval} {
146             lappend glflags $arg
147             set nextisval 0
148             continue
149         }
150         switch -glob -- $arg {
151             "-d" -
152             "--date-order" {
153                 set vdatemode($n) 1
154                 # remove from origargs in case we hit an unknown option
155                 set origargs [lreplace $origargs $i $i]
156                 incr i -1
157             }
158             "-[puabwcrRBMC]" -
159             "--no-renames" - "--full-index" - "--binary" - "--abbrev=*" -
160             "--find-copies-harder" - "-l*" - "--ext-diff" - "--no-ext-diff" -
161             "--src-prefix=*" - "--dst-prefix=*" - "--no-prefix" -
162             "-O*" - "--text" - "--full-diff" - "--ignore-space-at-eol" -
163             "--ignore-space-change" - "-U*" - "--unified=*" {
164                 # These request or affect diff output, which we don't want.
165                 # Some could be used to set our defaults for diff display.
166                 lappend diffargs $arg
167             }
168             "--raw" - "--patch-with-raw" - "--patch-with-stat" -
169             "--name-only" - "--name-status" - "--color" - "--color-words" -
170             "--log-size" - "--pretty=*" - "--decorate" - "--abbrev-commit" -
171             "--cc" - "-z" - "--header" - "--parents" - "--boundary" -
172             "--no-color" - "-g" - "--walk-reflogs" - "--no-walk" -
173             "--timestamp" - "relative-date" - "--date=*" - "--stdin" -
174             "--objects" - "--objects-edge" - "--reverse" {
175                 # These cause our parsing of git log's output to fail, or else
176                 # they're options we want to set ourselves, so ignore them.
177             }
178             "--stat=*" - "--numstat" - "--shortstat" - "--summary" -
179             "--check" - "--exit-code" - "--quiet" - "--topo-order" -
180             "--full-history" - "--dense" - "--sparse" -
181             "--follow" - "--left-right" - "--encoding=*" {
182                 # These are harmless, and some are even useful
183                 lappend glflags $arg
184             }
185             "--diff-filter=*" - "--no-merges" - "--unpacked" -
186             "--max-count=*" - "--skip=*" - "--since=*" - "--after=*" -
187             "--until=*" - "--before=*" - "--max-age=*" - "--min-age=*" -
188             "--author=*" - "--committer=*" - "--grep=*" - "-[iE]" -
189             "--remove-empty" - "--first-parent" - "--cherry-pick" -
190             "-S*" - "--pickaxe-all" - "--pickaxe-regex" {
191                 # These mean that we get a subset of the commits
192                 set filtered 1
193                 lappend glflags $arg
194             }
195             "-n" {
196                 # This appears to be the only one that has a value as a
197                 # separate word following it
198                 set filtered 1
199                 set nextisval 1
200                 lappend glflags $arg
201             }
202             "--not" - "--all" {
203                 lappend revargs $arg
204             }
205             "--merge" {
206                 set vmergeonly($n) 1
207                 # git rev-parse doesn't understand --merge
208                 lappend revargs --gitk-symmetric-diff-marker MERGE_HEAD...HEAD
209             }
210             "-*" {
211                 # Other flag arguments including -<n>
212                 if {[string is digit -strict [string range $arg 1 end]]} {
213                     set filtered 1
214                 } else {
215                     # a flag argument that we don't recognize;
216                     # that means we can't optimize
217                     set allknown 0
218                 }
219                 lappend glflags $arg
220             }
221             default {
222                 # Non-flag arguments specify commits or ranges of commits
223                 if {[string match "*...*" $arg]} {
224                     lappend revargs --gitk-symmetric-diff-marker
225                 }
226                 lappend revargs $arg
227             }
228         }
229     }
230     set vdflags($n) $diffargs
231     set vflags($n) $glflags
232     set vrevs($n) $revargs
233     set vfiltered($n) $filtered
234     set vorigargs($n) $origargs
235     return $allknown
236 }
237
238 proc parseviewrevs {view revs} {
239     global vposids vnegids
240
241     if {$revs eq {}} {
242         set revs HEAD
243     }
244     if {[catch {set ids [eval exec git rev-parse $revs]} err]} {
245         # we get stdout followed by stderr in $err
246         # for an unknown rev, git rev-parse echoes it and then errors out
247         set errlines [split $err "\n"]
248         set badrev {}
249         for {set l 0} {$l < [llength $errlines]} {incr l} {
250             set line [lindex $errlines $l]
251             if {!([string length $line] == 40 && [string is xdigit $line])} {
252                 if {[string match "fatal:*" $line]} {
253                     if {[string match "fatal: ambiguous argument*" $line]
254                         && $badrev ne {}} {
255                         if {[llength $badrev] == 1} {
256                             set err "unknown revision $badrev"
257                         } else {
258                             set err "unknown revisions: [join $badrev ", "]"
259                         }
260                     } else {
261                         set err [join [lrange $errlines $l end] "\n"]
262                     }
263                     break
264                 }
265                 lappend badrev $line
266             }
267         }                   
268         error_popup "[mc "Error parsing revisions:"] $err"
269         return {}
270     }
271     set ret {}
272     set pos {}
273     set neg {}
274     set sdm 0
275     foreach id [split $ids "\n"] {
276         if {$id eq "--gitk-symmetric-diff-marker"} {
277             set sdm 4
278         } elseif {[string match "^*" $id]} {
279             if {$sdm != 1} {
280                 lappend ret $id
281                 if {$sdm == 3} {
282                     set sdm 0
283                 }
284             }
285             lappend neg [string range $id 1 end]
286         } else {
287             if {$sdm != 2} {
288                 lappend ret $id
289             } else {
290                 lset ret end [lindex $ret end]...$id
291             }
292             lappend pos $id
293         }
294         incr sdm -1
295     }
296     set vposids($view) $pos
297     set vnegids($view) $neg
298     return $ret
299 }
300
301 # Start off a git log process and arrange to read its output
302 proc start_rev_list {view} {
303     global startmsecs commitidx viewcomplete curview
304     global tclencoding
305     global viewargs viewargscmd viewfiles vfilelimit
306     global showlocalchanges
307     global viewactive viewinstances vmergeonly
308     global mainheadid viewmainheadid viewmainheadid_orig
309     global vcanopt vflags vrevs vorigargs
310
311     set startmsecs [clock clicks -milliseconds]
312     set commitidx($view) 0
313     # these are set this way for the error exits
314     set viewcomplete($view) 1
315     set viewactive($view) 0
316     varcinit $view
317
318     set args $viewargs($view)
319     if {$viewargscmd($view) ne {}} {
320         if {[catch {
321             set str [exec sh -c $viewargscmd($view)]
322         } err]} {
323             error_popup "[mc "Error executing --argscmd command:"] $err"
324             return 0
325         }
326         set args [concat $args [split $str "\n"]]
327     }
328     set vcanopt($view) [parseviewargs $view $args]
329
330     set files $viewfiles($view)
331     if {$vmergeonly($view)} {
332         set files [unmerged_files $files]
333         if {$files eq {}} {
334             global nr_unmerged
335             if {$nr_unmerged == 0} {
336                 error_popup [mc "No files selected: --merge specified but\
337                              no files are unmerged."]
338             } else {
339                 error_popup [mc "No files selected: --merge specified but\
340                              no unmerged files are within file limit."]
341             }
342             return 0
343         }
344     }
345     set vfilelimit($view) $files
346
347     if {$vcanopt($view)} {
348         set revs [parseviewrevs $view $vrevs($view)]
349         if {$revs eq {}} {
350             return 0
351         }
352         set args [concat $vflags($view) $revs]
353     } else {
354         set args $vorigargs($view)
355     }
356
357     if {[catch {
358         set fd [open [concat | git log --no-color -z --pretty=raw --parents \
359                          --boundary $args "--" $files] r]
360     } err]} {
361         error_popup "[mc "Error executing git log:"] $err"
362         return 0
363     }
364     set i [reg_instance $fd]
365     set viewinstances($view) [list $i]
366     set viewmainheadid($view) $mainheadid
367     set viewmainheadid_orig($view) $mainheadid
368     if {$files ne {} && $mainheadid ne {}} {
369         get_viewmainhead $view
370     }
371     if {$showlocalchanges && $viewmainheadid($view) ne {}} {
372         interestedin $viewmainheadid($view) dodiffindex
373     }
374     fconfigure $fd -blocking 0 -translation lf -eofchar {}
375     if {$tclencoding != {}} {
376         fconfigure $fd -encoding $tclencoding
377     }
378     filerun $fd [list getcommitlines $fd $i $view 0]
379     nowbusy $view [mc "Reading"]
380     set viewcomplete($view) 0
381     set viewactive($view) 1
382     return 1
383 }
384
385 proc stop_instance {inst} {
386     global commfd leftover
387
388     set fd $commfd($inst)
389     catch {
390         set pid [pid $fd]
391
392         if {$::tcl_platform(platform) eq {windows}} {
393             exec kill -f $pid
394         } else {
395             exec kill $pid
396         }
397     }
398     catch {close $fd}
399     nukefile $fd
400     unset commfd($inst)
401     unset leftover($inst)
402 }
403
404 proc stop_backends {} {
405     global commfd
406
407     foreach inst [array names commfd] {
408         stop_instance $inst
409     }
410 }
411
412 proc stop_rev_list {view} {
413     global viewinstances
414
415     foreach inst $viewinstances($view) {
416         stop_instance $inst
417     }
418     set viewinstances($view) {}
419 }
420
421 proc reset_pending_select {selid} {
422     global pending_select mainheadid selectheadid
423
424     if {$selid ne {}} {
425         set pending_select $selid
426     } elseif {$selectheadid ne {}} {
427         set pending_select $selectheadid
428     } else {
429         set pending_select $mainheadid
430     }
431 }
432
433 proc getcommits {selid} {
434     global canv curview need_redisplay viewactive
435
436     initlayout
437     if {[start_rev_list $curview]} {
438         reset_pending_select $selid
439         show_status [mc "Reading commits..."]
440         set need_redisplay 1
441     } else {
442         show_status [mc "No commits selected"]
443     }
444 }
445
446 proc updatecommits {} {
447     global curview vcanopt vorigargs vfilelimit viewinstances
448     global viewactive viewcomplete tclencoding
449     global startmsecs showneartags showlocalchanges
450     global mainheadid viewmainheadid viewmainheadid_orig pending_select
451     global isworktree
452     global varcid vposids vnegids vflags vrevs
453
454     set isworktree [expr {[exec git rev-parse --is-inside-work-tree] == "true"}]
455     rereadrefs
456     set view $curview
457     if {$mainheadid ne $viewmainheadid_orig($view)} {
458         if {$showlocalchanges} {
459             dohidelocalchanges
460         }
461         set viewmainheadid($view) $mainheadid
462         set viewmainheadid_orig($view) $mainheadid
463         if {$vfilelimit($view) ne {}} {
464             get_viewmainhead $view
465         }
466     }
467     if {$showlocalchanges} {
468         doshowlocalchanges
469     }
470     if {$vcanopt($view)} {
471         set oldpos $vposids($view)
472         set oldneg $vnegids($view)
473         set revs [parseviewrevs $view $vrevs($view)]
474         if {$revs eq {}} {
475             return
476         }
477         # note: getting the delta when negative refs change is hard,
478         # and could require multiple git log invocations, so in that
479         # case we ask git log for all the commits (not just the delta)
480         if {$oldneg eq $vnegids($view)} {
481             set newrevs {}
482             set npos 0
483             # take out positive refs that we asked for before or
484             # that we have already seen
485             foreach rev $revs {
486                 if {[string length $rev] == 40} {
487                     if {[lsearch -exact $oldpos $rev] < 0
488                         && ![info exists varcid($view,$rev)]} {
489                         lappend newrevs $rev
490                         incr npos
491                     }
492                 } else {
493                     lappend $newrevs $rev
494                 }
495             }
496             if {$npos == 0} return
497             set revs $newrevs
498             set vposids($view) [lsort -unique [concat $oldpos $vposids($view)]]
499         }
500         set args [concat $vflags($view) $revs --not $oldpos]
501     } else {
502         set args $vorigargs($view)
503     }
504     if {[catch {
505         set fd [open [concat | git log --no-color -z --pretty=raw --parents \
506                           --boundary $args "--" $vfilelimit($view)] r]
507     } err]} {
508         error_popup "[mc "Error executing git log:"] $err"
509         return
510     }
511     if {$viewactive($view) == 0} {
512         set startmsecs [clock clicks -milliseconds]
513     }
514     set i [reg_instance $fd]
515     lappend viewinstances($view) $i
516     fconfigure $fd -blocking 0 -translation lf -eofchar {}
517     if {$tclencoding != {}} {
518         fconfigure $fd -encoding $tclencoding
519     }
520     filerun $fd [list getcommitlines $fd $i $view 1]
521     incr viewactive($view)
522     set viewcomplete($view) 0
523     reset_pending_select {}
524     nowbusy $view "Reading"
525     if {$showneartags} {
526         getallcommits
527     }
528 }
529
530 proc reloadcommits {} {
531     global curview viewcomplete selectedline currentid thickerline
532     global showneartags treediffs commitinterest cached_commitrow
533     global targetid
534
535     set selid {}
536     if {$selectedline ne {}} {
537         set selid $currentid
538     }
539
540     if {!$viewcomplete($curview)} {
541         stop_rev_list $curview
542     }
543     resetvarcs $curview
544     set selectedline {}
545     catch {unset currentid}
546     catch {unset thickerline}
547     catch {unset treediffs}
548     readrefs
549     changedrefs
550     if {$showneartags} {
551         getallcommits
552     }
553     clear_display
554     catch {unset commitinterest}
555     catch {unset cached_commitrow}
556     catch {unset targetid}
557     setcanvscroll
558     getcommits $selid
559     return 0
560 }
561
562 # This makes a string representation of a positive integer which
563 # sorts as a string in numerical order
564 proc strrep {n} {
565     if {$n < 16} {
566         return [format "%x" $n]
567     } elseif {$n < 256} {
568         return [format "x%.2x" $n]
569     } elseif {$n < 65536} {
570         return [format "y%.4x" $n]
571     }
572     return [format "z%.8x" $n]
573 }
574
575 # Procedures used in reordering commits from git log (without
576 # --topo-order) into the order for display.
577
578 proc varcinit {view} {
579     global varcstart vupptr vdownptr vleftptr vbackptr varctok varcrow
580     global vtokmod varcmod vrowmod varcix vlastins
581
582     set varcstart($view) {{}}
583     set vupptr($view) {0}
584     set vdownptr($view) {0}
585     set vleftptr($view) {0}
586     set vbackptr($view) {0}
587     set varctok($view) {{}}
588     set varcrow($view) {{}}
589     set vtokmod($view) {}
590     set varcmod($view) 0
591     set vrowmod($view) 0
592     set varcix($view) {{}}
593     set vlastins($view) {0}
594 }
595
596 proc resetvarcs {view} {
597     global varcid varccommits parents children vseedcount ordertok
598
599     foreach vid [array names varcid $view,*] {
600         unset varcid($vid)
601         unset children($vid)
602         unset parents($vid)
603     }
604     # some commits might have children but haven't been seen yet
605     foreach vid [array names children $view,*] {
606         unset children($vid)
607     }
608     foreach va [array names varccommits $view,*] {
609         unset varccommits($va)
610     }
611     foreach vd [array names vseedcount $view,*] {
612         unset vseedcount($vd)
613     }
614     catch {unset ordertok}
615 }
616
617 # returns a list of the commits with no children
618 proc seeds {v} {
619     global vdownptr vleftptr varcstart
620
621     set ret {}
622     set a [lindex $vdownptr($v) 0]
623     while {$a != 0} {
624         lappend ret [lindex $varcstart($v) $a]
625         set a [lindex $vleftptr($v) $a]
626     }
627     return $ret
628 }
629
630 proc newvarc {view id} {
631     global varcid varctok parents children vdatemode
632     global vupptr vdownptr vleftptr vbackptr varcrow varcix varcstart
633     global commitdata commitinfo vseedcount varccommits vlastins
634
635     set a [llength $varctok($view)]
636     set vid $view,$id
637     if {[llength $children($vid)] == 0 || $vdatemode($view)} {
638         if {![info exists commitinfo($id)]} {
639             parsecommit $id $commitdata($id) 1
640         }
641         set cdate [lindex $commitinfo($id) 4]
642         if {![string is integer -strict $cdate]} {
643             set cdate 0
644         }
645         if {![info exists vseedcount($view,$cdate)]} {
646             set vseedcount($view,$cdate) -1
647         }
648         set c [incr vseedcount($view,$cdate)]
649         set cdate [expr {$cdate ^ 0xffffffff}]
650         set tok "s[strrep $cdate][strrep $c]"
651     } else {
652         set tok {}
653     }
654     set ka 0
655     if {[llength $children($vid)] > 0} {
656         set kid [lindex $children($vid) end]
657         set k $varcid($view,$kid)
658         if {[string compare [lindex $varctok($view) $k] $tok] > 0} {
659             set ki $kid
660             set ka $k
661             set tok [lindex $varctok($view) $k]
662         }
663     }
664     if {$ka != 0} {
665         set i [lsearch -exact $parents($view,$ki) $id]
666         set j [expr {[llength $parents($view,$ki)] - 1 - $i}]
667         append tok [strrep $j]
668     }
669     set c [lindex $vlastins($view) $ka]
670     if {$c == 0 || [string compare $tok [lindex $varctok($view) $c]] < 0} {
671         set c $ka
672         set b [lindex $vdownptr($view) $ka]
673     } else {
674         set b [lindex $vleftptr($view) $c]
675     }
676     while {$b != 0 && [string compare $tok [lindex $varctok($view) $b]] >= 0} {
677         set c $b
678         set b [lindex $vleftptr($view) $c]
679     }
680     if {$c == $ka} {
681         lset vdownptr($view) $ka $a
682         lappend vbackptr($view) 0
683     } else {
684         lset vleftptr($view) $c $a
685         lappend vbackptr($view) $c
686     }
687     lset vlastins($view) $ka $a
688     lappend vupptr($view) $ka
689     lappend vleftptr($view) $b
690     if {$b != 0} {
691         lset vbackptr($view) $b $a
692     }
693     lappend varctok($view) $tok
694     lappend varcstart($view) $id
695     lappend vdownptr($view) 0
696     lappend varcrow($view) {}
697     lappend varcix($view) {}
698     set varccommits($view,$a) {}
699     lappend vlastins($view) 0
700     return $a
701 }
702
703 proc splitvarc {p v} {
704     global varcid varcstart varccommits varctok vtokmod
705     global vupptr vdownptr vleftptr vbackptr varcix varcrow vlastins
706
707     set oa $varcid($v,$p)
708     set otok [lindex $varctok($v) $oa]
709     set ac $varccommits($v,$oa)
710     set i [lsearch -exact $varccommits($v,$oa) $p]
711     if {$i <= 0} return
712     set na [llength $varctok($v)]
713     # "%" sorts before "0"...
714     set tok "$otok%[strrep $i]"
715     lappend varctok($v) $tok
716     lappend varcrow($v) {}
717     lappend varcix($v) {}
718     set varccommits($v,$oa) [lrange $ac 0 [expr {$i - 1}]]
719     set varccommits($v,$na) [lrange $ac $i end]
720     lappend varcstart($v) $p
721     foreach id $varccommits($v,$na) {
722         set varcid($v,$id) $na
723     }
724     lappend vdownptr($v) [lindex $vdownptr($v) $oa]
725     lappend vlastins($v) [lindex $vlastins($v) $oa]
726     lset vdownptr($v) $oa $na
727     lset vlastins($v) $oa 0
728     lappend vupptr($v) $oa
729     lappend vleftptr($v) 0
730     lappend vbackptr($v) 0
731     for {set b [lindex $vdownptr($v) $na]} {$b != 0} {set b [lindex $vleftptr($v) $b]} {
732         lset vupptr($v) $b $na
733     }
734     if {[string compare $otok $vtokmod($v)] <= 0} {
735         modify_arc $v $oa
736     }
737 }
738
739 proc renumbervarc {a v} {
740     global parents children varctok varcstart varccommits
741     global vupptr vdownptr vleftptr vbackptr vlastins varcid vtokmod vdatemode
742
743     set t1 [clock clicks -milliseconds]
744     set todo {}
745     set isrelated($a) 1
746     set kidchanged($a) 1
747     set ntot 0
748     while {$a != 0} {
749         if {[info exists isrelated($a)]} {
750             lappend todo $a
751             set id [lindex $varccommits($v,$a) end]
752             foreach p $parents($v,$id) {
753                 if {[info exists varcid($v,$p)]} {
754                     set isrelated($varcid($v,$p)) 1
755                 }
756             }
757         }
758         incr ntot
759         set b [lindex $vdownptr($v) $a]
760         if {$b == 0} {
761             while {$a != 0} {
762                 set b [lindex $vleftptr($v) $a]
763                 if {$b != 0} break
764                 set a [lindex $vupptr($v) $a]
765             }
766         }
767         set a $b
768     }
769     foreach a $todo {
770         if {![info exists kidchanged($a)]} continue
771         set id [lindex $varcstart($v) $a]
772         if {[llength $children($v,$id)] > 1} {
773             set children($v,$id) [lsort -command [list vtokcmp $v] \
774                                       $children($v,$id)]
775         }
776         set oldtok [lindex $varctok($v) $a]
777         if {!$vdatemode($v)} {
778             set tok {}
779         } else {
780             set tok $oldtok
781         }
782         set ka 0
783         set kid [last_real_child $v,$id]
784         if {$kid ne {}} {
785             set k $varcid($v,$kid)
786             if {[string compare [lindex $varctok($v) $k] $tok] > 0} {
787                 set ki $kid
788                 set ka $k
789                 set tok [lindex $varctok($v) $k]
790             }
791         }
792         if {$ka != 0} {
793             set i [lsearch -exact $parents($v,$ki) $id]
794             set j [expr {[llength $parents($v,$ki)] - 1 - $i}]
795             append tok [strrep $j]
796         }
797         if {$tok eq $oldtok} {
798             continue
799         }
800         set id [lindex $varccommits($v,$a) end]
801         foreach p $parents($v,$id) {
802             if {[info exists varcid($v,$p)]} {
803                 set kidchanged($varcid($v,$p)) 1
804             } else {
805                 set sortkids($p) 1
806             }
807         }
808         lset varctok($v) $a $tok
809         set b [lindex $vupptr($v) $a]
810         if {$b != $ka} {
811             if {[string compare [lindex $varctok($v) $ka] $vtokmod($v)] < 0} {
812                 modify_arc $v $ka
813             }
814             if {[string compare [lindex $varctok($v) $b] $vtokmod($v)] < 0} {
815                 modify_arc $v $b
816             }
817             set c [lindex $vbackptr($v) $a]
818             set d [lindex $vleftptr($v) $a]
819             if {$c == 0} {
820                 lset vdownptr($v) $b $d
821             } else {
822                 lset vleftptr($v) $c $d
823             }
824             if {$d != 0} {
825                 lset vbackptr($v) $d $c
826             }
827             if {[lindex $vlastins($v) $b] == $a} {
828                 lset vlastins($v) $b $c
829             }
830             lset vupptr($v) $a $ka
831             set c [lindex $vlastins($v) $ka]
832             if {$c == 0 || \
833                     [string compare $tok [lindex $varctok($v) $c]] < 0} {
834                 set c $ka
835                 set b [lindex $vdownptr($v) $ka]
836             } else {
837                 set b [lindex $vleftptr($v) $c]
838             }
839             while {$b != 0 && \
840                       [string compare $tok [lindex $varctok($v) $b]] >= 0} {
841                 set c $b
842                 set b [lindex $vleftptr($v) $c]
843             }
844             if {$c == $ka} {
845                 lset vdownptr($v) $ka $a
846                 lset vbackptr($v) $a 0
847             } else {
848                 lset vleftptr($v) $c $a
849                 lset vbackptr($v) $a $c
850             }
851             lset vleftptr($v) $a $b
852             if {$b != 0} {
853                 lset vbackptr($v) $b $a
854             }
855             lset vlastins($v) $ka $a
856         }
857     }
858     foreach id [array names sortkids] {
859         if {[llength $children($v,$id)] > 1} {
860             set children($v,$id) [lsort -command [list vtokcmp $v] \
861                                       $children($v,$id)]
862         }
863     }
864     set t2 [clock clicks -milliseconds]
865     #puts "renumbervarc did [llength $todo] of $ntot arcs in [expr {$t2-$t1}]ms"
866 }
867
868 # Fix up the graph after we have found out that in view $v,
869 # $p (a commit that we have already seen) is actually the parent
870 # of the last commit in arc $a.
871 proc fix_reversal {p a v} {
872     global varcid varcstart varctok vupptr
873
874     set pa $varcid($v,$p)
875     if {$p ne [lindex $varcstart($v) $pa]} {
876         splitvarc $p $v
877         set pa $varcid($v,$p)
878     }
879     # seeds always need to be renumbered
880     if {[lindex $vupptr($v) $pa] == 0 ||
881         [string compare [lindex $varctok($v) $a] \
882              [lindex $varctok($v) $pa]] > 0} {
883         renumbervarc $pa $v
884     }
885 }
886
887 proc insertrow {id p v} {
888     global cmitlisted children parents varcid varctok vtokmod
889     global varccommits ordertok commitidx numcommits curview
890     global targetid targetrow
891
892     readcommit $id
893     set vid $v,$id
894     set cmitlisted($vid) 1
895     set children($vid) {}
896     set parents($vid) [list $p]
897     set a [newvarc $v $id]
898     set varcid($vid) $a
899     if {[string compare [lindex $varctok($v) $a] $vtokmod($v)] < 0} {
900         modify_arc $v $a
901     }
902     lappend varccommits($v,$a) $id
903     set vp $v,$p
904     if {[llength [lappend children($vp) $id]] > 1} {
905         set children($vp) [lsort -command [list vtokcmp $v] $children($vp)]
906         catch {unset ordertok}
907     }
908     fix_reversal $p $a $v
909     incr commitidx($v)
910     if {$v == $curview} {
911         set numcommits $commitidx($v)
912         setcanvscroll
913         if {[info exists targetid]} {
914             if {![comes_before $targetid $p]} {
915                 incr targetrow
916             }
917         }
918     }
919 }
920
921 proc insertfakerow {id p} {
922     global varcid varccommits parents children cmitlisted
923     global commitidx varctok vtokmod targetid targetrow curview numcommits
924
925     set v $curview
926     set a $varcid($v,$p)
927     set i [lsearch -exact $varccommits($v,$a) $p]
928     if {$i < 0} {
929         puts "oops: insertfakerow can't find [shortids $p] on arc $a"
930         return
931     }
932     set children($v,$id) {}
933     set parents($v,$id) [list $p]
934     set varcid($v,$id) $a
935     lappend children($v,$p) $id
936     set cmitlisted($v,$id) 1
937     set numcommits [incr commitidx($v)]
938     # note we deliberately don't update varcstart($v) even if $i == 0
939     set varccommits($v,$a) [linsert $varccommits($v,$a) $i $id]
940     modify_arc $v $a $i
941     if {[info exists targetid]} {
942         if {![comes_before $targetid $p]} {
943             incr targetrow
944         }
945     }
946     setcanvscroll
947     drawvisible
948 }
949
950 proc removefakerow {id} {
951     global varcid varccommits parents children commitidx
952     global varctok vtokmod cmitlisted currentid selectedline
953     global targetid curview numcommits
954
955     set v $curview
956     if {[llength $parents($v,$id)] != 1} {
957         puts "oops: removefakerow [shortids $id] has [llength $parents($v,$id)] parents"
958         return
959     }
960     set p [lindex $parents($v,$id) 0]
961     set a $varcid($v,$id)
962     set i [lsearch -exact $varccommits($v,$a) $id]
963     if {$i < 0} {
964         puts "oops: removefakerow can't find [shortids $id] on arc $a"
965         return
966     }
967     unset varcid($v,$id)
968     set varccommits($v,$a) [lreplace $varccommits($v,$a) $i $i]
969     unset parents($v,$id)
970     unset children($v,$id)
971     unset cmitlisted($v,$id)
972     set numcommits [incr commitidx($v) -1]
973     set j [lsearch -exact $children($v,$p) $id]
974     if {$j >= 0} {
975         set children($v,$p) [lreplace $children($v,$p) $j $j]
976     }
977     modify_arc $v $a $i
978     if {[info exist currentid] && $id eq $currentid} {
979         unset currentid
980         set selectedline {}
981     }
982     if {[info exists targetid] && $targetid eq $id} {
983         set targetid $p
984     }
985     setcanvscroll
986     drawvisible
987 }
988
989 proc first_real_child {vp} {
990     global children nullid nullid2
991
992     foreach id $children($vp) {
993         if {$id ne $nullid && $id ne $nullid2} {
994             return $id
995         }
996     }
997     return {}
998 }
999
1000 proc last_real_child {vp} {
1001     global children nullid nullid2
1002
1003     set kids $children($vp)
1004     for {set i [llength $kids]} {[incr i -1] >= 0} {} {
1005         set id [lindex $kids $i]
1006         if {$id ne $nullid && $id ne $nullid2} {
1007             return $id
1008         }
1009     }
1010     return {}
1011 }
1012
1013 proc vtokcmp {v a b} {
1014     global varctok varcid
1015
1016     return [string compare [lindex $varctok($v) $varcid($v,$a)] \
1017                 [lindex $varctok($v) $varcid($v,$b)]]
1018 }
1019
1020 # This assumes that if lim is not given, the caller has checked that
1021 # arc a's token is less than $vtokmod($v)
1022 proc modify_arc {v a {lim {}}} {
1023     global varctok vtokmod varcmod varcrow vupptr curview vrowmod varccommits
1024
1025     if {$lim ne {}} {
1026         set c [string compare [lindex $varctok($v) $a] $vtokmod($v)]
1027         if {$c > 0} return
1028         if {$c == 0} {
1029             set r [lindex $varcrow($v) $a]
1030             if {$r ne {} && $vrowmod($v) <= $r + $lim} return
1031         }
1032     }
1033     set vtokmod($v) [lindex $varctok($v) $a]
1034     set varcmod($v) $a
1035     if {$v == $curview} {
1036         while {$a != 0 && [lindex $varcrow($v) $a] eq {}} {
1037             set a [lindex $vupptr($v) $a]
1038             set lim {}
1039         }
1040         set r 0
1041         if {$a != 0} {
1042             if {$lim eq {}} {
1043                 set lim [llength $varccommits($v,$a)]
1044             }
1045             set r [expr {[lindex $varcrow($v) $a] + $lim}]
1046         }
1047         set vrowmod($v) $r
1048         undolayout $r
1049     }
1050 }
1051
1052 proc update_arcrows {v} {
1053     global vtokmod varcmod vrowmod varcrow commitidx currentid selectedline
1054     global varcid vrownum varcorder varcix varccommits
1055     global vupptr vdownptr vleftptr varctok
1056     global displayorder parentlist curview cached_commitrow
1057
1058     if {$vrowmod($v) == $commitidx($v)} return
1059     if {$v == $curview} {
1060         if {[llength $displayorder] > $vrowmod($v)} {
1061             set displayorder [lrange $displayorder 0 [expr {$vrowmod($v) - 1}]]
1062             set parentlist [lrange $parentlist 0 [expr {$vrowmod($v) - 1}]]
1063         }
1064         catch {unset cached_commitrow}
1065     }
1066     set narctot [expr {[llength $varctok($v)] - 1}]
1067     set a $varcmod($v)
1068     while {$a != 0 && [lindex $varcix($v) $a] eq {}} {
1069         # go up the tree until we find something that has a row number,
1070         # or we get to a seed
1071         set a [lindex $vupptr($v) $a]
1072     }
1073     if {$a == 0} {
1074         set a [lindex $vdownptr($v) 0]
1075         if {$a == 0} return
1076         set vrownum($v) {0}
1077         set varcorder($v) [list $a]
1078         lset varcix($v) $a 0
1079         lset varcrow($v) $a 0
1080         set arcn 0
1081         set row 0
1082     } else {
1083         set arcn [lindex $varcix($v) $a]
1084         if {[llength $vrownum($v)] > $arcn + 1} {
1085             set vrownum($v) [lrange $vrownum($v) 0 $arcn]
1086             set varcorder($v) [lrange $varcorder($v) 0 $arcn]
1087         }
1088         set row [lindex $varcrow($v) $a]
1089     }
1090     while {1} {
1091         set p $a
1092         incr row [llength $varccommits($v,$a)]
1093         # go down if possible
1094         set b [lindex $vdownptr($v) $a]
1095         if {$b == 0} {
1096             # if not, go left, or go up until we can go left
1097             while {$a != 0} {
1098                 set b [lindex $vleftptr($v) $a]
1099                 if {$b != 0} break
1100                 set a [lindex $vupptr($v) $a]
1101             }
1102             if {$a == 0} break
1103         }
1104         set a $b
1105         incr arcn
1106         lappend vrownum($v) $row
1107         lappend varcorder($v) $a
1108         lset varcix($v) $a $arcn
1109         lset varcrow($v) $a $row
1110     }
1111     set vtokmod($v) [lindex $varctok($v) $p]
1112     set varcmod($v) $p
1113     set vrowmod($v) $row
1114     if {[info exists currentid]} {
1115         set selectedline [rowofcommit $currentid]
1116     }
1117 }
1118
1119 # Test whether view $v contains commit $id
1120 proc commitinview {id v} {
1121     global varcid
1122
1123     return [info exists varcid($v,$id)]
1124 }
1125
1126 # Return the row number for commit $id in the current view
1127 proc rowofcommit {id} {
1128     global varcid varccommits varcrow curview cached_commitrow
1129     global varctok vtokmod
1130
1131     set v $curview
1132     if {![info exists varcid($v,$id)]} {
1133         puts "oops rowofcommit no arc for [shortids $id]"
1134         return {}
1135     }
1136     set a $varcid($v,$id)
1137     if {[string compare [lindex $varctok($v) $a] $vtokmod($v)] >= 0} {
1138         update_arcrows $v
1139     }
1140     if {[info exists cached_commitrow($id)]} {
1141         return $cached_commitrow($id)
1142     }
1143     set i [lsearch -exact $varccommits($v,$a) $id]
1144     if {$i < 0} {
1145         puts "oops didn't find commit [shortids $id] in arc $a"
1146         return {}
1147     }
1148     incr i [lindex $varcrow($v) $a]
1149     set cached_commitrow($id) $i
1150     return $i
1151 }
1152
1153 # Returns 1 if a is on an earlier row than b, otherwise 0
1154 proc comes_before {a b} {
1155     global varcid varctok curview
1156
1157     set v $curview
1158     if {$a eq $b || ![info exists varcid($v,$a)] || \
1159             ![info exists varcid($v,$b)]} {
1160         return 0
1161     }
1162     if {$varcid($v,$a) != $varcid($v,$b)} {
1163         return [expr {[string compare [lindex $varctok($v) $varcid($v,$a)] \
1164                            [lindex $varctok($v) $varcid($v,$b)]] < 0}]
1165     }
1166     return [expr {[rowofcommit $a] < [rowofcommit $b]}]
1167 }
1168
1169 proc bsearch {l elt} {
1170     if {[llength $l] == 0 || $elt <= [lindex $l 0]} {
1171         return 0
1172     }
1173     set lo 0
1174     set hi [llength $l]
1175     while {$hi - $lo > 1} {
1176         set mid [expr {int(($lo + $hi) / 2)}]
1177         set t [lindex $l $mid]
1178         if {$elt < $t} {
1179             set hi $mid
1180         } elseif {$elt > $t} {
1181             set lo $mid
1182         } else {
1183             return $mid
1184         }
1185     }
1186     return $lo
1187 }
1188
1189 # Make sure rows $start..$end-1 are valid in displayorder and parentlist
1190 proc make_disporder {start end} {
1191     global vrownum curview commitidx displayorder parentlist
1192     global varccommits varcorder parents vrowmod varcrow
1193     global d_valid_start d_valid_end
1194
1195     if {$end > $vrowmod($curview)} {
1196         update_arcrows $curview
1197     }
1198     set ai [bsearch $vrownum($curview) $start]
1199     set start [lindex $vrownum($curview) $ai]
1200     set narc [llength $vrownum($curview)]
1201     for {set r $start} {$ai < $narc && $r < $end} {incr ai} {
1202         set a [lindex $varcorder($curview) $ai]
1203         set l [llength $displayorder]
1204         set al [llength $varccommits($curview,$a)]
1205         if {$l < $r + $al} {
1206             if {$l < $r} {
1207                 set pad [ntimes [expr {$r - $l}] {}]
1208                 set displayorder [concat $displayorder $pad]
1209                 set parentlist [concat $parentlist $pad]
1210             } elseif {$l > $r} {
1211                 set displayorder [lrange $displayorder 0 [expr {$r - 1}]]
1212                 set parentlist [lrange $parentlist 0 [expr {$r - 1}]]
1213             }
1214             foreach id $varccommits($curview,$a) {
1215                 lappend displayorder $id
1216                 lappend parentlist $parents($curview,$id)
1217             }
1218         } elseif {[lindex $displayorder [expr {$r + $al - 1}]] eq {}} {
1219             set i $r
1220             foreach id $varccommits($curview,$a) {
1221                 lset displayorder $i $id
1222                 lset parentlist $i $parents($curview,$id)
1223                 incr i
1224             }
1225         }
1226         incr r $al
1227     }
1228 }
1229
1230 proc commitonrow {row} {
1231     global displayorder
1232
1233     set id [lindex $displayorder $row]
1234     if {$id eq {}} {
1235         make_disporder $row [expr {$row + 1}]
1236         set id [lindex $displayorder $row]
1237     }
1238     return $id
1239 }
1240
1241 proc closevarcs {v} {
1242     global varctok varccommits varcid parents children
1243     global cmitlisted commitidx vtokmod
1244
1245     set missing_parents 0
1246     set scripts {}
1247     set narcs [llength $varctok($v)]
1248     for {set a 1} {$a < $narcs} {incr a} {
1249         set id [lindex $varccommits($v,$a) end]
1250         foreach p $parents($v,$id) {
1251             if {[info exists varcid($v,$p)]} continue
1252             # add p as a new commit
1253             incr missing_parents
1254             set cmitlisted($v,$p) 0
1255             set parents($v,$p) {}
1256             if {[llength $children($v,$p)] == 1 &&
1257                 [llength $parents($v,$id)] == 1} {
1258                 set b $a
1259             } else {
1260                 set b [newvarc $v $p]
1261             }
1262             set varcid($v,$p) $b
1263             if {[string compare [lindex $varctok($v) $b] $vtokmod($v)] < 0} {
1264                 modify_arc $v $b
1265             }
1266             lappend varccommits($v,$b) $p
1267             incr commitidx($v)
1268             set scripts [check_interest $p $scripts]
1269         }
1270     }
1271     if {$missing_parents > 0} {
1272         foreach s $scripts {
1273             eval $s
1274         }
1275     }
1276 }
1277
1278 # Use $rwid as a substitute for $id, i.e. reparent $id's children to $rwid
1279 # Assumes we already have an arc for $rwid.
1280 proc rewrite_commit {v id rwid} {
1281     global children parents varcid varctok vtokmod varccommits
1282
1283     foreach ch $children($v,$id) {
1284         # make $rwid be $ch's parent in place of $id
1285         set i [lsearch -exact $parents($v,$ch) $id]
1286         if {$i < 0} {
1287             puts "oops rewrite_commit didn't find $id in parent list for $ch"
1288         }
1289         set parents($v,$ch) [lreplace $parents($v,$ch) $i $i $rwid]
1290         # add $ch to $rwid's children and sort the list if necessary
1291         if {[llength [lappend children($v,$rwid) $ch]] > 1} {
1292             set children($v,$rwid) [lsort -command [list vtokcmp $v] \
1293                                         $children($v,$rwid)]
1294         }
1295         # fix the graph after joining $id to $rwid
1296         set a $varcid($v,$ch)
1297         fix_reversal $rwid $a $v
1298         # parentlist is wrong for the last element of arc $a
1299         # even if displayorder is right, hence the 3rd arg here
1300         modify_arc $v $a [expr {[llength $varccommits($v,$a)] - 1}]
1301     }
1302 }
1303
1304 # Mechanism for registering a command to be executed when we come
1305 # across a particular commit.  To handle the case when only the
1306 # prefix of the commit is known, the commitinterest array is now
1307 # indexed by the first 4 characters of the ID.  Each element is a
1308 # list of id, cmd pairs.
1309 proc interestedin {id cmd} {
1310     global commitinterest
1311
1312     lappend commitinterest([string range $id 0 3]) $id $cmd
1313 }
1314
1315 proc check_interest {id scripts} {
1316     global commitinterest
1317
1318     set prefix [string range $id 0 3]
1319     if {[info exists commitinterest($prefix)]} {
1320         set newlist {}
1321         foreach {i script} $commitinterest($prefix) {
1322             if {[string match "$i*" $id]} {
1323                 lappend scripts [string map [list "%I" $id "%P" $i] $script]
1324             } else {
1325                 lappend newlist $i $script
1326             }
1327         }
1328         if {$newlist ne {}} {
1329             set commitinterest($prefix) $newlist
1330         } else {
1331             unset commitinterest($prefix)
1332         }
1333     }
1334     return $scripts
1335 }
1336
1337 proc getcommitlines {fd inst view updating}  {
1338     global cmitlisted leftover
1339     global commitidx commitdata vdatemode
1340     global parents children curview hlview
1341     global idpending ordertok
1342     global varccommits varcid varctok vtokmod vfilelimit
1343
1344     set stuff [read $fd 500000]
1345     # git log doesn't terminate the last commit with a null...
1346     if {$stuff == {} && $leftover($inst) ne {} && [eof $fd]} {
1347         set stuff "\0"
1348     }
1349     if {$stuff == {}} {
1350         if {![eof $fd]} {
1351             return 1
1352         }
1353         global commfd viewcomplete viewactive viewname
1354         global viewinstances
1355         unset commfd($inst)
1356         set i [lsearch -exact $viewinstances($view) $inst]
1357         if {$i >= 0} {
1358             set viewinstances($view) [lreplace $viewinstances($view) $i $i]
1359         }
1360         # set it blocking so we wait for the process to terminate
1361         fconfigure $fd -blocking 1
1362         if {[catch {close $fd} err]} {
1363             set fv {}
1364             if {$view != $curview} {
1365                 set fv " for the \"$viewname($view)\" view"
1366             }
1367             if {[string range $err 0 4] == "usage"} {
1368                 set err "Gitk: error reading commits$fv:\
1369                         bad arguments to git log."
1370                 if {$viewname($view) eq "Command line"} {
1371                     append err \
1372                         "  (Note: arguments to gitk are passed to git log\
1373                          to allow selection of commits to be displayed.)"
1374                 }
1375             } else {
1376                 set err "Error reading commits$fv: $err"
1377             }
1378             error_popup $err
1379         }
1380         if {[incr viewactive($view) -1] <= 0} {
1381             set viewcomplete($view) 1
1382             # Check if we have seen any ids listed as parents that haven't
1383             # appeared in the list
1384             closevarcs $view
1385             notbusy $view
1386         }
1387         if {$view == $curview} {
1388             run chewcommits
1389         }
1390         return 0
1391     }
1392     set start 0
1393     set gotsome 0
1394     set scripts {}
1395     while 1 {
1396         set i [string first "\0" $stuff $start]
1397         if {$i < 0} {
1398             append leftover($inst) [string range $stuff $start end]
1399             break
1400         }
1401         if {$start == 0} {
1402             set cmit $leftover($inst)
1403             append cmit [string range $stuff 0 [expr {$i - 1}]]
1404             set leftover($inst) {}
1405         } else {
1406             set cmit [string range $stuff $start [expr {$i - 1}]]
1407         }
1408         set start [expr {$i + 1}]
1409         set j [string first "\n" $cmit]
1410         set ok 0
1411         set listed 1
1412         if {$j >= 0 && [string match "commit *" $cmit]} {
1413             set ids [string range $cmit 7 [expr {$j - 1}]]
1414             if {[string match {[-^<>]*} $ids]} {
1415                 switch -- [string index $ids 0] {
1416                     "-" {set listed 0}
1417                     "^" {set listed 2}
1418                     "<" {set listed 3}
1419                     ">" {set listed 4}
1420                 }
1421                 set ids [string range $ids 1 end]
1422             }
1423             set ok 1
1424             foreach id $ids {
1425                 if {[string length $id] != 40} {
1426                     set ok 0
1427                     break
1428                 }
1429             }
1430         }
1431         if {!$ok} {
1432             set shortcmit $cmit
1433             if {[string length $shortcmit] > 80} {
1434                 set shortcmit "[string range $shortcmit 0 80]..."
1435             }
1436             error_popup "[mc "Can't parse git log output:"] {$shortcmit}"
1437             exit 1
1438         }
1439         set id [lindex $ids 0]
1440         set vid $view,$id
1441
1442         if {!$listed && $updating && ![info exists varcid($vid)] &&
1443             $vfilelimit($view) ne {}} {
1444             # git log doesn't rewrite parents for unlisted commits
1445             # when doing path limiting, so work around that here
1446             # by working out the rewritten parent with git rev-list
1447             # and if we already know about it, using the rewritten
1448             # parent as a substitute parent for $id's children.
1449             if {![catch {
1450                 set rwid [exec git rev-list --first-parent --max-count=1 \
1451                               $id -- $vfilelimit($view)]
1452             }]} {
1453                 if {$rwid ne {} && [info exists varcid($view,$rwid)]} {
1454                     # use $rwid in place of $id
1455                     rewrite_commit $view $id $rwid
1456                     continue
1457                 }
1458             }
1459         }
1460
1461         set a 0
1462         if {[info exists varcid($vid)]} {
1463             if {$cmitlisted($vid) || !$listed} continue
1464             set a $varcid($vid)
1465         }
1466         if {$listed} {
1467             set olds [lrange $ids 1 end]
1468         } else {
1469             set olds {}
1470         }
1471         set commitdata($id) [string range $cmit [expr {$j + 1}] end]
1472         set cmitlisted($vid) $listed
1473         set parents($vid) $olds
1474         if {![info exists children($vid)]} {
1475             set children($vid) {}
1476         } elseif {$a == 0 && [llength $children($vid)] == 1} {
1477             set k [lindex $children($vid) 0]
1478             if {[llength $parents($view,$k)] == 1 &&
1479                 (!$vdatemode($view) ||
1480                  $varcid($view,$k) == [llength $varctok($view)] - 1)} {
1481                 set a $varcid($view,$k)
1482             }
1483         }
1484         if {$a == 0} {
1485             # new arc
1486             set a [newvarc $view $id]
1487         }
1488         if {[string compare [lindex $varctok($view) $a] $vtokmod($view)] < 0} {
1489             modify_arc $view $a
1490         }
1491         if {![info exists varcid($vid)]} {
1492             set varcid($vid) $a
1493             lappend varccommits($view,$a) $id
1494             incr commitidx($view)
1495         }
1496
1497         set i 0
1498         foreach p $olds {
1499             if {$i == 0 || [lsearch -exact $olds $p] >= $i} {
1500                 set vp $view,$p
1501                 if {[llength [lappend children($vp) $id]] > 1 &&
1502                     [vtokcmp $view [lindex $children($vp) end-1] $id] > 0} {
1503                     set children($vp) [lsort -command [list vtokcmp $view] \
1504                                            $children($vp)]
1505                     catch {unset ordertok}
1506                 }
1507                 if {[info exists varcid($view,$p)]} {
1508                     fix_reversal $p $a $view
1509                 }
1510             }
1511             incr i
1512         }
1513
1514         set scripts [check_interest $id $scripts]
1515         set gotsome 1
1516     }
1517     if {$gotsome} {
1518         global numcommits hlview
1519
1520         if {$view == $curview} {
1521             set numcommits $commitidx($view)
1522             run chewcommits
1523         }
1524         if {[info exists hlview] && $view == $hlview} {
1525             # we never actually get here...
1526             run vhighlightmore
1527         }
1528         foreach s $scripts {
1529             eval $s
1530         }
1531     }
1532     return 2
1533 }
1534
1535 proc chewcommits {} {
1536     global curview hlview viewcomplete
1537     global pending_select
1538
1539     layoutmore
1540     if {$viewcomplete($curview)} {
1541         global commitidx varctok
1542         global numcommits startmsecs
1543
1544         if {[info exists pending_select]} {
1545             update
1546             reset_pending_select {}
1547
1548             if {[commitinview $pending_select $curview]} {
1549                 selectline [rowofcommit $pending_select] 1
1550             } else {
1551                 set row [first_real_row]
1552                 selectline $row 1
1553             }
1554         }
1555         if {$commitidx($curview) > 0} {
1556             #set ms [expr {[clock clicks -milliseconds] - $startmsecs}]
1557             #puts "overall $ms ms for $numcommits commits"
1558             #puts "[llength $varctok($view)] arcs, $commitidx($view) commits"
1559         } else {
1560             show_status [mc "No commits selected"]
1561         }
1562         notbusy layout
1563     }
1564     return 0
1565 }
1566
1567 proc do_readcommit {id} {
1568     global tclencoding
1569
1570     # Invoke git-log to handle automatic encoding conversion
1571     set fd [open [concat | git log --no-color --pretty=raw -1 $id] r]
1572     # Read the results using i18n.logoutputencoding
1573     fconfigure $fd -translation lf -eofchar {}
1574     if {$tclencoding != {}} {
1575         fconfigure $fd -encoding $tclencoding
1576     }
1577     set contents [read $fd]
1578     close $fd
1579     # Remove the heading line
1580     regsub {^commit [0-9a-f]+\n} $contents {} contents
1581
1582     return $contents
1583 }
1584
1585 proc readcommit {id} {
1586     if {[catch {set contents [do_readcommit $id]}]} return
1587     parsecommit $id $contents 1
1588 }
1589
1590 proc parsecommit {id contents listed} {
1591     global commitinfo cdate
1592
1593     set inhdr 1
1594     set comment {}
1595     set headline {}
1596     set auname {}
1597     set audate {}
1598     set comname {}
1599     set comdate {}
1600     set hdrend [string first "\n\n" $contents]
1601     if {$hdrend < 0} {
1602         # should never happen...
1603         set hdrend [string length $contents]
1604     }
1605     set header [string range $contents 0 [expr {$hdrend - 1}]]
1606     set comment [string range $contents [expr {$hdrend + 2}] end]
1607     foreach line [split $header "\n"] {
1608         set line [split $line " "]
1609         set tag [lindex $line 0]
1610         if {$tag == "author"} {
1611             set audate [lindex $line end-1]
1612             set auname [join [lrange $line 1 end-2] " "]
1613         } elseif {$tag == "committer"} {
1614             set comdate [lindex $line end-1]
1615             set comname [join [lrange $line 1 end-2] " "]
1616         }
1617     }
1618     set headline {}
1619     # take the first non-blank line of the comment as the headline
1620     set headline [string trimleft $comment]
1621     set i [string first "\n" $headline]
1622     if {$i >= 0} {
1623         set headline [string range $headline 0 $i]
1624     }
1625     set headline [string trimright $headline]
1626     set i [string first "\r" $headline]
1627     if {$i >= 0} {
1628         set headline [string trimright [string range $headline 0 $i]]
1629     }
1630     if {!$listed} {
1631         # git log indents the comment by 4 spaces;
1632         # if we got this via git cat-file, add the indentation
1633         set newcomment {}
1634         foreach line [split $comment "\n"] {
1635             append newcomment "    "
1636             append newcomment $line
1637             append newcomment "\n"
1638         }
1639         set comment $newcomment
1640     }
1641     if {$comdate != {}} {
1642         set cdate($id) $comdate
1643     }
1644     set commitinfo($id) [list $headline $auname $audate \
1645                              $comname $comdate $comment]
1646 }
1647
1648 proc getcommit {id} {
1649     global commitdata commitinfo
1650
1651     if {[info exists commitdata($id)]} {
1652         parsecommit $id $commitdata($id) 1
1653     } else {
1654         readcommit $id
1655         if {![info exists commitinfo($id)]} {
1656             set commitinfo($id) [list [mc "No commit information available"]]
1657         }
1658     }
1659     return 1
1660 }
1661
1662 # Expand an abbreviated commit ID to a list of full 40-char IDs that match
1663 # and are present in the current view.
1664 # This is fairly slow...
1665 proc longid {prefix} {
1666     global varcid curview
1667
1668     set ids {}
1669     foreach match [array names varcid "$curview,$prefix*"] {
1670         lappend ids [lindex [split $match ","] 1]
1671     }
1672     return $ids
1673 }
1674
1675 proc readrefs {} {
1676     global tagids idtags headids idheads tagobjid
1677     global otherrefids idotherrefs mainhead mainheadid
1678     global selecthead selectheadid
1679
1680     foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
1681         catch {unset $v}
1682     }
1683     set refd [open [list | git show-ref -d] r]
1684     while {[gets $refd line] >= 0} {
1685         if {[string index $line 40] ne " "} continue
1686         set id [string range $line 0 39]
1687         set ref [string range $line 41 end]
1688         if {![string match "refs/*" $ref]} continue
1689         set name [string range $ref 5 end]
1690         if {[string match "remotes/*" $name]} {
1691             if {![string match "*/HEAD" $name]} {
1692                 set headids($name) $id
1693                 lappend idheads($id) $name
1694             }
1695         } elseif {[string match "heads/*" $name]} {
1696             set name [string range $name 6 end]
1697             set headids($name) $id
1698             lappend idheads($id) $name
1699         } elseif {[string match "tags/*" $name]} {
1700             # this lets refs/tags/foo^{} overwrite refs/tags/foo,
1701             # which is what we want since the former is the commit ID
1702             set name [string range $name 5 end]
1703             if {[string match "*^{}" $name]} {
1704                 set name [string range $name 0 end-3]
1705             } else {
1706                 set tagobjid($name) $id
1707             }
1708             set tagids($name) $id
1709             lappend idtags($id) $name
1710         } else {
1711             set otherrefids($name) $id
1712             lappend idotherrefs($id) $name
1713         }
1714     }
1715     catch {close $refd}
1716     set mainhead {}
1717     set mainheadid {}
1718     catch {
1719         set mainheadid [exec git rev-parse HEAD]
1720         set thehead [exec git symbolic-ref HEAD]
1721         if {[string match "refs/heads/*" $thehead]} {
1722             set mainhead [string range $thehead 11 end]
1723         }
1724     }
1725     set selectheadid {}
1726     if {$selecthead ne {}} {
1727         catch {
1728             set selectheadid [exec git rev-parse --verify $selecthead]
1729         }
1730     }
1731 }
1732
1733 # skip over fake commits
1734 proc first_real_row {} {
1735     global nullid nullid2 numcommits
1736
1737     for {set row 0} {$row < $numcommits} {incr row} {
1738         set id [commitonrow $row]
1739         if {$id ne $nullid && $id ne $nullid2} {
1740             break
1741         }
1742     }
1743     return $row
1744 }
1745
1746 # update things for a head moved to a child of its previous location
1747 proc movehead {id name} {
1748     global headids idheads
1749
1750     removehead $headids($name) $name
1751     set headids($name) $id
1752     lappend idheads($id) $name
1753 }
1754
1755 # update things when a head has been removed
1756 proc removehead {id name} {
1757     global headids idheads
1758
1759     if {$idheads($id) eq $name} {
1760         unset idheads($id)
1761     } else {
1762         set i [lsearch -exact $idheads($id) $name]
1763         if {$i >= 0} {
1764             set idheads($id) [lreplace $idheads($id) $i $i]
1765         }
1766     }
1767     unset headids($name)
1768 }
1769
1770 proc make_transient {window origin} {
1771     global have_tk85
1772
1773     # In MacOS Tk 8.4 transient appears to work by setting
1774     # overrideredirect, which is utterly useless, since the
1775     # windows get no border, and are not even kept above
1776     # the parent.
1777     if {!$have_tk85 && [tk windowingsystem] eq {aqua}} return
1778
1779     wm transient $window $origin
1780
1781     # Windows fails to place transient windows normally, so
1782     # schedule a callback to center them on the parent.
1783     if {[tk windowingsystem] eq {win32}} {
1784         after idle [list tk::PlaceWindow $window widget $origin]
1785     }
1786 }
1787
1788 proc show_error {w top msg} {
1789     message $w.m -text $msg -justify center -aspect 400
1790     pack $w.m -side top -fill x -padx 20 -pady 20
1791     button $w.ok -text [mc OK] -command "destroy $top"
1792     pack $w.ok -side bottom -fill x
1793     bind $top <Visibility> "grab $top; focus $top"
1794     bind $top <Key-Return> "destroy $top"
1795     bind $top <Key-space>  "destroy $top"
1796     bind $top <Key-Escape> "destroy $top"
1797     tkwait window $top
1798 }
1799
1800 proc error_popup {msg {owner .}} {
1801     set w .error
1802     toplevel $w
1803     make_transient $w $owner
1804     show_error $w $w $msg
1805 }
1806
1807 proc confirm_popup {msg {owner .}} {
1808     global confirm_ok
1809     set confirm_ok 0
1810     set w .confirm
1811     toplevel $w
1812     make_transient $w $owner
1813     message $w.m -text $msg -justify center -aspect 400
1814     pack $w.m -side top -fill x -padx 20 -pady 20
1815     button $w.ok -text [mc OK] -command "set confirm_ok 1; destroy $w"
1816     pack $w.ok -side left -fill x
1817     button $w.cancel -text [mc Cancel] -command "destroy $w"
1818     pack $w.cancel -side right -fill x
1819     bind $w <Visibility> "grab $w; focus $w"
1820     bind $w <Key-Return> "set confirm_ok 1; destroy $w"
1821     bind $w <Key-space>  "set confirm_ok 1; destroy $w"
1822     bind $w <Key-Escape> "destroy $w"
1823     tkwait window $w
1824     return $confirm_ok
1825 }
1826
1827 proc setoptions {} {
1828     option add *Panedwindow.showHandle 1 startupFile
1829     option add *Panedwindow.sashRelief raised startupFile
1830     option add *Button.font uifont startupFile
1831     option add *Checkbutton.font uifont startupFile
1832     option add *Radiobutton.font uifont startupFile
1833     if {[tk windowingsystem] ne "aqua"} {
1834         option add *Menu.font uifont startupFile
1835     }
1836     option add *Menubutton.font uifont startupFile
1837     option add *Label.font uifont startupFile
1838     option add *Message.font uifont startupFile
1839     option add *Entry.font uifont startupFile
1840 }
1841
1842 # Make a menu and submenus.
1843 # m is the window name for the menu, items is the list of menu items to add.
1844 # Each item is a list {mc label type description options...}
1845 # mc is ignored; it's so we can put mc there to alert xgettext
1846 # label is the string that appears in the menu
1847 # type is cascade, command or radiobutton (should add checkbutton)
1848 # description depends on type; it's the sublist for cascade, the
1849 # command to invoke for command, or {variable value} for radiobutton
1850 proc makemenu {m items} {
1851     menu $m
1852     if {[tk windowingsystem] eq {aqua}} {
1853         set Meta1 Cmd
1854     } else {
1855         set Meta1 Ctrl
1856     }
1857     foreach i $items {
1858         set name [mc [lindex $i 1]]
1859         set type [lindex $i 2]
1860         set thing [lindex $i 3]
1861         set params [list $type]
1862         if {$name ne {}} {
1863             set u [string first "&" [string map {&& x} $name]]
1864             lappend params -label [string map {&& & & {}} $name]
1865             if {$u >= 0} {
1866                 lappend params -underline $u
1867             }
1868         }
1869         switch -- $type {
1870             "cascade" {
1871                 set submenu [string tolower [string map {& ""} [lindex $i 1]]]
1872                 lappend params -menu $m.$submenu
1873             }
1874             "command" {
1875                 lappend params -command $thing
1876             }
1877             "radiobutton" {
1878                 lappend params -variable [lindex $thing 0] \
1879                     -value [lindex $thing 1]
1880             }
1881         }
1882         set tail [lrange $i 4 end]
1883         regsub -all {\yMeta1\y} $tail $Meta1 tail
1884         eval $m add $params $tail
1885         if {$type eq "cascade"} {
1886             makemenu $m.$submenu $thing
1887         }
1888     }
1889 }
1890
1891 # translate string and remove ampersands
1892 proc mca {str} {
1893     return [string map {&& & & {}} [mc $str]]
1894 }
1895
1896 proc makewindow {} {
1897     global canv canv2 canv3 linespc charspc ctext cflist cscroll
1898     global tabstop
1899     global findtype findtypemenu findloc findstring fstring geometry
1900     global entries sha1entry sha1string sha1but
1901     global diffcontextstring diffcontext
1902     global ignorespace
1903     global maincursor textcursor curtextcursor
1904     global rowctxmenu fakerowmenu mergemax wrapcomment
1905     global highlight_files gdttype
1906     global searchstring sstring
1907     global bgcolor fgcolor bglist fglist diffcolors selectbgcolor
1908     global headctxmenu progresscanv progressitem progresscoords statusw
1909     global fprogitem fprogcoord lastprogupdate progupdatepending
1910     global rprogitem rprogcoord rownumsel numcommits
1911     global have_tk85
1912
1913     # The "mc" arguments here are purely so that xgettext
1914     # sees the following string as needing to be translated
1915     set file {
1916         mc "File" cascade {
1917             {mc "Update" command updatecommits -accelerator F5}
1918             {mc "Reload" command reloadcommits -accelerator Meta1-F5}
1919             {mc "Reread references" command rereadrefs}
1920             {mc "List references" command showrefs -accelerator F2}
1921             {xx "" separator}
1922             {mc "Start git gui" command {exec git gui &}}
1923             {xx "" separator}
1924             {mc "Quit" command doquit -accelerator Meta1-Q}
1925         }}
1926     set edit {
1927         mc "Edit" cascade {
1928             {mc "Preferences" command doprefs}
1929         }}
1930     set view {
1931         mc "View" cascade {
1932             {mc "New view..." command {newview 0} -accelerator Shift-F4}
1933             {mc "Edit view..." command editview -state disabled -accelerator F4}
1934             {mc "Delete view" command delview -state disabled}
1935             {xx "" separator}
1936             {mc "All files" radiobutton {selectedview 0} -command {showview 0}}
1937         }}
1938     if {[tk windowingsystem] ne "aqua"} {
1939         set help {
1940         mc "Help" cascade {
1941             {mc "About gitk" command about}
1942             {mc "Key bindings" command keys}
1943         }}
1944         set bar [list $file $edit $view $help]
1945     } else {
1946         proc ::tk::mac::ShowPreferences {} {doprefs}
1947         proc ::tk::mac::Quit {} {doquit}
1948         lset file end [lreplace [lindex $file end] end-1 end]
1949         set apple {
1950         xx "Apple" cascade {
1951             {mc "About gitk" command about}
1952             {xx "" separator}
1953         }}
1954         set help {
1955         mc "Help" cascade {
1956             {mc "Key bindings" command keys}
1957         }}
1958         set bar [list $apple $file $view $help]
1959     }
1960     makemenu .bar $bar
1961     . configure -menu .bar
1962
1963     # the gui has upper and lower half, parts of a paned window.
1964     panedwindow .ctop -orient vertical
1965
1966     # possibly use assumed geometry
1967     if {![info exists geometry(pwsash0)]} {
1968         set geometry(topheight) [expr {15 * $linespc}]
1969         set geometry(topwidth) [expr {80 * $charspc}]
1970         set geometry(botheight) [expr {15 * $linespc}]
1971         set geometry(botwidth) [expr {50 * $charspc}]
1972         set geometry(pwsash0) "[expr {40 * $charspc}] 2"
1973         set geometry(pwsash1) "[expr {60 * $charspc}] 2"
1974     }
1975
1976     # the upper half will have a paned window, a scroll bar to the right, and some stuff below
1977     frame .tf -height $geometry(topheight) -width $geometry(topwidth)
1978     frame .tf.histframe
1979     panedwindow .tf.histframe.pwclist -orient horizontal -sashpad 0 -handlesize 4
1980
1981     # create three canvases
1982     set cscroll .tf.histframe.csb
1983     set canv .tf.histframe.pwclist.canv
1984     canvas $canv \
1985         -selectbackground $selectbgcolor \
1986         -background $bgcolor -bd 0 \
1987         -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll"
1988     .tf.histframe.pwclist add $canv
1989     set canv2 .tf.histframe.pwclist.canv2
1990     canvas $canv2 \
1991         -selectbackground $selectbgcolor \
1992         -background $bgcolor -bd 0 -yscrollincr $linespc
1993     .tf.histframe.pwclist add $canv2
1994     set canv3 .tf.histframe.pwclist.canv3
1995     canvas $canv3 \
1996         -selectbackground $selectbgcolor \
1997         -background $bgcolor -bd 0 -yscrollincr $linespc
1998     .tf.histframe.pwclist add $canv3
1999     eval .tf.histframe.pwclist sash place 0 $geometry(pwsash0)
2000     eval .tf.histframe.pwclist sash place 1 $geometry(pwsash1)
2001
2002     # a scroll bar to rule them
2003     scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
2004     pack $cscroll -side right -fill y
2005     bind .tf.histframe.pwclist <Configure> {resizeclistpanes %W %w}
2006     lappend bglist $canv $canv2 $canv3
2007     pack .tf.histframe.pwclist -fill both -expand 1 -side left
2008
2009     # we have two button bars at bottom of top frame. Bar 1
2010     frame .tf.bar
2011     frame .tf.lbar -height 15
2012
2013     set sha1entry .tf.bar.sha1
2014     set entries $sha1entry
2015     set sha1but .tf.bar.sha1label
2016     button $sha1but -text [mc "SHA1 ID: "] -state disabled -relief flat \
2017         -command gotocommit -width 8
2018     $sha1but conf -disabledforeground [$sha1but cget -foreground]
2019     pack .tf.bar.sha1label -side left
2020     entry $sha1entry -width 40 -font textfont -textvariable sha1string
2021     trace add variable sha1string write sha1change
2022     pack $sha1entry -side left -pady 2
2023
2024     image create bitmap bm-left -data {
2025         #define left_width 16
2026         #define left_height 16
2027         static unsigned char left_bits[] = {
2028         0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
2029         0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
2030         0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
2031     }
2032     image create bitmap bm-right -data {
2033         #define right_width 16
2034         #define right_height 16
2035         static unsigned char right_bits[] = {
2036         0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
2037         0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
2038         0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
2039     }
2040     button .tf.bar.leftbut -image bm-left -command goback \
2041         -state disabled -width 26
2042     pack .tf.bar.leftbut -side left -fill y
2043     button .tf.bar.rightbut -image bm-right -command goforw \
2044         -state disabled -width 26
2045     pack .tf.bar.rightbut -side left -fill y
2046
2047     label .tf.bar.rowlabel -text [mc "Row"]
2048     set rownumsel {}
2049     label .tf.bar.rownum -width 7 -font textfont -textvariable rownumsel \
2050         -relief sunken -anchor e
2051     label .tf.bar.rowlabel2 -text "/"
2052     label .tf.bar.numcommits -width 7 -font textfont -textvariable numcommits \
2053         -relief sunken -anchor e
2054     pack .tf.bar.rowlabel .tf.bar.rownum .tf.bar.rowlabel2 .tf.bar.numcommits \
2055         -side left
2056     global selectedline
2057     trace add variable selectedline write selectedline_change
2058
2059     # Status label and progress bar
2060     set statusw .tf.bar.status
2061     label $statusw -width 15 -relief sunken
2062     pack $statusw -side left -padx 5
2063     set h [expr {[font metrics uifont -linespace] + 2}]
2064     set progresscanv .tf.bar.progress
2065     canvas $progresscanv -relief sunken -height $h -borderwidth 2
2066     set progressitem [$progresscanv create rect -1 0 0 $h -fill green]
2067     set fprogitem [$progresscanv create rect -1 0 0 $h -fill yellow]
2068     set rprogitem [$progresscanv create rect -1 0 0 $h -fill red]
2069     pack $progresscanv -side right -expand 1 -fill x
2070     set progresscoords {0 0}
2071     set fprogcoord 0
2072     set rprogcoord 0
2073     bind $progresscanv <Configure> adjustprogress
2074     set lastprogupdate [clock clicks -milliseconds]
2075     set progupdatepending 0
2076
2077     # build up the bottom bar of upper window
2078     label .tf.lbar.flabel -text "[mc "Find"] "
2079     button .tf.lbar.fnext -text [mc "next"] -command {dofind 1 1}
2080     button .tf.lbar.fprev -text [mc "prev"] -command {dofind -1 1}
2081     label .tf.lbar.flab2 -text " [mc "commit"] "
2082     pack .tf.lbar.flabel .tf.lbar.fnext .tf.lbar.fprev .tf.lbar.flab2 \
2083         -side left -fill y
2084     set gdttype [mc "containing:"]
2085     set gm [tk_optionMenu .tf.lbar.gdttype gdttype \
2086                 [mc "containing:"] \
2087                 [mc "touching paths:"] \
2088                 [mc "adding/removing string:"]]
2089     trace add variable gdttype write gdttype_change
2090     pack .tf.lbar.gdttype -side left -fill y
2091
2092     set findstring {}
2093     set fstring .tf.lbar.findstring
2094     lappend entries $fstring
2095     entry $fstring -width 30 -font textfont -textvariable findstring
2096     trace add variable findstring write find_change
2097     set findtype [mc "Exact"]
2098     set findtypemenu [tk_optionMenu .tf.lbar.findtype \
2099                       findtype [mc "Exact"] [mc "IgnCase"] [mc "Regexp"]]
2100     trace add variable findtype write findcom_change
2101     set findloc [mc "All fields"]
2102     tk_optionMenu .tf.lbar.findloc findloc [mc "All fields"] [mc "Headline"] \
2103         [mc "Comments"] [mc "Author"] [mc "Committer"]
2104     trace add variable findloc write find_change
2105     pack .tf.lbar.findloc -side right
2106     pack .tf.lbar.findtype -side right
2107     pack $fstring -side left -expand 1 -fill x
2108
2109     # Finish putting the upper half of the viewer together
2110     pack .tf.lbar -in .tf -side bottom -fill x
2111     pack .tf.bar -in .tf -side bottom -fill x
2112     pack .tf.histframe -fill both -side top -expand 1
2113     .ctop add .tf
2114     .ctop paneconfigure .tf -height $geometry(topheight)
2115     .ctop paneconfigure .tf -width $geometry(topwidth)
2116
2117     # now build up the bottom
2118     panedwindow .pwbottom -orient horizontal
2119
2120     # lower left, a text box over search bar, scroll bar to the right
2121     # if we know window height, then that will set the lower text height, otherwise
2122     # we set lower text height which will drive window height
2123     if {[info exists geometry(main)]} {
2124         frame .bleft -width $geometry(botwidth)
2125     } else {
2126         frame .bleft -width $geometry(botwidth) -height $geometry(botheight)
2127     }
2128     frame .bleft.top
2129     frame .bleft.mid
2130     frame .bleft.bottom
2131
2132     button .bleft.top.search -text [mc "Search"] -command dosearch
2133     pack .bleft.top.search -side left -padx 5
2134     set sstring .bleft.top.sstring
2135     entry $sstring -width 20 -font textfont -textvariable searchstring
2136     lappend entries $sstring
2137     trace add variable searchstring write incrsearch
2138     pack $sstring -side left -expand 1 -fill x
2139     radiobutton .bleft.mid.diff -text [mc "Diff"] \
2140         -command changediffdisp -variable diffelide -value {0 0}
2141     radiobutton .bleft.mid.old -text [mc "Old version"] \
2142         -command changediffdisp -variable diffelide -value {0 1}
2143     radiobutton .bleft.mid.new -text [mc "New version"] \
2144         -command changediffdisp -variable diffelide -value {1 0}
2145     label .bleft.mid.labeldiffcontext -text "      [mc "Lines of context"]: "
2146     pack .bleft.mid.diff .bleft.mid.old .bleft.mid.new -side left
2147     spinbox .bleft.mid.diffcontext -width 5 -font textfont \
2148         -from 1 -increment 1 -to 10000000 \
2149         -validate all -validatecommand "diffcontextvalidate %P" \
2150         -textvariable diffcontextstring
2151     .bleft.mid.diffcontext set $diffcontext
2152     trace add variable diffcontextstring write diffcontextchange
2153     lappend entries .bleft.mid.diffcontext
2154     pack .bleft.mid.labeldiffcontext .bleft.mid.diffcontext -side left
2155     checkbutton .bleft.mid.ignspace -text [mc "Ignore space change"] \
2156         -command changeignorespace -variable ignorespace
2157     pack .bleft.mid.ignspace -side left -padx 5
2158     set ctext .bleft.bottom.ctext
2159     text $ctext -background $bgcolor -foreground $fgcolor \
2160         -state disabled -font textfont \
2161         -yscrollcommand scrolltext -wrap none \
2162         -xscrollcommand ".bleft.bottom.sbhorizontal set"
2163     if {$have_tk85} {
2164         $ctext conf -tabstyle wordprocessor
2165     }
2166     scrollbar .bleft.bottom.sb -command "$ctext yview"
2167     scrollbar .bleft.bottom.sbhorizontal -command "$ctext xview" -orient h \
2168         -width 10
2169     pack .bleft.top -side top -fill x
2170     pack .bleft.mid -side top -fill x
2171     grid $ctext .bleft.bottom.sb -sticky nsew
2172     grid .bleft.bottom.sbhorizontal -sticky ew
2173     grid columnconfigure .bleft.bottom 0 -weight 1
2174     grid rowconfigure .bleft.bottom 0 -weight 1
2175     grid rowconfigure .bleft.bottom 1 -weight 0
2176     pack .bleft.bottom -side top -fill both -expand 1
2177     lappend bglist $ctext
2178     lappend fglist $ctext
2179
2180     $ctext tag conf comment -wrap $wrapcomment
2181     $ctext tag conf filesep -font textfontbold -back "#aaaaaa"
2182     $ctext tag conf hunksep -fore [lindex $diffcolors 2]
2183     $ctext tag conf d0 -fore [lindex $diffcolors 0]
2184     $ctext tag conf dresult -fore [lindex $diffcolors 1]
2185     $ctext tag conf m0 -fore red
2186     $ctext tag conf m1 -fore blue
2187     $ctext tag conf m2 -fore green
2188     $ctext tag conf m3 -fore purple
2189     $ctext tag conf m4 -fore brown
2190     $ctext tag conf m5 -fore "#009090"
2191     $ctext tag conf m6 -fore magenta
2192     $ctext tag conf m7 -fore "#808000"
2193     $ctext tag conf m8 -fore "#009000"
2194     $ctext tag conf m9 -fore "#ff0080"
2195     $ctext tag conf m10 -fore cyan
2196     $ctext tag conf m11 -fore "#b07070"
2197     $ctext tag conf m12 -fore "#70b0f0"
2198     $ctext tag conf m13 -fore "#70f0b0"
2199     $ctext tag conf m14 -fore "#f0b070"
2200     $ctext tag conf m15 -fore "#ff70b0"
2201     $ctext tag conf mmax -fore darkgrey
2202     set mergemax 16
2203     $ctext tag conf mresult -font textfontbold
2204     $ctext tag conf msep -font textfontbold
2205     $ctext tag conf found -back yellow
2206
2207     .pwbottom add .bleft
2208     .pwbottom paneconfigure .bleft -width $geometry(botwidth)
2209
2210     # lower right
2211     frame .bright
2212     frame .bright.mode
2213     radiobutton .bright.mode.patch -text [mc "Patch"] \
2214         -command reselectline -variable cmitmode -value "patch"
2215     radiobutton .bright.mode.tree -text [mc "Tree"] \
2216         -command reselectline -variable cmitmode -value "tree"
2217     grid .bright.mode.patch .bright.mode.tree -sticky ew
2218     pack .bright.mode -side top -fill x
2219     set cflist .bright.cfiles
2220     set indent [font measure mainfont "nn"]
2221     text $cflist \
2222         -selectbackground $selectbgcolor \
2223         -background $bgcolor -foreground $fgcolor \
2224         -font mainfont \
2225         -tabs [list $indent [expr {2 * $indent}]] \
2226         -yscrollcommand ".bright.sb set" \
2227         -cursor [. cget -cursor] \
2228         -spacing1 1 -spacing3 1
2229     lappend bglist $cflist
2230     lappend fglist $cflist
2231     scrollbar .bright.sb -command "$cflist yview"
2232     pack .bright.sb -side right -fill y
2233     pack $cflist -side left -fill both -expand 1
2234     $cflist tag configure highlight \
2235         -background [$cflist cget -selectbackground]
2236     $cflist tag configure bold -font mainfontbold
2237
2238     .pwbottom add .bright
2239     .ctop add .pwbottom
2240
2241     # restore window width & height if known
2242     if {[info exists geometry(main)]} {
2243         if {[scan $geometry(main) "%dx%d" w h] >= 2} {
2244             if {$w > [winfo screenwidth .]} {
2245                 set w [winfo screenwidth .]
2246             }
2247             if {$h > [winfo screenheight .]} {
2248                 set h [winfo screenheight .]
2249             }
2250             wm geometry . "${w}x$h"
2251         }
2252     }
2253
2254     if {[tk windowingsystem] eq {aqua}} {
2255         set M1B M1
2256         set ::BM "3"
2257     } else {
2258         set M1B Control
2259         set ::BM "2"
2260     }
2261
2262     bind .pwbottom <Configure> {resizecdetpanes %W %w}
2263     pack .ctop -fill both -expand 1
2264     bindall <1> {selcanvline %W %x %y}
2265     #bindall <B1-Motion> {selcanvline %W %x %y}
2266     if {[tk windowingsystem] == "win32"} {
2267         bind . <MouseWheel> { windows_mousewheel_redirector %W %X %Y %D }
2268         bind $ctext <MouseWheel> { windows_mousewheel_redirector %W %X %Y %D ; break }
2269     } else {
2270         bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
2271         bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
2272         if {[tk windowingsystem] eq "aqua"} {
2273             bindall <MouseWheel> {
2274                 set delta [expr {- (%D)}]
2275                 allcanvs yview scroll $delta units
2276             }
2277             bindall <Shift-MouseWheel> {
2278                 set delta [expr {- (%D)}]
2279                 $canv xview scroll $delta units
2280             }
2281         }
2282     }
2283     bindall <$::BM> "canvscan mark %W %x %y"
2284     bindall <B$::BM-Motion> "canvscan dragto %W %x %y"
2285     bindkey <Home> selfirstline
2286     bindkey <End> sellastline
2287     bind . <Key-Up> "selnextline -1"
2288     bind . <Key-Down> "selnextline 1"
2289     bind . <Shift-Key-Up> "dofind -1 0"
2290     bind . <Shift-Key-Down> "dofind 1 0"
2291     bindkey <Key-Right> "goforw"
2292     bindkey <Key-Left> "goback"
2293     bind . <Key-Prior> "selnextpage -1"
2294     bind . <Key-Next> "selnextpage 1"
2295     bind . <$M1B-Home> "allcanvs yview moveto 0.0"
2296     bind . <$M1B-End> "allcanvs yview moveto 1.0"
2297     bind . <$M1B-Key-Up> "allcanvs yview scroll -1 units"
2298     bind . <$M1B-Key-Down> "allcanvs yview scroll 1 units"
2299     bind . <$M1B-Key-Prior> "allcanvs yview scroll -1 pages"
2300     bind . <$M1B-Key-Next> "allcanvs yview scroll 1 pages"
2301     bindkey <Key-Delete> "$ctext yview scroll -1 pages"
2302     bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
2303     bindkey <Key-space> "$ctext yview scroll 1 pages"
2304     bindkey p "selnextline -1"
2305     bindkey n "selnextline 1"
2306     bindkey z "goback"
2307     bindkey x "goforw"
2308     bindkey i "selnextline -1"
2309     bindkey k "selnextline 1"
2310     bindkey j "goback"
2311     bindkey l "goforw"
2312     bindkey b prevfile
2313     bindkey d "$ctext yview scroll 18 units"
2314     bindkey u "$ctext yview scroll -18 units"
2315     bindkey / {focus $fstring}
2316     bindkey <Key-Return> {dofind 1 1}
2317     bindkey ? {dofind -1 1}
2318     bindkey f nextfile
2319     bind . <F5> updatecommits
2320     bind . <$M1B-F5> reloadcommits
2321     bind . <F2> showrefs
2322     bind . <Shift-F4> {newview 0}
2323     catch { bind . <Shift-Key-XF86_Switch_VT_4> {newview 0} }
2324     bind . <F4> edit_or_newview
2325     bind . <$M1B-q> doquit
2326     bind . <$M1B-f> {dofind 1 1}
2327     bind . <$M1B-g> {dofind 1 0}
2328     bind . <$M1B-r> dosearchback
2329     bind . <$M1B-s> dosearch
2330     bind . <$M1B-equal> {incrfont 1}
2331     bind . <$M1B-plus> {incrfont 1}
2332     bind . <$M1B-KP_Add> {incrfont 1}
2333     bind . <$M1B-minus> {incrfont -1}
2334     bind . <$M1B-KP_Subtract> {incrfont -1}
2335     wm protocol . WM_DELETE_WINDOW doquit
2336     bind . <Destroy> {stop_backends}
2337     bind . <Button-1> "click %W"
2338     bind $fstring <Key-Return> {dofind 1 1}
2339     bind $sha1entry <Key-Return> {gotocommit; break}
2340     bind $sha1entry <<PasteSelection>> clearsha1
2341     bind $cflist <1> {sel_flist %W %x %y; break}
2342     bind $cflist <B1-Motion> {sel_flist %W %x %y; break}
2343     bind $cflist <ButtonRelease-1> {treeclick %W %x %y}
2344     global ctxbut
2345     bind $cflist $ctxbut {pop_flist_menu %W %X %Y %x %y}
2346     bind $ctext $ctxbut {pop_diff_menu %W %X %Y %x %y}
2347
2348     set maincursor [. cget -cursor]
2349     set textcursor [$ctext cget -cursor]
2350     set curtextcursor $textcursor
2351
2352     set rowctxmenu .rowctxmenu
2353     makemenu $rowctxmenu {
2354         {mc "Diff this -> selected" command {diffvssel 0}}
2355         {mc "Diff selected -> this" command {diffvssel 1}}
2356         {mc "Make patch" command mkpatch}
2357         {mc "Create tag" command mktag}
2358         {mc "Write commit to file" command writecommit}
2359         {mc "Create new branch" command mkbranch}
2360         {mc "Cherry-pick this commit" command cherrypick}
2361         {mc "Reset HEAD branch to here" command resethead}
2362         {mc "Mark this commit" command markhere}
2363         {mc "Return to mark" command gotomark}
2364         {mc "Find descendant of this and mark" command find_common_desc}
2365     }
2366     $rowctxmenu configure -tearoff 0
2367
2368     set fakerowmenu .fakerowmenu
2369     makemenu $fakerowmenu {
2370         {mc "Diff this -> selected" command {diffvssel 0}}
2371         {mc "Diff selected -> this" command {diffvssel 1}}
2372         {mc "Make patch" command mkpatch}
2373     }
2374     $fakerowmenu configure -tearoff 0
2375
2376     set headctxmenu .headctxmenu
2377     makemenu $headctxmenu {
2378         {mc "Check out this branch" command cobranch}
2379         {mc "Remove this branch" command rmbranch}
2380     }
2381     $headctxmenu configure -tearoff 0
2382
2383     global flist_menu
2384     set flist_menu .flistctxmenu
2385     makemenu $flist_menu {
2386         {mc "Highlight this too" command {flist_hl 0}}
2387         {mc "Highlight this only" command {flist_hl 1}}
2388         {mc "External diff" command {external_diff}}
2389         {mc "Blame parent commit" command {external_blame 1}}
2390     }
2391     $flist_menu configure -tearoff 0
2392
2393     global diff_menu
2394     set diff_menu .diffctxmenu
2395     makemenu $diff_menu {
2396         {mc "Show origin of this line" command show_line_source}
2397         {mc "Run git gui blame on this line" command {external_blame_diff}}
2398     }
2399     $diff_menu configure -tearoff 0
2400 }
2401
2402 # Windows sends all mouse wheel events to the current focused window, not
2403 # the one where the mouse hovers, so bind those events here and redirect
2404 # to the correct window
2405 proc windows_mousewheel_redirector {W X Y D} {
2406     global canv canv2 canv3
2407     set w [winfo containing -displayof $W $X $Y]
2408     if {$w ne ""} {
2409         set u [expr {$D < 0 ? 5 : -5}]
2410         if {$w == $canv || $w == $canv2 || $w == $canv3} {
2411             allcanvs yview scroll $u units
2412         } else {
2413             catch {
2414                 $w yview scroll $u units
2415             }
2416         }
2417     }
2418 }
2419
2420 # Update row number label when selectedline changes
2421 proc selectedline_change {n1 n2 op} {
2422     global selectedline rownumsel
2423
2424     if {$selectedline eq {}} {
2425         set rownumsel {}
2426     } else {
2427         set rownumsel [expr {$selectedline + 1}]
2428     }
2429 }
2430
2431 # mouse-2 makes all windows scan vertically, but only the one
2432 # the cursor is in scans horizontally
2433 proc canvscan {op w x y} {
2434     global canv canv2 canv3
2435     foreach c [list $canv $canv2 $canv3] {
2436         if {$c == $w} {
2437             $c scan $op $x $y
2438         } else {
2439             $c scan $op 0 $y
2440         }
2441     }
2442 }
2443
2444 proc scrollcanv {cscroll f0 f1} {
2445     $cscroll set $f0 $f1
2446     drawvisible
2447     flushhighlights
2448 }
2449
2450 # when we make a key binding for the toplevel, make sure
2451 # it doesn't get triggered when that key is pressed in the
2452 # find string entry widget.
2453 proc bindkey {ev script} {
2454     global entries
2455     bind . $ev $script
2456     set escript [bind Entry $ev]
2457     if {$escript == {}} {
2458         set escript [bind Entry <Key>]
2459     }
2460     foreach e $entries {
2461         bind $e $ev "$escript; break"
2462     }
2463 }
2464
2465 # set the focus back to the toplevel for any click outside
2466 # the entry widgets
2467 proc click {w} {
2468     global ctext entries
2469     foreach e [concat $entries $ctext] {
2470         if {$w == $e} return
2471     }
2472     focus .
2473 }
2474
2475 # Adjust the progress bar for a change in requested extent or canvas size
2476 proc adjustprogress {} {
2477     global progresscanv progressitem progresscoords
2478     global fprogitem fprogcoord lastprogupdate progupdatepending
2479     global rprogitem rprogcoord
2480
2481     set w [expr {[winfo width $progresscanv] - 4}]
2482     set x0 [expr {$w * [lindex $progresscoords 0]}]
2483     set x1 [expr {$w * [lindex $progresscoords 1]}]
2484     set h [winfo height $progresscanv]
2485     $progresscanv coords $progressitem $x0 0 $x1 $h
2486     $progresscanv coords $fprogitem 0 0 [expr {$w * $fprogcoord}] $h
2487     $progresscanv coords $rprogitem 0 0 [expr {$w * $rprogcoord}] $h
2488     set now [clock clicks -milliseconds]
2489     if {$now >= $lastprogupdate + 100} {
2490         set progupdatepending 0
2491         update
2492     } elseif {!$progupdatepending} {
2493         set progupdatepending 1
2494         after [expr {$lastprogupdate + 100 - $now}] doprogupdate
2495     }
2496 }
2497
2498 proc doprogupdate {} {
2499     global lastprogupdate progupdatepending
2500
2501     if {$progupdatepending} {
2502         set progupdatepending 0
2503         set lastprogupdate [clock clicks -milliseconds]
2504         update
2505     }
2506 }
2507
2508 proc savestuff {w} {
2509     global canv canv2 canv3 mainfont textfont uifont tabstop
2510     global stuffsaved findmergefiles maxgraphpct
2511     global maxwidth showneartags showlocalchanges
2512     global viewname viewfiles viewargs viewargscmd viewperm nextviewnum
2513     global cmitmode wrapcomment datetimeformat limitdiffs
2514     global colors bgcolor fgcolor diffcolors diffcontext selectbgcolor
2515     global autoselect extdifftool perfile_attrs markbgcolor
2516
2517     if {$stuffsaved} return
2518     if {![winfo viewable .]} return
2519     catch {
2520         set f [open "~/.gitk-new" w]
2521         puts $f [list set mainfont $mainfont]
2522         puts $f [list set textfont $textfont]
2523         puts $f [list set uifont $uifont]
2524         puts $f [list set tabstop $tabstop]
2525         puts $f [list set findmergefiles $findmergefiles]
2526         puts $f [list set maxgraphpct $maxgraphpct]
2527         puts $f [list set maxwidth $maxwidth]
2528         puts $f [list set cmitmode $cmitmode]
2529         puts $f [list set wrapcomment $wrapcomment]
2530         puts $f [list set autoselect $autoselect]
2531         puts $f [list set showneartags $showneartags]
2532         puts $f [list set showlocalchanges $showlocalchanges]
2533         puts $f [list set datetimeformat $datetimeformat]
2534         puts $f [list set limitdiffs $limitdiffs]
2535         puts $f [list set bgcolor $bgcolor]
2536         puts $f [list set fgcolor $fgcolor]
2537         puts $f [list set colors $colors]
2538         puts $f [list set diffcolors $diffcolors]
2539         puts $f [list set markbgcolor $markbgcolor]
2540         puts $f [list set diffcontext $diffcontext]
2541         puts $f [list set selectbgcolor $selectbgcolor]
2542         puts $f [list set extdifftool $extdifftool]
2543         puts $f [list set perfile_attrs $perfile_attrs]
2544
2545         puts $f "set geometry(main) [wm geometry .]"
2546         puts $f "set geometry(topwidth) [winfo width .tf]"
2547         puts $f "set geometry(topheight) [winfo height .tf]"
2548         puts $f "set geometry(pwsash0) \"[.tf.histframe.pwclist sash coord 0]\""
2549         puts $f "set geometry(pwsash1) \"[.tf.histframe.pwclist sash coord 1]\""
2550         puts $f "set geometry(botwidth) [winfo width .bleft]"
2551         puts $f "set geometry(botheight) [winfo height .bleft]"
2552
2553         puts -nonewline $f "set permviews {"
2554         for {set v 0} {$v < $nextviewnum} {incr v} {
2555             if {$viewperm($v)} {
2556                 puts $f "{[list $viewname($v) $viewfiles($v) $viewargs($v) $viewargscmd($v)]}"
2557             }
2558         }
2559         puts $f "}"
2560         close $f
2561         file rename -force "~/.gitk-new" "~/.gitk"
2562     }
2563     set stuffsaved 1
2564 }
2565
2566 proc resizeclistpanes {win w} {
2567     global oldwidth
2568     if {[info exists oldwidth($win)]} {
2569         set s0 [$win sash coord 0]
2570         set s1 [$win sash coord 1]
2571         if {$w < 60} {
2572             set sash0 [expr {int($w/2 - 2)}]
2573             set sash1 [expr {int($w*5/6 - 2)}]
2574         } else {
2575             set factor [expr {1.0 * $w / $oldwidth($win)}]
2576             set sash0 [expr {int($factor * [lindex $s0 0])}]
2577             set sash1 [expr {int($factor * [lindex $s1 0])}]
2578             if {$sash0 < 30} {
2579                 set sash0 30
2580             }
2581             if {$sash1 < $sash0 + 20} {
2582                 set sash1 [expr {$sash0 + 20}]
2583             }
2584             if {$sash1 > $w - 10} {
2585                 set sash1 [expr {$w - 10}]
2586                 if {$sash0 > $sash1 - 20} {
2587                     set sash0 [expr {$sash1 - 20}]
2588                 }
2589             }
2590         }
2591         $win sash place 0 $sash0 [lindex $s0 1]
2592         $win sash place 1 $sash1 [lindex $s1 1]
2593     }
2594     set oldwidth($win) $w
2595 }
2596
2597 proc resizecdetpanes {win w} {
2598     global oldwidth
2599     if {[info exists oldwidth($win)]} {
2600         set s0 [$win sash coord 0]
2601         if {$w < 60} {
2602             set sash0 [expr {int($w*3/4 - 2)}]
2603         } else {
2604             set factor [expr {1.0 * $w / $oldwidth($win)}]
2605             set sash0 [expr {int($factor * [lindex $s0 0])}]
2606             if {$sash0 < 45} {
2607                 set sash0 45
2608             }
2609             if {$sash0 > $w - 15} {
2610                 set sash0 [expr {$w - 15}]
2611             }
2612         }
2613         $win sash place 0 $sash0 [lindex $s0 1]
2614     }
2615     set oldwidth($win) $w
2616 }
2617
2618 proc allcanvs args {
2619     global canv canv2 canv3
2620     eval $canv $args
2621     eval $canv2 $args
2622     eval $canv3 $args
2623 }
2624
2625 proc bindall {event action} {
2626     global canv canv2 canv3
2627     bind $canv $event $action
2628     bind $canv2 $event $action
2629     bind $canv3 $event $action
2630 }
2631
2632 proc about {} {
2633     global uifont
2634     set w .about
2635     if {[winfo exists $w]} {
2636         raise $w
2637         return
2638     }
2639     toplevel $w
2640     wm title $w [mc "About gitk"]
2641     make_transient $w .
2642     message $w.m -text [mc "
2643 Gitk - a commit viewer for git
2644
2645 Copyright © 2005-2008 Paul Mackerras
2646
2647 Use and redistribute under the terms of the GNU General Public License"] \
2648             -justify center -aspect 400 -border 2 -bg white -relief groove
2649     pack $w.m -side top -fill x -padx 2 -pady 2
2650     button $w.ok -text [mc "Close"] -command "destroy $w" -default active
2651     pack $w.ok -side bottom
2652     bind $w <Visibility> "focus $w.ok"
2653     bind $w <Key-Escape> "destroy $w"
2654     bind $w <Key-Return> "destroy $w"
2655 }
2656
2657 proc keys {} {
2658     set w .keys
2659     if {[winfo exists $w]} {
2660         raise $w
2661         return
2662     }
2663     if {[tk windowingsystem] eq {aqua}} {
2664         set M1T Cmd
2665     } else {
2666         set M1T Ctrl
2667     }
2668     toplevel $w
2669     wm title $w [mc "Gitk key bindings"]
2670     make_transient $w .
2671     message $w.m -text "
2672 [mc "Gitk key bindings:"]
2673
2674 [mc "<%s-Q>             Quit" $M1T]
2675 [mc "<Home>             Move to first commit"]
2676 [mc "<End>              Move to last commit"]
2677 [mc "<Up>, p, i Move up one commit"]
2678 [mc "<Down>, n, k       Move down one commit"]
2679 [mc "<Left>, z, j       Go back in history list"]
2680 [mc "<Right>, x, l      Go forward in history list"]
2681 [mc "<PageUp>   Move up one page in commit list"]
2682 [mc "<PageDown> Move down one page in commit list"]
2683 [mc "<%s-Home>  Scroll to top of commit list" $M1T]
2684 [mc "<%s-End>   Scroll to bottom of commit list" $M1T]
2685 [mc "<%s-Up>    Scroll commit list up one line" $M1T]
2686 [mc "<%s-Down>  Scroll commit list down one line" $M1T]
2687 [mc "<%s-PageUp>        Scroll commit list up one page" $M1T]
2688 [mc "<%s-PageDown>      Scroll commit list down one page" $M1T]
2689 [mc "<Shift-Up> Find backwards (upwards, later commits)"]
2690 [mc "<Shift-Down>       Find forwards (downwards, earlier commits)"]
2691 [mc "<Delete>, b        Scroll diff view up one page"]
2692 [mc "<Backspace>        Scroll diff view up one page"]
2693 [mc "<Space>            Scroll diff view down one page"]
2694 [mc "u          Scroll diff view up 18 lines"]
2695 [mc "d          Scroll diff view down 18 lines"]
2696 [mc "<%s-F>             Find" $M1T]
2697 [mc "<%s-G>             Move to next find hit" $M1T]
2698 [mc "<Return>   Move to next find hit"]
2699 [mc "/          Focus the search box"]
2700 [mc "?          Move to previous find hit"]
2701 [mc "f          Scroll diff view to next file"]
2702 [mc "<%s-S>             Search for next hit in diff view" $M1T]
2703 [mc "<%s-R>             Search for previous hit in diff view" $M1T]
2704 [mc "<%s-KP+>   Increase font size" $M1T]
2705 [mc "<%s-plus>  Increase font size" $M1T]
2706 [mc "<%s-KP->   Decrease font size" $M1T]
2707 [mc "<%s-minus> Decrease font size" $M1T]
2708 [mc "<F5>               Update"]
2709 " \
2710             -justify left -bg white -border 2 -relief groove
2711     pack $w.m -side top -fill both -padx 2 -pady 2
2712     button $w.ok -text [mc "Close"] -command "destroy $w" -default active
2713     bind $w <Key-Escape> [list destroy $w]
2714     pack $w.ok -side bottom
2715     bind $w <Visibility> "focus $w.ok"
2716     bind $w <Key-Escape> "destroy $w"
2717     bind $w <Key-Return> "destroy $w"
2718 }
2719
2720 # Procedures for manipulating the file list window at the
2721 # bottom right of the overall window.
2722
2723 proc treeview {w l openlevs} {
2724     global treecontents treediropen treeheight treeparent treeindex
2725
2726     set ix 0
2727     set treeindex() 0
2728     set lev 0
2729     set prefix {}
2730     set prefixend -1
2731     set prefendstack {}
2732     set htstack {}
2733     set ht 0
2734     set treecontents() {}
2735     $w conf -state normal
2736     foreach f $l {
2737         while {[string range $f 0 $prefixend] ne $prefix} {
2738             if {$lev <= $openlevs} {
2739                 $w mark set e:$treeindex($prefix) "end -1c"
2740                 $w mark gravity e:$treeindex($prefix) left
2741             }
2742             set treeheight($prefix) $ht
2743             incr ht [lindex $htstack end]
2744             set htstack [lreplace $htstack end end]
2745             set prefixend [lindex $prefendstack end]
2746             set prefendstack [lreplace $prefendstack end end]
2747             set prefix [string range $prefix 0 $prefixend]
2748             incr lev -1
2749         }
2750         set tail [string range $f [expr {$prefixend+1}] end]
2751         while {[set slash [string first "/" $tail]] >= 0} {
2752             lappend htstack $ht
2753             set ht 0
2754             lappend prefendstack $prefixend
2755             incr prefixend [expr {$slash + 1}]
2756             set d [string range $tail 0 $slash]
2757             lappend treecontents($prefix) $d
2758             set oldprefix $prefix
2759             append prefix $d
2760             set treecontents($prefix) {}
2761             set treeindex($prefix) [incr ix]
2762             set treeparent($prefix) $oldprefix
2763             set tail [string range $tail [expr {$slash+1}] end]
2764             if {$lev <= $openlevs} {
2765                 set ht 1
2766                 set treediropen($prefix) [expr {$lev < $openlevs}]
2767                 set bm [expr {$lev == $openlevs? "tri-rt": "tri-dn"}]
2768                 $w mark set d:$ix "end -1c"
2769                 $w mark gravity d:$ix left
2770                 set str "\n"
2771                 for {set i 0} {$i < $lev} {incr i} {append str "\t"}
2772                 $w insert end $str
2773                 $w image create end -align center -image $bm -padx 1 \
2774                     -name a:$ix
2775                 $w insert end $d [highlight_tag $prefix]
2776                 $w mark set s:$ix "end -1c"
2777                 $w mark gravity s:$ix left
2778             }
2779             incr lev
2780         }
2781         if {$tail ne {}} {
2782             if {$lev <= $openlevs} {
2783                 incr ht
2784                 set str "\n"
2785                 for {set i 0} {$i < $lev} {incr i} {append str "\t"}
2786                 $w insert end $str
2787                 $w insert end $tail [highlight_tag $f]
2788             }
2789             lappend treecontents($prefix) $tail
2790         }
2791     }
2792     while {$htstack ne {}} {
2793         set treeheight($prefix) $ht
2794         incr ht [lindex $htstack end]
2795         set htstack [lreplace $htstack end end]
2796         set prefixend [lindex $prefendstack end]
2797         set prefendstack [lreplace $prefendstack end end]
2798         set prefix [string range $prefix 0 $prefixend]
2799     }
2800     $w conf -state disabled
2801 }
2802
2803 proc linetoelt {l} {
2804     global treeheight treecontents
2805
2806     set y 2
2807     set prefix {}
2808     while {1} {
2809         foreach e $treecontents($prefix) {
2810             if {$y == $l} {
2811                 return "$prefix$e"
2812             }
2813             set n 1
2814             if {[string index $e end] eq "/"} {
2815                 set n $treeheight($prefix$e)
2816                 if {$y + $n > $l} {
2817                     append prefix $e
2818                     incr y
2819                     break
2820                 }
2821             }
2822             incr y $n
2823         }
2824     }
2825 }
2826
2827 proc highlight_tree {y prefix} {
2828     global treeheight treecontents cflist
2829
2830     foreach e $treecontents($prefix) {
2831         set path $prefix$e
2832         if {[highlight_tag $path] ne {}} {
2833             $cflist tag add bold $y.0 "$y.0 lineend"
2834         }
2835         incr y
2836         if {[string index $e end] eq "/" && $treeheight($path) > 1} {
2837             set y [highlight_tree $y $path]
2838         }
2839     }
2840     return $y
2841 }
2842
2843 proc treeclosedir {w dir} {
2844     global treediropen treeheight treeparent treeindex
2845
2846     set ix $treeindex($dir)
2847     $w conf -state normal
2848     $w delete s:$ix e:$ix
2849     set treediropen($dir) 0
2850     $w image configure a:$ix -image tri-rt
2851     $w conf -state disabled
2852     set n [expr {1 - $treeheight($dir)}]
2853     while {$dir ne {}} {
2854         incr treeheight($dir) $n
2855         set dir $treeparent($dir)
2856     }
2857 }
2858
2859 proc treeopendir {w dir} {
2860     global treediropen treeheight treeparent treecontents treeindex
2861
2862     set ix $treeindex($dir)
2863     $w conf -state normal
2864     $w image configure a:$ix -image tri-dn
2865     $w mark set e:$ix s:$ix
2866     $w mark gravity e:$ix right
2867     set lev 0
2868     set str "\n"
2869     set n [llength $treecontents($dir)]
2870     for {set x $dir} {$x ne {}} {set x $treeparent($x)} {
2871         incr lev
2872         append str "\t"
2873         incr treeheight($x) $n
2874     }
2875     foreach e $treecontents($dir) {
2876         set de $dir$e
2877         if {[string index $e end] eq "/"} {
2878             set iy $treeindex($de)
2879             $w mark set d:$iy e:$ix
2880             $w mark gravity d:$iy left
2881             $w insert e:$ix $str
2882             set treediropen($de) 0
2883             $w image create e:$ix -align center -image tri-rt -padx 1 \
2884                 -name a:$iy
2885             $w insert e:$ix $e [highlight_tag $de]
2886             $w mark set s:$iy e:$ix
2887             $w mark gravity s:$iy left
2888             set treeheight($de) 1
2889         } else {
2890             $w insert e:$ix $str
2891             $w insert e:$ix $e [highlight_tag $de]
2892         }
2893     }
2894     $w mark gravity e:$ix right
2895     $w conf -state disabled
2896     set treediropen($dir) 1
2897     set top [lindex [split [$w index @0,0] .] 0]
2898     set ht [$w cget -height]
2899     set l [lindex [split [$w index s:$ix] .] 0]
2900     if {$l < $top} {
2901         $w yview $l.0
2902     } elseif {$l + $n + 1 > $top + $ht} {
2903         set top [expr {$l + $n + 2 - $ht}]
2904         if {$l < $top} {
2905             set top $l
2906         }
2907         $w yview $top.0
2908     }
2909 }
2910
2911 proc treeclick {w x y} {
2912     global treediropen cmitmode ctext cflist cflist_top
2913
2914     if {$cmitmode ne "tree"} return
2915     if {![info exists cflist_top]} return
2916     set l [lindex [split [$w index "@$x,$y"] "."] 0]
2917     $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
2918     $cflist tag add highlight $l.0 "$l.0 lineend"
2919     set cflist_top $l
2920     if {$l == 1} {
2921         $ctext yview 1.0
2922         return
2923     }
2924     set e [linetoelt $l]
2925     if {[string index $e end] ne "/"} {
2926         showfile $e
2927     } elseif {$treediropen($e)} {
2928         treeclosedir $w $e
2929     } else {
2930         treeopendir $w $e
2931     }
2932 }
2933
2934 proc setfilelist {id} {
2935     global treefilelist cflist jump_to_here
2936
2937     treeview $cflist $treefilelist($id) 0
2938     if {$jump_to_here ne {}} {
2939         set f [lindex $jump_to_here 0]
2940         if {[lsearch -exact $treefilelist($id) $f] >= 0} {
2941             showfile $f
2942         }
2943     }
2944 }
2945
2946 image create bitmap tri-rt -background black -foreground blue -data {
2947     #define tri-rt_width 13
2948     #define tri-rt_height 13
2949     static unsigned char tri-rt_bits[] = {
2950        0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x70, 0x00, 0xf0, 0x00,
2951        0xf0, 0x01, 0xf0, 0x00, 0x70, 0x00, 0x30, 0x00, 0x10, 0x00, 0x00, 0x00,
2952        0x00, 0x00};
2953 } -maskdata {
2954     #define tri-rt-mask_width 13
2955     #define tri-rt-mask_height 13
2956     static unsigned char tri-rt-mask_bits[] = {
2957        0x08, 0x00, 0x18, 0x00, 0x38, 0x00, 0x78, 0x00, 0xf8, 0x00, 0xf8, 0x01,
2958        0xf8, 0x03, 0xf8, 0x01, 0xf8, 0x00, 0x78, 0x00, 0x38, 0x00, 0x18, 0x00,
2959        0x08, 0x00};
2960 }
2961 image create bitmap tri-dn -background black -foreground blue -data {
2962     #define tri-dn_width 13
2963     #define tri-dn_height 13
2964     static unsigned char tri-dn_bits[] = {
2965        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x07, 0xf8, 0x03,
2966        0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2967        0x00, 0x00};
2968 } -maskdata {
2969     #define tri-dn-mask_width 13
2970     #define tri-dn-mask_height 13
2971     static unsigned char tri-dn-mask_bits[] = {
2972        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x1f, 0xfe, 0x0f, 0xfc, 0x07,
2973        0xf8, 0x03, 0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
2974        0x00, 0x00};
2975 }
2976
2977 image create bitmap reficon-T -background black -foreground yellow -data {
2978     #define tagicon_width 13
2979     #define tagicon_height 9
2980     static unsigned char tagicon_bits[] = {
2981        0x00, 0x00, 0x00, 0x00, 0xf0, 0x07, 0xf8, 0x07,
2982        0xfc, 0x07, 0xf8, 0x07, 0xf0, 0x07, 0x00, 0x00, 0x00, 0x00};
2983 } -maskdata {
2984     #define tagicon-mask_width 13
2985     #define tagicon-mask_height 9
2986     static unsigned char tagicon-mask_bits[] = {
2987        0x00, 0x00, 0xf0, 0x0f, 0xf8, 0x0f, 0xfc, 0x0f,
2988        0xfe, 0x0f, 0xfc, 0x0f, 0xf8, 0x0f, 0xf0, 0x0f, 0x00, 0x00};
2989 }
2990 set rectdata {
2991     #define headicon_width 13
2992     #define headicon_height 9
2993     static unsigned char headicon_bits[] = {
2994        0x00, 0x00, 0x00, 0x00, 0xf8, 0x07, 0xf8, 0x07,
2995        0xf8, 0x07, 0xf8, 0x07, 0xf8, 0x07, 0x00, 0x00, 0x00, 0x00};
2996 }
2997 set rectmask {
2998     #define headicon-mask_width 13
2999     #define headicon-mask_height 9
3000     static unsigned char headicon-mask_bits[] = {
3001        0x00, 0x00, 0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f,
3002        0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f, 0x00, 0x00};
3003 }
3004 image create bitmap reficon-H -background black -foreground green \
3005     -data $rectdata -maskdata $rectmask
3006 image create bitmap reficon-o -background black -foreground "#ddddff" \
3007     -data $rectdata -maskdata $rectmask
3008
3009 proc init_flist {first} {
3010     global cflist cflist_top difffilestart
3011
3012     $cflist conf -state normal
3013     $cflist delete 0.0 end
3014     if {$first ne {}} {
3015         $cflist insert end $first
3016         set cflist_top 1
3017         $cflist tag add highlight 1.0 "1.0 lineend"
3018     } else {
3019         catch {unset cflist_top}
3020     }
3021     $cflist conf -state disabled
3022     set difffilestart {}
3023 }
3024
3025 proc highlight_tag {f} {
3026     global highlight_paths
3027
3028     foreach p $highlight_paths {
3029         if {[string match $p $f]} {
3030             return "bold"
3031         }
3032     }
3033     return {}
3034 }
3035
3036 proc highlight_filelist {} {
3037     global cmitmode cflist
3038
3039     $cflist conf -state normal
3040     if {$cmitmode ne "tree"} {
3041         set end [lindex [split [$cflist index end] .] 0]
3042         for {set l 2} {$l < $end} {incr l} {
3043             set line [$cflist get $l.0 "$l.0 lineend"]
3044             if {[highlight_tag $line] ne {}} {
3045                 $cflist tag add bold $l.0 "$l.0 lineend"
3046             }
3047         }
3048     } else {
3049         highlight_tree 2 {}
3050     }
3051     $cflist conf -state disabled
3052 }
3053
3054 proc unhighlight_filelist {} {
3055     global cflist
3056
3057     $cflist conf -state normal
3058     $cflist tag remove bold 1.0 end
3059     $cflist conf -state disabled
3060 }
3061
3062 proc add_flist {fl} {
3063     global cflist
3064
3065     $cflist conf -state normal
3066     foreach f $fl {
3067         $cflist insert end "\n"
3068         $cflist insert end $f [highlight_tag $f]
3069     }
3070     $cflist conf -state disabled
3071 }
3072
3073 proc sel_flist {w x y} {
3074     global ctext difffilestart cflist cflist_top cmitmode
3075
3076     if {$cmitmode eq "tree"} return
3077     if {![info exists cflist_top]} return
3078     set l [lindex [split [$w index "@$x,$y"] "."] 0]
3079     $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
3080     $cflist tag add highlight $l.0 "$l.0 lineend"
3081     set cflist_top $l
3082     if {$l == 1} {
3083         $ctext yview 1.0
3084     } else {
3085         catch {$ctext yview [lindex $difffilestart [expr {$l - 2}]]}
3086     }
3087 }
3088
3089 proc pop_flist_menu {w X Y x y} {
3090     global ctext cflist cmitmode flist_menu flist_menu_file
3091     global treediffs diffids
3092
3093     stopfinding
3094     set l [lindex [split [$w index "@$x,$y"] "."] 0]
3095     if {$l <= 1} return
3096     if {$cmitmode eq "tree"} {
3097         set e [linetoelt $l]
3098         if {[string index $e end] eq "/"} return
3099     } else {
3100         set e [lindex $treediffs($diffids) [expr {$l-2}]]
3101     }
3102     set flist_menu_file $e
3103     set xdiffstate "normal"
3104     if {$cmitmode eq "tree"} {
3105         set xdiffstate "disabled"
3106     }
3107     # Disable "External diff" item in tree mode
3108     $flist_menu entryconf 2 -state $xdiffstate
3109     tk_popup $flist_menu $X $Y
3110 }
3111
3112 proc find_ctext_fileinfo {line} {
3113     global ctext_file_names ctext_file_lines
3114
3115     set ok [bsearch $ctext_file_lines $line]
3116     set tline [lindex $ctext_file_lines $ok]
3117
3118     if {$ok >= [llength $ctext_file_lines] || $line < $tline} {
3119         return {}
3120     } else {
3121         return [list [lindex $ctext_file_names $ok] $tline]
3122     }
3123 }
3124
3125 proc pop_diff_menu {w X Y x y} {
3126     global ctext diff_menu flist_menu_file
3127     global diff_menu_txtpos diff_menu_line
3128     global diff_menu_filebase
3129
3130     set diff_menu_txtpos [split [$w index "@$x,$y"] "."]
3131     set diff_menu_line [lindex $diff_menu_txtpos 0]
3132     # don't pop up the menu on hunk-separator or file-separator lines
3133     if {[lsearch -glob [$ctext tag names $diff_menu_line.0] "*sep"] >= 0} {
3134         return
3135     }
3136     stopfinding
3137     set f [find_ctext_fileinfo $diff_menu_line]
3138     if {$f eq {}} return
3139     set flist_menu_file [lindex $f 0]
3140     set diff_menu_filebase [lindex $f 1]
3141     tk_popup $diff_menu $X $Y
3142 }
3143
3144 proc flist_hl {only} {
3145     global flist_menu_file findstring gdttype
3146
3147     set x [shellquote $flist_menu_file]
3148     if {$only || $findstring eq {} || $gdttype ne [mc "touching paths:"]} {
3149         set findstring $x
3150     } else {
3151         append findstring " " $x
3152     }
3153     set gdttype [mc "touching paths:"]
3154 }
3155
3156 proc save_file_from_commit {filename output what} {
3157     global nullfile
3158
3159     if {[catch {exec git show $filename -- > $output} err]} {
3160         if {[string match "fatal: bad revision *" $err]} {
3161             return $nullfile
3162         }
3163         error_popup "[mc "Error getting \"%s\" from %s:" $filename $what] $err"
3164         return {}
3165     }
3166     return $output
3167 }
3168
3169 proc external_diff_get_one_file {diffid filename diffdir} {
3170     global nullid nullid2 nullfile
3171     global gitdir
3172
3173     if {$diffid == $nullid} {
3174         set difffile [file join [file dirname $gitdir] $filename]
3175         if {[file exists $difffile]} {
3176             return $difffile
3177         }
3178         return $nullfile
3179     }
3180     if {$diffid == $nullid2} {
3181         set difffile [file join $diffdir "\[index\] [file tail $filename]"]
3182         return [save_file_from_commit :$filename $difffile index]
3183     }
3184     set difffile [file join $diffdir "\[$diffid\] [file tail $filename]"]
3185     return [save_file_from_commit $diffid:$filename $difffile \
3186                "revision $diffid"]
3187 }
3188
3189 proc external_diff {} {
3190     global gitktmpdir nullid nullid2
3191     global flist_menu_file
3192     global diffids
3193     global diffnum
3194     global gitdir extdifftool
3195
3196     if {[llength $diffids] == 1} {
3197         # no reference commit given
3198         set diffidto [lindex $diffids 0]
3199         if {$diffidto eq $nullid} {
3200             # diffing working copy with index
3201             set diffidfrom $nullid2
3202         } elseif {$diffidto eq $nullid2} {
3203             # diffing index with HEAD
3204             set diffidfrom "HEAD"
3205         } else {
3206             # use first parent commit
3207             global parentlist selectedline
3208             set diffidfrom [lindex $parentlist $selectedline 0]
3209         }
3210     } else {
3211         set diffidfrom [lindex $diffids 0]
3212         set diffidto [lindex $diffids 1]
3213     }
3214
3215     # make sure that several diffs wont collide
3216     if {![info exists gitktmpdir]} {
3217         set gitktmpdir [file join [file dirname $gitdir] \
3218                             [format ".gitk-tmp.%s" [pid]]]
3219         if {[catch {file mkdir $gitktmpdir} err]} {
3220             error_popup "[mc "Error creating temporary directory %s:" $gitktmpdir] $err"
3221             unset gitktmpdir
3222             return
3223         }
3224         set diffnum 0
3225     }
3226     incr diffnum
3227     set diffdir [file join $gitktmpdir $diffnum]
3228     if {[catch {file mkdir $diffdir} err]} {
3229         error_popup "[mc "Error creating temporary directory %s:" $diffdir] $err"
3230         return
3231     }
3232
3233     # gather files to diff
3234     set difffromfile [external_diff_get_one_file $diffidfrom $flist_menu_file $diffdir]
3235     set difftofile [external_diff_get_one_file $diffidto $flist_menu_file $diffdir]
3236
3237     if {$difffromfile ne {} && $difftofile ne {}} {
3238         set cmd [concat | [shellsplit $extdifftool] \
3239                      [list $difffromfile $difftofile]]
3240         if {[catch {set fl [open $cmd r]} err]} {
3241             file delete -force $diffdir
3242             error_popup "$extdifftool: [mc "command failed:"] $err"
3243         } else {
3244             fconfigure $fl -blocking 0
3245             filerun $fl [list delete_at_eof $fl $diffdir]
3246         }
3247     }
3248 }
3249
3250 proc find_hunk_blamespec {base line} {
3251     global ctext
3252
3253     # Find and parse the hunk header
3254     set s_lix [$ctext search -backwards -regexp ^@@ "$line.0 lineend" $base.0]
3255     if {$s_lix eq {}} return
3256
3257     set s_line [$ctext get $s_lix "$s_lix + 1 lines"]
3258     if {![regexp {^@@@*(( -\d+(,\d+)?)+) \+(\d+)(,\d+)? @@} $s_line \
3259             s_line old_specs osz osz1 new_line nsz]} {
3260         return
3261     }
3262
3263     # base lines for the parents
3264     set base_lines [list $new_line]
3265     foreach old_spec [lrange [split $old_specs " "] 1 end] {
3266         if {![regexp -- {-(\d+)(,\d+)?} $old_spec \
3267                 old_spec old_line osz]} {
3268             return
3269         }
3270         lappend base_lines $old_line
3271     }
3272
3273     # Now scan the lines to determine offset within the hunk
3274     set max_parent [expr {[llength $base_lines]-2}]
3275     set dline 0
3276     set s_lno [lindex [split $s_lix "."] 0]
3277
3278     # Determine if the line is removed
3279     set chunk [$ctext get $line.0 "$line.1 + $max_parent chars"]
3280     if {[string match {[-+ ]*} $chunk]} {
3281         set removed_idx [string first "-" $chunk]
3282         # Choose a parent index
3283         if {$removed_idx >= 0} {
3284             set parent $removed_idx
3285         } else {
3286             set unchanged_idx [string first " " $chunk]
3287             if {$unchanged_idx >= 0} {
3288                 set parent $unchanged_idx
3289             } else {
3290                 # blame the current commit
3291                 set parent -1
3292             }
3293         }
3294         # then count other lines that belong to it
3295         for {set i $line} {[incr i -1] > $s_lno} {} {
3296             set chunk [$ctext get $i.0 "$i.1 + $max_parent chars"]
3297             # Determine if the line is removed
3298             set removed_idx [string first "-" $chunk]
3299             if {$parent >= 0} {
3300                 set code [string index $chunk $parent]
3301                 if {$code eq "-" || ($removed_idx < 0 && $code ne "+")} {
3302                     incr dline
3303                 }
3304             } else {
3305                 if {$removed_idx < 0} {
3306                     incr dline
3307                 }
3308             }
3309         }
3310         incr parent
3311     } else {
3312         set parent 0
3313     }
3314
3315     incr dline [lindex $base_lines $parent]
3316     return [list $parent $dline]
3317 }
3318
3319 proc external_blame_diff {} {
3320     global currentid cmitmode
3321     global diff_menu_txtpos diff_menu_line
3322     global diff_menu_filebase flist_menu_file
3323
3324     if {$cmitmode eq "tree"} {
3325         set parent_idx 0
3326         set line [expr {$diff_menu_line - $diff_menu_filebase}]
3327     } else {
3328         set hinfo [find_hunk_blamespec $diff_menu_filebase $diff_menu_line]
3329         if {$hinfo ne {}} {
3330             set parent_idx [lindex $hinfo 0]
3331             set line [lindex $hinfo 1]
3332         } else {
3333             set parent_idx 0
3334             set line 0
3335         }
3336     }
3337
3338     external_blame $parent_idx $line
3339 }
3340
3341 # Find the SHA1 ID of the blob for file $fname in the index
3342 # at stage 0 or 2
3343 proc index_sha1 {fname} {
3344     set f [open [list | git ls-files -s $fname] r]
3345     while {[gets $f line] >= 0} {
3346         set info [lindex [split $line "\t"] 0]
3347         set stage [lindex $info 2]
3348         if {$stage eq "0" || $stage eq "2"} {
3349             close $f
3350             return [lindex $info 1]
3351         }
3352     }
3353     close $f
3354     return {}
3355 }
3356
3357 # Turn an absolute path into one relative to the current directory
3358 proc make_relative {f} {
3359     set elts [file split $f]
3360     set here [file split [pwd]]
3361     set ei 0
3362     set hi 0
3363     set res {}
3364     foreach d $here {
3365         if {$ei < $hi || $ei >= [llength $elts] || [lindex $elts $ei] ne $d} {
3366             lappend res ".."
3367         } else {
3368             incr ei
3369         }
3370         incr hi
3371     }
3372     set elts [concat $res [lrange $elts $ei end]]
3373     return [eval file join $elts]
3374 }
3375
3376 proc external_blame {parent_idx {line {}}} {
3377     global flist_menu_file gitdir
3378     global nullid nullid2
3379     global parentlist selectedline currentid
3380
3381     if {$parent_idx > 0} {
3382         set base_commit [lindex $parentlist $selectedline [expr {$parent_idx-1}]]
3383     } else {
3384         set base_commit $currentid
3385     }
3386
3387     if {$base_commit eq {} || $base_commit eq $nullid || $base_commit eq $nullid2} {
3388         error_popup [mc "No such commit"]
3389         return
3390     }
3391
3392     set cmdline [list git gui blame]
3393     if {$line ne {} && $line > 1} {
3394         lappend cmdline "--line=$line"
3395     }
3396     set f [file join [file dirname $gitdir] $flist_menu_file]
3397     # Unfortunately it seems git gui blame doesn't like
3398     # being given an absolute path...
3399     set f [make_relative $f]
3400     lappend cmdline $base_commit $f
3401     if {[catch {eval exec $cmdline &} err]} {
3402         error_popup "[mc "git gui blame: command failed:"] $err"
3403     }
3404 }
3405
3406 proc show_line_source {} {
3407     global cmitmode currentid parents curview blamestuff blameinst
3408     global diff_menu_line diff_menu_filebase flist_menu_file
3409     global nullid nullid2 gitdir
3410
3411     set from_index {}
3412     if {$cmitmode eq "tree"} {
3413         set id $currentid
3414         set line [expr {$diff_menu_line - $diff_menu_filebase}]
3415     } else {
3416         set h [find_hunk_blamespec $diff_menu_filebase $diff_menu_line]
3417         if {$h eq {}} return
3418         set pi [lindex $h 0]
3419         if {$pi == 0} {
3420             mark_ctext_line $diff_menu_line
3421             return
3422         }
3423         incr pi -1
3424         if {$currentid eq $nullid} {
3425             if {$pi > 0} {
3426                 # must be a merge in progress...
3427                 if {[catch {
3428                     # get the last line from .git/MERGE_HEAD
3429                     set f [open [file join $gitdir MERGE_HEAD] r]
3430                     set id [lindex [split [read $f] "\n"] end-1]
3431                     close $f
3432                 } err]} {
3433                     error_popup [mc "Couldn't read merge head: %s" $err]
3434                     return
3435                 }
3436             } elseif {$parents($curview,$currentid) eq $nullid2} {
3437                 # need to do the blame from the index
3438                 if {[catch {
3439                     set from_index [index_sha1 $flist_menu_file]
3440                 } err]} {
3441                     error_popup [mc "Error reading index: %s" $err]
3442                     return
3443                 }
3444             } else {
3445                 set id $parents($curview,$currentid)
3446             }
3447         } else {
3448             set id [lindex $parents($curview,$currentid) $pi]
3449         }
3450         set line [lindex $h 1]
3451     }
3452     set blameargs {}
3453     if {$from_index ne {}} {
3454         lappend blameargs | git cat-file blob $from_index
3455     }
3456     lappend blameargs | git blame -p -L$line,+1
3457     if {$from_index ne {}} {
3458         lappend blameargs --contents -
3459     } else {
3460         lappend blameargs $id
3461     }
3462     lappend blameargs -- [file join [file dirname $gitdir] $flist_menu_file]
3463     if {[catch {
3464         set f [open $blameargs r]
3465     } err]} {
3466         error_popup [mc "Couldn't start git blame: %s" $err]
3467         return
3468     }
3469     nowbusy blaming [mc "Searching"]
3470     fconfigure $f -blocking 0
3471     set i [reg_instance $f]
3472     set blamestuff($i) {}
3473     set blameinst $i
3474     filerun $f [list read_line_source $f $i]
3475 }
3476
3477 proc stopblaming {} {
3478     global blameinst
3479
3480     if {[info exists blameinst]} {
3481         stop_instance $blameinst
3482         unset blameinst
3483         notbusy blaming
3484     }
3485 }
3486
3487 proc read_line_source {fd inst} {
3488     global blamestuff curview commfd blameinst nullid nullid2
3489
3490     while {[gets $fd line] >= 0} {
3491         lappend blamestuff($inst) $line
3492     }
3493     if {![eof $fd]} {
3494         return 1
3495     }
3496     unset commfd($inst)
3497     unset blameinst
3498     notbusy blaming
3499     fconfigure $fd -blocking 1
3500     if {[catch {close $fd} err]} {
3501         error_popup [mc "Error running git blame: %s" $err]
3502         return 0
3503     }
3504
3505     set fname {}
3506     set line [split [lindex $blamestuff($inst) 0] " "]
3507     set id [lindex $line 0]
3508     set lnum [lindex $line 1]
3509     if {[string length $id] == 40 && [string is xdigit $id] &&
3510         [string is digit -strict $lnum]} {
3511         # look for "filename" line
3512         foreach l $blamestuff($inst) {
3513             if {[string match "filename *" $l]} {
3514                 set fname [string range $l 9 end]
3515                 break
3516             }
3517         }
3518     }
3519     if {$fname ne {}} {
3520         # all looks good, select it
3521         if {$id eq $nullid} {
3522             # blame uses all-zeroes to mean not committed,
3523             # which would mean a change in the index
3524             set id $nullid2
3525         }
3526         if {[commitinview $id $curview]} {
3527             selectline [rowofcommit $id] 1 [list $fname $lnum]
3528         } else {
3529             error_popup [mc "That line comes from commit %s, \
3530                              which is not in this view" [shortids $id]]
3531         }
3532     } else {
3533         puts "oops couldn't parse git blame output"
3534     }
3535     return 0
3536 }
3537
3538 # delete $dir when we see eof on $f (presumably because the child has exited)
3539 proc delete_at_eof {f dir} {
3540     while {[gets $f line] >= 0} {}
3541     if {[eof $f]} {
3542         if {[catch {close $f} err]} {
3543             error_popup "[mc "External diff viewer failed:"] $err"
3544         }
3545         file delete -force $dir
3546         return 0
3547     }
3548     return 1
3549 }
3550
3551 # Functions for adding and removing shell-type quoting
3552
3553 proc shellquote {str} {
3554     if {![string match "*\['\"\\ \t]*" $str]} {
3555         return $str
3556     }
3557     if {![string match "*\['\"\\]*" $str]} {
3558         return "\"$str\""
3559     }
3560     if {![string match "*'*" $str]} {
3561         return "'$str'"
3562     }
3563     return "\"[string map {\" \\\" \\ \\\\} $str]\""
3564 }
3565
3566 proc shellarglist {l} {
3567     set str {}
3568     foreach a $l {
3569         if {$str ne {}} {
3570             append str " "
3571         }
3572         append str [shellquote $a]
3573     }
3574     return $str
3575 }
3576
3577 proc shelldequote {str} {
3578     set ret {}
3579     set used -1
3580     while {1} {
3581         incr used
3582         if {![regexp -start $used -indices "\['\"\\\\ \t]" $str first]} {
3583             append ret [string range $str $used end]
3584             set used [string length $str]
3585             break
3586         }
3587         set first [lindex $first 0]
3588         set ch [string index $str $first]
3589         if {$first > $used} {
3590             append ret [string range $str $used [expr {$first - 1}]]
3591             set used $first
3592         }
3593         if {$ch eq " " || $ch eq "\t"} break
3594         incr used
3595         if {$ch eq "'"} {
3596             set first [string first "'" $str $used]
3597             if {$first < 0} {
3598                 error "unmatched single-quote"
3599             }
3600             append ret [string range $str $used [expr {$first - 1}]]
3601             set used $first
3602             continue
3603         }
3604         if {$ch eq "\\"} {
3605             if {$used >= [string length $str]} {
3606                 error "trailing backslash"
3607             }
3608             append ret [string index $str $used]
3609             continue
3610         }
3611         # here ch == "\""
3612         while {1} {
3613             if {![regexp -start $used -indices "\[\"\\\\]" $str first]} {
3614                 error "unmatched double-quote"
3615             }
3616             set first [lindex $first 0]
3617             set ch [string index $str $first]
3618             if {$first > $used} {
3619                 append ret [string range $str $used [expr {$first - 1}]]
3620                 set used $first
3621             }
3622             if {$ch eq "\""} break
3623             incr used
3624             append ret [string index $str $used]
3625             incr used
3626         }
3627     }
3628     return [list $used $ret]
3629 }
3630
3631 proc shellsplit {str} {
3632     set l {}
3633     while {1} {
3634         set str [string trimleft $str]
3635         if {$str eq {}} break
3636         set dq [shelldequote $str]
3637         set n [lindex $dq 0]
3638         set word [lindex $dq 1]
3639         set str [string range $str $n end]
3640         lappend l $word
3641     }
3642     return $l
3643 }
3644
3645 # Code to implement multiple views
3646
3647 proc newview {ishighlight} {
3648     global nextviewnum newviewname newishighlight
3649     global revtreeargs viewargscmd newviewopts curview
3650
3651     set newishighlight $ishighlight
3652     set top .gitkview
3653     if {[winfo exists $top]} {
3654         raise $top
3655         return
3656     }
3657     set newviewname($nextviewnum) "[mc "View"] $nextviewnum"
3658     set newviewopts($nextviewnum,perm) 0
3659     set newviewopts($nextviewnum,cmd)  $viewargscmd($curview)
3660     decode_view_opts $nextviewnum $revtreeargs
3661     vieweditor $top $nextviewnum [mc "Gitk view definition"]
3662 }
3663
3664 set known_view_options {
3665     {perm    b    . {}               {mc "Remember this view"}}
3666     {args    t50= + {}               {mc "Commits to include (arguments to git log):"}}
3667     {all     b    * "--all"          {mc "Use all refs"}}
3668     {dorder  b    . {"--date-order" "-d"}      {mc "Strictly sort by date"}}
3669     {lright  b    . "--left-right"   {mc "Mark branch sides"}}
3670     {since   t15  + {"--since=*" "--after=*"}  {mc "Since date:"}}
3671     {until   t15  . {"--until=*" "--before=*"} {mc "Until date:"}}
3672     {limit   t10  + "--max-count=*"  {mc "Max count:"}}
3673     {skip    t10  . "--skip=*"       {mc "Skip:"}}
3674     {first   b    . "--first-parent" {mc "Limit to first parent"}}
3675     {cmd     t50= + {}               {mc "Command to generate more commits to include:"}}
3676     }
3677
3678 proc encode_view_opts {n} {
3679     global known_view_options newviewopts
3680
3681     set rargs [list]
3682     foreach opt $known_view_options {
3683         set patterns [lindex $opt 3]
3684         if {$patterns eq {}} continue
3685         set pattern [lindex $patterns 0]
3686
3687         set val $newviewopts($n,[lindex $opt 0])
3688         
3689         if {[lindex $opt 1] eq "b"} {
3690             if {$val} {
3691                 lappend rargs $pattern
3692             }
3693         } else {
3694             set val [string trim $val]
3695             if {$val ne {}} {
3696                 set pfix [string range $pattern 0 end-1]
3697                 lappend rargs $pfix$val
3698             }
3699         }
3700     }
3701     return [concat $rargs [shellsplit $newviewopts($n,args)]]
3702 }
3703
3704 proc decode_view_opts {n view_args} {
3705     global known_view_options newviewopts
3706
3707     foreach opt $known_view_options {
3708         if {[lindex $opt 1] eq "b"} {
3709             set val 0
3710         } else {
3711             set val {}
3712         }
3713         set newviewopts($n,[lindex $opt 0]) $val
3714     }
3715     set oargs [list]
3716     foreach arg $view_args {
3717         if {[regexp -- {^-([0-9]+)$} $arg arg cnt]
3718             && ![info exists found(limit)]} {
3719             set newviewopts($n,limit) $cnt
3720             set found(limit) 1
3721             continue
3722         }
3723         catch { unset val }
3724         foreach opt $known_view_options {
3725             set id [lindex $opt 0]
3726             if {[info exists found($id)]} continue
3727             foreach pattern [lindex $opt 3] {
3728                 if {![string match $pattern $arg]} continue
3729                 if {[lindex $opt 1] ne "b"} {
3730                     set size [string length $pattern]
3731                     set val [string range $arg [expr {$size-1}] end]
3732                 } else {
3733                     set val 1
3734                 }
3735                 set newviewopts($n,$id) $val
3736                 set found($id) 1
3737                 break
3738             }
3739             if {[info exists val]} break
3740         }
3741         if {[info exists val]} continue
3742         lappend oargs $arg
3743     }
3744     set newviewopts($n,args) [shellarglist $oargs]
3745 }
3746
3747 proc edit_or_newview {} {
3748     global curview
3749
3750     if {$curview > 0} {
3751         editview
3752     } else {
3753         newview 0
3754     }
3755 }
3756
3757 proc editview {} {
3758     global curview
3759     global viewname viewperm newviewname newviewopts
3760     global viewargs viewargscmd
3761
3762     set top .gitkvedit-$curview
3763     if {[winfo exists $top]} {
3764         raise $top
3765         return
3766     }
3767     set newviewname($curview)      $viewname($curview)
3768     set newviewopts($curview,perm) $viewperm($curview)
3769     set newviewopts($curview,cmd)  $viewargscmd($curview)
3770     decode_view_opts $curview $viewargs($curview)
3771     vieweditor $top $curview "Gitk: edit view $viewname($curview)"
3772 }
3773
3774 proc vieweditor {top n title} {
3775     global newviewname newviewopts viewfiles bgcolor
3776     global known_view_options
3777
3778     toplevel $top
3779     wm title $top $title
3780     make_transient $top .
3781
3782     # View name
3783     frame $top.nfr
3784     label $top.nl -text [mc "Name"]
3785     entry $top.name -width 20 -textvariable newviewname($n)
3786     pack $top.nfr -in $top -fill x -pady 5 -padx 3
3787     pack $top.nl -in $top.nfr -side left -padx {0 30}
3788     pack $top.name -in $top.nfr -side left
3789
3790     # View options
3791     set cframe $top.nfr
3792     set cexpand 0
3793     set cnt 0
3794     foreach opt $known_view_options {
3795         set id [lindex $opt 0]
3796         set type [lindex $opt 1]
3797         set flags [lindex $opt 2]
3798         set title [eval [lindex $opt 4]]
3799         set lxpad 0
3800
3801         if {$flags eq "+" || $flags eq "*"} {
3802             set cframe $top.fr$cnt
3803             incr cnt
3804             frame $cframe
3805             pack $cframe -in $top -fill x -pady 3 -padx 3
3806             set cexpand [expr {$flags eq "*"}]
3807         } else {
3808             set lxpad 5
3809         }
3810
3811         if {$type eq "b"} {
3812             checkbutton $cframe.c_$id -text $title -variable newviewopts($n,$id)
3813             pack $cframe.c_$id -in $cframe -side left \
3814                 -padx [list $lxpad 0] -expand $cexpand -anchor w
3815         } elseif {[regexp {^t(\d+)$} $type type sz]} {
3816             message $cframe.l_$id -aspect 1500 -text $title
3817             entry $cframe.e_$id -width $sz -background $bgcolor \
3818                 -textvariable newviewopts($n,$id)
3819             pack $cframe.l_$id -in $cframe -side left -padx [list $lxpad 0]
3820             pack $cframe.e_$id -in $cframe -side left -expand 1 -fill x
3821         } elseif {[regexp {^t(\d+)=$} $type type sz]} {
3822             message $cframe.l_$id -aspect 1500 -text $title
3823             entry $cframe.e_$id -width $sz -background $bgcolor \
3824                 -textvariable newviewopts($n,$id)
3825             pack $cframe.l_$id -in $cframe -side top -pady [list 3 0] -anchor w
3826             pack $cframe.e_$id -in $cframe -side top -fill x
3827         }
3828     }
3829
3830     # Path list
3831     message $top.l -aspect 1500 \
3832         -text [mc "Enter files and directories to include, one per line:"]
3833     pack $top.l -in $top -side top -pady [list 7 0] -anchor w -padx 3
3834     text $top.t -width 40 -height 5 -background $bgcolor -font uifont
3835     if {[info exists viewfiles($n)]} {
3836         foreach f $viewfiles($n) {
3837             $top.t insert end $f
3838             $top.t insert end "\n"
3839         }
3840         $top.t delete {end - 1c} end
3841         $top.t mark set insert 0.0
3842     }
3843     pack $top.t -in $top -side top -pady [list 0 5] -fill both -expand 1 -padx 3
3844     frame $top.buts
3845     button $top.buts.ok -text [mc "OK"] -command [list newviewok $top $n]
3846     button $top.buts.apply -text [mc "Apply (F5)"] -command [list newviewok $top $n 1]
3847     button $top.buts.can -text [mc "Cancel"] -command [list destroy $top]
3848     bind $top <Control-Return> [list newviewok $top $n]
3849     bind $top <F5> [list newviewok $top $n 1]
3850     bind $top <Escape> [list destroy $top]
3851     grid $top.buts.ok $top.buts.apply $top.buts.can
3852     grid columnconfigure $top.buts 0 -weight 1 -uniform a
3853     grid columnconfigure $top.buts 1 -weight 1 -uniform a
3854     grid columnconfigure $top.buts 2 -weight 1 -uniform a
3855     pack $top.buts -in $top -side top -fill x
3856     focus $top.t
3857 }
3858
3859 proc doviewmenu {m first cmd op argv} {
3860     set nmenu [$m index end]
3861     for {set i $first} {$i <= $nmenu} {incr i} {
3862         if {[$m entrycget $i -command] eq $cmd} {
3863             eval $m $op $i $argv
3864             break
3865         }
3866     }
3867 }
3868
3869 proc allviewmenus {n op args} {
3870     # global viewhlmenu
3871
3872     doviewmenu .bar.view 5 [list showview $n] $op $args
3873     # doviewmenu $viewhlmenu 1 [list addvhighlight $n] $op $args
3874 }
3875
3876 proc newviewok {top n {apply 0}} {
3877     global nextviewnum newviewperm newviewname newishighlight
3878     global viewname viewfiles viewperm selectedview curview
3879     global viewargs viewargscmd newviewopts viewhlmenu
3880
3881     if {[catch {
3882         set newargs [encode_view_opts $n]
3883     } err]} {
3884         error_popup "[mc "Error in commit selection arguments:"] $err" $top
3885         return
3886     }
3887     set files {}
3888     foreach f [split [$top.t get 0.0 end] "\n"] {
3889         set ft [string trim $f]
3890         if {$ft ne {}} {
3891             lappend files $ft
3892         }
3893     }
3894     if {![info exists viewfiles($n)]} {
3895         # creating a new view
3896         incr nextviewnum
3897         set viewname($n) $newviewname($n)
3898         set viewperm($n) $newviewopts($n,perm)
3899         set viewfiles($n) $files
3900         set viewargs($n) $newargs
3901         set viewargscmd($n) $newviewopts($n,cmd)
3902         addviewmenu $n
3903         if {!$newishighlight} {
3904             run showview $n
3905         } else {
3906             run addvhighlight $n
3907         }
3908     } else {
3909         # editing an existing view
3910         set viewperm($n) $newviewopts($n,perm)
3911         if {$newviewname($n) ne $viewname($n)} {
3912             set viewname($n) $newviewname($n)
3913             doviewmenu .bar.view 5 [list showview $n] \
3914                 entryconf [list -label $viewname($n)]
3915             # doviewmenu $viewhlmenu 1 [list addvhighlight $n] \
3916                 # entryconf [list -label $viewname($n) -value $viewname($n)]
3917         }
3918         if {$files ne $viewfiles($n) || $newargs ne $viewargs($n) || \
3919                 $newviewopts($n,cmd) ne $viewargscmd($n)} {
3920             set viewfiles($n) $files
3921             set viewargs($n) $newargs
3922             set viewargscmd($n) $newviewopts($n,cmd)
3923             if {$curview == $n} {
3924                 run reloadcommits
3925             }
3926         }
3927     }
3928     if {$apply} return
3929     catch {destroy $top}
3930 }
3931
3932 proc delview {} {
3933     global curview viewperm hlview selectedhlview
3934
3935     if {$curview == 0} return
3936     if {[info exists hlview] && $hlview == $curview} {
3937         set selectedhlview [mc "None"]
3938         unset hlview
3939     }
3940     allviewmenus $curview delete
3941     set viewperm($curview) 0
3942     showview 0
3943 }
3944
3945 proc addviewmenu {n} {
3946     global viewname viewhlmenu
3947
3948     .bar.view add radiobutton -label $viewname($n) \
3949         -command [list showview $n] -variable selectedview -value $n
3950     #$viewhlmenu add radiobutton -label $viewname($n) \
3951     #   -command [list addvhighlight $n] -variable selectedhlview
3952 }
3953
3954 proc showview {n} {
3955     global curview cached_commitrow ordertok
3956     global displayorder parentlist rowidlist rowisopt rowfinal
3957     global colormap rowtextx nextcolor canvxmax
3958     global numcommits viewcomplete
3959     global selectedline currentid canv canvy0
3960     global treediffs
3961     global pending_select mainheadid
3962     global commitidx
3963     global selectedview
3964     global hlview selectedhlview commitinterest
3965
3966     if {$n == $curview} return
3967     set selid {}
3968     set ymax [lindex [$canv cget -scrollregion] 3]
3969     set span [$canv yview]
3970     set ytop [expr {[lindex $span 0] * $ymax}]
3971     set ybot [expr {[lindex $span 1] * $ymax}]
3972     set yscreen [expr {($ybot - $ytop) / 2}]
3973     if {$selectedline ne {}} {
3974         set selid $currentid
3975         set y [yc $selectedline]
3976         if {$ytop < $y && $y < $ybot} {
3977             set yscreen [expr {$y - $ytop}]
3978         }
3979     } elseif {[info exists pending_select]} {
3980         set selid $pending_select
3981         unset pending_select
3982     }
3983     unselectline
3984     normalline
3985     catch {unset treediffs}
3986     clear_display
3987     if {[info exists hlview] && $hlview == $n} {
3988         unset hlview
3989         set selectedhlview [mc "None"]
3990     }
3991     catch {unset commitinterest}
3992     catch {unset cached_commitrow}
3993     catch {unset ordertok}
3994
3995     set curview $n
3996     set selectedview $n
3997     .bar.view entryconf [mca "Edit view..."] -state [expr {$n == 0? "disabled": "normal"}]
3998     .bar.view entryconf [mca "Delete view"] -state [expr {$n == 0? "disabled": "normal"}]
3999
4000     run refill_reflist
4001     if {![info exists viewcomplete($n)]} {
4002         getcommits $selid
4003         return
4004     }
4005
4006     set displayorder {}
4007     set parentlist {}
4008     set rowidlist {}
4009     set rowisopt {}
4010     set rowfinal {}
4011     set numcommits $commitidx($n)
4012
4013     catch {unset colormap}
4014     catch {unset rowtextx}
4015     set nextcolor 0
4016     set canvxmax [$canv cget -width]
4017     set curview $n
4018     set row 0
4019     setcanvscroll
4020     set yf 0
4021     set row {}
4022     if {$selid ne {} && [commitinview $selid $n]} {
4023         set row [rowofcommit $selid]
4024         # try to get the selected row in the same position on the screen
4025         set ymax [lindex [$canv cget -scrollregion] 3]
4026         set ytop [expr {[yc $row] - $yscreen}]
4027         if {$ytop < 0} {
4028             set ytop 0
4029         }
4030         set yf [expr {$ytop * 1.0 / $ymax}]
4031     }
4032     allcanvs yview moveto $yf
4033     drawvisible
4034     if {$row ne {}} {
4035         selectline $row 0
4036     } elseif {!$viewcomplete($n)} {
4037         reset_pending_select $selid
4038     } else {
4039         reset_pending_select {}
4040
4041         if {[commitinview $pending_select $curview]} {
4042             selectline [rowofcommit $pending_select] 1
4043         } else {
4044             set row [first_real_row]
4045             if {$row < $numcommits} {
4046                 selectline $row 0
4047             }
4048         }
4049     }
4050     if {!$viewcomplete($n)} {
4051         if {$numcommits == 0} {
4052             show_status [mc "Reading commits..."]
4053         }
4054     } elseif {$numcommits == 0} {
4055         show_status [mc "No commits selected"]
4056     }
4057 }
4058
4059 # Stuff relating to the highlighting facility
4060
4061 proc ishighlighted {id} {
4062     global vhighlights fhighlights nhighlights rhighlights
4063
4064     if {[info exists nhighlights($id)] && $nhighlights($id) > 0} {
4065         return $nhighlights($id)
4066     }
4067     if {[info exists vhighlights($id)] && $vhighlights($id) > 0} {
4068         return $vhighlights($id)
4069     }
4070     if {[info exists fhighlights($id)] && $fhighlights($id) > 0} {
4071         return $fhighlights($id)
4072     }
4073     if {[info exists rhighlights($id)] && $rhighlights($id) > 0} {
4074         return $rhighlights($id)
4075     }
4076     return 0
4077 }
4078
4079 proc bolden {id font} {
4080     global canv linehtag currentid boldids need_redisplay markedid
4081
4082     # need_redisplay = 1 means the display is stale and about to be redrawn
4083     if {$need_redisplay} return
4084     lappend boldids $id
4085     $canv itemconf $linehtag($id) -font $font
4086     if {[info exists currentid] && $id eq $currentid} {
4087         $canv delete secsel
4088         set t [eval $canv create rect [$canv bbox $linehtag($id)] \
4089                    -outline {{}} -tags secsel \
4090                    -fill [$canv cget -selectbackground]]
4091         $canv lower $t
4092     }
4093     if {[info exists markedid] && $id eq $markedid} {
4094         make_idmark $id
4095     }
4096 }
4097
4098 proc bolden_name {id font} {
4099     global canv2 linentag currentid boldnameids need_redisplay
4100
4101     if {$need_redisplay} return
4102     lappend boldnameids $id
4103     $canv2 itemconf $linentag($id) -font $font
4104     if {[info exists currentid] && $id eq $currentid} {
4105         $canv2 delete secsel
4106         set t [eval $canv2 create rect [$canv2 bbox $linentag($id)] \
4107                    -outline {{}} -tags secsel \
4108                    -fill [$canv2 cget -selectbackground]]
4109         $canv2 lower $t
4110     }
4111 }
4112
4113 proc unbolden {} {
4114     global boldids
4115
4116     set stillbold {}
4117     foreach id $boldids {
4118         if {![ishighlighted $id]} {
4119             bolden $id mainfont
4120         } else {
4121             lappend stillbold $id
4122         }
4123     }
4124     set boldids $stillbold
4125 }
4126
4127 proc addvhighlight {n} {
4128     global hlview viewcomplete curview vhl_done commitidx
4129
4130     if {[info exists hlview]} {
4131         delvhighlight
4132     }
4133     set hlview $n
4134     if {$n != $curview && ![info exists viewcomplete($n)]} {
4135         start_rev_list $n
4136     }
4137     set vhl_done $commitidx($hlview)
4138     if {$vhl_done > 0} {
4139         drawvisible
4140     }
4141 }
4142
4143 proc delvhighlight {} {
4144     global hlview vhighlights
4145
4146     if {![info exists hlview]} return
4147     unset hlview
4148     catch {unset vhighlights}
4149     unbolden
4150 }
4151
4152 proc vhighlightmore {} {
4153     global hlview vhl_done commitidx vhighlights curview
4154
4155     set max $commitidx($hlview)
4156     set vr [visiblerows]
4157     set r0 [lindex $vr 0]
4158     set r1 [lindex $vr 1]
4159     for {set i $vhl_done} {$i < $max} {incr i} {
4160         set id [commitonrow $i $hlview]
4161         if {[commitinview $id $curview]} {
4162             set row [rowofcommit $id]
4163             if {$r0 <= $row && $row <= $r1} {
4164                 if {![highlighted $row]} {
4165                     bolden $id mainfontbold
4166                 }
4167                 set vhighlights($id) 1
4168             }
4169         }
4170     }
4171     set vhl_done $max
4172     return 0
4173 }
4174
4175 proc askvhighlight {row id} {
4176     global hlview vhighlights iddrawn
4177
4178     if {[commitinview $id $hlview]} {
4179         if {[info exists iddrawn($id)] && ![ishighlighted $id]} {
4180             bolden $id mainfontbold
4181         }
4182         set vhighlights($id) 1
4183     } else {
4184         set vhighlights($id) 0
4185     }
4186 }
4187
4188 proc hfiles_change {} {
4189     global highlight_files filehighlight fhighlights fh_serial
4190     global highlight_paths
4191
4192     if {[info exists filehighlight]} {
4193         # delete previous highlights
4194         catch {close $filehighlight}
4195         unset filehighlight
4196         catch {unset fhighlights}
4197         unbolden
4198         unhighlight_filelist
4199     }
4200     set highlight_paths {}
4201     after cancel do_file_hl $fh_serial
4202     incr fh_serial
4203     if {$highlight_files ne {}} {
4204         after 300 do_file_hl $fh_serial
4205     }
4206 }
4207
4208 proc gdttype_change {name ix op} {
4209     global gdttype highlight_files findstring findpattern
4210
4211     stopfinding
4212     if {$findstring ne {}} {
4213         if {$gdttype eq [mc "containing:"]} {
4214             if {$highlight_files ne {}} {
4215                 set highlight_files {}
4216                 hfiles_change
4217             }
4218             findcom_change
4219         } else {
4220             if {$findpattern ne {}} {
4221                 set findpattern {}
4222                 findcom_change
4223             }
4224             set highlight_files $findstring
4225             hfiles_change
4226         }
4227         drawvisible
4228     }
4229     # enable/disable findtype/findloc menus too
4230 }
4231
4232 proc find_change {name ix op} {
4233     global gdttype findstring highlight_files
4234
4235     stopfinding
4236     if {$gdttype eq [mc "containing:"]} {
4237         findcom_change
4238     } else {
4239         if {$highlight_files ne $findstring} {
4240             set highlight_files $findstring
4241             hfiles_change
4242         }
4243     }
4244     drawvisible
4245 }
4246
4247 proc findcom_change args {
4248     global nhighlights boldnameids
4249     global findpattern findtype findstring gdttype
4250
4251     stopfinding
4252     # delete previous highlights, if any
4253     foreach id $boldnameids {
4254         bolden_name $id mainfont
4255     }
4256     set boldnameids {}
4257     catch {unset nhighlights}
4258     unbolden
4259     unmarkmatches
4260     if {$gdttype ne [mc "containing:"] || $findstring eq {}} {
4261         set findpattern {}
4262     } elseif {$findtype eq [mc "Regexp"]} {
4263         set findpattern $findstring
4264     } else {
4265         set e [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} \
4266                    $findstring]
4267         set findpattern "*$e*"
4268     }
4269 }
4270
4271 proc makepatterns {l} {
4272     set ret {}
4273     foreach e $l {
4274         set ee [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} $e]
4275         if {[string index $ee end] eq "/"} {
4276             lappend ret "$ee*"
4277         } else {
4278             lappend ret $ee
4279             lappend ret "$ee/*"
4280         }
4281     }
4282     return $ret
4283 }
4284
4285 proc do_file_hl {serial} {
4286     global highlight_files filehighlight highlight_paths gdttype fhl_list
4287
4288     if {$gdttype eq [mc "touching paths:"]} {
4289         if {[catch {set paths [shellsplit $highlight_files]}]} return
4290         set highlight_paths [makepatterns $paths]
4291         highlight_filelist
4292         set gdtargs [concat -- $paths]
4293     } elseif {$gdttype eq [mc "adding/removing string:"]} {
4294         set gdtargs [list "-S$highlight_files"]
4295     } else {
4296         # must be "containing:", i.e. we're searching commit info
4297         return
4298     }
4299     set cmd [concat | git diff-tree -r -s --stdin $gdtargs]
4300     set filehighlight [open $cmd r+]
4301     fconfigure $filehighlight -blocking 0
4302     filerun $filehighlight readfhighlight
4303     set fhl_list {}
4304     drawvisible
4305     flushhighlights
4306 }
4307
4308 proc flushhighlights {} {
4309     global filehighlight fhl_list
4310
4311     if {[info exists filehighlight]} {
4312         lappend fhl_list {}
4313         puts $filehighlight ""
4314         flush $filehighlight
4315     }
4316 }
4317
4318 proc askfilehighlight {row id} {
4319     global filehighlight fhighlights fhl_list
4320
4321     lappend fhl_list $id
4322     set fhighlights($id) -1
4323     puts $filehighlight $id
4324 }
4325
4326 proc readfhighlight {} {
4327     global filehighlight fhighlights curview iddrawn
4328     global fhl_list find_dirn
4329
4330     if {![info exists filehighlight]} {
4331         return 0
4332     }
4333     set nr 0
4334     while {[incr nr] <= 100 && [gets $filehighlight line] >= 0} {
4335         set line [string trim $line]
4336         set i [lsearch -exact $fhl_list $line]
4337         if {$i < 0} continue
4338         for {set j 0} {$j < $i} {incr j} {
4339             set id [lindex $fhl_list $j]
4340             set fhighlights($id) 0
4341         }
4342         set fhl_list [lrange $fhl_list [expr {$i+1}] end]
4343         if {$line eq {}} continue
4344         if {![commitinview $line $curview]} continue
4345         if {[info exists iddrawn($line)] && ![ishighlighted $line]} {
4346             bolden $line mainfontbold
4347         }
4348         set fhighlights($line) 1
4349     }
4350     if {[eof $filehighlight]} {
4351         # strange...
4352         puts "oops, git diff-tree died"
4353         catch {close $filehighlight}
4354         unset filehighlight
4355         return 0
4356     }
4357     if {[info exists find_dirn]} {
4358         run findmore
4359     }
4360     return 1
4361 }
4362
4363 proc doesmatch {f} {
4364     global findtype findpattern
4365
4366     if {$findtype eq [mc "Regexp"]} {
4367         return [regexp $findpattern $f]
4368     } elseif {$findtype eq [mc "IgnCase"]} {
4369         return [string match -nocase $findpattern $f]
4370     } else {
4371         return [string match $findpattern $f]
4372     }
4373 }
4374
4375 proc askfindhighlight {row id} {
4376     global nhighlights commitinfo iddrawn
4377     global findloc
4378     global markingmatches
4379
4380     if {![info exists commitinfo($id)]} {
4381         getcommit $id
4382     }
4383     set info $commitinfo($id)
4384     set isbold 0
4385     set fldtypes [list [mc Headline] [mc Author] [mc Date] [mc Committer] [mc CDate] [mc Comments]]
4386     foreach f $info ty $fldtypes {
4387         if {($findloc eq [mc "All fields"] || $findloc eq $ty) &&
4388             [doesmatch $f]} {
4389             if {$ty eq [mc "Author"]} {
4390                 set isbold 2
4391                 break
4392             }
4393             set isbold 1
4394         }
4395     }
4396     if {$isbold && [info exists iddrawn($id)]} {
4397         if {![ishighlighted $id]} {
4398             bolden $id mainfontbold
4399             if {$isbold > 1} {
4400                 bolden_name $id mainfontbold
4401             }
4402         }
4403         if {$markingmatches} {
4404             markrowmatches $row $id
4405         }
4406     }
4407     set nhighlights($id) $isbold
4408 }
4409
4410 proc markrowmatches {row id} {
4411     global canv canv2 linehtag linentag commitinfo findloc
4412
4413     set headline [lindex $commitinfo($id) 0]
4414     set author [lindex $commitinfo($id) 1]
4415     $canv delete match$row
4416     $canv2 delete match$row
4417     if {$findloc eq [mc "All fields"] || $findloc eq [mc "Headline"]} {
4418         set m [findmatches $headline]
4419         if {$m ne {}} {
4420             markmatches $canv $row $headline $linehtag($id) $m \
4421                 [$canv itemcget $linehtag($id) -font] $row
4422         }
4423     }
4424     if {$findloc eq [mc "All fields"] || $findloc eq [mc "Author"]} {
4425         set m [findmatches $author]
4426         if {$m ne {}} {
4427             markmatches $canv2 $row $author $linentag($id) $m \
4428                 [$canv2 itemcget $linentag($id) -font] $row
4429         }
4430     }
4431 }
4432
4433 proc vrel_change {name ix op} {
4434     global highlight_related
4435
4436     rhighlight_none
4437     if {$highlight_related ne [mc "None"]} {
4438         run drawvisible
4439     }
4440 }
4441
4442 # prepare for testing whether commits are descendents or ancestors of a
4443 proc rhighlight_sel {a} {
4444     global descendent desc_todo ancestor anc_todo
4445     global highlight_related
4446
4447     catch {unset descendent}
4448     set desc_todo [list $a]
4449     catch {unset ancestor}
4450     set anc_todo [list $a]
4451     if {$highlight_related ne [mc "None"]} {
4452         rhighlight_none
4453         run drawvisible
4454     }
4455 }
4456
4457 proc rhighlight_none {} {
4458     global rhighlights
4459
4460     catch {unset rhighlights}
4461     unbolden
4462 }
4463
4464 proc is_descendent {a} {
4465     global curview children descendent desc_todo
4466
4467     set v $curview
4468     set la [rowofcommit $a]
4469     set todo $desc_todo
4470     set leftover {}
4471     set done 0
4472     for {set i 0} {$i < [llength $todo]} {incr i} {
4473         set do [lindex $todo $i]
4474         if {[rowofcommit $do] < $la} {
4475             lappend leftover $do
4476             continue
4477         }
4478         foreach nk $children($v,$do) {
4479             if {![info exists descendent($nk)]} {
4480                 set descendent($nk) 1
4481                 lappend todo $nk
4482                 if {$nk eq $a} {
4483                     set done 1
4484                 }
4485             }
4486         }
4487         if {$done} {
4488             set desc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
4489             return
4490         }
4491     }
4492     set descendent($a) 0
4493     set desc_todo $leftover
4494 }
4495
4496 proc is_ancestor {a} {
4497     global curview parents ancestor anc_todo
4498
4499     set v $curview
4500     set la [rowofcommit $a]
4501     set todo $anc_todo
4502     set leftover {}
4503     set done 0
4504     for {set i 0} {$i < [llength $todo]} {incr i} {
4505         set do [lindex $todo $i]
4506         if {![commitinview $do $v] || [rowofcommit $do] > $la} {
4507             lappend leftover $do
4508             continue
4509         }
4510         foreach np $parents($v,$do) {
4511             if {![info exists ancestor($np)]} {
4512                 set ancestor($np) 1
4513                 lappend todo $np
4514                 if {$np eq $a} {
4515                     set done 1
4516                 }
4517             }
4518         }
4519         if {$done} {
4520             set anc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
4521             return
4522         }
4523     }
4524     set ancestor($a) 0
4525     set anc_todo $leftover
4526 }
4527
4528 proc askrelhighlight {row id} {
4529     global descendent highlight_related iddrawn rhighlights
4530     global selectedline ancestor
4531
4532     if {$selectedline eq {}} return
4533     set isbold 0
4534     if {$highlight_related eq [mc "Descendant"] ||
4535         $highlight_related eq [mc "Not descendant"]} {
4536         if {![info exists descendent($id)]} {
4537             is_descendent $id
4538         }
4539         if {$descendent($id) == ($highlight_related eq [mc "Descendant"])} {
4540             set isbold 1
4541         }
4542     } elseif {$highlight_related eq [mc "Ancestor"] ||
4543               $highlight_related eq [mc "Not ancestor"]} {
4544         if {![info exists ancestor($id)]} {
4545             is_ancestor $id
4546         }
4547         if {$ancestor($id) == ($highlight_related eq [mc "Ancestor"])} {
4548             set isbold 1
4549         }
4550     }
4551     if {[info exists iddrawn($id)]} {
4552         if {$isbold && ![ishighlighted $id]} {
4553             bolden $id mainfontbold
4554         }
4555     }
4556     set rhighlights($id) $isbold
4557 }
4558
4559 # Graph layout functions
4560
4561 proc shortids {ids} {
4562     set res {}
4563     foreach id $ids {
4564         if {[llength $id] > 1} {
4565             lappend res [shortids $id]
4566         } elseif {[regexp {^[0-9a-f]{40}$} $id]} {
4567             lappend res [string range $id 0 7]
4568         } else {
4569             lappend res $id
4570         }
4571     }
4572     return $res
4573 }
4574
4575 proc ntimes {n o} {
4576     set ret {}
4577     set o [list $o]
4578     for {set mask 1} {$mask <= $n} {incr mask $mask} {
4579         if {($n & $mask) != 0} {
4580             set ret [concat $ret $o]
4581         }
4582         set o [concat $o $o]
4583     }
4584     return $ret
4585 }
4586
4587 proc ordertoken {id} {
4588     global ordertok curview varcid varcstart varctok curview parents children
4589     global nullid nullid2
4590
4591     if {[info exists ordertok($id)]} {
4592         return $ordertok($id)
4593     }
4594     set origid $id
4595     set todo {}
4596     while {1} {
4597         if {[info exists varcid($curview,$id)]} {
4598             set a $varcid($curview,$id)
4599             set p [lindex $varcstart($curview) $a]
4600         } else {
4601             set p [lindex $children($curview,$id) 0]
4602         }
4603         if {[info exists ordertok($p)]} {
4604             set tok $ordertok($p)
4605             break
4606         }
4607         set id [first_real_child $curview,$p]
4608         if {$id eq {}} {
4609             # it's a root
4610             set tok [lindex $varctok($curview) $varcid($curview,$p)]
4611             break
4612         }
4613         if {[llength $parents($curview,$id)] == 1} {
4614             lappend todo [list $p {}]
4615         } else {
4616             set j [lsearch -exact $parents($curview,$id) $p]
4617             if {$j < 0} {
4618                 puts "oops didn't find [shortids $p] in parents of [shortids $id]"
4619             }
4620             lappend todo [list $p [strrep $j]]
4621         }
4622     }
4623     for {set i [llength $todo]} {[incr i -1] >= 0} {} {
4624         set p [lindex $todo $i 0]
4625         append tok [lindex $todo $i 1]
4626         set ordertok($p) $tok
4627     }
4628     set ordertok($origid) $tok
4629     return $tok
4630 }
4631
4632 # Work out where id should go in idlist so that order-token
4633 # values increase from left to right
4634 proc idcol {idlist id {i 0}} {
4635     set t [ordertoken $id]
4636     if {$i < 0} {
4637         set i 0
4638     }
4639     if {$i >= [llength $idlist] || $t < [ordertoken [lindex $idlist $i]]} {
4640         if {$i > [llength $idlist]} {
4641             set i [llength $idlist]
4642         }
4643         while {[incr i -1] >= 0 && $t < [ordertoken [lindex $idlist $i]]} {}
4644         incr i
4645     } else {
4646         if {$t > [ordertoken [lindex $idlist $i]]} {
4647             while {[incr i] < [llength $idlist] &&
4648                    $t >= [ordertoken [lindex $idlist $i]]} {}
4649         }
4650     }
4651     return $i
4652 }
4653
4654 proc initlayout {} {
4655     global rowidlist rowisopt rowfinal displayorder parentlist
4656     global numcommits canvxmax canv
4657     global nextcolor
4658     global colormap rowtextx
4659
4660     set numcommits 0
4661     set displayorder {}
4662     set parentlist {}
4663     set nextcolor 0
4664     set rowidlist {}
4665     set rowisopt {}
4666     set rowfinal {}
4667     set canvxmax [$canv cget -width]
4668     catch {unset colormap}
4669     catch {unset rowtextx}
4670     setcanvscroll
4671 }
4672
4673 proc setcanvscroll {} {
4674     global canv canv2 canv3 numcommits linespc canvxmax canvy0
4675     global lastscrollset lastscrollrows
4676
4677     set ymax [expr {$canvy0 + ($numcommits - 0.5) * $linespc + 2}]
4678     $canv conf -scrollregion [list 0 0 $canvxmax $ymax]
4679     $canv2 conf -scrollregion [list 0 0 0 $ymax]
4680     $canv3 conf -scrollregion [list 0 0 0 $ymax]
4681     set lastscrollset [clock clicks -milliseconds]
4682     set lastscrollrows $numcommits
4683 }
4684
4685 proc visiblerows {} {
4686     global canv numcommits linespc
4687
4688     set ymax [lindex [$canv cget -scrollregion] 3]
4689     if {$ymax eq {} || $ymax == 0} return
4690     set f [$canv yview]
4691     set y0 [expr {int([lindex $f 0] * $ymax)}]
4692     set r0 [expr {int(($y0 - 3) / $linespc) - 1}]
4693     if {$r0 < 0} {
4694         set r0 0
4695     }
4696     set y1 [expr {int([lindex $f 1] * $ymax)}]
4697     set r1 [expr {int(($y1 - 3) / $linespc) + 1}]
4698     if {$r1 >= $numcommits} {
4699         set r1 [expr {$numcommits - 1}]
4700     }
4701     return [list $r0 $r1]
4702 }
4703
4704 proc layoutmore {} {
4705     global commitidx viewcomplete curview
4706     global numcommits pending_select curview
4707     global lastscrollset lastscrollrows
4708
4709     if {$lastscrollrows < 100 || $viewcomplete($curview) ||
4710         [clock clicks -milliseconds] - $lastscrollset > 500} {
4711         setcanvscroll
4712     }
4713     if {[info exists pending_select] &&
4714         [commitinview $pending_select $curview]} {
4715         update
4716         selectline [rowofcommit $pending_select] 1
4717     }
4718     drawvisible
4719 }
4720
4721 # With path limiting, we mightn't get the actual HEAD commit,
4722 # so ask git rev-list what is the first ancestor of HEAD that
4723 # touches a file in the path limit.
4724 proc get_viewmainhead {view} {
4725     global viewmainheadid vfilelimit viewinstances mainheadid
4726
4727     catch {
4728         set rfd [open [concat | git rev-list -1 $mainheadid \
4729                            -- $vfilelimit($view)] r]
4730         set j [reg_instance $rfd]
4731         lappend viewinstances($view) $j
4732         fconfigure $rfd -blocking 0
4733         filerun $rfd [list getviewhead $rfd $j $view]
4734         set viewmainheadid($curview) {}
4735     }
4736 }
4737
4738 # git rev-list should give us just 1 line to use as viewmainheadid($view)
4739 proc getviewhead {fd inst view} {
4740     global viewmainheadid commfd curview viewinstances showlocalchanges
4741
4742     set id {}
4743     if {[gets $fd line] < 0} {
4744         if {![eof $fd]} {
4745             return 1
4746         }
4747     } elseif {[string length $line] == 40 && [string is xdigit $line]} {
4748         set id $line
4749     }
4750     set viewmainheadid($view) $id
4751     close $fd
4752     unset commfd($inst)
4753     set i [lsearch -exact $viewinstances($view) $inst]
4754     if {$i >= 0} {
4755         set viewinstances($view) [lreplace $viewinstances($view) $i $i]
4756     }
4757     if {$showlocalchanges && $id ne {} && $view == $curview} {
4758         doshowlocalchanges
4759     }
4760     return 0
4761 }
4762
4763 proc doshowlocalchanges {} {
4764     global curview viewmainheadid
4765
4766     if {$viewmainheadid($curview) eq {}} return
4767     if {[commitinview $viewmainheadid($curview) $curview]} {
4768         dodiffindex
4769     } else {
4770         interestedin $viewmainheadid($curview) dodiffindex
4771     }
4772 }
4773
4774 proc dohidelocalchanges {} {
4775     global nullid nullid2 lserial curview
4776
4777     if {[commitinview $nullid $curview]} {
4778         removefakerow $nullid
4779     }
4780     if {[commitinview $nullid2 $curview]} {
4781         removefakerow $nullid2
4782     }
4783     incr lserial
4784 }
4785
4786 # spawn off a process to do git diff-index --cached HEAD
4787 proc dodiffindex {} {
4788     global lserial showlocalchanges vfilelimit curview
4789     global isworktree
4790
4791     if {!$showlocalchanges || !$isworktree} return
4792     incr lserial
4793     set cmd "|git diff-index --cached HEAD"
4794     if {$vfilelimit($curview) ne {}} {
4795         set cmd [concat $cmd -- $vfilelimit($curview)]
4796     }
4797     set fd [open $cmd r]
4798     fconfigure $fd -blocking 0
4799     set i [reg_instance $fd]
4800     filerun $fd [list readdiffindex $fd $lserial $i]
4801 }
4802
4803 proc readdiffindex {fd serial inst} {
4804     global viewmainheadid nullid nullid2 curview commitinfo commitdata lserial
4805     global vfilelimit
4806
4807     set isdiff 1
4808     if {[gets $fd line] < 0} {
4809         if {![eof $fd]} {
4810             return 1
4811         }
4812         set isdiff 0
4813     }
4814     # we only need to see one line and we don't really care what it says...
4815     stop_instance $inst
4816
4817     if {$serial != $lserial} {
4818         return 0
4819     }
4820
4821     # now see if there are any local changes not checked in to the index
4822     set cmd "|git diff-files"
4823     if {$vfilelimit($curview) ne {}} {
4824         set cmd [concat $cmd -- $vfilelimit($curview)]
4825     }
4826     set fd [open $cmd r]
4827     fconfigure $fd -blocking 0
4828     set i [reg_instance $fd]
4829     filerun $fd [list readdifffiles $fd $serial $i]
4830
4831     if {$isdiff && ![commitinview $nullid2 $curview]} {
4832         # add the line for the changes in the index to the graph
4833         set hl [mc "Local changes checked in to index but not committed"]
4834         set commitinfo($nullid2) [list  $hl {} {} {} {} "    $hl\n"]
4835         set commitdata($nullid2) "\n    $hl\n"
4836         if {[commitinview $nullid $curview]} {
4837             removefakerow $nullid
4838         }
4839         insertfakerow $nullid2 $viewmainheadid($curview)
4840     } elseif {!$isdiff && [commitinview $nullid2 $curview]} {
4841         if {[commitinview $nullid $curview]} {
4842             removefakerow $nullid
4843         }
4844         removefakerow $nullid2
4845     }
4846     return 0
4847 }
4848
4849 proc readdifffiles {fd serial inst} {
4850     global viewmainheadid nullid nullid2 curview
4851     global commitinfo commitdata lserial
4852
4853     set isdiff 1
4854     if {[gets $fd line] < 0} {
4855         if {![eof $fd]} {
4856             return 1
4857         }
4858         set isdiff 0
4859     }
4860     # we only need to see one line and we don't really care what it says...
4861     stop_instance $inst
4862
4863     if {$serial != $lserial} {
4864         return 0
4865     }
4866
4867     if {$isdiff && ![commitinview $nullid $curview]} {
4868         # add the line for the local diff to the graph
4869         set hl [mc "Local uncommitted changes, not checked in to index"]
4870         set commitinfo($nullid) [list  $hl {} {} {} {} "    $hl\n"]
4871         set commitdata($nullid) "\n    $hl\n"
4872         if {[commitinview $nullid2 $curview]} {
4873             set p $nullid2
4874         } else {
4875             set p $viewmainheadid($curview)
4876         }
4877         insertfakerow $nullid $p
4878     } elseif {!$isdiff && [commitinview $nullid $curview]} {
4879         removefakerow $nullid
4880     }
4881     return 0
4882 }
4883
4884 proc nextuse {id row} {
4885     global curview children
4886
4887     if {[info exists children($curview,$id)]} {
4888         foreach kid $children($curview,$id) {
4889             if {![commitinview $kid $curview]} {
4890                 return -1
4891             }
4892             if {[rowofcommit $kid] > $row} {
4893                 return [rowofcommit $kid]
4894             }
4895         }
4896     }
4897     if {[commitinview $id $curview]} {
4898         return [rowofcommit $id]
4899     }
4900     return -1
4901 }
4902
4903 proc prevuse {id row} {
4904     global curview children
4905
4906     set ret -1
4907     if {[info exists children($curview,$id)]} {
4908         foreach kid $children($curview,$id) {
4909             if {![commitinview $kid $curview]} break
4910             if {[rowofcommit $kid] < $row} {
4911                 set ret [rowofcommit $kid]
4912             }
4913         }
4914     }
4915     return $ret
4916 }
4917
4918 proc make_idlist {row} {
4919     global displayorder parentlist uparrowlen downarrowlen mingaplen
4920     global commitidx curview children
4921
4922     set r [expr {$row - $mingaplen - $downarrowlen - 1}]
4923     if {$r < 0} {
4924         set r 0
4925     }
4926     set ra [expr {$row - $downarrowlen}]
4927     if {$ra < 0} {
4928         set ra 0
4929     }
4930     set rb [expr {$row + $uparrowlen}]
4931     if {$rb > $commitidx($curview)} {
4932         set rb $commitidx($curview)
4933     }
4934     make_disporder $r [expr {$rb + 1}]
4935     set ids {}
4936     for {} {$r < $ra} {incr r} {
4937         set nextid [lindex $displayorder [expr {$r + 1}]]
4938         foreach p [lindex $parentlist $r] {
4939             if {$p eq $nextid} continue
4940             set rn [nextuse $p $r]
4941             if {$rn >= $row &&
4942                 $rn <= $r + $downarrowlen + $mingaplen + $uparrowlen} {
4943                 lappend ids [list [ordertoken $p] $p]
4944             }
4945         }
4946     }
4947     for {} {$r < $row} {incr r} {
4948         set nextid [lindex $displayorder [expr {$r + 1}]]
4949         foreach p [lindex $parentlist $r] {
4950             if {$p eq $nextid} continue
4951             set rn [nextuse $p $r]
4952             if {$rn < 0 || $rn >= $row} {
4953                 lappend ids [list [ordertoken $p] $p]
4954             }
4955         }
4956     }
4957     set id [lindex $displayorder $row]
4958     lappend ids [list [ordertoken $id] $id]
4959     while {$r < $rb} {
4960         foreach p [lindex $parentlist $r] {
4961             set firstkid [lindex $children($curview,$p) 0]
4962             if {[rowofcommit $firstkid] < $row} {
4963                 lappend ids [list [ordertoken $p] $p]
4964             }
4965         }
4966         incr r
4967         set id [lindex $displayorder $r]
4968         if {$id ne {}} {
4969             set firstkid [lindex $children($curview,$id) 0]
4970             if {$firstkid ne {} && [rowofcommit $firstkid] < $row} {
4971                 lappend ids [list [ordertoken $id] $id]
4972             }
4973         }
4974     }
4975     set idlist {}
4976     foreach idx [lsort -unique $ids] {
4977         lappend idlist [lindex $idx 1]
4978     }
4979     return $idlist
4980 }
4981
4982 proc rowsequal {a b} {
4983     while {[set i [lsearch -exact $a {}]] >= 0} {
4984         set a [lreplace $a $i $i]
4985     }
4986     while {[set i [lsearch -exact $b {}]] >= 0} {
4987         set b [lreplace $b $i $i]
4988     }
4989     return [expr {$a eq $b}]
4990 }
4991
4992 proc makeupline {id row rend col} {
4993     global rowidlist uparrowlen downarrowlen mingaplen
4994
4995     for {set r $rend} {1} {set r $rstart} {
4996         set rstart [prevuse $id $r]
4997         if {$rstart < 0} return
4998         if {$rstart < $row} break
4999     }
5000     if {$rstart + $uparrowlen + $mingaplen + $downarrowlen < $rend} {
5001         set rstart [expr {$rend - $uparrowlen - 1}]
5002     }
5003     for {set r $rstart} {[incr r] <= $row} {} {
5004         set idlist [lindex $rowidlist $r]
5005         if {$idlist ne {} && [lsearch -exact $idlist $id] < 0} {
5006             set col [idcol $idlist $id $col]
5007             lset rowidlist $r [linsert $idlist $col $id]
5008             changedrow $r
5009         }
5010     }
5011 }
5012
5013 proc layoutrows {row endrow} {
5014     global rowidlist rowisopt rowfinal displayorder
5015     global uparrowlen downarrowlen maxwidth mingaplen
5016     global children parentlist
5017     global commitidx viewcomplete curview
5018
5019     make_disporder [expr {$row - 1}] [expr {$endrow + $uparrowlen}]
5020     set idlist {}
5021     if {$row > 0} {
5022         set rm1 [expr {$row - 1}]
5023         foreach id [lindex $rowidlist $rm1] {
5024             if {$id ne {}} {
5025                 lappend idlist $id
5026             }
5027         }
5028         set final [lindex $rowfinal $rm1]
5029     }
5030     for {} {$row < $endrow} {incr row} {
5031         set rm1 [expr {$row - 1}]
5032         if {$rm1 < 0 || $idlist eq {}} {
5033             set idlist [make_idlist $row]
5034             set final 1
5035         } else {
5036             set id [lindex $displayorder $rm1]
5037             set col [lsearch -exact $idlist $id]
5038             set idlist [lreplace $idlist $col $col]
5039             foreach p [lindex $parentlist $rm1] {
5040                 if {[lsearch -exact $idlist $p] < 0} {
5041                     set col [idcol $idlist $p $col]
5042                     set idlist [linsert $idlist $col $p]
5043                     # if not the first child, we have to insert a line going up
5044                     if {$id ne [lindex $children($curview,$p) 0]} {
5045                         makeupline $p $rm1 $row $col
5046                     }
5047                 }
5048             }
5049             set id [lindex $displayorder $row]
5050             if {$row > $downarrowlen} {
5051                 set termrow [expr {$row - $downarrowlen - 1}]
5052                 foreach p [lindex $parentlist $termrow] {
5053                     set i [lsearch -exact $idlist $p]
5054                     if {$i < 0} continue
5055                     set nr [nextuse $p $termrow]
5056                     if {$nr < 0 || $nr >= $row + $mingaplen + $uparrowlen} {
5057                         set idlist [lreplace $idlist $i $i]
5058                     }
5059                 }
5060             }
5061             set col [lsearch -exact $idlist $id]
5062             if {$col < 0} {
5063                 set col [idcol $idlist $id]
5064                 set idlist [linsert $idlist $col $id]
5065                 if {$children($curview,$id) ne {}} {
5066                     makeupline $id $rm1 $row $col
5067                 }
5068             }
5069             set r [expr {$row + $uparrowlen - 1}]
5070             if {$r < $commitidx($curview)} {
5071                 set x $col
5072                 foreach p [lindex $parentlist $r] {
5073                     if {[lsearch -exact $idlist $p] >= 0} continue
5074                     set fk [lindex $children($curview,$p) 0]
5075                     if {[rowofcommit $fk] < $row} {
5076                         set x [idcol $idlist $p $x]
5077                         set idlist [linsert $idlist $x $p]
5078                     }
5079                 }
5080                 if {[incr r] < $commitidx($curview)} {
5081                     set p [lindex $displayorder $r]
5082                     if {[lsearch -exact $idlist $p] < 0} {
5083                         set fk [lindex $children($curview,$p) 0]
5084                         if {$fk ne {} && [rowofcommit $fk] < $row} {
5085                             set x [idcol $idlist $p $x]
5086                             set idlist [linsert $idlist $x $p]
5087                         }
5088                     }
5089                 }
5090             }
5091         }
5092         if {$final && !$viewcomplete($curview) &&
5093             $row + $uparrowlen + $mingaplen + $downarrowlen
5094                 >= $commitidx($curview)} {
5095             set final 0
5096         }
5097         set l [llength $rowidlist]
5098         if {$row == $l} {
5099             lappend rowidlist $idlist
5100             lappend rowisopt 0
5101             lappend rowfinal $final
5102         } elseif {$row < $l} {
5103             if {![rowsequal $idlist [lindex $rowidlist $row]]} {
5104                 lset rowidlist $row $idlist
5105                 changedrow $row
5106             }
5107             lset rowfinal $row $final
5108         } else {
5109             set pad [ntimes [expr {$row - $l}] {}]
5110             set rowidlist [concat $rowidlist $pad]
5111             lappend rowidlist $idlist
5112             set rowfinal [concat $rowfinal $pad]
5113             lappend rowfinal $final
5114             set rowisopt [concat $rowisopt [ntimes [expr {$row - $l + 1}] 0]]
5115         }
5116     }
5117     return $row
5118 }
5119
5120 proc changedrow {row} {
5121     global displayorder iddrawn rowisopt need_redisplay
5122
5123     set l [llength $rowisopt]
5124     if {$row < $l} {
5125         lset rowisopt $row 0
5126         if {$row + 1 < $l} {
5127             lset rowisopt [expr {$row + 1}] 0
5128             if {$row + 2 < $l} {
5129                 lset rowisopt [expr {$row + 2}] 0
5130             }
5131         }
5132     }
5133     set id [lindex $displayorder $row]
5134     if {[info exists iddrawn($id)]} {
5135         set need_redisplay 1
5136     }
5137 }
5138
5139 proc insert_pad {row col npad} {
5140     global rowidlist
5141
5142     set pad [ntimes $npad {}]
5143     set idlist [lindex $rowidlist $row]
5144     set bef [lrange $idlist 0 [expr {$col - 1}]]
5145     set aft [lrange $idlist $col end]
5146     set i [lsearch -exact $aft {}]
5147     if {$i > 0} {
5148         set aft [lreplace $aft $i $i]
5149     }
5150     lset rowidlist $row [concat $bef $pad $aft]
5151     changedrow $row
5152 }
5153
5154 proc optimize_rows {row col endrow} {
5155     global rowidlist rowisopt displayorder curview children
5156
5157     if {$row < 1} {
5158         set row 1
5159     }
5160     for {} {$row < $endrow} {incr row; set col 0} {
5161         if {[lindex $rowisopt $row]} continue
5162         set haspad 0
5163         set y0 [expr {$row - 1}]
5164         set ym [expr {$row - 2}]
5165         set idlist [lindex $rowidlist $row]
5166         set previdlist [lindex $rowidlist $y0]
5167         if {$idlist eq {} || $previdlist eq {}} continue
5168         if {$ym >= 0} {
5169             set pprevidlist [lindex $rowidlist $ym]
5170             if {$pprevidlist eq {}} continue
5171         } else {
5172             set pprevidlist {}
5173         }
5174         set x0 -1
5175         set xm -1
5176         for {} {$col < [llength $idlist]} {incr col} {
5177             set id [lindex $idlist $col]
5178             if {[lindex $previdlist $col] eq $id} continue
5179             if {$id eq {}} {
5180                 set haspad 1
5181                 continue
5182             }
5183             set x0 [lsearch -exact $previdlist $id]
5184             if {$x0 < 0} continue
5185             set z [expr {$x0 - $col}]
5186             set isarrow 0
5187             set z0 {}
5188             if {$ym >= 0} {
5189                 set xm [lsearch -exact $pprevidlist $id]
5190                 if {$xm >= 0} {
5191                     set z0 [expr {$xm - $x0}]
5192                 }
5193             }
5194             if {$z0 eq {}} {
5195                 # if row y0 is the first child of $id then it's not an arrow
5196                 if {[lindex $children($curview,$id) 0] ne
5197                     [lindex $displayorder $y0]} {
5198                     set isarrow 1
5199                 }
5200             }
5201             if {!$isarrow && $id ne [lindex $displayorder $row] &&
5202                 [lsearch -exact [lindex $rowidlist [expr {$row+1}]] $id] < 0} {
5203                 set isarrow 1
5204             }
5205             # Looking at lines from this row to the previous row,
5206             # make them go straight up if they end in an arrow on
5207             # the previous row; otherwise make them go straight up
5208             # or at 45 degrees.
5209             if {$z < -1 || ($z < 0 && $isarrow)} {
5210                 # Line currently goes left too much;
5211                 # insert pads in the previous row, then optimize it
5212                 set npad [expr {-1 - $z + $isarrow}]
5213                 insert_pad $y0 $x0 $npad
5214                 if {$y0 > 0} {
5215                     optimize_rows $y0 $x0 $row
5216                 }
5217                 set previdlist [lindex $rowidlist $y0]
5218                 set x0 [lsearch -exact $previdlist $id]
5219                 set z [expr {$x0 - $col}]
5220                 if {$z0 ne {}} {
5221                     set pprevidlist [lindex $rowidlist $ym]
5222                     set xm [lsearch -exact $pprevidlist $id]
5223                     set z0 [expr {$xm - $x0}]
5224                 }
5225             } elseif {$z > 1 || ($z > 0 && $isarrow)} {
5226                 # Line currently goes right too much;
5227                 # insert pads in this line
5228                 set npad [expr {$z - 1 + $isarrow}]
5229                 insert_pad $row $col $npad
5230                 set idlist [lindex $rowidlist $row]
5231                 incr col $npad
5232                 set z [expr {$x0 - $col}]
5233                 set haspad 1
5234             }
5235             if {$z0 eq {} && !$isarrow && $ym >= 0} {
5236                 # this line links to its first child on row $row-2
5237                 set id [lindex $displayorder $ym]
5238                 set xc [lsearch -exact $pprevidlist $id]
5239                 if {$xc >= 0} {
5240                     set z0 [expr {$xc - $x0}]
5241                 }
5242             }
5243             # avoid lines jigging left then immediately right
5244             if {$z0 ne {} && $z < 0 && $z0 > 0} {
5245                 insert_pad $y0 $x0 1
5246                 incr x0
5247                 optimize_rows $y0 $x0 $row
5248                 set previdlist [lindex $rowidlist $y0]
5249             }
5250         }
5251         if {!$haspad} {
5252             # Find the first column that doesn't have a line going right
5253             for {set col [llength $idlist]} {[incr col -1] >= 0} {} {
5254                 set id [lindex $idlist $col]
5255                 if {$id eq {}} break
5256                 set x0 [lsearch -exact $previdlist $id]
5257                 if {$x0 < 0} {
5258                     # check if this is the link to the first child
5259                     set kid [lindex $displayorder $y0]
5260                     if {[lindex $children($curview,$id) 0] eq $kid} {
5261                         # it is, work out offset to child
5262                         set x0 [lsearch -exact $previdlist $kid]
5263                     }
5264                 }
5265                 if {$x0 <= $col} break
5266             }
5267             # Insert a pad at that column as long as it has a line and
5268             # isn't the last column
5269             if {$x0 >= 0 && [incr col] < [llength $idlist]} {
5270                 set idlist [linsert $idlist $col {}]
5271                 lset rowidlist $row $idlist
5272                 changedrow $row
5273             }
5274         }
5275     }
5276 }
5277
5278 proc xc {row col} {
5279     global canvx0 linespc
5280     return [expr {$canvx0 + $col * $linespc}]
5281 }
5282
5283 proc yc {row} {
5284     global canvy0 linespc
5285     return [expr {$canvy0 + $row * $linespc}]
5286 }
5287
5288 proc linewidth {id} {
5289     global thickerline lthickness
5290
5291     set wid $lthickness
5292     if {[info exists thickerline] && $id eq $thickerline} {
5293         set wid [expr {2 * $lthickness}]
5294     }
5295     return $wid
5296 }
5297
5298 proc rowranges {id} {
5299     global curview children uparrowlen downarrowlen
5300     global rowidlist
5301
5302     set kids $children($curview,$id)
5303     if {$kids eq {}} {
5304         return {}
5305     }
5306     set ret {}
5307     lappend kids $id
5308     foreach child $kids {
5309         if {![commitinview $child $curview]} break
5310         set row [rowofcommit $child]
5311         if {![info exists prev]} {
5312             lappend ret [expr {$row + 1}]
5313         } else {
5314             if {$row <= $prevrow} {
5315                 puts "oops children of [shortids $id] out of order [shortids $child] $row <= [shortids $prev] $prevrow"
5316             }
5317             # see if the line extends the whole way from prevrow to row
5318             if {$row > $prevrow + $uparrowlen + $downarrowlen &&
5319                 [lsearch -exact [lindex $rowidlist \
5320                             [expr {int(($row + $prevrow) / 2)}]] $id] < 0} {
5321                 # it doesn't, see where it ends
5322                 set r [expr {$prevrow + $downarrowlen}]
5323                 if {[lsearch -exact [lindex $rowidlist $r] $id] < 0} {
5324                     while {[incr r -1] > $prevrow &&
5325                            [lsearch -exact [lindex $rowidlist $r] $id] < 0} {}
5326                 } else {
5327                     while {[incr r] <= $row &&
5328                            [lsearch -exact [lindex $rowidlist $r] $id] >= 0} {}
5329                     incr r -1
5330                 }
5331                 lappend ret $r
5332                 # see where it starts up again
5333                 set r [expr {$row - $uparrowlen}]
5334                 if {[lsearch -exact [lindex $rowidlist $r] $id] < 0} {
5335                     while {[incr r] < $row &&
5336                            [lsearch -exact [lindex $rowidlist $r] $id] < 0} {}
5337                 } else {
5338                     while {[incr r -1] >= $prevrow &&
5339                            [lsearch -exact [lindex $rowidlist $r] $id] >= 0} {}
5340                     incr r
5341                 }
5342                 lappend ret $r
5343             }
5344         }
5345         if {$child eq $id} {
5346             lappend ret $row
5347         }
5348         set prev $child
5349         set prevrow $row
5350     }
5351     return $ret
5352 }
5353
5354 proc drawlineseg {id row endrow arrowlow} {
5355     global rowidlist displayorder iddrawn linesegs
5356     global canv colormap linespc curview maxlinelen parentlist
5357
5358     set cols [list [lsearch -exact [lindex $rowidlist $row] $id]]
5359     set le [expr {$row + 1}]
5360     set arrowhigh 1
5361     while {1} {
5362         set c [lsearch -exact [lindex $rowidlist $le] $id]
5363         if {$c < 0} {
5364             incr le -1
5365             break
5366         }
5367         lappend cols $c
5368         set x [lindex $displayorder $le]
5369         if {$x eq $id} {
5370             set arrowhigh 0
5371             break
5372         }
5373         if {[info exists iddrawn($x)] || $le == $endrow} {
5374             set c [lsearch -exact [lindex $rowidlist [expr {$le+1}]] $id]
5375             if {$c >= 0} {
5376                 lappend cols $c
5377                 set arrowhigh 0
5378             }
5379             break
5380         }
5381         incr le
5382     }
5383     if {$le <= $row} {
5384         return $row
5385     }
5386
5387     set lines {}
5388     set i 0
5389     set joinhigh 0
5390     if {[info exists linesegs($id)]} {
5391         set lines $linesegs($id)
5392         foreach li $lines {
5393             set r0 [lindex $li 0]
5394             if {$r0 > $row} {
5395                 if {$r0 == $le && [lindex $li 1] - $row <= $maxlinelen} {
5396                     set joinhigh 1
5397                 }
5398                 break
5399             }
5400             incr i
5401         }
5402     }
5403     set joinlow 0
5404     if {$i > 0} {
5405         set li [lindex $lines [expr {$i-1}]]
5406         set r1 [lindex $li 1]
5407         if {$r1 == $row && $le - [lindex $li 0] <= $maxlinelen} {
5408             set joinlow 1
5409         }
5410     }
5411
5412     set x [lindex $cols [expr {$le - $row}]]
5413     set xp [lindex $cols [expr {$le - 1 - $row}]]
5414     set dir [expr {$xp - $x}]
5415     if {$joinhigh} {
5416         set ith [lindex $lines $i 2]
5417         set coords [$canv coords $ith]
5418         set ah [$canv itemcget $ith -arrow]
5419         set arrowhigh [expr {$ah eq "first" || $ah eq "both"}]
5420         set x2 [lindex $cols [expr {$le + 1 - $row}]]
5421         if {$x2 ne {} && $x - $x2 == $dir} {
5422             set coords [lrange $coords 0 end-2]
5423         }
5424     } else {
5425         set coords [list [xc $le $x] [yc $le]]
5426     }
5427     if {$joinlow} {
5428         set itl [lindex $lines [expr {$i-1}] 2]
5429         set al [$canv itemcget $itl -arrow]
5430         set arrowlow [expr {$al eq "last" || $al eq "both"}]
5431     } elseif {$arrowlow} {
5432         if {[lsearch -exact [lindex $rowidlist [expr {$row-1}]] $id] >= 0 ||
5433             [lsearch -exact [lindex $parentlist [expr {$row-1}]] $id] >= 0} {
5434             set arrowlow 0
5435         }
5436     }
5437     set arrow [lindex {none first last both} [expr {$arrowhigh + 2*$arrowlow}]]
5438     for {set y $le} {[incr y -1] > $row} {} {
5439         set x $xp
5440         set xp [lindex $cols [expr {$y - 1 - $row}]]
5441         set ndir [expr {$xp - $x}]
5442         if {$dir != $ndir || $xp < 0} {
5443             lappend coords [xc $y $x] [yc $y]
5444         }
5445         set dir $ndir
5446     }
5447     if {!$joinlow} {
5448         if {$xp < 0} {
5449             # join parent line to first child
5450             set ch [lindex $displayorder $row]
5451             set xc [lsearch -exact [lindex $rowidlist $row] $ch]
5452             if {$xc < 0} {
5453                 puts "oops: drawlineseg: child $ch not on row $row"
5454             } elseif {$xc != $x} {
5455                 if {($arrowhigh && $le == $row + 1) || $dir == 0} {
5456                     set d [expr {int(0.5 * $linespc)}]
5457                     set x1 [xc $row $x]
5458                     if {$xc < $x} {
5459                         set x2 [expr {$x1 - $d}]
5460                     } else {
5461                         set x2 [expr {$x1 + $d}]
5462                     }
5463                     set y2 [yc $row]
5464                     set y1 [expr {$y2 + $d}]
5465                     lappend coords $x1 $y1 $x2 $y2
5466                 } elseif {$xc < $x - 1} {
5467                     lappend coords [xc $row [expr {$x-1}]] [yc $row]
5468                 } elseif {$xc > $x + 1} {
5469                     lappend coords [xc $row [expr {$x+1}]] [yc $row]
5470                 }
5471                 set x $xc
5472             }
5473             lappend coords [xc $row $x] [yc $row]
5474         } else {
5475             set xn [xc $row $xp]
5476             set yn [yc $row]
5477             lappend coords $xn $yn
5478         }
5479         if {!$joinhigh} {
5480             assigncolor $id
5481             set t [$canv create line $coords -width [linewidth $id] \
5482                        -fill $colormap($id) -tags lines.$id -arrow $arrow]
5483             $canv lower $t
5484             bindline $t $id
5485             set lines [linsert $lines $i [list $row $le $t]]
5486         } else {
5487             $canv coords $ith $coords
5488             if {$arrow ne $ah} {
5489                 $canv itemconf $ith -arrow $arrow
5490             }
5491             lset lines $i 0 $row
5492         }
5493     } else {
5494         set xo [lsearch -exact [lindex $rowidlist [expr {$row - 1}]] $id]
5495         set ndir [expr {$xo - $xp}]
5496         set clow [$canv coords $itl]
5497         if {$dir == $ndir} {
5498             set clow [lrange $clow 2 end]
5499         }
5500         set coords [concat $coords $clow]
5501         if {!$joinhigh} {
5502             lset lines [expr {$i-1}] 1 $le
5503         } else {
5504             # coalesce two pieces
5505             $canv delete $ith
5506             set b [lindex $lines [expr {$i-1}] 0]
5507             set e [lindex $lines $i 1]
5508             set lines [lreplace $lines [expr {$i-1}] $i [list $b $e $itl]]
5509         }
5510         $canv coords $itl $coords
5511         if {$arrow ne $al} {
5512             $canv itemconf $itl -arrow $arrow
5513         }
5514     }
5515
5516     set linesegs($id) $lines
5517     return $le
5518 }
5519
5520 proc drawparentlinks {id row} {
5521     global rowidlist canv colormap curview parentlist
5522     global idpos linespc
5523
5524     set rowids [lindex $rowidlist $row]
5525     set col [lsearch -exact $rowids $id]
5526     if {$col < 0} return
5527     set olds [lindex $parentlist $row]
5528     set row2 [expr {$row + 1}]
5529     set x [xc $row $col]
5530     set y [yc $row]
5531     set y2 [yc $row2]
5532     set d [expr {int(0.5 * $linespc)}]
5533     set ymid [expr {$y + $d}]
5534     set ids [lindex $rowidlist $row2]
5535     # rmx = right-most X coord used
5536     set rmx 0
5537     foreach p $olds {
5538         set i [lsearch -exact $ids $p]
5539         if {$i < 0} {
5540             puts "oops, parent $p of $id not in list"
5541             continue
5542         }
5543         set x2 [xc $row2 $i]
5544         if {$x2 > $rmx} {
5545             set rmx $x2
5546         }
5547         set j [lsearch -exact $rowids $p]
5548         if {$j < 0} {
5549             # drawlineseg will do this one for us
5550             continue
5551         }
5552         assigncolor $p
5553         # should handle duplicated parents here...
5554         set coords [list $x $y]
5555         if {$i != $col} {
5556             # if attaching to a vertical segment, draw a smaller
5557             # slant for visual distinctness
5558             if {$i == $j} {
5559                 if {$i < $col} {
5560                     lappend coords [expr {$x2 + $d}] $y $x2 $ymid
5561                 } else {
5562                     lappend coords [expr {$x2 - $d}] $y $x2 $ymid
5563                 }
5564             } elseif {$i < $col && $i < $j} {
5565                 # segment slants towards us already
5566                 lappend coords [xc $row $j] $y
5567             } else {
5568                 if {$i < $col - 1} {
5569                     lappend coords [expr {$x2 + $linespc}] $y
5570                 } elseif {$i > $col + 1} {
5571                     lappend coords [expr {$x2 - $linespc}] $y
5572                 }
5573                 lappend coords $x2 $y2
5574             }
5575         } else {
5576             lappend coords $x2 $y2
5577         }
5578         set t [$canv create line $coords -width [linewidth $p] \
5579                    -fill $colormap($p) -tags lines.$p]
5580         $canv lower $t
5581         bindline $t $p
5582     }
5583     if {$rmx > [lindex $idpos($id) 1]} {
5584         lset idpos($id) 1 $rmx
5585         redrawtags $id
5586     }
5587 }
5588
5589 proc drawlines {id} {
5590     global canv
5591
5592     $canv itemconf lines.$id -width [linewidth $id]
5593 }
5594
5595 proc drawcmittext {id row col} {
5596     global linespc canv canv2 canv3 fgcolor curview
5597     global cmitlisted commitinfo rowidlist parentlist
5598     global rowtextx idpos idtags idheads idotherrefs
5599     global linehtag linentag linedtag selectedline
5600     global canvxmax boldids boldnameids fgcolor markedid
5601     global mainheadid nullid nullid2 circleitem circlecolors ctxbut
5602
5603     # listed is 0 for boundary, 1 for normal, 2 for negative, 3 for left, 4 for right
5604     set listed $cmitlisted($curview,$id)
5605     if {$id eq $nullid} {
5606         set ofill red
5607     } elseif {$id eq $nullid2} {
5608         set ofill green
5609     } elseif {$id eq $mainheadid} {
5610         set ofill yellow
5611     } else {
5612         set ofill [lindex $circlecolors $listed]
5613     }
5614     set x [xc $row $col]
5615     set y [yc $row]
5616     set orad [expr {$linespc / 3}]
5617     if {$listed <= 2} {
5618         set t [$canv create oval [expr {$x - $orad}] [expr {$y - $orad}] \
5619                    [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
5620                    -fill $ofill -outline $fgcolor -width 1 -tags circle]
5621     } elseif {$listed == 3} {
5622         # triangle pointing left for left-side commits
5623         set t [$canv create polygon \
5624                    [expr {$x - $orad}] $y \
5625                    [expr {$x + $orad - 1}] [expr {$y - $orad}] \
5626                    [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
5627                    -fill $ofill -outline $fgcolor -width 1 -tags circle]
5628     } else {
5629         # triangle pointing right for right-side commits
5630         set t [$canv create polygon \
5631                    [expr {$x + $orad - 1}] $y \
5632                    [expr {$x - $orad}] [expr {$y - $orad}] \
5633                    [expr {$x - $orad}] [expr {$y + $orad - 1}] \
5634                    -fill $ofill -outline $fgcolor -width 1 -tags circle]
5635     }
5636     set circleitem($row) $t
5637     $canv raise $t
5638     $canv bind $t <1> {selcanvline {} %x %y}
5639     set rmx [llength [lindex $rowidlist $row]]
5640     set olds [lindex $parentlist $row]
5641     if {$olds ne {}} {
5642         set nextids [lindex $rowidlist [expr {$row + 1}]]
5643         foreach p $olds {
5644             set i [lsearch -exact $nextids $p]
5645             if {$i > $rmx} {
5646                 set rmx $i
5647             }
5648         }
5649     }
5650     set xt [xc $row $rmx]
5651     set rowtextx($row) $xt
5652     set idpos($id) [list $x $xt $y]
5653     if {[info exists idtags($id)] || [info exists idheads($id)]
5654         || [info exists idotherrefs($id)]} {
5655         set xt [drawtags $id $x $xt $y]
5656     }
5657     set headline [lindex $commitinfo($id) 0]
5658     set name [lindex $commitinfo($id) 1]
5659     set date [lindex $commitinfo($id) 2]
5660     set date [formatdate $date]
5661     set font mainfont
5662     set nfont mainfont
5663     set isbold [ishighlighted $id]
5664     if {$isbold > 0} {
5665         lappend boldids $id
5666         set font mainfontbold
5667         if {$isbold > 1} {
5668             lappend boldnameids $id
5669             set nfont mainfontbold
5670         }
5671     }
5672     set linehtag($id) [$canv create text $xt $y -anchor w -fill $fgcolor \
5673                            -text $headline -font $font -tags text]
5674     $canv bind $linehtag($id) $ctxbut "rowmenu %X %Y $id"
5675     set linentag($id) [$canv2 create text 3 $y -anchor w -fill $fgcolor \
5676                            -text $name -font $nfont -tags text]
5677     set linedtag($id) [$canv3 create text 3 $y -anchor w -fill $fgcolor \
5678                            -text $date -font mainfont -tags text]
5679     if {$selectedline == $row} {
5680         make_secsel $id
5681     }
5682     if {[info exists markedid] && $markedid eq $id} {
5683         make_idmark $id
5684     }
5685     set xr [expr {$xt + [font measure $font $headline]}]
5686     if {$xr > $canvxmax} {
5687         set canvxmax $xr
5688         setcanvscroll
5689     }
5690 }
5691
5692 proc drawcmitrow {row} {
5693     global displayorder rowidlist nrows_drawn
5694     global iddrawn markingmatches
5695     global commitinfo numcommits
5696     global filehighlight fhighlights findpattern nhighlights
5697     global hlview vhighlights
5698     global highlight_related rhighlights
5699
5700     if {$row >= $numcommits} return
5701
5702     set id [lindex $displayorder $row]
5703     if {[info exists hlview] && ![info exists vhighlights($id)]} {
5704         askvhighlight $row $id
5705     }
5706     if {[info exists filehighlight] && ![info exists fhighlights($id)]} {
5707         askfilehighlight $row $id
5708     }
5709     if {$findpattern ne {} && ![info exists nhighlights($id)]} {
5710         askfindhighlight $row $id
5711     }
5712     if {$highlight_related ne [mc "None"] && ![info exists rhighlights($id)]} {
5713         askrelhighlight $row $id
5714     }
5715     if {![info exists iddrawn($id)]} {
5716         set col [lsearch -exact [lindex $rowidlist $row] $id]
5717         if {$col < 0} {
5718             puts "oops, row $row id $id not in list"
5719             return
5720         }
5721         if {![info exists commitinfo($id)]} {
5722             getcommit $id
5723         }
5724         assigncolor $id
5725         drawcmittext $id $row $col
5726         set iddrawn($id) 1
5727         incr nrows_drawn
5728     }
5729     if {$markingmatches} {
5730         markrowmatches $row $id
5731     }
5732 }
5733
5734 proc drawcommits {row {endrow {}}} {
5735     global numcommits iddrawn displayorder curview need_redisplay
5736     global parentlist rowidlist rowfinal uparrowlen downarrowlen nrows_drawn
5737
5738     if {$row < 0} {
5739         set row 0
5740     }
5741     if {$endrow eq {}} {
5742         set endrow $row
5743     }
5744     if {$endrow >= $numcommits} {
5745         set endrow [expr {$numcommits - 1}]
5746     }
5747
5748     set rl1 [expr {$row - $downarrowlen - 3}]
5749     if {$rl1 < 0} {
5750         set rl1 0
5751     }
5752     set ro1 [expr {$row - 3}]
5753     if {$ro1 < 0} {
5754         set ro1 0
5755     }
5756     set r2 [expr {$endrow + $uparrowlen + 3}]
5757     if {$r2 > $numcommits} {
5758         set r2 $numcommits
5759     }
5760     for {set r $rl1} {$r < $r2} {incr r} {
5761         if {[lindex $rowidlist $r] ne {} && [lindex $rowfinal $r]} {
5762             if {$rl1 < $r} {
5763                 layoutrows $rl1 $r
5764             }
5765             set rl1 [expr {$r + 1}]
5766         }
5767     }
5768     if {$rl1 < $r} {
5769         layoutrows $rl1 $r
5770     }
5771     optimize_rows $ro1 0 $r2
5772     if {$need_redisplay || $nrows_drawn > 2000} {
5773         clear_display
5774     }
5775
5776     # make the lines join to already-drawn rows either side
5777     set r [expr {$row - 1}]
5778     if {$r < 0 || ![info exists iddrawn([lindex $displayorder $r])]} {
5779         set r $row
5780     }
5781     set er [expr {$endrow + 1}]
5782     if {$er >= $numcommits ||
5783         ![info exists iddrawn([lindex $displayorder $er])]} {
5784         set er $endrow
5785     }
5786     for {} {$r <= $er} {incr r} {
5787         set id [lindex $displayorder $r]
5788         set wasdrawn [info exists iddrawn($id)]
5789         drawcmitrow $r
5790         if {$r == $er} break
5791         set nextid [lindex $displayorder [expr {$r + 1}]]
5792         if {$wasdrawn && [info exists iddrawn($nextid)]} continue
5793         drawparentlinks $id $r
5794
5795         set rowids [lindex $rowidlist $r]
5796         foreach lid $rowids {
5797             if {$lid eq {}} continue
5798             if {[info exists lineend($lid)] && $lineend($lid) > $r} continue
5799             if {$lid eq $id} {
5800                 # see if this is the first child of any of its parents
5801                 foreach p [lindex $parentlist $r] {
5802                     if {[lsearch -exact $rowids $p] < 0} {
5803                         # make this line extend up to the child
5804                         set lineend($p) [drawlineseg $p $r $er 0]
5805                     }
5806                 }
5807             } else {
5808                 set lineend($lid) [drawlineseg $lid $r $er 1]
5809             }
5810         }
5811     }
5812 }
5813
5814 proc undolayout {row} {
5815     global uparrowlen mingaplen downarrowlen
5816     global rowidlist rowisopt rowfinal need_redisplay
5817
5818     set r [expr {$row - ($uparrowlen + $mingaplen + $downarrowlen)}]
5819     if {$r < 0} {
5820         set r 0
5821     }
5822     if {[llength $rowidlist] > $r} {
5823         incr r -1
5824         set rowidlist [lrange $rowidlist 0 $r]
5825         set rowfinal [lrange $rowfinal 0 $r]
5826         set rowisopt [lrange $rowisopt 0 $r]
5827         set need_redisplay 1
5828         run drawvisible
5829     }
5830 }
5831
5832 proc drawvisible {} {
5833     global canv linespc curview vrowmod selectedline targetrow targetid
5834     global need_redisplay cscroll numcommits
5835
5836     set fs [$canv yview]
5837     set ymax [lindex [$canv cget -scrollregion] 3]
5838     if {$ymax eq {} || $ymax == 0 || $numcommits == 0} return
5839     set f0 [lindex $fs 0]
5840     set f1 [lindex $fs 1]
5841     set y0 [expr {int($f0 * $ymax)}]
5842     set y1 [expr {int($f1 * $ymax)}]
5843
5844     if {[info exists targetid]} {
5845         if {[commitinview $targetid $curview]} {
5846             set r [rowofcommit $targetid]
5847             if {$r != $targetrow} {
5848                 # Fix up the scrollregion and change the scrolling position
5849                 # now that our target row has moved.
5850                 set diff [expr {($r - $targetrow) * $linespc}]
5851                 set targetrow $r
5852                 setcanvscroll
5853                 set ymax [lindex [$canv cget -scrollregion] 3]
5854                 incr y0 $diff
5855                 incr y1 $diff
5856                 set f0 [expr {$y0 / $ymax}]
5857                 set f1 [expr {$y1 / $ymax}]
5858                 allcanvs yview moveto $f0
5859                 $cscroll set $f0 $f1
5860                 set need_redisplay 1
5861             }
5862         } else {
5863             unset targetid
5864         }
5865     }
5866
5867     set row [expr {int(($y0 - 3) / $linespc) - 1}]
5868     set endrow [expr {int(($y1 - 3) / $linespc) + 1}]
5869     if {$endrow >= $vrowmod($curview)} {
5870         update_arcrows $curview
5871     }
5872     if {$selectedline ne {} &&
5873         $row <= $selectedline && $selectedline <= $endrow} {
5874         set targetrow $selectedline
5875     } elseif {[info exists targetid]} {
5876         set targetrow [expr {int(($row + $endrow) / 2)}]
5877     }
5878     if {[info exists targetrow]} {
5879         if {$targetrow >= $numcommits} {
5880             set targetrow [expr {$numcommits - 1}]
5881         }
5882         set targetid [commitonrow $targetrow]
5883     }
5884     drawcommits $row $endrow
5885 }
5886
5887 proc clear_display {} {
5888     global iddrawn linesegs need_redisplay nrows_drawn
5889     global vhighlights fhighlights nhighlights rhighlights
5890     global linehtag linentag linedtag boldids boldnameids
5891
5892     allcanvs delete all
5893     catch {unset iddrawn}
5894     catch {unset linesegs}
5895     catch {unset linehtag}
5896     catch {unset linentag}
5897     catch {unset linedtag}
5898     set boldids {}
5899     set boldnameids {}
5900     catch {unset vhighlights}
5901     catch {unset fhighlights}
5902     catch {unset nhighlights}
5903     catch {unset rhighlights}
5904     set need_redisplay 0
5905     set nrows_drawn 0
5906 }
5907
5908 proc findcrossings {id} {
5909     global rowidlist parentlist numcommits displayorder
5910
5911     set cross {}
5912     set ccross {}
5913     foreach {s e} [rowranges $id] {
5914         if {$e >= $numcommits} {
5915             set e [expr {$numcommits - 1}]
5916         }
5917         if {$e <= $s} continue
5918         for {set row $e} {[incr row -1] >= $s} {} {
5919             set x [lsearch -exact [lindex $rowidlist $row] $id]
5920             if {$x < 0} break
5921             set olds [lindex $parentlist $row]
5922             set kid [lindex $displayorder $row]
5923             set kidx [lsearch -exact [lindex $rowidlist $row] $kid]
5924             if {$kidx < 0} continue
5925             set nextrow [lindex $rowidlist [expr {$row + 1}]]
5926             foreach p $olds {
5927                 set px [lsearch -exact $nextrow $p]
5928                 if {$px < 0} continue
5929                 if {($kidx < $x && $x < $px) || ($px < $x && $x < $kidx)} {
5930                     if {[lsearch -exact $ccross $p] >= 0} continue
5931                     if {$x == $px + ($kidx < $px? -1: 1)} {
5932                         lappend ccross $p
5933                     } elseif {[lsearch -exact $cross $p] < 0} {
5934                         lappend cross $p
5935                     }
5936                 }
5937             }
5938         }
5939     }
5940     return [concat $ccross {{}} $cross]
5941 }
5942
5943 proc assigncolor {id} {
5944     global colormap colors nextcolor
5945     global parents children children curview
5946
5947     if {[info exists colormap($id)]} return
5948     set ncolors [llength $colors]
5949     if {[info exists children($curview,$id)]} {
5950         set kids $children($curview,$id)
5951     } else {
5952         set kids {}
5953     }
5954     if {[llength $kids] == 1} {
5955         set child [lindex $kids 0]
5956         if {[info exists colormap($child)]
5957             && [llength $parents($curview,$child)] == 1} {
5958             set colormap($id) $colormap($child)
5959             return
5960         }
5961     }
5962     set badcolors {}
5963     set origbad {}
5964     foreach x [findcrossings $id] {
5965         if {$x eq {}} {
5966             # delimiter between corner crossings and other crossings
5967             if {[llength $badcolors] >= $ncolors - 1} break
5968             set origbad $badcolors
5969         }
5970         if {[info exists colormap($x)]
5971             && [lsearch -exact $badcolors $colormap($x)] < 0} {
5972             lappend badcolors $colormap($x)
5973         }
5974     }
5975     if {[llength $badcolors] >= $ncolors} {
5976         set badcolors $origbad
5977     }
5978     set origbad $badcolors
5979     if {[llength $badcolors] < $ncolors - 1} {
5980         foreach child $kids {
5981             if {[info exists colormap($child)]
5982                 && [lsearch -exact $badcolors $colormap($child)] < 0} {
5983                 lappend badcolors $colormap($child)
5984             }
5985             foreach p $parents($curview,$child) {
5986                 if {[info exists colormap($p)]
5987                     && [lsearch -exact $badcolors $colormap($p)] < 0} {
5988                     lappend badcolors $colormap($p)
5989                 }
5990             }
5991         }
5992         if {[llength $badcolors] >= $ncolors} {
5993             set badcolors $origbad
5994         }
5995     }
5996     for {set i 0} {$i <= $ncolors} {incr i} {
5997         set c [lindex $colors $nextcolor]
5998         if {[incr nextcolor] >= $ncolors} {
5999             set nextcolor 0
6000         }
6001         if {[lsearch -exact $badcolors $c]} break
6002     }
6003     set colormap($id) $c
6004 }
6005
6006 proc bindline {t id} {
6007     global canv
6008
6009     $canv bind $t <Enter> "lineenter %x %y $id"
6010     $canv bind $t <Motion> "linemotion %x %y $id"
6011     $canv bind $t <Leave> "lineleave $id"
6012     $canv bind $t <Button-1> "lineclick %x %y $id 1"
6013 }
6014
6015 proc drawtags {id x xt y1} {
6016     global idtags idheads idotherrefs mainhead
6017     global linespc lthickness
6018     global canv rowtextx curview fgcolor bgcolor ctxbut
6019
6020     set marks {}
6021     set ntags 0
6022     set nheads 0
6023     if {[info exists idtags($id)]} {
6024         set marks $idtags($id)
6025         set ntags [llength $marks]
6026     }
6027     if {[info exists idheads($id)]} {
6028         set marks [concat $marks $idheads($id)]
6029         set nheads [llength $idheads($id)]
6030     }
6031     if {[info exists idotherrefs($id)]} {
6032         set marks [concat $marks $idotherrefs($id)]
6033     }
6034     if {$marks eq {}} {
6035         return $xt
6036     }
6037
6038     set delta [expr {int(0.5 * ($linespc - $lthickness))}]
6039     set yt [expr {$y1 - 0.5 * $linespc}]
6040     set yb [expr {$yt + $linespc - 1}]
6041     set xvals {}
6042     set wvals {}
6043     set i -1
6044     foreach tag $marks {
6045         incr i
6046         if {$i >= $ntags && $i < $ntags + $nheads && $tag eq $mainhead} {
6047             set wid [font measure mainfontbold $tag]
6048         } else {
6049             set wid [font measure mainfont $tag]
6050         }
6051         lappend xvals $xt
6052         lappend wvals $wid
6053         set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
6054     }
6055     set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
6056                -width $lthickness -fill black -tags tag.$id]
6057     $canv lower $t
6058     foreach tag $marks x $xvals wid $wvals {
6059         set xl [expr {$x + $delta}]
6060         set xr [expr {$x + $delta + $wid + $lthickness}]
6061         set font mainfont
6062         if {[incr ntags -1] >= 0} {
6063             # draw a tag
6064             set t [$canv create polygon $x [expr {$yt + $delta}] $xl $yt \
6065                        $xr $yt $xr $yb $xl $yb $x [expr {$yb - $delta}] \
6066                        -width 1 -outline black -fill yellow -tags tag.$id]
6067             $canv bind $t <1> [list showtag $tag 1]
6068             set rowtextx([rowofcommit $id]) [expr {$xr + $linespc}]
6069         } else {
6070             # draw a head or other ref
6071             if {[incr nheads -1] >= 0} {
6072                 set col green
6073                 if {$tag eq $mainhead} {
6074                     set font mainfontbold
6075                 }
6076             } else {
6077                 set col "#ddddff"
6078             }
6079             set xl [expr {$xl - $delta/2}]
6080             $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
6081                 -width 1 -outline black -fill $col -tags tag.$id
6082             if {[regexp {^(remotes/.*/|remotes/)} $tag match remoteprefix]} {
6083                 set rwid [font measure mainfont $remoteprefix]
6084                 set xi [expr {$x + 1}]
6085                 set yti [expr {$yt + 1}]
6086                 set xri [expr {$x + $rwid}]
6087                 $canv create polygon $xi $yti $xri $yti $xri $yb $xi $yb \
6088                         -width 0 -fill "#ffddaa" -tags tag.$id
6089             }
6090         }
6091         set t [$canv create text $xl $y1 -anchor w -text $tag -fill $fgcolor \
6092                    -font $font -tags [list tag.$id text]]
6093         if {$ntags >= 0} {
6094             $canv bind $t <1> [list showtag $tag 1]
6095         } elseif {$nheads >= 0} {
6096             $canv bind $t $ctxbut [list headmenu %X %Y $id $tag]
6097         }
6098     }
6099     return $xt
6100 }
6101
6102 proc xcoord {i level ln} {
6103     global canvx0 xspc1 xspc2
6104
6105     set x [expr {$canvx0 + $i * $xspc1($ln)}]
6106     if {$i > 0 && $i == $level} {
6107         set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
6108     } elseif {$i > $level} {
6109         set x [expr {$x + $xspc2 - $xspc1($ln)}]
6110     }
6111     return $x
6112 }
6113
6114 proc show_status {msg} {
6115     global canv fgcolor
6116
6117     clear_display
6118     $canv create text 3 3 -anchor nw -text $msg -font mainfont \
6119         -tags text -fill $fgcolor
6120 }
6121
6122 # Don't change the text pane cursor if it is currently the hand cursor,
6123 # showing that we are over a sha1 ID link.
6124 proc settextcursor {c} {
6125     global ctext curtextcursor
6126
6127     if {[$ctext cget -cursor] == $curtextcursor} {
6128         $ctext config -cursor $c
6129     }
6130     set curtextcursor $c
6131 }
6132
6133 proc nowbusy {what {name {}}} {
6134     global isbusy busyname statusw
6135
6136     if {[array names isbusy] eq {}} {
6137         . config -cursor watch
6138         settextcursor watch
6139     }
6140     set isbusy($what) 1
6141     set busyname($what) $name
6142     if {$name ne {}} {
6143         $statusw conf -text $name
6144     }
6145 }
6146
6147 proc notbusy {what} {
6148     global isbusy maincursor textcursor busyname statusw
6149
6150     catch {
6151         unset isbusy($what)
6152         if {$busyname($what) ne {} &&
6153             [$statusw cget -text] eq $busyname($what)} {
6154             $statusw conf -text {}
6155         }
6156     }
6157     if {[array names isbusy] eq {}} {
6158         . config -cursor $maincursor
6159         settextcursor $textcursor
6160     }
6161 }
6162
6163 proc findmatches {f} {
6164     global findtype findstring
6165     if {$findtype == [mc "Regexp"]} {
6166         set matches [regexp -indices -all -inline $findstring $f]
6167     } else {
6168         set fs $findstring
6169         if {$findtype == [mc "IgnCase"]} {
6170             set f [string tolower $f]
6171             set fs [string tolower $fs]
6172         }
6173         set matches {}
6174         set i 0
6175         set l [string length $fs]
6176         while {[set j [string first $fs $f $i]] >= 0} {
6177             lappend matches [list $j [expr {$j+$l-1}]]
6178             set i [expr {$j + $l}]
6179         }
6180     }
6181     return $matches
6182 }
6183
6184 proc dofind {{dirn 1} {wrap 1}} {
6185     global findstring findstartline findcurline selectedline numcommits
6186     global gdttype filehighlight fh_serial find_dirn findallowwrap
6187
6188     if {[info exists find_dirn]} {
6189         if {$find_dirn == $dirn} return
6190         stopfinding
6191     }
6192     focus .
6193     if {$findstring eq {} || $numcommits == 0} return
6194     if {$selectedline eq {}} {
6195         set findstartline [lindex [visiblerows] [expr {$dirn < 0}]]
6196     } else {
6197         set findstartline $selectedline
6198     }
6199     set findcurline $findstartline
6200     nowbusy finding [mc "Searching"]
6201     if {$gdttype ne [mc "containing:"] && ![info exists filehighlight]} {
6202         after cancel do_file_hl $fh_serial
6203         do_file_hl $fh_serial
6204     }
6205     set find_dirn $dirn
6206     set findallowwrap $wrap
6207     run findmore
6208 }
6209
6210 proc stopfinding {} {
6211     global find_dirn findcurline fprogcoord
6212
6213     if {[info exists find_dirn]} {
6214         unset find_dirn
6215         unset findcurline
6216         notbusy finding
6217         set fprogcoord 0
6218         adjustprogress
6219     }
6220     stopblaming
6221 }
6222
6223 proc findmore {} {
6224     global commitdata commitinfo numcommits findpattern findloc
6225     global findstartline findcurline findallowwrap
6226     global find_dirn gdttype fhighlights fprogcoord
6227     global curview varcorder vrownum varccommits vrowmod
6228
6229     if {![info exists find_dirn]} {
6230         return 0
6231     }
6232     set fldtypes [list [mc "Headline"] [mc "Author"] [mc "Date"] [mc "Committer"] [mc "CDate"] [mc "Comments"]]
6233     set l $findcurline
6234     set moretodo 0
6235     if {$find_dirn > 0} {
6236         incr l
6237         if {$l >= $numcommits} {
6238             set l 0
6239         }
6240         if {$l <= $findstartline} {
6241             set lim [expr {$findstartline + 1}]
6242         } else {
6243             set lim $numcommits
6244             set moretodo $findallowwrap
6245         }
6246     } else {
6247         if {$l == 0} {
6248             set l $numcommits
6249         }
6250         incr l -1
6251         if {$l >= $findstartline} {
6252             set lim [expr {$findstartline - 1}]
6253         } else {
6254             set lim -1
6255             set moretodo $findallowwrap
6256         }
6257     }
6258     set n [expr {($lim - $l) * $find_dirn}]
6259     if {$n > 500} {
6260         set n 500
6261         set moretodo 1
6262     }
6263     if {$l + ($find_dirn > 0? $n: 1) > $vrowmod($curview)} {
6264         update_arcrows $curview
6265     }
6266     set found 0
6267     set domore 1
6268     set ai [bsearch $vrownum($curview) $l]
6269     set a [lindex $varcorder($curview) $ai]
6270     set arow [lindex $vrownum($curview) $ai]
6271     set ids [lindex $varccommits($curview,$a)]
6272     set arowend [expr {$arow + [llength $ids]}]
6273     if {$gdttype eq [mc "containing:"]} {
6274         for {} {$n > 0} {incr n -1; incr l $find_dirn} {
6275             if {$l < $arow || $l >= $arowend} {
6276                 incr ai $find_dirn
6277                 set a [lindex $varcorder($curview) $ai]
6278                 set arow [lindex $vrownum($curview) $ai]
6279                 set ids [lindex $varccommits($curview,$a)]
6280                 set arowend [expr {$arow + [llength $ids]}]
6281             }
6282             set id [lindex $ids [expr {$l - $arow}]]
6283             # shouldn't happen unless git log doesn't give all the commits...
6284             if {![info exists commitdata($id)] ||
6285                 ![doesmatch $commitdata($id)]} {
6286                 continue
6287             }
6288             if {![info exists commitinfo($id)]} {
6289                 getcommit $id
6290             }
6291             set info $commitinfo($id)
6292             foreach f $info ty $fldtypes {
6293                 if {($findloc eq [mc "All fields"] || $findloc eq $ty) &&
6294                     [doesmatch $f]} {
6295                     set found 1
6296                     break
6297                 }
6298             }
6299             if {$found} break
6300         }
6301     } else {
6302         for {} {$n > 0} {incr n -1; incr l $find_dirn} {
6303             if {$l < $arow || $l >= $arowend} {
6304                 incr ai $find_dirn
6305                 set a [lindex $varcorder($curview) $ai]
6306                 set arow [lindex $vrownum($curview) $ai]
6307                 set ids [lindex $varccommits($curview,$a)]
6308                 set arowend [expr {$arow + [llength $ids]}]
6309             }
6310             set id [lindex $ids [expr {$l - $arow}]]
6311             if {![info exists fhighlights($id)]} {
6312                 # this sets fhighlights($id) to -1
6313                 askfilehighlight $l $id
6314             }
6315             if {$fhighlights($id) > 0} {
6316                 set found $domore
6317                 break
6318             }
6319             if {$fhighlights($id) < 0} {
6320                 if {$domore} {
6321                     set domore 0
6322                     set findcurline [expr {$l - $find_dirn}]
6323                 }
6324             }
6325         }
6326     }
6327     if {$found || ($domore && !$moretodo)} {
6328         unset findcurline
6329         unset find_dirn
6330         notbusy finding
6331         set fprogcoord 0
6332         adjustprogress
6333         if {$found} {
6334             findselectline $l
6335         } else {
6336             bell
6337         }
6338         return 0
6339     }
6340     if {!$domore} {
6341         flushhighlights
6342     } else {
6343         set findcurline [expr {$l - $find_dirn}]
6344     }
6345     set n [expr {($findcurline - $findstartline) * $find_dirn - 1}]
6346     if {$n < 0} {
6347         incr n $numcommits
6348     }
6349     set fprogcoord [expr {$n * 1.0 / $numcommits}]
6350     adjustprogress
6351     return $domore
6352 }
6353
6354 proc findselectline {l} {
6355     global findloc commentend ctext findcurline markingmatches gdttype
6356
6357     set markingmatches [expr {$gdttype eq [mc "containing:"]}]
6358     set findcurline $l
6359     selectline $l 1
6360     if {$markingmatches &&
6361         ($findloc eq [mc "All fields"] || $findloc eq [mc "Comments"])} {
6362         # highlight the matches in the comments
6363         set f [$ctext get 1.0 $commentend]
6364         set matches [findmatches $f]
6365         foreach match $matches {
6366             set start [lindex $match 0]
6367             set end [expr {[lindex $match 1] + 1}]
6368             $ctext tag add found "1.0 + $start c" "1.0 + $end c"
6369         }
6370     }
6371     drawvisible
6372 }
6373
6374 # mark the bits of a headline or author that match a find string
6375 proc markmatches {canv l str tag matches font row} {
6376     global selectedline
6377
6378     set bbox [$canv bbox $tag]
6379     set x0 [lindex $bbox 0]
6380     set y0 [lindex $bbox 1]
6381     set y1 [lindex $bbox 3]
6382     foreach match $matches {
6383         set start [lindex $match 0]
6384         set end [lindex $match 1]
6385         if {$start > $end} continue
6386         set xoff [font measure $font [string range $str 0 [expr {$start-1}]]]
6387         set xlen [font measure $font [string range $str 0 [expr {$end}]]]
6388         set t [$canv create rect [expr {$x0+$xoff}] $y0 \
6389                    [expr {$x0+$xlen+2}] $y1 \
6390                    -outline {} -tags [list match$l matches] -fill yellow]
6391         $canv lower $t
6392         if {$row == $selectedline} {
6393             $canv raise $t secsel
6394         }
6395     }
6396 }
6397
6398 proc unmarkmatches {} {
6399     global markingmatches
6400
6401     allcanvs delete matches
6402     set markingmatches 0
6403     stopfinding
6404 }
6405
6406 proc selcanvline {w x y} {
6407     global canv canvy0 ctext linespc
6408     global rowtextx
6409     set ymax [lindex [$canv cget -scrollregion] 3]
6410     if {$ymax == {}} return
6411     set yfrac [lindex [$canv yview] 0]
6412     set y [expr {$y + $yfrac * $ymax}]
6413     set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
6414     if {$l < 0} {
6415         set l 0
6416     }
6417     if {$w eq $canv} {
6418         set xmax [lindex [$canv cget -scrollregion] 2]
6419         set xleft [expr {[lindex [$canv xview] 0] * $xmax}]
6420         if {![info exists rowtextx($l)] || $xleft + $x < $rowtextx($l)} return
6421     }
6422     unmarkmatches
6423     selectline $l 1
6424 }
6425
6426 proc commit_descriptor {p} {
6427     global commitinfo
6428     if {![info exists commitinfo($p)]} {
6429         getcommit $p
6430     }
6431     set l "..."
6432     if {[llength $commitinfo($p)] > 1} {
6433         set l [lindex $commitinfo($p) 0]
6434     }
6435     return "$p ($l)\n"
6436 }
6437
6438 # append some text to the ctext widget, and make any SHA1 ID
6439 # that we know about be a clickable link.
6440 proc appendwithlinks {text tags} {
6441     global ctext linknum curview
6442
6443     set start [$ctext index "end - 1c"]
6444     $ctext insert end $text $tags
6445     set links [regexp -indices -all -inline {\m[0-9a-f]{6,40}\M} $text]
6446     foreach l $links {
6447         set s [lindex $l 0]
6448         set e [lindex $l 1]
6449         set linkid [string range $text $s $e]
6450         incr e
6451         $ctext tag delete link$linknum
6452         $ctext tag add link$linknum "$start + $s c" "$start + $e c"
6453         setlink $linkid link$linknum
6454         incr linknum
6455     }
6456 }
6457
6458 proc setlink {id lk} {
6459     global curview ctext pendinglinks
6460
6461     set known 0
6462     if {[string length $id] < 40} {
6463         set matches [longid $id]
6464         if {[llength $matches] > 0} {
6465             if {[llength $matches] > 1} return
6466             set known 1
6467             set id [lindex $matches 0]
6468         }
6469     } else {
6470         set known [commitinview $id $curview]
6471     }
6472     if {$known} {
6473         $ctext tag conf $lk -foreground blue -underline 1
6474         $ctext tag bind $lk <1> [list selbyid $id]
6475         $ctext tag bind $lk <Enter> {linkcursor %W 1}
6476         $ctext tag bind $lk <Leave> {linkcursor %W -1}
6477     } else {
6478         lappend pendinglinks($id) $lk
6479         interestedin $id {makelink %P}
6480     }
6481 }
6482
6483 proc makelink {id} {
6484     global pendinglinks
6485
6486     if {![info exists pendinglinks($id)]} return
6487     foreach lk $pendinglinks($id) {
6488         setlink $id $lk
6489     }
6490     unset pendinglinks($id)
6491 }
6492
6493 proc linkcursor {w inc} {
6494     global linkentercount curtextcursor
6495
6496     if {[incr linkentercount $inc] > 0} {
6497         $w configure -cursor hand2
6498     } else {
6499         $w configure -cursor $curtextcursor
6500         if {$linkentercount < 0} {
6501             set linkentercount 0
6502         }
6503     }
6504 }
6505
6506 proc viewnextline {dir} {
6507     global canv linespc
6508
6509     $canv delete hover
6510     set ymax [lindex [$canv cget -scrollregion] 3]
6511     set wnow [$canv yview]
6512     set wtop [expr {[lindex $wnow 0] * $ymax}]
6513     set newtop [expr {$wtop + $dir * $linespc}]
6514     if {$newtop < 0} {
6515         set newtop 0
6516     } elseif {$newtop > $ymax} {
6517         set newtop $ymax
6518     }
6519     allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
6520 }
6521
6522 # add a list of tag or branch names at position pos
6523 # returns the number of names inserted
6524 proc appendrefs {pos ids var} {
6525     global ctext linknum curview $var maxrefs
6526
6527     if {[catch {$ctext index $pos}]} {
6528         return 0
6529     }
6530     $ctext conf -state normal
6531     $ctext delete $pos "$pos lineend"
6532     set tags {}
6533     foreach id $ids {
6534         foreach tag [set $var\($id\)] {
6535             lappend tags [list $tag $id]
6536         }
6537     }
6538     if {[llength $tags] > $maxrefs} {
6539         $ctext insert $pos "many ([llength $tags])"
6540     } else {
6541         set tags [lsort -index 0 -decreasing $tags]
6542         set sep {}
6543         foreach ti $tags {
6544             set id [lindex $ti 1]
6545             set lk link$linknum
6546             incr linknum
6547             $ctext tag delete $lk
6548             $ctext insert $pos $sep
6549             $ctext insert $pos [lindex $ti 0] $lk
6550             setlink $id $lk
6551             set sep ", "
6552         }
6553     }
6554     $ctext conf -state disabled
6555     return [llength $tags]
6556 }
6557
6558 # called when we have finished computing the nearby tags
6559 proc dispneartags {delay} {
6560     global selectedline currentid showneartags tagphase
6561
6562     if {$selectedline eq {} || !$showneartags} return
6563     after cancel dispnexttag
6564     if {$delay} {
6565         after 200 dispnexttag
6566         set tagphase -1
6567     } else {
6568         after idle dispnexttag
6569         set tagphase 0
6570     }
6571 }
6572
6573 proc dispnexttag {} {
6574     global selectedline currentid showneartags tagphase ctext
6575
6576     if {$selectedline eq {} || !$showneartags} return
6577     switch -- $tagphase {
6578         0 {
6579             set dtags [desctags $currentid]
6580             if {$dtags ne {}} {
6581                 appendrefs precedes $dtags idtags
6582             }
6583         }
6584         1 {
6585             set atags [anctags $currentid]
6586             if {$atags ne {}} {
6587                 appendrefs follows $atags idtags
6588             }
6589         }
6590         2 {
6591             set dheads [descheads $currentid]
6592             if {$dheads ne {}} {
6593                 if {[appendrefs branch $dheads idheads] > 1
6594                     && [$ctext get "branch -3c"] eq "h"} {
6595                     # turn "Branch" into "Branches"
6596                     $ctext conf -state normal
6597                     $ctext insert "branch -2c" "es"
6598                     $ctext conf -state disabled
6599                 }
6600             }
6601         }
6602     }
6603     if {[incr tagphase] <= 2} {
6604         after idle dispnexttag
6605     }
6606 }
6607
6608 proc make_secsel {id} {
6609     global linehtag linentag linedtag canv canv2 canv3
6610
6611     if {![info exists linehtag($id)]} return
6612     $canv delete secsel
6613     set t [eval $canv create rect [$canv bbox $linehtag($id)] -outline {{}} \
6614                -tags secsel -fill [$canv cget -selectbackground]]
6615     $canv lower $t
6616     $canv2 delete secsel
6617     set t [eval $canv2 create rect [$canv2 bbox $linentag($id)] -outline {{}} \
6618                -tags secsel -fill [$canv2 cget -selectbackground]]
6619     $canv2 lower $t
6620     $canv3 delete secsel
6621     set t [eval $canv3 create rect [$canv3 bbox $linedtag($id)] -outline {{}} \
6622                -tags secsel -fill [$canv3 cget -selectbackground]]
6623     $canv3 lower $t
6624 }
6625
6626 proc make_idmark {id} {
6627     global linehtag canv fgcolor
6628
6629     if {![info exists linehtag($id)]} return
6630     $canv delete markid
6631     set t [eval $canv create rect [$canv bbox $linehtag($id)] \
6632                -tags markid -outline $fgcolor]
6633     $canv raise $t
6634 }
6635
6636 proc selectline {l isnew {desired_loc {}}} {
6637     global canv ctext commitinfo selectedline
6638     global canvy0 linespc parents children curview
6639     global currentid sha1entry
6640     global commentend idtags linknum
6641     global mergemax numcommits pending_select
6642     global cmitmode showneartags allcommits
6643     global targetrow targetid lastscrollrows
6644     global autoselect jump_to_here
6645
6646     catch {unset pending_select}
6647     $canv delete hover
6648     normalline
6649     unsel_reflist
6650     stopfinding
6651     if {$l < 0 || $l >= $numcommits} return
6652     set id [commitonrow $l]
6653     set targetid $id
6654     set targetrow $l
6655     set selectedline $l
6656     set currentid $id
6657     if {$lastscrollrows < $numcommits} {
6658         setcanvscroll
6659     }
6660
6661     set y [expr {$canvy0 + $l * $linespc}]
6662     set ymax [lindex [$canv cget -scrollregion] 3]
6663     set ytop [expr {$y - $linespc - 1}]
6664     set ybot [expr {$y + $linespc + 1}]
6665     set wnow [$canv yview]
6666     set wtop [expr {[lindex $wnow 0] * $ymax}]
6667     set wbot [expr {[lindex $wnow 1] * $ymax}]
6668     set wh [expr {$wbot - $wtop}]
6669     set newtop $wtop
6670     if {$ytop < $wtop} {
6671         if {$ybot < $wtop} {
6672             set newtop [expr {$y - $wh / 2.0}]
6673         } else {
6674             set newtop $ytop
6675             if {$newtop > $wtop - $linespc} {
6676                 set newtop [expr {$wtop - $linespc}]
6677             }
6678         }
6679     } elseif {$ybot > $wbot} {
6680         if {$ytop > $wbot} {
6681             set newtop [expr {$y - $wh / 2.0}]
6682         } else {
6683             set newtop [expr {$ybot - $wh}]
6684             if {$newtop < $wtop + $linespc} {
6685                 set newtop [expr {$wtop + $linespc}]
6686             }
6687         }
6688     }
6689     if {$newtop != $wtop} {
6690         if {$newtop < 0} {
6691             set newtop 0
6692         }
6693         allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
6694         drawvisible
6695     }
6696
6697     make_secsel $id
6698
6699     if {$isnew} {
6700         addtohistory [list selbyid $id]
6701     }
6702
6703     $sha1entry delete 0 end
6704     $sha1entry insert 0 $id
6705     if {$autoselect} {
6706         $sha1entry selection from 0
6707         $sha1entry selection to end
6708     }
6709     rhighlight_sel $id
6710
6711     $ctext conf -state normal
6712     clear_ctext
6713     set linknum 0
6714     if {![info exists commitinfo($id)]} {
6715         getcommit $id
6716     }
6717     set info $commitinfo($id)
6718     set date [formatdate [lindex $info 2]]
6719     $ctext insert end "[mc "Author"]: [lindex $info 1]  $date\n"
6720     set date [formatdate [lindex $info 4]]
6721     $ctext insert end "[mc "Committer"]: [lindex $info 3]  $date\n"
6722     if {[info exists idtags($id)]} {
6723         $ctext insert end [mc "Tags:"]
6724         foreach tag $idtags($id) {
6725             $ctext insert end " $tag"
6726         }
6727         $ctext insert end "\n"
6728     }
6729
6730     set headers {}
6731     set olds $parents($curview,$id)
6732     if {[llength $olds] > 1} {
6733         set np 0
6734         foreach p $olds {
6735             if {$np >= $mergemax} {
6736                 set tag mmax
6737             } else {
6738                 set tag m$np
6739             }
6740             $ctext insert end "[mc "Parent"]: " $tag
6741             appendwithlinks [commit_descriptor $p] {}
6742             incr np
6743         }
6744     } else {
6745         foreach p $olds {
6746             append headers "[mc "Parent"]: [commit_descriptor $p]"
6747         }
6748     }
6749
6750     foreach c $children($curview,$id) {
6751         append headers "[mc "Child"]:  [commit_descriptor $c]"
6752     }
6753
6754     # make anything that looks like a SHA1 ID be a clickable link
6755     appendwithlinks $headers {}
6756     if {$showneartags} {
6757         if {![info exists allcommits]} {
6758             getallcommits
6759         }
6760         $ctext insert end "[mc "Branch"]: "
6761         $ctext mark set branch "end -1c"
6762         $ctext mark gravity branch left
6763         $ctext insert end "\n[mc "Follows"]: "
6764         $ctext mark set follows "end -1c"
6765         $ctext mark gravity follows left
6766         $ctext insert end "\n[mc "Precedes"]: "
6767         $ctext mark set precedes "end -1c"
6768         $ctext mark gravity precedes left
6769         $ctext insert end "\n"
6770         dispneartags 1
6771     }
6772     $ctext insert end "\n"
6773     set comment [lindex $info 5]
6774     if {[string first "\r" $comment] >= 0} {
6775         set comment [string map {"\r" "\n    "} $comment]
6776     }
6777     appendwithlinks $comment {comment}
6778
6779     $ctext tag remove found 1.0 end
6780     $ctext conf -state disabled
6781     set commentend [$ctext index "end - 1c"]
6782
6783     set jump_to_here $desired_loc
6784     init_flist [mc "Comments"]
6785     if {$cmitmode eq "tree"} {
6786         gettree $id
6787     } elseif {[llength $olds] <= 1} {
6788         startdiff $id
6789     } else {
6790         mergediff $id
6791     }
6792 }
6793
6794 proc selfirstline {} {
6795     unmarkmatches
6796     selectline 0 1
6797 }
6798
6799 proc sellastline {} {
6800     global numcommits
6801     unmarkmatches
6802     set l [expr {$numcommits - 1}]
6803     selectline $l 1
6804 }
6805
6806 proc selnextline {dir} {
6807     global selectedline
6808     focus .
6809     if {$selectedline eq {}} return
6810     set l [expr {$selectedline + $dir}]
6811     unmarkmatches
6812     selectline $l 1
6813 }
6814
6815 proc selnextpage {dir} {
6816     global canv linespc selectedline numcommits
6817
6818     set lpp [expr {([winfo height $canv] - 2) / $linespc}]
6819     if {$lpp < 1} {
6820         set lpp 1
6821     }
6822     allcanvs yview scroll [expr {$dir * $lpp}] units
6823     drawvisible
6824     if {$selectedline eq {}} return
6825     set l [expr {$selectedline + $dir * $lpp}]
6826     if {$l < 0} {
6827         set l 0
6828     } elseif {$l >= $numcommits} {
6829         set l [expr $numcommits - 1]
6830     }
6831     unmarkmatches
6832     selectline $l 1
6833 }
6834
6835 proc unselectline {} {
6836     global selectedline currentid
6837
6838     set selectedline {}
6839     catch {unset currentid}
6840     allcanvs delete secsel
6841     rhighlight_none
6842 }
6843
6844 proc reselectline {} {
6845     global selectedline
6846
6847     if {$selectedline ne {}} {
6848         selectline $selectedline 0
6849     }
6850 }
6851
6852 proc addtohistory {cmd} {
6853     global history historyindex curview
6854
6855     set elt [list $curview $cmd]
6856     if {$historyindex > 0
6857         && [lindex $history [expr {$historyindex - 1}]] == $elt} {
6858         return
6859     }
6860
6861     if {$historyindex < [llength $history]} {
6862         set history [lreplace $history $historyindex end $elt]
6863     } else {
6864         lappend history $elt
6865     }
6866     incr historyindex
6867     if {$historyindex > 1} {
6868         .tf.bar.leftbut conf -state normal
6869     } else {
6870         .tf.bar.leftbut conf -state disabled
6871     }
6872     .tf.bar.rightbut conf -state disabled
6873 }
6874
6875 proc godo {elt} {
6876     global curview
6877
6878     set view [lindex $elt 0]
6879     set cmd [lindex $elt 1]
6880     if {$curview != $view} {
6881         showview $view
6882     }
6883     eval $cmd
6884 }
6885
6886 proc goback {} {
6887     global history historyindex
6888     focus .
6889
6890     if {$historyindex > 1} {
6891         incr historyindex -1
6892         godo [lindex $history [expr {$historyindex - 1}]]
6893         .tf.bar.rightbut conf -state normal
6894     }
6895     if {$historyindex <= 1} {
6896         .tf.bar.leftbut conf -state disabled
6897     }
6898 }
6899
6900 proc goforw {} {
6901     global history historyindex
6902     focus .
6903
6904     if {$historyindex < [llength $history]} {
6905         set cmd [lindex $history $historyindex]
6906         incr historyindex
6907         godo $cmd
6908         .tf.bar.leftbut conf -state normal
6909     }
6910     if {$historyindex >= [llength $history]} {
6911         .tf.bar.rightbut conf -state disabled
6912     }
6913 }
6914
6915 proc gettree {id} {
6916     global treefilelist treeidlist diffids diffmergeid treepending
6917     global nullid nullid2
6918
6919     set diffids $id
6920     catch {unset diffmergeid}
6921     if {![info exists treefilelist($id)]} {
6922         if {![info exists treepending]} {
6923             if {$id eq $nullid} {
6924                 set cmd [list | git ls-files]
6925             } elseif {$id eq $nullid2} {
6926                 set cmd [list | git ls-files --stage -t]
6927             } else {
6928                 set cmd [list | git ls-tree -r $id]
6929             }
6930             if {[catch {set gtf [open $cmd r]}]} {
6931                 return
6932             }
6933             set treepending $id
6934             set treefilelist($id) {}
6935             set treeidlist($id) {}
6936             fconfigure $gtf -blocking 0 -encoding binary
6937             filerun $gtf [list gettreeline $gtf $id]
6938         }
6939     } else {
6940         setfilelist $id
6941     }
6942 }
6943
6944 proc gettreeline {gtf id} {
6945     global treefilelist treeidlist treepending cmitmode diffids nullid nullid2
6946
6947     set nl 0
6948     while {[incr nl] <= 1000 && [gets $gtf line] >= 0} {
6949         if {$diffids eq $nullid} {
6950             set fname $line
6951         } else {
6952             set i [string first "\t" $line]
6953             if {$i < 0} continue
6954             set fname [string range $line [expr {$i+1}] end]
6955             set line [string range $line 0 [expr {$i-1}]]
6956             if {$diffids ne $nullid2 && [lindex $line 1] ne "blob"} continue
6957             set sha1 [lindex $line 2]
6958             lappend treeidlist($id) $sha1
6959         }
6960         if {[string index $fname 0] eq "\""} {
6961             set fname [lindex $fname 0]
6962         }
6963         set fname [encoding convertfrom $fname]
6964         lappend treefilelist($id) $fname
6965     }
6966     if {![eof $gtf]} {
6967         return [expr {$nl >= 1000? 2: 1}]
6968     }
6969     close $gtf
6970     unset treepending
6971     if {$cmitmode ne "tree"} {
6972         if {![info exists diffmergeid]} {
6973             gettreediffs $diffids
6974         }
6975     } elseif {$id ne $diffids} {
6976         gettree $diffids
6977     } else {
6978         setfilelist $id
6979     }
6980     return 0
6981 }
6982
6983 proc showfile {f} {
6984     global treefilelist treeidlist diffids nullid nullid2
6985     global ctext_file_names ctext_file_lines
6986     global ctext commentend
6987
6988     set i [lsearch -exact $treefilelist($diffids) $f]
6989     if {$i < 0} {
6990         puts "oops, $f not in list for id $diffids"
6991         return
6992     }
6993     if {$diffids eq $nullid} {
6994         if {[catch {set bf [open $f r]} err]} {
6995             puts "oops, can't read $f: $err"
6996             return
6997         }
6998     } else {
6999         set blob [lindex $treeidlist($diffids) $i]
7000         if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} {
7001             puts "oops, error reading blob $blob: $err"
7002             return
7003         }
7004     }
7005     fconfigure $bf -blocking 0 -encoding [get_path_encoding $f]
7006     filerun $bf [list getblobline $bf $diffids]
7007     $ctext config -state normal
7008     clear_ctext $commentend
7009     lappend ctext_file_names $f
7010     lappend ctext_file_lines [lindex [split $commentend "."] 0]
7011     $ctext insert end "\n"
7012     $ctext insert end "$f\n" filesep
7013     $ctext config -state disabled
7014     $ctext yview $commentend
7015     settabs 0
7016 }
7017
7018 proc getblobline {bf id} {
7019     global diffids cmitmode ctext
7020
7021     if {$id ne $diffids || $cmitmode ne "tree"} {
7022         catch {close $bf}
7023         return 0
7024     }
7025     $ctext config -state normal
7026     set nl 0
7027     while {[incr nl] <= 1000 && [gets $bf line] >= 0} {
7028         $ctext insert end "$line\n"
7029     }
7030     if {[eof $bf]} {
7031         global jump_to_here ctext_file_names commentend
7032
7033         # delete last newline
7034         $ctext delete "end - 2c" "end - 1c"
7035         close $bf
7036         if {$jump_to_here ne {} &&
7037             [lindex $jump_to_here 0] eq [lindex $ctext_file_names 0]} {
7038             set lnum [expr {[lindex $jump_to_here 1] +
7039                             [lindex [split $commentend .] 0]}]
7040             mark_ctext_line $lnum
7041         }
7042         return 0
7043     }
7044     $ctext config -state disabled
7045     return [expr {$nl >= 1000? 2: 1}]
7046 }
7047
7048 proc mark_ctext_line {lnum} {
7049     global ctext markbgcolor
7050
7051     $ctext tag delete omark
7052     $ctext tag add omark $lnum.0 "$lnum.0 + 1 line"
7053     $ctext tag conf omark -background $markbgcolor
7054     $ctext see $lnum.0
7055 }
7056
7057 proc mergediff {id} {
7058     global diffmergeid
7059     global diffids treediffs
7060     global parents curview
7061
7062     set diffmergeid $id
7063     set diffids $id
7064     set treediffs($id) {}
7065     set np [llength $parents($curview,$id)]
7066     settabs $np
7067     getblobdiffs $id
7068 }
7069
7070 proc startdiff {ids} {
7071     global treediffs diffids treepending diffmergeid nullid nullid2
7072
7073     settabs 1
7074     set diffids $ids
7075     catch {unset diffmergeid}
7076     if {![info exists treediffs($ids)] ||
7077         [lsearch -exact $ids $nullid] >= 0 ||
7078         [lsearch -exact $ids $nullid2] >= 0} {
7079         if {![info exists treepending]} {
7080             gettreediffs $ids
7081         }
7082     } else {
7083         addtocflist $ids
7084     }
7085 }
7086
7087 proc path_filter {filter name} {
7088     foreach p $filter {
7089         set l [string length $p]
7090         if {[string index $p end] eq "/"} {
7091             if {[string compare -length $l $p $name] == 0} {
7092                 return 1
7093             }
7094         } else {
7095             if {[string compare -length $l $p $name] == 0 &&
7096                 ([string length $name] == $l ||
7097                  [string index $name $l] eq "/")} {
7098                 return 1
7099             }
7100         }
7101     }
7102     return 0
7103 }
7104
7105 proc addtocflist {ids} {
7106     global treediffs
7107
7108     add_flist $treediffs($ids)
7109     getblobdiffs $ids
7110 }
7111
7112 proc diffcmd {ids flags} {
7113     global nullid nullid2
7114
7115     set i [lsearch -exact $ids $nullid]
7116     set j [lsearch -exact $ids $nullid2]
7117     if {$i >= 0} {
7118         if {[llength $ids] > 1 && $j < 0} {
7119             # comparing working directory with some specific revision
7120             set cmd [concat | git diff-index $flags]
7121             if {$i == 0} {
7122                 lappend cmd -R [lindex $ids 1]
7123             } else {
7124                 lappend cmd [lindex $ids 0]
7125             }
7126         } else {
7127             # comparing working directory with index
7128             set cmd [concat | git diff-files $flags]
7129             if {$j == 1} {
7130                 lappend cmd -R
7131             }
7132         }
7133     } elseif {$j >= 0} {
7134         set cmd [concat | git diff-index --cached $flags]
7135         if {[llength $ids] > 1} {
7136             # comparing index with specific revision
7137             if {$i == 0} {
7138                 lappend cmd -R [lindex $ids 1]
7139             } else {
7140                 lappend cmd [lindex $ids 0]
7141             }
7142         } else {
7143             # comparing index with HEAD
7144             lappend cmd HEAD
7145         }
7146     } else {
7147         set cmd [concat | git diff-tree -r $flags $ids]
7148     }
7149     return $cmd
7150 }
7151
7152 proc gettreediffs {ids} {
7153     global treediff treepending
7154
7155     if {[catch {set gdtf [open [diffcmd $ids {--no-commit-id}] r]}]} return
7156
7157     set treepending $ids
7158     set treediff {}
7159     fconfigure $gdtf -blocking 0 -encoding binary
7160     filerun $gdtf [list gettreediffline $gdtf $ids]
7161 }
7162
7163 proc gettreediffline {gdtf ids} {
7164     global treediff treediffs treepending diffids diffmergeid
7165     global cmitmode vfilelimit curview limitdiffs perfile_attrs
7166
7167     set nr 0
7168     set sublist {}
7169     set max 1000
7170     if {$perfile_attrs} {
7171         # cache_gitattr is slow, and even slower on win32 where we
7172         # have to invoke it for only about 30 paths at a time
7173         set max 500
7174         if {[tk windowingsystem] == "win32"} {
7175             set max 120
7176         }
7177     }
7178     while {[incr nr] <= $max && [gets $gdtf line] >= 0} {
7179         set i [string first "\t" $line]
7180         if {$i >= 0} {
7181             set file [string range $line [expr {$i+1}] end]
7182             if {[string index $file 0] eq "\""} {
7183                 set file [lindex $file 0]
7184             }
7185             set file [encoding convertfrom $file]
7186             if {$file ne [lindex $treediff end]} {
7187                 lappend treediff $file
7188                 lappend sublist $file
7189             }
7190         }
7191     }
7192     if {$perfile_attrs} {
7193         cache_gitattr encoding $sublist
7194     }
7195     if {![eof $gdtf]} {
7196         return [expr {$nr >= $max? 2: 1}]
7197     }
7198     close $gdtf
7199     if {$limitdiffs && $vfilelimit($curview) ne {}} {
7200         set flist {}
7201         foreach f $treediff {
7202             if {[path_filter $vfilelimit($curview) $f]} {
7203                 lappend flist $f
7204             }
7205         }
7206         set treediffs($ids) $flist
7207     } else {
7208         set treediffs($ids) $treediff
7209     }
7210     unset treepending
7211     if {$cmitmode eq "tree" && [llength $diffids] == 1} {
7212         gettree $diffids
7213     } elseif {$ids != $diffids} {
7214         if {![info exists diffmergeid]} {
7215             gettreediffs $diffids
7216         }
7217     } else {
7218         addtocflist $ids
7219     }
7220     return 0
7221 }
7222
7223 # empty string or positive integer
7224 proc diffcontextvalidate {v} {
7225     return [regexp {^(|[1-9][0-9]*)$} $v]
7226 }
7227
7228 proc diffcontextchange {n1 n2 op} {
7229     global diffcontextstring diffcontext
7230
7231     if {[string is integer -strict $diffcontextstring]} {
7232         if {$diffcontextstring > 0} {
7233             set diffcontext $diffcontextstring
7234             reselectline
7235         }
7236     }
7237 }
7238
7239 proc changeignorespace {} {
7240     reselectline
7241 }
7242
7243 proc getblobdiffs {ids} {
7244     global blobdifffd diffids env
7245     global diffinhdr treediffs
7246     global diffcontext
7247     global ignorespace
7248     global limitdiffs vfilelimit curview
7249     global diffencoding targetline diffnparents
7250
7251     set cmd [diffcmd $ids "-p -C --cc --no-commit-id -U$diffcontext"]
7252     if {$ignorespace} {
7253         append cmd " -w"
7254     }
7255     if {$limitdiffs && $vfilelimit($curview) ne {}} {
7256         set cmd [concat $cmd -- $vfilelimit($curview)]
7257     }
7258     if {[catch {set bdf [open $cmd r]} err]} {
7259         error_popup [mc "Error getting diffs: %s" $err]
7260         return
7261     }
7262     set targetline {}
7263     set diffnparents 0
7264     set diffinhdr 0
7265     set diffencoding [get_path_encoding {}]
7266     fconfigure $bdf -blocking 0 -encoding binary -eofchar {}
7267     set blobdifffd($ids) $bdf
7268     filerun $bdf [list getblobdiffline $bdf $diffids]
7269 }
7270
7271 proc setinlist {var i val} {
7272     global $var
7273
7274     while {[llength [set $var]] < $i} {
7275         lappend $var {}
7276     }
7277     if {[llength [set $var]] == $i} {
7278         lappend $var $val
7279     } else {
7280         lset $var $i $val
7281     }
7282 }
7283
7284 proc makediffhdr {fname ids} {
7285     global ctext curdiffstart treediffs diffencoding
7286     global ctext_file_names jump_to_here targetline diffline
7287
7288     set fname [encoding convertfrom $fname]
7289     set diffencoding [get_path_encoding $fname]
7290     set i [lsearch -exact $treediffs($ids) $fname]
7291     if {$i >= 0} {
7292         setinlist difffilestart $i $curdiffstart
7293     }
7294     lset ctext_file_names end $fname
7295     set l [expr {(78 - [string length $fname]) / 2}]
7296     set pad [string range "----------------------------------------" 1 $l]
7297     $ctext insert $curdiffstart "$pad $fname $pad" filesep
7298     set targetline {}
7299     if {$jump_to_here ne {} && [lindex $jump_to_here 0] eq $fname} {
7300         set targetline [lindex $jump_to_here 1]
7301     }
7302     set diffline 0
7303 }
7304
7305 proc getblobdiffline {bdf ids} {
7306     global diffids blobdifffd ctext curdiffstart
7307     global diffnexthead diffnextnote difffilestart
7308     global ctext_file_names ctext_file_lines
7309     global diffinhdr treediffs mergemax diffnparents
7310     global diffencoding jump_to_here targetline diffline
7311
7312     set nr 0
7313     $ctext conf -state normal
7314     while {[incr nr] <= 1000 && [gets $bdf line] >= 0} {
7315         if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
7316             close $bdf
7317             return 0
7318         }
7319         if {![string compare -length 5 "diff " $line]} {
7320             if {![regexp {^diff (--cc|--git) } $line m type]} {
7321                 set line [encoding convertfrom $line]
7322                 $ctext insert end "$line\n" hunksep
7323                 continue
7324             }
7325             # start of a new file
7326             set diffinhdr 1
7327             $ctext insert end "\n"
7328             set curdiffstart [$ctext index "end - 1c"]
7329             lappend ctext_file_names ""
7330             lappend ctext_file_lines [lindex [split $curdiffstart "."] 0]
7331             $ctext insert end "\n" filesep
7332
7333             if {$type eq "--cc"} {
7334                 # start of a new file in a merge diff
7335                 set fname [string range $line 10 end]
7336                 if {[lsearch -exact $treediffs($ids) $fname] < 0} {
7337                     lappend treediffs($ids) $fname
7338                     add_flist [list $fname]
7339                 }
7340
7341             } else {
7342                 set line [string range $line 11 end]
7343                 # If the name hasn't changed the length will be odd,
7344                 # the middle char will be a space, and the two bits either
7345                 # side will be a/name and b/name, or "a/name" and "b/name".
7346                 # If the name has changed we'll get "rename from" and
7347                 # "rename to" or "copy from" and "copy to" lines following
7348                 # this, and we'll use them to get the filenames.
7349                 # This complexity is necessary because spaces in the
7350                 # filename(s) don't get escaped.
7351                 set l [string length $line]
7352                 set i [expr {$l / 2}]
7353                 if {!(($l & 1) && [string index $line $i] eq " " &&
7354                       [string range $line 2 [expr {$i - 1}]] eq \
7355                           [string range $line [expr {$i + 3}] end])} {
7356                     continue
7357                 }
7358                 # unescape if quoted and chop off the a/ from the front
7359                 if {[string index $line 0] eq "\""} {
7360                     set fname [string range [lindex $line 0] 2 end]
7361                 } else {
7362                     set fname [string range $line 2 [expr {$i - 1}]]
7363                 }
7364             }
7365             makediffhdr $fname $ids
7366
7367         } elseif {![string compare -length 16 "* Unmerged path " $line]} {
7368             set fname [encoding convertfrom [string range $line 16 end]]
7369             $ctext insert end "\n"
7370             set curdiffstart [$ctext index "end - 1c"]
7371             lappend ctext_file_names $fname
7372             lappend ctext_file_lines [lindex [split $curdiffstart "."] 0]
7373             $ctext insert end "$line\n" filesep
7374             set i [lsearch -exact $treediffs($ids) $fname]
7375             if {$i >= 0} {
7376                 setinlist difffilestart $i $curdiffstart
7377             }
7378
7379         } elseif {![string compare -length 2 "@@" $line]} {
7380             regexp {^@@+} $line ats
7381             set line [encoding convertfrom $diffencoding $line]
7382             $ctext insert end "$line\n" hunksep
7383             if {[regexp { \+(\d+),\d+ @@} $line m nl]} {
7384                 set diffline $nl
7385             }
7386             set diffnparents [expr {[string length $ats] - 1}]
7387             set diffinhdr 0
7388
7389         } elseif {$diffinhdr} {
7390             if {![string compare -length 12 "rename from " $line]} {
7391                 set fname [string range $line [expr 6 + [string first " from " $line] ] end]
7392                 if {[string index $fname 0] eq "\""} {
7393                     set fname [lindex $fname 0]
7394                 }
7395                 set fname [encoding convertfrom $fname]
7396                 set i [lsearch -exact $treediffs($ids) $fname]
7397                 if {$i >= 0} {
7398                     setinlist difffilestart $i $curdiffstart
7399                 }
7400             } elseif {![string compare -length 10 $line "rename to "] ||
7401                       ![string compare -length 8 $line "copy to "]} {
7402                 set fname [string range $line [expr 4 + [string first " to " $line] ] end]
7403                 if {[string index $fname 0] eq "\""} {
7404                     set fname [lindex $fname 0]
7405                 }
7406                 makediffhdr $fname $ids
7407             } elseif {[string compare -length 3 $line "---"] == 0} {
7408                 # do nothing
7409                 continue
7410             } elseif {[string compare -length 3 $line "+++"] == 0} {
7411                 set diffinhdr 0
7412                 continue
7413             }
7414             $ctext insert end "$line\n" filesep
7415
7416         } else {
7417             set line [string map {\x1A ^Z} \
7418                           [encoding convertfrom $diffencoding $line]]
7419             # parse the prefix - one ' ', '-' or '+' for each parent
7420             set prefix [string range $line 0 [expr {$diffnparents - 1}]]
7421             set tag [expr {$diffnparents > 1? "m": "d"}]
7422             if {[string trim $prefix " -+"] eq {}} {
7423                 # prefix only has " ", "-" and "+" in it: normal diff line
7424                 set num [string first "-" $prefix]
7425                 if {$num >= 0} {
7426                     # removed line, first parent with line is $num
7427                     if {$num >= $mergemax} {
7428                         set num "max"
7429                     }
7430                     $ctext insert end "$line\n" $tag$num
7431                 } else {
7432                     set tags {}
7433                     if {[string first "+" $prefix] >= 0} {
7434                         # added line
7435                         lappend tags ${tag}result
7436                         if {$diffnparents > 1} {
7437                             set num [string first " " $prefix]
7438                             if {$num >= 0} {
7439                                 if {$num >= $mergemax} {
7440                                     set num "max"
7441                                 }
7442                                 lappend tags m$num
7443                             }
7444                         }
7445                     }
7446                     if {$targetline ne {}} {
7447                         if {$diffline == $targetline} {
7448                             set seehere [$ctext index "end - 1 chars"]
7449                             set targetline {}
7450                         } else {
7451                             incr diffline
7452                         }
7453                     }
7454                     $ctext insert end "$line\n" $tags
7455                 }
7456             } else {
7457                 # "\ No newline at end of file",
7458                 # or something else we don't recognize
7459                 $ctext insert end "$line\n" hunksep
7460             }
7461         }
7462     }
7463     if {[info exists seehere]} {
7464         mark_ctext_line [lindex [split $seehere .] 0]
7465     }
7466     $ctext conf -state disabled
7467     if {[eof $bdf]} {
7468         close $bdf
7469         return 0
7470     }
7471     return [expr {$nr >= 1000? 2: 1}]
7472 }
7473
7474 proc changediffdisp {} {
7475     global ctext diffelide
7476
7477     $ctext tag conf d0 -elide [lindex $diffelide 0]
7478     $ctext tag conf dresult -elide [lindex $diffelide 1]
7479 }
7480
7481 proc highlightfile {loc cline} {
7482     global ctext cflist cflist_top
7483
7484     $ctext yview $loc
7485     $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
7486     $cflist tag add highlight $cline.0 "$cline.0 lineend"
7487     $cflist see $cline.0
7488     set cflist_top $cline
7489 }
7490
7491 proc prevfile {} {
7492     global difffilestart ctext cmitmode
7493
7494     if {$cmitmode eq "tree"} return
7495     set prev 0.0
7496     set prevline 1
7497     set here [$ctext index @0,0]
7498     foreach loc $difffilestart {
7499         if {[$ctext compare $loc >= $here]} {
7500             highlightfile $prev $prevline
7501             return
7502         }
7503         set prev $loc
7504         incr prevline
7505     }
7506     highlightfile $prev $prevline
7507 }
7508
7509 proc nextfile {} {
7510     global difffilestart ctext cmitmode
7511
7512     if {$cmitmode eq "tree"} return
7513     set here [$ctext index @0,0]
7514     set line 1
7515     foreach loc $difffilestart {
7516         incr line
7517         if {[$ctext compare $loc > $here]} {
7518             highlightfile $loc $line
7519             return
7520         }
7521     }
7522 }
7523
7524 proc clear_ctext {{first 1.0}} {
7525     global ctext smarktop smarkbot
7526     global ctext_file_names ctext_file_lines
7527     global pendinglinks
7528
7529     set l [lindex [split $first .] 0]
7530     if {![info exists smarktop] || [$ctext compare $first < $smarktop.0]} {
7531         set smarktop $l
7532     }
7533     if {![info exists smarkbot] || [$ctext compare $first < $smarkbot.0]} {
7534         set smarkbot $l
7535     }
7536     $ctext delete $first end
7537     if {$first eq "1.0"} {
7538         catch {unset pendinglinks}
7539     }
7540     set ctext_file_names {}
7541     set ctext_file_lines {}
7542 }
7543
7544 proc settabs {{firstab {}}} {
7545     global firsttabstop tabstop ctext have_tk85
7546
7547     if {$firstab ne {} && $have_tk85} {
7548         set firsttabstop $firstab
7549     }
7550     set w [font measure textfont "0"]
7551     if {$firsttabstop != 0} {
7552         $ctext conf -tabs [list [expr {($firsttabstop + $tabstop) * $w}] \
7553                                [expr {($firsttabstop + 2 * $tabstop) * $w}]]
7554     } elseif {$have_tk85 || $tabstop != 8} {
7555         $ctext conf -tabs [expr {$tabstop * $w}]
7556     } else {
7557         $ctext conf -tabs {}
7558     }
7559 }
7560
7561 proc incrsearch {name ix op} {
7562     global ctext searchstring searchdirn
7563
7564     $ctext tag remove found 1.0 end
7565     if {[catch {$ctext index anchor}]} {
7566         # no anchor set, use start of selection, or of visible area
7567         set sel [$ctext tag ranges sel]
7568         if {$sel ne {}} {
7569             $ctext mark set anchor [lindex $sel 0]
7570         } elseif {$searchdirn eq "-forwards"} {
7571             $ctext mark set anchor @0,0
7572         } else {
7573             $ctext mark set anchor @0,[winfo height $ctext]
7574         }
7575     }
7576     if {$searchstring ne {}} {
7577         set here [$ctext search $searchdirn -- $searchstring anchor]
7578         if {$here ne {}} {
7579             $ctext see $here
7580         }
7581         searchmarkvisible 1
7582     }
7583 }
7584
7585 proc dosearch {} {
7586     global sstring ctext searchstring searchdirn
7587
7588     focus $sstring
7589     $sstring icursor end
7590     set searchdirn -forwards
7591     if {$searchstring ne {}} {
7592         set sel [$ctext tag ranges sel]
7593         if {$sel ne {}} {
7594             set start "[lindex $sel 0] + 1c"
7595         } elseif {[catch {set start [$ctext index anchor]}]} {
7596             set start "@0,0"
7597         }
7598         set match [$ctext search -count mlen -- $searchstring $start]
7599         $ctext tag remove sel 1.0 end
7600         if {$match eq {}} {
7601             bell
7602             return
7603         }
7604         $ctext see $match
7605         set mend "$match + $mlen c"
7606         $ctext tag add sel $match $mend
7607         $ctext mark unset anchor
7608     }
7609 }
7610
7611 proc dosearchback {} {
7612     global sstring ctext searchstring searchdirn
7613
7614     focus $sstring
7615     $sstring icursor end
7616     set searchdirn -backwards
7617     if {$searchstring ne {}} {
7618         set sel [$ctext tag ranges sel]
7619         if {$sel ne {}} {
7620             set start [lindex $sel 0]
7621         } elseif {[catch {set start [$ctext index anchor]}]} {
7622             set start @0,[winfo height $ctext]
7623         }
7624         set match [$ctext search -backwards -count ml -- $searchstring $start]
7625         $ctext tag remove sel 1.0 end
7626         if {$match eq {}} {
7627             bell
7628             return
7629         }
7630         $ctext see $match
7631         set mend "$match + $ml c"
7632         $ctext tag add sel $match $mend
7633         $ctext mark unset anchor
7634     }
7635 }
7636
7637 proc searchmark {first last} {
7638     global ctext searchstring
7639
7640     set mend $first.0
7641     while {1} {
7642         set match [$ctext search -count mlen -- $searchstring $mend $last.end]
7643         if {$match eq {}} break
7644         set mend "$match + $mlen c"
7645         $ctext tag add found $match $mend
7646     }
7647 }
7648
7649 proc searchmarkvisible {doall} {
7650     global ctext smarktop smarkbot
7651
7652     set topline [lindex [split [$ctext index @0,0] .] 0]
7653     set botline [lindex [split [$ctext index @0,[winfo height $ctext]] .] 0]
7654     if {$doall || $botline < $smarktop || $topline > $smarkbot} {
7655         # no overlap with previous
7656         searchmark $topline $botline
7657         set smarktop $topline
7658         set smarkbot $botline
7659     } else {
7660         if {$topline < $smarktop} {
7661             searchmark $topline [expr {$smarktop-1}]
7662             set smarktop $topline
7663         }
7664         if {$botline > $smarkbot} {
7665             searchmark [expr {$smarkbot+1}] $botline
7666             set smarkbot $botline
7667         }
7668     }
7669 }
7670
7671 proc scrolltext {f0 f1} {
7672     global searchstring
7673
7674     .bleft.bottom.sb set $f0 $f1
7675     if {$searchstring ne {}} {
7676         searchmarkvisible 0
7677     }
7678 }
7679
7680 proc setcoords {} {
7681     global linespc charspc canvx0 canvy0
7682     global xspc1 xspc2 lthickness
7683
7684     set linespc [font metrics mainfont -linespace]
7685     set charspc [font measure mainfont "m"]
7686     set canvy0 [expr {int(3 + 0.5 * $linespc)}]
7687     set canvx0 [expr {int(3 + 0.5 * $linespc)}]
7688     set lthickness [expr {int($linespc / 9) + 1}]
7689     set xspc1(0) $linespc
7690     set xspc2 $linespc
7691 }
7692
7693 proc redisplay {} {
7694     global canv
7695     global selectedline
7696
7697     set ymax [lindex [$canv cget -scrollregion] 3]
7698     if {$ymax eq {} || $ymax == 0} return
7699     set span [$canv yview]
7700     clear_display
7701     setcanvscroll
7702     allcanvs yview moveto [lindex $span 0]
7703     drawvisible
7704     if {$selectedline ne {}} {
7705         selectline $selectedline 0
7706         allcanvs yview moveto [lindex $span 0]
7707     }
7708 }
7709
7710 proc parsefont {f n} {
7711     global fontattr
7712
7713     set fontattr($f,family) [lindex $n 0]
7714     set s [lindex $n 1]
7715     if {$s eq {} || $s == 0} {
7716         set s 10
7717     } elseif {$s < 0} {
7718         set s [expr {int(-$s / [winfo fpixels . 1p] + 0.5)}]
7719     }
7720     set fontattr($f,size) $s
7721     set fontattr($f,weight) normal
7722     set fontattr($f,slant) roman
7723     foreach style [lrange $n 2 end] {
7724         switch -- $style {
7725             "normal" -
7726             "bold"   {set fontattr($f,weight) $style}
7727             "roman" -
7728             "italic" {set fontattr($f,slant) $style}
7729         }
7730     }
7731 }
7732
7733 proc fontflags {f {isbold 0}} {
7734     global fontattr
7735
7736     return [list -family $fontattr($f,family) -size $fontattr($f,size) \
7737                 -weight [expr {$isbold? "bold": $fontattr($f,weight)}] \
7738                 -slant $fontattr($f,slant)]
7739 }
7740
7741 proc fontname {f} {
7742     global fontattr
7743
7744     set n [list $fontattr($f,family) $fontattr($f,size)]
7745     if {$fontattr($f,weight) eq "bold"} {
7746         lappend n "bold"
7747     }
7748     if {$fontattr($f,slant) eq "italic"} {
7749         lappend n "italic"
7750     }
7751     return $n
7752 }
7753
7754 proc incrfont {inc} {
7755     global mainfont textfont ctext canv cflist showrefstop
7756     global stopped entries fontattr
7757
7758     unmarkmatches
7759     set s $fontattr(mainfont,size)
7760     incr s $inc
7761     if {$s < 1} {
7762         set s 1
7763     }
7764     set fontattr(mainfont,size) $s
7765     font config mainfont -size $s
7766     font config mainfontbold -size $s
7767     set mainfont [fontname mainfont]
7768     set s $fontattr(textfont,size)
7769     incr s $inc
7770     if {$s < 1} {
7771         set s 1
7772     }
7773     set fontattr(textfont,size) $s
7774     font config textfont -size $s
7775     font config textfontbold -size $s
7776     set textfont [fontname textfont]
7777     setcoords
7778     settabs
7779     redisplay
7780 }
7781
7782 proc clearsha1 {} {
7783     global sha1entry sha1string
7784     if {[string length $sha1string] == 40} {
7785         $sha1entry delete 0 end
7786     }
7787 }
7788
7789 proc sha1change {n1 n2 op} {
7790     global sha1string currentid sha1but
7791     if {$sha1string == {}
7792         || ([info exists currentid] && $sha1string == $currentid)} {
7793         set state disabled
7794     } else {
7795         set state normal
7796     }
7797     if {[$sha1but cget -state] == $state} return
7798     if {$state == "normal"} {
7799         $sha1but conf -state normal -relief raised -text "[mc "Goto:"] "
7800     } else {
7801         $sha1but conf -state disabled -relief flat -text "[mc "SHA1 ID:"] "
7802     }
7803 }
7804
7805 proc gotocommit {} {
7806     global sha1string tagids headids curview varcid
7807
7808     if {$sha1string == {}
7809         || ([info exists currentid] && $sha1string == $currentid)} return
7810     if {[info exists tagids($sha1string)]} {
7811         set id $tagids($sha1string)
7812     } elseif {[info exists headids($sha1string)]} {
7813         set id $headids($sha1string)
7814     } else {
7815         set id [string tolower $sha1string]
7816         if {[regexp {^[0-9a-f]{4,39}$} $id]} {
7817             set matches [longid $id]
7818             if {$matches ne {}} {
7819                 if {[llength $matches] > 1} {
7820                     error_popup [mc "Short SHA1 id %s is ambiguous" $id]
7821                     return
7822                 }
7823                 set id [lindex $matches 0]
7824             }
7825         }
7826     }
7827     if {[commitinview $id $curview]} {
7828         selectline [rowofcommit $id] 1
7829         return
7830     }
7831     if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
7832         set msg [mc "SHA1 id %s is not known" $sha1string]
7833     } else {
7834         set msg [mc "Tag/Head %s is not known" $sha1string]
7835     }
7836     error_popup $msg
7837 }
7838
7839 proc lineenter {x y id} {
7840     global hoverx hovery hoverid hovertimer
7841     global commitinfo canv
7842
7843     if {![info exists commitinfo($id)] && ![getcommit $id]} return
7844     set hoverx $x
7845     set hovery $y
7846     set hoverid $id
7847     if {[info exists hovertimer]} {
7848         after cancel $hovertimer
7849     }
7850     set hovertimer [after 500 linehover]
7851     $canv delete hover
7852 }
7853
7854 proc linemotion {x y id} {
7855     global hoverx hovery hoverid hovertimer
7856
7857     if {[info exists hoverid] && $id == $hoverid} {
7858         set hoverx $x
7859         set hovery $y
7860         if {[info exists hovertimer]} {
7861             after cancel $hovertimer
7862         }
7863         set hovertimer [after 500 linehover]
7864     }
7865 }
7866
7867 proc lineleave {id} {
7868     global hoverid hovertimer canv
7869
7870     if {[info exists hoverid] && $id == $hoverid} {
7871         $canv delete hover
7872         if {[info exists hovertimer]} {
7873             after cancel $hovertimer
7874             unset hovertimer
7875         }
7876         unset hoverid
7877     }
7878 }
7879
7880 proc linehover {} {
7881     global hoverx hovery hoverid hovertimer
7882     global canv linespc lthickness
7883     global commitinfo
7884
7885     set text [lindex $commitinfo($hoverid) 0]
7886     set ymax [lindex [$canv cget -scrollregion] 3]
7887     if {$ymax == {}} return
7888     set yfrac [lindex [$canv yview] 0]
7889     set x [expr {$hoverx + 2 * $linespc}]
7890     set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
7891     set x0 [expr {$x - 2 * $lthickness}]
7892     set y0 [expr {$y - 2 * $lthickness}]
7893     set x1 [expr {$x + [font measure mainfont $text] + 2 * $lthickness}]
7894     set y1 [expr {$y + $linespc + 2 * $lthickness}]
7895     set t [$canv create rectangle $x0 $y0 $x1 $y1 \
7896                -fill \#ffff80 -outline black -width 1 -tags hover]
7897     $canv raise $t
7898     set t [$canv create text $x $y -anchor nw -text $text -tags hover \
7899                -font mainfont]
7900     $canv raise $t
7901 }
7902
7903 proc clickisonarrow {id y} {
7904     global lthickness
7905
7906     set ranges [rowranges $id]
7907     set thresh [expr {2 * $lthickness + 6}]
7908     set n [expr {[llength $ranges] - 1}]
7909     for {set i 1} {$i < $n} {incr i} {
7910         set row [lindex $ranges $i]
7911         if {abs([yc $row] - $y) < $thresh} {
7912             return $i
7913         }
7914     }
7915     return {}
7916 }
7917
7918 proc arrowjump {id n y} {
7919     global canv
7920
7921     # 1 <-> 2, 3 <-> 4, etc...
7922     set n [expr {(($n - 1) ^ 1) + 1}]
7923     set row [lindex [rowranges $id] $n]
7924     set yt [yc $row]
7925     set ymax [lindex [$canv cget -scrollregion] 3]
7926     if {$ymax eq {} || $ymax <= 0} return
7927     set view [$canv yview]
7928     set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
7929     set yfrac [expr {$yt / $ymax - $yspan / 2}]
7930     if {$yfrac < 0} {
7931         set yfrac 0
7932     }
7933     allcanvs yview moveto $yfrac
7934 }
7935
7936 proc lineclick {x y id isnew} {
7937     global ctext commitinfo children canv thickerline curview
7938
7939     if {![info exists commitinfo($id)] && ![getcommit $id]} return
7940     unmarkmatches
7941     unselectline
7942     normalline
7943     $canv delete hover
7944     # draw this line thicker than normal
7945     set thickerline $id
7946     drawlines $id
7947     if {$isnew} {
7948         set ymax [lindex [$canv cget -scrollregion] 3]
7949         if {$ymax eq {}} return
7950         set yfrac [lindex [$canv yview] 0]
7951         set y [expr {$y + $yfrac * $ymax}]
7952     }
7953     set dirn [clickisonarrow $id $y]
7954     if {$dirn ne {}} {
7955         arrowjump $id $dirn $y
7956         return
7957     }
7958
7959     if {$isnew} {
7960         addtohistory [list lineclick $x $y $id 0]
7961     }
7962     # fill the details pane with info about this line
7963     $ctext conf -state normal
7964     clear_ctext
7965     settabs 0
7966     $ctext insert end "[mc "Parent"]:\t"
7967     $ctext insert end $id link0
7968     setlink $id link0
7969     set info $commitinfo($id)
7970     $ctext insert end "\n\t[lindex $info 0]\n"
7971     $ctext insert end "\t[mc "Author"]:\t[lindex $info 1]\n"
7972     set date [formatdate [lindex $info 2]]
7973     $ctext insert end "\t[mc "Date"]:\t$date\n"
7974     set kids $children($curview,$id)
7975     if {$kids ne {}} {
7976         $ctext insert end "\n[mc "Children"]:"
7977         set i 0
7978         foreach child $kids {
7979             incr i
7980             if {![info exists commitinfo($child)] && ![getcommit $child]} continue
7981             set info $commitinfo($child)
7982             $ctext insert end "\n\t"
7983             $ctext insert end $child link$i
7984             setlink $child link$i
7985             $ctext insert end "\n\t[lindex $info 0]"
7986             $ctext insert end "\n\t[mc "Author"]:\t[lindex $info 1]"
7987             set date [formatdate [lindex $info 2]]
7988             $ctext insert end "\n\t[mc "Date"]:\t$date\n"
7989         }
7990     }
7991     $ctext conf -state disabled
7992     init_flist {}
7993 }
7994
7995 proc normalline {} {
7996     global thickerline
7997     if {[info exists thickerline]} {
7998         set id $thickerline
7999         unset thickerline
8000         drawlines $id
8001     }
8002 }
8003
8004 proc selbyid {id} {
8005     global curview
8006     if {[commitinview $id $curview]} {
8007         selectline [rowofcommit $id] 1
8008     }
8009 }
8010
8011 proc mstime {} {
8012     global startmstime
8013     if {![info exists startmstime]} {
8014         set startmstime [clock clicks -milliseconds]
8015     }
8016     return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
8017 }
8018
8019 proc rowmenu {x y id} {
8020     global rowctxmenu selectedline rowmenuid curview
8021     global nullid nullid2 fakerowmenu mainhead markedid
8022
8023     stopfinding
8024     set rowmenuid $id
8025     if {$selectedline eq {} || [rowofcommit $id] eq $selectedline} {
8026         set state disabled
8027     } else {
8028         set state normal
8029     }
8030     if {$id ne $nullid && $id ne $nullid2} {
8031         set menu $rowctxmenu
8032         if {$mainhead ne {}} {
8033             $menu entryconfigure 7 -label [mc "Reset %s branch to here" $mainhead] -state normal
8034         } else {
8035             $menu entryconfigure 7 -label [mc "Detached head: can't reset" $mainhead] -state disabled
8036         }
8037         if {[info exists markedid] && $markedid ne $id} {
8038             $menu entryconfigure 9 -state normal
8039             $menu entryconfigure 10 -state normal
8040         } else {
8041             $menu entryconfigure 9 -state disabled
8042             $menu entryconfigure 10 -state disabled
8043         }
8044     } else {
8045         set menu $fakerowmenu
8046     }
8047     $menu entryconfigure [mca "Diff this -> selected"] -state $state
8048     $menu entryconfigure [mca "Diff selected -> this"] -state $state
8049     $menu entryconfigure [mca "Make patch"] -state $state
8050     tk_popup $menu $x $y
8051 }
8052
8053 proc markhere {} {
8054     global rowmenuid markedid canv
8055
8056     set markedid $rowmenuid
8057     make_idmark $markedid
8058 }
8059
8060 proc gotomark {} {
8061     global markedid
8062
8063     if {[info exists markedid]} {
8064         selbyid $markedid
8065     }
8066 }
8067
8068 proc replace_by_kids {l r} {
8069     global curview children
8070
8071     set id [commitonrow $r]
8072     set l [lreplace $l 0 0]
8073     foreach kid $children($curview,$id) {
8074         lappend l [rowofcommit $kid]
8075     }
8076     return [lsort -integer -decreasing -unique $l]
8077 }
8078
8079 proc find_common_desc {} {
8080     global markedid rowmenuid curview children
8081
8082     if {![info exists markedid]} return
8083     if {![commitinview $markedid $curview] ||
8084         ![commitinview $rowmenuid $curview]} return
8085     #set t1 [clock clicks -milliseconds]
8086     set l1 [list [rowofcommit $markedid]]
8087     set l2 [list [rowofcommit $rowmenuid]]
8088     while 1 {
8089         set r1 [lindex $l1 0]
8090         set r2 [lindex $l2 0]
8091         if {$r1 eq {} || $r2 eq {}} break
8092         if {$r1 == $r2} {
8093             selectline $r1 1
8094             break
8095         }
8096         if {$r1 > $r2} {
8097             set l1 [replace_by_kids $l1 $r1]
8098         } else {
8099             set l2 [replace_by_kids $l2 $r2]
8100         }
8101     }
8102     #set t2 [clock clicks -milliseconds]
8103     #puts "took [expr {$t2-$t1}]ms"
8104 }
8105
8106 proc diffvssel {dirn} {
8107     global rowmenuid selectedline
8108
8109     if {$selectedline eq {}} return
8110     if {$dirn} {
8111         set oldid [commitonrow $selectedline]
8112         set newid $rowmenuid
8113     } else {
8114         set oldid $rowmenuid
8115         set newid [commitonrow $selectedline]
8116     }
8117     addtohistory [list doseldiff $oldid $newid]
8118     doseldiff $oldid $newid
8119 }
8120
8121 proc doseldiff {oldid newid} {
8122     global ctext
8123     global commitinfo
8124
8125     $ctext conf -state normal
8126     clear_ctext
8127     init_flist [mc "Top"]
8128     $ctext insert end "[mc "From"] "
8129     $ctext insert end $oldid link0
8130     setlink $oldid link0
8131     $ctext insert end "\n     "
8132     $ctext insert end [lindex $commitinfo($oldid) 0]
8133     $ctext insert end "\n\n[mc "To"]   "
8134     $ctext insert end $newid link1
8135     setlink $newid link1
8136     $ctext insert end "\n     "
8137     $ctext insert end [lindex $commitinfo($newid) 0]
8138     $ctext insert end "\n"
8139     $ctext conf -state disabled
8140     $ctext tag remove found 1.0 end
8141     startdiff [list $oldid $newid]
8142 }
8143
8144 proc mkpatch {} {
8145     global rowmenuid currentid commitinfo patchtop patchnum
8146
8147     if {![info exists currentid]} return
8148     set oldid $currentid
8149     set oldhead [lindex $commitinfo($oldid) 0]
8150     set newid $rowmenuid
8151     set newhead [lindex $commitinfo($newid) 0]
8152     set top .patch
8153     set patchtop $top
8154     catch {destroy $top}
8155     toplevel $top
8156     make_transient $top .
8157     label $top.title -text [mc "Generate patch"]
8158     grid $top.title - -pady 10
8159     label $top.from -text [mc "From:"]
8160     entry $top.fromsha1 -width 40 -relief flat
8161     $top.fromsha1 insert 0 $oldid
8162     $top.fromsha1 conf -state readonly
8163     grid $top.from $top.fromsha1 -sticky w
8164     entry $top.fromhead -width 60 -relief flat
8165     $top.fromhead insert 0 $oldhead
8166     $top.fromhead conf -state readonly
8167     grid x $top.fromhead -sticky w
8168     label $top.to -text [mc "To:"]
8169     entry $top.tosha1 -width 40 -relief flat
8170     $top.tosha1 insert 0 $newid
8171     $top.tosha1 conf -state readonly
8172     grid $top.to $top.tosha1 -sticky w
8173     entry $top.tohead -width 60 -relief flat
8174     $top.tohead insert 0 $newhead
8175     $top.tohead conf -state readonly
8176     grid x $top.tohead -sticky w
8177     button $top.rev -text [mc "Reverse"] -command mkpatchrev -padx 5
8178     grid $top.rev x -pady 10
8179     label $top.flab -text [mc "Output file:"]
8180     entry $top.fname -width 60
8181     $top.fname insert 0 [file normalize "patch$patchnum.patch"]
8182     incr patchnum
8183     grid $top.flab $top.fname -sticky w
8184     frame $top.buts
8185     button $top.buts.gen -text [mc "Generate"] -command mkpatchgo
8186     button $top.buts.can -text [mc "Cancel"] -command mkpatchcan
8187     bind $top <Key-Return> mkpatchgo
8188     bind $top <Key-Escape> mkpatchcan
8189     grid $top.buts.gen $top.buts.can
8190     grid columnconfigure $top.buts 0 -weight 1 -uniform a
8191     grid columnconfigure $top.buts 1 -weight 1 -uniform a
8192     grid $top.buts - -pady 10 -sticky ew
8193     focus $top.fname
8194 }
8195
8196 proc mkpatchrev {} {
8197     global patchtop
8198
8199     set oldid [$patchtop.fromsha1 get]
8200     set oldhead [$patchtop.fromhead get]
8201     set newid [$patchtop.tosha1 get]
8202     set newhead [$patchtop.tohead get]
8203     foreach e [list fromsha1 fromhead tosha1 tohead] \
8204             v [list $newid $newhead $oldid $oldhead] {
8205         $patchtop.$e conf -state normal
8206         $patchtop.$e delete 0 end
8207         $patchtop.$e insert 0 $v
8208         $patchtop.$e conf -state readonly
8209     }
8210 }
8211
8212 proc mkpatchgo {} {
8213     global patchtop nullid nullid2
8214
8215     set oldid [$patchtop.fromsha1 get]
8216     set newid [$patchtop.tosha1 get]
8217     set fname [$patchtop.fname get]
8218     set cmd [diffcmd [list $oldid $newid] -p]
8219     # trim off the initial "|"
8220     set cmd [lrange $cmd 1 end]
8221     lappend cmd >$fname &
8222     if {[catch {eval exec $cmd} err]} {
8223         error_popup "[mc "Error creating patch:"] $err" $patchtop
8224     }
8225     catch {destroy $patchtop}
8226     unset patchtop
8227 }
8228
8229 proc mkpatchcan {} {
8230     global patchtop
8231
8232     catch {destroy $patchtop}
8233     unset patchtop
8234 }
8235
8236 proc mktag {} {
8237     global rowmenuid mktagtop commitinfo
8238
8239     set top .maketag
8240     set mktagtop $top
8241     catch {destroy $top}
8242     toplevel $top
8243     make_transient $top .
8244     label $top.title -text [mc "Create tag"]
8245     grid $top.title - -pady 10
8246     label $top.id -text [mc "ID:"]
8247     entry $top.sha1 -width 40 -relief flat
8248     $top.sha1 insert 0 $rowmenuid
8249     $top.sha1 conf -state readonly
8250     grid $top.id $top.sha1 -sticky w
8251     entry $top.head -width 60 -relief flat
8252     $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
8253     $top.head conf -state readonly
8254     grid x $top.head -sticky w
8255     label $top.tlab -text [mc "Tag name:"]
8256     entry $top.tag -width 60
8257     grid $top.tlab $top.tag -sticky w
8258     frame $top.buts
8259     button $top.buts.gen -text [mc "Create"] -command mktaggo
8260     button $top.buts.can -text [mc "Cancel"] -command mktagcan
8261     bind $top <Key-Return> mktaggo
8262     bind $top <Key-Escape> mktagcan
8263     grid $top.buts.gen $top.buts.can
8264     grid columnconfigure $top.buts 0 -weight 1 -uniform a
8265     grid columnconfigure $top.buts 1 -weight 1 -uniform a
8266     grid $top.buts - -pady 10 -sticky ew
8267     focus $top.tag
8268 }
8269
8270 proc domktag {} {
8271     global mktagtop env tagids idtags
8272
8273     set id [$mktagtop.sha1 get]
8274     set tag [$mktagtop.tag get]
8275     if {$tag == {}} {
8276         error_popup [mc "No tag name specified"] $mktagtop
8277         return 0
8278     }
8279     if {[info exists tagids($tag)]} {
8280         error_popup [mc "Tag \"%s\" already exists" $tag] $mktagtop
8281         return 0
8282     }
8283     if {[catch {
8284         exec git tag $tag $id
8285     } err]} {
8286         error_popup "[mc "Error creating tag:"] $err" $mktagtop
8287         return 0
8288     }
8289
8290     set tagids($tag) $id
8291     lappend idtags($id) $tag
8292     redrawtags $id
8293     addedtag $id
8294     dispneartags 0
8295     run refill_reflist
8296     return 1
8297 }
8298
8299 proc redrawtags {id} {
8300     global canv linehtag idpos currentid curview cmitlisted markedid
8301     global canvxmax iddrawn circleitem mainheadid circlecolors
8302
8303     if {![commitinview $id $curview]} return
8304     if {![info exists iddrawn($id)]} return
8305     set row [rowofcommit $id]
8306     if {$id eq $mainheadid} {
8307         set ofill yellow
8308     } else {
8309         set ofill [lindex $circlecolors $cmitlisted($curview,$id)]
8310     }
8311     $canv itemconf $circleitem($row) -fill $ofill
8312     $canv delete tag.$id
8313     set xt [eval drawtags $id $idpos($id)]
8314     $canv coords $linehtag($id) $xt [lindex $idpos($id) 2]
8315     set text [$canv itemcget $linehtag($id) -text]
8316     set font [$canv itemcget $linehtag($id) -font]
8317     set xr [expr {$xt + [font measure $font $text]}]
8318     if {$xr > $canvxmax} {
8319         set canvxmax $xr
8320         setcanvscroll
8321     }
8322     if {[info exists currentid] && $currentid == $id} {
8323         make_secsel $id
8324     }
8325     if {[info exists markedid] && $markedid eq $id} {
8326         make_idmark $id
8327     }
8328 }
8329
8330 proc mktagcan {} {
8331     global mktagtop
8332
8333     catch {destroy $mktagtop}
8334     unset mktagtop
8335 }
8336
8337 proc mktaggo {} {
8338     if {![domktag]} return
8339     mktagcan
8340 }
8341
8342 proc writecommit {} {
8343     global rowmenuid wrcomtop commitinfo wrcomcmd
8344
8345     set top .writecommit
8346     set wrcomtop $top
8347     catch {destroy $top}
8348     toplevel $top
8349     make_transient $top .
8350     label $top.title -text [mc "Write commit to file"]
8351     grid $top.title - -pady 10
8352     label $top.id -text [mc "ID:"]
8353     entry $top.sha1 -width 40 -relief flat
8354     $top.sha1 insert 0 $rowmenuid
8355     $top.sha1 conf -state readonly
8356     grid $top.id $top.sha1 -sticky w
8357     entry $top.head -width 60 -relief flat
8358     $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
8359     $top.head conf -state readonly
8360     grid x $top.head -sticky w
8361     label $top.clab -text [mc "Command:"]
8362     entry $top.cmd -width 60 -textvariable wrcomcmd
8363     grid $top.clab $top.cmd -sticky w -pady 10
8364     label $top.flab -text [mc "Output file:"]
8365     entry $top.fname -width 60
8366     $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
8367     grid $top.flab $top.fname -sticky w
8368     frame $top.buts
8369     button $top.buts.gen -text [mc "Write"] -command wrcomgo
8370     button $top.buts.can -text [mc "Cancel"] -command wrcomcan
8371     bind $top <Key-Return> wrcomgo
8372     bind $top <Key-Escape> wrcomcan
8373     grid $top.buts.gen $top.buts.can
8374     grid columnconfigure $top.buts 0 -weight 1 -uniform a
8375     grid columnconfigure $top.buts 1 -weight 1 -uniform a
8376     grid $top.buts - -pady 10 -sticky ew
8377     focus $top.fname
8378 }
8379
8380 proc wrcomgo {} {
8381     global wrcomtop
8382
8383     set id [$wrcomtop.sha1 get]
8384     set cmd "echo $id | [$wrcomtop.cmd get]"
8385     set fname [$wrcomtop.fname get]
8386     if {[catch {exec sh -c $cmd >$fname &} err]} {
8387         error_popup "[mc "Error writing commit:"] $err" $wrcomtop
8388     }
8389     catch {destroy $wrcomtop}
8390     unset wrcomtop
8391 }
8392
8393 proc wrcomcan {} {
8394     global wrcomtop
8395
8396     catch {destroy $wrcomtop}
8397     unset wrcomtop
8398 }
8399
8400 proc mkbranch {} {
8401     global rowmenuid mkbrtop
8402
8403     set top .makebranch
8404     catch {destroy $top}
8405     toplevel $top
8406     make_transient $top .
8407     label $top.title -text [mc "Create new branch"]
8408     grid $top.title - -pady 10
8409     label $top.id -text [mc "ID:"]
8410     entry $top.sha1 -width 40 -relief flat
8411     $top.sha1 insert 0 $rowmenuid
8412     $top.sha1 conf -state readonly
8413     grid $top.id $top.sha1 -sticky w
8414     label $top.nlab -text [mc "Name:"]
8415     entry $top.name -width 40
8416     grid $top.nlab $top.name -sticky w
8417     frame $top.buts
8418     button $top.buts.go -text [mc "Create"] -command [list mkbrgo $top]
8419     button $top.buts.can -text [mc "Cancel"] -command "catch {destroy $top}"
8420     bind $top <Key-Return> [list mkbrgo $top]
8421     bind $top <Key-Escape> "catch {destroy $top}"
8422     grid $top.buts.go $top.buts.can
8423     grid columnconfigure $top.buts 0 -weight 1 -uniform a
8424     grid columnconfigure $top.buts 1 -weight 1 -uniform a
8425     grid $top.buts - -pady 10 -sticky ew
8426     focus $top.name
8427 }
8428
8429 proc mkbrgo {top} {
8430     global headids idheads
8431
8432     set name [$top.name get]
8433     set id [$top.sha1 get]
8434     set cmdargs {}
8435     set old_id {}
8436     if {$name eq {}} {
8437         error_popup [mc "Please specify a name for the new branch"] $top
8438         return
8439     }
8440     if {[info exists headids($name)]} {
8441         if {![confirm_popup [mc \
8442                 "Branch '%s' already exists. Overwrite?" $name] $top]} {
8443             return
8444         }
8445         set old_id $headids($name)
8446         lappend cmdargs -f
8447     }
8448     catch {destroy $top}
8449     lappend cmdargs $name $id
8450     nowbusy newbranch
8451     update
8452     if {[catch {
8453         eval exec git branch $cmdargs
8454     } err]} {
8455         notbusy newbranch
8456         error_popup $err
8457     } else {
8458         notbusy newbranch
8459         if {$old_id ne {}} {
8460             movehead $id $name
8461             movedhead $id $name
8462             redrawtags $old_id
8463             redrawtags $id
8464         } else {
8465             set headids($name) $id
8466             lappend idheads($id) $name
8467             addedhead $id $name
8468             redrawtags $id
8469         }
8470         dispneartags 0
8471         run refill_reflist
8472     }
8473 }
8474
8475 proc exec_citool {tool_args {baseid {}}} {
8476     global commitinfo env
8477
8478     set save_env [array get env GIT_AUTHOR_*]
8479
8480     if {$baseid ne {}} {
8481         if {![info exists commitinfo($baseid)]} {
8482             getcommit $baseid
8483         }
8484         set author [lindex $commitinfo($baseid) 1]
8485         set date [lindex $commitinfo($baseid) 2]
8486         if {[regexp {^\s*(\S.*\S|\S)\s*<(.*)>\s*$} \
8487                     $author author name email]
8488             && $date ne {}} {
8489             set env(GIT_AUTHOR_NAME) $name
8490             set env(GIT_AUTHOR_EMAIL) $email
8491             set env(GIT_AUTHOR_DATE) $date
8492         }
8493     }
8494
8495     eval exec git citool $tool_args &
8496
8497     array unset env GIT_AUTHOR_*
8498     array set env $save_env
8499 }
8500
8501 proc cherrypick {} {
8502     global rowmenuid curview
8503     global mainhead mainheadid
8504
8505     set oldhead [exec git rev-parse HEAD]
8506     set dheads [descheads $rowmenuid]
8507     if {$dheads ne {} && [lsearch -exact $dheads $oldhead] >= 0} {
8508         set ok [confirm_popup [mc "Commit %s is already\
8509                 included in branch %s -- really re-apply it?" \
8510                                    [string range $rowmenuid 0 7] $mainhead]]
8511         if {!$ok} return
8512     }
8513     nowbusy cherrypick [mc "Cherry-picking"]
8514     update
8515     # Unfortunately git-cherry-pick writes stuff to stderr even when
8516     # no error occurs, and exec takes that as an indication of error...
8517     if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} {
8518         notbusy cherrypick
8519         if {[regexp -line \
8520                  {Entry '(.*)' (would be overwritten by merge|not uptodate)} \
8521                  $err msg fname]} {
8522             error_popup [mc "Cherry-pick failed because of local changes\
8523                         to file '%s'.\nPlease commit, reset or stash\
8524                         your changes and try again." $fname]
8525         } elseif {[regexp -line \
8526                        {^(CONFLICT \(.*\):|Automatic cherry-pick failed)} \
8527                        $err]} {
8528             if {[confirm_popup [mc "Cherry-pick failed because of merge\
8529                         conflict.\nDo you wish to run git citool to\
8530                         resolve it?"]]} {
8531                 # Force citool to read MERGE_MSG
8532                 file delete [file join [gitdir] "GITGUI_MSG"]
8533                 exec_citool {} $rowmenuid
8534             }
8535         } else {
8536             error_popup $err
8537         }
8538         run updatecommits
8539         return
8540     }
8541     set newhead [exec git rev-parse HEAD]
8542     if {$newhead eq $oldhead} {
8543         notbusy cherrypick
8544         error_popup [mc "No changes committed"]
8545         return
8546     }
8547     addnewchild $newhead $oldhead
8548     if {[commitinview $oldhead $curview]} {
8549         # XXX this isn't right if we have a path limit...
8550         insertrow $newhead $oldhead $curview
8551         if {$mainhead ne {}} {
8552             movehead $newhead $mainhead
8553             movedhead $newhead $mainhead
8554         }
8555         set mainheadid $newhead
8556         redrawtags $oldhead
8557         redrawtags $newhead
8558         selbyid $newhead
8559     }
8560     notbusy cherrypick
8561 }
8562
8563 proc resethead {} {
8564     global mainhead rowmenuid confirm_ok resettype
8565
8566     set confirm_ok 0
8567     set w ".confirmreset"
8568     toplevel $w
8569     make_transient $w .
8570     wm title $w [mc "Confirm reset"]
8571     message $w.m -text \
8572         [mc "Reset branch %s to %s?" $mainhead [string range $rowmenuid 0 7]] \
8573         -justify center -aspect 1000
8574     pack $w.m -side top -fill x -padx 20 -pady 20
8575     frame $w.f -relief sunken -border 2
8576     message $w.f.rt -text [mc "Reset type:"] -aspect 1000
8577     grid $w.f.rt -sticky w
8578     set resettype mixed
8579     radiobutton $w.f.soft -value soft -variable resettype -justify left \
8580         -text [mc "Soft: Leave working tree and index untouched"]
8581     grid $w.f.soft -sticky w
8582     radiobutton $w.f.mixed -value mixed -variable resettype -justify left \
8583         -text [mc "Mixed: Leave working tree untouched, reset index"]
8584     grid $w.f.mixed -sticky w
8585     radiobutton $w.f.hard -value hard -variable resettype -justify left \
8586         -text [mc "Hard: Reset working tree and index\n(discard ALL local changes)"]
8587     grid $w.f.hard -sticky w
8588     pack $w.f -side top -fill x
8589     button $w.ok -text [mc OK] -command "set confirm_ok 1; destroy $w"
8590     pack $w.ok -side left -fill x -padx 20 -pady 20
8591     button $w.cancel -text [mc Cancel] -command "destroy $w"
8592     bind $w <Key-Escape> [list destroy $w]
8593     pack $w.cancel -side right -fill x -padx 20 -pady 20
8594     bind $w <Visibility> "grab $w; focus $w"
8595     tkwait window $w
8596     if {!$confirm_ok} return
8597     if {[catch {set fd [open \
8598             [list | git reset --$resettype $rowmenuid 2>@1] r]} err]} {
8599         error_popup $err
8600     } else {
8601         dohidelocalchanges
8602         filerun $fd [list readresetstat $fd]
8603         nowbusy reset [mc "Resetting"]
8604         selbyid $rowmenuid
8605     }
8606 }
8607
8608 proc readresetstat {fd} {
8609     global mainhead mainheadid showlocalchanges rprogcoord
8610
8611     if {[gets $fd line] >= 0} {
8612         if {[regexp {([0-9]+)% \(([0-9]+)/([0-9]+)\)} $line match p m n]} {
8613             set rprogcoord [expr {1.0 * $m / $n}]
8614             adjustprogress
8615         }
8616         return 1
8617     }
8618     set rprogcoord 0
8619     adjustprogress
8620     notbusy reset
8621     if {[catch {close $fd} err]} {
8622         error_popup $err
8623     }
8624     set oldhead $mainheadid
8625     set newhead [exec git rev-parse HEAD]
8626     if {$newhead ne $oldhead} {
8627         movehead $newhead $mainhead
8628         movedhead $newhead $mainhead
8629         set mainheadid $newhead
8630         redrawtags $oldhead
8631         redrawtags $newhead
8632     }
8633     if {$showlocalchanges} {
8634         doshowlocalchanges
8635     }
8636     return 0
8637 }
8638
8639 # context menu for a head
8640 proc headmenu {x y id head} {
8641     global headmenuid headmenuhead headctxmenu mainhead
8642
8643     stopfinding
8644     set headmenuid $id
8645     set headmenuhead $head
8646     set state normal
8647     if {$head eq $mainhead} {
8648         set state disabled
8649     }
8650     $headctxmenu entryconfigure 0 -state $state
8651     $headctxmenu entryconfigure 1 -state $state
8652     tk_popup $headctxmenu $x $y
8653 }
8654
8655 proc cobranch {} {
8656     global headmenuid headmenuhead headids
8657     global showlocalchanges
8658
8659     # check the tree is clean first??
8660     nowbusy checkout [mc "Checking out"]
8661     update
8662     dohidelocalchanges
8663     if {[catch {
8664         set fd [open [list | git checkout $headmenuhead 2>@1] r]
8665     } err]} {
8666         notbusy checkout
8667         error_popup $err
8668         if {$showlocalchanges} {
8669             dodiffindex
8670         }
8671     } else {
8672         filerun $fd [list readcheckoutstat $fd $headmenuhead $headmenuid]
8673     }
8674 }
8675
8676 proc readcheckoutstat {fd newhead newheadid} {
8677     global mainhead mainheadid headids showlocalchanges progresscoords
8678     global viewmainheadid curview
8679
8680     if {[gets $fd line] >= 0} {
8681         if {[regexp {([0-9]+)% \(([0-9]+)/([0-9]+)\)} $line match p m n]} {
8682             set progresscoords [list 0 [expr {1.0 * $m / $n}]]
8683             adjustprogress
8684         }
8685         return 1
8686     }
8687     set progresscoords {0 0}
8688     adjustprogress
8689     notbusy checkout
8690     if {[catch {close $fd} err]} {
8691         error_popup $err
8692     }
8693     set oldmainid $mainheadid
8694     set mainhead $newhead
8695     set mainheadid $newheadid
8696     set viewmainheadid($curview) $newheadid
8697     redrawtags $oldmainid
8698     redrawtags $newheadid
8699     selbyid $newheadid
8700     if {$showlocalchanges} {
8701         dodiffindex
8702     }
8703 }
8704
8705 proc rmbranch {} {
8706     global headmenuid headmenuhead mainhead
8707     global idheads
8708
8709     set head $headmenuhead
8710     set id $headmenuid
8711     # this check shouldn't be needed any more...
8712     if {$head eq $mainhead} {
8713         error_popup [mc "Cannot delete the currently checked-out branch"]
8714         return
8715     }
8716     set dheads [descheads $id]
8717     if {[llength $dheads] == 1 && $idheads($dheads) eq $head} {
8718         # the stuff on this branch isn't on any other branch
8719         if {![confirm_popup [mc "The commits on branch %s aren't on any other\
8720                         branch.\nReally delete branch %s?" $head $head]]} return
8721     }
8722     nowbusy rmbranch
8723     update
8724     if {[catch {exec git branch -D $head} err]} {
8725         notbusy rmbranch
8726         error_popup $err
8727         return
8728     }
8729     removehead $id $head
8730     removedhead $id $head
8731     redrawtags $id
8732     notbusy rmbranch
8733     dispneartags 0
8734     run refill_reflist
8735 }
8736
8737 # Display a list of tags and heads
8738 proc showrefs {} {
8739     global showrefstop bgcolor fgcolor selectbgcolor
8740     global bglist fglist reflistfilter reflist maincursor
8741
8742     set top .showrefs
8743     set showrefstop $top
8744     if {[winfo exists $top]} {
8745         raise $top
8746         refill_reflist
8747         return
8748     }
8749     toplevel $top
8750     wm title $top [mc "Tags and heads: %s" [file tail [pwd]]]
8751     make_transient $top .
8752     text $top.list -background $bgcolor -foreground $fgcolor \
8753         -selectbackground $selectbgcolor -font mainfont \
8754         -xscrollcommand "$top.xsb set" -yscrollcommand "$top.ysb set" \
8755         -width 30 -height 20 -cursor $maincursor \
8756         -spacing1 1 -spacing3 1 -state disabled
8757     $top.list tag configure highlight -background $selectbgcolor
8758     lappend bglist $top.list
8759     lappend fglist $top.list
8760     scrollbar $top.ysb -command "$top.list yview" -orient vertical
8761     scrollbar $top.xsb -command "$top.list xview" -orient horizontal
8762     grid $top.list $top.ysb -sticky nsew
8763     grid $top.xsb x -sticky ew
8764     frame $top.f
8765     label $top.f.l -text "[mc "Filter"]: "
8766     entry $top.f.e -width 20 -textvariable reflistfilter
8767     set reflistfilter "*"
8768     trace add variable reflistfilter write reflistfilter_change
8769     pack $top.f.e -side right -fill x -expand 1
8770     pack $top.f.l -side left
8771     grid $top.f - -sticky ew -pady 2
8772     button $top.close -command [list destroy $top] -text [mc "Close"]
8773     bind $top <Key-Escape> [list destroy $top]
8774     grid $top.close -
8775     grid columnconfigure $top 0 -weight 1
8776     grid rowconfigure $top 0 -weight 1
8777     bind $top.list <1> {break}
8778     bind $top.list <B1-Motion> {break}
8779     bind $top.list <ButtonRelease-1> {sel_reflist %W %x %y; break}
8780     set reflist {}
8781     refill_reflist
8782 }
8783
8784 proc sel_reflist {w x y} {
8785     global showrefstop reflist headids tagids otherrefids
8786
8787     if {![winfo exists $showrefstop]} return
8788     set l [lindex [split [$w index "@$x,$y"] "."] 0]
8789     set ref [lindex $reflist [expr {$l-1}]]
8790     set n [lindex $ref 0]
8791     switch -- [lindex $ref 1] {
8792         "H" {selbyid $headids($n)}
8793         "T" {selbyid $tagids($n)}
8794         "o" {selbyid $otherrefids($n)}
8795     }
8796     $showrefstop.list tag add highlight $l.0 "$l.0 lineend"
8797 }
8798
8799 proc unsel_reflist {} {
8800     global showrefstop
8801
8802     if {![info exists showrefstop] || ![winfo exists $showrefstop]} return
8803     $showrefstop.list tag remove highlight 0.0 end
8804 }
8805
8806 proc reflistfilter_change {n1 n2 op} {
8807     global reflistfilter
8808
8809     after cancel refill_reflist
8810     after 200 refill_reflist
8811 }
8812
8813 proc refill_reflist {} {
8814     global reflist reflistfilter showrefstop headids tagids otherrefids
8815     global curview
8816
8817     if {![info exists showrefstop] || ![winfo exists $showrefstop]} return
8818     set refs {}
8819     foreach n [array names headids] {
8820         if {[string match $reflistfilter $n]} {
8821             if {[commitinview $headids($n) $curview]} {
8822                 lappend refs [list $n H]
8823             } else {
8824                 interestedin $headids($n) {run refill_reflist}
8825             }
8826         }
8827     }
8828     foreach n [array names tagids] {
8829         if {[string match $reflistfilter $n]} {
8830             if {[commitinview $tagids($n) $curview]} {
8831                 lappend refs [list $n T]
8832             } else {
8833                 interestedin $tagids($n) {run refill_reflist}
8834             }
8835         }
8836     }
8837     foreach n [array names otherrefids] {
8838         if {[string match $reflistfilter $n]} {
8839             if {[commitinview $otherrefids($n) $curview]} {
8840                 lappend refs [list $n o]
8841             } else {
8842                 interestedin $otherrefids($n) {run refill_reflist}
8843             }
8844         }
8845     }
8846     set refs [lsort -index 0 $refs]
8847     if {$refs eq $reflist} return
8848
8849     # Update the contents of $showrefstop.list according to the
8850     # differences between $reflist (old) and $refs (new)
8851     $showrefstop.list conf -state normal
8852     $showrefstop.list insert end "\n"
8853     set i 0
8854     set j 0
8855     while {$i < [llength $reflist] || $j < [llength $refs]} {
8856         if {$i < [llength $reflist]} {
8857             if {$j < [llength $refs]} {
8858                 set cmp [string compare [lindex $reflist $i 0] \
8859                              [lindex $refs $j 0]]
8860                 if {$cmp == 0} {
8861                     set cmp [string compare [lindex $reflist $i 1] \
8862                                  [lindex $refs $j 1]]
8863                 }
8864             } else {
8865                 set cmp -1
8866             }
8867         } else {
8868             set cmp 1
8869         }
8870         switch -- $cmp {
8871             -1 {
8872                 $showrefstop.list delete "[expr {$j+1}].0" "[expr {$j+2}].0"
8873                 incr i
8874             }
8875             0 {
8876                 incr i
8877                 incr j
8878             }
8879             1 {
8880                 set l [expr {$j + 1}]
8881                 $showrefstop.list image create $l.0 -align baseline \
8882                     -image reficon-[lindex $refs $j 1] -padx 2
8883                 $showrefstop.list insert $l.1 "[lindex $refs $j 0]\n"
8884                 incr j
8885             }
8886         }
8887     }
8888     set reflist $refs
8889     # delete last newline
8890     $showrefstop.list delete end-2c end-1c
8891     $showrefstop.list conf -state disabled
8892 }
8893
8894 # Stuff for finding nearby tags
8895 proc getallcommits {} {
8896     global allcommits nextarc seeds allccache allcwait cachedarcs allcupdate
8897     global idheads idtags idotherrefs allparents tagobjid
8898
8899     if {![info exists allcommits]} {
8900         set nextarc 0
8901         set allcommits 0
8902         set seeds {}
8903         set allcwait 0
8904         set cachedarcs 0
8905         set allccache [file join [gitdir] "gitk.cache"]
8906         if {![catch {
8907             set f [open $allccache r]
8908             set allcwait 1
8909             getcache $f
8910         }]} return
8911     }
8912
8913     if {$allcwait} {
8914         return
8915     }
8916     set cmd [list | git rev-list --parents]
8917     set allcupdate [expr {$seeds ne {}}]
8918     if {!$allcupdate} {
8919         set ids "--all"
8920     } else {
8921         set refs [concat [array names idheads] [array names idtags] \
8922                       [array names idotherrefs]]
8923         set ids {}
8924         set tagobjs {}
8925         foreach name [array names tagobjid] {
8926             lappend tagobjs $tagobjid($name)
8927         }
8928         foreach id [lsort -unique $refs] {
8929             if {![info exists allparents($id)] &&
8930                 [lsearch -exact $tagobjs $id] < 0} {
8931                 lappend ids $id
8932             }
8933         }
8934         if {$ids ne {}} {
8935             foreach id $seeds {
8936                 lappend ids "^$id"
8937             }
8938         }
8939     }
8940     if {$ids ne {}} {
8941         set fd [open [concat $cmd $ids] r]
8942         fconfigure $fd -blocking 0
8943         incr allcommits
8944         nowbusy allcommits
8945         filerun $fd [list getallclines $fd]
8946     } else {
8947         dispneartags 0
8948     }
8949 }
8950
8951 # Since most commits have 1 parent and 1 child, we group strings of
8952 # such commits into "arcs" joining branch/merge points (BMPs), which
8953 # are commits that either don't have 1 parent or don't have 1 child.
8954 #
8955 # arcnos(id) - incoming arcs for BMP, arc we're on for other nodes
8956 # arcout(id) - outgoing arcs for BMP
8957 # arcids(a) - list of IDs on arc including end but not start
8958 # arcstart(a) - BMP ID at start of arc
8959 # arcend(a) - BMP ID at end of arc
8960 # growing(a) - arc a is still growing
8961 # arctags(a) - IDs out of arcids (excluding end) that have tags
8962 # archeads(a) - IDs out of arcids (excluding end) that have heads
8963 # The start of an arc is at the descendent end, so "incoming" means
8964 # coming from descendents, and "outgoing" means going towards ancestors.
8965
8966 proc getallclines {fd} {
8967     global allparents allchildren idtags idheads nextarc
8968     global arcnos arcids arctags arcout arcend arcstart archeads growing
8969     global seeds allcommits cachedarcs allcupdate
8970     
8971     set nid 0
8972     while {[incr nid] <= 1000 && [gets $fd line] >= 0} {
8973         set id [lindex $line 0]
8974         if {[info exists allparents($id)]} {
8975             # seen it already
8976             continue
8977         }
8978         set cachedarcs 0
8979         set olds [lrange $line 1 end]
8980         set allparents($id) $olds
8981         if {![info exists allchildren($id)]} {
8982             set allchildren($id) {}
8983             set arcnos($id) {}
8984             lappend seeds $id
8985         } else {
8986             set a $arcnos($id)
8987             if {[llength $olds] == 1 && [llength $a] == 1} {
8988                 lappend arcids($a) $id
8989                 if {[info exists idtags($id)]} {
8990                     lappend arctags($a) $id
8991                 }
8992                 if {[info exists idheads($id)]} {
8993                     lappend archeads($a) $id
8994                 }
8995                 if {[info exists allparents($olds)]} {
8996                     # seen parent already
8997                     if {![info exists arcout($olds)]} {
8998                         splitarc $olds
8999                     }
9000                     lappend arcids($a) $olds
9001                     set arcend($a) $olds
9002                     unset growing($a)
9003                 }
9004                 lappend allchildren($olds) $id
9005                 lappend arcnos($olds) $a
9006                 continue
9007             }
9008         }
9009         foreach a $arcnos($id) {
9010             lappend arcids($a) $id
9011             set arcend($a) $id
9012             unset growing($a)
9013         }
9014
9015         set ao {}
9016         foreach p $olds {
9017             lappend allchildren($p) $id
9018             set a [incr nextarc]
9019             set arcstart($a) $id
9020             set archeads($a) {}
9021             set arctags($a) {}
9022             set archeads($a) {}
9023             set arcids($a) {}
9024             lappend ao $a
9025             set growing($a) 1
9026             if {[info exists allparents($p)]} {
9027                 # seen it already, may need to make a new branch
9028                 if {![info exists arcout($p)]} {
9029                     splitarc $p
9030                 }
9031                 lappend arcids($a) $p
9032                 set arcend($a) $p
9033                 unset growing($a)
9034             }
9035             lappend arcnos($p) $a
9036         }
9037         set arcout($id) $ao
9038     }
9039     if {$nid > 0} {
9040         global cached_dheads cached_dtags cached_atags
9041         catch {unset cached_dheads}
9042         catch {unset cached_dtags}
9043         catch {unset cached_atags}
9044     }
9045     if {![eof $fd]} {
9046         return [expr {$nid >= 1000? 2: 1}]
9047     }
9048     set cacheok 1
9049     if {[catch {
9050         fconfigure $fd -blocking 1
9051         close $fd
9052     } err]} {
9053         # got an error reading the list of commits
9054         # if we were updating, try rereading the whole thing again
9055         if {$allcupdate} {
9056             incr allcommits -1
9057             dropcache $err
9058             return
9059         }
9060         error_popup "[mc "Error reading commit topology information;\
9061                 branch and preceding/following tag information\
9062                 will be incomplete."]\n($err)"
9063         set cacheok 0
9064     }
9065     if {[incr allcommits -1] == 0} {
9066         notbusy allcommits
9067         if {$cacheok} {
9068             run savecache
9069         }
9070     }
9071     dispneartags 0
9072     return 0
9073 }
9074
9075 proc recalcarc {a} {
9076     global arctags archeads arcids idtags idheads
9077
9078     set at {}
9079     set ah {}
9080     foreach id [lrange $arcids($a) 0 end-1] {
9081         if {[info exists idtags($id)]} {
9082             lappend at $id
9083         }
9084         if {[info exists idheads($id)]} {
9085             lappend ah $id
9086         }
9087     }
9088     set arctags($a) $at
9089     set archeads($a) $ah
9090 }
9091
9092 proc splitarc {p} {
9093     global arcnos arcids nextarc arctags archeads idtags idheads
9094     global arcstart arcend arcout allparents growing
9095
9096     set a $arcnos($p)
9097     if {[llength $a] != 1} {
9098         puts "oops splitarc called but [llength $a] arcs already"
9099         return
9100     }
9101     set a [lindex $a 0]
9102     set i [lsearch -exact $arcids($a) $p]
9103     if {$i < 0} {
9104         puts "oops splitarc $p not in arc $a"
9105         return
9106     }
9107     set na [incr nextarc]
9108     if {[info exists arcend($a)]} {
9109         set arcend($na) $arcend($a)
9110     } else {
9111         set l [lindex $allparents([lindex $arcids($a) end]) 0]
9112         set j [lsearch -exact $arcnos($l) $a]
9113         set arcnos($l) [lreplace $arcnos($l) $j $j $na]
9114     }
9115     set tail [lrange $arcids($a) [expr {$i+1}] end]
9116     set arcids($a) [lrange $arcids($a) 0 $i]
9117     set arcend($a) $p
9118     set arcstart($na) $p
9119     set arcout($p) $na
9120     set arcids($na) $tail
9121     if {[info exists growing($a)]} {
9122         set growing($na) 1
9123         unset growing($a)
9124     }
9125
9126     foreach id $tail {
9127         if {[llength $arcnos($id)] == 1} {
9128             set arcnos($id) $na
9129         } else {
9130             set j [lsearch -exact $arcnos($id) $a]
9131             set arcnos($id) [lreplace $arcnos($id) $j $j $na]
9132         }
9133     }
9134
9135     # reconstruct tags and heads lists
9136     if {$arctags($a) ne {} || $archeads($a) ne {}} {
9137         recalcarc $a
9138         recalcarc $na
9139     } else {
9140         set arctags($na) {}
9141         set archeads($na) {}
9142     }
9143 }
9144
9145 # Update things for a new commit added that is a child of one
9146 # existing commit.  Used when cherry-picking.
9147 proc addnewchild {id p} {
9148     global allparents allchildren idtags nextarc
9149     global arcnos arcids arctags arcout arcend arcstart archeads growing
9150     global seeds allcommits
9151
9152     if {![info exists allcommits] || ![info exists arcnos($p)]} return
9153     set allparents($id) [list $p]
9154     set allchildren($id) {}
9155     set arcnos($id) {}
9156     lappend seeds $id
9157     lappend allchildren($p) $id
9158     set a [incr nextarc]
9159     set arcstart($a) $id
9160     set archeads($a) {}
9161     set arctags($a) {}
9162     set arcids($a) [list $p]
9163     set arcend($a) $p
9164     if {![info exists arcout($p)]} {
9165         splitarc $p
9166     }
9167     lappend arcnos($p) $a
9168     set arcout($id) [list $a]
9169 }
9170
9171 # This implements a cache for the topology information.
9172 # The cache saves, for each arc, the start and end of the arc,
9173 # the ids on the arc, and the outgoing arcs from the end.
9174 proc readcache {f} {
9175     global arcnos arcids arcout arcstart arcend arctags archeads nextarc
9176     global idtags idheads allparents cachedarcs possible_seeds seeds growing
9177     global allcwait
9178
9179     set a $nextarc
9180     set lim $cachedarcs
9181     if {$lim - $a > 500} {
9182         set lim [expr {$a + 500}]
9183     }
9184     if {[catch {
9185         if {$a == $lim} {
9186             # finish reading the cache and setting up arctags, etc.
9187             set line [gets $f]
9188             if {$line ne "1"} {error "bad final version"}
9189             close $f
9190             foreach id [array names idtags] {
9191                 if {[info exists arcnos($id)] && [llength $arcnos($id)] == 1 &&
9192                     [llength $allparents($id)] == 1} {
9193                     set a [lindex $arcnos($id) 0]
9194                     if {$arctags($a) eq {}} {
9195                         recalcarc $a
9196                     }
9197                 }
9198             }
9199             foreach id [array names idheads] {
9200                 if {[info exists arcnos($id)] && [llength $arcnos($id)] == 1 &&
9201                     [llength $allparents($id)] == 1} {
9202                     set a [lindex $arcnos($id) 0]
9203                     if {$archeads($a) eq {}} {
9204                         recalcarc $a
9205                     }
9206                 }
9207             }
9208             foreach id [lsort -unique $possible_seeds] {
9209                 if {$arcnos($id) eq {}} {
9210                     lappend seeds $id
9211                 }
9212             }
9213             set allcwait 0
9214         } else {
9215             while {[incr a] <= $lim} {
9216                 set line [gets $f]
9217                 if {[llength $line] != 3} {error "bad line"}
9218                 set s [lindex $line 0]
9219                 set arcstart($a) $s
9220                 lappend arcout($s) $a
9221                 if {![info exists arcnos($s)]} {
9222                     lappend possible_seeds $s
9223                     set arcnos($s) {}
9224                 }
9225                 set e [lindex $line 1]
9226                 if {$e eq {}} {
9227                     set growing($a) 1
9228                 } else {
9229                     set arcend($a) $e
9230                     if {![info exists arcout($e)]} {
9231                         set arcout($e) {}
9232                     }
9233                 }
9234                 set arcids($a) [lindex $line 2]
9235                 foreach id $arcids($a) {
9236                     lappend allparents($s) $id
9237                     set s $id
9238                     lappend arcnos($id) $a
9239                 }
9240                 if {![info exists allparents($s)]} {
9241                     set allparents($s) {}
9242                 }
9243                 set arctags($a) {}
9244                 set archeads($a) {}
9245             }
9246             set nextarc [expr {$a - 1}]
9247         }
9248     } err]} {
9249         dropcache $err
9250         return 0
9251     }
9252     if {!$allcwait} {
9253         getallcommits
9254     }
9255     return $allcwait
9256 }
9257
9258 proc getcache {f} {
9259     global nextarc cachedarcs possible_seeds
9260
9261     if {[catch {
9262         set line [gets $f]
9263         if {[llength $line] != 2 || [lindex $line 0] ne "1"} {error "bad version"}
9264         # make sure it's an integer
9265         set cachedarcs [expr {int([lindex $line 1])}]
9266         if {$cachedarcs < 0} {error "bad number of arcs"}
9267         set nextarc 0
9268         set possible_seeds {}
9269         run readcache $f
9270     } err]} {
9271         dropcache $err
9272     }
9273     return 0
9274 }
9275
9276 proc dropcache {err} {
9277     global allcwait nextarc cachedarcs seeds
9278
9279     #puts "dropping cache ($err)"
9280     foreach v {arcnos arcout arcids arcstart arcend growing \
9281                    arctags archeads allparents allchildren} {
9282         global $v
9283         catch {unset $v}
9284     }
9285     set allcwait 0
9286     set nextarc 0
9287     set cachedarcs 0
9288     set seeds {}
9289     getallcommits
9290 }
9291
9292 proc writecache {f} {
9293     global cachearc cachedarcs allccache
9294     global arcstart arcend arcnos arcids arcout
9295
9296     set a $cachearc
9297     set lim $cachedarcs
9298     if {$lim - $a > 1000} {
9299         set lim [expr {$a + 1000}]
9300     }
9301     if {[catch {
9302         while {[incr a] <= $lim} {
9303             if {[info exists arcend($a)]} {
9304                 puts $f [list $arcstart($a) $arcend($a) $arcids($a)]
9305             } else {
9306                 puts $f [list $arcstart($a) {} $arcids($a)]
9307             }
9308         }
9309     } err]} {
9310         catch {close $f}
9311         catch {file delete $allccache}
9312         #puts "writing cache failed ($err)"
9313         return 0
9314     }
9315     set cachearc [expr {$a - 1}]
9316     if {$a > $cachedarcs} {
9317         puts $f "1"
9318         close $f
9319         return 0
9320     }
9321     return 1
9322 }
9323
9324 proc savecache {} {
9325     global nextarc cachedarcs cachearc allccache
9326
9327     if {$nextarc == $cachedarcs} return
9328     set cachearc 0
9329     set cachedarcs $nextarc
9330     catch {
9331         set f [open $allccache w]
9332         puts $f [list 1 $cachedarcs]
9333         run writecache $f
9334     }
9335 }
9336
9337 # Returns 1 if a is an ancestor of b, -1 if b is an ancestor of a,
9338 # or 0 if neither is true.
9339 proc anc_or_desc {a b} {
9340     global arcout arcstart arcend arcnos cached_isanc
9341
9342     if {$arcnos($a) eq $arcnos($b)} {
9343         # Both are on the same arc(s); either both are the same BMP,
9344         # or if one is not a BMP, the other is also not a BMP or is
9345         # the BMP at end of the arc (and it only has 1 incoming arc).
9346         # Or both can be BMPs with no incoming arcs.
9347         if {$a eq $b || $arcnos($a) eq {}} {
9348             return 0
9349         }
9350         # assert {[llength $arcnos($a)] == 1}
9351         set arc [lindex $arcnos($a) 0]
9352         set i [lsearch -exact $arcids($arc) $a]
9353         set j [lsearch -exact $arcids($arc) $b]
9354         if {$i < 0 || $i > $j} {
9355             return 1
9356         } else {
9357             return -1
9358         }
9359     }
9360
9361     if {![info exists arcout($a)]} {
9362         set arc [lindex $arcnos($a) 0]
9363         if {[info exists arcend($arc)]} {
9364             set aend $arcend($arc)
9365         } else {
9366             set aend {}
9367         }
9368         set a $arcstart($arc)
9369     } else {
9370         set aend $a
9371     }
9372     if {![info exists arcout($b)]} {
9373         set arc [lindex $arcnos($b) 0]
9374         if {[info exists arcend($arc)]} {
9375             set bend $arcend($arc)
9376         } else {
9377             set bend {}
9378         }
9379         set b $arcstart($arc)
9380     } else {
9381         set bend $b
9382     }
9383     if {$a eq $bend} {
9384         return 1
9385     }
9386     if {$b eq $aend} {
9387         return -1
9388     }
9389     if {[info exists cached_isanc($a,$bend)]} {
9390         if {$cached_isanc($a,$bend)} {
9391             return 1
9392         }
9393     }
9394     if {[info exists cached_isanc($b,$aend)]} {
9395         if {$cached_isanc($b,$aend)} {
9396             return -1
9397         }
9398         if {[info exists cached_isanc($a,$bend)]} {
9399             return 0
9400         }
9401     }
9402
9403     set todo [list $a $b]
9404     set anc($a) a
9405     set anc($b) b
9406     for {set i 0} {$i < [llength $todo]} {incr i} {
9407         set x [lindex $todo $i]
9408         if {$anc($x) eq {}} {
9409             continue
9410         }
9411         foreach arc $arcnos($x) {
9412             set xd $arcstart($arc)
9413             if {$xd eq $bend} {
9414                 set cached_isanc($a,$bend) 1
9415                 set cached_isanc($b,$aend) 0
9416                 return 1
9417             } elseif {$xd eq $aend} {
9418                 set cached_isanc($b,$aend) 1
9419                 set cached_isanc($a,$bend) 0
9420                 return -1
9421             }
9422             if {![info exists anc($xd)]} {
9423                 set anc($xd) $anc($x)
9424                 lappend todo $xd
9425             } elseif {$anc($xd) ne $anc($x)} {
9426                 set anc($xd) {}
9427             }
9428         }
9429     }
9430     set cached_isanc($a,$bend) 0
9431     set cached_isanc($b,$aend) 0
9432     return 0
9433 }
9434
9435 # This identifies whether $desc has an ancestor that is
9436 # a growing tip of the graph and which is not an ancestor of $anc
9437 # and returns 0 if so and 1 if not.
9438 # If we subsequently discover a tag on such a growing tip, and that
9439 # turns out to be a descendent of $anc (which it could, since we
9440 # don't necessarily see children before parents), then $desc
9441 # isn't a good choice to display as a descendent tag of
9442 # $anc (since it is the descendent of another tag which is
9443 # a descendent of $anc).  Similarly, $anc isn't a good choice to
9444 # display as a ancestor tag of $desc.
9445 #
9446 proc is_certain {desc anc} {
9447     global arcnos arcout arcstart arcend growing problems
9448
9449     set certain {}
9450     if {[llength $arcnos($anc)] == 1} {
9451         # tags on the same arc are certain
9452         if {$arcnos($desc) eq $arcnos($anc)} {
9453             return 1
9454         }
9455         if {![info exists arcout($anc)]} {
9456             # if $anc is partway along an arc, use the start of the arc instead
9457             set a [lindex $arcnos($anc) 0]
9458             set anc $arcstart($a)
9459         }
9460     }
9461     if {[llength $arcnos($desc)] > 1 || [info exists arcout($desc)]} {
9462         set x $desc
9463     } else {
9464         set a [lindex $arcnos($desc) 0]
9465         set x $arcend($a)
9466     }
9467     if {$x == $anc} {
9468         return 1
9469     }
9470     set anclist [list $x]
9471     set dl($x) 1
9472     set nnh 1
9473     set ngrowanc 0
9474     for {set i 0} {$i < [llength $anclist] && ($nnh > 0 || $ngrowanc > 0)} {incr i} {
9475         set x [lindex $anclist $i]
9476         if {$dl($x)} {
9477             incr nnh -1
9478         }
9479         set done($x) 1
9480         foreach a $arcout($x) {
9481             if {[info exists growing($a)]} {
9482                 if {![info exists growanc($x)] && $dl($x)} {
9483                     set growanc($x) 1
9484                     incr ngrowanc
9485                 }
9486             } else {
9487                 set y $arcend($a)
9488                 if {[info exists dl($y)]} {
9489                     if {$dl($y)} {
9490                         if {!$dl($x)} {
9491                             set dl($y) 0
9492                             if {![info exists done($y)]} {
9493                                 incr nnh -1
9494                             }
9495                             if {[info exists growanc($x)]} {
9496                                 incr ngrowanc -1
9497                             }
9498                             set xl [list $y]
9499                             for {set k 0} {$k < [llength $xl]} {incr k} {
9500                                 set z [lindex $xl $k]
9501                                 foreach c $arcout($z) {
9502                                     if {[info exists arcend($c)]} {
9503                                         set v $arcend($c)
9504                                         if {[info exists dl($v)] && $dl($v)} {
9505                                             set dl($v) 0
9506                                             if {![info exists done($v)]} {
9507                                                 incr nnh -1
9508                                             }
9509                                             if {[info exists growanc($v)]} {
9510                                                 incr ngrowanc -1
9511                                             }
9512                                             lappend xl $v
9513                                         }
9514                                     }
9515                                 }
9516                             }
9517                         }
9518                     }
9519                 } elseif {$y eq $anc || !$dl($x)} {
9520                     set dl($y) 0
9521                     lappend anclist $y
9522                 } else {
9523                     set dl($y) 1
9524                     lappend anclist $y
9525                     incr nnh
9526                 }
9527             }
9528         }
9529     }
9530     foreach x [array names growanc] {
9531         if {$dl($x)} {
9532             return 0
9533         }
9534         return 0
9535     }
9536     return 1
9537 }
9538
9539 proc validate_arctags {a} {
9540     global arctags idtags
9541
9542     set i -1
9543     set na $arctags($a)
9544     foreach id $arctags($a) {
9545         incr i
9546         if {![info exists idtags($id)]} {
9547             set na [lreplace $na $i $i]
9548             incr i -1
9549         }
9550     }
9551     set arctags($a) $na
9552 }
9553
9554 proc validate_archeads {a} {
9555     global archeads idheads
9556
9557     set i -1
9558     set na $archeads($a)
9559     foreach id $archeads($a) {
9560         incr i
9561         if {![info exists idheads($id)]} {
9562             set na [lreplace $na $i $i]
9563             incr i -1
9564         }
9565     }
9566     set archeads($a) $na
9567 }
9568
9569 # Return the list of IDs that have tags that are descendents of id,
9570 # ignoring IDs that are descendents of IDs already reported.
9571 proc desctags {id} {
9572     global arcnos arcstart arcids arctags idtags allparents
9573     global growing cached_dtags
9574
9575     if {![info exists allparents($id)]} {
9576         return {}
9577     }
9578     set t1 [clock clicks -milliseconds]
9579     set argid $id
9580     if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
9581         # part-way along an arc; check that arc first
9582         set a [lindex $arcnos($id) 0]
9583         if {$arctags($a) ne {}} {
9584             validate_arctags $a
9585             set i [lsearch -exact $arcids($a) $id]
9586             set tid {}
9587             foreach t $arctags($a) {
9588                 set j [lsearch -exact $arcids($a) $t]
9589                 if {$j >= $i} break
9590                 set tid $t
9591             }
9592             if {$tid ne {}} {
9593                 return $tid
9594             }
9595         }
9596         set id $arcstart($a)
9597         if {[info exists idtags($id)]} {
9598             return $id
9599         }
9600     }
9601     if {[info exists cached_dtags($id)]} {
9602         return $cached_dtags($id)
9603     }
9604
9605     set origid $id
9606     set todo [list $id]
9607     set queued($id) 1
9608     set nc 1
9609     for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
9610         set id [lindex $todo $i]
9611         set done($id) 1
9612         set ta [info exists hastaggedancestor($id)]
9613         if {!$ta} {
9614             incr nc -1
9615         }
9616         # ignore tags on starting node
9617         if {!$ta && $i > 0} {
9618             if {[info exists idtags($id)]} {
9619                 set tagloc($id) $id
9620                 set ta 1
9621             } elseif {[info exists cached_dtags($id)]} {
9622                 set tagloc($id) $cached_dtags($id)
9623                 set ta 1
9624             }
9625         }
9626         foreach a $arcnos($id) {
9627             set d $arcstart($a)
9628             if {!$ta && $arctags($a) ne {}} {
9629                 validate_arctags $a
9630                 if {$arctags($a) ne {}} {
9631                     lappend tagloc($id) [lindex $arctags($a) end]
9632                 }
9633             }
9634             if {$ta || $arctags($a) ne {}} {
9635                 set tomark [list $d]
9636                 for {set j 0} {$j < [llength $tomark]} {incr j} {
9637                     set dd [lindex $tomark $j]
9638                     if {![info exists hastaggedancestor($dd)]} {
9639                         if {[info exists done($dd)]} {
9640                             foreach b $arcnos($dd) {
9641                                 lappend tomark $arcstart($b)
9642                             }
9643                             if {[info exists tagloc($dd)]} {
9644                                 unset tagloc($dd)
9645                             }
9646                         } elseif {[info exists queued($dd)]} {
9647                             incr nc -1
9648                         }
9649                         set hastaggedancestor($dd) 1
9650                     }
9651                 }
9652             }
9653             if {![info exists queued($d)]} {
9654                 lappend todo $d
9655                 set queued($d) 1
9656                 if {![info exists hastaggedancestor($d)]} {
9657                     incr nc
9658                 }
9659             }
9660         }
9661     }
9662     set tags {}
9663     foreach id [array names tagloc] {
9664         if {![info exists hastaggedancestor($id)]} {
9665             foreach t $tagloc($id) {
9666                 if {[lsearch -exact $tags $t] < 0} {
9667                     lappend tags $t
9668                 }
9669             }
9670         }
9671     }
9672     set t2 [clock clicks -milliseconds]
9673     set loopix $i
9674
9675     # remove tags that are descendents of other tags
9676     for {set i 0} {$i < [llength $tags]} {incr i} {
9677         set a [lindex $tags $i]
9678         for {set j 0} {$j < $i} {incr j} {
9679             set b [lindex $tags $j]
9680             set r [anc_or_desc $a $b]
9681             if {$r == 1} {
9682                 set tags [lreplace $tags $j $j]
9683                 incr j -1
9684                 incr i -1
9685             } elseif {$r == -1} {
9686                 set tags [lreplace $tags $i $i]
9687                 incr i -1
9688                 break
9689             }
9690         }
9691     }
9692
9693     if {[array names growing] ne {}} {
9694         # graph isn't finished, need to check if any tag could get
9695         # eclipsed by another tag coming later.  Simply ignore any
9696         # tags that could later get eclipsed.
9697         set ctags {}
9698         foreach t $tags {
9699             if {[is_certain $t $origid]} {
9700                 lappend ctags $t
9701             }
9702         }
9703         if {$tags eq $ctags} {
9704             set cached_dtags($origid) $tags
9705         } else {
9706             set tags $ctags
9707         }
9708     } else {
9709         set cached_dtags($origid) $tags
9710     }
9711     set t3 [clock clicks -milliseconds]
9712     if {0 && $t3 - $t1 >= 100} {
9713         puts "iterating descendents ($loopix/[llength $todo] nodes) took\
9714             [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
9715     }
9716     return $tags
9717 }
9718
9719 proc anctags {id} {
9720     global arcnos arcids arcout arcend arctags idtags allparents
9721     global growing cached_atags
9722
9723     if {![info exists allparents($id)]} {
9724         return {}
9725     }
9726     set t1 [clock clicks -milliseconds]
9727     set argid $id
9728     if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
9729         # part-way along an arc; check that arc first
9730         set a [lindex $arcnos($id) 0]
9731         if {$arctags($a) ne {}} {
9732             validate_arctags $a
9733             set i [lsearch -exact $arcids($a) $id]
9734             foreach t $arctags($a) {
9735                 set j [lsearch -exact $arcids($a) $t]
9736                 if {$j > $i} {
9737                     return $t
9738                 }
9739             }
9740         }
9741         if {![info exists arcend($a)]} {
9742             return {}
9743         }
9744         set id $arcend($a)
9745         if {[info exists idtags($id)]} {
9746             return $id
9747         }
9748     }
9749     if {[info exists cached_atags($id)]} {
9750         return $cached_atags($id)
9751     }
9752
9753     set origid $id
9754     set todo [list $id]
9755     set queued($id) 1
9756     set taglist {}
9757     set nc 1
9758     for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
9759         set id [lindex $todo $i]
9760         set done($id) 1
9761         set td [info exists hastaggeddescendent($id)]
9762         if {!$td} {
9763             incr nc -1
9764         }
9765         # ignore tags on starting node
9766         if {!$td && $i > 0} {
9767             if {[info exists idtags($id)]} {
9768                 set tagloc($id) $id
9769                 set td 1
9770             } elseif {[info exists cached_atags($id)]} {
9771                 set tagloc($id) $cached_atags($id)
9772                 set td 1
9773             }
9774         }
9775         foreach a $arcout($id) {
9776             if {!$td && $arctags($a) ne {}} {
9777                 validate_arctags $a
9778                 if {$arctags($a) ne {}} {
9779                     lappend tagloc($id) [lindex $arctags($a) 0]
9780                 }
9781             }
9782             if {![info exists arcend($a)]} continue
9783             set d $arcend($a)
9784             if {$td || $arctags($a) ne {}} {
9785                 set tomark [list $d]
9786                 for {set j 0} {$j < [llength $tomark]} {incr j} {
9787                     set dd [lindex $tomark $j]
9788                     if {![info exists hastaggeddescendent($dd)]} {
9789                         if {[info exists done($dd)]} {
9790                             foreach b $arcout($dd) {
9791                                 if {[info exists arcend($b)]} {
9792                                     lappend tomark $arcend($b)
9793                                 }
9794                             }
9795                             if {[info exists tagloc($dd)]} {
9796                                 unset tagloc($dd)
9797                             }
9798                         } elseif {[info exists queued($dd)]} {
9799                             incr nc -1
9800                         }
9801                         set hastaggeddescendent($dd) 1
9802                     }
9803                 }
9804             }
9805             if {![info exists queued($d)]} {
9806                 lappend todo $d
9807                 set queued($d) 1
9808                 if {![info exists hastaggeddescendent($d)]} {
9809                     incr nc
9810                 }
9811             }
9812         }
9813     }
9814     set t2 [clock clicks -milliseconds]
9815     set loopix $i
9816     set tags {}
9817     foreach id [array names tagloc] {
9818         if {![info exists hastaggeddescendent($id)]} {
9819             foreach t $tagloc($id) {
9820                 if {[lsearch -exact $tags $t] < 0} {
9821                     lappend tags $t
9822                 }
9823             }
9824         }
9825     }
9826
9827     # remove tags that are ancestors of other tags
9828     for {set i 0} {$i < [llength $tags]} {incr i} {
9829         set a [lindex $tags $i]
9830         for {set j 0} {$j < $i} {incr j} {
9831             set b [lindex $tags $j]
9832             set r [anc_or_desc $a $b]
9833             if {$r == -1} {
9834                 set tags [lreplace $tags $j $j]
9835                 incr j -1
9836                 incr i -1
9837             } elseif {$r == 1} {
9838                 set tags [lreplace $tags $i $i]
9839                 incr i -1
9840                 break
9841             }
9842         }
9843     }
9844
9845     if {[array names growing] ne {}} {
9846         # graph isn't finished, need to check if any tag could get
9847         # eclipsed by another tag coming later.  Simply ignore any
9848         # tags that could later get eclipsed.
9849         set ctags {}
9850         foreach t $tags {
9851             if {[is_certain $origid $t]} {
9852                 lappend ctags $t
9853             }
9854         }
9855         if {$tags eq $ctags} {
9856             set cached_atags($origid) $tags
9857         } else {
9858             set tags $ctags
9859         }
9860     } else {
9861         set cached_atags($origid) $tags
9862     }
9863     set t3 [clock clicks -milliseconds]
9864     if {0 && $t3 - $t1 >= 100} {
9865         puts "iterating ancestors ($loopix/[llength $todo] nodes) took\
9866             [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
9867     }
9868     return $tags
9869 }
9870
9871 # Return the list of IDs that have heads that are descendents of id,
9872 # including id itself if it has a head.
9873 proc descheads {id} {
9874     global arcnos arcstart arcids archeads idheads cached_dheads
9875     global allparents
9876
9877     if {![info exists allparents($id)]} {
9878         return {}
9879     }
9880     set aret {}
9881     if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
9882         # part-way along an arc; check it first
9883         set a [lindex $arcnos($id) 0]
9884         if {$archeads($a) ne {}} {
9885             validate_archeads $a
9886             set i [lsearch -exact $arcids($a) $id]
9887             foreach t $archeads($a) {
9888                 set j [lsearch -exact $arcids($a) $t]
9889                 if {$j > $i} break
9890                 lappend aret $t
9891             }
9892         }
9893         set id $arcstart($a)
9894     }
9895     set origid $id
9896     set todo [list $id]
9897     set seen($id) 1
9898     set ret {}
9899     for {set i 0} {$i < [llength $todo]} {incr i} {
9900         set id [lindex $todo $i]
9901         if {[info exists cached_dheads($id)]} {
9902             set ret [concat $ret $cached_dheads($id)]
9903         } else {
9904             if {[info exists idheads($id)]} {
9905                 lappend ret $id
9906             }
9907             foreach a $arcnos($id) {
9908                 if {$archeads($a) ne {}} {
9909                     validate_archeads $a
9910                     if {$archeads($a) ne {}} {
9911                         set ret [concat $ret $archeads($a)]
9912                     }
9913                 }
9914                 set d $arcstart($a)
9915                 if {![info exists seen($d)]} {
9916                     lappend todo $d
9917                     set seen($d) 1
9918                 }
9919             }
9920         }
9921     }
9922     set ret [lsort -unique $ret]
9923     set cached_dheads($origid) $ret
9924     return [concat $ret $aret]
9925 }
9926
9927 proc addedtag {id} {
9928     global arcnos arcout cached_dtags cached_atags
9929
9930     if {![info exists arcnos($id)]} return
9931     if {![info exists arcout($id)]} {
9932         recalcarc [lindex $arcnos($id) 0]
9933     }
9934     catch {unset cached_dtags}
9935     catch {unset cached_atags}
9936 }
9937
9938 proc addedhead {hid head} {
9939     global arcnos arcout cached_dheads
9940
9941     if {![info exists arcnos($hid)]} return
9942     if {![info exists arcout($hid)]} {
9943         recalcarc [lindex $arcnos($hid) 0]
9944     }
9945     catch {unset cached_dheads}
9946 }
9947
9948 proc removedhead {hid head} {
9949     global cached_dheads
9950
9951     catch {unset cached_dheads}
9952 }
9953
9954 proc movedhead {hid head} {
9955     global arcnos arcout cached_dheads
9956
9957     if {![info exists arcnos($hid)]} return
9958     if {![info exists arcout($hid)]} {
9959         recalcarc [lindex $arcnos($hid) 0]
9960     }
9961     catch {unset cached_dheads}
9962 }
9963
9964 proc changedrefs {} {
9965     global cached_dheads cached_dtags cached_atags
9966     global arctags archeads arcnos arcout idheads idtags
9967
9968     foreach id [concat [array names idheads] [array names idtags]] {
9969         if {[info exists arcnos($id)] && ![info exists arcout($id)]} {
9970             set a [lindex $arcnos($id) 0]
9971             if {![info exists donearc($a)]} {
9972                 recalcarc $a
9973                 set donearc($a) 1
9974             }
9975         }
9976     }
9977     catch {unset cached_dtags}
9978     catch {unset cached_atags}
9979     catch {unset cached_dheads}
9980 }
9981
9982 proc rereadrefs {} {
9983     global idtags idheads idotherrefs mainheadid
9984
9985     set refids [concat [array names idtags] \
9986                     [array names idheads] [array names idotherrefs]]
9987     foreach id $refids {
9988         if {![info exists ref($id)]} {
9989             set ref($id) [listrefs $id]
9990         }
9991     }
9992     set oldmainhead $mainheadid
9993     readrefs
9994     changedrefs
9995     set refids [lsort -unique [concat $refids [array names idtags] \
9996                         [array names idheads] [array names idotherrefs]]]
9997     foreach id $refids {
9998         set v [listrefs $id]
9999         if {![info exists ref($id)] || $ref($id) != $v} {
10000             redrawtags $id
10001         }
10002     }
10003     if {$oldmainhead ne $mainheadid} {
10004         redrawtags $oldmainhead
10005         redrawtags $mainheadid
10006     }
10007     run refill_reflist
10008 }
10009
10010 proc listrefs {id} {
10011     global idtags idheads idotherrefs
10012
10013     set x {}
10014     if {[info exists idtags($id)]} {
10015         set x $idtags($id)
10016     }
10017     set y {}
10018     if {[info exists idheads($id)]} {
10019         set y $idheads($id)
10020     }
10021     set z {}
10022     if {[info exists idotherrefs($id)]} {
10023         set z $idotherrefs($id)
10024     }
10025     return [list $x $y $z]
10026 }
10027
10028 proc showtag {tag isnew} {
10029     global ctext tagcontents tagids linknum tagobjid
10030
10031     if {$isnew} {
10032         addtohistory [list showtag $tag 0]
10033     }
10034     $ctext conf -state normal
10035     clear_ctext
10036     settabs 0
10037     set linknum 0
10038     if {![info exists tagcontents($tag)]} {
10039         catch {
10040             set tagcontents($tag) [exec git cat-file tag $tagobjid($tag)]
10041         }
10042     }
10043     if {[info exists tagcontents($tag)]} {
10044         set text $tagcontents($tag)
10045     } else {
10046         set text "[mc "Tag"]: $tag\n[mc "Id"]:  $tagids($tag)"
10047     }
10048     appendwithlinks $text {}
10049     $ctext conf -state disabled
10050     init_flist {}
10051 }
10052
10053 proc doquit {} {
10054     global stopped
10055     global gitktmpdir
10056
10057     set stopped 100
10058     savestuff .
10059     destroy .
10060
10061     if {[info exists gitktmpdir]} {
10062         catch {file delete -force $gitktmpdir}
10063     }
10064 }
10065
10066 proc mkfontdisp {font top which} {
10067     global fontattr fontpref $font
10068
10069     set fontpref($font) [set $font]
10070     button $top.${font}but -text $which -font optionfont \
10071         -command [list choosefont $font $which]
10072     label $top.$font -relief flat -font $font \
10073         -text $fontattr($font,family) -justify left
10074     grid x $top.${font}but $top.$font -sticky w
10075 }
10076
10077 proc choosefont {font which} {
10078     global fontparam fontlist fonttop fontattr
10079     global prefstop
10080
10081     set fontparam(which) $which
10082     set fontparam(font) $font
10083     set fontparam(family) [font actual $font -family]
10084     set fontparam(size) $fontattr($font,size)
10085     set fontparam(weight) $fontattr($font,weight)
10086     set fontparam(slant) $fontattr($font,slant)
10087     set top .gitkfont
10088     set fonttop $top
10089     if {![winfo exists $top]} {
10090         font create sample
10091         eval font config sample [font actual $font]
10092         toplevel $top
10093         make_transient $top $prefstop
10094         wm title $top [mc "Gitk font chooser"]
10095         label $top.l -textvariable fontparam(which)
10096         pack $top.l -side top
10097         set fontlist [lsort [font families]]
10098         frame $top.f
10099         listbox $top.f.fam -listvariable fontlist \
10100             -yscrollcommand [list $top.f.sb set]
10101         bind $top.f.fam <<ListboxSelect>> selfontfam
10102         scrollbar $top.f.sb -command [list $top.f.fam yview]
10103         pack $top.f.sb -side right -fill y
10104         pack $top.f.fam -side left -fill both -expand 1
10105         pack $top.f -side top -fill both -expand 1
10106         frame $top.g
10107         spinbox $top.g.size -from 4 -to 40 -width 4 \
10108             -textvariable fontparam(size) \
10109             -validatecommand {string is integer -strict %s}
10110         checkbutton $top.g.bold -padx 5 \
10111             -font {{Times New Roman} 12 bold} -text [mc "B"] -indicatoron 0 \
10112             -variable fontparam(weight) -onvalue bold -offvalue normal
10113         checkbutton $top.g.ital -padx 5 \
10114             -font {{Times New Roman} 12 italic} -text [mc "I"] -indicatoron 0  \
10115             -variable fontparam(slant) -onvalue italic -offvalue roman
10116         pack $top.g.size $top.g.bold $top.g.ital -side left
10117         pack $top.g -side top
10118         canvas $top.c -width 150 -height 50 -border 2 -relief sunk \
10119             -background white
10120         $top.c create text 100 25 -anchor center -text $which -font sample \
10121             -fill black -tags text
10122         bind $top.c <Configure> [list centertext $top.c]
10123         pack $top.c -side top -fill x
10124         frame $top.buts
10125         button $top.buts.ok -text [mc "OK"] -command fontok -default active
10126         button $top.buts.can -text [mc "Cancel"] -command fontcan -default normal
10127         bind $top <Key-Return> fontok
10128         bind $top <Key-Escape> fontcan
10129         grid $top.buts.ok $top.buts.can
10130         grid columnconfigure $top.buts 0 -weight 1 -uniform a
10131         grid columnconfigure $top.buts 1 -weight 1 -uniform a
10132         pack $top.buts -side bottom -fill x
10133         trace add variable fontparam write chg_fontparam
10134     } else {
10135         raise $top
10136         $top.c itemconf text -text $which
10137     }
10138     set i [lsearch -exact $fontlist $fontparam(family)]
10139     if {$i >= 0} {
10140         $top.f.fam selection set $i
10141         $top.f.fam see $i
10142     }
10143 }
10144
10145 proc centertext {w} {
10146     $w coords text [expr {[winfo width $w] / 2}] [expr {[winfo height $w] / 2}]
10147 }
10148
10149 proc fontok {} {
10150     global fontparam fontpref prefstop
10151
10152     set f $fontparam(font)
10153     set fontpref($f) [list $fontparam(family) $fontparam(size)]
10154     if {$fontparam(weight) eq "bold"} {
10155         lappend fontpref($f) "bold"
10156     }
10157     if {$fontparam(slant) eq "italic"} {
10158         lappend fontpref($f) "italic"
10159     }
10160     set w $prefstop.$f
10161     $w conf -text $fontparam(family) -font $fontpref($f)
10162         
10163     fontcan
10164 }
10165
10166 proc fontcan {} {
10167     global fonttop fontparam
10168
10169     if {[info exists fonttop]} {
10170         catch {destroy $fonttop}
10171         catch {font delete sample}
10172         unset fonttop
10173         unset fontparam
10174     }
10175 }
10176
10177 proc selfontfam {} {
10178     global fonttop fontparam
10179
10180     set i [$fonttop.f.fam curselection]
10181     if {$i ne {}} {
10182         set fontparam(family) [$fonttop.f.fam get $i]
10183     }
10184 }
10185
10186 proc chg_fontparam {v sub op} {
10187     global fontparam
10188
10189     font config sample -$sub $fontparam($sub)
10190 }
10191
10192 proc doprefs {} {
10193     global maxwidth maxgraphpct
10194     global oldprefs prefstop showneartags showlocalchanges
10195     global bgcolor fgcolor ctext diffcolors selectbgcolor markbgcolor
10196     global tabstop limitdiffs autoselect extdifftool perfile_attrs
10197
10198     set top .gitkprefs
10199     set prefstop $top
10200     if {[winfo exists $top]} {
10201         raise $top
10202         return
10203     }
10204     foreach v {maxwidth maxgraphpct showneartags showlocalchanges \
10205                    limitdiffs tabstop perfile_attrs} {
10206         set oldprefs($v) [set $v]
10207     }
10208     toplevel $top
10209     wm title $top [mc "Gitk preferences"]
10210     make_transient $top .
10211     label $top.ldisp -text [mc "Commit list display options"]
10212     grid $top.ldisp - -sticky w -pady 10
10213     label $top.spacer -text " "
10214     label $top.maxwidthl -text [mc "Maximum graph width (lines)"] \
10215         -font optionfont
10216     spinbox $top.maxwidth -from 0 -to 100 -width 4 -textvariable maxwidth
10217     grid $top.spacer $top.maxwidthl $top.maxwidth -sticky w
10218     label $top.maxpctl -text [mc "Maximum graph width (% of pane)"] \
10219         -font optionfont
10220     spinbox $top.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct
10221     grid x $top.maxpctl $top.maxpct -sticky w
10222     checkbutton $top.showlocal -text [mc "Show local changes"] \
10223         -font optionfont -variable showlocalchanges
10224     grid x $top.showlocal -sticky w
10225     checkbutton $top.autoselect -text [mc "Auto-select SHA1"] \
10226         -font optionfont -variable autoselect
10227     grid x $top.autoselect -sticky w
10228
10229     label $top.ddisp -text [mc "Diff display options"]
10230     grid $top.ddisp - -sticky w -pady 10
10231     label $top.tabstopl -text [mc "Tab spacing"] -font optionfont
10232     spinbox $top.tabstop -from 1 -to 20 -width 4 -textvariable tabstop
10233     grid x $top.tabstopl $top.tabstop -sticky w
10234     checkbutton $top.ntag -text [mc "Display nearby tags"] \
10235         -font optionfont -variable showneartags
10236     grid x $top.ntag -sticky w
10237     checkbutton $top.ldiff -text [mc "Limit diffs to listed paths"] \
10238         -font optionfont -variable limitdiffs
10239     grid x $top.ldiff -sticky w
10240     checkbutton $top.lattr -text [mc "Support per-file encodings"] \
10241         -font optionfont -variable perfile_attrs
10242     grid x $top.lattr -sticky w
10243
10244     entry $top.extdifft -textvariable extdifftool
10245     frame $top.extdifff
10246     label $top.extdifff.l -text [mc "External diff tool" ] -font optionfont \
10247         -padx 10
10248     button $top.extdifff.b -text [mc "Choose..."] -font optionfont \
10249         -command choose_extdiff
10250     pack $top.extdifff.l $top.extdifff.b -side left
10251     grid x $top.extdifff $top.extdifft -sticky w
10252
10253     label $top.cdisp -text [mc "Colors: press to choose"]
10254     grid $top.cdisp - -sticky w -pady 10
10255     label $top.bg -padx 40 -relief sunk -background $bgcolor
10256     button $top.bgbut -text [mc "Background"] -font optionfont \
10257         -command [list choosecolor bgcolor {} $top.bg [mc "background"] setbg]
10258     grid x $top.bgbut $top.bg -sticky w
10259     label $top.fg -padx 40 -relief sunk -background $fgcolor
10260     button $top.fgbut -text [mc "Foreground"] -font optionfont \
10261         -command [list choosecolor fgcolor {} $top.fg [mc "foreground"] setfg]
10262     grid x $top.fgbut $top.fg -sticky w
10263     label $top.diffold -padx 40 -relief sunk -background [lindex $diffcolors 0]
10264     button $top.diffoldbut -text [mc "Diff: old lines"] -font optionfont \
10265         -command [list choosecolor diffcolors 0 $top.diffold [mc "diff old lines"] \
10266                       [list $ctext tag conf d0 -foreground]]
10267     grid x $top.diffoldbut $top.diffold -sticky w
10268     label $top.diffnew -padx 40 -relief sunk -background [lindex $diffcolors 1]
10269     button $top.diffnewbut -text [mc "Diff: new lines"] -font optionfont \
10270         -command [list choosecolor diffcolors 1 $top.diffnew [mc "diff new lines"] \
10271                       [list $ctext tag conf dresult -foreground]]
10272     grid x $top.diffnewbut $top.diffnew -sticky w
10273     label $top.hunksep -padx 40 -relief sunk -background [lindex $diffcolors 2]
10274     button $top.hunksepbut -text [mc "Diff: hunk header"] -font optionfont \
10275         -command [list choosecolor diffcolors 2 $top.hunksep \
10276                       [mc "diff hunk header"] \
10277                       [list $ctext tag conf hunksep -foreground]]
10278     grid x $top.hunksepbut $top.hunksep -sticky w
10279     label $top.markbgsep -padx 40 -relief sunk -background $markbgcolor
10280     button $top.markbgbut -text [mc "Marked line bg"] -font optionfont \
10281         -command [list choosecolor markbgcolor {} $top.markbgsep \
10282                       [mc "marked line background"] \
10283                       [list $ctext tag conf omark -background]]
10284     grid x $top.markbgbut $top.markbgsep -sticky w
10285     label $top.selbgsep -padx 40 -relief sunk -background $selectbgcolor
10286     button $top.selbgbut -text [mc "Select bg"] -font optionfont \
10287         -command [list choosecolor selectbgcolor {} $top.selbgsep [mc "background"] setselbg]
10288     grid x $top.selbgbut $top.selbgsep -sticky w
10289
10290     label $top.cfont -text [mc "Fonts: press to choose"]
10291     grid $top.cfont - -sticky w -pady 10
10292     mkfontdisp mainfont $top [mc "Main font"]
10293     mkfontdisp textfont $top [mc "Diff display font"]
10294     mkfontdisp uifont $top [mc "User interface font"]
10295
10296     frame $top.buts
10297     button $top.buts.ok -text [mc "OK"] -command prefsok -default active
10298     button $top.buts.can -text [mc "Cancel"] -command prefscan -default normal
10299     bind $top <Key-Return> prefsok
10300     bind $top <Key-Escape> prefscan
10301     grid $top.buts.ok $top.buts.can
10302     grid columnconfigure $top.buts 0 -weight 1 -uniform a
10303     grid columnconfigure $top.buts 1 -weight 1 -uniform a
10304     grid $top.buts - - -pady 10 -sticky ew
10305     bind $top <Visibility> "focus $top.buts.ok"
10306 }
10307
10308 proc choose_extdiff {} {
10309     global extdifftool
10310
10311     set prog [tk_getOpenFile -title "External diff tool" -multiple false]
10312     if {$prog ne {}} {
10313         set extdifftool $prog
10314     }
10315 }
10316
10317 proc choosecolor {v vi w x cmd} {
10318     global $v
10319
10320     set c [tk_chooseColor -initialcolor [lindex [set $v] $vi] \
10321                -title [mc "Gitk: choose color for %s" $x]]
10322     if {$c eq {}} return
10323     $w conf -background $c
10324     lset $v $vi $c
10325     eval $cmd $c
10326 }
10327
10328 proc setselbg {c} {
10329     global bglist cflist
10330     foreach w $bglist {
10331         $w configure -selectbackground $c
10332     }
10333     $cflist tag configure highlight \
10334         -background [$cflist cget -selectbackground]
10335     allcanvs itemconf secsel -fill $c
10336 }
10337
10338 proc setbg {c} {
10339     global bglist
10340
10341     foreach w $bglist {
10342         $w conf -background $c
10343     }
10344 }
10345
10346 proc setfg {c} {
10347     global fglist canv
10348
10349     foreach w $fglist {
10350         $w conf -foreground $c
10351     }
10352     allcanvs itemconf text -fill $c
10353     $canv itemconf circle -outline $c
10354     $canv itemconf markid -outline $c
10355 }
10356
10357 proc prefscan {} {
10358     global oldprefs prefstop
10359
10360     foreach v {maxwidth maxgraphpct showneartags showlocalchanges \
10361                    limitdiffs tabstop perfile_attrs} {
10362         global $v
10363         set $v $oldprefs($v)
10364     }
10365     catch {destroy $prefstop}
10366     unset prefstop
10367     fontcan
10368 }
10369
10370 proc prefsok {} {
10371     global maxwidth maxgraphpct
10372     global oldprefs prefstop showneartags showlocalchanges
10373     global fontpref mainfont textfont uifont
10374     global limitdiffs treediffs perfile_attrs
10375
10376     catch {destroy $prefstop}
10377     unset prefstop
10378     fontcan
10379     set fontchanged 0
10380     if {$mainfont ne $fontpref(mainfont)} {
10381         set mainfont $fontpref(mainfont)
10382         parsefont mainfont $mainfont
10383         eval font configure mainfont [fontflags mainfont]
10384         eval font configure mainfontbold [fontflags mainfont 1]
10385         setcoords
10386         set fontchanged 1
10387     }
10388     if {$textfont ne $fontpref(textfont)} {
10389         set textfont $fontpref(textfont)
10390         parsefont textfont $textfont
10391         eval font configure textfont [fontflags textfont]
10392         eval font configure textfontbold [fontflags textfont 1]
10393     }
10394     if {$uifont ne $fontpref(uifont)} {
10395         set uifont $fontpref(uifont)
10396         parsefont uifont $uifont
10397         eval font configure uifont [fontflags uifont]
10398     }
10399     settabs
10400     if {$showlocalchanges != $oldprefs(showlocalchanges)} {
10401         if {$showlocalchanges} {
10402             doshowlocalchanges
10403         } else {
10404             dohidelocalchanges
10405         }
10406     }
10407     if {$limitdiffs != $oldprefs(limitdiffs) ||
10408         ($perfile_attrs && !$oldprefs(perfile_attrs))} {
10409         # treediffs elements are limited by path;
10410         # won't have encodings cached if perfile_attrs was just turned on
10411         catch {unset treediffs}
10412     }
10413     if {$fontchanged || $maxwidth != $oldprefs(maxwidth)
10414         || $maxgraphpct != $oldprefs(maxgraphpct)} {
10415         redisplay
10416     } elseif {$showneartags != $oldprefs(showneartags) ||
10417           $limitdiffs != $oldprefs(limitdiffs)} {
10418         reselectline
10419     }
10420 }
10421
10422 proc formatdate {d} {
10423     global datetimeformat
10424     if {$d ne {}} {
10425         set d [clock format $d -format $datetimeformat]
10426     }
10427     return $d
10428 }
10429
10430 # This list of encoding names and aliases is distilled from
10431 # http://www.iana.org/assignments/character-sets.
10432 # Not all of them are supported by Tcl.
10433 set encoding_aliases {
10434     { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII
10435       ISO646-US US-ASCII us IBM367 cp367 csASCII }
10436     { ISO-10646-UTF-1 csISO10646UTF1 }
10437     { ISO_646.basic:1983 ref csISO646basic1983 }
10438     { INVARIANT csINVARIANT }
10439     { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion }
10440     { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom }
10441     { NATS-SEFI iso-ir-8-1 csNATSSEFI }
10442     { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD }
10443     { NATS-DANO iso-ir-9-1 csNATSDANO }
10444     { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD }
10445     { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish }
10446     { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames }
10447     { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 }
10448     { ISO-2022-KR csISO2022KR }
10449     { EUC-KR csEUCKR }
10450     { ISO-2022-JP csISO2022JP }
10451     { ISO-2022-JP-2 csISO2022JP2 }
10452     { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7
10453       csISO13JISC6220jp }
10454     { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro }
10455     { IT iso-ir-15 ISO646-IT csISO15Italian }
10456     { PT iso-ir-16 ISO646-PT csISO16Portuguese }
10457     { ES iso-ir-17 ISO646-ES csISO17Spanish }
10458     { greek7-old iso-ir-18 csISO18Greek7Old }
10459     { latin-greek iso-ir-19 csISO19LatinGreek }
10460     { DIN_66003 iso-ir-21 de ISO646-DE csISO21German }
10461     { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French }
10462     { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 }
10463     { ISO_5427 iso-ir-37 csISO5427Cyrillic }
10464     { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 }
10465     { BS_viewdata iso-ir-47 csISO47BSViewdata }
10466     { INIS iso-ir-49 csISO49INIS }
10467     { INIS-8 iso-ir-50 csISO50INIS8 }
10468     { INIS-cyrillic iso-ir-51 csISO51INISCyrillic }
10469     { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 }
10470     { ISO_5428:1980 iso-ir-55 csISO5428Greek }
10471     { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 }
10472     { GB_2312-80 iso-ir-58 chinese csISO58GB231280 }
10473     { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian
10474       csISO60Norwegian1 }
10475     { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 }
10476     { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French }
10477     { videotex-suppl iso-ir-70 csISO70VideotexSupp1 }
10478     { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 }
10479     { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 }
10480     { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian }
10481     { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 }
10482     { greek7 iso-ir-88 csISO88Greek7 }
10483     { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 }
10484     { iso-ir-90 csISO90 }
10485     { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a }
10486     { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b
10487       csISO92JISC62991984b }
10488     { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd }
10489     { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand }
10490     { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add
10491       csISO95JIS62291984handadd }
10492     { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana }
10493     { ISO_2033-1983 iso-ir-98 e13b csISO2033 }
10494     { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS }
10495     { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819
10496       CP819 csISOLatin1 }
10497     { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 }
10498     { T.61-7bit iso-ir-102 csISO102T617bit }
10499     { T.61-8bit T.61 iso-ir-103 csISO103T618bit }
10500     { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 }
10501     { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 }
10502     { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic }
10503     { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 }
10504     { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 }
10505     { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr }
10506     { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708
10507       arabic csISOLatinArabic }
10508     { ISO_8859-6-E csISO88596E ISO-8859-6-E }
10509     { ISO_8859-6-I csISO88596I ISO-8859-6-I }
10510     { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118
10511       greek greek8 csISOLatinGreek }
10512     { T.101-G2 iso-ir-128 csISO128T101G2 }
10513     { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew
10514       csISOLatinHebrew }
10515     { ISO_8859-8-E csISO88598E ISO-8859-8-E }
10516     { ISO_8859-8-I csISO88598I ISO-8859-8-I }
10517     { CSN_369103 iso-ir-139 csISO139CSN369103 }
10518     { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 }
10519     { ISO_6937-2-add iso-ir-142 csISOTextComm }
10520     { IEC_P27-1 iso-ir-143 csISO143IECP271 }
10521     { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic
10522       csISOLatinCyrillic }
10523     { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian }
10524     { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian }
10525     { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 }
10526     { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT }
10527     { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba }
10528     { ISO_6937-2-25 iso-ir-152 csISO6937Add }
10529     { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 }
10530     { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp }
10531     { ISO_10367-box iso-ir-155 csISO10367Box }
10532     { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 }
10533     { latin-lap lap iso-ir-158 csISO158Lap }
10534     { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 }
10535     { DS_2089 DS2089 ISO646-DK dk csISO646Danish }
10536     { us-dk csUSDK }
10537     { dk-us csDKUS }
10538     { JIS_X0201 X0201 csHalfWidthKatakana }
10539     { KSC5636 ISO646-KR csKSC5636 }
10540     { ISO-10646-UCS-2 csUnicode }
10541     { ISO-10646-UCS-4 csUCS4 }
10542     { DEC-MCS dec csDECMCS }
10543     { hp-roman8 roman8 r8 csHPRoman8 }
10544     { macintosh mac csMacintosh }
10545     { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl
10546       csIBM037 }
10547     { IBM038 EBCDIC-INT cp038 csIBM038 }
10548     { IBM273 CP273 csIBM273 }
10549     { IBM274 EBCDIC-BE CP274 csIBM274 }
10550     { IBM275 EBCDIC-BR cp275 csIBM275 }
10551     { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 }
10552     { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 }
10553     { IBM280 CP280 ebcdic-cp-it csIBM280 }
10554     { IBM281 EBCDIC-JP-E cp281 csIBM281 }
10555     { IBM284 CP284 ebcdic-cp-es csIBM284 }
10556     { IBM285 CP285 ebcdic-cp-gb csIBM285 }
10557     { IBM290 cp290 EBCDIC-JP-kana csIBM290 }
10558     { IBM297 cp297 ebcdic-cp-fr csIBM297 }
10559     { IBM420 cp420 ebcdic-cp-ar1 csIBM420 }
10560     { IBM423 cp423 ebcdic-cp-gr csIBM423 }
10561     { IBM424 cp424 ebcdic-cp-he csIBM424 }
10562     { IBM437 cp437 437 csPC8CodePage437 }
10563     { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 }
10564     { IBM775 cp775 csPC775Baltic }
10565     { IBM850 cp850 850 csPC850Multilingual }
10566     { IBM851 cp851 851 csIBM851 }
10567     { IBM852 cp852 852 csPCp852 }
10568     { IBM855 cp855 855 csIBM855 }
10569     { IBM857 cp857 857 csIBM857 }
10570     { IBM860 cp860 860 csIBM860 }
10571     { IBM861 cp861 861 cp-is csIBM861 }
10572     { IBM862 cp862 862 csPC862LatinHebrew }
10573     { IBM863 cp863 863 csIBM863 }
10574     { IBM864 cp864 csIBM864 }
10575     { IBM865 cp865 865 csIBM865 }
10576     { IBM866 cp866 866 csIBM866 }
10577     { IBM868 CP868 cp-ar csIBM868 }
10578     { IBM869 cp869 869 cp-gr csIBM869 }
10579     { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 }
10580     { IBM871 CP871 ebcdic-cp-is csIBM871 }
10581     { IBM880 cp880 EBCDIC-Cyrillic csIBM880 }
10582     { IBM891 cp891 csIBM891 }
10583     { IBM903 cp903 csIBM903 }
10584     { IBM904 cp904 904 csIBBM904 }
10585     { IBM905 CP905 ebcdic-cp-tr csIBM905 }
10586     { IBM918 CP918 ebcdic-cp-ar2 csIBM918 }
10587     { IBM1026 CP1026 csIBM1026 }
10588     { EBCDIC-AT-DE csIBMEBCDICATDE }
10589     { EBCDIC-AT-DE-A csEBCDICATDEA }
10590     { EBCDIC-CA-FR csEBCDICCAFR }
10591     { EBCDIC-DK-NO csEBCDICDKNO }
10592     { EBCDIC-DK-NO-A csEBCDICDKNOA }
10593     { EBCDIC-FI-SE csEBCDICFISE }
10594     { EBCDIC-FI-SE-A csEBCDICFISEA }
10595     { EBCDIC-FR csEBCDICFR }
10596     { EBCDIC-IT csEBCDICIT }
10597     { EBCDIC-PT csEBCDICPT }
10598     { EBCDIC-ES csEBCDICES }
10599     { EBCDIC-ES-A csEBCDICESA }
10600     { EBCDIC-ES-S csEBCDICESS }
10601     { EBCDIC-UK csEBCDICUK }
10602     { EBCDIC-US csEBCDICUS }
10603     { UNKNOWN-8BIT csUnknown8BiT }
10604     { MNEMONIC csMnemonic }
10605     { MNEM csMnem }
10606     { VISCII csVISCII }
10607     { VIQR csVIQR }
10608     { KOI8-R csKOI8R }
10609     { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro }
10610     { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro }
10611     { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro }
10612     { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro }
10613     { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro }
10614     { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro }
10615     { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro }
10616     { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro }
10617     { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro }
10618     { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro }
10619     { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro }
10620     { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro }
10621     { IBM1047 IBM-1047 }
10622     { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian }
10623     { Amiga-1251 Ami1251 Amiga1251 Ami-1251 }
10624     { UNICODE-1-1 csUnicode11 }
10625     { CESU-8 csCESU-8 }
10626     { BOCU-1 csBOCU-1 }
10627     { UNICODE-1-1-UTF-7 csUnicode11UTF7 }
10628     { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic
10629       l8 }
10630     { ISO-8859-15 ISO_8859-15 Latin-9 }
10631     { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 }
10632     { GBK CP936 MS936 windows-936 }
10633     { JIS_Encoding csJISEncoding }
10634     { Shift_JIS MS_Kanji csShiftJIS ShiftJIS Shift-JIS }
10635     { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese
10636       EUC-JP }
10637     { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese }
10638     { ISO-10646-UCS-Basic csUnicodeASCII }
10639     { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 }
10640     { ISO-Unicode-IBM-1261 csUnicodeIBM1261 }
10641     { ISO-Unicode-IBM-1268 csUnicodeIBM1268 }
10642     { ISO-Unicode-IBM-1276 csUnicodeIBM1276 }
10643     { ISO-Unicode-IBM-1264 csUnicodeIBM1264 }
10644     { ISO-Unicode-IBM-1265 csUnicodeIBM1265 }
10645     { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 }
10646     { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 }
10647     { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 }
10648     { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 }
10649     { Adobe-Standard-Encoding csAdobeStandardEncoding }
10650     { Ventura-US csVenturaUS }
10651     { Ventura-International csVenturaInternational }
10652     { PC8-Danish-Norwegian csPC8DanishNorwegian }
10653     { PC8-Turkish csPC8Turkish }
10654     { IBM-Symbols csIBMSymbols }
10655     { IBM-Thai csIBMThai }
10656     { HP-Legal csHPLegal }
10657     { HP-Pi-font csHPPiFont }
10658     { HP-Math8 csHPMath8 }
10659     { Adobe-Symbol-Encoding csHPPSMath }
10660     { HP-DeskTop csHPDesktop }
10661     { Ventura-Math csVenturaMath }
10662     { Microsoft-Publishing csMicrosoftPublishing }
10663     { Windows-31J csWindows31J }
10664     { GB2312 csGB2312 }
10665     { Big5 csBig5 }
10666 }
10667
10668 proc tcl_encoding {enc} {
10669     global encoding_aliases tcl_encoding_cache
10670     if {[info exists tcl_encoding_cache($enc)]} {
10671         return $tcl_encoding_cache($enc)
10672     }
10673     set names [encoding names]
10674     set lcnames [string tolower $names]
10675     set enc [string tolower $enc]
10676     set i [lsearch -exact $lcnames $enc]
10677     if {$i < 0} {
10678         # look for "isonnn" instead of "iso-nnn" or "iso_nnn"
10679         if {[regsub {^(iso|cp|ibm|jis)[-_]} $enc {\1} encx]} {
10680             set i [lsearch -exact $lcnames $encx]
10681         }
10682     }
10683     if {$i < 0} {
10684         foreach l $encoding_aliases {
10685             set ll [string tolower $l]
10686             if {[lsearch -exact $ll $enc] < 0} continue
10687             # look through the aliases for one that tcl knows about
10688             foreach e $ll {
10689                 set i [lsearch -exact $lcnames $e]
10690                 if {$i < 0} {
10691                     if {[regsub {^(iso|cp|ibm|jis)[-_]} $e {\1} ex]} {
10692                         set i [lsearch -exact $lcnames $ex]
10693                     }
10694                 }
10695                 if {$i >= 0} break
10696             }
10697             break
10698         }
10699     }
10700     set tclenc {}
10701     if {$i >= 0} {
10702         set tclenc [lindex $names $i]
10703     }
10704     set tcl_encoding_cache($enc) $tclenc
10705     return $tclenc
10706 }
10707
10708 proc gitattr {path attr default} {
10709     global path_attr_cache
10710     if {[info exists path_attr_cache($attr,$path)]} {
10711         set r $path_attr_cache($attr,$path)
10712     } else {
10713         set r "unspecified"
10714         if {![catch {set line [exec git check-attr $attr -- $path]}]} {
10715             regexp "(.*): encoding: (.*)" $line m f r
10716         }
10717         set path_attr_cache($attr,$path) $r
10718     }
10719     if {$r eq "unspecified"} {
10720         return $default
10721     }
10722     return $r
10723 }
10724
10725 proc cache_gitattr {attr pathlist} {
10726     global path_attr_cache
10727     set newlist {}
10728     foreach path $pathlist {
10729         if {![info exists path_attr_cache($attr,$path)]} {
10730             lappend newlist $path
10731         }
10732     }
10733     set lim 1000
10734     if {[tk windowingsystem] == "win32"} {
10735         # windows has a 32k limit on the arguments to a command...
10736         set lim 30
10737     }
10738     while {$newlist ne {}} {
10739         set head [lrange $newlist 0 [expr {$lim - 1}]]
10740         set newlist [lrange $newlist $lim end]
10741         if {![catch {set rlist [eval exec git check-attr $attr -- $head]}]} {
10742             foreach row [split $rlist "\n"] {
10743                 if {[regexp "(.*): encoding: (.*)" $row m path value]} {
10744                     if {[string index $path 0] eq "\""} {
10745                         set path [encoding convertfrom [lindex $path 0]]
10746                     }
10747                     set path_attr_cache($attr,$path) $value
10748                 }
10749             }
10750         }
10751     }
10752 }
10753
10754 proc get_path_encoding {path} {
10755     global gui_encoding perfile_attrs
10756     set tcl_enc $gui_encoding
10757     if {$path ne {} && $perfile_attrs} {
10758         set enc2 [tcl_encoding [gitattr $path encoding $tcl_enc]]
10759         if {$enc2 ne {}} {
10760             set tcl_enc $enc2
10761         }
10762     }
10763     return $tcl_enc
10764 }
10765
10766 # First check that Tcl/Tk is recent enough
10767 if {[catch {package require Tk 8.4} err]} {
10768     show_error {} . [mc "Sorry, gitk cannot run with this version of Tcl/Tk.\n\
10769                      Gitk requires at least Tcl/Tk 8.4."]
10770     exit 1
10771 }
10772
10773 # defaults...
10774 set wrcomcmd "git diff-tree --stdin -p --pretty"
10775
10776 set gitencoding {}
10777 catch {
10778     set gitencoding [exec git config --get i18n.commitencoding]
10779 }
10780 catch {
10781     set gitencoding [exec git config --get i18n.logoutputencoding]
10782 }
10783 if {$gitencoding == ""} {
10784     set gitencoding "utf-8"
10785 }
10786 set tclencoding [tcl_encoding $gitencoding]
10787 if {$tclencoding == {}} {
10788     puts stderr "Warning: encoding $gitencoding is not supported by Tcl/Tk"
10789 }
10790
10791 set gui_encoding [encoding system]
10792 catch {
10793     set enc [exec git config --get gui.encoding]
10794     if {$enc ne {}} {
10795         set tclenc [tcl_encoding $enc]
10796         if {$tclenc ne {}} {
10797             set gui_encoding $tclenc
10798         } else {
10799             puts stderr "Warning: encoding $enc is not supported by Tcl/Tk"
10800         }
10801     }
10802 }
10803
10804 if {[tk windowingsystem] eq "aqua"} {
10805     set mainfont {{Lucida Grande} 9}
10806     set textfont {Monaco 9}
10807     set uifont {{Lucida Grande} 9 bold}
10808 } else {
10809     set mainfont {Helvetica 9}
10810     set textfont {Courier 9}
10811     set uifont {Helvetica 9 bold}
10812 }
10813 set tabstop 8
10814 set findmergefiles 0
10815 set maxgraphpct 50
10816 set maxwidth 16
10817 set revlistorder 0
10818 set fastdate 0
10819 set uparrowlen 5
10820 set downarrowlen 5
10821 set mingaplen 100
10822 set cmitmode "patch"
10823 set wrapcomment "none"
10824 set showneartags 1
10825 set maxrefs 20
10826 set maxlinelen 200
10827 set showlocalchanges 1
10828 set limitdiffs 1
10829 set datetimeformat "%Y-%m-%d %H:%M:%S"
10830 set autoselect 1
10831 set perfile_attrs 0
10832
10833 if {[tk windowingsystem] eq "aqua"} {
10834     set extdifftool "opendiff"
10835 } else {
10836     set extdifftool "meld"
10837 }
10838
10839 set colors {green red blue magenta darkgrey brown orange}
10840 set bgcolor white
10841 set fgcolor black
10842 set diffcolors {red "#00a000" blue}
10843 set diffcontext 3
10844 set ignorespace 0
10845 set selectbgcolor gray85
10846 set markbgcolor "#e0e0ff"
10847
10848 set circlecolors {white blue gray blue blue}
10849
10850 # button for popping up context menus
10851 if {[tk windowingsystem] eq "aqua"} {
10852     set ctxbut <Button-2>
10853 } else {
10854     set ctxbut <Button-3>
10855 }
10856
10857 ## For msgcat loading, first locate the installation location.
10858 if { [info exists ::env(GITK_MSGSDIR)] } {
10859     ## Msgsdir was manually set in the environment.
10860     set gitk_msgsdir $::env(GITK_MSGSDIR)
10861 } else {
10862     ## Let's guess the prefix from argv0.
10863     set gitk_prefix [file dirname [file dirname [file normalize $argv0]]]
10864     set gitk_libdir [file join $gitk_prefix share gitk lib]
10865     set gitk_msgsdir [file join $gitk_libdir msgs]
10866     unset gitk_prefix
10867 }
10868
10869 ## Internationalization (i18n) through msgcat and gettext. See
10870 ## http://www.gnu.org/software/gettext/manual/html_node/Tcl.html
10871 package require msgcat
10872 namespace import ::msgcat::mc
10873 ## And eventually load the actual message catalog
10874 ::msgcat::mcload $gitk_msgsdir
10875
10876 catch {source ~/.gitk}
10877
10878 font create optionfont -family sans-serif -size -12
10879
10880 parsefont mainfont $mainfont
10881 eval font create mainfont [fontflags mainfont]
10882 eval font create mainfontbold [fontflags mainfont 1]
10883
10884 parsefont textfont $textfont
10885 eval font create textfont [fontflags textfont]
10886 eval font create textfontbold [fontflags textfont 1]
10887
10888 parsefont uifont $uifont
10889 eval font create uifont [fontflags uifont]
10890
10891 setoptions
10892
10893 # check that we can find a .git directory somewhere...
10894 if {[catch {set gitdir [gitdir]}]} {
10895     show_error {} . [mc "Cannot find a git repository here."]
10896     exit 1
10897 }
10898 if {![file isdirectory $gitdir]} {
10899     show_error {} . [mc "Cannot find the git directory \"%s\"." $gitdir]
10900     exit 1
10901 }
10902
10903 set selecthead {}
10904 set selectheadid {}
10905
10906 set revtreeargs {}
10907 set cmdline_files {}
10908 set i 0
10909 set revtreeargscmd {}
10910 foreach arg $argv {
10911     switch -glob -- $arg {
10912         "" { }
10913         "--" {
10914             set cmdline_files [lrange $argv [expr {$i + 1}] end]
10915             break
10916         }
10917         "--select-commit=*" {
10918             set selecthead [string range $arg 16 end]
10919         }
10920         "--argscmd=*" {
10921             set revtreeargscmd [string range $arg 10 end]
10922         }
10923         default {
10924             lappend revtreeargs $arg
10925         }
10926     }
10927     incr i
10928 }
10929
10930 if {$selecthead eq "HEAD"} {
10931     set selecthead {}
10932 }
10933
10934 if {$i >= [llength $argv] && $revtreeargs ne {}} {
10935     # no -- on command line, but some arguments (other than --argscmd)
10936     if {[catch {
10937         set f [eval exec git rev-parse --no-revs --no-flags $revtreeargs]
10938         set cmdline_files [split $f "\n"]
10939         set n [llength $cmdline_files]
10940         set revtreeargs [lrange $revtreeargs 0 end-$n]
10941         # Unfortunately git rev-parse doesn't produce an error when
10942         # something is both a revision and a filename.  To be consistent
10943         # with git log and git rev-list, check revtreeargs for filenames.
10944         foreach arg $revtreeargs {
10945             if {[file exists $arg]} {
10946                 show_error {} . [mc "Ambiguous argument '%s': both revision\
10947                                  and filename" $arg]
10948                 exit 1
10949             }
10950         }
10951     } err]} {
10952         # unfortunately we get both stdout and stderr in $err,
10953         # so look for "fatal:".
10954         set i [string first "fatal:" $err]
10955         if {$i > 0} {
10956             set err [string range $err [expr {$i + 6}] end]
10957         }
10958         show_error {} . "[mc "Bad arguments to gitk:"]\n$err"
10959         exit 1
10960     }
10961 }
10962
10963 set nullid "0000000000000000000000000000000000000000"
10964 set nullid2 "0000000000000000000000000000000000000001"
10965 set nullfile "/dev/null"
10966
10967 set have_tk85 [expr {[package vcompare $tk_version "8.5"] >= 0}]
10968
10969 set runq {}
10970 set history {}
10971 set historyindex 0
10972 set fh_serial 0
10973 set nhl_names {}
10974 set highlight_paths {}
10975 set findpattern {}
10976 set searchdirn -forwards
10977 set boldids {}
10978 set boldnameids {}
10979 set diffelide {0 0}
10980 set markingmatches 0
10981 set linkentercount 0
10982 set need_redisplay 0
10983 set nrows_drawn 0
10984 set firsttabstop 0
10985
10986 set nextviewnum 1
10987 set curview 0
10988 set selectedview 0
10989 set selectedhlview [mc "None"]
10990 set highlight_related [mc "None"]
10991 set highlight_files {}
10992 set viewfiles(0) {}
10993 set viewperm(0) 0
10994 set viewargs(0) {}
10995 set viewargscmd(0) {}
10996
10997 set selectedline {}
10998 set numcommits 0
10999 set loginstance 0
11000 set cmdlineok 0
11001 set stopped 0
11002 set stuffsaved 0
11003 set patchnum 0
11004 set lserial 0
11005 set isworktree [expr {[exec git rev-parse --is-inside-work-tree] == "true"}]
11006 setcoords
11007 makewindow
11008 catch {
11009     image create photo gitlogo      -width 16 -height 16
11010
11011     image create photo gitlogominus -width  4 -height  2
11012     gitlogominus put #C00000 -to 0 0 4 2
11013     gitlogo copy gitlogominus -to  1 5
11014     gitlogo copy gitlogominus -to  6 5
11015     gitlogo copy gitlogominus -to 11 5
11016     image delete gitlogominus
11017
11018     image create photo gitlogoplus  -width  4 -height  4
11019     gitlogoplus  put #008000 -to 1 0 3 4
11020     gitlogoplus  put #008000 -to 0 1 4 3
11021     gitlogo copy gitlogoplus  -to  1 9
11022     gitlogo copy gitlogoplus  -to  6 9
11023     gitlogo copy gitlogoplus  -to 11 9
11024     image delete gitlogoplus
11025
11026     image create photo gitlogo32    -width 32 -height 32
11027     gitlogo32 copy gitlogo -zoom 2 2
11028
11029     wm iconphoto . -default gitlogo gitlogo32
11030 }
11031 # wait for the window to become visible
11032 tkwait visibility .
11033 wm title . "[file tail $argv0]: [file tail [pwd]]"
11034 readrefs
11035
11036 if {$cmdline_files ne {} || $revtreeargs ne {} || $revtreeargscmd ne {}} {
11037     # create a view for the files/dirs specified on the command line
11038     set curview 1
11039     set selectedview 1
11040     set nextviewnum 2
11041     set viewname(1) [mc "Command line"]
11042     set viewfiles(1) $cmdline_files
11043     set viewargs(1) $revtreeargs
11044     set viewargscmd(1) $revtreeargscmd
11045     set viewperm(1) 0
11046     set vdatemode(1) 0
11047     addviewmenu 1
11048     .bar.view entryconf [mca "Edit view..."] -state normal
11049     .bar.view entryconf [mca "Delete view"] -state normal
11050 }
11051
11052 if {[info exists permviews]} {
11053     foreach v $permviews {
11054         set n $nextviewnum
11055         incr nextviewnum
11056         set viewname($n) [lindex $v 0]
11057         set viewfiles($n) [lindex $v 1]
11058         set viewargs($n) [lindex $v 2]
11059         set viewargscmd($n) [lindex $v 3]
11060         set viewperm($n) 1
11061         addviewmenu $n
11062     }
11063 }
11064
11065 if {[tk windowingsystem] eq "win32"} {
11066     focus -force .
11067 }
11068
11069 getcommits {}