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