Add a menu entry for generating a patch between any two commits.
[git.git] / gitk
1 #!/bin/sh
2 # Tcl ignores the next line -*- tcl -*- \
3 exec wish "$0" -- "${1+$@}"
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 getcommits {rargs} {
11     global commits commfd phase canv mainfont env
12     global startmsecs nextupdate
13     global ctext maincursor textcursor leftover
14
15     # check that we can find a .git directory somewhere...
16     if {[info exists env(GIT_DIR)]} {
17         set gitdir $env(GIT_DIR)
18     } else {
19         set gitdir ".git"
20     }
21     if {![file isdirectory $gitdir]} {
22         error_popup "Cannot find the git directory \"$gitdir\"."
23         exit 1
24     }
25     set commits {}
26     set phase getcommits
27     set startmsecs [clock clicks -milliseconds]
28     set nextupdate [expr $startmsecs + 100]
29     if [catch {
30         set parse_args [concat --default HEAD $rargs]
31         set parsed_args [split [eval exec git-rev-parse $parse_args] "\n"]
32     }] {
33         # if git-rev-parse failed for some reason...
34         if {$rargs == {}} {
35             set rargs HEAD
36         }
37         set parsed_args $rargs
38     }
39     if [catch {
40         set commfd [open "|git-rev-list --header --merge-order $parsed_args" r]
41     } err] {
42         puts stderr "Error executing git-rev-list: $err"
43         exit 1
44     }
45     set leftover {}
46     fconfigure $commfd -blocking 0 -translation binary
47     fileevent $commfd readable "getcommitlines $commfd"
48     $canv delete all
49     $canv create text 3 3 -anchor nw -text "Reading commits..." \
50         -font $mainfont -tags textitems
51     . config -cursor watch
52     $ctext config -cursor watch
53 }
54
55 proc getcommitlines {commfd}  {
56     global commits parents cdate children nchildren
57     global commitlisted phase commitinfo nextupdate
58     global stopped redisplaying leftover
59
60     set stuff [read $commfd]
61     if {$stuff == {}} {
62         if {![eof $commfd]} return
63         # this works around what is apparently a bug in Tcl...
64         fconfigure $commfd -blocking 1
65         if {![catch {close $commfd} err]} {
66             after idle finishcommits
67             return
68         }
69         if {[string range $err 0 4] == "usage"} {
70             set err \
71 {Gitk: error reading commits: bad arguments to git-rev-list.
72 (Note: arguments to gitk are passed to git-rev-list
73 to allow selection of commits to be displayed.)}
74         } else {
75             set err "Error reading commits: $err"
76         }
77         error_popup $err
78         exit 1
79     }
80     set start 0
81     while 1 {
82         set i [string first "\0" $stuff $start]
83         if {$i < 0} {
84             set leftover [string range $stuff $start end]
85             return
86         }
87         set cmit [string range $stuff $start [expr {$i - 1}]]
88         if {$start == 0} {
89             set cmit "$leftover$cmit"
90         }
91         set start [expr {$i + 1}]
92         if {![regexp {^([0-9a-f]{40})\n} $cmit match id]} {
93             error_popup "Can't parse git-rev-list output: {$cmit}"
94             exit 1
95         }
96         set cmit [string range $cmit 41 end]
97         lappend commits $id
98         set commitlisted($id) 1
99         parsecommit $id $cmit 1
100         drawcommit $id
101         if {[clock clicks -milliseconds] >= $nextupdate} {
102             doupdate
103         }
104         while {$redisplaying} {
105             set redisplaying 0
106             if {$stopped == 1} {
107                 set stopped 0
108                 set phase "getcommits"
109                 foreach id $commits {
110                     drawcommit $id
111                     if {$stopped} break
112                     if {[clock clicks -milliseconds] >= $nextupdate} {
113                         doupdate
114                     }
115                 }
116             }
117         }
118     }
119 }
120
121 proc doupdate {} {
122     global commfd nextupdate
123
124     incr nextupdate 100
125     fileevent $commfd readable {}
126     update
127     fileevent $commfd readable "getcommitlines $commfd"
128 }
129
130 proc readcommit {id} {
131     if [catch {set contents [exec git-cat-file commit $id]}] return
132     parsecommit $id $contents 0
133 }
134
135 proc parsecommit {id contents listed} {
136     global commitinfo children nchildren parents nparents cdate ncleft
137
138     set inhdr 1
139     set comment {}
140     set headline {}
141     set auname {}
142     set audate {}
143     set comname {}
144     set comdate {}
145     if {![info exists nchildren($id)]} {
146         set children($id) {}
147         set nchildren($id) 0
148         set ncleft($id) 0
149     }
150     set parents($id) {}
151     set nparents($id) 0
152     foreach line [split $contents "\n"] {
153         if {$inhdr} {
154             if {$line == {}} {
155                 set inhdr 0
156             } else {
157                 set tag [lindex $line 0]
158                 if {$tag == "parent"} {
159                     set p [lindex $line 1]
160                     if {![info exists nchildren($p)]} {
161                         set children($p) {}
162                         set nchildren($p) 0
163                         set ncleft($p) 0
164                     }
165                     lappend parents($id) $p
166                     incr nparents($id)
167                     # sometimes we get a commit that lists a parent twice...
168                     if {$listed && [lsearch -exact $children($p) $id] < 0} {
169                         lappend children($p) $id
170                         incr nchildren($p)
171                         incr ncleft($p)
172                     }
173                 } elseif {$tag == "author"} {
174                     set x [expr {[llength $line] - 2}]
175                     set audate [lindex $line $x]
176                     set auname [lrange $line 1 [expr {$x - 1}]]
177                 } elseif {$tag == "committer"} {
178                     set x [expr {[llength $line] - 2}]
179                     set comdate [lindex $line $x]
180                     set comname [lrange $line 1 [expr {$x - 1}]]
181                 }
182             }
183         } else {
184             if {$comment == {}} {
185                 set headline [string trim $line]
186             } else {
187                 append comment "\n"
188             }
189             if {!$listed} {
190                 # git-rev-list indents the comment by 4 spaces;
191                 # if we got this via git-cat-file, add the indentation
192                 append comment "    "
193             }
194             append comment $line
195         }
196     }
197     if {$audate != {}} {
198         set audate [clock format $audate -format "%Y-%m-%d %H:%M:%S"]
199     }
200     if {$comdate != {}} {
201         set cdate($id) $comdate
202         set comdate [clock format $comdate -format "%Y-%m-%d %H:%M:%S"]
203     }
204     set commitinfo($id) [list $headline $auname $audate \
205                              $comname $comdate $comment]
206 }
207
208 proc readrefs {} {
209     global tagids idtags headids idheads
210     set tags [glob -nocomplain -types f .git/refs/tags/*]
211     foreach f $tags {
212         catch {
213             set fd [open $f r]
214             set line [read $fd]
215             if {[regexp {^[0-9a-f]{40}} $line id]} {
216                 set direct [file tail $f]
217                 set tagids($direct) $id
218                 lappend idtags($id) $direct
219                 set contents [split [exec git-cat-file tag $id] "\n"]
220                 set obj {}
221                 set type {}
222                 set tag {}
223                 foreach l $contents {
224                     if {$l == {}} break
225                     switch -- [lindex $l 0] {
226                         "object" {set obj [lindex $l 1]}
227                         "type" {set type [lindex $l 1]}
228                         "tag" {set tag [string range $l 4 end]}
229                     }
230                 }
231                 if {$obj != {} && $type == "commit" && $tag != {}} {
232                     set tagids($tag) $obj
233                     lappend idtags($obj) $tag
234                 }
235             }
236             close $fd
237         }
238     }
239     set heads [glob -nocomplain -types f .git/refs/heads/*]
240     foreach f $heads {
241         catch {
242             set fd [open $f r]
243             set line [read $fd 40]
244             if {[regexp {^[0-9a-f]{40}} $line id]} {
245                 set head [file tail $f]
246                 set headids($head) $line
247                 lappend idheads($line) $head
248             }
249             close $fd
250         }
251     }
252 }
253
254 proc error_popup msg {
255     set w .error
256     toplevel $w
257     wm transient $w .
258     message $w.m -text $msg -justify center -aspect 400
259     pack $w.m -side top -fill x -padx 20 -pady 20
260     button $w.ok -text OK -command "destroy $w"
261     pack $w.ok -side bottom -fill x
262     bind $w <Visibility> "grab $w; focus $w"
263     tkwait window $w
264 }
265
266 proc makewindow {} {
267     global canv canv2 canv3 linespc charspc ctext cflist textfont
268     global findtype findloc findstring fstring geometry
269     global entries sha1entry sha1string sha1but
270     global maincursor textcursor
271     global rowctxmenu
272
273     menu .bar
274     .bar add cascade -label "File" -menu .bar.file
275     menu .bar.file
276     .bar.file add command -label "Quit" -command doquit
277     menu .bar.help
278     .bar add cascade -label "Help" -menu .bar.help
279     .bar.help add command -label "About gitk" -command about
280     . configure -menu .bar
281
282     if {![info exists geometry(canv1)]} {
283         set geometry(canv1) [expr 45 * $charspc]
284         set geometry(canv2) [expr 30 * $charspc]
285         set geometry(canv3) [expr 15 * $charspc]
286         set geometry(canvh) [expr 25 * $linespc + 4]
287         set geometry(ctextw) 80
288         set geometry(ctexth) 30
289         set geometry(cflistw) 30
290     }
291     panedwindow .ctop -orient vertical
292     if {[info exists geometry(width)]} {
293         .ctop conf -width $geometry(width) -height $geometry(height)
294         set texth [expr {$geometry(height) - $geometry(canvh) - 56}]
295         set geometry(ctexth) [expr {($texth - 8) /
296                                     [font metrics $textfont -linespace]}]
297     }
298     frame .ctop.top
299     frame .ctop.top.bar
300     pack .ctop.top.bar -side bottom -fill x
301     set cscroll .ctop.top.csb
302     scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
303     pack $cscroll -side right -fill y
304     panedwindow .ctop.top.clist -orient horizontal -sashpad 0 -handlesize 4
305     pack .ctop.top.clist -side top -fill both -expand 1
306     .ctop add .ctop.top
307     set canv .ctop.top.clist.canv
308     canvas $canv -height $geometry(canvh) -width $geometry(canv1) \
309         -bg white -bd 0 \
310         -yscrollincr $linespc -yscrollcommand "$cscroll set"
311     .ctop.top.clist add $canv
312     set canv2 .ctop.top.clist.canv2
313     canvas $canv2 -height $geometry(canvh) -width $geometry(canv2) \
314         -bg white -bd 0 -yscrollincr $linespc
315     .ctop.top.clist add $canv2
316     set canv3 .ctop.top.clist.canv3
317     canvas $canv3 -height $geometry(canvh) -width $geometry(canv3) \
318         -bg white -bd 0 -yscrollincr $linespc
319     .ctop.top.clist add $canv3
320     bind .ctop.top.clist <Configure> {resizeclistpanes %W %w}
321
322     set sha1entry .ctop.top.bar.sha1
323     set entries $sha1entry
324     set sha1but .ctop.top.bar.sha1label
325     button $sha1but -text "SHA1 ID: " -state disabled -relief flat \
326         -command gotocommit -width 8
327     $sha1but conf -disabledforeground [$sha1but cget -foreground]
328     pack .ctop.top.bar.sha1label -side left
329     entry $sha1entry -width 40 -font $textfont -textvariable sha1string
330     trace add variable sha1string write sha1change
331     pack $sha1entry -side left -pady 2
332     button .ctop.top.bar.findbut -text "Find" -command dofind
333     pack .ctop.top.bar.findbut -side left
334     set findstring {}
335     set fstring .ctop.top.bar.findstring
336     lappend entries $fstring
337     entry $fstring -width 30 -font $textfont -textvariable findstring
338     pack $fstring -side left -expand 1 -fill x
339     set findtype Exact
340     tk_optionMenu .ctop.top.bar.findtype findtype Exact IgnCase Regexp
341     set findloc "All fields"
342     tk_optionMenu .ctop.top.bar.findloc findloc "All fields" Headline \
343         Comments Author Committer
344     pack .ctop.top.bar.findloc -side right
345     pack .ctop.top.bar.findtype -side right
346
347     panedwindow .ctop.cdet -orient horizontal
348     .ctop add .ctop.cdet
349     frame .ctop.cdet.left
350     set ctext .ctop.cdet.left.ctext
351     text $ctext -bg white -state disabled -font $textfont \
352         -width $geometry(ctextw) -height $geometry(ctexth) \
353         -yscrollcommand ".ctop.cdet.left.sb set"
354     scrollbar .ctop.cdet.left.sb -command "$ctext yview"
355     pack .ctop.cdet.left.sb -side right -fill y
356     pack $ctext -side left -fill both -expand 1
357     .ctop.cdet add .ctop.cdet.left
358
359     $ctext tag conf filesep -font [concat $textfont bold]
360     $ctext tag conf hunksep -back blue -fore white
361     $ctext tag conf d0 -back "#ff8080"
362     $ctext tag conf d1 -back green
363     $ctext tag conf found -back yellow
364
365     frame .ctop.cdet.right
366     set cflist .ctop.cdet.right.cfiles
367     listbox $cflist -bg white -selectmode extended -width $geometry(cflistw) \
368         -yscrollcommand ".ctop.cdet.right.sb set"
369     scrollbar .ctop.cdet.right.sb -command "$cflist yview"
370     pack .ctop.cdet.right.sb -side right -fill y
371     pack $cflist -side left -fill both -expand 1
372     .ctop.cdet add .ctop.cdet.right
373     bind .ctop.cdet <Configure> {resizecdetpanes %W %w}
374
375     pack .ctop -side top -fill both -expand 1
376
377     bindall <1> {selcanvline %W %x %y}
378     #bindall <B1-Motion> {selcanvline %W %x %y}
379     bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
380     bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
381     bindall <2> "allcanvs scan mark 0 %y"
382     bindall <B2-Motion> "allcanvs scan dragto 0 %y"
383     bind . <Key-Up> "selnextline -1"
384     bind . <Key-Down> "selnextline 1"
385     bind . <Key-Prior> "allcanvs yview scroll -1 pages"
386     bind . <Key-Next> "allcanvs yview scroll 1 pages"
387     bindkey <Key-Delete> "$ctext yview scroll -1 pages"
388     bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
389     bindkey <Key-space> "$ctext yview scroll 1 pages"
390     bindkey p "selnextline -1"
391     bindkey n "selnextline 1"
392     bindkey b "$ctext yview scroll -1 pages"
393     bindkey d "$ctext yview scroll 18 units"
394     bindkey u "$ctext yview scroll -18 units"
395     bindkey / findnext
396     bindkey ? findprev
397     bindkey f nextfile
398     bind . <Control-q> doquit
399     bind . <Control-f> dofind
400     bind . <Control-g> findnext
401     bind . <Control-r> findprev
402     bind . <Control-equal> {incrfont 1}
403     bind . <Control-KP_Add> {incrfont 1}
404     bind . <Control-minus> {incrfont -1}
405     bind . <Control-KP_Subtract> {incrfont -1}
406     bind $cflist <<ListboxSelect>> listboxsel
407     bind . <Destroy> {savestuff %W}
408     bind . <Button-1> "click %W"
409     bind $fstring <Key-Return> dofind
410     bind $sha1entry <Key-Return> gotocommit
411     bind $sha1entry <<PasteSelection>> clearsha1
412
413     set maincursor [. cget -cursor]
414     set textcursor [$ctext cget -cursor]
415
416     set rowctxmenu .rowctxmenu
417     menu $rowctxmenu -tearoff 0
418     $rowctxmenu add command -label "Diff this -> selected" \
419         -command {diffvssel 0}
420     $rowctxmenu add command -label "Diff selected -> this" \
421         -command {diffvssel 1}
422     $rowctxmenu add command -label "Make patch" -command mkpatch
423 }
424
425 # when we make a key binding for the toplevel, make sure
426 # it doesn't get triggered when that key is pressed in the
427 # find string entry widget.
428 proc bindkey {ev script} {
429     global entries
430     bind . $ev $script
431     set escript [bind Entry $ev]
432     if {$escript == {}} {
433         set escript [bind Entry <Key>]
434     }
435     foreach e $entries {
436         bind $e $ev "$escript; break"
437     }
438 }
439
440 # set the focus back to the toplevel for any click outside
441 # the entry widgets
442 proc click {w} {
443     global entries
444     foreach e $entries {
445         if {$w == $e} return
446     }
447     focus .
448 }
449
450 proc savestuff {w} {
451     global canv canv2 canv3 ctext cflist mainfont textfont
452     global stuffsaved
453     if {$stuffsaved} return
454     if {![winfo viewable .]} return
455     catch {
456         set f [open "~/.gitk-new" w]
457         puts $f "set mainfont {$mainfont}"
458         puts $f "set textfont {$textfont}"
459         puts $f "set geometry(width) [winfo width .ctop]"
460         puts $f "set geometry(height) [winfo height .ctop]"
461         puts $f "set geometry(canv1) [expr [winfo width $canv]-2]"
462         puts $f "set geometry(canv2) [expr [winfo width $canv2]-2]"
463         puts $f "set geometry(canv3) [expr [winfo width $canv3]-2]"
464         puts $f "set geometry(canvh) [expr [winfo height $canv]-2]"
465         set wid [expr {([winfo width $ctext] - 8) \
466                            / [font measure $textfont "0"]}]
467         puts $f "set geometry(ctextw) $wid"
468         set wid [expr {([winfo width $cflist] - 11) \
469                            / [font measure [$cflist cget -font] "0"]}]
470         puts $f "set geometry(cflistw) $wid"
471         close $f
472         file rename -force "~/.gitk-new" "~/.gitk"
473     }
474     set stuffsaved 1
475 }
476
477 proc resizeclistpanes {win w} {
478     global oldwidth
479     if [info exists oldwidth($win)] {
480         set s0 [$win sash coord 0]
481         set s1 [$win sash coord 1]
482         if {$w < 60} {
483             set sash0 [expr {int($w/2 - 2)}]
484             set sash1 [expr {int($w*5/6 - 2)}]
485         } else {
486             set factor [expr {1.0 * $w / $oldwidth($win)}]
487             set sash0 [expr {int($factor * [lindex $s0 0])}]
488             set sash1 [expr {int($factor * [lindex $s1 0])}]
489             if {$sash0 < 30} {
490                 set sash0 30
491             }
492             if {$sash1 < $sash0 + 20} {
493                 set sash1 [expr $sash0 + 20]
494             }
495             if {$sash1 > $w - 10} {
496                 set sash1 [expr $w - 10]
497                 if {$sash0 > $sash1 - 20} {
498                     set sash0 [expr $sash1 - 20]
499                 }
500             }
501         }
502         $win sash place 0 $sash0 [lindex $s0 1]
503         $win sash place 1 $sash1 [lindex $s1 1]
504     }
505     set oldwidth($win) $w
506 }
507
508 proc resizecdetpanes {win w} {
509     global oldwidth
510     if [info exists oldwidth($win)] {
511         set s0 [$win sash coord 0]
512         if {$w < 60} {
513             set sash0 [expr {int($w*3/4 - 2)}]
514         } else {
515             set factor [expr {1.0 * $w / $oldwidth($win)}]
516             set sash0 [expr {int($factor * [lindex $s0 0])}]
517             if {$sash0 < 45} {
518                 set sash0 45
519             }
520             if {$sash0 > $w - 15} {
521                 set sash0 [expr $w - 15]
522             }
523         }
524         $win sash place 0 $sash0 [lindex $s0 1]
525     }
526     set oldwidth($win) $w
527 }
528
529 proc allcanvs args {
530     global canv canv2 canv3
531     eval $canv $args
532     eval $canv2 $args
533     eval $canv3 $args
534 }
535
536 proc bindall {event action} {
537     global canv canv2 canv3
538     bind $canv $event $action
539     bind $canv2 $event $action
540     bind $canv3 $event $action
541 }
542
543 proc about {} {
544     set w .about
545     if {[winfo exists $w]} {
546         raise $w
547         return
548     }
549     toplevel $w
550     wm title $w "About gitk"
551     message $w.m -text {
552 Gitk version 1.2
553
554 Copyright Â© 2005 Paul Mackerras
555
556 Use and redistribute under the terms of the GNU General Public License} \
557             -justify center -aspect 400
558     pack $w.m -side top -fill x -padx 20 -pady 20
559     button $w.ok -text Close -command "destroy $w"
560     pack $w.ok -side bottom
561 }
562
563 proc assigncolor {id} {
564     global commitinfo colormap commcolors colors nextcolor
565     global parents nparents children nchildren
566     global cornercrossings crossings
567
568     if [info exists colormap($id)] return
569     set ncolors [llength $colors]
570     if {$nparents($id) <= 1 && $nchildren($id) == 1} {
571         set child [lindex $children($id) 0]
572         if {[info exists colormap($child)]
573             && $nparents($child) == 1} {
574             set colormap($id) $colormap($child)
575             return
576         }
577     }
578     set badcolors {}
579     if {[info exists cornercrossings($id)]} {
580         foreach x $cornercrossings($id) {
581             if {[info exists colormap($x)]
582                 && [lsearch -exact $badcolors $colormap($x)] < 0} {
583                 lappend badcolors $colormap($x)
584             }
585         }
586         if {[llength $badcolors] >= $ncolors} {
587             set badcolors {}
588         }
589     }
590     set origbad $badcolors
591     if {[llength $badcolors] < $ncolors - 1} {
592         if {[info exists crossings($id)]} {
593             foreach x $crossings($id) {
594                 if {[info exists colormap($x)]
595                     && [lsearch -exact $badcolors $colormap($x)] < 0} {
596                     lappend badcolors $colormap($x)
597                 }
598             }
599             if {[llength $badcolors] >= $ncolors} {
600                 set badcolors $origbad
601             }
602         }
603         set origbad $badcolors
604     }
605     if {[llength $badcolors] < $ncolors - 1} {
606         foreach child $children($id) {
607             if {[info exists colormap($child)]
608                 && [lsearch -exact $badcolors $colormap($child)] < 0} {
609                 lappend badcolors $colormap($child)
610             }
611             if {[info exists parents($child)]} {
612                 foreach p $parents($child) {
613                     if {[info exists colormap($p)]
614                         && [lsearch -exact $badcolors $colormap($p)] < 0} {
615                         lappend badcolors $colormap($p)
616                     }
617                 }
618             }
619         }
620         if {[llength $badcolors] >= $ncolors} {
621             set badcolors $origbad
622         }
623     }
624     for {set i 0} {$i <= $ncolors} {incr i} {
625         set c [lindex $colors $nextcolor]
626         if {[incr nextcolor] >= $ncolors} {
627             set nextcolor 0
628         }
629         if {[lsearch -exact $badcolors $c]} break
630     }
631     set colormap($id) $c
632 }
633
634 proc initgraph {} {
635     global canvy canvy0 lineno numcommits lthickness nextcolor linespc
636     global mainline sidelines
637     global nchildren ncleft
638
639     allcanvs delete all
640     set nextcolor 0
641     set canvy $canvy0
642     set lineno -1
643     set numcommits 0
644     set lthickness [expr {int($linespc / 9) + 1}]
645     catch {unset mainline}
646     catch {unset sidelines}
647     foreach id [array names nchildren] {
648         set ncleft($id) $nchildren($id)
649     }
650 }
651
652 proc bindline {t id} {
653     global canv
654
655     $canv bind $t <Enter> "lineenter %x %y $id"
656     $canv bind $t <Motion> "linemotion %x %y $id"
657     $canv bind $t <Leave> "lineleave $id"
658     $canv bind $t <Button-1> "lineclick %x %y $id"
659 }
660
661 proc drawcommitline {level} {
662     global parents children nparents nchildren todo
663     global canv canv2 canv3 mainfont namefont canvx0 canvy linespc
664     global lineid linehtag linentag linedtag commitinfo
665     global colormap numcommits currentparents dupparents
666     global oldlevel oldnlines oldtodo
667     global idtags idline idheads
668     global lineno lthickness mainline sidelines
669     global commitlisted rowtextx
670
671     incr numcommits
672     incr lineno
673     set id [lindex $todo $level]
674     set lineid($lineno) $id
675     set idline($id) $lineno
676     set ofill [expr {[info exists commitlisted($id)]? "blue": "white"}]
677     if {![info exists commitinfo($id)]} {
678         readcommit $id
679         if {![info exists commitinfo($id)]} {
680             set commitinfo($id) {"No commit information available"}
681             set nparents($id) 0
682         }
683     }
684     assigncolor $id
685     set currentparents {}
686     set dupparents {}
687     if {[info exists commitlisted($id)] && [info exists parents($id)]} {
688         foreach p $parents($id) {
689             if {[lsearch -exact $currentparents $p] < 0} {
690                 lappend currentparents $p
691             } else {
692                 # remember that this parent was listed twice
693                 lappend dupparents $p
694             }
695         }
696     }
697     set x [expr $canvx0 + $level * $linespc]
698     set y1 $canvy
699     set canvy [expr $canvy + $linespc]
700     allcanvs conf -scrollregion \
701         [list 0 0 0 [expr $y1 + 0.5 * $linespc + 2]]
702     if {[info exists mainline($id)]} {
703         lappend mainline($id) $x $y1
704         set t [$canv create line $mainline($id) \
705                    -width $lthickness -fill $colormap($id)]
706         $canv lower $t
707         bindline $t $id
708     }
709     if {[info exists sidelines($id)]} {
710         foreach ls $sidelines($id) {
711             set coords [lindex $ls 0]
712             set thick [lindex $ls 1]
713             set t [$canv create line $coords -fill $colormap($id) \
714                        -width [expr {$thick * $lthickness}]]
715             $canv lower $t
716             bindline $t $id
717         }
718     }
719     set orad [expr {$linespc / 3}]
720     set t [$canv create oval [expr $x - $orad] [expr $y1 - $orad] \
721                [expr $x + $orad - 1] [expr $y1 + $orad - 1] \
722                -fill $ofill -outline black -width 1]
723     $canv raise $t
724     $canv bind $t <1> {selcanvline {} %x %y}
725     set xt [expr $canvx0 + [llength $todo] * $linespc]
726     if {[llength $currentparents] > 2} {
727         set xt [expr {$xt + ([llength $currentparents] - 2) * $linespc}]
728     }
729     set rowtextx($lineno) $xt
730     set marks {}
731     set ntags 0
732     if {[info exists idtags($id)]} {
733         set marks $idtags($id)
734         set ntags [llength $marks]
735     }
736     if {[info exists idheads($id)]} {
737         set marks [concat $marks $idheads($id)]
738     }
739     if {$marks != {}} {
740         set delta [expr {int(0.5 * ($linespc - $lthickness))}]
741         set yt [expr $y1 - 0.5 * $linespc]
742         set yb [expr $yt + $linespc - 1]
743         set xvals {}
744         set wvals {}
745         foreach tag $marks {
746             set wid [font measure $mainfont $tag]
747             lappend xvals $xt
748             lappend wvals $wid
749             set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
750         }
751         set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
752                    -width $lthickness -fill black]
753         $canv lower $t
754         foreach tag $marks x $xvals wid $wvals {
755             set xl [expr $x + $delta]
756             set xr [expr $x + $delta + $wid + $lthickness]
757             if {[incr ntags -1] >= 0} {
758                 # draw a tag
759                 $canv create polygon $x [expr $yt + $delta] $xl $yt\
760                     $xr $yt $xr $yb $xl $yb $x [expr $yb - $delta] \
761                     -width 1 -outline black -fill yellow
762             } else {
763                 # draw a head
764                 set xl [expr $xl - $delta/2]
765                 $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
766                     -width 1 -outline black -fill green
767             }
768             $canv create text $xl $y1 -anchor w -text $tag \
769                 -font $mainfont
770         }
771     }
772     set headline [lindex $commitinfo($id) 0]
773     set name [lindex $commitinfo($id) 1]
774     set date [lindex $commitinfo($id) 2]
775     set linehtag($lineno) [$canv create text $xt $y1 -anchor w \
776                                -text $headline -font $mainfont ]
777     $canv bind $linehtag($lineno) <Button-3> "rowmenu %X %Y $id"
778     set linentag($lineno) [$canv2 create text 3 $y1 -anchor w \
779                                -text $name -font $namefont]
780     set linedtag($lineno) [$canv3 create text 3 $y1 -anchor w \
781                                -text $date -font $mainfont]
782 }
783
784 proc updatetodo {level noshortcut} {
785     global currentparents ncleft todo
786     global mainline oldlevel oldtodo oldnlines
787     global canvx0 canvy linespc mainline
788     global commitinfo
789
790     set oldlevel $level
791     set oldtodo $todo
792     set oldnlines [llength $todo]
793     if {!$noshortcut && [llength $currentparents] == 1} {
794         set p [lindex $currentparents 0]
795         if {$ncleft($p) == 1 && [lsearch -exact $todo $p] < 0} {
796             set ncleft($p) 0
797             set x [expr $canvx0 + $level * $linespc]
798             set y [expr $canvy - $linespc]
799             set mainline($p) [list $x $y]
800             set todo [lreplace $todo $level $level $p]
801             return 0
802         }
803     }
804
805     set todo [lreplace $todo $level $level]
806     set i $level
807     foreach p $currentparents {
808         incr ncleft($p) -1
809         set k [lsearch -exact $todo $p]
810         if {$k < 0} {
811             set todo [linsert $todo $i $p]
812             incr i
813         }
814     }
815     return 1
816 }
817
818 proc notecrossings {id lo hi corner} {
819     global oldtodo crossings cornercrossings
820
821     for {set i $lo} {[incr i] < $hi} {} {
822         set p [lindex $oldtodo $i]
823         if {$p == {}} continue
824         if {$i == $corner} {
825             if {![info exists cornercrossings($id)]
826                 || [lsearch -exact $cornercrossings($id) $p] < 0} {
827                 lappend cornercrossings($id) $p
828             }
829             if {![info exists cornercrossings($p)]
830                 || [lsearch -exact $cornercrossings($p) $id] < 0} {
831                 lappend cornercrossings($p) $id
832             }
833         } else {
834             if {![info exists crossings($id)]
835                 || [lsearch -exact $crossings($id) $p] < 0} {
836                 lappend crossings($id) $p
837             }
838             if {![info exists crossings($p)]
839                 || [lsearch -exact $crossings($p) $id] < 0} {
840                 lappend crossings($p) $id
841             }
842         }
843     }
844 }
845
846 proc drawslants {} {
847     global canv mainline sidelines canvx0 canvy linespc
848     global oldlevel oldtodo todo currentparents dupparents
849     global lthickness linespc canvy colormap
850
851     set y1 [expr $canvy - $linespc]
852     set y2 $canvy
853     set i -1
854     foreach id $oldtodo {
855         incr i
856         if {$id == {}} continue
857         set xi [expr {$canvx0 + $i * $linespc}]
858         if {$i == $oldlevel} {
859             foreach p $currentparents {
860                 set j [lsearch -exact $todo $p]
861                 set coords [list $xi $y1]
862                 set xj [expr {$canvx0 + $j * $linespc}]
863                 if {$j < $i - 1} {
864                     lappend coords [expr $xj + $linespc] $y1
865                     notecrossings $p $j $i [expr {$j + 1}]
866                 } elseif {$j > $i + 1} {
867                     lappend coords [expr $xj - $linespc] $y1
868                     notecrossings $p $i $j [expr {$j - 1}]
869                 }
870                 if {[lsearch -exact $dupparents $p] >= 0} {
871                     # draw a double-width line to indicate the doubled parent
872                     lappend coords $xj $y2
873                     lappend sidelines($p) [list $coords 2]
874                     if {![info exists mainline($p)]} {
875                         set mainline($p) [list $xj $y2]
876                     }
877                 } else {
878                     # normal case, no parent duplicated
879                     if {![info exists mainline($p)]} {
880                         if {$i != $j} {
881                             lappend coords $xj $y2
882                         }
883                         set mainline($p) $coords
884                     } else {
885                         lappend coords $xj $y2
886                         lappend sidelines($p) [list $coords 1]
887                     }
888                 }
889             }
890         } elseif {[lindex $todo $i] != $id} {
891             set j [lsearch -exact $todo $id]
892             set xj [expr {$canvx0 + $j * $linespc}]
893             lappend mainline($id) $xi $y1 $xj $y2
894         }
895     }
896 }
897
898 proc decidenext {{noread 0}} {
899     global parents children nchildren ncleft todo
900     global canv canv2 canv3 mainfont namefont canvx0 canvy linespc
901     global datemode cdate
902     global commitinfo
903     global currentparents oldlevel oldnlines oldtodo
904     global lineno lthickness
905
906     # remove the null entry if present
907     set nullentry [lsearch -exact $todo {}]
908     if {$nullentry >= 0} {
909         set todo [lreplace $todo $nullentry $nullentry]
910     }
911
912     # choose which one to do next time around
913     set todol [llength $todo]
914     set level -1
915     set latest {}
916     for {set k $todol} {[incr k -1] >= 0} {} {
917         set p [lindex $todo $k]
918         if {$ncleft($p) == 0} {
919             if {$datemode} {
920                 if {![info exists commitinfo($p)]} {
921                     if {$noread} {
922                         return {}
923                     }
924                     readcommit $p
925                 }
926                 if {$latest == {} || $cdate($p) > $latest} {
927                     set level $k
928                     set latest $cdate($p)
929                 }
930             } else {
931                 set level $k
932                 break
933             }
934         }
935     }
936     if {$level < 0} {
937         if {$todo != {}} {
938             puts "ERROR: none of the pending commits can be done yet:"
939             foreach p $todo {
940                 puts "  $p ($ncleft($p))"
941             }
942         }
943         return -1
944     }
945
946     # If we are reducing, put in a null entry
947     if {$todol < $oldnlines} {
948         if {$nullentry >= 0} {
949             set i $nullentry
950             while {$i < $todol
951                    && [lindex $oldtodo $i] == [lindex $todo $i]} {
952                 incr i
953             }
954         } else {
955             set i $oldlevel
956             if {$level >= $i} {
957                 incr i
958             }
959         }
960         if {$i < $todol} {
961             set todo [linsert $todo $i {}]
962             if {$level >= $i} {
963                 incr level
964             }
965         }
966     }
967     return $level
968 }
969
970 proc drawcommit {id} {
971     global phase todo nchildren datemode nextupdate
972     global startcommits
973
974     if {$phase != "incrdraw"} {
975         set phase incrdraw
976         set todo $id
977         set startcommits $id
978         initgraph
979         drawcommitline 0
980         updatetodo 0 $datemode
981     } else {
982         if {$nchildren($id) == 0} {
983             lappend todo $id
984             lappend startcommits $id
985         }
986         set level [decidenext 1]
987         if {$level == {} || $id != [lindex $todo $level]} {
988             return
989         }
990         while 1 {
991             drawslants
992             drawcommitline $level
993             if {[updatetodo $level $datemode]} {
994                 set level [decidenext 1]
995                 if {$level == {}} break
996             }
997             set id [lindex $todo $level]
998             if {![info exists commitlisted($id)]} {
999                 break
1000             }
1001             if {[clock clicks -milliseconds] >= $nextupdate} {
1002                 doupdate
1003                 if {$stopped} break
1004             }
1005         }
1006     }
1007 }
1008
1009 proc finishcommits {} {
1010     global phase
1011     global startcommits
1012     global canv mainfont ctext maincursor textcursor
1013
1014     if {$phase != "incrdraw"} {
1015         $canv delete all
1016         $canv create text 3 3 -anchor nw -text "No commits selected" \
1017             -font $mainfont -tags textitems
1018         set phase {}
1019     } else {
1020         drawslants
1021         set level [decidenext]
1022         drawrest $level [llength $startcommits]
1023     }
1024     . config -cursor $maincursor
1025     $ctext config -cursor $textcursor
1026 }
1027
1028 proc drawgraph {} {
1029     global nextupdate startmsecs startcommits todo
1030
1031     if {$startcommits == {}} return
1032     set startmsecs [clock clicks -milliseconds]
1033     set nextupdate [expr $startmsecs + 100]
1034     initgraph
1035     set todo [lindex $startcommits 0]
1036     drawrest 0 1
1037 }
1038
1039 proc drawrest {level startix} {
1040     global phase stopped redisplaying selectedline
1041     global datemode currentparents todo
1042     global numcommits
1043     global nextupdate startmsecs startcommits idline
1044
1045     if {$level >= 0} {
1046         set phase drawgraph
1047         set startid [lindex $startcommits $startix]
1048         set startline -1
1049         if {$startid != {}} {
1050             set startline $idline($startid)
1051         }
1052         while 1 {
1053             if {$stopped} break
1054             drawcommitline $level
1055             set hard [updatetodo $level $datemode]
1056             if {$numcommits == $startline} {
1057                 lappend todo $startid
1058                 set hard 1
1059                 incr startix
1060                 set startid [lindex $startcommits $startix]
1061                 set startline -1
1062                 if {$startid != {}} {
1063                     set startline $idline($startid)
1064                 }
1065             }
1066             if {$hard} {
1067                 set level [decidenext]
1068                 if {$level < 0} break
1069                 drawslants
1070             }
1071             if {[clock clicks -milliseconds] >= $nextupdate} {
1072                 update
1073                 incr nextupdate 100
1074             }
1075         }
1076     }
1077     set phase {}
1078     set drawmsecs [expr [clock clicks -milliseconds] - $startmsecs]
1079     #puts "overall $drawmsecs ms for $numcommits commits"
1080     if {$redisplaying} {
1081         if {$stopped == 0 && [info exists selectedline]} {
1082             selectline $selectedline
1083         }
1084         if {$stopped == 1} {
1085             set stopped 0
1086             after idle drawgraph
1087         } else {
1088             set redisplaying 0
1089         }
1090     }
1091 }
1092
1093 proc findmatches {f} {
1094     global findtype foundstring foundstrlen
1095     if {$findtype == "Regexp"} {
1096         set matches [regexp -indices -all -inline $foundstring $f]
1097     } else {
1098         if {$findtype == "IgnCase"} {
1099             set str [string tolower $f]
1100         } else {
1101             set str $f
1102         }
1103         set matches {}
1104         set i 0
1105         while {[set j [string first $foundstring $str $i]] >= 0} {
1106             lappend matches [list $j [expr $j+$foundstrlen-1]]
1107             set i [expr $j + $foundstrlen]
1108         }
1109     }
1110     return $matches
1111 }
1112
1113 proc dofind {} {
1114     global findtype findloc findstring markedmatches commitinfo
1115     global numcommits lineid linehtag linentag linedtag
1116     global mainfont namefont canv canv2 canv3 selectedline
1117     global matchinglines foundstring foundstrlen
1118     unmarkmatches
1119     focus .
1120     set matchinglines {}
1121     set fldtypes {Headline Author Date Committer CDate Comment}
1122     if {$findtype == "IgnCase"} {
1123         set foundstring [string tolower $findstring]
1124     } else {
1125         set foundstring $findstring
1126     }
1127     set foundstrlen [string length $findstring]
1128     if {$foundstrlen == 0} return
1129     if {![info exists selectedline]} {
1130         set oldsel -1
1131     } else {
1132         set oldsel $selectedline
1133     }
1134     set didsel 0
1135     for {set l 0} {$l < $numcommits} {incr l} {
1136         set id $lineid($l)
1137         set info $commitinfo($id)
1138         set doesmatch 0
1139         foreach f $info ty $fldtypes {
1140             if {$findloc != "All fields" && $findloc != $ty} {
1141                 continue
1142             }
1143             set matches [findmatches $f]
1144             if {$matches == {}} continue
1145             set doesmatch 1
1146             if {$ty == "Headline"} {
1147                 markmatches $canv $l $f $linehtag($l) $matches $mainfont
1148             } elseif {$ty == "Author"} {
1149                 markmatches $canv2 $l $f $linentag($l) $matches $namefont
1150             } elseif {$ty == "Date"} {
1151                 markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
1152             }
1153         }
1154         if {$doesmatch} {
1155             lappend matchinglines $l
1156             if {!$didsel && $l > $oldsel} {
1157                 findselectline $l
1158                 set didsel 1
1159             }
1160         }
1161     }
1162     if {$matchinglines == {}} {
1163         bell
1164     } elseif {!$didsel} {
1165         findselectline [lindex $matchinglines 0]
1166     }
1167 }
1168
1169 proc findselectline {l} {
1170     global findloc commentend ctext
1171     selectline $l
1172     if {$findloc == "All fields" || $findloc == "Comments"} {
1173         # highlight the matches in the comments
1174         set f [$ctext get 1.0 $commentend]
1175         set matches [findmatches $f]
1176         foreach match $matches {
1177             set start [lindex $match 0]
1178             set end [expr [lindex $match 1] + 1]
1179             $ctext tag add found "1.0 + $start c" "1.0 + $end c"
1180         }
1181     }
1182 }
1183
1184 proc findnext {} {
1185     global matchinglines selectedline
1186     if {![info exists matchinglines]} {
1187         dofind
1188         return
1189     }
1190     if {![info exists selectedline]} return
1191     foreach l $matchinglines {
1192         if {$l > $selectedline} {
1193             findselectline $l
1194             return
1195         }
1196     }
1197     bell
1198 }
1199
1200 proc findprev {} {
1201     global matchinglines selectedline
1202     if {![info exists matchinglines]} {
1203         dofind
1204         return
1205     }
1206     if {![info exists selectedline]} return
1207     set prev {}
1208     foreach l $matchinglines {
1209         if {$l >= $selectedline} break
1210         set prev $l
1211     }
1212     if {$prev != {}} {
1213         findselectline $prev
1214     } else {
1215         bell
1216     }
1217 }
1218
1219 proc markmatches {canv l str tag matches font} {
1220     set bbox [$canv bbox $tag]
1221     set x0 [lindex $bbox 0]
1222     set y0 [lindex $bbox 1]
1223     set y1 [lindex $bbox 3]
1224     foreach match $matches {
1225         set start [lindex $match 0]
1226         set end [lindex $match 1]
1227         if {$start > $end} continue
1228         set xoff [font measure $font [string range $str 0 [expr $start-1]]]
1229         set xlen [font measure $font [string range $str 0 [expr $end]]]
1230         set t [$canv create rect [expr $x0+$xoff] $y0 [expr $x0+$xlen+2] $y1 \
1231                    -outline {} -tags matches -fill yellow]
1232         $canv lower $t
1233     }
1234 }
1235
1236 proc unmarkmatches {} {
1237     global matchinglines
1238     allcanvs delete matches
1239     catch {unset matchinglines}
1240 }
1241
1242 proc selcanvline {w x y} {
1243     global canv canvy0 ctext linespc selectedline
1244     global lineid linehtag linentag linedtag rowtextx
1245     set ymax [lindex [$canv cget -scrollregion] 3]
1246     if {$ymax == {}} return
1247     set yfrac [lindex [$canv yview] 0]
1248     set y [expr {$y + $yfrac * $ymax}]
1249     set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
1250     if {$l < 0} {
1251         set l 0
1252     }
1253     if {$w eq $canv} {
1254         if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
1255     }
1256     unmarkmatches
1257     selectline $l
1258 }
1259
1260 proc selectline {l} {
1261     global canv canv2 canv3 ctext commitinfo selectedline
1262     global lineid linehtag linentag linedtag
1263     global canvy0 linespc parents nparents
1264     global cflist currentid sha1entry diffids
1265     global commentend seenfile idtags
1266     $canv delete hover
1267     if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
1268     $canv delete secsel
1269     set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
1270                -tags secsel -fill [$canv cget -selectbackground]]
1271     $canv lower $t
1272     $canv2 delete secsel
1273     set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
1274                -tags secsel -fill [$canv2 cget -selectbackground]]
1275     $canv2 lower $t
1276     $canv3 delete secsel
1277     set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
1278                -tags secsel -fill [$canv3 cget -selectbackground]]
1279     $canv3 lower $t
1280     set y [expr {$canvy0 + $l * $linespc}]
1281     set ymax [lindex [$canv cget -scrollregion] 3]
1282     set ytop [expr {$y - $linespc - 1}]
1283     set ybot [expr {$y + $linespc + 1}]
1284     set wnow [$canv yview]
1285     set wtop [expr [lindex $wnow 0] * $ymax]
1286     set wbot [expr [lindex $wnow 1] * $ymax]
1287     set wh [expr {$wbot - $wtop}]
1288     set newtop $wtop
1289     if {$ytop < $wtop} {
1290         if {$ybot < $wtop} {
1291             set newtop [expr {$y - $wh / 2.0}]
1292         } else {
1293             set newtop $ytop
1294             if {$newtop > $wtop - $linespc} {
1295                 set newtop [expr {$wtop - $linespc}]
1296             }
1297         }
1298     } elseif {$ybot > $wbot} {
1299         if {$ytop > $wbot} {
1300             set newtop [expr {$y - $wh / 2.0}]
1301         } else {
1302             set newtop [expr {$ybot - $wh}]
1303             if {$newtop < $wtop + $linespc} {
1304                 set newtop [expr {$wtop + $linespc}]
1305             }
1306         }
1307     }
1308     if {$newtop != $wtop} {
1309         if {$newtop < 0} {
1310             set newtop 0
1311         }
1312         allcanvs yview moveto [expr $newtop * 1.0 / $ymax]
1313     }
1314     set selectedline $l
1315
1316     set id $lineid($l)
1317     set currentid $id
1318     set diffids [concat $id $parents($id)]
1319     $sha1entry delete 0 end
1320     $sha1entry insert 0 $id
1321     $sha1entry selection from 0
1322     $sha1entry selection to end
1323
1324     $ctext conf -state normal
1325     $ctext delete 0.0 end
1326     $ctext mark set fmark.0 0.0
1327     $ctext mark gravity fmark.0 left
1328     set info $commitinfo($id)
1329     $ctext insert end "Author: [lindex $info 1]  [lindex $info 2]\n"
1330     $ctext insert end "Committer: [lindex $info 3]  [lindex $info 4]\n"
1331     if {[info exists idtags($id)]} {
1332         $ctext insert end "Tags:"
1333         foreach tag $idtags($id) {
1334             $ctext insert end " $tag"
1335         }
1336         $ctext insert end "\n"
1337     }
1338     $ctext insert end "\n"
1339     $ctext insert end [lindex $info 5]
1340     $ctext insert end "\n"
1341     $ctext tag delete Comments
1342     $ctext tag remove found 1.0 end
1343     $ctext conf -state disabled
1344     set commentend [$ctext index "end - 1c"]
1345
1346     $cflist delete 0 end
1347     $cflist insert end "Comments"
1348     if {$nparents($id) == 1} {
1349         startdiff
1350     }
1351     catch {unset seenfile}
1352 }
1353
1354 proc startdiff {} {
1355     global treediffs diffids treepending
1356
1357     if {![info exists treediffs($diffids)]} {
1358         if {![info exists treepending]} {
1359             gettreediffs $diffids
1360         }
1361     } else {
1362         addtocflist $diffids
1363     }
1364 }
1365
1366 proc selnextline {dir} {
1367     global selectedline
1368     if {![info exists selectedline]} return
1369     set l [expr $selectedline + $dir]
1370     unmarkmatches
1371     selectline $l
1372 }
1373
1374 proc addtocflist {ids} {
1375     global diffids treediffs cflist
1376     if {$ids != $diffids} {
1377         gettreediffs $diffids
1378         return
1379     }
1380     foreach f $treediffs($ids) {
1381         $cflist insert end $f
1382     }
1383     getblobdiffs $ids
1384 }
1385
1386 proc gettreediffs {ids} {
1387     global treediffs parents treepending
1388     set treepending $ids
1389     set treediffs($ids) {}
1390     set id [lindex $ids 0]
1391     set p [lindex $ids 1]
1392     if [catch {set gdtf [open "|git-diff-tree -r $p $id" r]}] return
1393     fconfigure $gdtf -blocking 0
1394     fileevent $gdtf readable "gettreediffline $gdtf {$ids}"
1395 }
1396
1397 proc gettreediffline {gdtf ids} {
1398     global treediffs treepending
1399     set n [gets $gdtf line]
1400     if {$n < 0} {
1401         if {![eof $gdtf]} return
1402         close $gdtf
1403         unset treepending
1404         addtocflist $ids
1405         return
1406     }
1407     set file [lindex $line 5]
1408     lappend treediffs($ids) $file
1409 }
1410
1411 proc getblobdiffs {ids} {
1412     global diffopts blobdifffd env curdifftag curtagstart
1413     global diffindex difffilestart nextupdate
1414
1415     set id [lindex $ids 0]
1416     set p [lindex $ids 1]
1417     set env(GIT_DIFF_OPTS) $diffopts
1418     if [catch {set bdf [open "|git-diff-tree -r -p $p $id" r]} err] {
1419         puts "error getting diffs: $err"
1420         return
1421     }
1422     fconfigure $bdf -blocking 0
1423     set blobdifffd($ids) $bdf
1424     set curdifftag Comments
1425     set curtagstart 0.0
1426     set diffindex 0
1427     catch {unset difffilestart}
1428     fileevent $bdf readable "getblobdiffline $bdf {$ids}"
1429     set nextupdate [expr {[clock clicks -milliseconds] + 100}]
1430 }
1431
1432 proc getblobdiffline {bdf ids} {
1433     global diffids blobdifffd ctext curdifftag curtagstart seenfile
1434     global diffnexthead diffnextnote diffindex difffilestart
1435     global nextupdate
1436
1437     set n [gets $bdf line]
1438     if {$n < 0} {
1439         if {[eof $bdf]} {
1440             close $bdf
1441             if {$ids == $diffids && $bdf == $blobdifffd($ids)} {
1442                 $ctext tag add $curdifftag $curtagstart end
1443                 set seenfile($curdifftag) 1
1444             }
1445         }
1446         return
1447     }
1448     if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
1449         return
1450     }
1451     $ctext conf -state normal
1452     if {[regexp {^---[ \t]+([^/])*/(.*)} $line match s1 fname]} {
1453         # start of a new file
1454         $ctext insert end "\n"
1455         $ctext tag add $curdifftag $curtagstart end
1456         set seenfile($curdifftag) 1
1457         set curtagstart [$ctext index "end - 1c"]
1458         set header $fname
1459         if {[info exists diffnexthead]} {
1460             set fname $diffnexthead
1461             set header "$diffnexthead ($diffnextnote)"
1462             unset diffnexthead
1463         }
1464         set here [$ctext index "end - 1c"]
1465         set difffilestart($diffindex) $here
1466         incr diffindex
1467         # start mark names at fmark.1 for first file
1468         $ctext mark set fmark.$diffindex $here
1469         $ctext mark gravity fmark.$diffindex left
1470         set curdifftag "f:$fname"
1471         $ctext tag delete $curdifftag
1472         set l [expr {(78 - [string length $header]) / 2}]
1473         set pad [string range "----------------------------------------" 1 $l]
1474         $ctext insert end "$pad $header $pad\n" filesep
1475     } elseif {[string range $line 0 2] == "+++"} {
1476         # no need to do anything with this
1477     } elseif {[regexp {^Created: (.*) \((mode: *[0-7]*)\)} $line match fn m]} {
1478         set diffnexthead $fn
1479         set diffnextnote "created, mode $m"
1480     } elseif {[string range $line 0 8] == "Deleted: "} {
1481         set diffnexthead [string range $line 9 end]
1482         set diffnextnote "deleted"
1483     } elseif {[regexp {^diff --git a/(.*) b/} $line match fn]} {
1484         # save the filename in case the next thing is "new file mode ..."
1485         set diffnexthead $fn
1486         set diffnextnote "modified"
1487     } elseif {[regexp {^new file mode ([0-7]+)} $line match m]} {
1488         set diffnextnote "new file, mode $m"
1489     } elseif {[string range $line 0 11] == "deleted file"} {
1490         set diffnextnote "deleted"
1491     } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
1492                    $line match f1l f1c f2l f2c rest]} {
1493         $ctext insert end "\t" hunksep
1494         $ctext insert end "    $f1l    " d0 "    $f2l    " d1
1495         $ctext insert end "    $rest \n" hunksep
1496     } else {
1497         set x [string range $line 0 0]
1498         if {$x == "-" || $x == "+"} {
1499             set tag [expr {$x == "+"}]
1500             set line [string range $line 1 end]
1501             $ctext insert end "$line\n" d$tag
1502         } elseif {$x == " "} {
1503             set line [string range $line 1 end]
1504             $ctext insert end "$line\n"
1505         } elseif {$x == "\\"} {
1506             # e.g. "\ No newline at end of file"
1507             $ctext insert end "$line\n" filesep
1508         } else {
1509             # Something else we don't recognize
1510             if {$curdifftag != "Comments"} {
1511                 $ctext insert end "\n"
1512                 $ctext tag add $curdifftag $curtagstart end
1513                 set seenfile($curdifftag) 1
1514                 set curtagstart [$ctext index "end - 1c"]
1515                 set curdifftag Comments
1516             }
1517             $ctext insert end "$line\n" filesep
1518         }
1519     }
1520     $ctext conf -state disabled
1521     if {[clock clicks -milliseconds] >= $nextupdate} {
1522         incr nextupdate 100
1523         fileevent $bdf readable {}
1524         update
1525         fileevent $bdf readable "getblobdiffline $bdf {$ids}"
1526     }
1527 }
1528
1529 proc nextfile {} {
1530     global difffilestart ctext
1531     set here [$ctext index @0,0]
1532     for {set i 0} {[info exists difffilestart($i)]} {incr i} {
1533         if {[$ctext compare $difffilestart($i) > $here]} {
1534             $ctext yview $difffilestart($i)
1535             break
1536         }
1537     }
1538 }
1539
1540 proc listboxsel {} {
1541     global ctext cflist currentid treediffs seenfile
1542     if {![info exists currentid]} return
1543     set sel [lsort [$cflist curselection]]
1544     if {$sel eq {}} return
1545     set first [lindex $sel 0]
1546     catch {$ctext yview fmark.$first}
1547 }
1548
1549 proc setcoords {} {
1550     global linespc charspc canvx0 canvy0 mainfont
1551     set linespc [font metrics $mainfont -linespace]
1552     set charspc [font measure $mainfont "m"]
1553     set canvy0 [expr 3 + 0.5 * $linespc]
1554     set canvx0 [expr 3 + 0.5 * $linespc]
1555 }
1556
1557 proc redisplay {} {
1558     global selectedline stopped redisplaying phase
1559     if {$stopped > 1} return
1560     if {$phase == "getcommits"} return
1561     set redisplaying 1
1562     if {$phase == "drawgraph" || $phase == "incrdraw"} {
1563         set stopped 1
1564     } else {
1565         drawgraph
1566     }
1567 }
1568
1569 proc incrfont {inc} {
1570     global mainfont namefont textfont selectedline ctext canv phase
1571     global stopped entries
1572     unmarkmatches
1573     set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
1574     set namefont [lreplace $namefont 1 1 [expr {[lindex $namefont 1] + $inc}]]
1575     set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
1576     setcoords
1577     $ctext conf -font $textfont
1578     $ctext tag conf filesep -font [concat $textfont bold]
1579     foreach e $entries {
1580         $e conf -font $mainfont
1581     }
1582     if {$phase == "getcommits"} {
1583         $canv itemconf textitems -font $mainfont
1584     }
1585     redisplay
1586 }
1587
1588 proc clearsha1 {} {
1589     global sha1entry sha1string
1590     if {[string length $sha1string] == 40} {
1591         $sha1entry delete 0 end
1592     }
1593 }
1594
1595 proc sha1change {n1 n2 op} {
1596     global sha1string currentid sha1but
1597     if {$sha1string == {}
1598         || ([info exists currentid] && $sha1string == $currentid)} {
1599         set state disabled
1600     } else {
1601         set state normal
1602     }
1603     if {[$sha1but cget -state] == $state} return
1604     if {$state == "normal"} {
1605         $sha1but conf -state normal -relief raised -text "Goto: "
1606     } else {
1607         $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
1608     }
1609 }
1610
1611 proc gotocommit {} {
1612     global sha1string currentid idline tagids
1613     if {$sha1string == {}
1614         || ([info exists currentid] && $sha1string == $currentid)} return
1615     if {[info exists tagids($sha1string)]} {
1616         set id $tagids($sha1string)
1617     } else {
1618         set id [string tolower $sha1string]
1619     }
1620     if {[info exists idline($id)]} {
1621         selectline $idline($id)
1622         return
1623     }
1624     if {[regexp {^[0-9a-fA-F]{40}$} $sha1string]} {
1625         set type "SHA1 id"
1626     } else {
1627         set type "Tag"
1628     }
1629     error_popup "$type $sha1string is not known"
1630 }
1631
1632 proc lineenter {x y id} {
1633     global hoverx hovery hoverid hovertimer
1634     global commitinfo canv
1635
1636     if {![info exists commitinfo($id)]} return
1637     set hoverx $x
1638     set hovery $y
1639     set hoverid $id
1640     if {[info exists hovertimer]} {
1641         after cancel $hovertimer
1642     }
1643     set hovertimer [after 500 linehover]
1644     $canv delete hover
1645 }
1646
1647 proc linemotion {x y id} {
1648     global hoverx hovery hoverid hovertimer
1649
1650     if {[info exists hoverid] && $id == $hoverid} {
1651         set hoverx $x
1652         set hovery $y
1653         if {[info exists hovertimer]} {
1654             after cancel $hovertimer
1655         }
1656         set hovertimer [after 500 linehover]
1657     }
1658 }
1659
1660 proc lineleave {id} {
1661     global hoverid hovertimer canv
1662
1663     if {[info exists hoverid] && $id == $hoverid} {
1664         $canv delete hover
1665         if {[info exists hovertimer]} {
1666             after cancel $hovertimer
1667             unset hovertimer
1668         }
1669         unset hoverid
1670     }
1671 }
1672
1673 proc linehover {} {
1674     global hoverx hovery hoverid hovertimer
1675     global canv linespc lthickness
1676     global commitinfo mainfont
1677
1678     set text [lindex $commitinfo($hoverid) 0]
1679     set ymax [lindex [$canv cget -scrollregion] 3]
1680     if {$ymax == {}} return
1681     set yfrac [lindex [$canv yview] 0]
1682     set x [expr {$hoverx + 2 * $linespc}]
1683     set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
1684     set x0 [expr {$x - 2 * $lthickness}]
1685     set y0 [expr {$y - 2 * $lthickness}]
1686     set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}]
1687     set y1 [expr {$y + $linespc + 2 * $lthickness}]
1688     set t [$canv create rectangle $x0 $y0 $x1 $y1 \
1689                -fill \#ffff80 -outline black -width 1 -tags hover]
1690     $canv raise $t
1691     set t [$canv create text $x $y -anchor nw -text $text -tags hover]
1692     $canv raise $t
1693 }
1694
1695 proc lineclick {x y id} {
1696     global ctext commitinfo children cflist canv
1697
1698     unmarkmatches
1699     $canv delete hover
1700     # fill the details pane with info about this line
1701     $ctext conf -state normal
1702     $ctext delete 0.0 end
1703     $ctext insert end "Parent:\n "
1704     catch {destroy $ctext.$id}
1705     button $ctext.$id -text "Go:" -command "selbyid $id" \
1706         -padx 4 -pady 0
1707     $ctext window create end -window $ctext.$id -align center
1708     set info $commitinfo($id)
1709     $ctext insert end "\t[lindex $info 0]\n"
1710     $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
1711     $ctext insert end "\tDate:\t[lindex $info 2]\n"
1712     $ctext insert end "\tID:\t$id\n"
1713     if {[info exists children($id)]} {
1714         $ctext insert end "\nChildren:"
1715         foreach child $children($id) {
1716             $ctext insert end "\n "
1717             catch {destroy $ctext.$child}
1718             button $ctext.$child -text "Go:" -command "selbyid $child" \
1719                 -padx 4 -pady 0
1720             $ctext window create end -window $ctext.$child -align center
1721             set info $commitinfo($child)
1722             $ctext insert end "\t[lindex $info 0]"
1723         }
1724     }
1725     $ctext conf -state disabled
1726
1727     $cflist delete 0 end
1728 }
1729
1730 proc selbyid {id} {
1731     global idline
1732     if {[info exists idline($id)]} {
1733         selectline $idline($id)
1734     }
1735 }
1736
1737 proc mstime {} {
1738     global startmstime
1739     if {![info exists startmstime]} {
1740         set startmstime [clock clicks -milliseconds]
1741     }
1742     return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
1743 }
1744
1745 proc rowmenu {x y id} {
1746     global rowctxmenu idline selectedline rowmenuid
1747
1748     if {![info exists selectedline] || $idline($id) eq $selectedline} {
1749         set state disabled
1750     } else {
1751         set state normal
1752     }
1753     $rowctxmenu entryconfigure 0 -state $state
1754     $rowctxmenu entryconfigure 1 -state $state
1755     $rowctxmenu entryconfigure 2 -state $state
1756     set rowmenuid $id
1757     tk_popup $rowctxmenu $x $y
1758 }
1759
1760 proc diffvssel {dirn} {
1761     global rowmenuid selectedline lineid
1762     global ctext cflist
1763     global diffids commitinfo
1764
1765     if {![info exists selectedline]} return
1766     if {$dirn} {
1767         set oldid $lineid($selectedline)
1768         set newid $rowmenuid
1769     } else {
1770         set oldid $rowmenuid
1771         set newid $lineid($selectedline)
1772     }
1773     $ctext conf -state normal
1774     $ctext delete 0.0 end
1775     $ctext mark set fmark.0 0.0
1776     $ctext mark gravity fmark.0 left
1777     $cflist delete 0 end
1778     $cflist insert end "Top"
1779     $ctext insert end "From $oldid\n     "
1780     $ctext insert end [lindex $commitinfo($oldid) 0]
1781     $ctext insert end "\n\nTo   $newid\n     "
1782     $ctext insert end [lindex $commitinfo($newid) 0]
1783     $ctext insert end "\n"
1784     $ctext conf -state disabled
1785     $ctext tag delete Comments
1786     $ctext tag remove found 1.0 end
1787     set diffids [list $newid $oldid]
1788     startdiff
1789 }
1790
1791 proc mkpatch {} {
1792     global rowmenuid currentid commitinfo patchtop patchnum
1793
1794     if {![info exists currentid]} return
1795     set oldid $currentid
1796     set oldhead [lindex $commitinfo($oldid) 0]
1797     set newid $rowmenuid
1798     set newhead [lindex $commitinfo($newid) 0]
1799     set top .patch
1800     set patchtop $top
1801     catch {destroy $top}
1802     toplevel $top
1803     label $top.title -text "Generate patch"
1804     grid $top.title -
1805     label $top.from -text "From:"
1806     entry $top.fromsha1 -width 40
1807     $top.fromsha1 insert 0 $oldid
1808     $top.fromsha1 conf -state readonly
1809     grid $top.from $top.fromsha1 -sticky w
1810     entry $top.fromhead -width 60
1811     $top.fromhead insert 0 $oldhead
1812     $top.fromhead conf -state readonly
1813     grid x $top.fromhead -sticky w
1814     label $top.to -text "To:"
1815     entry $top.tosha1 -width 40
1816     $top.tosha1 insert 0 $newid
1817     $top.tosha1 conf -state readonly
1818     grid $top.to $top.tosha1 -sticky w
1819     entry $top.tohead -width 60
1820     $top.tohead insert 0 $newhead
1821     $top.tohead conf -state readonly
1822     grid x $top.tohead -sticky w
1823     button $top.rev -text "Reverse" -command mkpatchrev -padx 5
1824     grid $top.rev x -pady 10
1825     label $top.flab -text "Output file:"
1826     entry $top.fname -width 60
1827     $top.fname insert 0 [file normalize "patch$patchnum.patch"]
1828     incr patchnum
1829     grid $top.flab $top.fname
1830     frame $top.buts
1831     button $top.buts.gen -text "Generate" -command mkpatchgo
1832     button $top.buts.can -text "Cancel" -command mkpatchcan
1833     grid $top.buts.gen $top.buts.can
1834     grid columnconfigure $top.buts 0 -weight 1 -uniform a
1835     grid columnconfigure $top.buts 1 -weight 1 -uniform a
1836     grid $top.buts - -pady 10 -sticky ew
1837 }
1838
1839 proc mkpatchrev {} {
1840     global patchtop
1841
1842     set oldid [$patchtop.fromsha1 get]
1843     set oldhead [$patchtop.fromhead get]
1844     set newid [$patchtop.tosha1 get]
1845     set newhead [$patchtop.tohead get]
1846     foreach e [list fromsha1 fromhead tosha1 tohead] \
1847             v [list $newid $newhead $oldid $oldhead] {
1848         $patchtop.$e conf -state normal
1849         $patchtop.$e delete 0 end
1850         $patchtop.$e insert 0 $v
1851         $patchtop.$e conf -state readonly
1852     }
1853 }
1854
1855 proc mkpatchgo {} {
1856     global patchtop
1857
1858     set oldid [$patchtop.fromsha1 get]
1859     set newid [$patchtop.tosha1 get]
1860     set fname [$patchtop.fname get]
1861     if {[catch {exec git-diff-tree -p $oldid $newid >$fname &} err]} {
1862         error_popup "Error creating patch: $err"
1863     }
1864     catch {destroy $patchtop}
1865     unset patchtop
1866 }
1867
1868 proc mkpatchcan {} {
1869     global patchtop
1870
1871     catch {destroy $patchtop}
1872     unset patchtop
1873 }
1874
1875 proc doquit {} {
1876     global stopped
1877     set stopped 100
1878     destroy .
1879 }
1880
1881 # defaults...
1882 set datemode 0
1883 set boldnames 0
1884 set diffopts "-U 5 -p"
1885
1886 set mainfont {Helvetica 9}
1887 set textfont {Courier 9}
1888
1889 set colors {green red blue magenta darkgrey brown orange}
1890
1891 catch {source ~/.gitk}
1892
1893 set namefont $mainfont
1894 if {$boldnames} {
1895     lappend namefont bold
1896 }
1897
1898 set revtreeargs {}
1899 foreach arg $argv {
1900     switch -regexp -- $arg {
1901         "^$" { }
1902         "^-b" { set boldnames 1 }
1903         "^-d" { set datemode 1 }
1904         default {
1905             lappend revtreeargs $arg
1906         }
1907     }
1908 }
1909
1910 set stopped 0
1911 set redisplaying 0
1912 set stuffsaved 0
1913 set patchnum 0
1914 setcoords
1915 makewindow
1916 readrefs
1917 getcommits $revtreeargs