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