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