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