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