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
10# CVS $Revision: 1.6 $
11
12set datemode 0
13set boldnames 0
14set revtreeargs {}
15set diffopts "-U 5 -p"
16
17set mainfont {Helvetica 9}
18set namefont $mainfont
19set textfont {Courier 9}
20if {$boldnames} {
21 lappend namefont bold
22}
23
24set colors {green red blue magenta darkgrey brown orange}
25set colorbycommitter false
26
27catch {source ~/.gitk}
28
29foreach arg $argv {
30 switch -regexp -- $arg {
31 "^$" { }
32 "^-b" { set boldnames 1 }
33 "^-c" { set colorbycommitter 1 }
34 "^-d" { set datemode 1 }
35 "^-.*" {
36 puts stderr "unrecognized option $arg"
37 exit 1
38 }
39 default {
40 lappend revtreeargs $arg
41 }
42 }
43}
44
45proc getcommits {rargs} {
46 global commits parents cdate nparents children nchildren
47 if {$rargs == {}} {
48 set rargs HEAD
49 }
50 set commits {}
51 foreach c [split [eval exec git-rev-tree $rargs] "\n"] {
52 set i 0
53 set cid {}
54 foreach f $c {
55 if {$i == 0} {
56 set d $f
57 } else {
58 set id [lindex [split $f :] 0]
59 if {![info exists nchildren($id)]} {
60 set children($id) {}
61 set nchildren($id) 0
62 }
63 if {$i == 1} {
64 set cid $id
65 lappend commits $id
66 set parents($id) {}
67 set cdate($id) $d
68 set nparents($id) 0
69 } else {
70 lappend parents($cid) $id
71 incr nparents($cid)
72 incr nchildren($id)
73 lappend children($id) $cid
74 }
75 }
76 incr i
77 }
78 }
79}
80
81proc readcommit {id} {
82 global commitinfo
83 set inhdr 1
84 set comment {}
85 set headline {}
86 set auname {}
87 set audate {}
88 set comname {}
89 set comdate {}
90 foreach line [split [exec git-cat-file commit $id] "\n"] {
91 if {$inhdr} {
92 if {$line == {}} {
93 set inhdr 0
94 } else {
95 set tag [lindex $line 0]
96 if {$tag == "author"} {
97 set x [expr {[llength $line] - 2}]
98 set audate [lindex $line $x]
99 set auname [lrange $line 1 [expr {$x - 1}]]
100 } elseif {$tag == "committer"} {
101 set x [expr {[llength $line] - 2}]
102 set comdate [lindex $line $x]
103 set comname [lrange $line 1 [expr {$x - 1}]]
104 }
105 }
106 } else {
107 if {$comment == {}} {
108 set headline $line
109 } else {
110 append comment "\n"
111 }
112 append comment $line
113 }
114 }
115 if {$audate != {}} {
116 set audate [clock format $audate -format "%Y-%m-%d %H:%M:%S"]
117 }
118 if {$comdate != {}} {
119 set comdate [clock format $comdate -format "%Y-%m-%d %H:%M:%S"]
120 }
121 set commitinfo($id) [list $headline $auname $audate \
122 $comname $comdate $comment]
123}
124
125proc makewindow {} {
126 global canv canv2 canv3 linespc charspc ctext cflist textfont
127 panedwindow .ctop -orient vertical
128 panedwindow .ctop.clist -orient horizontal -sashpad 0 -handlesize 4
129 .ctop add .ctop.clist
130 set canv .ctop.clist.canv
131 set cscroll .ctop.clist.dates.csb
132 set height [expr 25 * $linespc + 4]
133 canvas $canv -height $height -width [expr 45 * $charspc] \
134 -bg white -bd 0 \
135 -yscrollincr $linespc -yscrollcommand "$cscroll set"
136 .ctop.clist add $canv
137 set canv2 .ctop.clist.canv2
138 canvas $canv2 -height $height -width [expr 30 * $charspc] \
139 -bg white -bd 0 -yscrollincr $linespc
140 .ctop.clist add $canv2
141 frame .ctop.clist.dates
142 .ctop.clist add .ctop.clist.dates
143 set canv3 .ctop.clist.dates.canv3
144 canvas $canv3 -height $height -width [expr 15 * $charspc] \
145 -bg white -bd 0 -yscrollincr $linespc
146 scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
147 pack .ctop.clist.dates.csb -side right -fill y
148 pack $canv3 -side left -fill both -expand 1
149
150 panedwindow .ctop.cdet -orient horizontal
151 .ctop add .ctop.cdet
152 frame .ctop.cdet.left
153 set ctext .ctop.cdet.left.ctext
154 text $ctext -bg white -state disabled -font $textfont -height 32 \
155 -yscrollcommand ".ctop.cdet.left.sb set"
156 scrollbar .ctop.cdet.left.sb -command "$ctext yview"
157 pack .ctop.cdet.left.sb -side right -fill y
158 pack $ctext -side left -fill both -expand 1
159 .ctop.cdet add .ctop.cdet.left
160
161 $ctext tag conf filesep -font [concat $textfont bold]
162 $ctext tag conf hunksep -back blue -fore white
163 $ctext tag conf d0 -back "#ff8080"
164 $ctext tag conf d1 -back green
165
166 frame .ctop.cdet.right
167 set cflist .ctop.cdet.right.cfiles
168 listbox $cflist -width 30 -bg white -selectmode extended \
169 -yscrollcommand ".ctop.cdet.right.sb set"
170 scrollbar .ctop.cdet.right.sb -command "$cflist yview"
171 pack .ctop.cdet.right.sb -side right -fill y
172 pack $cflist -side left -fill both -expand 1
173 .ctop.cdet add .ctop.cdet.right
174
175 pack .ctop -side top -fill both -expand 1
176
177 bindall <1> {selcanvline %x %y}
178 bindall <B1-Motion> {selcanvline %x %y}
179 bindall <ButtonRelease-4> "allcanvs yview scroll -5 u"
180 bindall <ButtonRelease-5> "allcanvs yview scroll 5 u"
181 bindall <2> "allcanvs scan mark 0 %y"
182 bindall <B2-Motion> "allcanvs scan dragto 0 %y"
183 bind . <Key-Up> "selnextline -1"
184 bind . <Key-Down> "selnextline 1"
185 bind . p "selnextline -1"
186 bind . n "selnextline 1"
187 bind . <Key-Prior> "allcanvs yview scroll -1 p"
188 bind . <Key-Next> "allcanvs yview scroll 1 p"
189 bind . <Key-Delete> "$ctext yview scroll -1 p"
190 bind . <Key-BackSpace> "$ctext yview scroll -1 p"
191 bind . <Key-space> "$ctext yview scroll 1 p"
192 bind . b "$ctext yview scroll -1 p"
193 bind . d "$ctext yview scroll 18 u"
194 bind . u "$ctext yview scroll -18 u"
195 bind . Q "set stopped 1; destroy ."
196 bind $cflist <<ListboxSelect>> listboxsel
197}
198
199proc allcanvs args {
200 global canv canv2 canv3
201 eval $canv $args
202 eval $canv2 $args
203 eval $canv3 $args
204}
205
206proc bindall {event action} {
207 global canv canv2 canv3
208 bind $canv $event $action
209 bind $canv2 $event $action
210 bind $canv3 $event $action
211}
212
213proc truncatetofit {str width font} {
214 if {[font measure $font $str] <= $width} {
215 return $str
216 }
217 set best 0
218 set bad [string length $str]
219 set tmp $str
220 while {$best < $bad - 1} {
221 set try [expr {int(($best + $bad) / 2)}]
222 set tmp "[string range $str 0 [expr $try-1]]..."
223 if {[font measure $font $tmp] <= $width} {
224 set best $try
225 } else {
226 set bad $try
227 }
228 }
229 return $tmp
230}
231
232proc assigncolor {id} {
233 global commitinfo colormap commcolors colors nextcolor
234 global colorbycommitter
235 global parents nparents children nchildren
236 if [info exists colormap($id)] return
237 set ncolors [llength $colors]
238 if {$colorbycommitter} {
239 if {![info exists commitinfo($id)]} {
240 readcommit $id
241 }
242 set comm [lindex $commitinfo($id) 3]
243 if {![info exists commcolors($comm)]} {
244 set commcolors($comm) [lindex $colors $nextcolor]
245 if {[incr nextcolor] >= $ncolors} {
246 set nextcolor 0
247 }
248 }
249 set colormap($id) $commcolors($comm)
250 } else {
251 if {$nparents($id) == 1 && $nchildren($id) == 1} {
252 set child [lindex $children($id) 0]
253 if {[info exists colormap($child)]
254 && $nparents($child) == 1} {
255 set colormap($id) $colormap($child)
256 return
257 }
258 }
259 set badcolors {}
260 foreach child $children($id) {
261 if {[info exists colormap($child)]
262 && [lsearch -exact $badcolors $colormap($child)] < 0} {
263 lappend badcolors $colormap($child)
264 }
265 if {[info exists parents($child)]} {
266 foreach p $parents($child) {
267 if {[info exists colormap($p)]
268 && [lsearch -exact $badcolors $colormap($p)] < 0} {
269 lappend badcolors $colormap($p)
270 }
271 }
272 }
273 }
274 if {[llength $badcolors] >= $ncolors} {
275 set badcolors {}
276 }
277 for {set i 0} {$i <= $ncolors} {incr i} {
278 set c [lindex $colors $nextcolor]
279 if {[incr nextcolor] >= $ncolors} {
280 set nextcolor 0
281 }
282 if {[lsearch -exact $badcolors $c]} break
283 }
284 set colormap($id) $c
285 }
286}
287
288proc drawgraph {start} {
289 global parents children nparents nchildren commits
290 global canv canv2 canv3 mainfont namefont canvx0 canvy0 canvy linespc
291 global datemode cdate
292 global lineid linehtag linentag linedtag commitinfo
293 global nextcolor colormap
294
295 set nextcolor 0
296 assigncolor $start
297 foreach id $commits {
298 set ncleft($id) $nchildren($id)
299 }
300 set todo [list $start]
301 set level 0
302 set y2 $canvy0
303 set linestarty(0) $canvy0
304 set nullentry -1
305 set lineno -1
306 while 1 {
307 set canvy $y2
308 allcanvs conf -scrollregion [list 0 0 0 $canvy]
309 update
310 incr lineno
311 set nlines [llength $todo]
312 set id [lindex $todo $level]
313 set lineid($lineno) $id
314 set actualparents {}
315 foreach p $parents($id) {
316 if {[info exists ncleft($p)]} {
317 incr ncleft($p) -1
318 lappend actualparents $p
319 }
320 }
321 if {![info exists commitinfo($id)]} {
322 readcommit $id
323 }
324 set x [expr $canvx0 + $level * $linespc]
325 set y2 [expr $canvy + $linespc]
326 if {$linestarty($level) < $canvy} {
327 set t [$canv create line $x $linestarty($level) $x $canvy \
328 -width 2 -fill $colormap($id)]
329 $canv lower $t
330 set linestarty($level) $canvy
331 }
332 set t [$canv create oval [expr $x - 4] [expr $canvy - 4] \
333 [expr $x + 3] [expr $canvy + 3] \
334 -fill blue -outline black -width 1]
335 $canv raise $t
336 set xt [expr $canvx0 + $nlines * $linespc]
337 set headline [lindex $commitinfo($id) 0]
338 set name [lindex $commitinfo($id) 1]
339 set date [lindex $commitinfo($id) 2]
340 set linehtag($lineno) [$canv create text $xt $canvy -anchor w \
341 -text $headline -font $mainfont ]
342 set linentag($lineno) [$canv2 create text 3 $canvy -anchor w \
343 -text $name -font $namefont]
344 set linedtag($lineno) [$canv3 create text 3 $canvy -anchor w \
345 -text $date -font $mainfont]
346 if {!$datemode && [llength $actualparents] == 1} {
347 set p [lindex $actualparents 0]
348 if {$ncleft($p) == 0 && [lsearch -exact $todo $p] < 0} {
349 assigncolor $p
350 set todo [lreplace $todo $level $level $p]
351 continue
352 }
353 }
354
355 set oldtodo $todo
356 set oldlevel $level
357 set lines {}
358 for {set i 0} {$i < $nlines} {incr i} {
359 if {[lindex $todo $i] == {}} continue
360 set oldstarty($i) $linestarty($i)
361 if {$i != $level} {
362 lappend lines [list $i [lindex $todo $i]]
363 }
364 }
365 unset linestarty
366 if {$nullentry >= 0} {
367 set todo [lreplace $todo $nullentry $nullentry]
368 if {$nullentry < $level} {
369 incr level -1
370 }
371 }
372
373 set todo [lreplace $todo $level $level]
374 if {$nullentry > $level} {
375 incr nullentry -1
376 }
377 set i $level
378 foreach p $actualparents {
379 set k [lsearch -exact $todo $p]
380 if {$k < 0} {
381 assigncolor $p
382 set todo [linsert $todo $i $p]
383 if {$nullentry >= $i} {
384 incr nullentry
385 }
386 }
387 lappend lines [list $oldlevel $p]
388 }
389
390 # choose which one to do next time around
391 set todol [llength $todo]
392 set level -1
393 set latest {}
394 for {set k $todol} {[incr k -1] >= 0} {} {
395 set p [lindex $todo $k]
396 if {$p == {}} continue
397 if {$ncleft($p) == 0} {
398 if {$datemode} {
399 if {$latest == {} || $cdate($p) > $latest} {
400 set level $k
401 set latest $cdate($p)
402 }
403 } else {
404 set level $k
405 break
406 }
407 }
408 }
409 if {$level < 0} {
410 if {$todo != {}} {
411 puts "ERROR: none of the pending commits can be done yet:"
412 foreach p $todo {
413 puts " $p"
414 }
415 }
416 break
417 }
418
419 # If we are reducing, put in a null entry
420 if {$todol < $nlines} {
421 if {$nullentry >= 0} {
422 set i $nullentry
423 while {$i < $todol
424 && [lindex $oldtodo $i] == [lindex $todo $i]} {
425 incr i
426 }
427 } else {
428 set i $oldlevel
429 if {$level >= $i} {
430 incr i
431 }
432 }
433 if {$i >= $todol} {
434 set nullentry -1
435 } else {
436 set nullentry $i
437 set todo [linsert $todo $nullentry {}]
438 if {$level >= $i} {
439 incr level
440 }
441 }
442 } else {
443 set nullentry -1
444 }
445
446 foreach l $lines {
447 set i [lindex $l 0]
448 set dst [lindex $l 1]
449 set j [lsearch -exact $todo $dst]
450 if {$i == $j} {
451 set linestarty($i) $oldstarty($i)
452 continue
453 }
454 set xi [expr {$canvx0 + $i * $linespc}]
455 set xj [expr {$canvx0 + $j * $linespc}]
456 set coords {}
457 if {$oldstarty($i) < $canvy} {
458 lappend coords $xi $oldstarty($i)
459 }
460 lappend coords $xi $canvy
461 if {$j < $i - 1} {
462 lappend coords [expr $xj + $linespc] $canvy
463 } elseif {$j > $i + 1} {
464 lappend coords [expr $xj - $linespc] $canvy
465 }
466 lappend coords $xj $y2
467 set t [$canv create line $coords -width 2 -fill $colormap($dst)]
468 $canv lower $t
469 if {![info exists linestarty($j)]} {
470 set linestarty($j) $y2
471 }
472 }
473 }
474}
475
476proc selcanvline {x y} {
477 global canv canvy0 ctext linespc selectedline
478 global lineid linehtag linentag linedtag
479 set ymax [lindex [$canv cget -scrollregion] 3]
480 set yfrac [lindex [$canv yview] 0]
481 set y [expr {$y + $yfrac * $ymax}]
482 set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
483 if {$l < 0} {
484 set l 0
485 }
486 if {[info exists selectedline] && $selectedline == $l} return
487 selectline $l
488}
489
490proc selectline {l} {
491 global canv canv2 canv3 ctext commitinfo selectedline
492 global lineid linehtag linentag linedtag
493 global canvy canvy0 linespc nparents treepending
494 global cflist treediffs currentid
495 if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
496 $canv delete secsel
497 set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
498 -tags secsel -fill [$canv cget -selectbackground]]
499 $canv lower $t
500 $canv2 delete secsel
501 set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
502 -tags secsel -fill [$canv2 cget -selectbackground]]
503 $canv2 lower $t
504 $canv3 delete secsel
505 set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
506 -tags secsel -fill [$canv3 cget -selectbackground]]
507 $canv3 lower $t
508 set y [expr {$canvy0 + $l * $linespc}]
509 set ytop [expr {($y - $linespc / 2.0) / $canvy}]
510 set ybot [expr {($y + $linespc / 2.0) / $canvy}]
511 set wnow [$canv yview]
512 if {$ytop < [lindex $wnow 0]} {
513 allcanvs yview moveto $ytop
514 } elseif {$ybot > [lindex $wnow 1]} {
515 set wh [expr {[lindex $wnow 1] - [lindex $wnow 0]}]
516 allcanvs yview moveto [expr {$ybot - $wh}]
517 }
518 set selectedline $l
519
520 set id $lineid($l)
521 $ctext conf -state normal
522 $ctext delete 0.0 end
523 set info $commitinfo($id)
524 $ctext insert end "Author: [lindex $info 1] [lindex $info 2]\n"
525 $ctext insert end "Committer: [lindex $info 3] [lindex $info 4]\n"
526 $ctext insert end "\n"
527 $ctext insert end [lindex $info 5]
528 $ctext insert end "\n"
529 $ctext tag delete Comments
530 $ctext conf -state disabled
531
532 $cflist delete 0 end
533 set currentid $id
534 if {$nparents($id) == 1} {
535 if {![info exists treediffs($id)]} {
536 if {![info exists treepending]} {
537 gettreediffs $id
538 }
539 } else {
540 addtocflist $id
541 }
542 }
543}
544
545proc selnextline {dir} {
546 global selectedline
547 if {![info exists selectedline]} return
548 set l [expr $selectedline + $dir]
549 selectline $l
550}
551
552proc addtocflist {id} {
553 global currentid treediffs cflist treepending
554 if {$id != $currentid} {
555 gettreediffs $currentid
556 return
557 }
558 $cflist insert end "All files"
559 foreach f $treediffs($currentid) {
560 $cflist insert end $f
561 }
562 getblobdiffs $id
563}
564
565proc gettreediffs {id} {
566 global treediffs parents treepending
567 set treepending $id
568 set treediffs($id) {}
569 set p [lindex $parents($id) 0]
570 if [catch {set gdtf [open "|git-diff-tree -r $p $id" r]}] return
571 fconfigure $gdtf -blocking 0
572 fileevent $gdtf readable "gettreediffline $gdtf $id"
573}
574
575proc gettreediffline {gdtf id} {
576 global treediffs treepending
577 set n [gets $gdtf line]
578 if {$n < 0} {
579 if {![eof $gdtf]} return
580 close $gdtf
581 unset treepending
582 addtocflist $id
583 return
584 }
585 set type [lindex $line 1]
586 set file [lindex $line 3]
587 if {$type == "blob"} {
588 lappend treediffs($id) $file
589 }
590}
591
592proc getblobdiffs {id} {
593 global parents diffopts blobdifffd env curdifftag curtagstart
594 set p [lindex $parents($id) 0]
595 set env(GIT_DIFF_OPTS) $diffopts
596 if [catch {set bdf [open "|git-diff-tree -r -p $p $id" r]} err] {
597 puts "error getting diffs: $err"
598 return
599 }
600 fconfigure $bdf -blocking 0
601 set blobdifffd($id) $bdf
602 set curdifftag Comments
603 set curtagstart 0.0
604 fileevent $bdf readable "getblobdiffline $bdf $id"
605}
606
607proc getblobdiffline {bdf id} {
608 global currentid blobdifffd ctext curdifftag curtagstart
609 set n [gets $bdf line]
610 if {$n < 0} {
611 if {[eof $bdf]} {
612 close $bdf
613 if {$id == $currentid && $bdf == $blobdifffd($id)} {
614 $ctext tag add $curdifftag $curtagstart end
615 }
616 }
617 return
618 }
619 if {$id != $currentid || $bdf != $blobdifffd($id)} {
620 return
621 }
622 $ctext conf -state normal
623 if {[regexp {^---[ \t]+([^/])+/(.*)} $line match s1 fname]} {
624 # start of a new file
625 $ctext insert end "\n"
626 $ctext tag add $curdifftag $curtagstart end
627 set curtagstart [$ctext index "end - 1c"]
628 set curdifftag "f:$fname"
629 $ctext tag delete $curdifftag
630 set l [expr {(78 - [string length $fname]) / 2}]
631 set pad [string range "----------------------------------------" 1 $l]
632 $ctext insert end "$pad $fname $pad\n" filesep
633 } elseif {[string range $line 0 2] == "+++"} {
634 # no need to do anything with this
635 } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
636 $line match f1l f1c f2l f2c rest]} {
637 $ctext insert end "\t" hunksep
638 $ctext insert end " $f1l " d0 " $f2l " d1
639 $ctext insert end " $rest \n" hunksep
640 } else {
641 set x [string range $line 0 0]
642 if {$x == "-" || $x == "+"} {
643 set tag [expr {$x == "+"}]
644 set line [string range $line 1 end]
645 $ctext insert end "$line\n" d$tag
646 } elseif {$x == " "} {
647 set line [string range $line 1 end]
648 $ctext insert end "$line\n"
649 } else {
650 # Something else we don't recognize
651 if {$curdifftag != "Comments"} {
652 $ctext insert end "\n"
653 $ctext tag add $curdifftag $curtagstart end
654 set curtagstart [$ctext index "end - 1c"]
655 set curdifftag Comments
656 }
657 $ctext insert end "$line\n" filesep
658 }
659 }
660 $ctext conf -state disabled
661}
662
663proc listboxsel {} {
664 global ctext cflist currentid treediffs
665 set sel [$cflist curselection]
666 if {$sel == {} || [lsearch -exact $sel 0] >= 0} {
667 # show everything
668 $ctext tag conf Comments -elide 0
669 foreach f $treediffs($currentid) {
670 $ctext tag conf "f:$f" -elide 0
671 }
672 } else {
673 # just show selected files
674 $ctext tag conf Comments -elide 1
675 set i 1
676 foreach f $treediffs($currentid) {
677 set elide [expr {[lsearch -exact $sel $i] < 0}]
678 $ctext tag conf "f:$f" -elide $elide
679 incr i
680 }
681 }
682}
683
684getcommits $revtreeargs
685
686set linespc [font metrics $mainfont -linespace]
687set charspc [font measure $mainfont "m"]
688
689set canvy0 [expr 3 + 0.5 * $linespc]
690set canvx0 [expr 3 + 0.5 * $linespc]
691set namex [expr 45 * $charspc]
692set datex [expr 75 * $charspc]
693
694makewindow
695
696set start {}
697foreach id $commits {
698 if {$nchildren($id) == 0} {
699 set start $id
700 break
701 }
702}
703if {$start != {}} {
704 drawgraph $start
705}