git/gitk

756 lines
20 KiB
Plaintext
Raw Normal View History

#!/bin/sh
# Tcl ignores the next line -*- tcl -*- \
exec wish "$0" -- "${1+$@}"
# Copyright (C) 2005 Paul Mackerras. All rights reserved.
# This program is free software; it may be used, copied, modified
# and distributed under the terms of the GNU General Public Licence,
# either version 2, or (at your option) any later version.
# CVS $Revision: 1.7 $
set datemode 0
set boldnames 0
set revtreeargs {}
set diffopts "-U 5 -p"
set mainfont {Helvetica 9}
set namefont $mainfont
set textfont {Courier 9}
if {$boldnames} {
lappend namefont bold
}
set colors {green red blue magenta darkgrey brown orange}
set colorbycommitter false
catch {source ~/.gitk}
foreach arg $argv {
switch -regexp -- $arg {
"^$" { }
"^-b" { set boldnames 1 }
"^-c" { set colorbycommitter 1 }
"^-d" { set datemode 1 }
"^-.*" {
puts stderr "unrecognized option $arg"
exit 1
}
default {
lappend revtreeargs $arg
}
}
}
proc getcommits {rargs} {
global commits parents cdate nparents children nchildren
if {$rargs == {}} {
set rargs HEAD
}
set commits {}
if [catch {set clist [eval exec git-rev-tree $rargs]} err] {
if {[string range $err 0 4] == "usage"} {
puts stderr "Error reading commits: bad arguments to git-rev-tree"
puts stderr "Note: arguments to gitk are passed to git-rev-tree"
puts stderr " to allow selection of commits to be displayed"
} else {
puts stderr "Error reading commits: $err"
}
return 0
}
foreach c [split $clist "\n"] {
set i 0
set cid {}
foreach f $c {
if {$i == 0} {
set d $f
} else {
set id [lindex [split $f :] 0]
if {![info exists nchildren($id)]} {
set children($id) {}
set nchildren($id) 0
}
if {$i == 1} {
set cid $id
lappend commits $id
set parents($id) {}
set cdate($id) $d
set nparents($id) 0
} else {
lappend parents($cid) $id
incr nparents($cid)
incr nchildren($id)
lappend children($id) $cid
}
}
incr i
}
}
return 1
}
proc readcommit {id} {
global commitinfo
set inhdr 1
set comment {}
set headline {}
set auname {}
set audate {}
set comname {}
set comdate {}
foreach line [split [exec git-cat-file commit $id] "\n"] {
if {$inhdr} {
if {$line == {}} {
set inhdr 0
} else {
set tag [lindex $line 0]
if {$tag == "author"} {
set x [expr {[llength $line] - 2}]
set audate [lindex $line $x]
set auname [lrange $line 1 [expr {$x - 1}]]
} elseif {$tag == "committer"} {
set x [expr {[llength $line] - 2}]
set comdate [lindex $line $x]
set comname [lrange $line 1 [expr {$x - 1}]]
}
}
} else {
if {$comment == {}} {
set headline $line
} else {
append comment "\n"
}
append comment $line
}
}
if {$audate != {}} {
set audate [clock format $audate -format "%Y-%m-%d %H:%M:%S"]
}
if {$comdate != {}} {
set comdate [clock format $comdate -format "%Y-%m-%d %H:%M:%S"]
}
set commitinfo($id) [list $headline $auname $audate \
$comname $comdate $comment]
}
proc makewindow {} {
global canv canv2 canv3 linespc charspc ctext cflist textfont
menu .bar
.bar add cascade -label "File" -menu .bar.file
menu .bar.file
.bar.file add command -label "Quit" -command "set stopped 1; destroy ."
menu .bar.help
.bar add cascade -label "Help" -menu .bar.help
.bar.help add command -label "About gitk" -command about
. configure -menu .bar
panedwindow .ctop -orient vertical
panedwindow .ctop.clist -orient horizontal -sashpad 0 -handlesize 4
.ctop add .ctop.clist
set canv .ctop.clist.canv
set cscroll .ctop.clist.dates.csb
set height [expr 25 * $linespc + 4]
canvas $canv -height $height -width [expr 45 * $charspc] \
-bg white -bd 0 \
-yscrollincr $linespc -yscrollcommand "$cscroll set"
.ctop.clist add $canv
set canv2 .ctop.clist.canv2
canvas $canv2 -height $height -width [expr 30 * $charspc] \
-bg white -bd 0 -yscrollincr $linespc
.ctop.clist add $canv2
frame .ctop.clist.dates
.ctop.clist add .ctop.clist.dates
set canv3 .ctop.clist.dates.canv3
canvas $canv3 -height $height -width [expr 15 * $charspc] \
-bg white -bd 0 -yscrollincr $linespc
scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
pack .ctop.clist.dates.csb -side right -fill y
pack $canv3 -side left -fill both -expand 1
panedwindow .ctop.cdet -orient horizontal
.ctop add .ctop.cdet
frame .ctop.cdet.left
set ctext .ctop.cdet.left.ctext
text $ctext -bg white -state disabled -font $textfont -height 32 \
-yscrollcommand ".ctop.cdet.left.sb set"
scrollbar .ctop.cdet.left.sb -command "$ctext yview"
pack .ctop.cdet.left.sb -side right -fill y
pack $ctext -side left -fill both -expand 1
.ctop.cdet add .ctop.cdet.left
$ctext tag conf filesep -font [concat $textfont bold]
$ctext tag conf hunksep -back blue -fore white
$ctext tag conf d0 -back "#ff8080"
$ctext tag conf d1 -back green
frame .ctop.cdet.right
set cflist .ctop.cdet.right.cfiles
listbox $cflist -width 30 -bg white -selectmode extended \
-yscrollcommand ".ctop.cdet.right.sb set"
scrollbar .ctop.cdet.right.sb -command "$cflist yview"
pack .ctop.cdet.right.sb -side right -fill y
pack $cflist -side left -fill both -expand 1
.ctop.cdet add .ctop.cdet.right
pack .ctop -side top -fill both -expand 1
bindall <1> {selcanvline %x %y}
bindall <B1-Motion> {selcanvline %x %y}
bindall <ButtonRelease-4> "allcanvs yview scroll -5 u"
bindall <ButtonRelease-5> "allcanvs yview scroll 5 u"
bindall <2> "allcanvs scan mark 0 %y"
bindall <B2-Motion> "allcanvs scan dragto 0 %y"
bind . <Key-Up> "selnextline -1"
bind . <Key-Down> "selnextline 1"
bind . p "selnextline -1"
bind . n "selnextline 1"
bind . <Key-Prior> "allcanvs yview scroll -1 p"
bind . <Key-Next> "allcanvs yview scroll 1 p"
bind . <Key-Delete> "$ctext yview scroll -1 p"
bind . <Key-BackSpace> "$ctext yview scroll -1 p"
bind . <Key-space> "$ctext yview scroll 1 p"
bind . b "$ctext yview scroll -1 p"
bind . d "$ctext yview scroll 18 u"
bind . u "$ctext yview scroll -18 u"
bind . Q "set stopped 1; destroy ."
bind . <Control-q> "set stopped 1; destroy ."
bind $cflist <<ListboxSelect>> listboxsel
}
proc allcanvs args {
global canv canv2 canv3
eval $canv $args
eval $canv2 $args
eval $canv3 $args
}
proc bindall {event action} {
global canv canv2 canv3
bind $canv $event $action
bind $canv2 $event $action
bind $canv3 $event $action
}
proc about {} {
set w .about
if {[winfo exists $w]} {
raise $w
return
}
toplevel $w
wm title $w "About gitk"
message $w.m -text {
Gitk version 0.9
Copyright <20> 2005 Paul Mackerras
Use and redistribute under the terms of the GNU General Public License
(CVS $Revision: 1.7 $)} \
-justify center -aspect 400
pack $w.m -side top -fill x -padx 20 -pady 20
button $w.ok -text Close -command "destroy $w"
pack $w.ok -side bottom
}
proc truncatetofit {str width font} {
if {[font measure $font $str] <= $width} {
return $str
}
set best 0
set bad [string length $str]
set tmp $str
while {$best < $bad - 1} {
set try [expr {int(($best + $bad) / 2)}]
set tmp "[string range $str 0 [expr $try-1]]..."
if {[font measure $font $tmp] <= $width} {
set best $try
} else {
set bad $try
}
}
return $tmp
}
proc assigncolor {id} {
global commitinfo colormap commcolors colors nextcolor
global colorbycommitter
global parents nparents children nchildren
if [info exists colormap($id)] return
set ncolors [llength $colors]
if {$colorbycommitter} {
if {![info exists commitinfo($id)]} {
readcommit $id
}
set comm [lindex $commitinfo($id) 3]
if {![info exists commcolors($comm)]} {
set commcolors($comm) [lindex $colors $nextcolor]
if {[incr nextcolor] >= $ncolors} {
set nextcolor 0
}
}
set colormap($id) $commcolors($comm)
} else {
if {$nparents($id) == 1 && $nchildren($id) == 1} {
set child [lindex $children($id) 0]
if {[info exists colormap($child)]
&& $nparents($child) == 1} {
set colormap($id) $colormap($child)
return
}
}
set badcolors {}
foreach child $children($id) {
if {[info exists colormap($child)]
&& [lsearch -exact $badcolors $colormap($child)] < 0} {
lappend badcolors $colormap($child)
}
if {[info exists parents($child)]} {
foreach p $parents($child) {
if {[info exists colormap($p)]
&& [lsearch -exact $badcolors $colormap($p)] < 0} {
lappend badcolors $colormap($p)
}
}
}
}
if {[llength $badcolors] >= $ncolors} {
set badcolors {}
}
for {set i 0} {$i <= $ncolors} {incr i} {
set c [lindex $colors $nextcolor]
if {[incr nextcolor] >= $ncolors} {
set nextcolor 0
}
if {[lsearch -exact $badcolors $c]} break
}
set colormap($id) $c
}
}
proc drawgraph {start} {
global parents children nparents nchildren commits
global canv canv2 canv3 mainfont namefont canvx0 canvy0 canvy linespc
global datemode cdate
global lineid linehtag linentag linedtag commitinfo
global nextcolor colormap
global stopped
set nextcolor 0
assigncolor $start
foreach id $commits {
set ncleft($id) $nchildren($id)
}
set todo [list $start]
set level 0
set y2 $canvy0
set linestarty(0) $canvy0
set nullentry -1
set lineno -1
while 1 {
set canvy $y2
allcanvs conf -scrollregion [list 0 0 0 $canvy]
update
if {$stopped} return
incr lineno
set nlines [llength $todo]
set id [lindex $todo $level]
set lineid($lineno) $id
set actualparents {}
foreach p $parents($id) {
if {[info exists ncleft($p)]} {
incr ncleft($p) -1
lappend actualparents $p
}
}
if {![info exists commitinfo($id)]} {
readcommit $id
}
set x [expr $canvx0 + $level * $linespc]
set y2 [expr $canvy + $linespc]
if {$linestarty($level) < $canvy} {
set t [$canv create line $x $linestarty($level) $x $canvy \
-width 2 -fill $colormap($id)]
$canv lower $t
set linestarty($level) $canvy
}
set t [$canv create oval [expr $x - 4] [expr $canvy - 4] \
[expr $x + 3] [expr $canvy + 3] \
-fill blue -outline black -width 1]
$canv raise $t
set xt [expr $canvx0 + $nlines * $linespc]
set headline [lindex $commitinfo($id) 0]
set name [lindex $commitinfo($id) 1]
set date [lindex $commitinfo($id) 2]
set linehtag($lineno) [$canv create text $xt $canvy -anchor w \
-text $headline -font $mainfont ]
set linentag($lineno) [$canv2 create text 3 $canvy -anchor w \
-text $name -font $namefont]
set linedtag($lineno) [$canv3 create text 3 $canvy -anchor w \
-text $date -font $mainfont]
if {!$datemode && [llength $actualparents] == 1} {
set p [lindex $actualparents 0]
if {$ncleft($p) == 0 && [lsearch -exact $todo $p] < 0} {
assigncolor $p
set todo [lreplace $todo $level $level $p]
continue
}
}
set oldtodo $todo
set oldlevel $level
set lines {}
for {set i 0} {$i < $nlines} {incr i} {
if {[lindex $todo $i] == {}} continue
set oldstarty($i) $linestarty($i)
if {$i != $level} {
lappend lines [list $i [lindex $todo $i]]
}
}
unset linestarty
if {$nullentry >= 0} {
set todo [lreplace $todo $nullentry $nullentry]
if {$nullentry < $level} {
incr level -1
}
}
set todo [lreplace $todo $level $level]
if {$nullentry > $level} {
incr nullentry -1
}
set i $level
foreach p $actualparents {
set k [lsearch -exact $todo $p]
if {$k < 0} {
assigncolor $p
set todo [linsert $todo $i $p]
if {$nullentry >= $i} {
incr nullentry
}
}
lappend lines [list $oldlevel $p]
}
# choose which one to do next time around
set todol [llength $todo]
set level -1
set latest {}
for {set k $todol} {[incr k -1] >= 0} {} {
set p [lindex $todo $k]
if {$p == {}} continue
if {$ncleft($p) == 0} {
if {$datemode} {
if {$latest == {} || $cdate($p) > $latest} {
set level $k
set latest $cdate($p)
}
} else {
set level $k
break
}
}
}
if {$level < 0} {
if {$todo != {}} {
puts "ERROR: none of the pending commits can be done yet:"
foreach p $todo {
puts " $p"
}
}
break
}
# If we are reducing, put in a null entry
if {$todol < $nlines} {
if {$nullentry >= 0} {
set i $nullentry
while {$i < $todol
&& [lindex $oldtodo $i] == [lindex $todo $i]} {
incr i
}
} else {
set i $oldlevel
if {$level >= $i} {
incr i
}
}
if {$i >= $todol} {
set nullentry -1
} else {
set nullentry $i
set todo [linsert $todo $nullentry {}]
if {$level >= $i} {
incr level
}
}
} else {
set nullentry -1
}
foreach l $lines {
set i [lindex $l 0]
set dst [lindex $l 1]
set j [lsearch -exact $todo $dst]
if {$i == $j} {
set linestarty($i) $oldstarty($i)
continue
}
set xi [expr {$canvx0 + $i * $linespc}]
set xj [expr {$canvx0 + $j * $linespc}]
set coords {}
if {$oldstarty($i) < $canvy} {
lappend coords $xi $oldstarty($i)
}
lappend coords $xi $canvy
if {$j < $i - 1} {
lappend coords [expr $xj + $linespc] $canvy
} elseif {$j > $i + 1} {
lappend coords [expr $xj - $linespc] $canvy
}
lappend coords $xj $y2
set t [$canv create line $coords -width 2 -fill $colormap($dst)]
$canv lower $t
if {![info exists linestarty($j)]} {
set linestarty($j) $y2
}
}
}
}
proc selcanvline {x y} {
global canv canvy0 ctext linespc selectedline
global lineid linehtag linentag linedtag
set ymax [lindex [$canv cget -scrollregion] 3]
set yfrac [lindex [$canv yview] 0]
set y [expr {$y + $yfrac * $ymax}]
set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
if {$l < 0} {
set l 0
}
if {[info exists selectedline] && $selectedline == $l} return
selectline $l
}
proc selectline {l} {
global canv canv2 canv3 ctext commitinfo selectedline
global lineid linehtag linentag linedtag
global canvy canvy0 linespc nparents treepending
global cflist treediffs currentid
if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
$canv delete secsel
set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
-tags secsel -fill [$canv cget -selectbackground]]
$canv lower $t
$canv2 delete secsel
set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
-tags secsel -fill [$canv2 cget -selectbackground]]
$canv2 lower $t
$canv3 delete secsel
set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
-tags secsel -fill [$canv3 cget -selectbackground]]
$canv3 lower $t
set y [expr {$canvy0 + $l * $linespc}]
set ytop [expr {($y - $linespc / 2.0) / $canvy}]
set ybot [expr {($y + $linespc / 2.0) / $canvy}]
set wnow [$canv yview]
if {$ytop < [lindex $wnow 0]} {
allcanvs yview moveto $ytop
} elseif {$ybot > [lindex $wnow 1]} {
set wh [expr {[lindex $wnow 1] - [lindex $wnow 0]}]
allcanvs yview moveto [expr {$ybot - $wh}]
}
set selectedline $l
set id $lineid($l)
$ctext conf -state normal
$ctext delete 0.0 end
set info $commitinfo($id)
$ctext insert end "Author: [lindex $info 1] [lindex $info 2]\n"
$ctext insert end "Committer: [lindex $info 3] [lindex $info 4]\n"
$ctext insert end "\n"
$ctext insert end [lindex $info 5]
$ctext insert end "\n"
$ctext tag delete Comments
$ctext conf -state disabled
$cflist delete 0 end
set currentid $id
if {$nparents($id) == 1} {
if {![info exists treediffs($id)]} {
if {![info exists treepending]} {
gettreediffs $id
}
} else {
addtocflist $id
}
}
}
proc selnextline {dir} {
global selectedline
if {![info exists selectedline]} return
set l [expr $selectedline + $dir]
selectline $l
}
proc addtocflist {id} {
global currentid treediffs cflist treepending
if {$id != $currentid} {
gettreediffs $currentid
return
}
$cflist insert end "All files"
foreach f $treediffs($currentid) {
$cflist insert end $f
}
getblobdiffs $id
}
proc gettreediffs {id} {
global treediffs parents treepending
set treepending $id
set treediffs($id) {}
set p [lindex $parents($id) 0]
if [catch {set gdtf [open "|git-diff-tree -r $p $id" r]}] return
fconfigure $gdtf -blocking 0
fileevent $gdtf readable "gettreediffline $gdtf $id"
}
proc gettreediffline {gdtf id} {
global treediffs treepending
set n [gets $gdtf line]
if {$n < 0} {
if {![eof $gdtf]} return
close $gdtf
unset treepending
addtocflist $id
return
}
set type [lindex $line 1]
set file [lindex $line 3]
if {$type == "blob"} {
lappend treediffs($id) $file
}
}
proc getblobdiffs {id} {
global parents diffopts blobdifffd env curdifftag curtagstart
set p [lindex $parents($id) 0]
set env(GIT_DIFF_OPTS) $diffopts
if [catch {set bdf [open "|git-diff-tree -r -p $p $id" r]} err] {
puts "error getting diffs: $err"
return
}
fconfigure $bdf -blocking 0
set blobdifffd($id) $bdf
set curdifftag Comments
set curtagstart 0.0
fileevent $bdf readable "getblobdiffline $bdf $id"
}
proc getblobdiffline {bdf id} {
global currentid blobdifffd ctext curdifftag curtagstart
set n [gets $bdf line]
if {$n < 0} {
if {[eof $bdf]} {
close $bdf
if {$id == $currentid && $bdf == $blobdifffd($id)} {
$ctext tag add $curdifftag $curtagstart end
}
}
return
}
if {$id != $currentid || $bdf != $blobdifffd($id)} {
return
}
$ctext conf -state normal
if {[regexp {^---[ \t]+([^/])+/(.*)} $line match s1 fname]} {
# start of a new file
$ctext insert end "\n"
$ctext tag add $curdifftag $curtagstart end
set curtagstart [$ctext index "end - 1c"]
set curdifftag "f:$fname"
$ctext tag delete $curdifftag
set l [expr {(78 - [string length $fname]) / 2}]
set pad [string range "----------------------------------------" 1 $l]
$ctext insert end "$pad $fname $pad\n" filesep
} elseif {[string range $line 0 2] == "+++"} {
# no need to do anything with this
} elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
$line match f1l f1c f2l f2c rest]} {
$ctext insert end "\t" hunksep
$ctext insert end " $f1l " d0 " $f2l " d1
$ctext insert end " $rest \n" hunksep
} else {
set x [string range $line 0 0]
if {$x == "-" || $x == "+"} {
set tag [expr {$x == "+"}]
set line [string range $line 1 end]
$ctext insert end "$line\n" d$tag
} elseif {$x == " "} {
set line [string range $line 1 end]
$ctext insert end "$line\n"
} else {
# Something else we don't recognize
if {$curdifftag != "Comments"} {
$ctext insert end "\n"
$ctext tag add $curdifftag $curtagstart end
set curtagstart [$ctext index "end - 1c"]
set curdifftag Comments
}
$ctext insert end "$line\n" filesep
}
}
$ctext conf -state disabled
}
proc listboxsel {} {
global ctext cflist currentid treediffs
if {![info exists currentid]} return
set sel [$cflist curselection]
if {$sel == {} || [lsearch -exact $sel 0] >= 0} {
# show everything
$ctext tag conf Comments -elide 0
foreach f $treediffs($currentid) {
$ctext tag conf "f:$f" -elide 0
}
} else {
# just show selected files
$ctext tag conf Comments -elide 1
set i 1
foreach f $treediffs($currentid) {
set elide [expr {[lsearch -exact $sel $i] < 0}]
$ctext tag conf "f:$f" -elide $elide
incr i
}
}
}
if {![getcommits $revtreeargs]} {
exit 1
}
set linespc [font metrics $mainfont -linespace]
set charspc [font measure $mainfont "m"]
set canvy0 [expr 3 + 0.5 * $linespc]
set canvx0 [expr 3 + 0.5 * $linespc]
set namex [expr 45 * $charspc]
set datex [expr 75 * $charspc]
set stopped 0
makewindow
set start {}
foreach id $commits {
if {$nchildren($id) == 0} {
set start $id
break
}
}
if {$start != {}} {
drawgraph $start
}