[PATCH] gitk: add Update menu item.
[git.git] / gitk
1 #!/bin/sh
2 # Tcl ignores the next line -*- tcl -*- \
3 exec wish "$0" -- "$@"
4
5 # Copyright (C) 2005 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 ".git"
16     }
17 }
18
19 proc parse_args {rargs} {
20     global parsed_args
21
22     if [catch {
23         set parse_args [concat --default HEAD $rargs]
24         set parsed_args [split [eval exec git-rev-parse $parse_args] "\n"]
25     }] {
26         # if git-rev-parse failed for some reason...
27         if {$rargs == {}} {
28             set rargs HEAD
29         }
30         set parsed_args $rargs
31     }
32     return $parsed_args
33 }
34
35 proc getcommits {rargs} {
36     global oldcommits commits commfd phase canv mainfont env
37     global startmsecs nextupdate ncmupdate
38     global ctext maincursor textcursor leftover gitencoding
39
40     # check that we can find a .git directory somewhere...
41     set gitdir [gitdir]
42     if {![file isdirectory $gitdir]} {
43         error_popup "Cannot find the git directory \"$gitdir\"."
44         exit 1
45     }
46     set oldcommits {}
47     set commits {}
48     set phase getcommits
49     set startmsecs [clock clicks -milliseconds]
50     set nextupdate [expr {$startmsecs + 100}]
51     set ncmupdate 1
52     set parsed_args [parse_args $rargs]
53     if [catch {
54         set commfd [open "|git-rev-list --header --topo-order --parents $parsed_args" r]
55     } err] {
56         puts stderr "Error executing git-rev-list: $err"
57         exit 1
58     }
59     set leftover {}
60     fconfigure $commfd -blocking 0 -translation lf -encoding $gitencoding
61     fileevent $commfd readable [list getcommitlines $commfd]
62     $canv delete all
63     $canv create text 3 3 -anchor nw -text "Reading commits..." \
64         -font $mainfont -tags textitems
65     . config -cursor watch
66     settextcursor watch
67 }
68
69 proc getcommitlines {commfd}  {
70     global oldcommits commits parents cdate children nchildren
71     global commitlisted phase nextupdate
72     global stopped redisplaying leftover
73     global canv
74
75     set stuff [read $commfd]
76     if {$stuff == {}} {
77         if {![eof $commfd]} return
78         # set it blocking so we wait for the process to terminate
79         fconfigure $commfd -blocking 1
80         if {![catch {close $commfd} err]} {
81             after idle finishcommits
82             return
83         }
84         if {[string range $err 0 4] == "usage"} {
85             set err \
86                 "Gitk: error reading commits: bad arguments to git-rev-list.\
87                 (Note: arguments to gitk are passed to git-rev-list\
88                 to allow selection of commits to be displayed.)"
89         } else {
90             set err "Error reading commits: $err"
91         }
92         error_popup $err
93         exit 1
94     }
95     set start 0
96     while 1 {
97         set i [string first "\0" $stuff $start]
98         if {$i < 0} {
99             append leftover [string range $stuff $start end]
100             return
101         }
102         set cmit [string range $stuff $start [expr {$i - 1}]]
103         if {$start == 0} {
104             set cmit "$leftover$cmit"
105             set leftover {}
106         }
107         set start [expr {$i + 1}]
108         set j [string first "\n" $cmit]
109         set ok 0
110         if {$j >= 0} {
111             set ids [string range $cmit 0 [expr {$j - 1}]]
112             set ok 1
113             foreach id $ids {
114                 if {![regexp {^[0-9a-f]{40}$} $id]} {
115                     set ok 0
116                     break
117                 }
118             }
119         }
120         if {!$ok} {
121             set shortcmit $cmit
122             if {[string length $shortcmit] > 80} {
123                 set shortcmit "[string range $shortcmit 0 80]..."
124             }
125             error_popup "Can't parse git-rev-list output: {$shortcmit}"
126             exit 1
127         }
128         set id [lindex $ids 0]
129         set olds [lrange $ids 1 end]
130         set cmit [string range $cmit [expr {$j + 1}] end]
131         if {$phase == "updatecommits"} {
132             $canv delete all
133             set oldcommits $commits
134             set commits {}
135             unset children
136             unset nchildren
137             set phase getcommits
138         }
139         lappend commits $id
140         set commitlisted($id) 1
141         parsecommit $id $cmit 1 [lrange $ids 1 end]
142         drawcommit $id 1
143         if {[clock clicks -milliseconds] >= $nextupdate} {
144             doupdate 1
145         }
146         while {$redisplaying} {
147             set redisplaying 0
148             if {$stopped == 1} {
149                 set stopped 0
150                 set phase "getcommits"
151                 foreach id $commits {
152                     drawcommit $id 1
153                     if {$stopped} break
154                     if {[clock clicks -milliseconds] >= $nextupdate} {
155                         doupdate 1
156                     }
157                 }
158             }
159         }
160     }
161 }
162
163 proc doupdate {reading} {
164     global commfd nextupdate numcommits ncmupdate
165
166     if {$reading} {
167         fileevent $commfd readable {}
168     }
169     update
170     set nextupdate [expr {[clock clicks -milliseconds] + 100}]
171     if {$numcommits < 100} {
172         set ncmupdate [expr {$numcommits + 1}]
173     } elseif {$numcommits < 10000} {
174         set ncmupdate [expr {$numcommits + 10}]
175     } else {
176         set ncmupdate [expr {$numcommits + 100}]
177     }
178     if {$reading} {
179         fileevent $commfd readable [list getcommitlines $commfd]
180     }
181 }
182
183 proc readcommit {id} {
184     if [catch {set contents [exec git-cat-file commit $id]}] return
185     parsecommit $id $contents 0 {}
186 }
187
188 proc updatechildren {id olds} {
189     global children nchildren parents nparents ncleft
190
191     if {![info exists nchildren($id)]} {
192         set children($id) {}
193         set nchildren($id) 0
194         set ncleft($id) 0
195     }
196     set parents($id) $olds
197     set nparents($id) [llength $olds]
198     foreach p $olds {
199         if {![info exists nchildren($p)]} {
200             set children($p) [list $id]
201             set nchildren($p) 1
202             set ncleft($p) 1
203         } elseif {[lsearch -exact $children($p) $id] < 0} {
204             lappend children($p) $id
205             incr nchildren($p)
206             incr ncleft($p)
207         }
208     }
209 }
210
211 proc parsecommit {id contents listed olds} {
212     global commitinfo cdate
213
214     set inhdr 1
215     set comment {}
216     set headline {}
217     set auname {}
218     set audate {}
219     set comname {}
220     set comdate {}
221     updatechildren $id $olds
222     set hdrend [string first "\n\n" $contents]
223     if {$hdrend < 0} {
224         # should never happen...
225         set hdrend [string length $contents]
226     }
227     set header [string range $contents 0 [expr {$hdrend - 1}]]
228     set comment [string range $contents [expr {$hdrend + 2}] end]
229     foreach line [split $header "\n"] {
230         set tag [lindex $line 0]
231         if {$tag == "author"} {
232             set audate [lindex $line end-1]
233             set auname [lrange $line 1 end-2]
234         } elseif {$tag == "committer"} {
235             set comdate [lindex $line end-1]
236             set comname [lrange $line 1 end-2]
237         }
238     }
239     set headline {}
240     # take the first line of the comment as the headline
241     set i [string first "\n" $comment]
242     if {$i >= 0} {
243         set headline [string trim [string range $comment 0 $i]]
244     } else {
245         set headline $comment
246     }
247     if {!$listed} {
248         # git-rev-list indents the comment by 4 spaces;
249         # if we got this via git-cat-file, add the indentation
250         set newcomment {}
251         foreach line [split $comment "\n"] {
252             append newcomment "    "
253             append newcomment $line
254             append newcomment "\n"
255         }
256         set comment $newcomment
257     }
258     if {$comdate != {}} {
259         set cdate($id) $comdate
260     }
261     set commitinfo($id) [list $headline $auname $audate \
262                              $comname $comdate $comment]
263 }
264
265 proc readrefs {} {
266     global tagids idtags headids idheads tagcontents
267     global otherrefids idotherrefs
268
269     foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
270         catch {unset $v}
271     }
272     set refd [open [list | git-ls-remote [gitdir]] r]
273     while {0 <= [set n [gets $refd line]]} {
274         if {![regexp {^([0-9a-f]{40})   refs/([^^]*)$} $line \
275             match id path]} {
276             continue
277         }
278         if {![regexp {^(tags|heads)/(.*)$} $path match type name]} {
279             set type others
280             set name $path
281         }
282         if {$type == "tags"} {
283             set tagids($name) $id
284             lappend idtags($id) $name
285             set obj {}
286             set type {}
287             set tag {}
288             catch {
289                 set commit [exec git-rev-parse "$id^0"]
290                 if {"$commit" != "$id"} {
291                     set tagids($name) $commit
292                     lappend idtags($commit) $name
293                 }
294             }           
295             catch {
296                 set tagcontents($name) [exec git-cat-file tag "$id"]
297             }
298         } elseif { $type == "heads" } {
299             set headids($name) $id
300             lappend idheads($id) $name
301         } else {
302             set otherrefids($name) $id
303             lappend idotherrefs($id) $name
304         }
305     }
306     close $refd
307 }
308
309 proc error_popup msg {
310     set w .error
311     toplevel $w
312     wm transient $w .
313     message $w.m -text $msg -justify center -aspect 400
314     pack $w.m -side top -fill x -padx 20 -pady 20
315     button $w.ok -text OK -command "destroy $w"
316     pack $w.ok -side bottom -fill x
317     bind $w <Visibility> "grab $w; focus $w"
318     tkwait window $w
319 }
320
321 proc makewindow {rargs} {
322     global canv canv2 canv3 linespc charspc ctext cflist textfont
323     global findtype findtypemenu findloc findstring fstring geometry
324     global entries sha1entry sha1string sha1but
325     global maincursor textcursor curtextcursor
326     global rowctxmenu mergemax
327
328     menu .bar
329     .bar add cascade -label "File" -menu .bar.file
330     menu .bar.file
331     .bar.file add command -label "Update" -command [list updatecommits $rargs]
332     .bar.file add command -label "Reread references" -command rereadrefs
333     .bar.file add command -label "Quit" -command doquit
334     menu .bar.edit
335     .bar add cascade -label "Edit" -menu .bar.edit
336     .bar.edit add command -label "Preferences" -command doprefs
337     menu .bar.help
338     .bar add cascade -label "Help" -menu .bar.help
339     .bar.help add command -label "About gitk" -command about
340     . configure -menu .bar
341
342     if {![info exists geometry(canv1)]} {
343         set geometry(canv1) [expr {45 * $charspc}]
344         set geometry(canv2) [expr {30 * $charspc}]
345         set geometry(canv3) [expr {15 * $charspc}]
346         set geometry(canvh) [expr {25 * $linespc + 4}]
347         set geometry(ctextw) 80
348         set geometry(ctexth) 30
349         set geometry(cflistw) 30
350     }
351     panedwindow .ctop -orient vertical
352     if {[info exists geometry(width)]} {
353         .ctop conf -width $geometry(width) -height $geometry(height)
354         set texth [expr {$geometry(height) - $geometry(canvh) - 56}]
355         set geometry(ctexth) [expr {($texth - 8) /
356                                     [font metrics $textfont -linespace]}]
357     }
358     frame .ctop.top
359     frame .ctop.top.bar
360     pack .ctop.top.bar -side bottom -fill x
361     set cscroll .ctop.top.csb
362     scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
363     pack $cscroll -side right -fill y
364     panedwindow .ctop.top.clist -orient horizontal -sashpad 0 -handlesize 4
365     pack .ctop.top.clist -side top -fill both -expand 1
366     .ctop add .ctop.top
367     set canv .ctop.top.clist.canv
368     canvas $canv -height $geometry(canvh) -width $geometry(canv1) \
369         -bg white -bd 0 \
370         -yscrollincr $linespc -yscrollcommand "$cscroll set"
371     .ctop.top.clist add $canv
372     set canv2 .ctop.top.clist.canv2
373     canvas $canv2 -height $geometry(canvh) -width $geometry(canv2) \
374         -bg white -bd 0 -yscrollincr $linespc
375     .ctop.top.clist add $canv2
376     set canv3 .ctop.top.clist.canv3
377     canvas $canv3 -height $geometry(canvh) -width $geometry(canv3) \
378         -bg white -bd 0 -yscrollincr $linespc
379     .ctop.top.clist add $canv3
380     bind .ctop.top.clist <Configure> {resizeclistpanes %W %w}
381
382     set sha1entry .ctop.top.bar.sha1
383     set entries $sha1entry
384     set sha1but .ctop.top.bar.sha1label
385     button $sha1but -text "SHA1 ID: " -state disabled -relief flat \
386         -command gotocommit -width 8
387     $sha1but conf -disabledforeground [$sha1but cget -foreground]
388     pack .ctop.top.bar.sha1label -side left
389     entry $sha1entry -width 40 -font $textfont -textvariable sha1string
390     trace add variable sha1string write sha1change
391     pack $sha1entry -side left -pady 2
392
393     image create bitmap bm-left -data {
394         #define left_width 16
395         #define left_height 16
396         static unsigned char left_bits[] = {
397         0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
398         0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
399         0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
400     }
401     image create bitmap bm-right -data {
402         #define right_width 16
403         #define right_height 16
404         static unsigned char right_bits[] = {
405         0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
406         0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
407         0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
408     }
409     button .ctop.top.bar.leftbut -image bm-left -command goback \
410         -state disabled -width 26
411     pack .ctop.top.bar.leftbut -side left -fill y
412     button .ctop.top.bar.rightbut -image bm-right -command goforw \
413         -state disabled -width 26
414     pack .ctop.top.bar.rightbut -side left -fill y
415
416     button .ctop.top.bar.findbut -text "Find" -command dofind
417     pack .ctop.top.bar.findbut -side left
418     set findstring {}
419     set fstring .ctop.top.bar.findstring
420     lappend entries $fstring
421     entry $fstring -width 30 -font $textfont -textvariable findstring
422     pack $fstring -side left -expand 1 -fill x
423     set findtype Exact
424     set findtypemenu [tk_optionMenu .ctop.top.bar.findtype \
425                           findtype Exact IgnCase Regexp]
426     set findloc "All fields"
427     tk_optionMenu .ctop.top.bar.findloc findloc "All fields" Headline \
428         Comments Author Committer Files Pickaxe
429     pack .ctop.top.bar.findloc -side right
430     pack .ctop.top.bar.findtype -side right
431     # for making sure type==Exact whenever loc==Pickaxe
432     trace add variable findloc write findlocchange
433
434     panedwindow .ctop.cdet -orient horizontal
435     .ctop add .ctop.cdet
436     frame .ctop.cdet.left
437     set ctext .ctop.cdet.left.ctext
438     text $ctext -bg white -state disabled -font $textfont \
439         -width $geometry(ctextw) -height $geometry(ctexth) \
440         -yscrollcommand ".ctop.cdet.left.sb set" -wrap none
441     scrollbar .ctop.cdet.left.sb -command "$ctext yview"
442     pack .ctop.cdet.left.sb -side right -fill y
443     pack $ctext -side left -fill both -expand 1
444     .ctop.cdet add .ctop.cdet.left
445
446     $ctext tag conf filesep -font [concat $textfont bold] -back "#aaaaaa"
447     $ctext tag conf hunksep -fore blue
448     $ctext tag conf d0 -fore red
449     $ctext tag conf d1 -fore "#00a000"
450     $ctext tag conf m0 -fore red
451     $ctext tag conf m1 -fore blue
452     $ctext tag conf m2 -fore green
453     $ctext tag conf m3 -fore purple
454     $ctext tag conf m4 -fore brown
455     $ctext tag conf mmax -fore darkgrey
456     set mergemax 5
457     $ctext tag conf mresult -font [concat $textfont bold]
458     $ctext tag conf msep -font [concat $textfont bold]
459     $ctext tag conf found -back yellow
460
461     frame .ctop.cdet.right
462     set cflist .ctop.cdet.right.cfiles
463     listbox $cflist -bg white -selectmode extended -width $geometry(cflistw) \
464         -yscrollcommand ".ctop.cdet.right.sb set"
465     scrollbar .ctop.cdet.right.sb -command "$cflist yview"
466     pack .ctop.cdet.right.sb -side right -fill y
467     pack $cflist -side left -fill both -expand 1
468     .ctop.cdet add .ctop.cdet.right
469     bind .ctop.cdet <Configure> {resizecdetpanes %W %w}
470
471     pack .ctop -side top -fill both -expand 1
472
473     bindall <1> {selcanvline %W %x %y}
474     #bindall <B1-Motion> {selcanvline %W %x %y}
475     bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
476     bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
477     bindall <2> "allcanvs scan mark 0 %y"
478     bindall <B2-Motion> "allcanvs scan dragto 0 %y"
479     bind . <Key-Up> "selnextline -1"
480     bind . <Key-Down> "selnextline 1"
481     bind . <Key-Right> "goforw"
482     bind . <Key-Left> "goback"
483     bind . <Key-Prior> "allcanvs yview scroll -1 pages"
484     bind . <Key-Next> "allcanvs yview scroll 1 pages"
485     bindkey <Key-Delete> "$ctext yview scroll -1 pages"
486     bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
487     bindkey <Key-space> "$ctext yview scroll 1 pages"
488     bindkey p "selnextline -1"
489     bindkey n "selnextline 1"
490     bindkey z "goback"
491     bindkey x "goforw"
492     bindkey i "selnextline -1"
493     bindkey k "selnextline 1"
494     bindkey j "goback"
495     bindkey l "goforw"
496     bindkey b "$ctext yview scroll -1 pages"
497     bindkey d "$ctext yview scroll 18 units"
498     bindkey u "$ctext yview scroll -18 units"
499     bindkey / {findnext 1}
500     bindkey <Key-Return> {findnext 0}
501     bindkey ? findprev
502     bindkey f nextfile
503     bind . <Control-q> doquit
504     bind . <Control-f> dofind
505     bind . <Control-g> {findnext 0}
506     bind . <Control-r> findprev
507     bind . <Control-equal> {incrfont 1}
508     bind . <Control-KP_Add> {incrfont 1}
509     bind . <Control-minus> {incrfont -1}
510     bind . <Control-KP_Subtract> {incrfont -1}
511     bind $cflist <<ListboxSelect>> listboxsel
512     bind . <Destroy> {savestuff %W}
513     bind . <Button-1> "click %W"
514     bind $fstring <Key-Return> dofind
515     bind $sha1entry <Key-Return> gotocommit
516     bind $sha1entry <<PasteSelection>> clearsha1
517
518     set maincursor [. cget -cursor]
519     set textcursor [$ctext cget -cursor]
520     set curtextcursor $textcursor
521
522     set rowctxmenu .rowctxmenu
523     menu $rowctxmenu -tearoff 0
524     $rowctxmenu add command -label "Diff this -> selected" \
525         -command {diffvssel 0}
526     $rowctxmenu add command -label "Diff selected -> this" \
527         -command {diffvssel 1}
528     $rowctxmenu add command -label "Make patch" -command mkpatch
529     $rowctxmenu add command -label "Create tag" -command mktag
530     $rowctxmenu add command -label "Write commit to file" -command writecommit
531 }
532
533 # when we make a key binding for the toplevel, make sure
534 # it doesn't get triggered when that key is pressed in the
535 # find string entry widget.
536 proc bindkey {ev script} {
537     global entries
538     bind . $ev $script
539     set escript [bind Entry $ev]
540     if {$escript == {}} {
541         set escript [bind Entry <Key>]
542     }
543     foreach e $entries {
544         bind $e $ev "$escript; break"
545     }
546 }
547
548 # set the focus back to the toplevel for any click outside
549 # the entry widgets
550 proc click {w} {
551     global entries
552     foreach e $entries {
553         if {$w == $e} return
554     }
555     focus .
556 }
557
558 proc savestuff {w} {
559     global canv canv2 canv3 ctext cflist mainfont textfont
560     global stuffsaved findmergefiles maxgraphpct
561     global maxwidth
562
563     if {$stuffsaved} return
564     if {![winfo viewable .]} return
565     catch {
566         set f [open "~/.gitk-new" w]
567         puts $f [list set mainfont $mainfont]
568         puts $f [list set textfont $textfont]
569         puts $f [list set findmergefiles $findmergefiles]
570         puts $f [list set maxgraphpct $maxgraphpct]
571         puts $f [list set maxwidth $maxwidth]
572         puts $f "set geometry(width) [winfo width .ctop]"
573         puts $f "set geometry(height) [winfo height .ctop]"
574         puts $f "set geometry(canv1) [expr {[winfo width $canv]-2}]"
575         puts $f "set geometry(canv2) [expr {[winfo width $canv2]-2}]"
576         puts $f "set geometry(canv3) [expr {[winfo width $canv3]-2}]"
577         puts $f "set geometry(canvh) [expr {[winfo height $canv]-2}]"
578         set wid [expr {([winfo width $ctext] - 8) \
579                            / [font measure $textfont "0"]}]
580         puts $f "set geometry(ctextw) $wid"
581         set wid [expr {([winfo width $cflist] - 11) \
582                            / [font measure [$cflist cget -font] "0"]}]
583         puts $f "set geometry(cflistw) $wid"
584         close $f
585         file rename -force "~/.gitk-new" "~/.gitk"
586     }
587     set stuffsaved 1
588 }
589
590 proc resizeclistpanes {win w} {
591     global oldwidth
592     if [info exists oldwidth($win)] {
593         set s0 [$win sash coord 0]
594         set s1 [$win sash coord 1]
595         if {$w < 60} {
596             set sash0 [expr {int($w/2 - 2)}]
597             set sash1 [expr {int($w*5/6 - 2)}]
598         } else {
599             set factor [expr {1.0 * $w / $oldwidth($win)}]
600             set sash0 [expr {int($factor * [lindex $s0 0])}]
601             set sash1 [expr {int($factor * [lindex $s1 0])}]
602             if {$sash0 < 30} {
603                 set sash0 30
604             }
605             if {$sash1 < $sash0 + 20} {
606                 set sash1 [expr {$sash0 + 20}]
607             }
608             if {$sash1 > $w - 10} {
609                 set sash1 [expr {$w - 10}]
610                 if {$sash0 > $sash1 - 20} {
611                     set sash0 [expr {$sash1 - 20}]
612                 }
613             }
614         }
615         $win sash place 0 $sash0 [lindex $s0 1]
616         $win sash place 1 $sash1 [lindex $s1 1]
617     }
618     set oldwidth($win) $w
619 }
620
621 proc resizecdetpanes {win w} {
622     global oldwidth
623     if [info exists oldwidth($win)] {
624         set s0 [$win sash coord 0]
625         if {$w < 60} {
626             set sash0 [expr {int($w*3/4 - 2)}]
627         } else {
628             set factor [expr {1.0 * $w / $oldwidth($win)}]
629             set sash0 [expr {int($factor * [lindex $s0 0])}]
630             if {$sash0 < 45} {
631                 set sash0 45
632             }
633             if {$sash0 > $w - 15} {
634                 set sash0 [expr {$w - 15}]
635             }
636         }
637         $win sash place 0 $sash0 [lindex $s0 1]
638     }
639     set oldwidth($win) $w
640 }
641
642 proc allcanvs args {
643     global canv canv2 canv3
644     eval $canv $args
645     eval $canv2 $args
646     eval $canv3 $args
647 }
648
649 proc bindall {event action} {
650     global canv canv2 canv3
651     bind $canv $event $action
652     bind $canv2 $event $action
653     bind $canv3 $event $action
654 }
655
656 proc about {} {
657     set w .about
658     if {[winfo exists $w]} {
659         raise $w
660         return
661     }
662     toplevel $w
663     wm title $w "About gitk"
664     message $w.m -text {
665 Gitk version 1.2
666
667 Copyright Â© 2005 Paul Mackerras
668
669 Use and redistribute under the terms of the GNU General Public License} \
670             -justify center -aspect 400
671     pack $w.m -side top -fill x -padx 20 -pady 20
672     button $w.ok -text Close -command "destroy $w"
673     pack $w.ok -side bottom
674 }
675
676 proc assigncolor {id} {
677     global colormap commcolors colors nextcolor
678     global parents nparents children nchildren
679     global cornercrossings crossings
680
681     if [info exists colormap($id)] return
682     set ncolors [llength $colors]
683     if {$nparents($id) <= 1 && $nchildren($id) == 1} {
684         set child [lindex $children($id) 0]
685         if {[info exists colormap($child)]
686             && $nparents($child) == 1} {
687             set colormap($id) $colormap($child)
688             return
689         }
690     }
691     set badcolors {}
692     if {[info exists cornercrossings($id)]} {
693         foreach x $cornercrossings($id) {
694             if {[info exists colormap($x)]
695                 && [lsearch -exact $badcolors $colormap($x)] < 0} {
696                 lappend badcolors $colormap($x)
697             }
698         }
699         if {[llength $badcolors] >= $ncolors} {
700             set badcolors {}
701         }
702     }
703     set origbad $badcolors
704     if {[llength $badcolors] < $ncolors - 1} {
705         if {[info exists crossings($id)]} {
706             foreach x $crossings($id) {
707                 if {[info exists colormap($x)]
708                     && [lsearch -exact $badcolors $colormap($x)] < 0} {
709                     lappend badcolors $colormap($x)
710                 }
711             }
712             if {[llength $badcolors] >= $ncolors} {
713                 set badcolors $origbad
714             }
715         }
716         set origbad $badcolors
717     }
718     if {[llength $badcolors] < $ncolors - 1} {
719         foreach child $children($id) {
720             if {[info exists colormap($child)]
721                 && [lsearch -exact $badcolors $colormap($child)] < 0} {
722                 lappend badcolors $colormap($child)
723             }
724             if {[info exists parents($child)]} {
725                 foreach p $parents($child) {
726                     if {[info exists colormap($p)]
727                         && [lsearch -exact $badcolors $colormap($p)] < 0} {
728                         lappend badcolors $colormap($p)
729                     }
730                 }
731             }
732         }
733         if {[llength $badcolors] >= $ncolors} {
734             set badcolors $origbad
735         }
736     }
737     for {set i 0} {$i <= $ncolors} {incr i} {
738         set c [lindex $colors $nextcolor]
739         if {[incr nextcolor] >= $ncolors} {
740             set nextcolor 0
741         }
742         if {[lsearch -exact $badcolors $c]} break
743     }
744     set colormap($id) $c
745 }
746
747 proc initgraph {} {
748     global canvy canvy0 lineno numcommits nextcolor linespc
749     global mainline mainlinearrow sidelines
750     global nchildren ncleft
751     global displist nhyperspace
752
753     allcanvs delete all
754     set nextcolor 0
755     set canvy $canvy0
756     set lineno -1
757     set numcommits 0
758     catch {unset mainline}
759     catch {unset mainlinearrow}
760     catch {unset sidelines}
761     foreach id [array names nchildren] {
762         set ncleft($id) $nchildren($id)
763     }
764     set displist {}
765     set nhyperspace 0
766 }
767
768 proc bindline {t id} {
769     global canv
770
771     $canv bind $t <Enter> "lineenter %x %y $id"
772     $canv bind $t <Motion> "linemotion %x %y $id"
773     $canv bind $t <Leave> "lineleave $id"
774     $canv bind $t <Button-1> "lineclick %x %y $id 1"
775 }
776
777 proc drawlines {id xtra delold} {
778     global mainline mainlinearrow sidelines lthickness colormap canv
779
780     if {$delold} {
781         $canv delete lines.$id
782     }
783     if {[info exists mainline($id)]} {
784         set t [$canv create line $mainline($id) \
785                    -width [expr {($xtra + 1) * $lthickness}] \
786                    -fill $colormap($id) -tags lines.$id \
787                    -arrow $mainlinearrow($id)]
788         $canv lower $t
789         bindline $t $id
790     }
791     if {[info exists sidelines($id)]} {
792         foreach ls $sidelines($id) {
793             set coords [lindex $ls 0]
794             set thick [lindex $ls 1]
795             set arrow [lindex $ls 2]
796             set t [$canv create line $coords -fill $colormap($id) \
797                        -width [expr {($thick + $xtra) * $lthickness}] \
798                        -arrow $arrow -tags lines.$id]
799             $canv lower $t
800             bindline $t $id
801         }
802     }
803 }
804
805 # level here is an index in displist
806 proc drawcommitline {level} {
807     global parents children nparents displist
808     global canv canv2 canv3 mainfont namefont canvy linespc
809     global lineid linehtag linentag linedtag commitinfo
810     global colormap numcommits currentparents dupparents
811     global idtags idline idheads idotherrefs
812     global lineno lthickness mainline mainlinearrow sidelines
813     global commitlisted rowtextx idpos lastuse displist
814     global oldnlines olddlevel olddisplist
815
816     incr numcommits
817     incr lineno
818     set id [lindex $displist $level]
819     set lastuse($id) $lineno
820     set lineid($lineno) $id
821     set idline($id) $lineno
822     set ofill [expr {[info exists commitlisted($id)]? "blue": "white"}]
823     if {![info exists commitinfo($id)]} {
824         readcommit $id
825         if {![info exists commitinfo($id)]} {
826             set commitinfo($id) {"No commit information available"}
827             set nparents($id) 0
828         }
829     }
830     assigncolor $id
831     set currentparents {}
832     set dupparents {}
833     if {[info exists commitlisted($id)] && [info exists parents($id)]} {
834         foreach p $parents($id) {
835             if {[lsearch -exact $currentparents $p] < 0} {
836                 lappend currentparents $p
837             } else {
838                 # remember that this parent was listed twice
839                 lappend dupparents $p
840             }
841         }
842     }
843     set x [xcoord $level $level $lineno]
844     set y1 $canvy
845     set canvy [expr {$canvy + $linespc}]
846     allcanvs conf -scrollregion \
847         [list 0 0 0 [expr {$y1 + 0.5 * $linespc + 2}]]
848     if {[info exists mainline($id)]} {
849         lappend mainline($id) $x $y1
850         if {$mainlinearrow($id) ne "none"} {
851             set mainline($id) [trimdiagstart $mainline($id)]
852         }
853     }
854     drawlines $id 0 0
855     set orad [expr {$linespc / 3}]
856     set t [$canv create oval [expr {$x - $orad}] [expr {$y1 - $orad}] \
857                [expr {$x + $orad - 1}] [expr {$y1 + $orad - 1}] \
858                -fill $ofill -outline black -width 1]
859     $canv raise $t
860     $canv bind $t <1> {selcanvline {} %x %y}
861     set xt [xcoord [llength $displist] $level $lineno]
862     if {[llength $currentparents] > 2} {
863         set xt [expr {$xt + ([llength $currentparents] - 2) * $linespc}]
864     }
865     set rowtextx($lineno) $xt
866     set idpos($id) [list $x $xt $y1]
867     if {[info exists idtags($id)] || [info exists idheads($id)]
868         || [info exists idotherrefs($id)]} {
869         set xt [drawtags $id $x $xt $y1]
870     }
871     set headline [lindex $commitinfo($id) 0]
872     set name [lindex $commitinfo($id) 1]
873     set date [lindex $commitinfo($id) 2]
874     set date [formatdate $date]
875     set linehtag($lineno) [$canv create text $xt $y1 -anchor w \
876                                -text $headline -font $mainfont ]
877     $canv bind $linehtag($lineno) <Button-3> "rowmenu %X %Y $id"
878     set linentag($lineno) [$canv2 create text 3 $y1 -anchor w \
879                                -text $name -font $namefont]
880     set linedtag($lineno) [$canv3 create text 3 $y1 -anchor w \
881                                -text $date -font $mainfont]
882
883     set olddlevel $level
884     set olddisplist $displist
885     set oldnlines [llength $displist]
886 }
887
888 proc drawtags {id x xt y1} {
889     global idtags idheads idotherrefs
890     global linespc lthickness
891     global canv mainfont idline rowtextx
892
893     set marks {}
894     set ntags 0
895     set nheads 0
896     if {[info exists idtags($id)]} {
897         set marks $idtags($id)
898         set ntags [llength $marks]
899     }
900     if {[info exists idheads($id)]} {
901         set marks [concat $marks $idheads($id)]
902         set nheads [llength $idheads($id)]
903     }
904     if {[info exists idotherrefs($id)]} {
905         set marks [concat $marks $idotherrefs($id)]
906     }
907     if {$marks eq {}} {
908         return $xt
909     }
910
911     set delta [expr {int(0.5 * ($linespc - $lthickness))}]
912     set yt [expr {$y1 - 0.5 * $linespc}]
913     set yb [expr {$yt + $linespc - 1}]
914     set xvals {}
915     set wvals {}
916     foreach tag $marks {
917         set wid [font measure $mainfont $tag]
918         lappend xvals $xt
919         lappend wvals $wid
920         set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
921     }
922     set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
923                -width $lthickness -fill black -tags tag.$id]
924     $canv lower $t
925     foreach tag $marks x $xvals wid $wvals {
926         set xl [expr {$x + $delta}]
927         set xr [expr {$x + $delta + $wid + $lthickness}]
928         if {[incr ntags -1] >= 0} {
929             # draw a tag
930             set t [$canv create polygon $x [expr {$yt + $delta}] $xl $yt \
931                        $xr $yt $xr $yb $xl $yb $x [expr {$yb - $delta}] \
932                        -width 1 -outline black -fill yellow -tags tag.$id]
933             $canv bind $t <1> [list showtag $tag 1]
934             set rowtextx($idline($id)) [expr {$xr + $linespc}]
935         } else {
936             # draw a head or other ref
937             if {[incr nheads -1] >= 0} {
938                 set col green
939             } else {
940                 set col "#ddddff"
941             }
942             set xl [expr {$xl - $delta/2}]
943             $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
944                 -width 1 -outline black -fill $col -tags tag.$id
945         }
946         set t [$canv create text $xl $y1 -anchor w -text $tag \
947                    -font $mainfont -tags tag.$id]
948         if {$ntags >= 0} {
949             $canv bind $t <1> [list showtag $tag 1]
950         }
951     }
952     return $xt
953 }
954
955 proc notecrossings {id lo hi corner} {
956     global olddisplist crossings cornercrossings
957
958     for {set i $lo} {[incr i] < $hi} {} {
959         set p [lindex $olddisplist $i]
960         if {$p == {}} continue
961         if {$i == $corner} {
962             if {![info exists cornercrossings($id)]
963                 || [lsearch -exact $cornercrossings($id) $p] < 0} {
964                 lappend cornercrossings($id) $p
965             }
966             if {![info exists cornercrossings($p)]
967                 || [lsearch -exact $cornercrossings($p) $id] < 0} {
968                 lappend cornercrossings($p) $id
969             }
970         } else {
971             if {![info exists crossings($id)]
972                 || [lsearch -exact $crossings($id) $p] < 0} {
973                 lappend crossings($id) $p
974             }
975             if {![info exists crossings($p)]
976                 || [lsearch -exact $crossings($p) $id] < 0} {
977                 lappend crossings($p) $id
978             }
979         }
980     }
981 }
982
983 proc xcoord {i level ln} {
984     global canvx0 xspc1 xspc2
985
986     set x [expr {$canvx0 + $i * $xspc1($ln)}]
987     if {$i > 0 && $i == $level} {
988         set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
989     } elseif {$i > $level} {
990         set x [expr {$x + $xspc2 - $xspc1($ln)}]
991     }
992     return $x
993 }
994
995 # it seems Tk can't draw arrows on the end of diagonal line segments...
996 proc trimdiagend {line} {
997     while {[llength $line] > 4} {
998         set x1 [lindex $line end-3]
999         set y1 [lindex $line end-2]
1000         set x2 [lindex $line end-1]
1001         set y2 [lindex $line end]
1002         if {($x1 == $x2) != ($y1 == $y2)} break
1003         set line [lreplace $line end-1 end]
1004     }
1005     return $line
1006 }
1007
1008 proc trimdiagstart {line} {
1009     while {[llength $line] > 4} {
1010         set x1 [lindex $line 0]
1011         set y1 [lindex $line 1]
1012         set x2 [lindex $line 2]
1013         set y2 [lindex $line 3]
1014         if {($x1 == $x2) != ($y1 == $y2)} break
1015         set line [lreplace $line 0 1]
1016     }
1017     return $line
1018 }
1019
1020 proc drawslants {id needonscreen nohs} {
1021     global canv mainline mainlinearrow sidelines
1022     global canvx0 canvy xspc1 xspc2 lthickness
1023     global currentparents dupparents
1024     global lthickness linespc canvy colormap lineno geometry
1025     global maxgraphpct maxwidth
1026     global displist onscreen lastuse
1027     global parents commitlisted
1028     global oldnlines olddlevel olddisplist
1029     global nhyperspace numcommits nnewparents
1030
1031     if {$lineno < 0} {
1032         lappend displist $id
1033         set onscreen($id) 1
1034         return 0
1035     }
1036
1037     set y1 [expr {$canvy - $linespc}]
1038     set y2 $canvy
1039
1040     # work out what we need to get back on screen
1041     set reins {}
1042     if {$onscreen($id) < 0} {
1043         # next to do isn't displayed, better get it on screen...
1044         lappend reins [list $id 0]
1045     }
1046     # make sure all the previous commits's parents are on the screen
1047     foreach p $currentparents {
1048         if {$onscreen($p) < 0} {
1049             lappend reins [list $p 0]
1050         }
1051     }
1052     # bring back anything requested by caller
1053     if {$needonscreen ne {}} {
1054         lappend reins $needonscreen
1055     }
1056
1057     # try the shortcut
1058     if {$currentparents == $id && $onscreen($id) == 0 && $reins eq {}} {
1059         set dlevel $olddlevel
1060         set x [xcoord $dlevel $dlevel $lineno]
1061         set mainline($id) [list $x $y1]
1062         set mainlinearrow($id) none
1063         set lastuse($id) $lineno
1064         set displist [lreplace $displist $dlevel $dlevel $id]
1065         set onscreen($id) 1
1066         set xspc1([expr {$lineno + 1}]) $xspc1($lineno)
1067         return $dlevel
1068     }
1069
1070     # update displist
1071     set displist [lreplace $displist $olddlevel $olddlevel]
1072     set j $olddlevel
1073     foreach p $currentparents {
1074         set lastuse($p) $lineno
1075         if {$onscreen($p) == 0} {
1076             set displist [linsert $displist $j $p]
1077             set onscreen($p) 1
1078             incr j
1079         }
1080     }
1081     if {$onscreen($id) == 0} {
1082         lappend displist $id
1083         set onscreen($id) 1
1084     }
1085
1086     # remove the null entry if present
1087     set nullentry [lsearch -exact $displist {}]
1088     if {$nullentry >= 0} {
1089         set displist [lreplace $displist $nullentry $nullentry]
1090     }
1091
1092     # bring back the ones we need now (if we did it earlier
1093     # it would change displist and invalidate olddlevel)
1094     foreach pi $reins {
1095         # test again in case of duplicates in reins
1096         set p [lindex $pi 0]
1097         if {$onscreen($p) < 0} {
1098             set onscreen($p) 1
1099             set lastuse($p) $lineno
1100             set displist [linsert $displist [lindex $pi 1] $p]
1101             incr nhyperspace -1
1102         }
1103     }
1104
1105     set lastuse($id) $lineno
1106
1107     # see if we need to make any lines jump off into hyperspace
1108     set displ [llength $displist]
1109     if {$displ > $maxwidth} {
1110         set ages {}
1111         foreach x $displist {
1112             lappend ages [list $lastuse($x) $x]
1113         }
1114         set ages [lsort -integer -index 0 $ages]
1115         set k 0
1116         while {$displ > $maxwidth} {
1117             set use [lindex $ages $k 0]
1118             set victim [lindex $ages $k 1]
1119             if {$use >= $lineno - 5} break
1120             incr k
1121             if {[lsearch -exact $nohs $victim] >= 0} continue
1122             set i [lsearch -exact $displist $victim]
1123             set displist [lreplace $displist $i $i]
1124             set onscreen($victim) -1
1125             incr nhyperspace
1126             incr displ -1
1127             if {$i < $nullentry} {
1128                 incr nullentry -1
1129             }
1130             set x [lindex $mainline($victim) end-1]
1131             lappend mainline($victim) $x $y1
1132             set line [trimdiagend $mainline($victim)]
1133             set arrow "last"
1134             if {$mainlinearrow($victim) ne "none"} {
1135                 set line [trimdiagstart $line]
1136                 set arrow "both"
1137             }
1138             lappend sidelines($victim) [list $line 1 $arrow]
1139             unset mainline($victim)
1140         }
1141     }
1142
1143     set dlevel [lsearch -exact $displist $id]
1144
1145     # If we are reducing, put in a null entry
1146     if {$displ < $oldnlines} {
1147         # does the next line look like a merge?
1148         # i.e. does it have > 1 new parent?
1149         if {$nnewparents($id) > 1} {
1150             set i [expr {$dlevel + 1}]
1151         } elseif {$nnewparents([lindex $olddisplist $olddlevel]) == 0} {
1152             set i $olddlevel
1153             if {$nullentry >= 0 && $nullentry < $i} {
1154                 incr i -1
1155             }
1156         } elseif {$nullentry >= 0} {
1157             set i $nullentry
1158             while {$i < $displ
1159                    && [lindex $olddisplist $i] == [lindex $displist $i]} {
1160                 incr i
1161             }
1162         } else {
1163             set i $olddlevel
1164             if {$dlevel >= $i} {
1165                 incr i
1166             }
1167         }
1168         if {$i < $displ} {
1169             set displist [linsert $displist $i {}]
1170             incr displ
1171             if {$dlevel >= $i} {
1172                 incr dlevel
1173             }
1174         }
1175     }
1176
1177     # decide on the line spacing for the next line
1178     set lj [expr {$lineno + 1}]
1179     set maxw [expr {$maxgraphpct * $geometry(canv1) / 100}]
1180     if {$displ <= 1 || $canvx0 + $displ * $xspc2 <= $maxw} {
1181         set xspc1($lj) $xspc2
1182     } else {
1183         set xspc1($lj) [expr {($maxw - $canvx0 - $xspc2) / ($displ - 1)}]
1184         if {$xspc1($lj) < $lthickness} {
1185             set xspc1($lj) $lthickness
1186         }
1187     }
1188
1189     foreach idi $reins {
1190         set id [lindex $idi 0]
1191         set j [lsearch -exact $displist $id]
1192         set xj [xcoord $j $dlevel $lj]
1193         set mainline($id) [list $xj $y2]
1194         set mainlinearrow($id) first
1195     }
1196
1197     set i -1
1198     foreach id $olddisplist {
1199         incr i
1200         if {$id == {}} continue
1201         if {$onscreen($id) <= 0} continue
1202         set xi [xcoord $i $olddlevel $lineno]
1203         if {$i == $olddlevel} {
1204             foreach p $currentparents {
1205                 set j [lsearch -exact $displist $p]
1206                 set coords [list $xi $y1]
1207                 set xj [xcoord $j $dlevel $lj]
1208                 if {$xj < $xi - $linespc} {
1209                     lappend coords [expr {$xj + $linespc}] $y1
1210                     notecrossings $p $j $i [expr {$j + 1}]
1211                 } elseif {$xj > $xi + $linespc} {
1212                     lappend coords [expr {$xj - $linespc}] $y1
1213                     notecrossings $p $i $j [expr {$j - 1}]
1214                 }
1215                 if {[lsearch -exact $dupparents $p] >= 0} {
1216                     # draw a double-width line to indicate the doubled parent
1217                     lappend coords $xj $y2
1218                     lappend sidelines($p) [list $coords 2 none]
1219                     if {![info exists mainline($p)]} {
1220                         set mainline($p) [list $xj $y2]
1221                         set mainlinearrow($p) none
1222                     }
1223                 } else {
1224                     # normal case, no parent duplicated
1225                     set yb $y2
1226                     set dx [expr {abs($xi - $xj)}]
1227                     if {0 && $dx < $linespc} {
1228                         set yb [expr {$y1 + $dx}]
1229                     }
1230                     if {![info exists mainline($p)]} {
1231                         if {$xi != $xj} {
1232                             lappend coords $xj $yb
1233                         }
1234                         set mainline($p) $coords
1235                         set mainlinearrow($p) none
1236                     } else {
1237                         lappend coords $xj $yb
1238                         if {$yb < $y2} {
1239                             lappend coords $xj $y2
1240                         }
1241                         lappend sidelines($p) [list $coords 1 none]
1242                     }
1243                 }
1244             }
1245         } else {
1246             set j $i
1247             if {[lindex $displist $i] != $id} {
1248                 set j [lsearch -exact $displist $id]
1249             }
1250             if {$j != $i || $xspc1($lineno) != $xspc1($lj)
1251                 || ($olddlevel < $i && $i < $dlevel)
1252                 || ($dlevel < $i && $i < $olddlevel)} {
1253                 set xj [xcoord $j $dlevel $lj]
1254                 lappend mainline($id) $xi $y1 $xj $y2
1255             }
1256         }
1257     }
1258     return $dlevel
1259 }
1260
1261 # search for x in a list of lists
1262 proc llsearch {llist x} {
1263     set i 0
1264     foreach l $llist {
1265         if {$l == $x || [lsearch -exact $l $x] >= 0} {
1266             return $i
1267         }
1268         incr i
1269     }
1270     return -1
1271 }
1272
1273 proc drawmore {reading} {
1274     global displayorder numcommits ncmupdate nextupdate
1275     global stopped nhyperspace parents commitlisted
1276     global maxwidth onscreen displist currentparents olddlevel
1277
1278     set n [llength $displayorder]
1279     while {$numcommits < $n} {
1280         set id [lindex $displayorder $numcommits]
1281         set ctxend [expr {$numcommits + 10}]
1282         if {!$reading && $ctxend > $n} {
1283             set ctxend $n
1284         }
1285         set dlist {}
1286         if {$numcommits > 0} {
1287             set dlist [lreplace $displist $olddlevel $olddlevel]
1288             set i $olddlevel
1289             foreach p $currentparents {
1290                 if {$onscreen($p) == 0} {
1291                     set dlist [linsert $dlist $i $p]
1292                     incr i
1293                 }
1294             }
1295         }
1296         set nohs {}
1297         set reins {}
1298         set isfat [expr {[llength $dlist] > $maxwidth}]
1299         if {$nhyperspace > 0 || $isfat} {
1300             if {$ctxend > $n} break
1301             # work out what to bring back and
1302             # what we want to don't want to send into hyperspace
1303             set room 1
1304             for {set k $numcommits} {$k < $ctxend} {incr k} {
1305                 set x [lindex $displayorder $k]
1306                 set i [llsearch $dlist $x]
1307                 if {$i < 0} {
1308                     set i [llength $dlist]
1309                     lappend dlist $x
1310                 }
1311                 if {[lsearch -exact $nohs $x] < 0} {
1312                     lappend nohs $x
1313                 }
1314                 if {$reins eq {} && $onscreen($x) < 0 && $room} {
1315                     set reins [list $x $i]
1316                 }
1317                 set newp {}
1318                 if {[info exists commitlisted($x)]} {
1319                     set right 0
1320                     foreach p $parents($x) {
1321                         if {[llsearch $dlist $p] < 0} {
1322                             lappend newp $p
1323                             if {[lsearch -exact $nohs $p] < 0} {
1324                                 lappend nohs $p
1325                             }
1326                             if {$reins eq {} && $onscreen($p) < 0 && $room} {
1327                                 set reins [list $p [expr {$i + $right}]]
1328                             }
1329                         }
1330                         set right 1
1331                     }
1332                 }
1333                 set l [lindex $dlist $i]
1334                 if {[llength $l] == 1} {
1335                     set l $newp
1336                 } else {
1337                     set j [lsearch -exact $l $x]
1338                     set l [concat [lreplace $l $j $j] $newp]
1339                 }
1340                 set dlist [lreplace $dlist $i $i $l]
1341                 if {$room && $isfat && [llength $newp] <= 1} {
1342                     set room 0
1343                 }
1344             }
1345         }
1346
1347         set dlevel [drawslants $id $reins $nohs]
1348         drawcommitline $dlevel
1349         if {[clock clicks -milliseconds] >= $nextupdate
1350             && $numcommits >= $ncmupdate} {
1351             doupdate $reading
1352             if {$stopped} break
1353         }
1354     }
1355 }
1356
1357 # level here is an index in todo
1358 proc updatetodo {level noshortcut} {
1359     global ncleft todo nnewparents
1360     global commitlisted parents onscreen
1361
1362     set id [lindex $todo $level]
1363     set olds {}
1364     if {[info exists commitlisted($id)]} {
1365         foreach p $parents($id) {
1366             if {[lsearch -exact $olds $p] < 0} {
1367                 lappend olds $p
1368             }
1369         }
1370     }
1371     if {!$noshortcut && [llength $olds] == 1} {
1372         set p [lindex $olds 0]
1373         if {$ncleft($p) == 1 && [lsearch -exact $todo $p] < 0} {
1374             set ncleft($p) 0
1375             set todo [lreplace $todo $level $level $p]
1376             set onscreen($p) 0
1377             set nnewparents($id) 1
1378             return 0
1379         }
1380     }
1381
1382     set todo [lreplace $todo $level $level]
1383     set i $level
1384     set n 0
1385     foreach p $olds {
1386         incr ncleft($p) -1
1387         set k [lsearch -exact $todo $p]
1388         if {$k < 0} {
1389             set todo [linsert $todo $i $p]
1390             set onscreen($p) 0
1391             incr i
1392             incr n
1393         }
1394     }
1395     set nnewparents($id) $n
1396
1397     return 1
1398 }
1399
1400 proc decidenext {{noread 0}} {
1401     global ncleft todo
1402     global datemode cdate
1403     global commitinfo
1404
1405     # choose which one to do next time around
1406     set todol [llength $todo]
1407     set level -1
1408     set latest {}
1409     for {set k $todol} {[incr k -1] >= 0} {} {
1410         set p [lindex $todo $k]
1411         if {$ncleft($p) == 0} {
1412             if {$datemode} {
1413                 if {![info exists commitinfo($p)]} {
1414                     if {$noread} {
1415                         return {}
1416                     }
1417                     readcommit $p
1418                 }
1419                 if {$latest == {} || $cdate($p) > $latest} {
1420                     set level $k
1421                     set latest $cdate($p)
1422                 }
1423             } else {
1424                 set level $k
1425                 break
1426             }
1427         }
1428     }
1429     if {$level < 0} {
1430         if {$todo != {}} {
1431             puts "ERROR: none of the pending commits can be done yet:"
1432             foreach p $todo {
1433                 puts "  $p ($ncleft($p))"
1434             }
1435         }
1436         return -1
1437     }
1438
1439     return $level
1440 }
1441
1442 proc drawcommit {id reading} {
1443     global phase todo nchildren datemode nextupdate revlistorder
1444     global numcommits ncmupdate displayorder todo onscreen
1445     global numcommits ncmupdate displayorder todo onscreen parents
1446
1447     if {$phase != "incrdraw"} {
1448         set phase incrdraw
1449         set displayorder {}
1450         set todo {}
1451         initgraph
1452     }
1453     if {$nchildren($id) == 0} {
1454         lappend todo $id
1455         set onscreen($id) 0
1456     }
1457     if {$revlistorder} {
1458         set level [lsearch -exact $todo $id]
1459         if {$level < 0} {
1460             error_popup "oops, $id isn't in todo"
1461             return
1462         }
1463         lappend displayorder $id
1464         updatetodo $level 0
1465     } else {
1466         set level [decidenext 1]
1467         if {$level == {} || $id != [lindex $todo $level]} {
1468             return
1469         }
1470         while 1 {
1471             lappend displayorder [lindex $todo $level]
1472             if {[updatetodo $level $datemode]} {
1473                 set level [decidenext 1]
1474                 if {$level == {}} break
1475             }
1476             set id [lindex $todo $level]
1477             if {![info exists commitlisted($id)]} {
1478                 break
1479             }
1480         }
1481     }
1482     drawmore $reading
1483 }
1484
1485 proc finishcommits {} {
1486     global phase oldcommits commits
1487     global canv mainfont ctext maincursor textcursor
1488     global parents
1489
1490     if {$phase == "incrdraw" || $phase == "removecommits"} {
1491         foreach id $oldcommits {
1492             lappend commits $id
1493             updatechildren $id $parents($id)
1494             drawcommit $id 0
1495         }
1496         set oldcommits {}
1497         drawrest
1498     } elseif {$phase == "updatecommits"} {
1499         set phase {}
1500     } else {
1501         $canv delete all
1502         $canv create text 3 3 -anchor nw -text "No commits selected" \
1503             -font $mainfont -tags textitems
1504         set phase {}
1505     }
1506     . config -cursor $maincursor
1507     settextcursor $textcursor
1508 }
1509
1510 # Don't change the text pane cursor if it is currently the hand cursor,
1511 # showing that we are over a sha1 ID link.
1512 proc settextcursor {c} {
1513     global ctext curtextcursor
1514
1515     if {[$ctext cget -cursor] == $curtextcursor} {
1516         $ctext config -cursor $c
1517     }
1518     set curtextcursor $c
1519 }
1520
1521 proc drawgraph {} {
1522     global nextupdate startmsecs ncmupdate
1523     global displayorder onscreen
1524
1525     if {$displayorder == {}} return
1526     set startmsecs [clock clicks -milliseconds]
1527     set nextupdate [expr {$startmsecs + 100}]
1528     set ncmupdate 1
1529     initgraph
1530     foreach id $displayorder {
1531         set onscreen($id) 0
1532     }
1533     drawmore 0
1534 }
1535
1536 proc drawrest {} {
1537     global phase stopped redisplaying selectedline
1538     global datemode todo displayorder
1539     global numcommits ncmupdate
1540     global nextupdate startmsecs revlistorder
1541
1542     set level [decidenext]
1543     if {$level >= 0} {
1544         set phase drawgraph
1545         while 1 {
1546             lappend displayorder [lindex $todo $level]
1547             set hard [updatetodo $level $datemode]
1548             if {$hard} {
1549                 set level [decidenext]
1550                 if {$level < 0} break
1551             }
1552         }
1553     }
1554     drawmore 0
1555     set phase {}
1556     set drawmsecs [expr {[clock clicks -milliseconds] - $startmsecs}]
1557     #puts "overall $drawmsecs ms for $numcommits commits"
1558     if {$redisplaying} {
1559         if {$stopped == 0 && [info exists selectedline]} {
1560             selectline $selectedline 0
1561         }
1562         if {$stopped == 1} {
1563             set stopped 0
1564             after idle drawgraph
1565         } else {
1566             set redisplaying 0
1567         }
1568     }
1569 }
1570
1571 proc findmatches {f} {
1572     global findtype foundstring foundstrlen
1573     if {$findtype == "Regexp"} {
1574         set matches [regexp -indices -all -inline $foundstring $f]
1575     } else {
1576         if {$findtype == "IgnCase"} {
1577             set str [string tolower $f]
1578         } else {
1579             set str $f
1580         }
1581         set matches {}
1582         set i 0
1583         while {[set j [string first $foundstring $str $i]] >= 0} {
1584             lappend matches [list $j [expr {$j+$foundstrlen-1}]]
1585             set i [expr {$j + $foundstrlen}]
1586         }
1587     }
1588     return $matches
1589 }
1590
1591 proc dofind {} {
1592     global findtype findloc findstring markedmatches commitinfo
1593     global numcommits lineid linehtag linentag linedtag
1594     global mainfont namefont canv canv2 canv3 selectedline
1595     global matchinglines foundstring foundstrlen
1596
1597     stopfindproc
1598     unmarkmatches
1599     focus .
1600     set matchinglines {}
1601     if {$findloc == "Pickaxe"} {
1602         findpatches
1603         return
1604     }
1605     if {$findtype == "IgnCase"} {
1606         set foundstring [string tolower $findstring]
1607     } else {
1608         set foundstring $findstring
1609     }
1610     set foundstrlen [string length $findstring]
1611     if {$foundstrlen == 0} return
1612     if {$findloc == "Files"} {
1613         findfiles
1614         return
1615     }
1616     if {![info exists selectedline]} {
1617         set oldsel -1
1618     } else {
1619         set oldsel $selectedline
1620     }
1621     set didsel 0
1622     set fldtypes {Headline Author Date Committer CDate Comment}
1623     for {set l 0} {$l < $numcommits} {incr l} {
1624         set id $lineid($l)
1625         set info $commitinfo($id)
1626         set doesmatch 0
1627         foreach f $info ty $fldtypes {
1628             if {$findloc != "All fields" && $findloc != $ty} {
1629                 continue
1630             }
1631             set matches [findmatches $f]
1632             if {$matches == {}} continue
1633             set doesmatch 1
1634             if {$ty == "Headline"} {
1635                 markmatches $canv $l $f $linehtag($l) $matches $mainfont
1636             } elseif {$ty == "Author"} {
1637                 markmatches $canv2 $l $f $linentag($l) $matches $namefont
1638             } elseif {$ty == "Date"} {
1639                 markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
1640             }
1641         }
1642         if {$doesmatch} {
1643             lappend matchinglines $l
1644             if {!$didsel && $l > $oldsel} {
1645                 findselectline $l
1646                 set didsel 1
1647             }
1648         }
1649     }
1650     if {$matchinglines == {}} {
1651         bell
1652     } elseif {!$didsel} {
1653         findselectline [lindex $matchinglines 0]
1654     }
1655 }
1656
1657 proc findselectline {l} {
1658     global findloc commentend ctext
1659     selectline $l 1
1660     if {$findloc == "All fields" || $findloc == "Comments"} {
1661         # highlight the matches in the comments
1662         set f [$ctext get 1.0 $commentend]
1663         set matches [findmatches $f]
1664         foreach match $matches {
1665             set start [lindex $match 0]
1666             set end [expr {[lindex $match 1] + 1}]
1667             $ctext tag add found "1.0 + $start c" "1.0 + $end c"
1668         }
1669     }
1670 }
1671
1672 proc findnext {restart} {
1673     global matchinglines selectedline
1674     if {![info exists matchinglines]} {
1675         if {$restart} {
1676             dofind
1677         }
1678         return
1679     }
1680     if {![info exists selectedline]} return
1681     foreach l $matchinglines {
1682         if {$l > $selectedline} {
1683             findselectline $l
1684             return
1685         }
1686     }
1687     bell
1688 }
1689
1690 proc findprev {} {
1691     global matchinglines selectedline
1692     if {![info exists matchinglines]} {
1693         dofind
1694         return
1695     }
1696     if {![info exists selectedline]} return
1697     set prev {}
1698     foreach l $matchinglines {
1699         if {$l >= $selectedline} break
1700         set prev $l
1701     }
1702     if {$prev != {}} {
1703         findselectline $prev
1704     } else {
1705         bell
1706     }
1707 }
1708
1709 proc findlocchange {name ix op} {
1710     global findloc findtype findtypemenu
1711     if {$findloc == "Pickaxe"} {
1712         set findtype Exact
1713         set state disabled
1714     } else {
1715         set state normal
1716     }
1717     $findtypemenu entryconf 1 -state $state
1718     $findtypemenu entryconf 2 -state $state
1719 }
1720
1721 proc stopfindproc {{done 0}} {
1722     global findprocpid findprocfile findids
1723     global ctext findoldcursor phase maincursor textcursor
1724     global findinprogress
1725
1726     catch {unset findids}
1727     if {[info exists findprocpid]} {
1728         if {!$done} {
1729             catch {exec kill $findprocpid}
1730         }
1731         catch {close $findprocfile}
1732         unset findprocpid
1733     }
1734     if {[info exists findinprogress]} {
1735         unset findinprogress
1736         if {$phase != "incrdraw"} {
1737             . config -cursor $maincursor
1738             settextcursor $textcursor
1739         }
1740     }
1741 }
1742
1743 proc findpatches {} {
1744     global findstring selectedline numcommits
1745     global findprocpid findprocfile
1746     global finddidsel ctext lineid findinprogress
1747     global findinsertpos
1748
1749     if {$numcommits == 0} return
1750
1751     # make a list of all the ids to search, starting at the one
1752     # after the selected line (if any)
1753     if {[info exists selectedline]} {
1754         set l $selectedline
1755     } else {
1756         set l -1
1757     }
1758     set inputids {}
1759     for {set i 0} {$i < $numcommits} {incr i} {
1760         if {[incr l] >= $numcommits} {
1761             set l 0
1762         }
1763         append inputids $lineid($l) "\n"
1764     }
1765
1766     if {[catch {
1767         set f [open [list | git-diff-tree --stdin -s -r -S$findstring \
1768                          << $inputids] r]
1769     } err]} {
1770         error_popup "Error starting search process: $err"
1771         return
1772     }
1773
1774     set findinsertpos end
1775     set findprocfile $f
1776     set findprocpid [pid $f]
1777     fconfigure $f -blocking 0
1778     fileevent $f readable readfindproc
1779     set finddidsel 0
1780     . config -cursor watch
1781     settextcursor watch
1782     set findinprogress 1
1783 }
1784
1785 proc readfindproc {} {
1786     global findprocfile finddidsel
1787     global idline matchinglines findinsertpos
1788
1789     set n [gets $findprocfile line]
1790     if {$n < 0} {
1791         if {[eof $findprocfile]} {
1792             stopfindproc 1
1793             if {!$finddidsel} {
1794                 bell
1795             }
1796         }
1797         return
1798     }
1799     if {![regexp {^[0-9a-f]{40}} $line id]} {
1800         error_popup "Can't parse git-diff-tree output: $line"
1801         stopfindproc
1802         return
1803     }
1804     if {![info exists idline($id)]} {
1805         puts stderr "spurious id: $id"
1806         return
1807     }
1808     set l $idline($id)
1809     insertmatch $l $id
1810 }
1811
1812 proc insertmatch {l id} {
1813     global matchinglines findinsertpos finddidsel
1814
1815     if {$findinsertpos == "end"} {
1816         if {$matchinglines != {} && $l < [lindex $matchinglines 0]} {
1817             set matchinglines [linsert $matchinglines 0 $l]
1818             set findinsertpos 1
1819         } else {
1820             lappend matchinglines $l
1821         }
1822     } else {
1823         set matchinglines [linsert $matchinglines $findinsertpos $l]
1824         incr findinsertpos
1825     }
1826     markheadline $l $id
1827     if {!$finddidsel} {
1828         findselectline $l
1829         set finddidsel 1
1830     }
1831 }
1832
1833 proc findfiles {} {
1834     global selectedline numcommits lineid ctext
1835     global ffileline finddidsel parents nparents
1836     global findinprogress findstartline findinsertpos
1837     global treediffs fdiffids fdiffsneeded fdiffpos
1838     global findmergefiles
1839
1840     if {$numcommits == 0} return
1841
1842     if {[info exists selectedline]} {
1843         set l [expr {$selectedline + 1}]
1844     } else {
1845         set l 0
1846     }
1847     set ffileline $l
1848     set findstartline $l
1849     set diffsneeded {}
1850     set fdiffsneeded {}
1851     while 1 {
1852         set id $lineid($l)
1853         if {$findmergefiles || $nparents($id) == 1} {
1854             foreach p $parents($id) {
1855                 if {![info exists treediffs([list $id $p])]} {
1856                     append diffsneeded "$id $p\n"
1857                     lappend fdiffsneeded [list $id $p]
1858                 }
1859             }
1860         }
1861         if {[incr l] >= $numcommits} {
1862             set l 0
1863         }
1864         if {$l == $findstartline} break
1865     }
1866
1867     # start off a git-diff-tree process if needed
1868     if {$diffsneeded ne {}} {
1869         if {[catch {
1870             set df [open [list | git-diff-tree -r --stdin << $diffsneeded] r]
1871         } err ]} {
1872             error_popup "Error starting search process: $err"
1873             return
1874         }
1875         catch {unset fdiffids}
1876         set fdiffpos 0
1877         fconfigure $df -blocking 0
1878         fileevent $df readable [list readfilediffs $df]
1879     }
1880
1881     set finddidsel 0
1882     set findinsertpos end
1883     set id $lineid($l)
1884     set p [lindex $parents($id) 0]
1885     . config -cursor watch
1886     settextcursor watch
1887     set findinprogress 1
1888     findcont [list $id $p]
1889     update
1890 }
1891
1892 proc readfilediffs {df} {
1893     global findids fdiffids fdiffs
1894
1895     set n [gets $df line]
1896     if {$n < 0} {
1897         if {[eof $df]} {
1898             donefilediff
1899             if {[catch {close $df} err]} {
1900                 stopfindproc
1901                 bell
1902                 error_popup "Error in git-diff-tree: $err"
1903             } elseif {[info exists findids]} {
1904                 set ids $findids
1905                 stopfindproc
1906                 bell
1907                 error_popup "Couldn't find diffs for {$ids}"
1908             }
1909         }
1910         return
1911     }
1912     if {[regexp {^([0-9a-f]{40}) \(from ([0-9a-f]{40})\)} $line match id p]} {
1913         # start of a new string of diffs
1914         donefilediff
1915         set fdiffids [list $id $p]
1916         set fdiffs {}
1917     } elseif {[string match ":*" $line]} {
1918         lappend fdiffs [lindex $line 5]
1919     }
1920 }
1921
1922 proc donefilediff {} {
1923     global fdiffids fdiffs treediffs findids
1924     global fdiffsneeded fdiffpos
1925
1926     if {[info exists fdiffids]} {
1927         while {[lindex $fdiffsneeded $fdiffpos] ne $fdiffids
1928                && $fdiffpos < [llength $fdiffsneeded]} {
1929             # git-diff-tree doesn't output anything for a commit
1930             # which doesn't change anything
1931             set nullids [lindex $fdiffsneeded $fdiffpos]
1932             set treediffs($nullids) {}
1933             if {[info exists findids] && $nullids eq $findids} {
1934                 unset findids
1935                 findcont $nullids
1936             }
1937             incr fdiffpos
1938         }
1939         incr fdiffpos
1940
1941         if {![info exists treediffs($fdiffids)]} {
1942             set treediffs($fdiffids) $fdiffs
1943         }
1944         if {[info exists findids] && $fdiffids eq $findids} {
1945             unset findids
1946             findcont $fdiffids
1947         }
1948     }
1949 }
1950
1951 proc findcont {ids} {
1952     global findids treediffs parents nparents
1953     global ffileline findstartline finddidsel
1954     global lineid numcommits matchinglines findinprogress
1955     global findmergefiles
1956
1957     set id [lindex $ids 0]
1958     set p [lindex $ids 1]
1959     set pi [lsearch -exact $parents($id) $p]
1960     set l $ffileline
1961     while 1 {
1962         if {$findmergefiles || $nparents($id) == 1} {
1963             if {![info exists treediffs($ids)]} {
1964                 set findids $ids
1965                 set ffileline $l
1966                 return
1967             }
1968             set doesmatch 0
1969             foreach f $treediffs($ids) {
1970                 set x [findmatches $f]
1971                 if {$x != {}} {
1972                     set doesmatch 1
1973                     break
1974                 }
1975             }
1976             if {$doesmatch} {
1977                 insertmatch $l $id
1978                 set pi $nparents($id)
1979             }
1980         } else {
1981             set pi $nparents($id)
1982         }
1983         if {[incr pi] >= $nparents($id)} {
1984             set pi 0
1985             if {[incr l] >= $numcommits} {
1986                 set l 0
1987             }
1988             if {$l == $findstartline} break
1989             set id $lineid($l)
1990         }
1991         set p [lindex $parents($id) $pi]
1992         set ids [list $id $p]
1993     }
1994     stopfindproc
1995     if {!$finddidsel} {
1996         bell
1997     }
1998 }
1999
2000 # mark a commit as matching by putting a yellow background
2001 # behind the headline
2002 proc markheadline {l id} {
2003     global canv mainfont linehtag commitinfo
2004
2005     set bbox [$canv bbox $linehtag($l)]
2006     set t [$canv create rect $bbox -outline {} -tags matches -fill yellow]
2007     $canv lower $t
2008 }
2009
2010 # mark the bits of a headline, author or date that match a find string
2011 proc markmatches {canv l str tag matches font} {
2012     set bbox [$canv bbox $tag]
2013     set x0 [lindex $bbox 0]
2014     set y0 [lindex $bbox 1]
2015     set y1 [lindex $bbox 3]
2016     foreach match $matches {
2017         set start [lindex $match 0]
2018         set end [lindex $match 1]
2019         if {$start > $end} continue
2020         set xoff [font measure $font [string range $str 0 [expr {$start-1}]]]
2021         set xlen [font measure $font [string range $str 0 [expr {$end}]]]
2022         set t [$canv create rect [expr {$x0+$xoff}] $y0 \
2023                    [expr {$x0+$xlen+2}] $y1 \
2024                    -outline {} -tags matches -fill yellow]
2025         $canv lower $t
2026     }
2027 }
2028
2029 proc unmarkmatches {} {
2030     global matchinglines findids
2031     allcanvs delete matches
2032     catch {unset matchinglines}
2033     catch {unset findids}
2034 }
2035
2036 proc selcanvline {w x y} {
2037     global canv canvy0 ctext linespc
2038     global lineid linehtag linentag linedtag rowtextx
2039     set ymax [lindex [$canv cget -scrollregion] 3]
2040     if {$ymax == {}} return
2041     set yfrac [lindex [$canv yview] 0]
2042     set y [expr {$y + $yfrac * $ymax}]
2043     set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
2044     if {$l < 0} {
2045         set l 0
2046     }
2047     if {$w eq $canv} {
2048         if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
2049     }
2050     unmarkmatches
2051     selectline $l 1
2052 }
2053
2054 proc commit_descriptor {p} {
2055     global commitinfo
2056     set l "..."
2057     if {[info exists commitinfo($p)]} {
2058         set l [lindex $commitinfo($p) 0]
2059     }
2060     return "$p ($l)"
2061 }
2062
2063 # append some text to the ctext widget, and make any SHA1 ID
2064 # that we know about be a clickable link.
2065 proc appendwithlinks {text} {
2066     global ctext idline linknum
2067
2068     set start [$ctext index "end - 1c"]
2069     $ctext insert end $text
2070     $ctext insert end "\n"
2071     set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
2072     foreach l $links {
2073         set s [lindex $l 0]
2074         set e [lindex $l 1]
2075         set linkid [string range $text $s $e]
2076         if {![info exists idline($linkid)]} continue
2077         incr e
2078         $ctext tag add link "$start + $s c" "$start + $e c"
2079         $ctext tag add link$linknum "$start + $s c" "$start + $e c"
2080         $ctext tag bind link$linknum <1> [list selectline $idline($linkid) 1]
2081         incr linknum
2082     }
2083     $ctext tag conf link -foreground blue -underline 1
2084     $ctext tag bind link <Enter> { %W configure -cursor hand2 }
2085     $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
2086 }
2087
2088 proc selectline {l isnew} {
2089     global canv canv2 canv3 ctext commitinfo selectedline
2090     global lineid linehtag linentag linedtag
2091     global canvy0 linespc parents nparents children
2092     global cflist currentid sha1entry
2093     global commentend idtags idline linknum
2094
2095     $canv delete hover
2096     normalline
2097     if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
2098     $canv delete secsel
2099     set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
2100                -tags secsel -fill [$canv cget -selectbackground]]
2101     $canv lower $t
2102     $canv2 delete secsel
2103     set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
2104                -tags secsel -fill [$canv2 cget -selectbackground]]
2105     $canv2 lower $t
2106     $canv3 delete secsel
2107     set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
2108                -tags secsel -fill [$canv3 cget -selectbackground]]
2109     $canv3 lower $t
2110     set y [expr {$canvy0 + $l * $linespc}]
2111     set ymax [lindex [$canv cget -scrollregion] 3]
2112     set ytop [expr {$y - $linespc - 1}]
2113     set ybot [expr {$y + $linespc + 1}]
2114     set wnow [$canv yview]
2115     set wtop [expr {[lindex $wnow 0] * $ymax}]
2116     set wbot [expr {[lindex $wnow 1] * $ymax}]
2117     set wh [expr {$wbot - $wtop}]
2118     set newtop $wtop
2119     if {$ytop < $wtop} {
2120         if {$ybot < $wtop} {
2121             set newtop [expr {$y - $wh / 2.0}]
2122         } else {
2123             set newtop $ytop
2124             if {$newtop > $wtop - $linespc} {
2125                 set newtop [expr {$wtop - $linespc}]
2126             }
2127         }
2128     } elseif {$ybot > $wbot} {
2129         if {$ytop > $wbot} {
2130             set newtop [expr {$y - $wh / 2.0}]
2131         } else {
2132             set newtop [expr {$ybot - $wh}]
2133             if {$newtop < $wtop + $linespc} {
2134                 set newtop [expr {$wtop + $linespc}]
2135             }
2136         }
2137     }
2138     if {$newtop != $wtop} {
2139         if {$newtop < 0} {
2140             set newtop 0
2141         }
2142         allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
2143     }
2144
2145     if {$isnew} {
2146         addtohistory [list selectline $l 0]
2147     }
2148
2149     set selectedline $l
2150
2151     set id $lineid($l)
2152     set currentid $id
2153     $sha1entry delete 0 end
2154     $sha1entry insert 0 $id
2155     $sha1entry selection from 0
2156     $sha1entry selection to end
2157
2158     $ctext conf -state normal
2159     $ctext delete 0.0 end
2160     set linknum 0
2161     $ctext mark set fmark.0 0.0
2162     $ctext mark gravity fmark.0 left
2163     set info $commitinfo($id)
2164     set date [formatdate [lindex $info 2]]
2165     $ctext insert end "Author: [lindex $info 1]  $date\n"
2166     set date [formatdate [lindex $info 4]]
2167     $ctext insert end "Committer: [lindex $info 3]  $date\n"
2168     if {[info exists idtags($id)]} {
2169         $ctext insert end "Tags:"
2170         foreach tag $idtags($id) {
2171             $ctext insert end " $tag"
2172         }
2173         $ctext insert end "\n"
2174     }
2175  
2176     set comment {}
2177     if {[info exists parents($id)]} {
2178         foreach p $parents($id) {
2179             append comment "Parent: [commit_descriptor $p]\n"
2180         }
2181     }
2182     if {[info exists children($id)]} {
2183         foreach c $children($id) {
2184             append comment "Child:  [commit_descriptor $c]\n"
2185         }
2186     }
2187     append comment "\n"
2188     append comment [lindex $info 5]
2189
2190     # make anything that looks like a SHA1 ID be a clickable link
2191     appendwithlinks $comment
2192
2193     $ctext tag delete Comments
2194     $ctext tag remove found 1.0 end
2195     $ctext conf -state disabled
2196     set commentend [$ctext index "end - 1c"]
2197
2198     $cflist delete 0 end
2199     $cflist insert end "Comments"
2200     if {$nparents($id) == 1} {
2201         startdiff $id
2202     } elseif {$nparents($id) > 1} {
2203         mergediff $id
2204     }
2205 }
2206
2207 proc selnextline {dir} {
2208     global selectedline
2209     if {![info exists selectedline]} return
2210     set l [expr {$selectedline + $dir}]
2211     unmarkmatches
2212     selectline $l 1
2213 }
2214
2215 proc unselectline {} {
2216     global selectedline
2217
2218     catch {unset selectedline}
2219     allcanvs delete secsel
2220 }
2221
2222 proc addtohistory {cmd} {
2223     global history historyindex
2224
2225     if {$historyindex > 0
2226         && [lindex $history [expr {$historyindex - 1}]] == $cmd} {
2227         return
2228     }
2229
2230     if {$historyindex < [llength $history]} {
2231         set history [lreplace $history $historyindex end $cmd]
2232     } else {
2233         lappend history $cmd
2234     }
2235     incr historyindex
2236     if {$historyindex > 1} {
2237         .ctop.top.bar.leftbut conf -state normal
2238     } else {
2239         .ctop.top.bar.leftbut conf -state disabled
2240     }
2241     .ctop.top.bar.rightbut conf -state disabled
2242 }
2243
2244 proc goback {} {
2245     global history historyindex
2246
2247     if {$historyindex > 1} {
2248         incr historyindex -1
2249         set cmd [lindex $history [expr {$historyindex - 1}]]
2250         eval $cmd
2251         .ctop.top.bar.rightbut conf -state normal
2252     }
2253     if {$historyindex <= 1} {
2254         .ctop.top.bar.leftbut conf -state disabled
2255     }
2256 }
2257
2258 proc goforw {} {
2259     global history historyindex
2260
2261     if {$historyindex < [llength $history]} {
2262         set cmd [lindex $history $historyindex]
2263         incr historyindex
2264         eval $cmd
2265         .ctop.top.bar.leftbut conf -state normal
2266     }
2267     if {$historyindex >= [llength $history]} {
2268         .ctop.top.bar.rightbut conf -state disabled
2269     }
2270 }
2271
2272 proc mergediff {id} {
2273     global parents diffmergeid diffmergegca mergefilelist diffpindex
2274
2275     set diffmergeid $id
2276     set diffpindex -1
2277     set diffmergegca [findgca $parents($id)]
2278     if {[info exists mergefilelist($id)]} {
2279         if {$mergefilelist($id) ne {}} {
2280             showmergediff
2281         }
2282     } else {
2283         contmergediff {}
2284     }
2285 }
2286
2287 proc findgca {ids} {
2288     set gca {}
2289     foreach id $ids {
2290         if {$gca eq {}} {
2291             set gca $id
2292         } else {
2293             if {[catch {
2294                 set gca [exec git-merge-base $gca $id]
2295             } err]} {
2296                 return {}
2297             }
2298         }
2299     }
2300     return $gca
2301 }
2302
2303 proc contmergediff {ids} {
2304     global diffmergeid diffpindex parents nparents diffmergegca
2305     global treediffs mergefilelist diffids treepending
2306
2307     # diff the child against each of the parents, and diff
2308     # each of the parents against the GCA.
2309     while 1 {
2310         if {[lindex $ids 1] == $diffmergeid && $diffmergegca ne {}} {
2311             set ids [list $diffmergegca [lindex $ids 0]]
2312         } else {
2313             if {[incr diffpindex] >= $nparents($diffmergeid)} break
2314             set p [lindex $parents($diffmergeid) $diffpindex]
2315             set ids [list $p $diffmergeid]
2316         }
2317         if {![info exists treediffs($ids)]} {
2318             set diffids $ids
2319             if {![info exists treepending]} {
2320                 gettreediffs $ids
2321             }
2322             return
2323         }
2324     }
2325
2326     # If a file in some parent is different from the child and also
2327     # different from the GCA, then it's interesting.
2328     # If we don't have a GCA, then a file is interesting if it is
2329     # different from the child in all the parents.
2330     if {$diffmergegca ne {}} {
2331         set files {}
2332         foreach p $parents($diffmergeid) {
2333             set gcadiffs $treediffs([list $diffmergegca $p])
2334             foreach f $treediffs([list $p $diffmergeid]) {
2335                 if {[lsearch -exact $files $f] < 0
2336                     && [lsearch -exact $gcadiffs $f] >= 0} {
2337                     lappend files $f
2338                 }
2339             }
2340         }
2341         set files [lsort $files]
2342     } else {
2343         set p [lindex $parents($diffmergeid) 0]
2344         set files $treediffs([list $diffmergeid $p])
2345         for {set i 1} {$i < $nparents($diffmergeid) && $files ne {}} {incr i} {
2346             set p [lindex $parents($diffmergeid) $i]
2347             set df $treediffs([list $p $diffmergeid])
2348             set nf {}
2349             foreach f $files {
2350                 if {[lsearch -exact $df $f] >= 0} {
2351                     lappend nf $f
2352                 }
2353             }
2354             set files $nf
2355         }
2356     }
2357
2358     set mergefilelist($diffmergeid) $files
2359     if {$files ne {}} {
2360         showmergediff
2361     }
2362 }
2363
2364 proc showmergediff {} {
2365     global cflist diffmergeid mergefilelist parents
2366     global diffopts diffinhunk currentfile currenthunk filelines
2367     global diffblocked groupfilelast mergefds groupfilenum grouphunks
2368
2369     set files $mergefilelist($diffmergeid)
2370     foreach f $files {
2371         $cflist insert end $f
2372     }
2373     set env(GIT_DIFF_OPTS) $diffopts
2374     set flist {}
2375     catch {unset currentfile}
2376     catch {unset currenthunk}
2377     catch {unset filelines}
2378     catch {unset groupfilenum}
2379     catch {unset grouphunks}
2380     set groupfilelast -1
2381     foreach p $parents($diffmergeid) {
2382         set cmd [list | git-diff-tree -p $p $diffmergeid]
2383         set cmd [concat $cmd $mergefilelist($diffmergeid)]
2384         if {[catch {set f [open $cmd r]} err]} {
2385             error_popup "Error getting diffs: $err"
2386             foreach f $flist {
2387                 catch {close $f}
2388             }
2389             return
2390         }
2391         lappend flist $f
2392         set ids [list $diffmergeid $p]
2393         set mergefds($ids) $f
2394         set diffinhunk($ids) 0
2395         set diffblocked($ids) 0
2396         fconfigure $f -blocking 0
2397         fileevent $f readable [list getmergediffline $f $ids $diffmergeid]
2398     }
2399 }
2400
2401 proc getmergediffline {f ids id} {
2402     global diffmergeid diffinhunk diffoldlines diffnewlines
2403     global currentfile currenthunk
2404     global diffoldstart diffnewstart diffoldlno diffnewlno
2405     global diffblocked mergefilelist
2406     global noldlines nnewlines difflcounts filelines
2407
2408     set n [gets $f line]
2409     if {$n < 0} {
2410         if {![eof $f]} return
2411     }
2412
2413     if {!([info exists diffmergeid] && $diffmergeid == $id)} {
2414         if {$n < 0} {
2415             close $f
2416         }
2417         return
2418     }
2419
2420     if {$diffinhunk($ids) != 0} {
2421         set fi $currentfile($ids)
2422         if {$n > 0 && [regexp {^[-+ \\]} $line match]} {
2423             # continuing an existing hunk
2424             set line [string range $line 1 end]
2425             set p [lindex $ids 1]
2426             if {$match eq "-" || $match eq " "} {
2427                 set filelines($p,$fi,$diffoldlno($ids)) $line
2428                 incr diffoldlno($ids)
2429             }
2430             if {$match eq "+" || $match eq " "} {
2431                 set filelines($id,$fi,$diffnewlno($ids)) $line
2432                 incr diffnewlno($ids)
2433             }
2434             if {$match eq " "} {
2435                 if {$diffinhunk($ids) == 2} {
2436                     lappend difflcounts($ids) \
2437                         [list $noldlines($ids) $nnewlines($ids)]
2438                     set noldlines($ids) 0
2439                     set diffinhunk($ids) 1
2440                 }
2441                 incr noldlines($ids)
2442             } elseif {$match eq "-" || $match eq "+"} {
2443                 if {$diffinhunk($ids) == 1} {
2444                     lappend difflcounts($ids) [list $noldlines($ids)]
2445                     set noldlines($ids) 0
2446                     set nnewlines($ids) 0
2447                     set diffinhunk($ids) 2
2448                 }
2449                 if {$match eq "-"} {
2450                     incr noldlines($ids)
2451                 } else {
2452                     incr nnewlines($ids)
2453                 }
2454             }
2455             # and if it's \ No newline at end of line, then what?
2456             return
2457         }
2458         # end of a hunk
2459         if {$diffinhunk($ids) == 1 && $noldlines($ids) != 0} {
2460             lappend difflcounts($ids) [list $noldlines($ids)]
2461         } elseif {$diffinhunk($ids) == 2
2462                   && ($noldlines($ids) != 0 || $nnewlines($ids) != 0)} {
2463             lappend difflcounts($ids) [list $noldlines($ids) $nnewlines($ids)]
2464         }
2465         set currenthunk($ids) [list $currentfile($ids) \
2466                                    $diffoldstart($ids) $diffnewstart($ids) \
2467                                    $diffoldlno($ids) $diffnewlno($ids) \
2468                                    $difflcounts($ids)]
2469         set diffinhunk($ids) 0
2470         # -1 = need to block, 0 = unblocked, 1 = is blocked
2471         set diffblocked($ids) -1
2472         processhunks
2473         if {$diffblocked($ids) == -1} {
2474             fileevent $f readable {}
2475             set diffblocked($ids) 1
2476         }
2477     }
2478
2479     if {$n < 0} {
2480         # eof
2481         if {!$diffblocked($ids)} {
2482             close $f
2483             set currentfile($ids) [llength $mergefilelist($diffmergeid)]
2484             set currenthunk($ids) [list $currentfile($ids) 0 0 0 0 {}]
2485             processhunks
2486         }
2487     } elseif {[regexp {^diff --git a/(.*) b/} $line match fname]} {
2488         # start of a new file
2489         set currentfile($ids) \
2490             [lsearch -exact $mergefilelist($diffmergeid) $fname]
2491     } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
2492                    $line match f1l f1c f2l f2c rest]} {
2493         if {[info exists currentfile($ids)] && $currentfile($ids) >= 0} {
2494             # start of a new hunk
2495             if {$f1l == 0 && $f1c == 0} {
2496                 set f1l 1
2497             }
2498             if {$f2l == 0 && $f2c == 0} {
2499                 set f2l 1
2500             }
2501             set diffinhunk($ids) 1
2502             set diffoldstart($ids) $f1l
2503             set diffnewstart($ids) $f2l
2504             set diffoldlno($ids) $f1l
2505             set diffnewlno($ids) $f2l
2506             set difflcounts($ids) {}
2507             set noldlines($ids) 0
2508             set nnewlines($ids) 0
2509         }
2510     }
2511 }
2512
2513 proc processhunks {} {
2514     global diffmergeid parents nparents currenthunk
2515     global mergefilelist diffblocked mergefds
2516     global grouphunks grouplinestart grouplineend groupfilenum
2517
2518     set nfiles [llength $mergefilelist($diffmergeid)]
2519     while 1 {
2520         set fi $nfiles
2521         set lno 0
2522         # look for the earliest hunk
2523         foreach p $parents($diffmergeid) {
2524             set ids [list $diffmergeid $p]
2525             if {![info exists currenthunk($ids)]} return
2526             set i [lindex $currenthunk($ids) 0]
2527             set l [lindex $currenthunk($ids) 2]
2528             if {$i < $fi || ($i == $fi && $l < $lno)} {
2529                 set fi $i
2530                 set lno $l
2531                 set pi $p
2532             }
2533         }
2534
2535         if {$fi < $nfiles} {
2536             set ids [list $diffmergeid $pi]
2537             set hunk $currenthunk($ids)
2538             unset currenthunk($ids)
2539             if {$diffblocked($ids) > 0} {
2540                 fileevent $mergefds($ids) readable \
2541                     [list getmergediffline $mergefds($ids) $ids $diffmergeid]
2542             }
2543             set diffblocked($ids) 0
2544
2545             if {[info exists groupfilenum] && $groupfilenum == $fi
2546                 && $lno <= $grouplineend} {
2547                 # add this hunk to the pending group
2548                 lappend grouphunks($pi) $hunk
2549                 set endln [lindex $hunk 4]
2550                 if {$endln > $grouplineend} {
2551                     set grouplineend $endln
2552                 }
2553                 continue
2554             }
2555         }
2556
2557         # succeeding stuff doesn't belong in this group, so
2558         # process the group now
2559         if {[info exists groupfilenum]} {
2560             processgroup
2561             unset groupfilenum
2562             unset grouphunks
2563         }
2564
2565         if {$fi >= $nfiles} break
2566
2567         # start a new group
2568         set groupfilenum $fi
2569         set grouphunks($pi) [list $hunk]
2570         set grouplinestart $lno
2571         set grouplineend [lindex $hunk 4]
2572     }
2573 }
2574
2575 proc processgroup {} {
2576     global groupfilelast groupfilenum difffilestart
2577     global mergefilelist diffmergeid ctext filelines
2578     global parents diffmergeid diffoffset
2579     global grouphunks grouplinestart grouplineend nparents
2580     global mergemax
2581
2582     $ctext conf -state normal
2583     set id $diffmergeid
2584     set f $groupfilenum
2585     if {$groupfilelast != $f} {
2586         $ctext insert end "\n"
2587         set here [$ctext index "end - 1c"]
2588         set difffilestart($f) $here
2589         set mark fmark.[expr {$f + 1}]
2590         $ctext mark set $mark $here
2591         $ctext mark gravity $mark left
2592         set header [lindex $mergefilelist($id) $f]
2593         set l [expr {(78 - [string length $header]) / 2}]
2594         set pad [string range "----------------------------------------" 1 $l]
2595         $ctext insert end "$pad $header $pad\n" filesep
2596         set groupfilelast $f
2597         foreach p $parents($id) {
2598             set diffoffset($p) 0
2599         }
2600     }
2601
2602     $ctext insert end "@@" msep
2603     set nlines [expr {$grouplineend - $grouplinestart}]
2604     set events {}
2605     set pnum 0
2606     foreach p $parents($id) {
2607         set startline [expr {$grouplinestart + $diffoffset($p)}]
2608         set ol $startline
2609         set nl $grouplinestart
2610         if {[info exists grouphunks($p)]} {
2611             foreach h $grouphunks($p) {
2612                 set l [lindex $h 2]
2613                 if {$nl < $l} {
2614                     for {} {$nl < $l} {incr nl} {
2615                         set filelines($p,$f,$ol) $filelines($id,$f,$nl)
2616                         incr ol
2617                     }
2618                 }
2619                 foreach chunk [lindex $h 5] {
2620                     if {[llength $chunk] == 2} {
2621                         set olc [lindex $chunk 0]
2622                         set nlc [lindex $chunk 1]
2623                         set nnl [expr {$nl + $nlc}]
2624                         lappend events [list $nl $nnl $pnum $olc $nlc]
2625                         incr ol $olc
2626                         set nl $nnl
2627                     } else {
2628                         incr ol [lindex $chunk 0]
2629                         incr nl [lindex $chunk 0]
2630                     }
2631                 }
2632             }
2633         }
2634         if {$nl < $grouplineend} {
2635             for {} {$nl < $grouplineend} {incr nl} {
2636                 set filelines($p,$f,$ol) $filelines($id,$f,$nl)
2637                 incr ol
2638             }
2639         }
2640         set nlines [expr {$ol - $startline}]
2641         $ctext insert end " -$startline,$nlines" msep
2642         incr pnum
2643     }
2644
2645     set nlines [expr {$grouplineend - $grouplinestart}]
2646     $ctext insert end " +$grouplinestart,$nlines @@\n" msep
2647
2648     set events [lsort -integer -index 0 $events]
2649     set nevents [llength $events]
2650     set nmerge $nparents($diffmergeid)
2651     set l $grouplinestart
2652     for {set i 0} {$i < $nevents} {set i $j} {
2653         set nl [lindex $events $i 0]
2654         while {$l < $nl} {
2655             $ctext insert end " $filelines($id,$f,$l)\n"
2656             incr l
2657         }
2658         set e [lindex $events $i]
2659         set enl [lindex $e 1]
2660         set j $i
2661         set active {}
2662         while 1 {
2663             set pnum [lindex $e 2]
2664             set olc [lindex $e 3]
2665             set nlc [lindex $e 4]
2666             if {![info exists delta($pnum)]} {
2667                 set delta($pnum) [expr {$olc - $nlc}]
2668                 lappend active $pnum
2669             } else {
2670                 incr delta($pnum) [expr {$olc - $nlc}]
2671             }
2672             if {[incr j] >= $nevents} break
2673             set e [lindex $events $j]
2674             if {[lindex $e 0] >= $enl} break
2675             if {[lindex $e 1] > $enl} {
2676                 set enl [lindex $e 1]
2677             }
2678         }
2679         set nlc [expr {$enl - $l}]
2680         set ncol mresult
2681         set bestpn -1
2682         if {[llength $active] == $nmerge - 1} {
2683             # no diff for one of the parents, i.e. it's identical
2684             for {set pnum 0} {$pnum < $nmerge} {incr pnum} {
2685                 if {![info exists delta($pnum)]} {
2686                     if {$pnum < $mergemax} {
2687                         lappend ncol m$pnum
2688                     } else {
2689                         lappend ncol mmax
2690                     }
2691                     break
2692                 }
2693             }
2694         } elseif {[llength $active] == $nmerge} {
2695             # all parents are different, see if one is very similar
2696             set bestsim 30
2697             for {set pnum 0} {$pnum < $nmerge} {incr pnum} {
2698                 set sim [similarity $pnum $l $nlc $f \
2699                              [lrange $events $i [expr {$j-1}]]]
2700                 if {$sim > $bestsim} {
2701                     set bestsim $sim
2702                     set bestpn $pnum
2703                 }
2704             }
2705             if {$bestpn >= 0} {
2706                 lappend ncol m$bestpn
2707             }
2708         }
2709         set pnum -1
2710         foreach p $parents($id) {
2711             incr pnum
2712             if {![info exists delta($pnum)] || $pnum == $bestpn} continue
2713             set olc [expr {$nlc + $delta($pnum)}]
2714             set ol [expr {$l + $diffoffset($p)}]
2715             incr diffoffset($p) $delta($pnum)
2716             unset delta($pnum)
2717             for {} {$olc > 0} {incr olc -1} {
2718                 $ctext insert end "-$filelines($p,$f,$ol)\n" m$pnum
2719                 incr ol
2720             }
2721         }
2722         set endl [expr {$l + $nlc}]
2723         if {$bestpn >= 0} {
2724             # show this pretty much as a normal diff
2725             set p [lindex $parents($id) $bestpn]
2726             set ol [expr {$l + $diffoffset($p)}]
2727             incr diffoffset($p) $delta($bestpn)
2728             unset delta($bestpn)
2729             for {set k $i} {$k < $j} {incr k} {
2730                 set e [lindex $events $k]
2731                 if {[lindex $e 2] != $bestpn} continue
2732                 set nl [lindex $e 0]
2733                 set ol [expr {$ol + $nl - $l}]
2734                 for {} {$l < $nl} {incr l} {
2735                     $ctext insert end "+$filelines($id,$f,$l)\n" $ncol
2736                 }
2737                 set c [lindex $e 3]
2738                 for {} {$c > 0} {incr c -1} {
2739                     $ctext insert end "-$filelines($p,$f,$ol)\n" m$bestpn
2740                     incr ol
2741                 }
2742                 set nl [lindex $e 1]
2743                 for {} {$l < $nl} {incr l} {
2744                     $ctext insert end "+$filelines($id,$f,$l)\n" mresult
2745                 }
2746             }
2747         }
2748         for {} {$l < $endl} {incr l} {
2749             $ctext insert end "+$filelines($id,$f,$l)\n" $ncol
2750         }
2751     }
2752     while {$l < $grouplineend} {
2753         $ctext insert end " $filelines($id,$f,$l)\n"
2754         incr l
2755     }
2756     $ctext conf -state disabled
2757 }
2758
2759 proc similarity {pnum l nlc f events} {
2760     global diffmergeid parents diffoffset filelines
2761
2762     set id $diffmergeid
2763     set p [lindex $parents($id) $pnum]
2764     set ol [expr {$l + $diffoffset($p)}]
2765     set endl [expr {$l + $nlc}]
2766     set same 0
2767     set diff 0
2768     foreach e $events {
2769         if {[lindex $e 2] != $pnum} continue
2770         set nl [lindex $e 0]
2771         set ol [expr {$ol + $nl - $l}]
2772         for {} {$l < $nl} {incr l} {
2773             incr same [string length $filelines($id,$f,$l)]
2774             incr same
2775         }
2776         set oc [lindex $e 3]
2777         for {} {$oc > 0} {incr oc -1} {
2778             incr diff [string length $filelines($p,$f,$ol)]
2779             incr diff
2780             incr ol
2781         }
2782         set nl [lindex $e 1]
2783         for {} {$l < $nl} {incr l} {
2784             incr diff [string length $filelines($id,$f,$l)]
2785             incr diff
2786         }
2787     }
2788     for {} {$l < $endl} {incr l} {
2789         incr same [string length $filelines($id,$f,$l)]
2790         incr same
2791     }
2792     if {$same == 0} {
2793         return 0
2794     }
2795     return [expr {200 * $same / (2 * $same + $diff)}]
2796 }
2797
2798 proc startdiff {ids} {
2799     global treediffs diffids treepending diffmergeid
2800
2801     set diffids $ids
2802     catch {unset diffmergeid}
2803     if {![info exists treediffs($ids)]} {
2804         if {![info exists treepending]} {
2805             gettreediffs $ids
2806         }
2807     } else {
2808         addtocflist $ids
2809     }
2810 }
2811
2812 proc addtocflist {ids} {
2813     global treediffs cflist
2814     foreach f $treediffs($ids) {
2815         $cflist insert end $f
2816     }
2817     getblobdiffs $ids
2818 }
2819
2820 proc gettreediffs {ids} {
2821     global treediff parents treepending
2822     set treepending $ids
2823     set treediff {}
2824     if [catch {set gdtf [open [concat | git-diff-tree --no-commit-id -r $ids] r]}] return
2825     fconfigure $gdtf -blocking 0
2826     fileevent $gdtf readable [list gettreediffline $gdtf $ids]
2827 }
2828
2829 proc gettreediffline {gdtf ids} {
2830     global treediff treediffs treepending diffids diffmergeid
2831
2832     set n [gets $gdtf line]
2833     if {$n < 0} {
2834         if {![eof $gdtf]} return
2835         close $gdtf
2836         set treediffs($ids) $treediff
2837         unset treepending
2838         if {$ids != $diffids} {
2839             gettreediffs $diffids
2840         } else {
2841             if {[info exists diffmergeid]} {
2842                 contmergediff $ids
2843             } else {
2844                 addtocflist $ids
2845             }
2846         }
2847         return
2848     }
2849     set file [lindex $line 5]
2850     lappend treediff $file
2851 }
2852
2853 proc getblobdiffs {ids} {
2854     global diffopts blobdifffd diffids env curdifftag curtagstart
2855     global difffilestart nextupdate diffinhdr treediffs
2856
2857     set env(GIT_DIFF_OPTS) $diffopts
2858     set cmd [concat | git-diff-tree --no-commit-id -r -p -C $ids]
2859     if {[catch {set bdf [open $cmd r]} err]} {
2860         puts "error getting diffs: $err"
2861         return
2862     }
2863     set diffinhdr 0
2864     fconfigure $bdf -blocking 0
2865     set blobdifffd($ids) $bdf
2866     set curdifftag Comments
2867     set curtagstart 0.0
2868     catch {unset difffilestart}
2869     fileevent $bdf readable [list getblobdiffline $bdf $diffids]
2870     set nextupdate [expr {[clock clicks -milliseconds] + 100}]
2871 }
2872
2873 proc getblobdiffline {bdf ids} {
2874     global diffids blobdifffd ctext curdifftag curtagstart
2875     global diffnexthead diffnextnote difffilestart
2876     global nextupdate diffinhdr treediffs
2877
2878     set n [gets $bdf line]
2879     if {$n < 0} {
2880         if {[eof $bdf]} {
2881             close $bdf
2882             if {$ids == $diffids && $bdf == $blobdifffd($ids)} {
2883                 $ctext tag add $curdifftag $curtagstart end
2884             }
2885         }
2886         return
2887     }
2888     if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
2889         return
2890     }
2891     $ctext conf -state normal
2892     if {[regexp {^diff --git a/(.*) b/(.*)} $line match fname newname]} {
2893         # start of a new file
2894         $ctext insert end "\n"
2895         $ctext tag add $curdifftag $curtagstart end
2896         set curtagstart [$ctext index "end - 1c"]
2897         set header $newname
2898         set here [$ctext index "end - 1c"]
2899         set i [lsearch -exact $treediffs($diffids) $fname]
2900         if {$i >= 0} {
2901             set difffilestart($i) $here
2902             incr i
2903             $ctext mark set fmark.$i $here
2904             $ctext mark gravity fmark.$i left
2905         }
2906         if {$newname != $fname} {
2907             set i [lsearch -exact $treediffs($diffids) $newname]
2908             if {$i >= 0} {
2909                 set difffilestart($i) $here
2910                 incr i
2911                 $ctext mark set fmark.$i $here
2912                 $ctext mark gravity fmark.$i left
2913             }
2914         }
2915         set curdifftag "f:$fname"
2916         $ctext tag delete $curdifftag
2917         set l [expr {(78 - [string length $header]) / 2}]
2918         set pad [string range "----------------------------------------" 1 $l]
2919         $ctext insert end "$pad $header $pad\n" filesep
2920         set diffinhdr 1
2921     } elseif {[regexp {^(---|\+\+\+)} $line]} {
2922         set diffinhdr 0
2923     } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
2924                    $line match f1l f1c f2l f2c rest]} {
2925         $ctext insert end "$line\n" hunksep
2926         set diffinhdr 0
2927     } else {
2928         set x [string range $line 0 0]
2929         if {$x == "-" || $x == "+"} {
2930             set tag [expr {$x == "+"}]
2931             $ctext insert end "$line\n" d$tag
2932         } elseif {$x == " "} {
2933             $ctext insert end "$line\n"
2934         } elseif {$diffinhdr || $x == "\\"} {
2935             # e.g. "\ No newline at end of file"
2936             $ctext insert end "$line\n" filesep
2937         } else {
2938             # Something else we don't recognize
2939             if {$curdifftag != "Comments"} {
2940                 $ctext insert end "\n"
2941                 $ctext tag add $curdifftag $curtagstart end
2942                 set curtagstart [$ctext index "end - 1c"]
2943                 set curdifftag Comments
2944             }
2945             $ctext insert end "$line\n" filesep
2946         }
2947     }
2948     $ctext conf -state disabled
2949     if {[clock clicks -milliseconds] >= $nextupdate} {
2950         incr nextupdate 100
2951         fileevent $bdf readable {}
2952         update
2953         fileevent $bdf readable "getblobdiffline $bdf {$ids}"
2954     }
2955 }
2956
2957 proc nextfile {} {
2958     global difffilestart ctext
2959     set here [$ctext index @0,0]
2960     for {set i 0} {[info exists difffilestart($i)]} {incr i} {
2961         if {[$ctext compare $difffilestart($i) > $here]} {
2962             if {![info exists pos]
2963                 || [$ctext compare $difffilestart($i) < $pos]} {
2964                 set pos $difffilestart($i)
2965             }
2966         }
2967     }
2968     if {[info exists pos]} {
2969         $ctext yview $pos
2970     }
2971 }
2972
2973 proc listboxsel {} {
2974     global ctext cflist currentid
2975     if {![info exists currentid]} return
2976     set sel [lsort [$cflist curselection]]
2977     if {$sel eq {}} return
2978     set first [lindex $sel 0]
2979     catch {$ctext yview fmark.$first}
2980 }
2981
2982 proc setcoords {} {
2983     global linespc charspc canvx0 canvy0 mainfont
2984     global xspc1 xspc2 lthickness
2985
2986     set linespc [font metrics $mainfont -linespace]
2987     set charspc [font measure $mainfont "m"]
2988     set canvy0 [expr {3 + 0.5 * $linespc}]
2989     set canvx0 [expr {3 + 0.5 * $linespc}]
2990     set lthickness [expr {int($linespc / 9) + 1}]
2991     set xspc1(0) $linespc
2992     set xspc2 $linespc
2993 }
2994
2995 proc redisplay {} {
2996     global stopped redisplaying phase
2997     if {$stopped > 1} return
2998     if {$phase == "getcommits"} return
2999     set redisplaying 1
3000     if {$phase == "drawgraph" || $phase == "incrdraw"} {
3001         set stopped 1
3002     } else {
3003         drawgraph
3004     }
3005 }
3006
3007 proc incrfont {inc} {
3008     global mainfont namefont textfont ctext canv phase
3009     global stopped entries
3010     unmarkmatches
3011     set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
3012     set namefont [lreplace $namefont 1 1 [expr {[lindex $namefont 1] + $inc}]]
3013     set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
3014     setcoords
3015     $ctext conf -font $textfont
3016     $ctext tag conf filesep -font [concat $textfont bold]
3017     foreach e $entries {
3018         $e conf -font $mainfont
3019     }
3020     if {$phase == "getcommits"} {
3021         $canv itemconf textitems -font $mainfont
3022     }
3023     redisplay
3024 }
3025
3026 proc clearsha1 {} {
3027     global sha1entry sha1string
3028     if {[string length $sha1string] == 40} {
3029         $sha1entry delete 0 end
3030     }
3031 }
3032
3033 proc sha1change {n1 n2 op} {
3034     global sha1string currentid sha1but
3035     if {$sha1string == {}
3036         || ([info exists currentid] && $sha1string == $currentid)} {
3037         set state disabled
3038     } else {
3039         set state normal
3040     }
3041     if {[$sha1but cget -state] == $state} return
3042     if {$state == "normal"} {
3043         $sha1but conf -state normal -relief raised -text "Goto: "
3044     } else {
3045         $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
3046     }
3047 }
3048
3049 proc gotocommit {} {
3050     global sha1string currentid idline tagids
3051     global lineid numcommits
3052
3053     if {$sha1string == {}
3054         || ([info exists currentid] && $sha1string == $currentid)} return
3055     if {[info exists tagids($sha1string)]} {
3056         set id $tagids($sha1string)
3057     } else {
3058         set id [string tolower $sha1string]
3059         if {[regexp {^[0-9a-f]{4,39}$} $id]} {
3060             set matches {}
3061             for {set l 0} {$l < $numcommits} {incr l} {
3062                 if {[string match $id* $lineid($l)]} {
3063                     lappend matches $lineid($l)
3064                 }
3065             }
3066             if {$matches ne {}} {
3067                 if {[llength $matches] > 1} {
3068                     error_popup "Short SHA1 id $id is ambiguous"
3069                     return
3070                 }
3071                 set id [lindex $matches 0]
3072             }
3073         }
3074     }
3075     if {[info exists idline($id)]} {
3076         selectline $idline($id) 1
3077         return
3078     }
3079     if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
3080         set type "SHA1 id"
3081     } else {
3082         set type "Tag"
3083     }
3084     error_popup "$type $sha1string is not known"
3085 }
3086
3087 proc lineenter {x y id} {
3088     global hoverx hovery hoverid hovertimer
3089     global commitinfo canv
3090
3091     if {![info exists commitinfo($id)]} return
3092     set hoverx $x
3093     set hovery $y
3094     set hoverid $id
3095     if {[info exists hovertimer]} {
3096         after cancel $hovertimer
3097     }
3098     set hovertimer [after 500 linehover]
3099     $canv delete hover
3100 }
3101
3102 proc linemotion {x y id} {
3103     global hoverx hovery hoverid hovertimer
3104
3105     if {[info exists hoverid] && $id == $hoverid} {
3106         set hoverx $x
3107         set hovery $y
3108         if {[info exists hovertimer]} {
3109             after cancel $hovertimer
3110         }
3111         set hovertimer [after 500 linehover]
3112     }
3113 }
3114
3115 proc lineleave {id} {
3116     global hoverid hovertimer canv
3117
3118     if {[info exists hoverid] && $id == $hoverid} {
3119         $canv delete hover
3120         if {[info exists hovertimer]} {
3121             after cancel $hovertimer
3122             unset hovertimer
3123         }
3124         unset hoverid
3125     }
3126 }
3127
3128 proc linehover {} {
3129     global hoverx hovery hoverid hovertimer
3130     global canv linespc lthickness
3131     global commitinfo mainfont
3132
3133     set text [lindex $commitinfo($hoverid) 0]
3134     set ymax [lindex [$canv cget -scrollregion] 3]
3135     if {$ymax == {}} return
3136     set yfrac [lindex [$canv yview] 0]
3137     set x [expr {$hoverx + 2 * $linespc}]
3138     set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
3139     set x0 [expr {$x - 2 * $lthickness}]
3140     set y0 [expr {$y - 2 * $lthickness}]
3141     set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}]
3142     set y1 [expr {$y + $linespc + 2 * $lthickness}]
3143     set t [$canv create rectangle $x0 $y0 $x1 $y1 \
3144                -fill \#ffff80 -outline black -width 1 -tags hover]
3145     $canv raise $t
3146     set t [$canv create text $x $y -anchor nw -text $text -tags hover -font $mainfont]
3147     $canv raise $t
3148 }
3149
3150 proc clickisonarrow {id y} {
3151     global mainline mainlinearrow sidelines lthickness
3152
3153     set thresh [expr {2 * $lthickness + 6}]
3154     if {[info exists mainline($id)]} {
3155         if {$mainlinearrow($id) ne "none"} {
3156             if {abs([lindex $mainline($id) 1] - $y) < $thresh} {
3157                 return "up"
3158             }
3159         }
3160     }
3161     if {[info exists sidelines($id)]} {
3162         foreach ls $sidelines($id) {
3163             set coords [lindex $ls 0]
3164             set arrow [lindex $ls 2]
3165             if {$arrow eq "first" || $arrow eq "both"} {
3166                 if {abs([lindex $coords 1] - $y) < $thresh} {
3167                     return "up"
3168                 }
3169             }
3170             if {$arrow eq "last" || $arrow eq "both"} {
3171                 if {abs([lindex $coords end] - $y) < $thresh} {
3172                     return "down"
3173                 }
3174             }
3175         }
3176     }
3177     return {}
3178 }
3179
3180 proc arrowjump {id dirn y} {
3181     global mainline sidelines canv canv2 canv3
3182
3183     set yt {}
3184     if {$dirn eq "down"} {
3185         if {[info exists mainline($id)]} {
3186             set y1 [lindex $mainline($id) 1]
3187             if {$y1 > $y} {
3188                 set yt $y1
3189             }
3190         }
3191         if {[info exists sidelines($id)]} {
3192             foreach ls $sidelines($id) {
3193                 set y1 [lindex $ls 0 1]
3194                 if {$y1 > $y && ($yt eq {} || $y1 < $yt)} {
3195                     set yt $y1
3196                 }
3197             }
3198         }
3199     } else {
3200         if {[info exists sidelines($id)]} {
3201             foreach ls $sidelines($id) {
3202                 set y1 [lindex $ls 0 end]
3203                 if {$y1 < $y && ($yt eq {} || $y1 > $yt)} {
3204                     set yt $y1
3205                 }
3206             }
3207         }
3208     }
3209     if {$yt eq {}} return
3210     set ymax [lindex [$canv cget -scrollregion] 3]
3211     if {$ymax eq {} || $ymax <= 0} return
3212     set view [$canv yview]
3213     set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
3214     set yfrac [expr {$yt / $ymax - $yspan / 2}]
3215     if {$yfrac < 0} {
3216         set yfrac 0
3217     }
3218     $canv yview moveto $yfrac
3219     $canv2 yview moveto $yfrac
3220     $canv3 yview moveto $yfrac
3221 }
3222
3223 proc lineclick {x y id isnew} {
3224     global ctext commitinfo children cflist canv thickerline
3225
3226     unmarkmatches
3227     unselectline
3228     normalline
3229     $canv delete hover
3230     # draw this line thicker than normal
3231     drawlines $id 1 1
3232     set thickerline $id
3233     if {$isnew} {
3234         set ymax [lindex [$canv cget -scrollregion] 3]
3235         if {$ymax eq {}} return
3236         set yfrac [lindex [$canv yview] 0]
3237         set y [expr {$y + $yfrac * $ymax}]
3238     }
3239     set dirn [clickisonarrow $id $y]
3240     if {$dirn ne {}} {
3241         arrowjump $id $dirn $y
3242         return
3243     }
3244
3245     if {$isnew} {
3246         addtohistory [list lineclick $x $y $id 0]
3247     }
3248     # fill the details pane with info about this line
3249     $ctext conf -state normal
3250     $ctext delete 0.0 end
3251     $ctext tag conf link -foreground blue -underline 1
3252     $ctext tag bind link <Enter> { %W configure -cursor hand2 }
3253     $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
3254     $ctext insert end "Parent:\t"
3255     $ctext insert end $id [list link link0]
3256     $ctext tag bind link0 <1> [list selbyid $id]
3257     set info $commitinfo($id)
3258     $ctext insert end "\n\t[lindex $info 0]\n"
3259     $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
3260     set date [formatdate [lindex $info 2]]
3261     $ctext insert end "\tDate:\t$date\n"
3262     if {[info exists children($id)]} {
3263         $ctext insert end "\nChildren:"
3264         set i 0
3265         foreach child $children($id) {
3266             incr i
3267             set info $commitinfo($child)
3268             $ctext insert end "\n\t"
3269             $ctext insert end $child [list link link$i]
3270             $ctext tag bind link$i <1> [list selbyid $child]
3271             $ctext insert end "\n\t[lindex $info 0]"
3272             $ctext insert end "\n\tAuthor:\t[lindex $info 1]"
3273             set date [formatdate [lindex $info 2]]
3274             $ctext insert end "\n\tDate:\t$date\n"
3275         }
3276     }
3277     $ctext conf -state disabled
3278
3279     $cflist delete 0 end
3280 }
3281
3282 proc normalline {} {
3283     global thickerline
3284     if {[info exists thickerline]} {
3285         drawlines $thickerline 0 1
3286         unset thickerline
3287     }
3288 }
3289
3290 proc selbyid {id} {
3291     global idline
3292     if {[info exists idline($id)]} {
3293         selectline $idline($id) 1
3294     }
3295 }
3296
3297 proc mstime {} {
3298     global startmstime
3299     if {![info exists startmstime]} {
3300         set startmstime [clock clicks -milliseconds]
3301     }
3302     return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
3303 }
3304
3305 proc rowmenu {x y id} {
3306     global rowctxmenu idline selectedline rowmenuid
3307
3308     if {![info exists selectedline] || $idline($id) eq $selectedline} {
3309         set state disabled
3310     } else {
3311         set state normal
3312     }
3313     $rowctxmenu entryconfigure 0 -state $state
3314     $rowctxmenu entryconfigure 1 -state $state
3315     $rowctxmenu entryconfigure 2 -state $state
3316     set rowmenuid $id
3317     tk_popup $rowctxmenu $x $y
3318 }
3319
3320 proc diffvssel {dirn} {
3321     global rowmenuid selectedline lineid
3322
3323     if {![info exists selectedline]} return
3324     if {$dirn} {
3325         set oldid $lineid($selectedline)
3326         set newid $rowmenuid
3327     } else {
3328         set oldid $rowmenuid
3329         set newid $lineid($selectedline)
3330     }
3331     addtohistory [list doseldiff $oldid $newid]
3332     doseldiff $oldid $newid
3333 }
3334
3335 proc doseldiff {oldid newid} {
3336     global ctext cflist
3337     global commitinfo
3338
3339     $ctext conf -state normal
3340     $ctext delete 0.0 end
3341     $ctext mark set fmark.0 0.0
3342     $ctext mark gravity fmark.0 left
3343     $cflist delete 0 end
3344     $cflist insert end "Top"
3345     $ctext insert end "From "
3346     $ctext tag conf link -foreground blue -underline 1
3347     $ctext tag bind link <Enter> { %W configure -cursor hand2 }
3348     $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
3349     $ctext tag bind link0 <1> [list selbyid $oldid]
3350     $ctext insert end $oldid [list link link0]
3351     $ctext insert end "\n     "
3352     $ctext insert end [lindex $commitinfo($oldid) 0]
3353     $ctext insert end "\n\nTo   "
3354     $ctext tag bind link1 <1> [list selbyid $newid]
3355     $ctext insert end $newid [list link link1]
3356     $ctext insert end "\n     "
3357     $ctext insert end [lindex $commitinfo($newid) 0]
3358     $ctext insert end "\n"
3359     $ctext conf -state disabled
3360     $ctext tag delete Comments
3361     $ctext tag remove found 1.0 end
3362     startdiff [list $oldid $newid]
3363 }
3364
3365 proc mkpatch {} {
3366     global rowmenuid currentid commitinfo patchtop patchnum
3367
3368     if {![info exists currentid]} return
3369     set oldid $currentid
3370     set oldhead [lindex $commitinfo($oldid) 0]
3371     set newid $rowmenuid
3372     set newhead [lindex $commitinfo($newid) 0]
3373     set top .patch
3374     set patchtop $top
3375     catch {destroy $top}
3376     toplevel $top
3377     label $top.title -text "Generate patch"
3378     grid $top.title - -pady 10
3379     label $top.from -text "From:"
3380     entry $top.fromsha1 -width 40 -relief flat
3381     $top.fromsha1 insert 0 $oldid
3382     $top.fromsha1 conf -state readonly
3383     grid $top.from $top.fromsha1 -sticky w
3384     entry $top.fromhead -width 60 -relief flat
3385     $top.fromhead insert 0 $oldhead
3386     $top.fromhead conf -state readonly
3387     grid x $top.fromhead -sticky w
3388     label $top.to -text "To:"
3389     entry $top.tosha1 -width 40 -relief flat
3390     $top.tosha1 insert 0 $newid
3391     $top.tosha1 conf -state readonly
3392     grid $top.to $top.tosha1 -sticky w
3393     entry $top.tohead -width 60 -relief flat
3394     $top.tohead insert 0 $newhead
3395     $top.tohead conf -state readonly
3396     grid x $top.tohead -sticky w
3397     button $top.rev -text "Reverse" -command mkpatchrev -padx 5
3398     grid $top.rev x -pady 10
3399     label $top.flab -text "Output file:"
3400     entry $top.fname -width 60
3401     $top.fname insert 0 [file normalize "patch$patchnum.patch"]
3402     incr patchnum
3403     grid $top.flab $top.fname -sticky w
3404     frame $top.buts
3405     button $top.buts.gen -text "Generate" -command mkpatchgo
3406     button $top.buts.can -text "Cancel" -command mkpatchcan
3407     grid $top.buts.gen $top.buts.can
3408     grid columnconfigure $top.buts 0 -weight 1 -uniform a
3409     grid columnconfigure $top.buts 1 -weight 1 -uniform a
3410     grid $top.buts - -pady 10 -sticky ew
3411     focus $top.fname
3412 }
3413
3414 proc mkpatchrev {} {
3415     global patchtop
3416
3417     set oldid [$patchtop.fromsha1 get]
3418     set oldhead [$patchtop.fromhead get]
3419     set newid [$patchtop.tosha1 get]
3420     set newhead [$patchtop.tohead get]
3421     foreach e [list fromsha1 fromhead tosha1 tohead] \
3422             v [list $newid $newhead $oldid $oldhead] {
3423         $patchtop.$e conf -state normal
3424         $patchtop.$e delete 0 end
3425         $patchtop.$e insert 0 $v
3426         $patchtop.$e conf -state readonly
3427     }
3428 }
3429
3430 proc mkpatchgo {} {
3431     global patchtop
3432
3433     set oldid [$patchtop.fromsha1 get]
3434     set newid [$patchtop.tosha1 get]
3435     set fname [$patchtop.fname get]
3436     if {[catch {exec git-diff-tree -p $oldid $newid >$fname &} err]} {
3437         error_popup "Error creating patch: $err"
3438     }
3439     catch {destroy $patchtop}
3440     unset patchtop
3441 }
3442
3443 proc mkpatchcan {} {
3444     global patchtop
3445
3446     catch {destroy $patchtop}
3447     unset patchtop
3448 }
3449
3450 proc mktag {} {
3451     global rowmenuid mktagtop commitinfo
3452
3453     set top .maketag
3454     set mktagtop $top
3455     catch {destroy $top}
3456     toplevel $top
3457     label $top.title -text "Create tag"
3458     grid $top.title - -pady 10
3459     label $top.id -text "ID:"
3460     entry $top.sha1 -width 40 -relief flat
3461     $top.sha1 insert 0 $rowmenuid
3462     $top.sha1 conf -state readonly
3463     grid $top.id $top.sha1 -sticky w
3464     entry $top.head -width 60 -relief flat
3465     $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
3466     $top.head conf -state readonly
3467     grid x $top.head -sticky w
3468     label $top.tlab -text "Tag name:"
3469     entry $top.tag -width 60
3470     grid $top.tlab $top.tag -sticky w
3471     frame $top.buts
3472     button $top.buts.gen -text "Create" -command mktaggo
3473     button $top.buts.can -text "Cancel" -command mktagcan
3474     grid $top.buts.gen $top.buts.can
3475     grid columnconfigure $top.buts 0 -weight 1 -uniform a
3476     grid columnconfigure $top.buts 1 -weight 1 -uniform a
3477     grid $top.buts - -pady 10 -sticky ew
3478     focus $top.tag
3479 }
3480
3481 proc domktag {} {
3482     global mktagtop env tagids idtags
3483
3484     set id [$mktagtop.sha1 get]
3485     set tag [$mktagtop.tag get]
3486     if {$tag == {}} {
3487         error_popup "No tag name specified"
3488         return
3489     }
3490     if {[info exists tagids($tag)]} {
3491         error_popup "Tag \"$tag\" already exists"
3492         return
3493     }
3494     if {[catch {
3495         set dir [gitdir]
3496         set fname [file join $dir "refs/tags" $tag]
3497         set f [open $fname w]
3498         puts $f $id
3499         close $f
3500     } err]} {
3501         error_popup "Error creating tag: $err"
3502         return
3503     }
3504
3505     set tagids($tag) $id
3506     lappend idtags($id) $tag
3507     redrawtags $id
3508 }
3509
3510 proc redrawtags {id} {
3511     global canv linehtag idline idpos selectedline
3512
3513     if {![info exists idline($id)]} return
3514     $canv delete tag.$id
3515     set xt [eval drawtags $id $idpos($id)]
3516     $canv coords $linehtag($idline($id)) $xt [lindex $idpos($id) 2]
3517     if {[info exists selectedline] && $selectedline == $idline($id)} {
3518         selectline $selectedline 0
3519     }
3520 }
3521
3522 proc mktagcan {} {
3523     global mktagtop
3524
3525     catch {destroy $mktagtop}
3526     unset mktagtop
3527 }
3528
3529 proc mktaggo {} {
3530     domktag
3531     mktagcan
3532 }
3533
3534 proc writecommit {} {
3535     global rowmenuid wrcomtop commitinfo wrcomcmd
3536
3537     set top .writecommit
3538     set wrcomtop $top
3539     catch {destroy $top}
3540     toplevel $top
3541     label $top.title -text "Write commit to file"
3542     grid $top.title - -pady 10
3543     label $top.id -text "ID:"
3544     entry $top.sha1 -width 40 -relief flat
3545     $top.sha1 insert 0 $rowmenuid
3546     $top.sha1 conf -state readonly
3547     grid $top.id $top.sha1 -sticky w
3548     entry $top.head -width 60 -relief flat
3549     $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
3550     $top.head conf -state readonly
3551     grid x $top.head -sticky w
3552     label $top.clab -text "Command:"
3553     entry $top.cmd -width 60 -textvariable wrcomcmd
3554     grid $top.clab $top.cmd -sticky w -pady 10
3555     label $top.flab -text "Output file:"
3556     entry $top.fname -width 60
3557     $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
3558     grid $top.flab $top.fname -sticky w
3559     frame $top.buts
3560     button $top.buts.gen -text "Write" -command wrcomgo
3561     button $top.buts.can -text "Cancel" -command wrcomcan
3562     grid $top.buts.gen $top.buts.can
3563     grid columnconfigure $top.buts 0 -weight 1 -uniform a
3564     grid columnconfigure $top.buts 1 -weight 1 -uniform a
3565     grid $top.buts - -pady 10 -sticky ew
3566     focus $top.fname
3567 }
3568
3569 proc wrcomgo {} {
3570     global wrcomtop
3571
3572     set id [$wrcomtop.sha1 get]
3573     set cmd "echo $id | [$wrcomtop.cmd get]"
3574     set fname [$wrcomtop.fname get]
3575     if {[catch {exec sh -c $cmd >$fname &} err]} {
3576         error_popup "Error writing commit: $err"
3577     }
3578     catch {destroy $wrcomtop}
3579     unset wrcomtop
3580 }
3581
3582 proc wrcomcan {} {
3583     global wrcomtop
3584
3585     catch {destroy $wrcomtop}
3586     unset wrcomtop
3587 }
3588
3589 proc listrefs {id} {
3590     global idtags idheads idotherrefs
3591
3592     set x {}
3593     if {[info exists idtags($id)]} {
3594         set x $idtags($id)
3595     }
3596     set y {}
3597     if {[info exists idheads($id)]} {
3598         set y $idheads($id)
3599     }
3600     set z {}
3601     if {[info exists idotherrefs($id)]} {
3602         set z $idotherrefs($id)
3603     }
3604     return [list $x $y $z]
3605 }
3606
3607 proc rereadrefs {} {
3608     global idtags idheads idotherrefs
3609     global tagids headids otherrefids
3610
3611     set refids [concat [array names idtags] \
3612                     [array names idheads] [array names idotherrefs]]
3613     foreach id $refids {
3614         if {![info exists ref($id)]} {
3615             set ref($id) [listrefs $id]
3616         }
3617     }
3618     readrefs
3619     set refids [lsort -unique [concat $refids [array names idtags] \
3620                         [array names idheads] [array names idotherrefs]]]
3621     foreach id $refids {
3622         set v [listrefs $id]
3623         if {![info exists ref($id)] || $ref($id) != $v} {
3624             redrawtags $id
3625         }
3626     }
3627 }
3628
3629 proc updatecommits {rargs} {
3630     global commitlisted commfd phase
3631     global startmsecs nextupdate ncmupdate
3632     global idtags idheads idotherrefs
3633     global leftover
3634     global parsed_args
3635     global canv
3636     global oldcommits commits
3637     global parents nchildren children ncleft
3638
3639     set old_args $parsed_args
3640     parse_args $rargs
3641
3642     foreach id $old_args {
3643         if {![regexp {^[0-9a-f]{40}$} $id]} continue
3644         if {[info exists oldref($id)]} continue
3645         set oldref($id) $id
3646         lappend ignoreold "^$id"
3647     }
3648     foreach id $parsed_args {
3649         if {![regexp {^[0-9a-f]{40}$} $id]} continue
3650         if {[info exists ref($id)]} continue
3651         set ref($id) $id
3652         lappend ignorenew "^$id"
3653     }
3654
3655     foreach a $old_args {
3656         if {![info exists ref($a)]} {
3657             lappend ignorenew $a
3658         }
3659     }
3660
3661     set phase updatecommits
3662     set removed_commits [split [eval exec git-rev-list $ignorenew] "\n" ]
3663     if {[llength $removed_commits] > 0} {
3664         $canv delete all
3665         set oldcommits {}
3666         foreach c $commits {
3667             if {[lsearch $c $removed_commits] < 0} {
3668                 lappend oldcommits $c
3669             } else {
3670                 unset commitlisted($c)
3671             }
3672         }
3673         set commits {}
3674         unset children
3675         unset nchildren
3676         set phase removecommits
3677     }
3678
3679     set args {}
3680     foreach a $parsed_args {
3681         if {![info exists oldref($a)]} {
3682             lappend args $a
3683         }
3684     }
3685
3686     readrefs
3687     if [catch {
3688         set commfd [open "|git-rev-list --header --topo-order --parents $ignoreold $args" r]
3689     } err] {
3690         puts stderr "Error executing git-rev-list: $err"
3691         exit 1
3692     }
3693     set startmsecs [clock clicks -milliseconds]
3694     set nextupdate [expr $startmsecs + 100]
3695     set ncmupdate 1
3696     set leftover {}
3697     fconfigure $commfd -blocking 0 -translation lf
3698     fileevent $commfd readable [list getcommitlines $commfd]
3699     . config -cursor watch
3700     settextcursor watch
3701 }
3702
3703 proc showtag {tag isnew} {
3704     global ctext cflist tagcontents tagids linknum
3705
3706     if {$isnew} {
3707         addtohistory [list showtag $tag 0]
3708     }
3709     $ctext conf -state normal
3710     $ctext delete 0.0 end
3711     set linknum 0
3712     if {[info exists tagcontents($tag)]} {
3713         set text $tagcontents($tag)
3714     } else {
3715         set text "Tag: $tag\nId:  $tagids($tag)"
3716     }
3717     appendwithlinks $text
3718     $ctext conf -state disabled
3719     $cflist delete 0 end
3720 }
3721
3722 proc doquit {} {
3723     global stopped
3724     set stopped 100
3725     destroy .
3726 }
3727
3728 proc doprefs {} {
3729     global maxwidth maxgraphpct diffopts findmergefiles
3730     global oldprefs prefstop
3731
3732     set top .gitkprefs
3733     set prefstop $top
3734     if {[winfo exists $top]} {
3735         raise $top
3736         return
3737     }
3738     foreach v {maxwidth maxgraphpct diffopts findmergefiles} {
3739         set oldprefs($v) [set $v]
3740     }
3741     toplevel $top
3742     wm title $top "Gitk preferences"
3743     label $top.ldisp -text "Commit list display options"
3744     grid $top.ldisp - -sticky w -pady 10
3745     label $top.spacer -text " "
3746     label $top.maxwidthl -text "Maximum graph width (lines)" \
3747         -font optionfont
3748     spinbox $top.maxwidth -from 0 -to 100 -width 4 -textvariable maxwidth
3749     grid $top.spacer $top.maxwidthl $top.maxwidth -sticky w
3750     label $top.maxpctl -text "Maximum graph width (% of pane)" \
3751         -font optionfont
3752     spinbox $top.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct
3753     grid x $top.maxpctl $top.maxpct -sticky w
3754     checkbutton $top.findm -variable findmergefiles
3755     label $top.findml -text "Include merges for \"Find\" in \"Files\"" \
3756         -font optionfont
3757     grid $top.findm $top.findml - -sticky w
3758     label $top.ddisp -text "Diff display options"
3759     grid $top.ddisp - -sticky w -pady 10
3760     label $top.diffoptl -text "Options for diff program" \
3761         -font optionfont
3762     entry $top.diffopt -width 20 -textvariable diffopts
3763     grid x $top.diffoptl $top.diffopt -sticky w
3764     frame $top.buts
3765     button $top.buts.ok -text "OK" -command prefsok
3766     button $top.buts.can -text "Cancel" -command prefscan
3767     grid $top.buts.ok $top.buts.can
3768     grid columnconfigure $top.buts 0 -weight 1 -uniform a
3769     grid columnconfigure $top.buts 1 -weight 1 -uniform a
3770     grid $top.buts - - -pady 10 -sticky ew
3771 }
3772
3773 proc prefscan {} {
3774     global maxwidth maxgraphpct diffopts findmergefiles
3775     global oldprefs prefstop
3776
3777     foreach v {maxwidth maxgraphpct diffopts findmergefiles} {
3778         set $v $oldprefs($v)
3779     }
3780     catch {destroy $prefstop}
3781     unset prefstop
3782 }
3783
3784 proc prefsok {} {
3785     global maxwidth maxgraphpct
3786     global oldprefs prefstop
3787
3788     catch {destroy $prefstop}
3789     unset prefstop
3790     if {$maxwidth != $oldprefs(maxwidth)
3791         || $maxgraphpct != $oldprefs(maxgraphpct)} {
3792         redisplay
3793     }
3794 }
3795
3796 proc formatdate {d} {
3797     return [clock format $d -format "%Y-%m-%d %H:%M:%S"]
3798 }
3799
3800 # defaults...
3801 set datemode 0
3802 set diffopts "-U 5 -p"
3803 set wrcomcmd "git-diff-tree --stdin -p --pretty"
3804
3805 set gitencoding ""
3806 catch {
3807     set gitencoding [exec git-repo-config --get i18n.commitencoding]
3808 }
3809 if {$gitencoding == ""} {
3810         set gitencoding "utf-8"
3811 }
3812
3813 set mainfont {Helvetica 9}
3814 set textfont {Courier 9}
3815 set findmergefiles 0
3816 set maxgraphpct 50
3817 set maxwidth 16
3818 set revlistorder 0
3819 set fastdate 0
3820
3821 set colors {green red blue magenta darkgrey brown orange}
3822
3823 catch {source ~/.gitk}
3824
3825 set namefont $mainfont
3826
3827 font create optionfont -family sans-serif -size -12
3828
3829 set revtreeargs {}
3830 foreach arg $argv {
3831     switch -regexp -- $arg {
3832         "^$" { }
3833         "^-d" { set datemode 1 }
3834         "^-r" { set revlistorder 1 }
3835         default {
3836             lappend revtreeargs $arg
3837         }
3838     }
3839 }
3840
3841 set history {}
3842 set historyindex 0
3843
3844 set stopped 0
3845 set redisplaying 0
3846 set stuffsaved 0
3847 set patchnum 0
3848 setcoords
3849 makewindow $revtreeargs
3850 readrefs
3851 getcommits $revtreeargs