gitkon commit Accommodate new git-diff-tree output format (39ad857)
   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.17 $
  11
  12proc getcommits {rargs} {
  13    global commits commfd phase canv mainfont
  14    if {$rargs == {}} {
  15        set rargs HEAD
  16    }
  17    set commits {}
  18    set phase getcommits
  19    if [catch {set commfd [open "|git-rev-tree $rargs" r]} err] {
  20        puts stderr "Error executing git-rev-tree: $err"
  21        exit 1
  22    }
  23    fconfigure $commfd -blocking 0
  24    fileevent $commfd readable "getcommitline $commfd"
  25    $canv delete all
  26    $canv create text 3 3 -anchor nw -text "Reading commits..." \
  27        -font $mainfont -tags textitems
  28}
  29
  30proc getcommitline {commfd}  {
  31    global commits parents cdate nparents children nchildren
  32    set n [gets $commfd line]
  33    if {$n < 0} {
  34        if {![eof $commfd]} return
  35        # this works around what is apparently a bug in Tcl...
  36        fconfigure $commfd -blocking 1
  37        if {![catch {close $commfd} err]} {
  38            after idle drawgraph
  39            return
  40        }
  41        if {[string range $err 0 4] == "usage"} {
  42            set err "\
  43Gitk: error reading commits: bad arguments to git-rev-tree.\n\
  44(Note: arguments to gitk are passed to git-rev-tree\
  45to allow selection of commits to be displayed.)"
  46        } else {
  47            set err "Error reading commits: $err"
  48        }
  49        error_popup $err
  50        exit 1
  51    }
  52
  53    set i 0
  54    set cid {}
  55    foreach f $line {
  56        if {$i == 0} {
  57            set d $f
  58        } else {
  59            set id [lindex [split $f :] 0]
  60            if {![info exists nchildren($id)]} {
  61                set children($id) {}
  62                set nchildren($id) 0
  63            }
  64            if {$i == 1} {
  65                set cid $id
  66                lappend commits $id
  67                set parents($id) {}
  68                set cdate($id) $d
  69                set nparents($id) 0
  70            } else {
  71                lappend parents($cid) $id
  72                incr nparents($cid)
  73                incr nchildren($id)
  74                lappend children($id) $cid
  75            }
  76        }
  77        incr i
  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    if [catch {set contents [exec git-cat-file commit $id]}] return
  91    foreach line [split $contents "\n"] {
  92        if {$inhdr} {
  93            if {$line == {}} {
  94                set inhdr 0
  95            } else {
  96                set tag [lindex $line 0]
  97                if {$tag == "author"} {
  98                    set x [expr {[llength $line] - 2}]
  99                    set audate [lindex $line $x]
 100                    set auname [lrange $line 1 [expr {$x - 1}]]
 101                } elseif {$tag == "committer"} {
 102                    set x [expr {[llength $line] - 2}]
 103                    set comdate [lindex $line $x]
 104                    set comname [lrange $line 1 [expr {$x - 1}]]
 105                }
 106            }
 107        } else {
 108            if {$comment == {}} {
 109                set headline $line
 110            } else {
 111                append comment "\n"
 112            }
 113            append comment $line
 114        }
 115    }
 116    if {$audate != {}} {
 117        set audate [clock format $audate -format "%Y-%m-%d %H:%M:%S"]
 118    }
 119    if {$comdate != {}} {
 120        set comdate [clock format $comdate -format "%Y-%m-%d %H:%M:%S"]
 121    }
 122    set commitinfo($id) [list $headline $auname $audate \
 123                             $comname $comdate $comment]
 124}
 125
 126proc error_popup msg {
 127    set w .error
 128    toplevel $w
 129    wm transient $w .
 130    message $w.m -text $msg -justify center -aspect 400
 131    pack $w.m -side top -fill x -padx 20 -pady 20
 132    button $w.ok -text OK -command "destroy $w"
 133    pack $w.ok -side bottom -fill x
 134    bind $w <Visibility> "grab $w; focus $w"
 135    tkwait window $w
 136}
 137
 138proc makewindow {} {
 139    global canv canv2 canv3 linespc charspc ctext cflist textfont
 140    global sha1entry findtype findloc findstring fstring geometry
 141
 142    menu .bar
 143    .bar add cascade -label "File" -menu .bar.file
 144    menu .bar.file
 145    .bar.file add command -label "Quit" -command doquit
 146    menu .bar.help
 147    .bar add cascade -label "Help" -menu .bar.help
 148    .bar.help add command -label "About gitk" -command about
 149    . configure -menu .bar
 150
 151    if {![info exists geometry(canv1)]} {
 152        set geometry(canv1) [expr 45 * $charspc]
 153        set geometry(canv2) [expr 30 * $charspc]
 154        set geometry(canv3) [expr 15 * $charspc]
 155        set geometry(canvh) [expr 25 * $linespc + 4]
 156        set geometry(ctextw) 80
 157        set geometry(ctexth) 30
 158        set geometry(cflistw) 30
 159    }
 160    panedwindow .ctop -orient vertical
 161    if {[info exists geometry(width)]} {
 162        .ctop conf -width $geometry(width) -height $geometry(height)
 163        set texth [expr {$geometry(height) - $geometry(canvh) - 56}]
 164        set geometry(ctexth) [expr {($texth - 8) /
 165                                    [font metrics $textfont -linespace]}]
 166    }
 167    frame .ctop.top
 168    frame .ctop.top.bar
 169    pack .ctop.top.bar -side bottom -fill x
 170    set cscroll .ctop.top.csb
 171    scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
 172    pack $cscroll -side right -fill y
 173    panedwindow .ctop.top.clist -orient horizontal -sashpad 0 -handlesize 4
 174    pack .ctop.top.clist -side top -fill both -expand 1
 175    .ctop add .ctop.top
 176    set canv .ctop.top.clist.canv
 177    canvas $canv -height $geometry(canvh) -width $geometry(canv1) \
 178        -bg white -bd 0 \
 179        -yscrollincr $linespc -yscrollcommand "$cscroll set"
 180    .ctop.top.clist add $canv
 181    set canv2 .ctop.top.clist.canv2
 182    canvas $canv2 -height $geometry(canvh) -width $geometry(canv2) \
 183        -bg white -bd 0 -yscrollincr $linespc
 184    .ctop.top.clist add $canv2
 185    set canv3 .ctop.top.clist.canv3
 186    canvas $canv3 -height $geometry(canvh) -width $geometry(canv3) \
 187        -bg white -bd 0 -yscrollincr $linespc
 188    .ctop.top.clist add $canv3
 189    bind .ctop.top.clist <Configure> {resizeclistpanes %W %w}
 190
 191    set sha1entry .ctop.top.bar.sha1
 192    label .ctop.top.bar.sha1label -text "SHA1 ID: "
 193    pack .ctop.top.bar.sha1label -side left
 194    entry $sha1entry -width 40 -font $textfont -state readonly
 195    pack $sha1entry -side left -pady 2
 196    button .ctop.top.bar.findbut -text "Find" -command dofind
 197    pack .ctop.top.bar.findbut -side left
 198    set findstring {}
 199    set fstring .ctop.top.bar.findstring
 200    entry $fstring -width 30 -font $textfont -textvariable findstring
 201    pack $fstring -side left -expand 1 -fill x
 202    set findtype Exact
 203    tk_optionMenu .ctop.top.bar.findtype findtype Exact IgnCase Regexp
 204    set findloc "All fields"
 205    tk_optionMenu .ctop.top.bar.findloc findloc "All fields" Headline \
 206        Comments Author Committer
 207    pack .ctop.top.bar.findloc -side right
 208    pack .ctop.top.bar.findtype -side right
 209
 210    panedwindow .ctop.cdet -orient horizontal
 211    .ctop add .ctop.cdet
 212    frame .ctop.cdet.left
 213    set ctext .ctop.cdet.left.ctext
 214    text $ctext -bg white -state disabled -font $textfont \
 215        -width $geometry(ctextw) -height $geometry(ctexth) \
 216        -yscrollcommand ".ctop.cdet.left.sb set"
 217    scrollbar .ctop.cdet.left.sb -command "$ctext yview"
 218    pack .ctop.cdet.left.sb -side right -fill y
 219    pack $ctext -side left -fill both -expand 1
 220    .ctop.cdet add .ctop.cdet.left
 221
 222    $ctext tag conf filesep -font [concat $textfont bold]
 223    $ctext tag conf hunksep -back blue -fore white
 224    $ctext tag conf d0 -back "#ff8080"
 225    $ctext tag conf d1 -back green
 226    $ctext tag conf found -back yellow
 227
 228    frame .ctop.cdet.right
 229    set cflist .ctop.cdet.right.cfiles
 230    listbox $cflist -bg white -selectmode extended -width $geometry(cflistw) \
 231        -yscrollcommand ".ctop.cdet.right.sb set"
 232    scrollbar .ctop.cdet.right.sb -command "$cflist yview"
 233    pack .ctop.cdet.right.sb -side right -fill y
 234    pack $cflist -side left -fill both -expand 1
 235    .ctop.cdet add .ctop.cdet.right
 236    bind .ctop.cdet <Configure> {resizecdetpanes %W %w}
 237
 238    pack .ctop -side top -fill both -expand 1
 239
 240    bindall <1> {selcanvline %x %y}
 241    bindall <B1-Motion> {selcanvline %x %y}
 242    bindall <ButtonRelease-4> "allcanvs yview scroll -5 u"
 243    bindall <ButtonRelease-5> "allcanvs yview scroll 5 u"
 244    bindall <2> "allcanvs scan mark 0 %y"
 245    bindall <B2-Motion> "allcanvs scan dragto 0 %y"
 246    bind . <Key-Up> "selnextline -1"
 247    bind . <Key-Down> "selnextline 1"
 248    bind . <Key-Prior> "allcanvs yview scroll -1 p"
 249    bind . <Key-Next> "allcanvs yview scroll 1 p"
 250    bindkey <Key-Delete> "$ctext yview scroll -1 p"
 251    bindkey <Key-BackSpace> "$ctext yview scroll -1 p"
 252    bindkey <Key-space> "$ctext yview scroll 1 p"
 253    bindkey p "selnextline -1"
 254    bindkey n "selnextline 1"
 255    bindkey b "$ctext yview scroll -1 p"
 256    bindkey d "$ctext yview scroll 18 u"
 257    bindkey u "$ctext yview scroll -18 u"
 258    bindkey / findnext
 259    bindkey ? findprev
 260    bindkey f nextfile
 261    bind . <Control-q> doquit
 262    bind . <Control-f> dofind
 263    bind . <Control-g> findnext
 264    bind . <Control-r> findprev
 265    bind . <Control-equal> {incrfont 1}
 266    bind . <Control-KP_Add> {incrfont 1}
 267    bind . <Control-minus> {incrfont -1}
 268    bind . <Control-KP_Subtract> {incrfont -1}
 269    bind $cflist <<ListboxSelect>> listboxsel
 270    bind . <Destroy> {savestuff %W}
 271    bind . <Button-1> "click %W"
 272    bind $fstring <Key-Return> dofind
 273}
 274
 275# when we make a key binding for the toplevel, make sure
 276# it doesn't get triggered when that key is pressed in the
 277# find string entry widget.
 278proc bindkey {ev script} {
 279    global fstring
 280    bind . $ev $script
 281    set escript [bind Entry $ev]
 282    if {$escript == {}} {
 283        set escript [bind Entry <Key>]
 284    }
 285    bind $fstring $ev "$escript; break"
 286}
 287
 288# set the focus back to the toplevel for any click outside
 289# the find string entry widget
 290proc click {w} {
 291    global fstring
 292    if {$w != $fstring} {
 293        focus .
 294    }
 295}
 296
 297proc savestuff {w} {
 298    global canv canv2 canv3 ctext cflist mainfont textfont
 299    global stuffsaved
 300    if {$stuffsaved} return
 301    if {![winfo viewable .]} return
 302    catch {
 303        set f [open "~/.gitk-new" w]
 304        puts $f "set mainfont {$mainfont}"
 305        puts $f "set textfont {$textfont}"
 306        puts $f "set geometry(width) [winfo width .ctop]"
 307        puts $f "set geometry(height) [winfo height .ctop]"
 308        puts $f "set geometry(canv1) [expr [winfo width $canv]-2]"
 309        puts $f "set geometry(canv2) [expr [winfo width $canv2]-2]"
 310        puts $f "set geometry(canv3) [expr [winfo width $canv3]-2]"
 311        puts $f "set geometry(canvh) [expr [winfo height $canv]-2]"
 312        set wid [expr {([winfo width $ctext] - 8) \
 313                           / [font measure $textfont "0"]}]
 314        puts $f "set geometry(ctextw) $wid"
 315        set wid [expr {([winfo width $cflist] - 11) \
 316                           / [font measure [$cflist cget -font] "0"]}]
 317        puts $f "set geometry(cflistw) $wid"
 318        close $f
 319        file rename -force "~/.gitk-new" "~/.gitk"
 320    }
 321    set stuffsaved 1
 322}
 323
 324proc resizeclistpanes {win w} {
 325    global oldwidth
 326    if [info exists oldwidth($win)] {
 327        set s0 [$win sash coord 0]
 328        set s1 [$win sash coord 1]
 329        if {$w < 60} {
 330            set sash0 [expr {int($w/2 - 2)}]
 331            set sash1 [expr {int($w*5/6 - 2)}]
 332        } else {
 333            set factor [expr {1.0 * $w / $oldwidth($win)}]
 334            set sash0 [expr {int($factor * [lindex $s0 0])}]
 335            set sash1 [expr {int($factor * [lindex $s1 0])}]
 336            if {$sash0 < 30} {
 337                set sash0 30
 338            }
 339            if {$sash1 < $sash0 + 20} {
 340                set sash1 [expr $sash0 + 20]
 341            }
 342            if {$sash1 > $w - 10} {
 343                set sash1 [expr $w - 10]
 344                if {$sash0 > $sash1 - 20} {
 345                    set sash0 [expr $sash1 - 20]
 346                }
 347            }
 348        }
 349        $win sash place 0 $sash0 [lindex $s0 1]
 350        $win sash place 1 $sash1 [lindex $s1 1]
 351    }
 352    set oldwidth($win) $w
 353}
 354
 355proc resizecdetpanes {win w} {
 356    global oldwidth
 357    if [info exists oldwidth($win)] {
 358        set s0 [$win sash coord 0]
 359        if {$w < 60} {
 360            set sash0 [expr {int($w*3/4 - 2)}]
 361        } else {
 362            set factor [expr {1.0 * $w / $oldwidth($win)}]
 363            set sash0 [expr {int($factor * [lindex $s0 0])}]
 364            if {$sash0 < 45} {
 365                set sash0 45
 366            }
 367            if {$sash0 > $w - 15} {
 368                set sash0 [expr $w - 15]
 369            }
 370        }
 371        $win sash place 0 $sash0 [lindex $s0 1]
 372    }
 373    set oldwidth($win) $w
 374}
 375
 376proc allcanvs args {
 377    global canv canv2 canv3
 378    eval $canv $args
 379    eval $canv2 $args
 380    eval $canv3 $args
 381}
 382
 383proc bindall {event action} {
 384    global canv canv2 canv3
 385    bind $canv $event $action
 386    bind $canv2 $event $action
 387    bind $canv3 $event $action
 388}
 389
 390proc about {} {
 391    set w .about
 392    if {[winfo exists $w]} {
 393        raise $w
 394        return
 395    }
 396    toplevel $w
 397    wm title $w "About gitk"
 398    message $w.m -text {
 399Gitk version 1.0
 400
 401Copyright © 2005 Paul Mackerras
 402
 403Use and redistribute under the terms of the GNU General Public License
 404
 405(CVS $Revision: 1.17 $)} \
 406            -justify center -aspect 400
 407    pack $w.m -side top -fill x -padx 20 -pady 20
 408    button $w.ok -text Close -command "destroy $w"
 409    pack $w.ok -side bottom
 410}
 411
 412proc truncatetofit {str width font} {
 413    if {[font measure $font $str] <= $width} {
 414        return $str
 415    }
 416    set best 0
 417    set bad [string length $str]
 418    set tmp $str
 419    while {$best < $bad - 1} {
 420        set try [expr {int(($best + $bad) / 2)}]
 421        set tmp "[string range $str 0 [expr $try-1]]..."
 422        if {[font measure $font $tmp] <= $width} {
 423            set best $try
 424        } else {
 425            set bad $try
 426        }
 427    }
 428    return $tmp
 429}
 430
 431proc assigncolor {id} {
 432    global commitinfo colormap commcolors colors nextcolor
 433    global colorbycommitter
 434    global parents nparents children nchildren
 435    if [info exists colormap($id)] return
 436    set ncolors [llength $colors]
 437    if {$colorbycommitter} {
 438        if {![info exists commitinfo($id)]} {
 439            readcommit $id
 440        }
 441        set comm [lindex $commitinfo($id) 3]
 442        if {![info exists commcolors($comm)]} {
 443            set commcolors($comm) [lindex $colors $nextcolor]
 444            if {[incr nextcolor] >= $ncolors} {
 445                set nextcolor 0
 446            }
 447        }
 448        set colormap($id) $commcolors($comm)
 449    } else {
 450        if {$nparents($id) == 1 && $nchildren($id) == 1} {
 451            set child [lindex $children($id) 0]
 452            if {[info exists colormap($child)]
 453                && $nparents($child) == 1} {
 454                set colormap($id) $colormap($child)
 455                return
 456            }
 457        }
 458        set badcolors {}
 459        foreach child $children($id) {
 460            if {[info exists colormap($child)]
 461                && [lsearch -exact $badcolors $colormap($child)] < 0} {
 462                lappend badcolors $colormap($child)
 463            }
 464            if {[info exists parents($child)]} {
 465                foreach p $parents($child) {
 466                    if {[info exists colormap($p)]
 467                        && [lsearch -exact $badcolors $colormap($p)] < 0} {
 468                        lappend badcolors $colormap($p)
 469                    }
 470                }
 471            }
 472        }
 473        if {[llength $badcolors] >= $ncolors} {
 474            set badcolors {}
 475        }
 476        for {set i 0} {$i <= $ncolors} {incr i} {
 477            set c [lindex $colors $nextcolor]
 478            if {[incr nextcolor] >= $ncolors} {
 479                set nextcolor 0
 480            }
 481            if {[lsearch -exact $badcolors $c]} break
 482        }
 483        set colormap($id) $c
 484    }
 485}
 486
 487proc drawgraph {} {
 488    global parents children nparents nchildren commits
 489    global canv canv2 canv3 mainfont namefont canvx0 canvy0 canvy linespc
 490    global datemode cdate
 491    global lineid linehtag linentag linedtag commitinfo
 492    global nextcolor colormap numcommits
 493    global stopped phase redisplaying selectedline
 494
 495    allcanvs delete all
 496    set start {}
 497    foreach id [array names nchildren] {
 498        if {$nchildren($id) == 0} {
 499            lappend start $id
 500        }
 501        set ncleft($id) $nchildren($id)
 502        if {![info exists nparents($id)]} {
 503            set nparents($id) 0
 504        }
 505    }
 506    if {$start == {}} {
 507        error_popup "Gitk: ERROR: No starting commits found"
 508        exit 1
 509    }
 510
 511    set nextcolor 0
 512    foreach id $start {
 513        assigncolor $id
 514    }
 515    set todo $start
 516    set level [expr [llength $todo] - 1]
 517    set y2 $canvy0
 518    set nullentry -1
 519    set lineno -1
 520    set numcommits 0
 521    set phase drawgraph
 522    set lthickness [expr {($linespc / 9) + 1}]
 523    while 1 {
 524        set canvy $y2
 525        allcanvs conf -scrollregion \
 526            [list 0 0 0 [expr $canvy + 0.5 * $linespc + 2]]
 527        update
 528        if {$stopped} break
 529        incr numcommits
 530        incr lineno
 531        set nlines [llength $todo]
 532        set id [lindex $todo $level]
 533        set lineid($lineno) $id
 534        set actualparents {}
 535        if {[info exists parents($id)]} {
 536            foreach p $parents($id) {
 537                incr ncleft($p) -1
 538                if {![info exists commitinfo($p)]} {
 539                    readcommit $p
 540                    if {![info exists commitinfo($p)]} continue
 541                }
 542                lappend actualparents $p
 543            }
 544        }
 545        if {![info exists commitinfo($id)]} {
 546            readcommit $id
 547            if {![info exists commitinfo($id)]} {
 548                set commitinfo($id) {"No commit information available"}
 549            }
 550        }
 551        set x [expr $canvx0 + $level * $linespc]
 552        set y2 [expr $canvy + $linespc]
 553        if {[info exists linestarty($level)] && $linestarty($level) < $canvy} {
 554            set t [$canv create line $x $linestarty($level) $x $canvy \
 555                       -width $lthickness -fill $colormap($id)]
 556            $canv lower $t
 557        }
 558        set linestarty($level) $canvy
 559        set ofill [expr {[info exists parents($id)]? "blue": "white"}]
 560        set orad [expr {$linespc / 3}]
 561        set t [$canv create oval [expr $x - $orad] [expr $canvy - $orad] \
 562                   [expr $x + $orad - 1] [expr $canvy + $orad - 1] \
 563                   -fill $ofill -outline black -width 1]
 564        $canv raise $t
 565        set xt [expr $canvx0 + $nlines * $linespc]
 566        set headline [lindex $commitinfo($id) 0]
 567        set name [lindex $commitinfo($id) 1]
 568        set date [lindex $commitinfo($id) 2]
 569        set linehtag($lineno) [$canv create text $xt $canvy -anchor w \
 570                                   -text $headline -font $mainfont ]
 571        set linentag($lineno) [$canv2 create text 3 $canvy -anchor w \
 572                                   -text $name -font $namefont]
 573        set linedtag($lineno) [$canv3 create text 3 $canvy -anchor w \
 574                                 -text $date -font $mainfont]
 575        if {!$datemode && [llength $actualparents] == 1} {
 576            set p [lindex $actualparents 0]
 577            if {$ncleft($p) == 0 && [lsearch -exact $todo $p] < 0} {
 578                assigncolor $p
 579                set todo [lreplace $todo $level $level $p]
 580                continue
 581            }
 582        }
 583
 584        set oldtodo $todo
 585        set oldlevel $level
 586        set lines {}
 587        for {set i 0} {$i < $nlines} {incr i} {
 588            if {[lindex $todo $i] == {}} continue
 589            if {[info exists linestarty($i)]} {
 590                set oldstarty($i) $linestarty($i)
 591                unset linestarty($i)
 592            }
 593            if {$i != $level} {
 594                lappend lines [list $i [lindex $todo $i]]
 595            }
 596        }
 597        if {$nullentry >= 0} {
 598            set todo [lreplace $todo $nullentry $nullentry]
 599            if {$nullentry < $level} {
 600                incr level -1
 601            }
 602        }
 603
 604        set todo [lreplace $todo $level $level]
 605        if {$nullentry > $level} {
 606            incr nullentry -1
 607        }
 608        set i $level
 609        foreach p $actualparents {
 610            set k [lsearch -exact $todo $p]
 611            if {$k < 0} {
 612                assigncolor $p
 613                set todo [linsert $todo $i $p]
 614                if {$nullentry >= $i} {
 615                    incr nullentry
 616                }
 617            }
 618            lappend lines [list $oldlevel $p]
 619        }
 620
 621        # choose which one to do next time around
 622        set todol [llength $todo]
 623        set level -1
 624        set latest {}
 625        for {set k $todol} {[incr k -1] >= 0} {} {
 626            set p [lindex $todo $k]
 627            if {$p == {}} continue
 628            if {$ncleft($p) == 0} {
 629                if {$datemode} {
 630                    if {$latest == {} || $cdate($p) > $latest} {
 631                        set level $k
 632                        set latest $cdate($p)
 633                    }
 634                } else {
 635                    set level $k
 636                    break
 637                }
 638            }
 639        }
 640        if {$level < 0} {
 641            if {$todo != {}} {
 642                puts "ERROR: none of the pending commits can be done yet:"
 643                foreach p $todo {
 644                    puts "  $p"
 645                }
 646            }
 647            break
 648        }
 649
 650        # If we are reducing, put in a null entry
 651        if {$todol < $nlines} {
 652            if {$nullentry >= 0} {
 653                set i $nullentry
 654                while {$i < $todol
 655                       && [lindex $oldtodo $i] == [lindex $todo $i]} {
 656                    incr i
 657                }
 658            } else {
 659                set i $oldlevel
 660                if {$level >= $i} {
 661                    incr i
 662                }
 663            }
 664            if {$i >= $todol} {
 665                set nullentry -1
 666            } else {
 667                set nullentry $i
 668                set todo [linsert $todo $nullentry {}]
 669                if {$level >= $i} {
 670                    incr level
 671                }
 672            }
 673        } else {
 674            set nullentry -1
 675        }
 676
 677        foreach l $lines {
 678            set i [lindex $l 0]
 679            set dst [lindex $l 1]
 680            set j [lsearch -exact $todo $dst]
 681            if {$i == $j} {
 682                if {[info exists oldstarty($i)]} {
 683                    set linestarty($i) $oldstarty($i)
 684                }
 685                continue
 686            }
 687            set xi [expr {$canvx0 + $i * $linespc}]
 688            set xj [expr {$canvx0 + $j * $linespc}]
 689            set coords {}
 690            if {[info exists oldstarty($i)] && $oldstarty($i) < $canvy} {
 691                lappend coords $xi $oldstarty($i)
 692            }
 693            lappend coords $xi $canvy
 694            if {$j < $i - 1} {
 695                lappend coords [expr $xj + $linespc] $canvy
 696            } elseif {$j > $i + 1} {
 697                lappend coords [expr $xj - $linespc] $canvy
 698            }
 699            lappend coords $xj $y2
 700            set t [$canv create line $coords -width $lthickness \
 701                       -fill $colormap($dst)]
 702            $canv lower $t
 703            if {![info exists linestarty($j)]} {
 704                set linestarty($j) $y2
 705            }
 706        }
 707    }
 708    set phase {}
 709    if {$redisplaying} {
 710        if {$stopped == 0 && [info exists selectedline]} {
 711            selectline $selectedline
 712        }
 713        if {$stopped == 1} {
 714            set stopped 0
 715            after idle drawgraph
 716        } else {
 717            set redisplaying 0
 718        }
 719    }
 720}
 721
 722proc findmatches {f} {
 723    global findtype foundstring foundstrlen
 724    if {$findtype == "Regexp"} {
 725        set matches [regexp -indices -all -inline $foundstring $f]
 726    } else {
 727        if {$findtype == "IgnCase"} {
 728            set str [string tolower $f]
 729        } else {
 730            set str $f
 731        }
 732        set matches {}
 733        set i 0
 734        while {[set j [string first $foundstring $str $i]] >= 0} {
 735            lappend matches [list $j [expr $j+$foundstrlen-1]]
 736            set i [expr $j + $foundstrlen]
 737        }
 738    }
 739    return $matches
 740}
 741
 742proc dofind {} {
 743    global findtype findloc findstring markedmatches commitinfo
 744    global numcommits lineid linehtag linentag linedtag
 745    global mainfont namefont canv canv2 canv3 selectedline
 746    global matchinglines foundstring foundstrlen
 747    unmarkmatches
 748    focus .
 749    set matchinglines {}
 750    set fldtypes {Headline Author Date Committer CDate Comment}
 751    if {$findtype == "IgnCase"} {
 752        set foundstring [string tolower $findstring]
 753    } else {
 754        set foundstring $findstring
 755    }
 756    set foundstrlen [string length $findstring]
 757    if {$foundstrlen == 0} return
 758    if {![info exists selectedline]} {
 759        set oldsel -1
 760    } else {
 761        set oldsel $selectedline
 762    }
 763    set didsel 0
 764    for {set l 0} {$l < $numcommits} {incr l} {
 765        set id $lineid($l)
 766        set info $commitinfo($id)
 767        set doesmatch 0
 768        foreach f $info ty $fldtypes {
 769            if {$findloc != "All fields" && $findloc != $ty} {
 770                continue
 771            }
 772            set matches [findmatches $f]
 773            if {$matches == {}} continue
 774            set doesmatch 1
 775            if {$ty == "Headline"} {
 776                markmatches $canv $l $f $linehtag($l) $matches $mainfont
 777            } elseif {$ty == "Author"} {
 778                markmatches $canv2 $l $f $linentag($l) $matches $namefont
 779            } elseif {$ty == "Date"} {
 780                markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
 781            }
 782        }
 783        if {$doesmatch} {
 784            lappend matchinglines $l
 785            if {!$didsel && $l > $oldsel} {
 786                findselectline $l
 787                set didsel 1
 788            }
 789        }
 790    }
 791    if {$matchinglines == {}} {
 792        bell
 793    } elseif {!$didsel} {
 794        findselectline [lindex $matchinglines 0]
 795    }
 796}
 797
 798proc findselectline {l} {
 799    global findloc commentend ctext
 800    selectline $l
 801    if {$findloc == "All fields" || $findloc == "Comments"} {
 802        # highlight the matches in the comments
 803        set f [$ctext get 1.0 $commentend]
 804        set matches [findmatches $f]
 805        foreach match $matches {
 806            set start [lindex $match 0]
 807            set end [expr [lindex $match 1] + 1]
 808            $ctext tag add found "1.0 + $start c" "1.0 + $end c"
 809        }
 810    }
 811}
 812
 813proc findnext {} {
 814    global matchinglines selectedline
 815    if {![info exists matchinglines]} {
 816        dofind
 817        return
 818    }
 819    if {![info exists selectedline]} return
 820    foreach l $matchinglines {
 821        if {$l > $selectedline} {
 822            findselectline $l
 823            return
 824        }
 825    }
 826    bell
 827}
 828
 829proc findprev {} {
 830    global matchinglines selectedline
 831    if {![info exists matchinglines]} {
 832        dofind
 833        return
 834    }
 835    if {![info exists selectedline]} return
 836    set prev {}
 837    foreach l $matchinglines {
 838        if {$l >= $selectedline} break
 839        set prev $l
 840    }
 841    if {$prev != {}} {
 842        findselectline $prev
 843    } else {
 844        bell
 845    }
 846}
 847
 848proc markmatches {canv l str tag matches font} {
 849    set bbox [$canv bbox $tag]
 850    set x0 [lindex $bbox 0]
 851    set y0 [lindex $bbox 1]
 852    set y1 [lindex $bbox 3]
 853    foreach match $matches {
 854        set start [lindex $match 0]
 855        set end [lindex $match 1]
 856        if {$start > $end} continue
 857        set xoff [font measure $font [string range $str 0 [expr $start-1]]]
 858        set xlen [font measure $font [string range $str 0 [expr $end]]]
 859        set t [$canv create rect [expr $x0+$xoff] $y0 [expr $x0+$xlen+2] $y1 \
 860                   -outline {} -tags matches -fill yellow]
 861        $canv lower $t
 862    }
 863}
 864
 865proc unmarkmatches {} {
 866    global matchinglines
 867    allcanvs delete matches
 868    catch {unset matchinglines}
 869}
 870
 871proc selcanvline {x y} {
 872    global canv canvy0 ctext linespc selectedline
 873    global lineid linehtag linentag linedtag
 874    set ymax [lindex [$canv cget -scrollregion] 3]
 875    set yfrac [lindex [$canv yview] 0]
 876    set y [expr {$y + $yfrac * $ymax}]
 877    set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
 878    if {$l < 0} {
 879        set l 0
 880    }
 881    if {[info exists selectedline] && $selectedline == $l} return
 882    unmarkmatches
 883    selectline $l
 884}
 885
 886proc selectline {l} {
 887    global canv canv2 canv3 ctext commitinfo selectedline
 888    global lineid linehtag linentag linedtag
 889    global canvy0 linespc nparents treepending
 890    global cflist treediffs currentid sha1entry
 891    global commentend seenfile numcommits
 892    if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
 893    $canv delete secsel
 894    set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
 895               -tags secsel -fill [$canv cget -selectbackground]]
 896    $canv lower $t
 897    $canv2 delete secsel
 898    set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
 899               -tags secsel -fill [$canv2 cget -selectbackground]]
 900    $canv2 lower $t
 901    $canv3 delete secsel
 902    set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
 903               -tags secsel -fill [$canv3 cget -selectbackground]]
 904    $canv3 lower $t
 905    set y [expr {$canvy0 + $l * $linespc}]
 906    set ymax [lindex [$canv cget -scrollregion] 3]
 907    set ytop [expr {$y - $linespc - 1}]
 908    set ybot [expr {$y + $linespc + 1}]
 909    set wnow [$canv yview]
 910    set wtop [expr [lindex $wnow 0] * $ymax]
 911    set wbot [expr [lindex $wnow 1] * $ymax]
 912    set wh [expr {$wbot - $wtop}]
 913    set newtop $wtop
 914    if {$ytop < $wtop} {
 915        if {$ybot < $wtop} {
 916            set newtop [expr {$y - $wh / 2.0}]
 917        } else {
 918            set newtop $ytop
 919            if {$newtop > $wtop - $linespc} {
 920                set newtop [expr {$wtop - $linespc}]
 921            }
 922        }
 923    } elseif {$ybot > $wbot} {
 924        if {$ytop > $wbot} {
 925            set newtop [expr {$y - $wh / 2.0}]
 926        } else {
 927            set newtop [expr {$ybot - $wh}]
 928            if {$newtop < $wtop + $linespc} {
 929                set newtop [expr {$wtop + $linespc}]
 930            }
 931        }
 932    }
 933    if {$newtop != $wtop} {
 934        if {$newtop < 0} {
 935            set newtop 0
 936        }
 937        allcanvs yview moveto [expr $newtop * 1.0 / $ymax]
 938    }
 939    set selectedline $l
 940
 941    set id $lineid($l)
 942    $sha1entry conf -state normal
 943    $sha1entry delete 0 end
 944    $sha1entry insert 0 $id
 945    $sha1entry selection from 0
 946    $sha1entry selection to end
 947    $sha1entry conf -state readonly
 948
 949    $ctext conf -state normal
 950    $ctext delete 0.0 end
 951    set info $commitinfo($id)
 952    $ctext insert end "Author: [lindex $info 1]  [lindex $info 2]\n"
 953    $ctext insert end "Committer: [lindex $info 3]  [lindex $info 4]\n"
 954    $ctext insert end "\n"
 955    $ctext insert end [lindex $info 5]
 956    $ctext insert end "\n"
 957    $ctext tag delete Comments
 958    $ctext tag remove found 1.0 end
 959    $ctext conf -state disabled
 960    set commentend [$ctext index "end - 1c"]
 961
 962    $cflist delete 0 end
 963    set currentid $id
 964    if {$nparents($id) == 1} {
 965        if {![info exists treediffs($id)]} {
 966            if {![info exists treepending]} {
 967                gettreediffs $id
 968            }
 969        } else {
 970            addtocflist $id
 971        }
 972    }
 973    catch {unset seenfile}
 974}
 975
 976proc selnextline {dir} {
 977    global selectedline
 978    if {![info exists selectedline]} return
 979    set l [expr $selectedline + $dir]
 980    unmarkmatches
 981    selectline $l
 982}
 983
 984proc addtocflist {id} {
 985    global currentid treediffs cflist treepending
 986    if {$id != $currentid} {
 987        gettreediffs $currentid
 988        return
 989    }
 990    $cflist insert end "All files"
 991    foreach f $treediffs($currentid) {
 992        $cflist insert end $f
 993    }
 994    getblobdiffs $id
 995}
 996
 997proc gettreediffs {id} {
 998    global treediffs parents treepending
 999    set treepending $id
1000    set treediffs($id) {}
1001    set p [lindex $parents($id) 0]
1002    if [catch {set gdtf [open "|git-diff-tree -r $p $id" r]}] return
1003    fconfigure $gdtf -blocking 0
1004    fileevent $gdtf readable "gettreediffline $gdtf $id"
1005}
1006
1007proc gettreediffline {gdtf id} {
1008    global treediffs treepending
1009    set n [gets $gdtf line]
1010    if {$n < 0} {
1011        if {![eof $gdtf]} return
1012        close $gdtf
1013        unset treepending
1014        addtocflist $id
1015        return
1016    }
1017    set type [lindex $line 1]
1018    set file [lindex $line 3]
1019    if {$type == "blob"} {
1020        lappend treediffs($id) $file
1021    }
1022}
1023
1024proc getblobdiffs {id} {
1025    global parents diffopts blobdifffd env curdifftag curtagstart
1026    global diffindex difffilestart
1027    set p [lindex $parents($id) 0]
1028    set env(GIT_DIFF_OPTS) $diffopts
1029    if [catch {set bdf [open "|git-diff-tree -r -p $p $id" r]} err] {
1030        puts "error getting diffs: $err"
1031        return
1032    }
1033    fconfigure $bdf -blocking 0
1034    set blobdifffd($id) $bdf
1035    set curdifftag Comments
1036    set curtagstart 0.0
1037    set diffindex 0
1038    catch {unset difffilestart}
1039    fileevent $bdf readable "getblobdiffline $bdf $id"
1040}
1041
1042proc getblobdiffline {bdf id} {
1043    global currentid blobdifffd ctext curdifftag curtagstart seenfile
1044    global diffnexthead diffnextnote diffindex difffilestart
1045    set n [gets $bdf line]
1046    if {$n < 0} {
1047        if {[eof $bdf]} {
1048            close $bdf
1049            if {$id == $currentid && $bdf == $blobdifffd($id)} {
1050                $ctext tag add $curdifftag $curtagstart end
1051                set seenfile($curdifftag) 1
1052            }
1053        }
1054        return
1055    }
1056    if {$id != $currentid || $bdf != $blobdifffd($id)} {
1057        return
1058    }
1059    $ctext conf -state normal
1060    if {[regexp {^---[ \t]+([^/])*/(.*)} $line match s1 fname]} {
1061        # start of a new file
1062        $ctext insert end "\n"
1063        $ctext tag add $curdifftag $curtagstart end
1064        set seenfile($curdifftag) 1
1065        set curtagstart [$ctext index "end - 1c"]
1066        set header $fname
1067        if {[info exists diffnexthead]} {
1068            set fname $diffnexthead
1069            set header "$diffnexthead ($diffnextnote)"
1070            unset diffnexthead
1071        }
1072        set difffilestart($diffindex) [$ctext index "end - 1c"]
1073        incr diffindex
1074        set curdifftag "f:$fname"
1075        $ctext tag delete $curdifftag
1076        set l [expr {(78 - [string length $header]) / 2}]
1077        set pad [string range "----------------------------------------" 1 $l]
1078        $ctext insert end "$pad $header $pad\n" filesep
1079    } elseif {[string range $line 0 2] == "+++"} {
1080        # no need to do anything with this
1081    } elseif {[regexp {^Created: (.*) \((mode: *[0-7]*)\)} $line match fn m]} {
1082        set diffnexthead $fn
1083        set diffnextnote "created, mode $m"
1084    } elseif {[string range $line 0 8] == "Deleted: "} {
1085        set diffnexthead [string range $line 9 end]
1086        set diffnextnote "deleted"
1087    } elseif {[regexp {^diff --git a/(.*) b/} $line match fn]} {
1088        # save the filename in case the next thing is "new file mode ..."
1089        set diffnexthead $fn
1090        set diffnextnote "modified"
1091    } elseif {[regexp {^new file mode ([0-7]+)} $line match m]} {
1092        set diffnextnote "new file, mode $m"
1093    } elseif {[string range $line 0 11] == "deleted file"} {
1094        set diffnextnote "deleted"
1095    } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
1096                   $line match f1l f1c f2l f2c rest]} {
1097        $ctext insert end "\t" hunksep
1098        $ctext insert end "    $f1l    " d0 "    $f2l    " d1
1099        $ctext insert end "    $rest \n" hunksep
1100    } else {
1101        set x [string range $line 0 0]
1102        if {$x == "-" || $x == "+"} {
1103            set tag [expr {$x == "+"}]
1104            set line [string range $line 1 end]
1105            $ctext insert end "$line\n" d$tag
1106        } elseif {$x == " "} {
1107            set line [string range $line 1 end]
1108            $ctext insert end "$line\n"
1109        } elseif {$x == "\\"} {
1110            # e.g. "\ No newline at end of file"
1111            $ctext insert end "$line\n" filesep
1112        } else {
1113            # Something else we don't recognize
1114            if {$curdifftag != "Comments"} {
1115                $ctext insert end "\n"
1116                $ctext tag add $curdifftag $curtagstart end
1117                set seenfile($curdifftag) 1
1118                set curtagstart [$ctext index "end - 1c"]
1119                set curdifftag Comments
1120            }
1121            $ctext insert end "$line\n" filesep
1122        }
1123    }
1124    $ctext conf -state disabled
1125}
1126
1127proc nextfile {} {
1128    global difffilestart ctext
1129    set here [$ctext index @0,0]
1130    for {set i 0} {[info exists difffilestart($i)]} {incr i} {
1131        if {[$ctext compare $difffilestart($i) > $here]} {
1132            $ctext yview $difffilestart($i)
1133            break
1134        }
1135    }
1136}
1137
1138proc listboxsel {} {
1139    global ctext cflist currentid treediffs seenfile
1140    if {![info exists currentid]} return
1141    set sel [$cflist curselection]
1142    if {$sel == {} || [lsearch -exact $sel 0] >= 0} {
1143        # show everything
1144        $ctext tag conf Comments -elide 0
1145        foreach f $treediffs($currentid) {
1146            if [info exists seenfile(f:$f)] {
1147                $ctext tag conf "f:$f" -elide 0
1148            }
1149        }
1150    } else {
1151        # just show selected files
1152        $ctext tag conf Comments -elide 1
1153        set i 1
1154        foreach f $treediffs($currentid) {
1155            set elide [expr {[lsearch -exact $sel $i] < 0}]
1156            if [info exists seenfile(f:$f)] {
1157                $ctext tag conf "f:$f" -elide $elide
1158            }
1159            incr i
1160        }
1161    }
1162}
1163
1164proc setcoords {} {
1165    global linespc charspc canvx0 canvy0 mainfont
1166    set linespc [font metrics $mainfont -linespace]
1167    set charspc [font measure $mainfont "m"]
1168    set canvy0 [expr 3 + 0.5 * $linespc]
1169    set canvx0 [expr 3 + 0.5 * $linespc]
1170}
1171
1172proc redisplay {} {
1173    global selectedline stopped redisplaying phase
1174    if {$stopped > 1} return
1175    if {$phase == "getcommits"} return
1176    set redisplaying 1
1177    if {$phase == "drawgraph"} {
1178        set stopped 1
1179    } else {
1180        drawgraph
1181    }
1182}
1183
1184proc incrfont {inc} {
1185    global mainfont namefont textfont selectedline ctext canv phase
1186    global stopped
1187    unmarkmatches
1188    set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
1189    set namefont [lreplace $namefont 1 1 [expr {[lindex $namefont 1] + $inc}]]
1190    set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
1191    setcoords
1192    $ctext conf -font $textfont
1193    $ctext tag conf filesep -font [concat $textfont bold]
1194    if {$phase == "getcommits"} {
1195        $canv itemconf textitems -font $mainfont
1196    }
1197    redisplay
1198}
1199
1200proc doquit {} {
1201    global stopped
1202    set stopped 100
1203    destroy .
1204}
1205
1206# defaults...
1207set datemode 0
1208set boldnames 0
1209set diffopts "-U 5 -p"
1210
1211set mainfont {Helvetica 9}
1212set textfont {Courier 9}
1213
1214set colors {green red blue magenta darkgrey brown orange}
1215set colorbycommitter false
1216
1217catch {source ~/.gitk}
1218
1219set namefont $mainfont
1220if {$boldnames} {
1221    lappend namefont bold
1222}
1223
1224set revtreeargs {}
1225foreach arg $argv {
1226    switch -regexp -- $arg {
1227        "^$" { }
1228        "^-b" { set boldnames 1 }
1229        "^-c" { set colorbycommitter 1 }
1230        "^-d" { set datemode 1 }
1231        "^-.*" {
1232            puts stderr "unrecognized option $arg"
1233            exit 1
1234        }
1235        default {
1236            lappend revtreeargs $arg
1237        }
1238    }
1239}
1240
1241set stopped 0
1242set redisplaying 0
1243set stuffsaved 0
1244setcoords
1245makewindow
1246getcommits $revtreeargs