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