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