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