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