Isolate shared HTTP request functionality
[git/spearce.git] / gitk
blob95b05c02b4cb65aa4b09f651ba2b94ebbfdd02a3
1 #!/bin/sh
2 # Tcl ignores the next line -*- tcl -*- \
3 exec wish "$0" -- "$@"
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 --parents $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 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 set j [string first "\n" $cmit]
100 set ok 0
101 if {$j >= 0} {
102 set ids [string range $cmit 0 [expr {$j - 1}]]
103 set ok 1
104 foreach id $ids {
105 if {![regexp {^[0-9a-f]{40}$} $id]} {
106 set ok 0
107 break
111 if {!$ok} {
112 set shortcmit $cmit
113 if {[string length $shortcmit] > 80} {
114 set shortcmit "[string range $shortcmit 0 80]..."
116 error_popup "Can't parse git-rev-list output: {$shortcmit}"
117 exit 1
119 set id [lindex $ids 0]
120 set olds [lrange $ids 1 end]
121 set cmit [string range $cmit [expr {$j + 1}] end]
122 lappend commits $id
123 set commitlisted($id) 1
124 parsecommit $id $cmit 1 [lrange $ids 1 end]
125 drawcommit $id
126 if {[clock clicks -milliseconds] >= $nextupdate} {
127 doupdate 1
129 while {$redisplaying} {
130 set redisplaying 0
131 if {$stopped == 1} {
132 set stopped 0
133 set phase "getcommits"
134 foreach id $commits {
135 drawcommit $id
136 if {$stopped} break
137 if {[clock clicks -milliseconds] >= $nextupdate} {
138 doupdate 1
146 proc doupdate {reading} {
147 global commfd nextupdate numcommits ncmupdate
149 if {$reading} {
150 fileevent $commfd readable {}
152 update
153 set nextupdate [expr {[clock clicks -milliseconds] + 100}]
154 if {$numcommits < 100} {
155 set ncmupdate [expr {$numcommits + 1}]
156 } elseif {$numcommits < 10000} {
157 set ncmupdate [expr {$numcommits + 10}]
158 } else {
159 set ncmupdate [expr {$numcommits + 100}]
161 if {$reading} {
162 fileevent $commfd readable [list getcommitlines $commfd]
166 proc readcommit {id} {
167 if [catch {set contents [exec git-cat-file commit $id]}] return
168 parsecommit $id $contents 0 {}
171 proc parsecommit {id contents listed olds} {
172 global commitinfo children nchildren parents nparents cdate ncleft
174 set inhdr 1
175 set comment {}
176 set headline {}
177 set auname {}
178 set audate {}
179 set comname {}
180 set comdate {}
181 if {![info exists nchildren($id)]} {
182 set children($id) {}
183 set nchildren($id) 0
184 set ncleft($id) 0
186 set parents($id) $olds
187 set nparents($id) [llength $olds]
188 foreach p $olds {
189 if {![info exists nchildren($p)]} {
190 set children($p) [list $id]
191 set nchildren($p) 1
192 set ncleft($p) 1
193 } elseif {[lsearch -exact $children($p) $id] < 0} {
194 lappend children($p) $id
195 incr nchildren($p)
196 incr ncleft($p)
199 set hdrend [string first "\n\n" $contents]
200 if {$hdrend < 0} {
201 # should never happen...
202 set hdrend [string length $contents]
204 set header [string range $contents 0 [expr {$hdrend - 1}]]
205 set comment [string range $contents [expr {$hdrend + 2}] end]
206 foreach line [split $header "\n"] {
207 set tag [lindex $line 0]
208 if {$tag == "author"} {
209 set audate [lindex $line end-1]
210 set auname [lrange $line 1 end-2]
211 } elseif {$tag == "committer"} {
212 set comdate [lindex $line end-1]
213 set comname [lrange $line 1 end-2]
216 set headline {}
217 # take the first line of the comment as the headline
218 set i [string first "\n" $comment]
219 if {$i >= 0} {
220 set headline [string trim [string range $comment 0 $i]]
222 if {!$listed} {
223 # git-rev-list indents the comment by 4 spaces;
224 # if we got this via git-cat-file, add the indentation
225 set newcomment {}
226 foreach line [split $comment "\n"] {
227 append newcomment " "
228 append newcomment $line
230 set comment $newcomment
232 if {$comdate != {}} {
233 set cdate($id) $comdate
235 set commitinfo($id) [list $headline $auname $audate \
236 $comname $comdate $comment]
239 proc readrefs {} {
240 global tagids idtags headids idheads tagcontents
242 set tags [glob -nocomplain -types f [gitdir]/refs/tags/*]
243 foreach f $tags {
244 catch {
245 set fd [open $f r]
246 set line [read $fd]
247 if {[regexp {^[0-9a-f]{40}} $line id]} {
248 set direct [file tail $f]
249 set tagids($direct) $id
250 lappend idtags($id) $direct
251 set tagblob [exec git-cat-file tag $id]
252 set contents [split $tagblob "\n"]
253 set obj {}
254 set type {}
255 set tag {}
256 foreach l $contents {
257 if {$l == {}} break
258 switch -- [lindex $l 0] {
259 "object" {set obj [lindex $l 1]}
260 "type" {set type [lindex $l 1]}
261 "tag" {set tag [string range $l 4 end]}
264 if {$obj != {} && $type == "commit" && $tag != {}} {
265 set tagids($tag) $obj
266 lappend idtags($obj) $tag
267 set tagcontents($tag) $tagblob
270 close $fd
273 set heads [glob -nocomplain -types f [gitdir]/refs/heads/*]
274 foreach f $heads {
275 catch {
276 set fd [open $f r]
277 set line [read $fd 40]
278 if {[regexp {^[0-9a-f]{40}} $line id]} {
279 set head [file tail $f]
280 set headids($head) $line
281 lappend idheads($line) $head
283 close $fd
286 readotherrefs refs {} {tags heads}
289 proc readotherrefs {base dname excl} {
290 global otherrefids idotherrefs
292 set git [gitdir]
293 set files [glob -nocomplain -types f [file join $git $base *]]
294 foreach f $files {
295 catch {
296 set fd [open $f r]
297 set line [read $fd 40]
298 if {[regexp {^[0-9a-f]{40}} $line id]} {
299 set name "$dname[file tail $f]"
300 set otherrefids($name) $id
301 lappend idotherrefs($id) $name
303 close $fd
306 set dirs [glob -nocomplain -types d [file join $git $base *]]
307 foreach d $dirs {
308 set dir [file tail $d]
309 if {[lsearch -exact $excl $dir] >= 0} continue
310 readotherrefs [file join $base $dir] "$dname$dir/" {}
314 proc error_popup msg {
315 set w .error
316 toplevel $w
317 wm transient $w .
318 message $w.m -text $msg -justify center -aspect 400
319 pack $w.m -side top -fill x -padx 20 -pady 20
320 button $w.ok -text OK -command "destroy $w"
321 pack $w.ok -side bottom -fill x
322 bind $w <Visibility> "grab $w; focus $w"
323 tkwait window $w
326 proc makewindow {} {
327 global canv canv2 canv3 linespc charspc ctext cflist textfont
328 global findtype findtypemenu findloc findstring fstring geometry
329 global entries sha1entry sha1string sha1but
330 global maincursor textcursor curtextcursor
331 global rowctxmenu gaudydiff mergemax
333 menu .bar
334 .bar add cascade -label "File" -menu .bar.file
335 menu .bar.file
336 .bar.file add command -label "Reread references" -command rereadrefs
337 .bar.file add command -label "Quit" -command doquit
338 menu .bar.help
339 .bar add cascade -label "Help" -menu .bar.help
340 .bar.help add command -label "About gitk" -command about
341 . configure -menu .bar
343 if {![info exists geometry(canv1)]} {
344 set geometry(canv1) [expr 45 * $charspc]
345 set geometry(canv2) [expr 30 * $charspc]
346 set geometry(canv3) [expr 15 * $charspc]
347 set geometry(canvh) [expr 25 * $linespc + 4]
348 set geometry(ctextw) 80
349 set geometry(ctexth) 30
350 set geometry(cflistw) 30
352 panedwindow .ctop -orient vertical
353 if {[info exists geometry(width)]} {
354 .ctop conf -width $geometry(width) -height $geometry(height)
355 set texth [expr {$geometry(height) - $geometry(canvh) - 56}]
356 set geometry(ctexth) [expr {($texth - 8) /
357 [font metrics $textfont -linespace]}]
359 frame .ctop.top
360 frame .ctop.top.bar
361 pack .ctop.top.bar -side bottom -fill x
362 set cscroll .ctop.top.csb
363 scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
364 pack $cscroll -side right -fill y
365 panedwindow .ctop.top.clist -orient horizontal -sashpad 0 -handlesize 4
366 pack .ctop.top.clist -side top -fill both -expand 1
367 .ctop add .ctop.top
368 set canv .ctop.top.clist.canv
369 canvas $canv -height $geometry(canvh) -width $geometry(canv1) \
370 -bg white -bd 0 \
371 -yscrollincr $linespc -yscrollcommand "$cscroll set"
372 .ctop.top.clist add $canv
373 set canv2 .ctop.top.clist.canv2
374 canvas $canv2 -height $geometry(canvh) -width $geometry(canv2) \
375 -bg white -bd 0 -yscrollincr $linespc
376 .ctop.top.clist add $canv2
377 set canv3 .ctop.top.clist.canv3
378 canvas $canv3 -height $geometry(canvh) -width $geometry(canv3) \
379 -bg white -bd 0 -yscrollincr $linespc
380 .ctop.top.clist add $canv3
381 bind .ctop.top.clist <Configure> {resizeclistpanes %W %w}
383 set sha1entry .ctop.top.bar.sha1
384 set entries $sha1entry
385 set sha1but .ctop.top.bar.sha1label
386 button $sha1but -text "SHA1 ID: " -state disabled -relief flat \
387 -command gotocommit -width 8
388 $sha1but conf -disabledforeground [$sha1but cget -foreground]
389 pack .ctop.top.bar.sha1label -side left
390 entry $sha1entry -width 40 -font $textfont -textvariable sha1string
391 trace add variable sha1string write sha1change
392 pack $sha1entry -side left -pady 2
394 image create bitmap bm-left -data {
395 #define left_width 16
396 #define left_height 16
397 static unsigned char left_bits[] = {
398 0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
399 0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
400 0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
402 image create bitmap bm-right -data {
403 #define right_width 16
404 #define right_height 16
405 static unsigned char right_bits[] = {
406 0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
407 0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
408 0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
410 button .ctop.top.bar.leftbut -image bm-left -command goback \
411 -state disabled -width 26
412 pack .ctop.top.bar.leftbut -side left -fill y
413 button .ctop.top.bar.rightbut -image bm-right -command goforw \
414 -state disabled -width 26
415 pack .ctop.top.bar.rightbut -side left -fill y
417 button .ctop.top.bar.findbut -text "Find" -command dofind
418 pack .ctop.top.bar.findbut -side left
419 set findstring {}
420 set fstring .ctop.top.bar.findstring
421 lappend entries $fstring
422 entry $fstring -width 30 -font $textfont -textvariable findstring
423 pack $fstring -side left -expand 1 -fill x
424 set findtype Exact
425 set findtypemenu [tk_optionMenu .ctop.top.bar.findtype \
426 findtype Exact IgnCase Regexp]
427 set findloc "All fields"
428 tk_optionMenu .ctop.top.bar.findloc findloc "All fields" Headline \
429 Comments Author Committer Files Pickaxe
430 pack .ctop.top.bar.findloc -side right
431 pack .ctop.top.bar.findtype -side right
432 # for making sure type==Exact whenever loc==Pickaxe
433 trace add variable findloc write findlocchange
435 panedwindow .ctop.cdet -orient horizontal
436 .ctop add .ctop.cdet
437 frame .ctop.cdet.left
438 set ctext .ctop.cdet.left.ctext
439 text $ctext -bg white -state disabled -font $textfont \
440 -width $geometry(ctextw) -height $geometry(ctexth) \
441 -yscrollcommand ".ctop.cdet.left.sb set" -wrap none
442 scrollbar .ctop.cdet.left.sb -command "$ctext yview"
443 pack .ctop.cdet.left.sb -side right -fill y
444 pack $ctext -side left -fill both -expand 1
445 .ctop.cdet add .ctop.cdet.left
447 $ctext tag conf filesep -font [concat $textfont bold] -back "#aaaaaa"
448 if {$gaudydiff} {
449 $ctext tag conf hunksep -back blue -fore white
450 $ctext tag conf d0 -back "#ff8080"
451 $ctext tag conf d1 -back green
452 } else {
453 $ctext tag conf hunksep -fore blue
454 $ctext tag conf d0 -fore red
455 $ctext tag conf d1 -fore "#00a000"
456 $ctext tag conf m0 -fore red
457 $ctext tag conf m1 -fore blue
458 $ctext tag conf m2 -fore green
459 $ctext tag conf m3 -fore purple
460 $ctext tag conf m4 -fore brown
461 $ctext tag conf mmax -fore darkgrey
462 set mergemax 5
463 $ctext tag conf mresult -font [concat $textfont bold]
464 $ctext tag conf msep -font [concat $textfont bold]
465 $ctext tag conf found -back yellow
468 frame .ctop.cdet.right
469 set cflist .ctop.cdet.right.cfiles
470 listbox $cflist -bg white -selectmode extended -width $geometry(cflistw) \
471 -yscrollcommand ".ctop.cdet.right.sb set"
472 scrollbar .ctop.cdet.right.sb -command "$cflist yview"
473 pack .ctop.cdet.right.sb -side right -fill y
474 pack $cflist -side left -fill both -expand 1
475 .ctop.cdet add .ctop.cdet.right
476 bind .ctop.cdet <Configure> {resizecdetpanes %W %w}
478 pack .ctop -side top -fill both -expand 1
480 bindall <1> {selcanvline %W %x %y}
481 #bindall <B1-Motion> {selcanvline %W %x %y}
482 bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
483 bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
484 bindall <2> "allcanvs scan mark 0 %y"
485 bindall <B2-Motion> "allcanvs scan dragto 0 %y"
486 bind . <Key-Up> "selnextline -1"
487 bind . <Key-Down> "selnextline 1"
488 bind . <Key-Right> "goforw"
489 bind . <Key-Left> "goback"
490 bind . <Key-Prior> "allcanvs yview scroll -1 pages"
491 bind . <Key-Next> "allcanvs yview scroll 1 pages"
492 bindkey <Key-Delete> "$ctext yview scroll -1 pages"
493 bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
494 bindkey <Key-space> "$ctext yview scroll 1 pages"
495 bindkey p "selnextline -1"
496 bindkey n "selnextline 1"
497 bindkey z "goback"
498 bindkey x "goforw"
499 bindkey i "selnextline -1"
500 bindkey k "selnextline 1"
501 bindkey j "goback"
502 bindkey l "goforw"
503 bindkey b "$ctext yview scroll -1 pages"
504 bindkey d "$ctext yview scroll 18 units"
505 bindkey u "$ctext yview scroll -18 units"
506 bindkey / {findnext 1}
507 bindkey <Key-Return> {findnext 0}
508 bindkey ? findprev
509 bindkey f nextfile
510 bind . <Control-q> doquit
511 bind . <Control-f> dofind
512 bind . <Control-g> {findnext 0}
513 bind . <Control-r> findprev
514 bind . <Control-equal> {incrfont 1}
515 bind . <Control-KP_Add> {incrfont 1}
516 bind . <Control-minus> {incrfont -1}
517 bind . <Control-KP_Subtract> {incrfont -1}
518 bind $cflist <<ListboxSelect>> listboxsel
519 bind . <Destroy> {savestuff %W}
520 bind . <Button-1> "click %W"
521 bind $fstring <Key-Return> dofind
522 bind $sha1entry <Key-Return> gotocommit
523 bind $sha1entry <<PasteSelection>> clearsha1
525 set maincursor [. cget -cursor]
526 set textcursor [$ctext cget -cursor]
527 set curtextcursor $textcursor
529 set rowctxmenu .rowctxmenu
530 menu $rowctxmenu -tearoff 0
531 $rowctxmenu add command -label "Diff this -> selected" \
532 -command {diffvssel 0}
533 $rowctxmenu add command -label "Diff selected -> this" \
534 -command {diffvssel 1}
535 $rowctxmenu add command -label "Make patch" -command mkpatch
536 $rowctxmenu add command -label "Create tag" -command mktag
537 $rowctxmenu add command -label "Write commit to file" -command writecommit
540 # when we make a key binding for the toplevel, make sure
541 # it doesn't get triggered when that key is pressed in the
542 # find string entry widget.
543 proc bindkey {ev script} {
544 global entries
545 bind . $ev $script
546 set escript [bind Entry $ev]
547 if {$escript == {}} {
548 set escript [bind Entry <Key>]
550 foreach e $entries {
551 bind $e $ev "$escript; break"
555 # set the focus back to the toplevel for any click outside
556 # the entry widgets
557 proc click {w} {
558 global entries
559 foreach e $entries {
560 if {$w == $e} return
562 focus .
565 proc savestuff {w} {
566 global canv canv2 canv3 ctext cflist mainfont textfont
567 global stuffsaved findmergefiles gaudydiff maxgraphpct
568 global maxwidth
570 if {$stuffsaved} return
571 if {![winfo viewable .]} return
572 catch {
573 set f [open "~/.gitk-new" w]
574 puts $f [list set mainfont $mainfont]
575 puts $f [list set textfont $textfont]
576 puts $f [list set findmergefiles $findmergefiles]
577 puts $f [list set gaudydiff $gaudydiff]
578 puts $f [list set maxgraphpct $maxgraphpct]
579 puts $f [list set maxwidth $maxwidth]
580 puts $f "set geometry(width) [winfo width .ctop]"
581 puts $f "set geometry(height) [winfo height .ctop]"
582 puts $f "set geometry(canv1) [expr [winfo width $canv]-2]"
583 puts $f "set geometry(canv2) [expr [winfo width $canv2]-2]"
584 puts $f "set geometry(canv3) [expr [winfo width $canv3]-2]"
585 puts $f "set geometry(canvh) [expr [winfo height $canv]-2]"
586 set wid [expr {([winfo width $ctext] - 8) \
587 / [font measure $textfont "0"]}]
588 puts $f "set geometry(ctextw) $wid"
589 set wid [expr {([winfo width $cflist] - 11) \
590 / [font measure [$cflist cget -font] "0"]}]
591 puts $f "set geometry(cflistw) $wid"
592 close $f
593 file rename -force "~/.gitk-new" "~/.gitk"
595 set stuffsaved 1
598 proc resizeclistpanes {win w} {
599 global oldwidth
600 if [info exists oldwidth($win)] {
601 set s0 [$win sash coord 0]
602 set s1 [$win sash coord 1]
603 if {$w < 60} {
604 set sash0 [expr {int($w/2 - 2)}]
605 set sash1 [expr {int($w*5/6 - 2)}]
606 } else {
607 set factor [expr {1.0 * $w / $oldwidth($win)}]
608 set sash0 [expr {int($factor * [lindex $s0 0])}]
609 set sash1 [expr {int($factor * [lindex $s1 0])}]
610 if {$sash0 < 30} {
611 set sash0 30
613 if {$sash1 < $sash0 + 20} {
614 set sash1 [expr $sash0 + 20]
616 if {$sash1 > $w - 10} {
617 set sash1 [expr $w - 10]
618 if {$sash0 > $sash1 - 20} {
619 set sash0 [expr $sash1 - 20]
623 $win sash place 0 $sash0 [lindex $s0 1]
624 $win sash place 1 $sash1 [lindex $s1 1]
626 set oldwidth($win) $w
629 proc resizecdetpanes {win w} {
630 global oldwidth
631 if [info exists oldwidth($win)] {
632 set s0 [$win sash coord 0]
633 if {$w < 60} {
634 set sash0 [expr {int($w*3/4 - 2)}]
635 } else {
636 set factor [expr {1.0 * $w / $oldwidth($win)}]
637 set sash0 [expr {int($factor * [lindex $s0 0])}]
638 if {$sash0 < 45} {
639 set sash0 45
641 if {$sash0 > $w - 15} {
642 set sash0 [expr $w - 15]
645 $win sash place 0 $sash0 [lindex $s0 1]
647 set oldwidth($win) $w
650 proc allcanvs args {
651 global canv canv2 canv3
652 eval $canv $args
653 eval $canv2 $args
654 eval $canv3 $args
657 proc bindall {event action} {
658 global canv canv2 canv3
659 bind $canv $event $action
660 bind $canv2 $event $action
661 bind $canv3 $event $action
664 proc about {} {
665 set w .about
666 if {[winfo exists $w]} {
667 raise $w
668 return
670 toplevel $w
671 wm title $w "About gitk"
672 message $w.m -text {
673 Gitk version 1.2
675 Copyright © 2005 Paul Mackerras
677 Use and redistribute under the terms of the GNU General Public License} \
678 -justify center -aspect 400
679 pack $w.m -side top -fill x -padx 20 -pady 20
680 button $w.ok -text Close -command "destroy $w"
681 pack $w.ok -side bottom
684 proc assigncolor {id} {
685 global colormap commcolors colors nextcolor
686 global parents nparents children nchildren
687 global cornercrossings crossings
689 if [info exists colormap($id)] return
690 set ncolors [llength $colors]
691 if {$nparents($id) <= 1 && $nchildren($id) == 1} {
692 set child [lindex $children($id) 0]
693 if {[info exists colormap($child)]
694 && $nparents($child) == 1} {
695 set colormap($id) $colormap($child)
696 return
699 set badcolors {}
700 if {[info exists cornercrossings($id)]} {
701 foreach x $cornercrossings($id) {
702 if {[info exists colormap($x)]
703 && [lsearch -exact $badcolors $colormap($x)] < 0} {
704 lappend badcolors $colormap($x)
707 if {[llength $badcolors] >= $ncolors} {
708 set badcolors {}
711 set origbad $badcolors
712 if {[llength $badcolors] < $ncolors - 1} {
713 if {[info exists crossings($id)]} {
714 foreach x $crossings($id) {
715 if {[info exists colormap($x)]
716 && [lsearch -exact $badcolors $colormap($x)] < 0} {
717 lappend badcolors $colormap($x)
720 if {[llength $badcolors] >= $ncolors} {
721 set badcolors $origbad
724 set origbad $badcolors
726 if {[llength $badcolors] < $ncolors - 1} {
727 foreach child $children($id) {
728 if {[info exists colormap($child)]
729 && [lsearch -exact $badcolors $colormap($child)] < 0} {
730 lappend badcolors $colormap($child)
732 if {[info exists parents($child)]} {
733 foreach p $parents($child) {
734 if {[info exists colormap($p)]
735 && [lsearch -exact $badcolors $colormap($p)] < 0} {
736 lappend badcolors $colormap($p)
741 if {[llength $badcolors] >= $ncolors} {
742 set badcolors $origbad
745 for {set i 0} {$i <= $ncolors} {incr i} {
746 set c [lindex $colors $nextcolor]
747 if {[incr nextcolor] >= $ncolors} {
748 set nextcolor 0
750 if {[lsearch -exact $badcolors $c]} break
752 set colormap($id) $c
755 proc initgraph {} {
756 global canvy canvy0 lineno numcommits nextcolor linespc
757 global mainline mainlinearrow sidelines
758 global nchildren ncleft
759 global displist nhyperspace
761 allcanvs delete all
762 set nextcolor 0
763 set canvy $canvy0
764 set lineno -1
765 set numcommits 0
766 catch {unset mainline}
767 catch {unset mainlinearrow}
768 catch {unset sidelines}
769 foreach id [array names nchildren] {
770 set ncleft($id) $nchildren($id)
772 set displist {}
773 set nhyperspace 0
776 proc bindline {t id} {
777 global canv
779 $canv bind $t <Enter> "lineenter %x %y $id"
780 $canv bind $t <Motion> "linemotion %x %y $id"
781 $canv bind $t <Leave> "lineleave $id"
782 $canv bind $t <Button-1> "lineclick %x %y $id 1"
785 proc drawlines {id xtra delold} {
786 global mainline mainlinearrow sidelines lthickness colormap canv
788 if {$delold} {
789 $canv delete lines.$id
791 if {[info exists mainline($id)]} {
792 set t [$canv create line $mainline($id) \
793 -width [expr {($xtra + 1) * $lthickness}] \
794 -fill $colormap($id) -tags lines.$id \
795 -arrow $mainlinearrow($id)]
796 $canv lower $t
797 bindline $t $id
799 if {[info exists sidelines($id)]} {
800 foreach ls $sidelines($id) {
801 set coords [lindex $ls 0]
802 set thick [lindex $ls 1]
803 set arrow [lindex $ls 2]
804 set t [$canv create line $coords -fill $colormap($id) \
805 -width [expr {($thick + $xtra) * $lthickness}] \
806 -arrow $arrow -tags lines.$id]
807 $canv lower $t
808 bindline $t $id
813 # level here is an index in displist
814 proc drawcommitline {level} {
815 global parents children nparents displist
816 global canv canv2 canv3 mainfont namefont canvy linespc
817 global lineid linehtag linentag linedtag commitinfo
818 global colormap numcommits currentparents dupparents
819 global idtags idline idheads idotherrefs
820 global lineno lthickness mainline mainlinearrow sidelines
821 global commitlisted rowtextx idpos lastuse displist
822 global oldnlines olddlevel olddisplist
824 incr numcommits
825 incr lineno
826 set id [lindex $displist $level]
827 set lastuse($id) $lineno
828 set lineid($lineno) $id
829 set idline($id) $lineno
830 set ofill [expr {[info exists commitlisted($id)]? "blue": "white"}]
831 if {![info exists commitinfo($id)]} {
832 readcommit $id
833 if {![info exists commitinfo($id)]} {
834 set commitinfo($id) {"No commit information available"}
835 set nparents($id) 0
838 assigncolor $id
839 set currentparents {}
840 set dupparents {}
841 if {[info exists commitlisted($id)] && [info exists parents($id)]} {
842 foreach p $parents($id) {
843 if {[lsearch -exact $currentparents $p] < 0} {
844 lappend currentparents $p
845 } else {
846 # remember that this parent was listed twice
847 lappend dupparents $p
851 set x [xcoord $level $level $lineno]
852 set y1 $canvy
853 set canvy [expr $canvy + $linespc]
854 allcanvs conf -scrollregion \
855 [list 0 0 0 [expr $y1 + 0.5 * $linespc + 2]]
856 if {[info exists mainline($id)]} {
857 lappend mainline($id) $x $y1
858 if {$mainlinearrow($id) ne "none"} {
859 set mainline($id) [trimdiagstart $mainline($id)]
862 drawlines $id 0 0
863 set orad [expr {$linespc / 3}]
864 set t [$canv create oval [expr $x - $orad] [expr $y1 - $orad] \
865 [expr $x + $orad - 1] [expr $y1 + $orad - 1] \
866 -fill $ofill -outline black -width 1]
867 $canv raise $t
868 $canv bind $t <1> {selcanvline {} %x %y}
869 set xt [xcoord [llength $displist] $level $lineno]
870 if {[llength $currentparents] > 2} {
871 set xt [expr {$xt + ([llength $currentparents] - 2) * $linespc}]
873 set rowtextx($lineno) $xt
874 set idpos($id) [list $x $xt $y1]
875 if {[info exists idtags($id)] || [info exists idheads($id)]
876 || [info exists idotherrefs($id)]} {
877 set xt [drawtags $id $x $xt $y1]
879 set headline [lindex $commitinfo($id) 0]
880 set name [lindex $commitinfo($id) 1]
881 set date [lindex $commitinfo($id) 2]
882 set date [formatdate $date]
883 set linehtag($lineno) [$canv create text $xt $y1 -anchor w \
884 -text $headline -font $mainfont ]
885 $canv bind $linehtag($lineno) <Button-3> "rowmenu %X %Y $id"
886 set linentag($lineno) [$canv2 create text 3 $y1 -anchor w \
887 -text $name -font $namefont]
888 set linedtag($lineno) [$canv3 create text 3 $y1 -anchor w \
889 -text $date -font $mainfont]
891 set olddlevel $level
892 set olddisplist $displist
893 set oldnlines [llength $displist]
896 proc drawtags {id x xt y1} {
897 global idtags idheads idotherrefs
898 global linespc lthickness
899 global canv mainfont idline rowtextx
901 set marks {}
902 set ntags 0
903 set nheads 0
904 if {[info exists idtags($id)]} {
905 set marks $idtags($id)
906 set ntags [llength $marks]
908 if {[info exists idheads($id)]} {
909 set marks [concat $marks $idheads($id)]
910 set nheads [llength $idheads($id)]
912 if {[info exists idotherrefs($id)]} {
913 set marks [concat $marks $idotherrefs($id)]
915 if {$marks eq {}} {
916 return $xt
919 set delta [expr {int(0.5 * ($linespc - $lthickness))}]
920 set yt [expr $y1 - 0.5 * $linespc]
921 set yb [expr $yt + $linespc - 1]
922 set xvals {}
923 set wvals {}
924 foreach tag $marks {
925 set wid [font measure $mainfont $tag]
926 lappend xvals $xt
927 lappend wvals $wid
928 set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
930 set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
931 -width $lthickness -fill black -tags tag.$id]
932 $canv lower $t
933 foreach tag $marks x $xvals wid $wvals {
934 set xl [expr $x + $delta]
935 set xr [expr $x + $delta + $wid + $lthickness]
936 if {[incr ntags -1] >= 0} {
937 # draw a tag
938 set t [$canv create polygon $x [expr $yt + $delta] $xl $yt \
939 $xr $yt $xr $yb $xl $yb $x [expr $yb - $delta] \
940 -width 1 -outline black -fill yellow -tags tag.$id]
941 $canv bind $t <1> [list showtag $tag 1]
942 set rowtextx($idline($id)) [expr {$xr + $linespc}]
943 } else {
944 # draw a head or other ref
945 if {[incr nheads -1] >= 0} {
946 set col green
947 } else {
948 set col "#ddddff"
950 set xl [expr $xl - $delta/2]
951 $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
952 -width 1 -outline black -fill $col -tags tag.$id
954 set t [$canv create text $xl $y1 -anchor w -text $tag \
955 -font $mainfont -tags tag.$id]
956 if {$ntags >= 0} {
957 $canv bind $t <1> [list showtag $tag 1]
960 return $xt
963 proc notecrossings {id lo hi corner} {
964 global olddisplist crossings cornercrossings
966 for {set i $lo} {[incr i] < $hi} {} {
967 set p [lindex $olddisplist $i]
968 if {$p == {}} continue
969 if {$i == $corner} {
970 if {![info exists cornercrossings($id)]
971 || [lsearch -exact $cornercrossings($id) $p] < 0} {
972 lappend cornercrossings($id) $p
974 if {![info exists cornercrossings($p)]
975 || [lsearch -exact $cornercrossings($p) $id] < 0} {
976 lappend cornercrossings($p) $id
978 } else {
979 if {![info exists crossings($id)]
980 || [lsearch -exact $crossings($id) $p] < 0} {
981 lappend crossings($id) $p
983 if {![info exists crossings($p)]
984 || [lsearch -exact $crossings($p) $id] < 0} {
985 lappend crossings($p) $id
991 proc xcoord {i level ln} {
992 global canvx0 xspc1 xspc2
994 set x [expr {$canvx0 + $i * $xspc1($ln)}]
995 if {$i > 0 && $i == $level} {
996 set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
997 } elseif {$i > $level} {
998 set x [expr {$x + $xspc2 - $xspc1($ln)}]
1000 return $x
1003 # it seems Tk can't draw arrows on the end of diagonal line segments...
1004 proc trimdiagend {line} {
1005 while {[llength $line] > 4} {
1006 set x1 [lindex $line end-3]
1007 set y1 [lindex $line end-2]
1008 set x2 [lindex $line end-1]
1009 set y2 [lindex $line end]
1010 if {($x1 == $x2) != ($y1 == $y2)} break
1011 set line [lreplace $line end-1 end]
1013 return $line
1016 proc trimdiagstart {line} {
1017 while {[llength $line] > 4} {
1018 set x1 [lindex $line 0]
1019 set y1 [lindex $line 1]
1020 set x2 [lindex $line 2]
1021 set y2 [lindex $line 3]
1022 if {($x1 == $x2) != ($y1 == $y2)} break
1023 set line [lreplace $line 0 1]
1025 return $line
1028 proc drawslants {id needonscreen nohs} {
1029 global canv mainline mainlinearrow sidelines
1030 global canvx0 canvy xspc1 xspc2 lthickness
1031 global currentparents dupparents
1032 global lthickness linespc canvy colormap lineno geometry
1033 global maxgraphpct maxwidth
1034 global displist onscreen lastuse
1035 global parents commitlisted
1036 global oldnlines olddlevel olddisplist
1037 global nhyperspace numcommits nnewparents
1039 if {$lineno < 0} {
1040 lappend displist $id
1041 set onscreen($id) 1
1042 return 0
1045 set y1 [expr {$canvy - $linespc}]
1046 set y2 $canvy
1048 # work out what we need to get back on screen
1049 set reins {}
1050 if {$onscreen($id) < 0} {
1051 # next to do isn't displayed, better get it on screen...
1052 lappend reins [list $id 0]
1054 # make sure all the previous commits's parents are on the screen
1055 foreach p $currentparents {
1056 if {$onscreen($p) < 0} {
1057 lappend reins [list $p 0]
1060 # bring back anything requested by caller
1061 if {$needonscreen ne {}} {
1062 lappend reins $needonscreen
1065 # try the shortcut
1066 if {$currentparents == $id && $onscreen($id) == 0 && $reins eq {}} {
1067 set dlevel $olddlevel
1068 set x [xcoord $dlevel $dlevel $lineno]
1069 set mainline($id) [list $x $y1]
1070 set mainlinearrow($id) none
1071 set lastuse($id) $lineno
1072 set displist [lreplace $displist $dlevel $dlevel $id]
1073 set onscreen($id) 1
1074 set xspc1([expr {$lineno + 1}]) $xspc1($lineno)
1075 return $dlevel
1078 # update displist
1079 set displist [lreplace $displist $olddlevel $olddlevel]
1080 set j $olddlevel
1081 foreach p $currentparents {
1082 set lastuse($p) $lineno
1083 if {$onscreen($p) == 0} {
1084 set displist [linsert $displist $j $p]
1085 set onscreen($p) 1
1086 incr j
1089 if {$onscreen($id) == 0} {
1090 lappend displist $id
1091 set onscreen($id) 1
1094 # remove the null entry if present
1095 set nullentry [lsearch -exact $displist {}]
1096 if {$nullentry >= 0} {
1097 set displist [lreplace $displist $nullentry $nullentry]
1100 # bring back the ones we need now (if we did it earlier
1101 # it would change displist and invalidate olddlevel)
1102 foreach pi $reins {
1103 # test again in case of duplicates in reins
1104 set p [lindex $pi 0]
1105 if {$onscreen($p) < 0} {
1106 set onscreen($p) 1
1107 set lastuse($p) $lineno
1108 set displist [linsert $displist [lindex $pi 1] $p]
1109 incr nhyperspace -1
1113 set lastuse($id) $lineno
1115 # see if we need to make any lines jump off into hyperspace
1116 set displ [llength $displist]
1117 if {$displ > $maxwidth} {
1118 set ages {}
1119 foreach x $displist {
1120 lappend ages [list $lastuse($x) $x]
1122 set ages [lsort -integer -index 0 $ages]
1123 set k 0
1124 while {$displ > $maxwidth} {
1125 set use [lindex $ages $k 0]
1126 set victim [lindex $ages $k 1]
1127 if {$use >= $lineno - 5} break
1128 incr k
1129 if {[lsearch -exact $nohs $victim] >= 0} continue
1130 set i [lsearch -exact $displist $victim]
1131 set displist [lreplace $displist $i $i]
1132 set onscreen($victim) -1
1133 incr nhyperspace
1134 incr displ -1
1135 if {$i < $nullentry} {
1136 incr nullentry -1
1138 set x [lindex $mainline($victim) end-1]
1139 lappend mainline($victim) $x $y1
1140 set line [trimdiagend $mainline($victim)]
1141 set arrow "last"
1142 if {$mainlinearrow($victim) ne "none"} {
1143 set line [trimdiagstart $line]
1144 set arrow "both"
1146 lappend sidelines($victim) [list $line 1 $arrow]
1147 unset mainline($victim)
1151 set dlevel [lsearch -exact $displist $id]
1153 # If we are reducing, put in a null entry
1154 if {$displ < $oldnlines} {
1155 # does the next line look like a merge?
1156 # i.e. does it have > 1 new parent?
1157 if {$nnewparents($id) > 1} {
1158 set i [expr {$dlevel + 1}]
1159 } elseif {$nnewparents([lindex $olddisplist $olddlevel]) == 0} {
1160 set i $olddlevel
1161 if {$nullentry >= 0 && $nullentry < $i} {
1162 incr i -1
1164 } elseif {$nullentry >= 0} {
1165 set i $nullentry
1166 while {$i < $displ
1167 && [lindex $olddisplist $i] == [lindex $displist $i]} {
1168 incr i
1170 } else {
1171 set i $olddlevel
1172 if {$dlevel >= $i} {
1173 incr i
1176 if {$i < $displ} {
1177 set displist [linsert $displist $i {}]
1178 incr displ
1179 if {$dlevel >= $i} {
1180 incr dlevel
1185 # decide on the line spacing for the next line
1186 set lj [expr {$lineno + 1}]
1187 set maxw [expr {$maxgraphpct * $geometry(canv1) / 100}]
1188 if {$displ <= 1 || $canvx0 + $displ * $xspc2 <= $maxw} {
1189 set xspc1($lj) $xspc2
1190 } else {
1191 set xspc1($lj) [expr {($maxw - $canvx0 - $xspc2) / ($displ - 1)}]
1192 if {$xspc1($lj) < $lthickness} {
1193 set xspc1($lj) $lthickness
1197 foreach idi $reins {
1198 set id [lindex $idi 0]
1199 set j [lsearch -exact $displist $id]
1200 set xj [xcoord $j $dlevel $lj]
1201 set mainline($id) [list $xj $y2]
1202 set mainlinearrow($id) first
1205 set i -1
1206 foreach id $olddisplist {
1207 incr i
1208 if {$id == {}} continue
1209 if {$onscreen($id) <= 0} continue
1210 set xi [xcoord $i $olddlevel $lineno]
1211 if {$i == $olddlevel} {
1212 foreach p $currentparents {
1213 set j [lsearch -exact $displist $p]
1214 set coords [list $xi $y1]
1215 set xj [xcoord $j $dlevel $lj]
1216 if {$xj < $xi - $linespc} {
1217 lappend coords [expr {$xj + $linespc}] $y1
1218 notecrossings $p $j $i [expr {$j + 1}]
1219 } elseif {$xj > $xi + $linespc} {
1220 lappend coords [expr {$xj - $linespc}] $y1
1221 notecrossings $p $i $j [expr {$j - 1}]
1223 if {[lsearch -exact $dupparents $p] >= 0} {
1224 # draw a double-width line to indicate the doubled parent
1225 lappend coords $xj $y2
1226 lappend sidelines($p) [list $coords 2 none]
1227 if {![info exists mainline($p)]} {
1228 set mainline($p) [list $xj $y2]
1229 set mainlinearrow($p) none
1231 } else {
1232 # normal case, no parent duplicated
1233 set yb $y2
1234 set dx [expr {abs($xi - $xj)}]
1235 if {0 && $dx < $linespc} {
1236 set yb [expr {$y1 + $dx}]
1238 if {![info exists mainline($p)]} {
1239 if {$xi != $xj} {
1240 lappend coords $xj $yb
1242 set mainline($p) $coords
1243 set mainlinearrow($p) none
1244 } else {
1245 lappend coords $xj $yb
1246 if {$yb < $y2} {
1247 lappend coords $xj $y2
1249 lappend sidelines($p) [list $coords 1 none]
1253 } else {
1254 set j $i
1255 if {[lindex $displist $i] != $id} {
1256 set j [lsearch -exact $displist $id]
1258 if {$j != $i || $xspc1($lineno) != $xspc1($lj)
1259 || ($olddlevel < $i && $i < $dlevel)
1260 || ($dlevel < $i && $i < $olddlevel)} {
1261 set xj [xcoord $j $dlevel $lj]
1262 lappend mainline($id) $xi $y1 $xj $y2
1266 return $dlevel
1269 # search for x in a list of lists
1270 proc llsearch {llist x} {
1271 set i 0
1272 foreach l $llist {
1273 if {$l == $x || [lsearch -exact $l $x] >= 0} {
1274 return $i
1276 incr i
1278 return -1
1281 proc drawmore {reading} {
1282 global displayorder numcommits ncmupdate nextupdate
1283 global stopped nhyperspace parents commitlisted
1284 global maxwidth onscreen displist currentparents olddlevel
1286 set n [llength $displayorder]
1287 while {$numcommits < $n} {
1288 set id [lindex $displayorder $numcommits]
1289 set ctxend [expr {$numcommits + 10}]
1290 if {!$reading && $ctxend > $n} {
1291 set ctxend $n
1293 set dlist {}
1294 if {$numcommits > 0} {
1295 set dlist [lreplace $displist $olddlevel $olddlevel]
1296 set i $olddlevel
1297 foreach p $currentparents {
1298 if {$onscreen($p) == 0} {
1299 set dlist [linsert $dlist $i $p]
1300 incr i
1304 set nohs {}
1305 set reins {}
1306 set isfat [expr {[llength $dlist] > $maxwidth}]
1307 if {$nhyperspace > 0 || $isfat} {
1308 if {$ctxend > $n} break
1309 # work out what to bring back and
1310 # what we want to don't want to send into hyperspace
1311 set room 1
1312 for {set k $numcommits} {$k < $ctxend} {incr k} {
1313 set x [lindex $displayorder $k]
1314 set i [llsearch $dlist $x]
1315 if {$i < 0} {
1316 set i [llength $dlist]
1317 lappend dlist $x
1319 if {[lsearch -exact $nohs $x] < 0} {
1320 lappend nohs $x
1322 if {$reins eq {} && $onscreen($x) < 0 && $room} {
1323 set reins [list $x $i]
1325 set newp {}
1326 if {[info exists commitlisted($x)]} {
1327 set right 0
1328 foreach p $parents($x) {
1329 if {[llsearch $dlist $p] < 0} {
1330 lappend newp $p
1331 if {[lsearch -exact $nohs $p] < 0} {
1332 lappend nohs $p
1334 if {$reins eq {} && $onscreen($p) < 0 && $room} {
1335 set reins [list $p [expr {$i + $right}]]
1338 set right 1
1341 set l [lindex $dlist $i]
1342 if {[llength $l] == 1} {
1343 set l $newp
1344 } else {
1345 set j [lsearch -exact $l $x]
1346 set l [concat [lreplace $l $j $j] $newp]
1348 set dlist [lreplace $dlist $i $i $l]
1349 if {$room && $isfat && [llength $newp] <= 1} {
1350 set room 0
1355 set dlevel [drawslants $id $reins $nohs]
1356 drawcommitline $dlevel
1357 if {[clock clicks -milliseconds] >= $nextupdate
1358 && $numcommits >= $ncmupdate} {
1359 doupdate $reading
1360 if {$stopped} break
1365 # level here is an index in todo
1366 proc updatetodo {level noshortcut} {
1367 global ncleft todo nnewparents
1368 global commitlisted parents onscreen
1370 set id [lindex $todo $level]
1371 set olds {}
1372 if {[info exists commitlisted($id)]} {
1373 foreach p $parents($id) {
1374 if {[lsearch -exact $olds $p] < 0} {
1375 lappend olds $p
1379 if {!$noshortcut && [llength $olds] == 1} {
1380 set p [lindex $olds 0]
1381 if {$ncleft($p) == 1 && [lsearch -exact $todo $p] < 0} {
1382 set ncleft($p) 0
1383 set todo [lreplace $todo $level $level $p]
1384 set onscreen($p) 0
1385 set nnewparents($id) 1
1386 return 0
1390 set todo [lreplace $todo $level $level]
1391 set i $level
1392 set n 0
1393 foreach p $olds {
1394 incr ncleft($p) -1
1395 set k [lsearch -exact $todo $p]
1396 if {$k < 0} {
1397 set todo [linsert $todo $i $p]
1398 set onscreen($p) 0
1399 incr i
1400 incr n
1403 set nnewparents($id) $n
1405 return 1
1408 proc decidenext {{noread 0}} {
1409 global ncleft todo
1410 global datemode cdate
1411 global commitinfo
1413 # choose which one to do next time around
1414 set todol [llength $todo]
1415 set level -1
1416 set latest {}
1417 for {set k $todol} {[incr k -1] >= 0} {} {
1418 set p [lindex $todo $k]
1419 if {$ncleft($p) == 0} {
1420 if {$datemode} {
1421 if {![info exists commitinfo($p)]} {
1422 if {$noread} {
1423 return {}
1425 readcommit $p
1427 if {$latest == {} || $cdate($p) > $latest} {
1428 set level $k
1429 set latest $cdate($p)
1431 } else {
1432 set level $k
1433 break
1437 if {$level < 0} {
1438 if {$todo != {}} {
1439 puts "ERROR: none of the pending commits can be done yet:"
1440 foreach p $todo {
1441 puts " $p ($ncleft($p))"
1444 return -1
1447 return $level
1450 proc drawcommit {id} {
1451 global phase todo nchildren datemode nextupdate revlistorder
1452 global numcommits ncmupdate displayorder todo onscreen parents
1454 if {$phase != "incrdraw"} {
1455 set phase incrdraw
1456 set displayorder {}
1457 set todo {}
1458 initgraph
1460 if {$nchildren($id) == 0} {
1461 lappend todo $id
1462 set onscreen($id) 0
1464 if {$revlistorder} {
1465 set level [lsearch -exact $todo $id]
1466 if {$level < 0} {
1467 error_popup "oops, $id isn't in todo"
1468 return
1470 lappend displayorder $id
1471 updatetodo $level 0
1472 } else {
1473 set level [decidenext 1]
1474 if {$level == {} || $id != [lindex $todo $level]} {
1475 return
1477 while 1 {
1478 lappend displayorder [lindex $todo $level]
1479 if {[updatetodo $level $datemode]} {
1480 set level [decidenext 1]
1481 if {$level == {}} break
1483 set id [lindex $todo $level]
1484 if {![info exists commitlisted($id)]} {
1485 break
1489 drawmore 1
1492 proc finishcommits {} {
1493 global phase
1494 global canv mainfont ctext maincursor textcursor
1496 if {$phase != "incrdraw"} {
1497 $canv delete all
1498 $canv create text 3 3 -anchor nw -text "No commits selected" \
1499 -font $mainfont -tags textitems
1500 set phase {}
1501 } else {
1502 drawrest
1504 . config -cursor $maincursor
1505 settextcursor $textcursor
1508 # Don't change the text pane cursor if it is currently the hand cursor,
1509 # showing that we are over a sha1 ID link.
1510 proc settextcursor {c} {
1511 global ctext curtextcursor
1513 if {[$ctext cget -cursor] == $curtextcursor} {
1514 $ctext config -cursor $c
1516 set curtextcursor $c
1519 proc drawgraph {} {
1520 global nextupdate startmsecs ncmupdate
1521 global displayorder onscreen
1523 if {$displayorder == {}} return
1524 set startmsecs [clock clicks -milliseconds]
1525 set nextupdate [expr $startmsecs + 100]
1526 set ncmupdate 1
1527 initgraph
1528 foreach id $displayorder {
1529 set onscreen($id) 0
1531 drawmore 0
1534 proc drawrest {} {
1535 global phase stopped redisplaying selectedline
1536 global datemode todo displayorder
1537 global numcommits ncmupdate
1538 global nextupdate startmsecs revlistorder
1540 if {!$revlistorder} {
1541 set level [decidenext]
1542 if {$level >= 0} {
1543 set phase drawgraph
1544 while 1 {
1545 lappend displayorder [lindex $todo $level]
1546 set hard [updatetodo $level $datemode]
1547 if {$hard} {
1548 set level [decidenext]
1549 if {$level < 0} break
1554 drawmore 0
1555 set phase {}
1556 set drawmsecs [expr [clock clicks -milliseconds] - $startmsecs]
1557 #puts "overall $drawmsecs ms for $numcommits commits"
1558 if {$redisplaying} {
1559 if {$stopped == 0 && [info exists selectedline]} {
1560 selectline $selectedline 0
1562 if {$stopped == 1} {
1563 set stopped 0
1564 after idle drawgraph
1565 } else {
1566 set redisplaying 0
1571 proc findmatches {f} {
1572 global findtype foundstring foundstrlen
1573 if {$findtype == "Regexp"} {
1574 set matches [regexp -indices -all -inline $foundstring $f]
1575 } else {
1576 if {$findtype == "IgnCase"} {
1577 set str [string tolower $f]
1578 } else {
1579 set str $f
1581 set matches {}
1582 set i 0
1583 while {[set j [string first $foundstring $str $i]] >= 0} {
1584 lappend matches [list $j [expr $j+$foundstrlen-1]]
1585 set i [expr $j + $foundstrlen]
1588 return $matches
1591 proc dofind {} {
1592 global findtype findloc findstring markedmatches commitinfo
1593 global numcommits lineid linehtag linentag linedtag
1594 global mainfont namefont canv canv2 canv3 selectedline
1595 global matchinglines foundstring foundstrlen
1597 stopfindproc
1598 unmarkmatches
1599 focus .
1600 set matchinglines {}
1601 if {$findloc == "Pickaxe"} {
1602 findpatches
1603 return
1605 if {$findtype == "IgnCase"} {
1606 set foundstring [string tolower $findstring]
1607 } else {
1608 set foundstring $findstring
1610 set foundstrlen [string length $findstring]
1611 if {$foundstrlen == 0} return
1612 if {$findloc == "Files"} {
1613 findfiles
1614 return
1616 if {![info exists selectedline]} {
1617 set oldsel -1
1618 } else {
1619 set oldsel $selectedline
1621 set didsel 0
1622 set fldtypes {Headline Author Date Committer CDate Comment}
1623 for {set l 0} {$l < $numcommits} {incr l} {
1624 set id $lineid($l)
1625 set info $commitinfo($id)
1626 set doesmatch 0
1627 foreach f $info ty $fldtypes {
1628 if {$findloc != "All fields" && $findloc != $ty} {
1629 continue
1631 set matches [findmatches $f]
1632 if {$matches == {}} continue
1633 set doesmatch 1
1634 if {$ty == "Headline"} {
1635 markmatches $canv $l $f $linehtag($l) $matches $mainfont
1636 } elseif {$ty == "Author"} {
1637 markmatches $canv2 $l $f $linentag($l) $matches $namefont
1638 } elseif {$ty == "Date"} {
1639 markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
1642 if {$doesmatch} {
1643 lappend matchinglines $l
1644 if {!$didsel && $l > $oldsel} {
1645 findselectline $l
1646 set didsel 1
1650 if {$matchinglines == {}} {
1651 bell
1652 } elseif {!$didsel} {
1653 findselectline [lindex $matchinglines 0]
1657 proc findselectline {l} {
1658 global findloc commentend ctext
1659 selectline $l 1
1660 if {$findloc == "All fields" || $findloc == "Comments"} {
1661 # highlight the matches in the comments
1662 set f [$ctext get 1.0 $commentend]
1663 set matches [findmatches $f]
1664 foreach match $matches {
1665 set start [lindex $match 0]
1666 set end [expr [lindex $match 1] + 1]
1667 $ctext tag add found "1.0 + $start c" "1.0 + $end c"
1672 proc findnext {restart} {
1673 global matchinglines selectedline
1674 if {![info exists matchinglines]} {
1675 if {$restart} {
1676 dofind
1678 return
1680 if {![info exists selectedline]} return
1681 foreach l $matchinglines {
1682 if {$l > $selectedline} {
1683 findselectline $l
1684 return
1687 bell
1690 proc findprev {} {
1691 global matchinglines selectedline
1692 if {![info exists matchinglines]} {
1693 dofind
1694 return
1696 if {![info exists selectedline]} return
1697 set prev {}
1698 foreach l $matchinglines {
1699 if {$l >= $selectedline} break
1700 set prev $l
1702 if {$prev != {}} {
1703 findselectline $prev
1704 } else {
1705 bell
1709 proc findlocchange {name ix op} {
1710 global findloc findtype findtypemenu
1711 if {$findloc == "Pickaxe"} {
1712 set findtype Exact
1713 set state disabled
1714 } else {
1715 set state normal
1717 $findtypemenu entryconf 1 -state $state
1718 $findtypemenu entryconf 2 -state $state
1721 proc stopfindproc {{done 0}} {
1722 global findprocpid findprocfile findids
1723 global ctext findoldcursor phase maincursor textcursor
1724 global findinprogress
1726 catch {unset findids}
1727 if {[info exists findprocpid]} {
1728 if {!$done} {
1729 catch {exec kill $findprocpid}
1731 catch {close $findprocfile}
1732 unset findprocpid
1734 if {[info exists findinprogress]} {
1735 unset findinprogress
1736 if {$phase != "incrdraw"} {
1737 . config -cursor $maincursor
1738 settextcursor $textcursor
1743 proc findpatches {} {
1744 global findstring selectedline numcommits
1745 global findprocpid findprocfile
1746 global finddidsel ctext lineid findinprogress
1747 global findinsertpos
1749 if {$numcommits == 0} return
1751 # make a list of all the ids to search, starting at the one
1752 # after the selected line (if any)
1753 if {[info exists selectedline]} {
1754 set l $selectedline
1755 } else {
1756 set l -1
1758 set inputids {}
1759 for {set i 0} {$i < $numcommits} {incr i} {
1760 if {[incr l] >= $numcommits} {
1761 set l 0
1763 append inputids $lineid($l) "\n"
1766 if {[catch {
1767 set f [open [list | git-diff-tree --stdin -s -r -S$findstring \
1768 << $inputids] r]
1769 } err]} {
1770 error_popup "Error starting search process: $err"
1771 return
1774 set findinsertpos end
1775 set findprocfile $f
1776 set findprocpid [pid $f]
1777 fconfigure $f -blocking 0
1778 fileevent $f readable readfindproc
1779 set finddidsel 0
1780 . config -cursor watch
1781 settextcursor watch
1782 set findinprogress 1
1785 proc readfindproc {} {
1786 global findprocfile finddidsel
1787 global idline matchinglines findinsertpos
1789 set n [gets $findprocfile line]
1790 if {$n < 0} {
1791 if {[eof $findprocfile]} {
1792 stopfindproc 1
1793 if {!$finddidsel} {
1794 bell
1797 return
1799 if {![regexp {^[0-9a-f]{40}} $line id]} {
1800 error_popup "Can't parse git-diff-tree output: $line"
1801 stopfindproc
1802 return
1804 if {![info exists idline($id)]} {
1805 puts stderr "spurious id: $id"
1806 return
1808 set l $idline($id)
1809 insertmatch $l $id
1812 proc insertmatch {l id} {
1813 global matchinglines findinsertpos finddidsel
1815 if {$findinsertpos == "end"} {
1816 if {$matchinglines != {} && $l < [lindex $matchinglines 0]} {
1817 set matchinglines [linsert $matchinglines 0 $l]
1818 set findinsertpos 1
1819 } else {
1820 lappend matchinglines $l
1822 } else {
1823 set matchinglines [linsert $matchinglines $findinsertpos $l]
1824 incr findinsertpos
1826 markheadline $l $id
1827 if {!$finddidsel} {
1828 findselectline $l
1829 set finddidsel 1
1833 proc findfiles {} {
1834 global selectedline numcommits lineid ctext
1835 global ffileline finddidsel parents nparents
1836 global findinprogress findstartline findinsertpos
1837 global treediffs fdiffids fdiffsneeded fdiffpos
1838 global findmergefiles
1840 if {$numcommits == 0} return
1842 if {[info exists selectedline]} {
1843 set l [expr {$selectedline + 1}]
1844 } else {
1845 set l 0
1847 set ffileline $l
1848 set findstartline $l
1849 set diffsneeded {}
1850 set fdiffsneeded {}
1851 while 1 {
1852 set id $lineid($l)
1853 if {$findmergefiles || $nparents($id) == 1} {
1854 foreach p $parents($id) {
1855 if {![info exists treediffs([list $id $p])]} {
1856 append diffsneeded "$id $p\n"
1857 lappend fdiffsneeded [list $id $p]
1861 if {[incr l] >= $numcommits} {
1862 set l 0
1864 if {$l == $findstartline} break
1867 # start off a git-diff-tree process if needed
1868 if {$diffsneeded ne {}} {
1869 if {[catch {
1870 set df [open [list | git-diff-tree -r --stdin << $diffsneeded] r]
1871 } err ]} {
1872 error_popup "Error starting search process: $err"
1873 return
1875 catch {unset fdiffids}
1876 set fdiffpos 0
1877 fconfigure $df -blocking 0
1878 fileevent $df readable [list readfilediffs $df]
1881 set finddidsel 0
1882 set findinsertpos end
1883 set id $lineid($l)
1884 set p [lindex $parents($id) 0]
1885 . config -cursor watch
1886 settextcursor watch
1887 set findinprogress 1
1888 findcont [list $id $p]
1889 update
1892 proc readfilediffs {df} {
1893 global findids fdiffids fdiffs
1895 set n [gets $df line]
1896 if {$n < 0} {
1897 if {[eof $df]} {
1898 donefilediff
1899 if {[catch {close $df} err]} {
1900 stopfindproc
1901 bell
1902 error_popup "Error in git-diff-tree: $err"
1903 } elseif {[info exists findids]} {
1904 set ids $findids
1905 stopfindproc
1906 bell
1907 error_popup "Couldn't find diffs for {$ids}"
1910 return
1912 if {[regexp {^([0-9a-f]{40}) \(from ([0-9a-f]{40})\)} $line match id p]} {
1913 # start of a new string of diffs
1914 donefilediff
1915 set fdiffids [list $id $p]
1916 set fdiffs {}
1917 } elseif {[string match ":*" $line]} {
1918 lappend fdiffs [lindex $line 5]
1922 proc donefilediff {} {
1923 global fdiffids fdiffs treediffs findids
1924 global fdiffsneeded fdiffpos
1926 if {[info exists fdiffids]} {
1927 while {[lindex $fdiffsneeded $fdiffpos] ne $fdiffids
1928 && $fdiffpos < [llength $fdiffsneeded]} {
1929 # git-diff-tree doesn't output anything for a commit
1930 # which doesn't change anything
1931 set nullids [lindex $fdiffsneeded $fdiffpos]
1932 set treediffs($nullids) {}
1933 if {[info exists findids] && $nullids eq $findids} {
1934 unset findids
1935 findcont $nullids
1937 incr fdiffpos
1939 incr fdiffpos
1941 if {![info exists treediffs($fdiffids)]} {
1942 set treediffs($fdiffids) $fdiffs
1944 if {[info exists findids] && $fdiffids eq $findids} {
1945 unset findids
1946 findcont $fdiffids
1951 proc findcont {ids} {
1952 global findids treediffs parents nparents
1953 global ffileline findstartline finddidsel
1954 global lineid numcommits matchinglines findinprogress
1955 global findmergefiles
1957 set id [lindex $ids 0]
1958 set p [lindex $ids 1]
1959 set pi [lsearch -exact $parents($id) $p]
1960 set l $ffileline
1961 while 1 {
1962 if {$findmergefiles || $nparents($id) == 1} {
1963 if {![info exists treediffs($ids)]} {
1964 set findids $ids
1965 set ffileline $l
1966 return
1968 set doesmatch 0
1969 foreach f $treediffs($ids) {
1970 set x [findmatches $f]
1971 if {$x != {}} {
1972 set doesmatch 1
1973 break
1976 if {$doesmatch} {
1977 insertmatch $l $id
1978 set pi $nparents($id)
1980 } else {
1981 set pi $nparents($id)
1983 if {[incr pi] >= $nparents($id)} {
1984 set pi 0
1985 if {[incr l] >= $numcommits} {
1986 set l 0
1988 if {$l == $findstartline} break
1989 set id $lineid($l)
1991 set p [lindex $parents($id) $pi]
1992 set ids [list $id $p]
1994 stopfindproc
1995 if {!$finddidsel} {
1996 bell
2000 # mark a commit as matching by putting a yellow background
2001 # behind the headline
2002 proc markheadline {l id} {
2003 global canv mainfont linehtag commitinfo
2005 set bbox [$canv bbox $linehtag($l)]
2006 set t [$canv create rect $bbox -outline {} -tags matches -fill yellow]
2007 $canv lower $t
2010 # mark the bits of a headline, author or date that match a find string
2011 proc markmatches {canv l str tag matches font} {
2012 set bbox [$canv bbox $tag]
2013 set x0 [lindex $bbox 0]
2014 set y0 [lindex $bbox 1]
2015 set y1 [lindex $bbox 3]
2016 foreach match $matches {
2017 set start [lindex $match 0]
2018 set end [lindex $match 1]
2019 if {$start > $end} continue
2020 set xoff [font measure $font [string range $str 0 [expr $start-1]]]
2021 set xlen [font measure $font [string range $str 0 [expr $end]]]
2022 set t [$canv create rect [expr $x0+$xoff] $y0 [expr $x0+$xlen+2] $y1 \
2023 -outline {} -tags matches -fill yellow]
2024 $canv lower $t
2028 proc unmarkmatches {} {
2029 global matchinglines findids
2030 allcanvs delete matches
2031 catch {unset matchinglines}
2032 catch {unset findids}
2035 proc selcanvline {w x y} {
2036 global canv canvy0 ctext linespc
2037 global lineid linehtag linentag linedtag rowtextx
2038 set ymax [lindex [$canv cget -scrollregion] 3]
2039 if {$ymax == {}} return
2040 set yfrac [lindex [$canv yview] 0]
2041 set y [expr {$y + $yfrac * $ymax}]
2042 set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
2043 if {$l < 0} {
2044 set l 0
2046 if {$w eq $canv} {
2047 if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
2049 unmarkmatches
2050 selectline $l 1
2053 proc commit_descriptor {p} {
2054 global commitinfo
2055 set l "..."
2056 if {[info exists commitinfo($p)]} {
2057 set l [lindex $commitinfo($p) 0]
2059 return "$p ($l)"
2062 # append some text to the ctext widget, and make any SHA1 ID
2063 # that we know about be a clickable link.
2064 proc appendwithlinks {text} {
2065 global ctext idline linknum
2067 set start [$ctext index "end - 1c"]
2068 $ctext insert end $text
2069 $ctext insert end "\n"
2070 set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
2071 foreach l $links {
2072 set s [lindex $l 0]
2073 set e [lindex $l 1]
2074 set linkid [string range $text $s $e]
2075 if {![info exists idline($linkid)]} continue
2076 incr e
2077 $ctext tag add link "$start + $s c" "$start + $e c"
2078 $ctext tag add link$linknum "$start + $s c" "$start + $e c"
2079 $ctext tag bind link$linknum <1> [list selectline $idline($linkid) 1]
2080 incr linknum
2082 $ctext tag conf link -foreground blue -underline 1
2083 $ctext tag bind link <Enter> { %W configure -cursor hand2 }
2084 $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
2087 proc selectline {l isnew} {
2088 global canv canv2 canv3 ctext commitinfo selectedline
2089 global lineid linehtag linentag linedtag
2090 global canvy0 linespc parents nparents children
2091 global cflist currentid sha1entry
2092 global commentend idtags idline linknum
2094 $canv delete hover
2095 normalline
2096 if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
2097 $canv delete secsel
2098 set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
2099 -tags secsel -fill [$canv cget -selectbackground]]
2100 $canv lower $t
2101 $canv2 delete secsel
2102 set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
2103 -tags secsel -fill [$canv2 cget -selectbackground]]
2104 $canv2 lower $t
2105 $canv3 delete secsel
2106 set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
2107 -tags secsel -fill [$canv3 cget -selectbackground]]
2108 $canv3 lower $t
2109 set y [expr {$canvy0 + $l * $linespc}]
2110 set ymax [lindex [$canv cget -scrollregion] 3]
2111 set ytop [expr {$y - $linespc - 1}]
2112 set ybot [expr {$y + $linespc + 1}]
2113 set wnow [$canv yview]
2114 set wtop [expr [lindex $wnow 0] * $ymax]
2115 set wbot [expr [lindex $wnow 1] * $ymax]
2116 set wh [expr {$wbot - $wtop}]
2117 set newtop $wtop
2118 if {$ytop < $wtop} {
2119 if {$ybot < $wtop} {
2120 set newtop [expr {$y - $wh / 2.0}]
2121 } else {
2122 set newtop $ytop
2123 if {$newtop > $wtop - $linespc} {
2124 set newtop [expr {$wtop - $linespc}]
2127 } elseif {$ybot > $wbot} {
2128 if {$ytop > $wbot} {
2129 set newtop [expr {$y - $wh / 2.0}]
2130 } else {
2131 set newtop [expr {$ybot - $wh}]
2132 if {$newtop < $wtop + $linespc} {
2133 set newtop [expr {$wtop + $linespc}]
2137 if {$newtop != $wtop} {
2138 if {$newtop < 0} {
2139 set newtop 0
2141 allcanvs yview moveto [expr $newtop * 1.0 / $ymax]
2144 if {$isnew} {
2145 addtohistory [list selectline $l 0]
2148 set selectedline $l
2150 set id $lineid($l)
2151 set currentid $id
2152 $sha1entry delete 0 end
2153 $sha1entry insert 0 $id
2154 $sha1entry selection from 0
2155 $sha1entry selection to end
2157 $ctext conf -state normal
2158 $ctext delete 0.0 end
2159 set linknum 0
2160 $ctext mark set fmark.0 0.0
2161 $ctext mark gravity fmark.0 left
2162 set info $commitinfo($id)
2163 set date [formatdate [lindex $info 2]]
2164 $ctext insert end "Author: [lindex $info 1] $date\n"
2165 set date [formatdate [lindex $info 4]]
2166 $ctext insert end "Committer: [lindex $info 3] $date\n"
2167 if {[info exists idtags($id)]} {
2168 $ctext insert end "Tags:"
2169 foreach tag $idtags($id) {
2170 $ctext insert end " $tag"
2172 $ctext insert end "\n"
2175 set comment {}
2176 if {[info exists parents($id)]} {
2177 foreach p $parents($id) {
2178 append comment "Parent: [commit_descriptor $p]\n"
2181 if {[info exists children($id)]} {
2182 foreach c $children($id) {
2183 append comment "Child: [commit_descriptor $c]\n"
2186 append comment "\n"
2187 append comment [lindex $info 5]
2189 # make anything that looks like a SHA1 ID be a clickable link
2190 appendwithlinks $comment
2192 $ctext tag delete Comments
2193 $ctext tag remove found 1.0 end
2194 $ctext conf -state disabled
2195 set commentend [$ctext index "end - 1c"]
2197 $cflist delete 0 end
2198 $cflist insert end "Comments"
2199 if {$nparents($id) == 1} {
2200 startdiff [concat $id $parents($id)]
2201 } elseif {$nparents($id) > 1} {
2202 mergediff $id
2206 proc selnextline {dir} {
2207 global selectedline
2208 if {![info exists selectedline]} return
2209 set l [expr $selectedline + $dir]
2210 unmarkmatches
2211 selectline $l 1
2214 proc unselectline {} {
2215 global selectedline
2217 catch {unset selectedline}
2218 allcanvs delete secsel
2221 proc addtohistory {cmd} {
2222 global history historyindex
2224 if {$historyindex > 0
2225 && [lindex $history [expr {$historyindex - 1}]] == $cmd} {
2226 return
2229 if {$historyindex < [llength $history]} {
2230 set history [lreplace $history $historyindex end $cmd]
2231 } else {
2232 lappend history $cmd
2234 incr historyindex
2235 if {$historyindex > 1} {
2236 .ctop.top.bar.leftbut conf -state normal
2237 } else {
2238 .ctop.top.bar.leftbut conf -state disabled
2240 .ctop.top.bar.rightbut conf -state disabled
2243 proc goback {} {
2244 global history historyindex
2246 if {$historyindex > 1} {
2247 incr historyindex -1
2248 set cmd [lindex $history [expr {$historyindex - 1}]]
2249 eval $cmd
2250 .ctop.top.bar.rightbut conf -state normal
2252 if {$historyindex <= 1} {
2253 .ctop.top.bar.leftbut conf -state disabled
2257 proc goforw {} {
2258 global history historyindex
2260 if {$historyindex < [llength $history]} {
2261 set cmd [lindex $history $historyindex]
2262 incr historyindex
2263 eval $cmd
2264 .ctop.top.bar.leftbut conf -state normal
2266 if {$historyindex >= [llength $history]} {
2267 .ctop.top.bar.rightbut conf -state disabled
2271 proc mergediff {id} {
2272 global parents diffmergeid diffmergegca mergefilelist diffpindex
2274 set diffmergeid $id
2275 set diffpindex -1
2276 set diffmergegca [findgca $parents($id)]
2277 if {[info exists mergefilelist($id)]} {
2278 if {$mergefilelist($id) ne {}} {
2279 showmergediff
2281 } else {
2282 contmergediff {}
2286 proc findgca {ids} {
2287 set gca {}
2288 foreach id $ids {
2289 if {$gca eq {}} {
2290 set gca $id
2291 } else {
2292 if {[catch {
2293 set gca [exec git-merge-base $gca $id]
2294 } err]} {
2295 return {}
2299 return $gca
2302 proc contmergediff {ids} {
2303 global diffmergeid diffpindex parents nparents diffmergegca
2304 global treediffs mergefilelist diffids treepending
2306 # diff the child against each of the parents, and diff
2307 # each of the parents against the GCA.
2308 while 1 {
2309 if {[lindex $ids 0] == $diffmergeid && $diffmergegca ne {}} {
2310 set ids [list [lindex $ids 1] $diffmergegca]
2311 } else {
2312 if {[incr diffpindex] >= $nparents($diffmergeid)} break
2313 set p [lindex $parents($diffmergeid) $diffpindex]
2314 set ids [list $diffmergeid $p]
2316 if {![info exists treediffs($ids)]} {
2317 set diffids $ids
2318 if {![info exists treepending]} {
2319 gettreediffs $ids
2321 return
2325 # If a file in some parent is different from the child and also
2326 # different from the GCA, then it's interesting.
2327 # If we don't have a GCA, then a file is interesting if it is
2328 # different from the child in all the parents.
2329 if {$diffmergegca ne {}} {
2330 set files {}
2331 foreach p $parents($diffmergeid) {
2332 set gcadiffs $treediffs([list $p $diffmergegca])
2333 foreach f $treediffs([list $diffmergeid $p]) {
2334 if {[lsearch -exact $files $f] < 0
2335 && [lsearch -exact $gcadiffs $f] >= 0} {
2336 lappend files $f
2340 set files [lsort $files]
2341 } else {
2342 set p [lindex $parents($diffmergeid) 0]
2343 set files $treediffs([list $diffmergeid $p])
2344 for {set i 1} {$i < $nparents($diffmergeid) && $files ne {}} {incr i} {
2345 set p [lindex $parents($diffmergeid) $i]
2346 set df $treediffs([list $diffmergeid $p])
2347 set nf {}
2348 foreach f $files {
2349 if {[lsearch -exact $df $f] >= 0} {
2350 lappend nf $f
2353 set files $nf
2357 set mergefilelist($diffmergeid) $files
2358 if {$files ne {}} {
2359 showmergediff
2363 proc showmergediff {} {
2364 global cflist diffmergeid mergefilelist parents
2365 global diffopts diffinhunk currentfile currenthunk filelines
2366 global diffblocked groupfilelast mergefds groupfilenum grouphunks
2368 set files $mergefilelist($diffmergeid)
2369 foreach f $files {
2370 $cflist insert end $f
2372 set env(GIT_DIFF_OPTS) $diffopts
2373 set flist {}
2374 catch {unset currentfile}
2375 catch {unset currenthunk}
2376 catch {unset filelines}
2377 catch {unset groupfilenum}
2378 catch {unset grouphunks}
2379 set groupfilelast -1
2380 foreach p $parents($diffmergeid) {
2381 set cmd [list | git-diff-tree -p $p $diffmergeid]
2382 set cmd [concat $cmd $mergefilelist($diffmergeid)]
2383 if {[catch {set f [open $cmd r]} err]} {
2384 error_popup "Error getting diffs: $err"
2385 foreach f $flist {
2386 catch {close $f}
2388 return
2390 lappend flist $f
2391 set ids [list $diffmergeid $p]
2392 set mergefds($ids) $f
2393 set diffinhunk($ids) 0
2394 set diffblocked($ids) 0
2395 fconfigure $f -blocking 0
2396 fileevent $f readable [list getmergediffline $f $ids $diffmergeid]
2400 proc getmergediffline {f ids id} {
2401 global diffmergeid diffinhunk diffoldlines diffnewlines
2402 global currentfile currenthunk
2403 global diffoldstart diffnewstart diffoldlno diffnewlno
2404 global diffblocked mergefilelist
2405 global noldlines nnewlines difflcounts filelines
2407 set n [gets $f line]
2408 if {$n < 0} {
2409 if {![eof $f]} return
2412 if {!([info exists diffmergeid] && $diffmergeid == $id)} {
2413 if {$n < 0} {
2414 close $f
2416 return
2419 if {$diffinhunk($ids) != 0} {
2420 set fi $currentfile($ids)
2421 if {$n > 0 && [regexp {^[-+ \\]} $line match]} {
2422 # continuing an existing hunk
2423 set line [string range $line 1 end]
2424 set p [lindex $ids 1]
2425 if {$match eq "-" || $match eq " "} {
2426 set filelines($p,$fi,$diffoldlno($ids)) $line
2427 incr diffoldlno($ids)
2429 if {$match eq "+" || $match eq " "} {
2430 set filelines($id,$fi,$diffnewlno($ids)) $line
2431 incr diffnewlno($ids)
2433 if {$match eq " "} {
2434 if {$diffinhunk($ids) == 2} {
2435 lappend difflcounts($ids) \
2436 [list $noldlines($ids) $nnewlines($ids)]
2437 set noldlines($ids) 0
2438 set diffinhunk($ids) 1
2440 incr noldlines($ids)
2441 } elseif {$match eq "-" || $match eq "+"} {
2442 if {$diffinhunk($ids) == 1} {
2443 lappend difflcounts($ids) [list $noldlines($ids)]
2444 set noldlines($ids) 0
2445 set nnewlines($ids) 0
2446 set diffinhunk($ids) 2
2448 if {$match eq "-"} {
2449 incr noldlines($ids)
2450 } else {
2451 incr nnewlines($ids)
2454 # and if it's \ No newline at end of line, then what?
2455 return
2457 # end of a hunk
2458 if {$diffinhunk($ids) == 1 && $noldlines($ids) != 0} {
2459 lappend difflcounts($ids) [list $noldlines($ids)]
2460 } elseif {$diffinhunk($ids) == 2
2461 && ($noldlines($ids) != 0 || $nnewlines($ids) != 0)} {
2462 lappend difflcounts($ids) [list $noldlines($ids) $nnewlines($ids)]
2464 set currenthunk($ids) [list $currentfile($ids) \
2465 $diffoldstart($ids) $diffnewstart($ids) \
2466 $diffoldlno($ids) $diffnewlno($ids) \
2467 $difflcounts($ids)]
2468 set diffinhunk($ids) 0
2469 # -1 = need to block, 0 = unblocked, 1 = is blocked
2470 set diffblocked($ids) -1
2471 processhunks
2472 if {$diffblocked($ids) == -1} {
2473 fileevent $f readable {}
2474 set diffblocked($ids) 1
2478 if {$n < 0} {
2479 # eof
2480 if {!$diffblocked($ids)} {
2481 close $f
2482 set currentfile($ids) [llength $mergefilelist($diffmergeid)]
2483 set currenthunk($ids) [list $currentfile($ids) 0 0 0 0 {}]
2484 processhunks
2486 } elseif {[regexp {^diff --git a/(.*) b/} $line match fname]} {
2487 # start of a new file
2488 set currentfile($ids) \
2489 [lsearch -exact $mergefilelist($diffmergeid) $fname]
2490 } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
2491 $line match f1l f1c f2l f2c rest]} {
2492 if {[info exists currentfile($ids)] && $currentfile($ids) >= 0} {
2493 # start of a new hunk
2494 if {$f1l == 0 && $f1c == 0} {
2495 set f1l 1
2497 if {$f2l == 0 && $f2c == 0} {
2498 set f2l 1
2500 set diffinhunk($ids) 1
2501 set diffoldstart($ids) $f1l
2502 set diffnewstart($ids) $f2l
2503 set diffoldlno($ids) $f1l
2504 set diffnewlno($ids) $f2l
2505 set difflcounts($ids) {}
2506 set noldlines($ids) 0
2507 set nnewlines($ids) 0
2512 proc processhunks {} {
2513 global diffmergeid parents nparents currenthunk
2514 global mergefilelist diffblocked mergefds
2515 global grouphunks grouplinestart grouplineend groupfilenum
2517 set nfiles [llength $mergefilelist($diffmergeid)]
2518 while 1 {
2519 set fi $nfiles
2520 set lno 0
2521 # look for the earliest hunk
2522 foreach p $parents($diffmergeid) {
2523 set ids [list $diffmergeid $p]
2524 if {![info exists currenthunk($ids)]} return
2525 set i [lindex $currenthunk($ids) 0]
2526 set l [lindex $currenthunk($ids) 2]
2527 if {$i < $fi || ($i == $fi && $l < $lno)} {
2528 set fi $i
2529 set lno $l
2530 set pi $p
2534 if {$fi < $nfiles} {
2535 set ids [list $diffmergeid $pi]
2536 set hunk $currenthunk($ids)
2537 unset currenthunk($ids)
2538 if {$diffblocked($ids) > 0} {
2539 fileevent $mergefds($ids) readable \
2540 [list getmergediffline $mergefds($ids) $ids $diffmergeid]
2542 set diffblocked($ids) 0
2544 if {[info exists groupfilenum] && $groupfilenum == $fi
2545 && $lno <= $grouplineend} {
2546 # add this hunk to the pending group
2547 lappend grouphunks($pi) $hunk
2548 set endln [lindex $hunk 4]
2549 if {$endln > $grouplineend} {
2550 set grouplineend $endln
2552 continue
2556 # succeeding stuff doesn't belong in this group, so
2557 # process the group now
2558 if {[info exists groupfilenum]} {
2559 processgroup
2560 unset groupfilenum
2561 unset grouphunks
2564 if {$fi >= $nfiles} break
2566 # start a new group
2567 set groupfilenum $fi
2568 set grouphunks($pi) [list $hunk]
2569 set grouplinestart $lno
2570 set grouplineend [lindex $hunk 4]
2574 proc processgroup {} {
2575 global groupfilelast groupfilenum difffilestart
2576 global mergefilelist diffmergeid ctext filelines
2577 global parents diffmergeid diffoffset
2578 global grouphunks grouplinestart grouplineend nparents
2579 global mergemax
2581 $ctext conf -state normal
2582 set id $diffmergeid
2583 set f $groupfilenum
2584 if {$groupfilelast != $f} {
2585 $ctext insert end "\n"
2586 set here [$ctext index "end - 1c"]
2587 set difffilestart($f) $here
2588 set mark fmark.[expr {$f + 1}]
2589 $ctext mark set $mark $here
2590 $ctext mark gravity $mark left
2591 set header [lindex $mergefilelist($id) $f]
2592 set l [expr {(78 - [string length $header]) / 2}]
2593 set pad [string range "----------------------------------------" 1 $l]
2594 $ctext insert end "$pad $header $pad\n" filesep
2595 set groupfilelast $f
2596 foreach p $parents($id) {
2597 set diffoffset($p) 0
2601 $ctext insert end "@@" msep
2602 set nlines [expr {$grouplineend - $grouplinestart}]
2603 set events {}
2604 set pnum 0
2605 foreach p $parents($id) {
2606 set startline [expr {$grouplinestart + $diffoffset($p)}]
2607 set ol $startline
2608 set nl $grouplinestart
2609 if {[info exists grouphunks($p)]} {
2610 foreach h $grouphunks($p) {
2611 set l [lindex $h 2]
2612 if {$nl < $l} {
2613 for {} {$nl < $l} {incr nl} {
2614 set filelines($p,$f,$ol) $filelines($id,$f,$nl)
2615 incr ol
2618 foreach chunk [lindex $h 5] {
2619 if {[llength $chunk] == 2} {
2620 set olc [lindex $chunk 0]
2621 set nlc [lindex $chunk 1]
2622 set nnl [expr {$nl + $nlc}]
2623 lappend events [list $nl $nnl $pnum $olc $nlc]
2624 incr ol $olc
2625 set nl $nnl
2626 } else {
2627 incr ol [lindex $chunk 0]
2628 incr nl [lindex $chunk 0]
2633 if {$nl < $grouplineend} {
2634 for {} {$nl < $grouplineend} {incr nl} {
2635 set filelines($p,$f,$ol) $filelines($id,$f,$nl)
2636 incr ol
2639 set nlines [expr {$ol - $startline}]
2640 $ctext insert end " -$startline,$nlines" msep
2641 incr pnum
2644 set nlines [expr {$grouplineend - $grouplinestart}]
2645 $ctext insert end " +$grouplinestart,$nlines @@\n" msep
2647 set events [lsort -integer -index 0 $events]
2648 set nevents [llength $events]
2649 set nmerge $nparents($diffmergeid)
2650 set l $grouplinestart
2651 for {set i 0} {$i < $nevents} {set i $j} {
2652 set nl [lindex $events $i 0]
2653 while {$l < $nl} {
2654 $ctext insert end " $filelines($id,$f,$l)\n"
2655 incr l
2657 set e [lindex $events $i]
2658 set enl [lindex $e 1]
2659 set j $i
2660 set active {}
2661 while 1 {
2662 set pnum [lindex $e 2]
2663 set olc [lindex $e 3]
2664 set nlc [lindex $e 4]
2665 if {![info exists delta($pnum)]} {
2666 set delta($pnum) [expr {$olc - $nlc}]
2667 lappend active $pnum
2668 } else {
2669 incr delta($pnum) [expr {$olc - $nlc}]
2671 if {[incr j] >= $nevents} break
2672 set e [lindex $events $j]
2673 if {[lindex $e 0] >= $enl} break
2674 if {[lindex $e 1] > $enl} {
2675 set enl [lindex $e 1]
2678 set nlc [expr {$enl - $l}]
2679 set ncol mresult
2680 set bestpn -1
2681 if {[llength $active] == $nmerge - 1} {
2682 # no diff for one of the parents, i.e. it's identical
2683 for {set pnum 0} {$pnum < $nmerge} {incr pnum} {
2684 if {![info exists delta($pnum)]} {
2685 if {$pnum < $mergemax} {
2686 lappend ncol m$pnum
2687 } else {
2688 lappend ncol mmax
2690 break
2693 } elseif {[llength $active] == $nmerge} {
2694 # all parents are different, see if one is very similar
2695 set bestsim 30
2696 for {set pnum 0} {$pnum < $nmerge} {incr pnum} {
2697 set sim [similarity $pnum $l $nlc $f \
2698 [lrange $events $i [expr {$j-1}]]]
2699 if {$sim > $bestsim} {
2700 set bestsim $sim
2701 set bestpn $pnum
2704 if {$bestpn >= 0} {
2705 lappend ncol m$bestpn
2708 set pnum -1
2709 foreach p $parents($id) {
2710 incr pnum
2711 if {![info exists delta($pnum)] || $pnum == $bestpn} continue
2712 set olc [expr {$nlc + $delta($pnum)}]
2713 set ol [expr {$l + $diffoffset($p)}]
2714 incr diffoffset($p) $delta($pnum)
2715 unset delta($pnum)
2716 for {} {$olc > 0} {incr olc -1} {
2717 $ctext insert end "-$filelines($p,$f,$ol)\n" m$pnum
2718 incr ol
2721 set endl [expr {$l + $nlc}]
2722 if {$bestpn >= 0} {
2723 # show this pretty much as a normal diff
2724 set p [lindex $parents($id) $bestpn]
2725 set ol [expr {$l + $diffoffset($p)}]
2726 incr diffoffset($p) $delta($bestpn)
2727 unset delta($bestpn)
2728 for {set k $i} {$k < $j} {incr k} {
2729 set e [lindex $events $k]
2730 if {[lindex $e 2] != $bestpn} continue
2731 set nl [lindex $e 0]
2732 set ol [expr {$ol + $nl - $l}]
2733 for {} {$l < $nl} {incr l} {
2734 $ctext insert end "+$filelines($id,$f,$l)\n" $ncol
2736 set c [lindex $e 3]
2737 for {} {$c > 0} {incr c -1} {
2738 $ctext insert end "-$filelines($p,$f,$ol)\n" m$bestpn
2739 incr ol
2741 set nl [lindex $e 1]
2742 for {} {$l < $nl} {incr l} {
2743 $ctext insert end "+$filelines($id,$f,$l)\n" mresult
2747 for {} {$l < $endl} {incr l} {
2748 $ctext insert end "+$filelines($id,$f,$l)\n" $ncol
2751 while {$l < $grouplineend} {
2752 $ctext insert end " $filelines($id,$f,$l)\n"
2753 incr l
2755 $ctext conf -state disabled
2758 proc similarity {pnum l nlc f events} {
2759 global diffmergeid parents diffoffset filelines
2761 set id $diffmergeid
2762 set p [lindex $parents($id) $pnum]
2763 set ol [expr {$l + $diffoffset($p)}]
2764 set endl [expr {$l + $nlc}]
2765 set same 0
2766 set diff 0
2767 foreach e $events {
2768 if {[lindex $e 2] != $pnum} continue
2769 set nl [lindex $e 0]
2770 set ol [expr {$ol + $nl - $l}]
2771 for {} {$l < $nl} {incr l} {
2772 incr same [string length $filelines($id,$f,$l)]
2773 incr same
2775 set oc [lindex $e 3]
2776 for {} {$oc > 0} {incr oc -1} {
2777 incr diff [string length $filelines($p,$f,$ol)]
2778 incr diff
2779 incr ol
2781 set nl [lindex $e 1]
2782 for {} {$l < $nl} {incr l} {
2783 incr diff [string length $filelines($id,$f,$l)]
2784 incr diff
2787 for {} {$l < $endl} {incr l} {
2788 incr same [string length $filelines($id,$f,$l)]
2789 incr same
2791 if {$same == 0} {
2792 return 0
2794 return [expr {200 * $same / (2 * $same + $diff)}]
2797 proc startdiff {ids} {
2798 global treediffs diffids treepending diffmergeid
2800 set diffids $ids
2801 catch {unset diffmergeid}
2802 if {![info exists treediffs($ids)]} {
2803 if {![info exists treepending]} {
2804 gettreediffs $ids
2806 } else {
2807 addtocflist $ids
2811 proc addtocflist {ids} {
2812 global treediffs cflist
2813 foreach f $treediffs($ids) {
2814 $cflist insert end $f
2816 getblobdiffs $ids
2819 proc gettreediffs {ids} {
2820 global treediff parents treepending
2821 set treepending $ids
2822 set treediff {}
2823 set id [lindex $ids 0]
2824 set p [lindex $ids 1]
2825 if [catch {set gdtf [open "|git-diff-tree -r $id" r]}] return
2826 fconfigure $gdtf -blocking 0
2827 fileevent $gdtf readable [list gettreediffline $gdtf $ids]
2830 proc gettreediffline {gdtf ids} {
2831 global treediff treediffs treepending diffids diffmergeid
2833 set n [gets $gdtf line]
2834 if {$n < 0} {
2835 if {![eof $gdtf]} return
2836 close $gdtf
2837 set treediffs($ids) $treediff
2838 unset treepending
2839 if {$ids != $diffids} {
2840 gettreediffs $diffids
2841 } else {
2842 if {[info exists diffmergeid]} {
2843 contmergediff $ids
2844 } else {
2845 addtocflist $ids
2848 return
2850 set file [lindex $line 5]
2851 lappend treediff $file
2854 proc getblobdiffs {ids} {
2855 global diffopts blobdifffd diffids env curdifftag curtagstart
2856 global difffilestart nextupdate diffinhdr treediffs
2858 set id [lindex $ids 0]
2859 set p [lindex $ids 1]
2860 set env(GIT_DIFF_OPTS) $diffopts
2861 set cmd [list | git-diff-tree -r -p -C $id]
2862 if {[catch {set bdf [open $cmd r]} err]} {
2863 puts "error getting diffs: $err"
2864 return
2866 set diffinhdr 0
2867 fconfigure $bdf -blocking 0
2868 set blobdifffd($ids) $bdf
2869 set curdifftag Comments
2870 set curtagstart 0.0
2871 catch {unset difffilestart}
2872 fileevent $bdf readable [list getblobdiffline $bdf $diffids]
2873 set nextupdate [expr {[clock clicks -milliseconds] + 100}]
2876 proc getblobdiffline {bdf ids} {
2877 global diffids blobdifffd ctext curdifftag curtagstart
2878 global diffnexthead diffnextnote difffilestart
2879 global nextupdate diffinhdr treediffs
2880 global gaudydiff
2882 set n [gets $bdf line]
2883 if {$n < 0} {
2884 if {[eof $bdf]} {
2885 close $bdf
2886 if {$ids == $diffids && $bdf == $blobdifffd($ids)} {
2887 $ctext tag add $curdifftag $curtagstart end
2890 return
2892 if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
2893 return
2895 $ctext conf -state normal
2896 if {[regexp {^diff --git a/(.*) b/(.*)} $line match fname newname]} {
2897 # start of a new file
2898 $ctext insert end "\n"
2899 $ctext tag add $curdifftag $curtagstart end
2900 set curtagstart [$ctext index "end - 1c"]
2901 set header $newname
2902 set here [$ctext index "end - 1c"]
2903 set i [lsearch -exact $treediffs($diffids) $fname]
2904 if {$i >= 0} {
2905 set difffilestart($i) $here
2906 incr i
2907 $ctext mark set fmark.$i $here
2908 $ctext mark gravity fmark.$i left
2910 if {$newname != $fname} {
2911 set i [lsearch -exact $treediffs($diffids) $newname]
2912 if {$i >= 0} {
2913 set difffilestart($i) $here
2914 incr i
2915 $ctext mark set fmark.$i $here
2916 $ctext mark gravity fmark.$i left
2919 set curdifftag "f:$fname"
2920 $ctext tag delete $curdifftag
2921 set l [expr {(78 - [string length $header]) / 2}]
2922 set pad [string range "----------------------------------------" 1 $l]
2923 $ctext insert end "$pad $header $pad\n" filesep
2924 set diffinhdr 1
2925 } elseif {[regexp {^(---|\+\+\+)} $line]} {
2926 set diffinhdr 0
2927 } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
2928 $line match f1l f1c f2l f2c rest]} {
2929 if {$gaudydiff} {
2930 $ctext insert end "\t" hunksep
2931 $ctext insert end " $f1l " d0 " $f2l " d1
2932 $ctext insert end " $rest \n" hunksep
2933 } else {
2934 $ctext insert end "$line\n" hunksep
2936 set diffinhdr 0
2937 } else {
2938 set x [string range $line 0 0]
2939 if {$x == "-" || $x == "+"} {
2940 set tag [expr {$x == "+"}]
2941 if {$gaudydiff} {
2942 set line [string range $line 1 end]
2944 $ctext insert end "$line\n" d$tag
2945 } elseif {$x == " "} {
2946 if {$gaudydiff} {
2947 set line [string range $line 1 end]
2949 $ctext insert end "$line\n"
2950 } elseif {$diffinhdr || $x == "\\"} {
2951 # e.g. "\ No newline at end of file"
2952 $ctext insert end "$line\n" filesep
2953 } else {
2954 # Something else we don't recognize
2955 if {$curdifftag != "Comments"} {
2956 $ctext insert end "\n"
2957 $ctext tag add $curdifftag $curtagstart end
2958 set curtagstart [$ctext index "end - 1c"]
2959 set curdifftag Comments
2961 $ctext insert end "$line\n" filesep
2964 $ctext conf -state disabled
2965 if {[clock clicks -milliseconds] >= $nextupdate} {
2966 incr nextupdate 100
2967 fileevent $bdf readable {}
2968 update
2969 fileevent $bdf readable "getblobdiffline $bdf {$ids}"
2973 proc nextfile {} {
2974 global difffilestart ctext
2975 set here [$ctext index @0,0]
2976 for {set i 0} {[info exists difffilestart($i)]} {incr i} {
2977 if {[$ctext compare $difffilestart($i) > $here]} {
2978 if {![info exists pos]
2979 || [$ctext compare $difffilestart($i) < $pos]} {
2980 set pos $difffilestart($i)
2984 if {[info exists pos]} {
2985 $ctext yview $pos
2989 proc listboxsel {} {
2990 global ctext cflist currentid
2991 if {![info exists currentid]} return
2992 set sel [lsort [$cflist curselection]]
2993 if {$sel eq {}} return
2994 set first [lindex $sel 0]
2995 catch {$ctext yview fmark.$first}
2998 proc setcoords {} {
2999 global linespc charspc canvx0 canvy0 mainfont
3000 global xspc1 xspc2 lthickness
3002 set linespc [font metrics $mainfont -linespace]
3003 set charspc [font measure $mainfont "m"]
3004 set canvy0 [expr 3 + 0.5 * $linespc]
3005 set canvx0 [expr 3 + 0.5 * $linespc]
3006 set lthickness [expr {int($linespc / 9) + 1}]
3007 set xspc1(0) $linespc
3008 set xspc2 $linespc
3011 proc redisplay {} {
3012 global stopped redisplaying phase
3013 if {$stopped > 1} return
3014 if {$phase == "getcommits"} return
3015 set redisplaying 1
3016 if {$phase == "drawgraph" || $phase == "incrdraw"} {
3017 set stopped 1
3018 } else {
3019 drawgraph
3023 proc incrfont {inc} {
3024 global mainfont namefont textfont ctext canv phase
3025 global stopped entries
3026 unmarkmatches
3027 set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
3028 set namefont [lreplace $namefont 1 1 [expr {[lindex $namefont 1] + $inc}]]
3029 set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
3030 setcoords
3031 $ctext conf -font $textfont
3032 $ctext tag conf filesep -font [concat $textfont bold]
3033 foreach e $entries {
3034 $e conf -font $mainfont
3036 if {$phase == "getcommits"} {
3037 $canv itemconf textitems -font $mainfont
3039 redisplay
3042 proc clearsha1 {} {
3043 global sha1entry sha1string
3044 if {[string length $sha1string] == 40} {
3045 $sha1entry delete 0 end
3049 proc sha1change {n1 n2 op} {
3050 global sha1string currentid sha1but
3051 if {$sha1string == {}
3052 || ([info exists currentid] && $sha1string == $currentid)} {
3053 set state disabled
3054 } else {
3055 set state normal
3057 if {[$sha1but cget -state] == $state} return
3058 if {$state == "normal"} {
3059 $sha1but conf -state normal -relief raised -text "Goto: "
3060 } else {
3061 $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
3065 proc gotocommit {} {
3066 global sha1string currentid idline tagids
3067 global lineid numcommits
3069 if {$sha1string == {}
3070 || ([info exists currentid] && $sha1string == $currentid)} return
3071 if {[info exists tagids($sha1string)]} {
3072 set id $tagids($sha1string)
3073 } else {
3074 set id [string tolower $sha1string]
3075 if {[regexp {^[0-9a-f]{4,39}$} $id]} {
3076 set matches {}
3077 for {set l 0} {$l < $numcommits} {incr l} {
3078 if {[string match $id* $lineid($l)]} {
3079 lappend matches $lineid($l)
3082 if {$matches ne {}} {
3083 if {[llength $matches] > 1} {
3084 error_popup "Short SHA1 id $id is ambiguous"
3085 return
3087 set id [lindex $matches 0]
3091 if {[info exists idline($id)]} {
3092 selectline $idline($id) 1
3093 return
3095 if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
3096 set type "SHA1 id"
3097 } else {
3098 set type "Tag"
3100 error_popup "$type $sha1string is not known"
3103 proc lineenter {x y id} {
3104 global hoverx hovery hoverid hovertimer
3105 global commitinfo canv
3107 if {![info exists commitinfo($id)]} return
3108 set hoverx $x
3109 set hovery $y
3110 set hoverid $id
3111 if {[info exists hovertimer]} {
3112 after cancel $hovertimer
3114 set hovertimer [after 500 linehover]
3115 $canv delete hover
3118 proc linemotion {x y id} {
3119 global hoverx hovery hoverid hovertimer
3121 if {[info exists hoverid] && $id == $hoverid} {
3122 set hoverx $x
3123 set hovery $y
3124 if {[info exists hovertimer]} {
3125 after cancel $hovertimer
3127 set hovertimer [after 500 linehover]
3131 proc lineleave {id} {
3132 global hoverid hovertimer canv
3134 if {[info exists hoverid] && $id == $hoverid} {
3135 $canv delete hover
3136 if {[info exists hovertimer]} {
3137 after cancel $hovertimer
3138 unset hovertimer
3140 unset hoverid
3144 proc linehover {} {
3145 global hoverx hovery hoverid hovertimer
3146 global canv linespc lthickness
3147 global commitinfo mainfont
3149 set text [lindex $commitinfo($hoverid) 0]
3150 set ymax [lindex [$canv cget -scrollregion] 3]
3151 if {$ymax == {}} return
3152 set yfrac [lindex [$canv yview] 0]
3153 set x [expr {$hoverx + 2 * $linespc}]
3154 set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
3155 set x0 [expr {$x - 2 * $lthickness}]
3156 set y0 [expr {$y - 2 * $lthickness}]
3157 set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}]
3158 set y1 [expr {$y + $linespc + 2 * $lthickness}]
3159 set t [$canv create rectangle $x0 $y0 $x1 $y1 \
3160 -fill \#ffff80 -outline black -width 1 -tags hover]
3161 $canv raise $t
3162 set t [$canv create text $x $y -anchor nw -text $text -tags hover]
3163 $canv raise $t
3166 proc clickisonarrow {id y} {
3167 global mainline mainlinearrow sidelines lthickness
3169 set thresh [expr {2 * $lthickness + 6}]
3170 if {[info exists mainline($id)]} {
3171 if {$mainlinearrow($id) ne "none"} {
3172 if {abs([lindex $mainline($id) 1] - $y) < $thresh} {
3173 return "up"
3177 if {[info exists sidelines($id)]} {
3178 foreach ls $sidelines($id) {
3179 set coords [lindex $ls 0]
3180 set arrow [lindex $ls 2]
3181 if {$arrow eq "first" || $arrow eq "both"} {
3182 if {abs([lindex $coords 1] - $y) < $thresh} {
3183 return "up"
3186 if {$arrow eq "last" || $arrow eq "both"} {
3187 if {abs([lindex $coords end] - $y) < $thresh} {
3188 return "down"
3193 return {}
3196 proc arrowjump {id dirn y} {
3197 global mainline sidelines canv
3199 set yt {}
3200 if {$dirn eq "down"} {
3201 if {[info exists mainline($id)]} {
3202 set y1 [lindex $mainline($id) 1]
3203 if {$y1 > $y} {
3204 set yt $y1
3207 if {[info exists sidelines($id)]} {
3208 foreach ls $sidelines($id) {
3209 set y1 [lindex $ls 0 1]
3210 if {$y1 > $y && ($yt eq {} || $y1 < $yt)} {
3211 set yt $y1
3215 } else {
3216 if {[info exists sidelines($id)]} {
3217 foreach ls $sidelines($id) {
3218 set y1 [lindex $ls 0 end]
3219 if {$y1 < $y && ($yt eq {} || $y1 > $yt)} {
3220 set yt $y1
3225 if {$yt eq {}} return
3226 set ymax [lindex [$canv cget -scrollregion] 3]
3227 if {$ymax eq {} || $ymax <= 0} return
3228 set view [$canv yview]
3229 set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
3230 set yfrac [expr {$yt / $ymax - $yspan / 2}]
3231 if {$yfrac < 0} {
3232 set yfrac 0
3234 $canv yview moveto $yfrac
3237 proc lineclick {x y id isnew} {
3238 global ctext commitinfo children cflist canv thickerline
3240 unmarkmatches
3241 unselectline
3242 normalline
3243 $canv delete hover
3244 # draw this line thicker than normal
3245 drawlines $id 1 1
3246 set thickerline $id
3247 if {$isnew} {
3248 set ymax [lindex [$canv cget -scrollregion] 3]
3249 if {$ymax eq {}} return
3250 set yfrac [lindex [$canv yview] 0]
3251 set y [expr {$y + $yfrac * $ymax}]
3253 set dirn [clickisonarrow $id $y]
3254 if {$dirn ne {}} {
3255 arrowjump $id $dirn $y
3256 return
3259 if {$isnew} {
3260 addtohistory [list lineclick $x $y $id 0]
3262 # fill the details pane with info about this line
3263 $ctext conf -state normal
3264 $ctext delete 0.0 end
3265 $ctext tag conf link -foreground blue -underline 1
3266 $ctext tag bind link <Enter> { %W configure -cursor hand2 }
3267 $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
3268 $ctext insert end "Parent:\t"
3269 $ctext insert end $id [list link link0]
3270 $ctext tag bind link0 <1> [list selbyid $id]
3271 set info $commitinfo($id)
3272 $ctext insert end "\n\t[lindex $info 0]\n"
3273 $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
3274 set date [formatdate [lindex $info 2]]
3275 $ctext insert end "\tDate:\t$date\n"
3276 if {[info exists children($id)]} {
3277 $ctext insert end "\nChildren:"
3278 set i 0
3279 foreach child $children($id) {
3280 incr i
3281 set info $commitinfo($child)
3282 $ctext insert end "\n\t"
3283 $ctext insert end $child [list link link$i]
3284 $ctext tag bind link$i <1> [list selbyid $child]
3285 $ctext insert end "\n\t[lindex $info 0]"
3286 $ctext insert end "\n\tAuthor:\t[lindex $info 1]"
3287 set date [formatdate [lindex $info 2]]
3288 $ctext insert end "\n\tDate:\t$date\n"
3291 $ctext conf -state disabled
3293 $cflist delete 0 end
3296 proc normalline {} {
3297 global thickerline
3298 if {[info exists thickerline]} {
3299 drawlines $thickerline 0 1
3300 unset thickerline
3304 proc selbyid {id} {
3305 global idline
3306 if {[info exists idline($id)]} {
3307 selectline $idline($id) 1
3311 proc mstime {} {
3312 global startmstime
3313 if {![info exists startmstime]} {
3314 set startmstime [clock clicks -milliseconds]
3316 return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
3319 proc rowmenu {x y id} {
3320 global rowctxmenu idline selectedline rowmenuid
3322 if {![info exists selectedline] || $idline($id) eq $selectedline} {
3323 set state disabled
3324 } else {
3325 set state normal
3327 $rowctxmenu entryconfigure 0 -state $state
3328 $rowctxmenu entryconfigure 1 -state $state
3329 $rowctxmenu entryconfigure 2 -state $state
3330 set rowmenuid $id
3331 tk_popup $rowctxmenu $x $y
3334 proc diffvssel {dirn} {
3335 global rowmenuid selectedline lineid
3337 if {![info exists selectedline]} return
3338 if {$dirn} {
3339 set oldid $lineid($selectedline)
3340 set newid $rowmenuid
3341 } else {
3342 set oldid $rowmenuid
3343 set newid $lineid($selectedline)
3345 addtohistory [list doseldiff $oldid $newid]
3346 doseldiff $oldid $newid
3349 proc doseldiff {oldid newid} {
3350 global ctext cflist
3351 global commitinfo
3353 $ctext conf -state normal
3354 $ctext delete 0.0 end
3355 $ctext mark set fmark.0 0.0
3356 $ctext mark gravity fmark.0 left
3357 $cflist delete 0 end
3358 $cflist insert end "Top"
3359 $ctext insert end "From "
3360 $ctext tag conf link -foreground blue -underline 1
3361 $ctext tag bind link <Enter> { %W configure -cursor hand2 }
3362 $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
3363 $ctext tag bind link0 <1> [list selbyid $oldid]
3364 $ctext insert end $oldid [list link link0]
3365 $ctext insert end "\n "
3366 $ctext insert end [lindex $commitinfo($oldid) 0]
3367 $ctext insert end "\n\nTo "
3368 $ctext tag bind link1 <1> [list selbyid $newid]
3369 $ctext insert end $newid [list link link1]
3370 $ctext insert end "\n "
3371 $ctext insert end [lindex $commitinfo($newid) 0]
3372 $ctext insert end "\n"
3373 $ctext conf -state disabled
3374 $ctext tag delete Comments
3375 $ctext tag remove found 1.0 end
3376 startdiff [list $newid $oldid]
3379 proc mkpatch {} {
3380 global rowmenuid currentid commitinfo patchtop patchnum
3382 if {![info exists currentid]} return
3383 set oldid $currentid
3384 set oldhead [lindex $commitinfo($oldid) 0]
3385 set newid $rowmenuid
3386 set newhead [lindex $commitinfo($newid) 0]
3387 set top .patch
3388 set patchtop $top
3389 catch {destroy $top}
3390 toplevel $top
3391 label $top.title -text "Generate patch"
3392 grid $top.title - -pady 10
3393 label $top.from -text "From:"
3394 entry $top.fromsha1 -width 40 -relief flat
3395 $top.fromsha1 insert 0 $oldid
3396 $top.fromsha1 conf -state readonly
3397 grid $top.from $top.fromsha1 -sticky w
3398 entry $top.fromhead -width 60 -relief flat
3399 $top.fromhead insert 0 $oldhead
3400 $top.fromhead conf -state readonly
3401 grid x $top.fromhead -sticky w
3402 label $top.to -text "To:"
3403 entry $top.tosha1 -width 40 -relief flat
3404 $top.tosha1 insert 0 $newid
3405 $top.tosha1 conf -state readonly
3406 grid $top.to $top.tosha1 -sticky w
3407 entry $top.tohead -width 60 -relief flat
3408 $top.tohead insert 0 $newhead
3409 $top.tohead conf -state readonly
3410 grid x $top.tohead -sticky w
3411 button $top.rev -text "Reverse" -command mkpatchrev -padx 5
3412 grid $top.rev x -pady 10
3413 label $top.flab -text "Output file:"
3414 entry $top.fname -width 60
3415 $top.fname insert 0 [file normalize "patch$patchnum.patch"]
3416 incr patchnum
3417 grid $top.flab $top.fname -sticky w
3418 frame $top.buts
3419 button $top.buts.gen -text "Generate" -command mkpatchgo
3420 button $top.buts.can -text "Cancel" -command mkpatchcan
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 mkpatchrev {} {
3429 global patchtop
3431 set oldid [$patchtop.fromsha1 get]
3432 set oldhead [$patchtop.fromhead get]
3433 set newid [$patchtop.tosha1 get]
3434 set newhead [$patchtop.tohead get]
3435 foreach e [list fromsha1 fromhead tosha1 tohead] \
3436 v [list $newid $newhead $oldid $oldhead] {
3437 $patchtop.$e conf -state normal
3438 $patchtop.$e delete 0 end
3439 $patchtop.$e insert 0 $v
3440 $patchtop.$e conf -state readonly
3444 proc mkpatchgo {} {
3445 global patchtop
3447 set oldid [$patchtop.fromsha1 get]
3448 set newid [$patchtop.tosha1 get]
3449 set fname [$patchtop.fname get]
3450 if {[catch {exec git-diff-tree -p $oldid $newid >$fname &} err]} {
3451 error_popup "Error creating patch: $err"
3453 catch {destroy $patchtop}
3454 unset patchtop
3457 proc mkpatchcan {} {
3458 global patchtop
3460 catch {destroy $patchtop}
3461 unset patchtop
3464 proc mktag {} {
3465 global rowmenuid mktagtop commitinfo
3467 set top .maketag
3468 set mktagtop $top
3469 catch {destroy $top}
3470 toplevel $top
3471 label $top.title -text "Create tag"
3472 grid $top.title - -pady 10
3473 label $top.id -text "ID:"
3474 entry $top.sha1 -width 40 -relief flat
3475 $top.sha1 insert 0 $rowmenuid
3476 $top.sha1 conf -state readonly
3477 grid $top.id $top.sha1 -sticky w
3478 entry $top.head -width 60 -relief flat
3479 $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
3480 $top.head conf -state readonly
3481 grid x $top.head -sticky w
3482 label $top.tlab -text "Tag name:"
3483 entry $top.tag -width 60
3484 grid $top.tlab $top.tag -sticky w
3485 frame $top.buts
3486 button $top.buts.gen -text "Create" -command mktaggo
3487 button $top.buts.can -text "Cancel" -command mktagcan
3488 grid $top.buts.gen $top.buts.can
3489 grid columnconfigure $top.buts 0 -weight 1 -uniform a
3490 grid columnconfigure $top.buts 1 -weight 1 -uniform a
3491 grid $top.buts - -pady 10 -sticky ew
3492 focus $top.tag
3495 proc domktag {} {
3496 global mktagtop env tagids idtags
3498 set id [$mktagtop.sha1 get]
3499 set tag [$mktagtop.tag get]
3500 if {$tag == {}} {
3501 error_popup "No tag name specified"
3502 return
3504 if {[info exists tagids($tag)]} {
3505 error_popup "Tag \"$tag\" already exists"
3506 return
3508 if {[catch {
3509 set dir [gitdir]
3510 set fname [file join $dir "refs/tags" $tag]
3511 set f [open $fname w]
3512 puts $f $id
3513 close $f
3514 } err]} {
3515 error_popup "Error creating tag: $err"
3516 return
3519 set tagids($tag) $id
3520 lappend idtags($id) $tag
3521 redrawtags $id
3524 proc redrawtags {id} {
3525 global canv linehtag idline idpos selectedline
3527 if {![info exists idline($id)]} return
3528 $canv delete tag.$id
3529 set xt [eval drawtags $id $idpos($id)]
3530 $canv coords $linehtag($idline($id)) $xt [lindex $idpos($id) 2]
3531 if {[info exists selectedline] && $selectedline == $idline($id)} {
3532 selectline $selectedline 0
3536 proc mktagcan {} {
3537 global mktagtop
3539 catch {destroy $mktagtop}
3540 unset mktagtop
3543 proc mktaggo {} {
3544 domktag
3545 mktagcan
3548 proc writecommit {} {
3549 global rowmenuid wrcomtop commitinfo wrcomcmd
3551 set top .writecommit
3552 set wrcomtop $top
3553 catch {destroy $top}
3554 toplevel $top
3555 label $top.title -text "Write commit to file"
3556 grid $top.title - -pady 10
3557 label $top.id -text "ID:"
3558 entry $top.sha1 -width 40 -relief flat
3559 $top.sha1 insert 0 $rowmenuid
3560 $top.sha1 conf -state readonly
3561 grid $top.id $top.sha1 -sticky w
3562 entry $top.head -width 60 -relief flat
3563 $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
3564 $top.head conf -state readonly
3565 grid x $top.head -sticky w
3566 label $top.clab -text "Command:"
3567 entry $top.cmd -width 60 -textvariable wrcomcmd
3568 grid $top.clab $top.cmd -sticky w -pady 10
3569 label $top.flab -text "Output file:"
3570 entry $top.fname -width 60
3571 $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
3572 grid $top.flab $top.fname -sticky w
3573 frame $top.buts
3574 button $top.buts.gen -text "Write" -command wrcomgo
3575 button $top.buts.can -text "Cancel" -command wrcomcan
3576 grid $top.buts.gen $top.buts.can
3577 grid columnconfigure $top.buts 0 -weight 1 -uniform a
3578 grid columnconfigure $top.buts 1 -weight 1 -uniform a
3579 grid $top.buts - -pady 10 -sticky ew
3580 focus $top.fname
3583 proc wrcomgo {} {
3584 global wrcomtop
3586 set id [$wrcomtop.sha1 get]
3587 set cmd "echo $id | [$wrcomtop.cmd get]"
3588 set fname [$wrcomtop.fname get]
3589 if {[catch {exec sh -c $cmd >$fname &} err]} {
3590 error_popup "Error writing commit: $err"
3592 catch {destroy $wrcomtop}
3593 unset wrcomtop
3596 proc wrcomcan {} {
3597 global wrcomtop
3599 catch {destroy $wrcomtop}
3600 unset wrcomtop
3603 proc listrefs {id} {
3604 global idtags idheads idotherrefs
3606 set x {}
3607 if {[info exists idtags($id)]} {
3608 set x $idtags($id)
3610 set y {}
3611 if {[info exists idheads($id)]} {
3612 set y $idheads($id)
3614 set z {}
3615 if {[info exists idotherrefs($id)]} {
3616 set z $idotherrefs($id)
3618 return [list $x $y $z]
3621 proc rereadrefs {} {
3622 global idtags idheads idotherrefs
3623 global tagids headids otherrefids
3625 set refids [concat [array names idtags] \
3626 [array names idheads] [array names idotherrefs]]
3627 foreach id $refids {
3628 if {![info exists ref($id)]} {
3629 set ref($id) [listrefs $id]
3632 foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
3633 catch {unset $v}
3635 readrefs
3636 set refids [lsort -unique [concat $refids [array names idtags] \
3637 [array names idheads] [array names idotherrefs]]]
3638 foreach id $refids {
3639 set v [listrefs $id]
3640 if {![info exists ref($id)] || $ref($id) != $v} {
3641 redrawtags $id
3646 proc showtag {tag isnew} {
3647 global ctext cflist tagcontents tagids linknum
3649 if {$isnew} {
3650 addtohistory [list showtag $tag 0]
3652 $ctext conf -state normal
3653 $ctext delete 0.0 end
3654 set linknum 0
3655 if {[info exists tagcontents($tag)]} {
3656 set text $tagcontents($tag)
3657 } else {
3658 set text "Tag: $tag\nId: $tagids($tag)"
3660 appendwithlinks $text
3661 $ctext conf -state disabled
3662 $cflist delete 0 end
3665 proc doquit {} {
3666 global stopped
3667 set stopped 100
3668 destroy .
3671 proc formatdate {d} {
3672 global hours nhours tfd
3674 set hr [expr {$d / 3600}]
3675 set ms [expr {$d % 3600}]
3676 if {![info exists hours($hr)]} {
3677 set hours($hr) [clock format $d -format "%Y-%m-%d %H"]
3678 set nhours($hr) 0
3680 incr nhours($hr)
3681 set minsec [format "%.2d:%.2d" [expr {$ms/60}] [expr {$ms%60}]]
3682 return "$hours($hr):$minsec"
3685 # defaults...
3686 set datemode 0
3687 set boldnames 0
3688 set diffopts "-U 5 -p"
3689 set wrcomcmd "git-diff-tree --stdin -p --pretty"
3691 set mainfont {Helvetica 9}
3692 set textfont {Courier 9}
3693 set findmergefiles 0
3694 set gaudydiff 0
3695 set maxgraphpct 50
3696 set maxwidth 16
3697 set revlistorder 0
3699 set colors {green red blue magenta darkgrey brown orange}
3701 catch {source ~/.gitk}
3703 set namefont $mainfont
3704 if {$boldnames} {
3705 lappend namefont bold
3708 set revtreeargs {}
3709 foreach arg $argv {
3710 switch -regexp -- $arg {
3711 "^$" { }
3712 "^-b" { set boldnames 1 }
3713 "^-d" { set datemode 1 }
3714 "^-r" { set revlistorder 1 }
3715 default {
3716 lappend revtreeargs $arg
3721 set history {}
3722 set historyindex 0
3724 set stopped 0
3725 set redisplaying 0
3726 set stuffsaved 0
3727 set patchnum 0
3728 setcoords
3729 makewindow
3730 readrefs
3731 getcommits $revtreeargs