gitkon commit Start of "git-send-pack", the local part of sending off a pack (6122147)
   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    $rowctxmenu add command -label "Create tag" -command mktag
 429}
 430
 431# when we make a key binding for the toplevel, make sure
 432# it doesn't get triggered when that key is pressed in the
 433# find string entry widget.
 434proc bindkey {ev script} {
 435    global entries
 436    bind . $ev $script
 437    set escript [bind Entry $ev]
 438    if {$escript == {}} {
 439        set escript [bind Entry <Key>]
 440    }
 441    foreach e $entries {
 442        bind $e $ev "$escript; break"
 443    }
 444}
 445
 446# set the focus back to the toplevel for any click outside
 447# the entry widgets
 448proc click {w} {
 449    global entries
 450    foreach e $entries {
 451        if {$w == $e} return
 452    }
 453    focus .
 454}
 455
 456proc savestuff {w} {
 457    global canv canv2 canv3 ctext cflist mainfont textfont
 458    global stuffsaved
 459    if {$stuffsaved} return
 460    if {![winfo viewable .]} return
 461    catch {
 462        set f [open "~/.gitk-new" w]
 463        puts $f "set mainfont {$mainfont}"
 464        puts $f "set textfont {$textfont}"
 465        puts $f "set geometry(width) [winfo width .ctop]"
 466        puts $f "set geometry(height) [winfo height .ctop]"
 467        puts $f "set geometry(canv1) [expr [winfo width $canv]-2]"
 468        puts $f "set geometry(canv2) [expr [winfo width $canv2]-2]"
 469        puts $f "set geometry(canv3) [expr [winfo width $canv3]-2]"
 470        puts $f "set geometry(canvh) [expr [winfo height $canv]-2]"
 471        set wid [expr {([winfo width $ctext] - 8) \
 472                           / [font measure $textfont "0"]}]
 473        puts $f "set geometry(ctextw) $wid"
 474        set wid [expr {([winfo width $cflist] - 11) \
 475                           / [font measure [$cflist cget -font] "0"]}]
 476        puts $f "set geometry(cflistw) $wid"
 477        close $f
 478        file rename -force "~/.gitk-new" "~/.gitk"
 479    }
 480    set stuffsaved 1
 481}
 482
 483proc resizeclistpanes {win w} {
 484    global oldwidth
 485    if [info exists oldwidth($win)] {
 486        set s0 [$win sash coord 0]
 487        set s1 [$win sash coord 1]
 488        if {$w < 60} {
 489            set sash0 [expr {int($w/2 - 2)}]
 490            set sash1 [expr {int($w*5/6 - 2)}]
 491        } else {
 492            set factor [expr {1.0 * $w / $oldwidth($win)}]
 493            set sash0 [expr {int($factor * [lindex $s0 0])}]
 494            set sash1 [expr {int($factor * [lindex $s1 0])}]
 495            if {$sash0 < 30} {
 496                set sash0 30
 497            }
 498            if {$sash1 < $sash0 + 20} {
 499                set sash1 [expr $sash0 + 20]
 500            }
 501            if {$sash1 > $w - 10} {
 502                set sash1 [expr $w - 10]
 503                if {$sash0 > $sash1 - 20} {
 504                    set sash0 [expr $sash1 - 20]
 505                }
 506            }
 507        }
 508        $win sash place 0 $sash0 [lindex $s0 1]
 509        $win sash place 1 $sash1 [lindex $s1 1]
 510    }
 511    set oldwidth($win) $w
 512}
 513
 514proc resizecdetpanes {win w} {
 515    global oldwidth
 516    if [info exists oldwidth($win)] {
 517        set s0 [$win sash coord 0]
 518        if {$w < 60} {
 519            set sash0 [expr {int($w*3/4 - 2)}]
 520        } else {
 521            set factor [expr {1.0 * $w / $oldwidth($win)}]
 522            set sash0 [expr {int($factor * [lindex $s0 0])}]
 523            if {$sash0 < 45} {
 524                set sash0 45
 525            }
 526            if {$sash0 > $w - 15} {
 527                set sash0 [expr $w - 15]
 528            }
 529        }
 530        $win sash place 0 $sash0 [lindex $s0 1]
 531    }
 532    set oldwidth($win) $w
 533}
 534
 535proc allcanvs args {
 536    global canv canv2 canv3
 537    eval $canv $args
 538    eval $canv2 $args
 539    eval $canv3 $args
 540}
 541
 542proc bindall {event action} {
 543    global canv canv2 canv3
 544    bind $canv $event $action
 545    bind $canv2 $event $action
 546    bind $canv3 $event $action
 547}
 548
 549proc about {} {
 550    set w .about
 551    if {[winfo exists $w]} {
 552        raise $w
 553        return
 554    }
 555    toplevel $w
 556    wm title $w "About gitk"
 557    message $w.m -text {
 558Gitk version 1.2
 559
 560Copyright © 2005 Paul Mackerras
 561
 562Use and redistribute under the terms of the GNU General Public License} \
 563            -justify center -aspect 400
 564    pack $w.m -side top -fill x -padx 20 -pady 20
 565    button $w.ok -text Close -command "destroy $w"
 566    pack $w.ok -side bottom
 567}
 568
 569proc assigncolor {id} {
 570    global commitinfo colormap commcolors colors nextcolor
 571    global parents nparents children nchildren
 572    global cornercrossings crossings
 573
 574    if [info exists colormap($id)] return
 575    set ncolors [llength $colors]
 576    if {$nparents($id) <= 1 && $nchildren($id) == 1} {
 577        set child [lindex $children($id) 0]
 578        if {[info exists colormap($child)]
 579            && $nparents($child) == 1} {
 580            set colormap($id) $colormap($child)
 581            return
 582        }
 583    }
 584    set badcolors {}
 585    if {[info exists cornercrossings($id)]} {
 586        foreach x $cornercrossings($id) {
 587            if {[info exists colormap($x)]
 588                && [lsearch -exact $badcolors $colormap($x)] < 0} {
 589                lappend badcolors $colormap($x)
 590            }
 591        }
 592        if {[llength $badcolors] >= $ncolors} {
 593            set badcolors {}
 594        }
 595    }
 596    set origbad $badcolors
 597    if {[llength $badcolors] < $ncolors - 1} {
 598        if {[info exists crossings($id)]} {
 599            foreach x $crossings($id) {
 600                if {[info exists colormap($x)]
 601                    && [lsearch -exact $badcolors $colormap($x)] < 0} {
 602                    lappend badcolors $colormap($x)
 603                }
 604            }
 605            if {[llength $badcolors] >= $ncolors} {
 606                set badcolors $origbad
 607            }
 608        }
 609        set origbad $badcolors
 610    }
 611    if {[llength $badcolors] < $ncolors - 1} {
 612        foreach child $children($id) {
 613            if {[info exists colormap($child)]
 614                && [lsearch -exact $badcolors $colormap($child)] < 0} {
 615                lappend badcolors $colormap($child)
 616            }
 617            if {[info exists parents($child)]} {
 618                foreach p $parents($child) {
 619                    if {[info exists colormap($p)]
 620                        && [lsearch -exact $badcolors $colormap($p)] < 0} {
 621                        lappend badcolors $colormap($p)
 622                    }
 623                }
 624            }
 625        }
 626        if {[llength $badcolors] >= $ncolors} {
 627            set badcolors $origbad
 628        }
 629    }
 630    for {set i 0} {$i <= $ncolors} {incr i} {
 631        set c [lindex $colors $nextcolor]
 632        if {[incr nextcolor] >= $ncolors} {
 633            set nextcolor 0
 634        }
 635        if {[lsearch -exact $badcolors $c]} break
 636    }
 637    set colormap($id) $c
 638}
 639
 640proc initgraph {} {
 641    global canvy canvy0 lineno numcommits lthickness nextcolor linespc
 642    global mainline sidelines
 643    global nchildren ncleft
 644
 645    allcanvs delete all
 646    set nextcolor 0
 647    set canvy $canvy0
 648    set lineno -1
 649    set numcommits 0
 650    set lthickness [expr {int($linespc / 9) + 1}]
 651    catch {unset mainline}
 652    catch {unset sidelines}
 653    foreach id [array names nchildren] {
 654        set ncleft($id) $nchildren($id)
 655    }
 656}
 657
 658proc bindline {t id} {
 659    global canv
 660
 661    $canv bind $t <Enter> "lineenter %x %y $id"
 662    $canv bind $t <Motion> "linemotion %x %y $id"
 663    $canv bind $t <Leave> "lineleave $id"
 664    $canv bind $t <Button-1> "lineclick %x %y $id"
 665}
 666
 667proc drawcommitline {level} {
 668    global parents children nparents nchildren todo
 669    global canv canv2 canv3 mainfont namefont canvx0 canvy linespc
 670    global lineid linehtag linentag linedtag commitinfo
 671    global colormap numcommits currentparents dupparents
 672    global oldlevel oldnlines oldtodo
 673    global idtags idline idheads
 674    global lineno lthickness mainline sidelines
 675    global commitlisted rowtextx idpos
 676
 677    incr numcommits
 678    incr lineno
 679    set id [lindex $todo $level]
 680    set lineid($lineno) $id
 681    set idline($id) $lineno
 682    set ofill [expr {[info exists commitlisted($id)]? "blue": "white"}]
 683    if {![info exists commitinfo($id)]} {
 684        readcommit $id
 685        if {![info exists commitinfo($id)]} {
 686            set commitinfo($id) {"No commit information available"}
 687            set nparents($id) 0
 688        }
 689    }
 690    assigncolor $id
 691    set currentparents {}
 692    set dupparents {}
 693    if {[info exists commitlisted($id)] && [info exists parents($id)]} {
 694        foreach p $parents($id) {
 695            if {[lsearch -exact $currentparents $p] < 0} {
 696                lappend currentparents $p
 697            } else {
 698                # remember that this parent was listed twice
 699                lappend dupparents $p
 700            }
 701        }
 702    }
 703    set x [expr $canvx0 + $level * $linespc]
 704    set y1 $canvy
 705    set canvy [expr $canvy + $linespc]
 706    allcanvs conf -scrollregion \
 707        [list 0 0 0 [expr $y1 + 0.5 * $linespc + 2]]
 708    if {[info exists mainline($id)]} {
 709        lappend mainline($id) $x $y1
 710        set t [$canv create line $mainline($id) \
 711                   -width $lthickness -fill $colormap($id)]
 712        $canv lower $t
 713        bindline $t $id
 714    }
 715    if {[info exists sidelines($id)]} {
 716        foreach ls $sidelines($id) {
 717            set coords [lindex $ls 0]
 718            set thick [lindex $ls 1]
 719            set t [$canv create line $coords -fill $colormap($id) \
 720                       -width [expr {$thick * $lthickness}]]
 721            $canv lower $t
 722            bindline $t $id
 723        }
 724    }
 725    set orad [expr {$linespc / 3}]
 726    set t [$canv create oval [expr $x - $orad] [expr $y1 - $orad] \
 727               [expr $x + $orad - 1] [expr $y1 + $orad - 1] \
 728               -fill $ofill -outline black -width 1]
 729    $canv raise $t
 730    $canv bind $t <1> {selcanvline {} %x %y}
 731    set xt [expr $canvx0 + [llength $todo] * $linespc]
 732    if {[llength $currentparents] > 2} {
 733        set xt [expr {$xt + ([llength $currentparents] - 2) * $linespc}]
 734    }
 735    set rowtextx($lineno) $xt
 736    set idpos($id) [list $x $xt $y1]
 737    if {[info exists idtags($id)] || [info exists idheads($id)]} {
 738        set xt [drawtags $id $x $xt $y1]
 739    }
 740    set headline [lindex $commitinfo($id) 0]
 741    set name [lindex $commitinfo($id) 1]
 742    set date [lindex $commitinfo($id) 2]
 743    set linehtag($lineno) [$canv create text $xt $y1 -anchor w \
 744                               -text $headline -font $mainfont ]
 745    $canv bind $linehtag($lineno) <Button-3> "rowmenu %X %Y $id"
 746    set linentag($lineno) [$canv2 create text 3 $y1 -anchor w \
 747                               -text $name -font $namefont]
 748    set linedtag($lineno) [$canv3 create text 3 $y1 -anchor w \
 749                               -text $date -font $mainfont]
 750}
 751
 752proc drawtags {id x xt y1} {
 753    global idtags idheads
 754    global linespc lthickness
 755    global canv mainfont
 756
 757    set marks {}
 758    set ntags 0
 759    if {[info exists idtags($id)]} {
 760        set marks $idtags($id)
 761        set ntags [llength $marks]
 762    }
 763    if {[info exists idheads($id)]} {
 764        set marks [concat $marks $idheads($id)]
 765    }
 766    if {$marks eq {}} {
 767        return $xt
 768    }
 769
 770    set delta [expr {int(0.5 * ($linespc - $lthickness))}]
 771    set yt [expr $y1 - 0.5 * $linespc]
 772    set yb [expr $yt + $linespc - 1]
 773    set xvals {}
 774    set wvals {}
 775    foreach tag $marks {
 776        set wid [font measure $mainfont $tag]
 777        lappend xvals $xt
 778        lappend wvals $wid
 779        set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
 780    }
 781    set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
 782               -width $lthickness -fill black -tags tag.$id]
 783    $canv lower $t
 784    foreach tag $marks x $xvals wid $wvals {
 785        set xl [expr $x + $delta]
 786        set xr [expr $x + $delta + $wid + $lthickness]
 787        if {[incr ntags -1] >= 0} {
 788            # draw a tag
 789            $canv create polygon $x [expr $yt + $delta] $xl $yt\
 790                $xr $yt $xr $yb $xl $yb $x [expr $yb - $delta] \
 791                -width 1 -outline black -fill yellow -tags tag.$id
 792        } else {
 793            # draw a head
 794            set xl [expr $xl - $delta/2]
 795            $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
 796                -width 1 -outline black -fill green -tags tag.$id
 797        }
 798        $canv create text $xl $y1 -anchor w -text $tag \
 799            -font $mainfont -tags tag.$id
 800    }
 801    return $xt
 802}
 803
 804proc updatetodo {level noshortcut} {
 805    global currentparents ncleft todo
 806    global mainline oldlevel oldtodo oldnlines
 807    global canvx0 canvy linespc mainline
 808    global commitinfo
 809
 810    set oldlevel $level
 811    set oldtodo $todo
 812    set oldnlines [llength $todo]
 813    if {!$noshortcut && [llength $currentparents] == 1} {
 814        set p [lindex $currentparents 0]
 815        if {$ncleft($p) == 1 && [lsearch -exact $todo $p] < 0} {
 816            set ncleft($p) 0
 817            set x [expr $canvx0 + $level * $linespc]
 818            set y [expr $canvy - $linespc]
 819            set mainline($p) [list $x $y]
 820            set todo [lreplace $todo $level $level $p]
 821            return 0
 822        }
 823    }
 824
 825    set todo [lreplace $todo $level $level]
 826    set i $level
 827    foreach p $currentparents {
 828        incr ncleft($p) -1
 829        set k [lsearch -exact $todo $p]
 830        if {$k < 0} {
 831            set todo [linsert $todo $i $p]
 832            incr i
 833        }
 834    }
 835    return 1
 836}
 837
 838proc notecrossings {id lo hi corner} {
 839    global oldtodo crossings cornercrossings
 840
 841    for {set i $lo} {[incr i] < $hi} {} {
 842        set p [lindex $oldtodo $i]
 843        if {$p == {}} continue
 844        if {$i == $corner} {
 845            if {![info exists cornercrossings($id)]
 846                || [lsearch -exact $cornercrossings($id) $p] < 0} {
 847                lappend cornercrossings($id) $p
 848            }
 849            if {![info exists cornercrossings($p)]
 850                || [lsearch -exact $cornercrossings($p) $id] < 0} {
 851                lappend cornercrossings($p) $id
 852            }
 853        } else {
 854            if {![info exists crossings($id)]
 855                || [lsearch -exact $crossings($id) $p] < 0} {
 856                lappend crossings($id) $p
 857            }
 858            if {![info exists crossings($p)]
 859                || [lsearch -exact $crossings($p) $id] < 0} {
 860                lappend crossings($p) $id
 861            }
 862        }
 863    }
 864}
 865
 866proc drawslants {} {
 867    global canv mainline sidelines canvx0 canvy linespc
 868    global oldlevel oldtodo todo currentparents dupparents
 869    global lthickness linespc canvy colormap
 870
 871    set y1 [expr $canvy - $linespc]
 872    set y2 $canvy
 873    set i -1
 874    foreach id $oldtodo {
 875        incr i
 876        if {$id == {}} continue
 877        set xi [expr {$canvx0 + $i * $linespc}]
 878        if {$i == $oldlevel} {
 879            foreach p $currentparents {
 880                set j [lsearch -exact $todo $p]
 881                set coords [list $xi $y1]
 882                set xj [expr {$canvx0 + $j * $linespc}]
 883                if {$j < $i - 1} {
 884                    lappend coords [expr $xj + $linespc] $y1
 885                    notecrossings $p $j $i [expr {$j + 1}]
 886                } elseif {$j > $i + 1} {
 887                    lappend coords [expr $xj - $linespc] $y1
 888                    notecrossings $p $i $j [expr {$j - 1}]
 889                }
 890                if {[lsearch -exact $dupparents $p] >= 0} {
 891                    # draw a double-width line to indicate the doubled parent
 892                    lappend coords $xj $y2
 893                    lappend sidelines($p) [list $coords 2]
 894                    if {![info exists mainline($p)]} {
 895                        set mainline($p) [list $xj $y2]
 896                    }
 897                } else {
 898                    # normal case, no parent duplicated
 899                    if {![info exists mainline($p)]} {
 900                        if {$i != $j} {
 901                            lappend coords $xj $y2
 902                        }
 903                        set mainline($p) $coords
 904                    } else {
 905                        lappend coords $xj $y2
 906                        lappend sidelines($p) [list $coords 1]
 907                    }
 908                }
 909            }
 910        } elseif {[lindex $todo $i] != $id} {
 911            set j [lsearch -exact $todo $id]
 912            set xj [expr {$canvx0 + $j * $linespc}]
 913            lappend mainline($id) $xi $y1 $xj $y2
 914        }
 915    }
 916}
 917
 918proc decidenext {{noread 0}} {
 919    global parents children nchildren ncleft todo
 920    global canv canv2 canv3 mainfont namefont canvx0 canvy linespc
 921    global datemode cdate
 922    global commitinfo
 923    global currentparents oldlevel oldnlines oldtodo
 924    global lineno lthickness
 925
 926    # remove the null entry if present
 927    set nullentry [lsearch -exact $todo {}]
 928    if {$nullentry >= 0} {
 929        set todo [lreplace $todo $nullentry $nullentry]
 930    }
 931
 932    # choose which one to do next time around
 933    set todol [llength $todo]
 934    set level -1
 935    set latest {}
 936    for {set k $todol} {[incr k -1] >= 0} {} {
 937        set p [lindex $todo $k]
 938        if {$ncleft($p) == 0} {
 939            if {$datemode} {
 940                if {![info exists commitinfo($p)]} {
 941                    if {$noread} {
 942                        return {}
 943                    }
 944                    readcommit $p
 945                }
 946                if {$latest == {} || $cdate($p) > $latest} {
 947                    set level $k
 948                    set latest $cdate($p)
 949                }
 950            } else {
 951                set level $k
 952                break
 953            }
 954        }
 955    }
 956    if {$level < 0} {
 957        if {$todo != {}} {
 958            puts "ERROR: none of the pending commits can be done yet:"
 959            foreach p $todo {
 960                puts "  $p ($ncleft($p))"
 961            }
 962        }
 963        return -1
 964    }
 965
 966    # If we are reducing, put in a null entry
 967    if {$todol < $oldnlines} {
 968        if {$nullentry >= 0} {
 969            set i $nullentry
 970            while {$i < $todol
 971                   && [lindex $oldtodo $i] == [lindex $todo $i]} {
 972                incr i
 973            }
 974        } else {
 975            set i $oldlevel
 976            if {$level >= $i} {
 977                incr i
 978            }
 979        }
 980        if {$i < $todol} {
 981            set todo [linsert $todo $i {}]
 982            if {$level >= $i} {
 983                incr level
 984            }
 985        }
 986    }
 987    return $level
 988}
 989
 990proc drawcommit {id} {
 991    global phase todo nchildren datemode nextupdate
 992    global startcommits
 993
 994    if {$phase != "incrdraw"} {
 995        set phase incrdraw
 996        set todo $id
 997        set startcommits $id
 998        initgraph
 999        drawcommitline 0
1000        updatetodo 0 $datemode
1001    } else {
1002        if {$nchildren($id) == 0} {
1003            lappend todo $id
1004            lappend startcommits $id
1005        }
1006        set level [decidenext 1]
1007        if {$level == {} || $id != [lindex $todo $level]} {
1008            return
1009        }
1010        while 1 {
1011            drawslants
1012            drawcommitline $level
1013            if {[updatetodo $level $datemode]} {
1014                set level [decidenext 1]
1015                if {$level == {}} break
1016            }
1017            set id [lindex $todo $level]
1018            if {![info exists commitlisted($id)]} {
1019                break
1020            }
1021            if {[clock clicks -milliseconds] >= $nextupdate} {
1022                doupdate
1023                if {$stopped} break
1024            }
1025        }
1026    }
1027}
1028
1029proc finishcommits {} {
1030    global phase
1031    global startcommits
1032    global canv mainfont ctext maincursor textcursor
1033
1034    if {$phase != "incrdraw"} {
1035        $canv delete all
1036        $canv create text 3 3 -anchor nw -text "No commits selected" \
1037            -font $mainfont -tags textitems
1038        set phase {}
1039    } else {
1040        drawslants
1041        set level [decidenext]
1042        drawrest $level [llength $startcommits]
1043    }
1044    . config -cursor $maincursor
1045    $ctext config -cursor $textcursor
1046}
1047
1048proc drawgraph {} {
1049    global nextupdate startmsecs startcommits todo
1050
1051    if {$startcommits == {}} return
1052    set startmsecs [clock clicks -milliseconds]
1053    set nextupdate [expr $startmsecs + 100]
1054    initgraph
1055    set todo [lindex $startcommits 0]
1056    drawrest 0 1
1057}
1058
1059proc drawrest {level startix} {
1060    global phase stopped redisplaying selectedline
1061    global datemode currentparents todo
1062    global numcommits
1063    global nextupdate startmsecs startcommits idline
1064
1065    if {$level >= 0} {
1066        set phase drawgraph
1067        set startid [lindex $startcommits $startix]
1068        set startline -1
1069        if {$startid != {}} {
1070            set startline $idline($startid)
1071        }
1072        while 1 {
1073            if {$stopped} break
1074            drawcommitline $level
1075            set hard [updatetodo $level $datemode]
1076            if {$numcommits == $startline} {
1077                lappend todo $startid
1078                set hard 1
1079                incr startix
1080                set startid [lindex $startcommits $startix]
1081                set startline -1
1082                if {$startid != {}} {
1083                    set startline $idline($startid)
1084                }
1085            }
1086            if {$hard} {
1087                set level [decidenext]
1088                if {$level < 0} break
1089                drawslants
1090            }
1091            if {[clock clicks -milliseconds] >= $nextupdate} {
1092                update
1093                incr nextupdate 100
1094            }
1095        }
1096    }
1097    set phase {}
1098    set drawmsecs [expr [clock clicks -milliseconds] - $startmsecs]
1099    #puts "overall $drawmsecs ms for $numcommits commits"
1100    if {$redisplaying} {
1101        if {$stopped == 0 && [info exists selectedline]} {
1102            selectline $selectedline
1103        }
1104        if {$stopped == 1} {
1105            set stopped 0
1106            after idle drawgraph
1107        } else {
1108            set redisplaying 0
1109        }
1110    }
1111}
1112
1113proc findmatches {f} {
1114    global findtype foundstring foundstrlen
1115    if {$findtype == "Regexp"} {
1116        set matches [regexp -indices -all -inline $foundstring $f]
1117    } else {
1118        if {$findtype == "IgnCase"} {
1119            set str [string tolower $f]
1120        } else {
1121            set str $f
1122        }
1123        set matches {}
1124        set i 0
1125        while {[set j [string first $foundstring $str $i]] >= 0} {
1126            lappend matches [list $j [expr $j+$foundstrlen-1]]
1127            set i [expr $j + $foundstrlen]
1128        }
1129    }
1130    return $matches
1131}
1132
1133proc dofind {} {
1134    global findtype findloc findstring markedmatches commitinfo
1135    global numcommits lineid linehtag linentag linedtag
1136    global mainfont namefont canv canv2 canv3 selectedline
1137    global matchinglines foundstring foundstrlen
1138    unmarkmatches
1139    focus .
1140    set matchinglines {}
1141    set fldtypes {Headline Author Date Committer CDate Comment}
1142    if {$findtype == "IgnCase"} {
1143        set foundstring [string tolower $findstring]
1144    } else {
1145        set foundstring $findstring
1146    }
1147    set foundstrlen [string length $findstring]
1148    if {$foundstrlen == 0} return
1149    if {![info exists selectedline]} {
1150        set oldsel -1
1151    } else {
1152        set oldsel $selectedline
1153    }
1154    set didsel 0
1155    for {set l 0} {$l < $numcommits} {incr l} {
1156        set id $lineid($l)
1157        set info $commitinfo($id)
1158        set doesmatch 0
1159        foreach f $info ty $fldtypes {
1160            if {$findloc != "All fields" && $findloc != $ty} {
1161                continue
1162            }
1163            set matches [findmatches $f]
1164            if {$matches == {}} continue
1165            set doesmatch 1
1166            if {$ty == "Headline"} {
1167                markmatches $canv $l $f $linehtag($l) $matches $mainfont
1168            } elseif {$ty == "Author"} {
1169                markmatches $canv2 $l $f $linentag($l) $matches $namefont
1170            } elseif {$ty == "Date"} {
1171                markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
1172            }
1173        }
1174        if {$doesmatch} {
1175            lappend matchinglines $l
1176            if {!$didsel && $l > $oldsel} {
1177                findselectline $l
1178                set didsel 1
1179            }
1180        }
1181    }
1182    if {$matchinglines == {}} {
1183        bell
1184    } elseif {!$didsel} {
1185        findselectline [lindex $matchinglines 0]
1186    }
1187}
1188
1189proc findselectline {l} {
1190    global findloc commentend ctext
1191    selectline $l
1192    if {$findloc == "All fields" || $findloc == "Comments"} {
1193        # highlight the matches in the comments
1194        set f [$ctext get 1.0 $commentend]
1195        set matches [findmatches $f]
1196        foreach match $matches {
1197            set start [lindex $match 0]
1198            set end [expr [lindex $match 1] + 1]
1199            $ctext tag add found "1.0 + $start c" "1.0 + $end c"
1200        }
1201    }
1202}
1203
1204proc findnext {} {
1205    global matchinglines selectedline
1206    if {![info exists matchinglines]} {
1207        dofind
1208        return
1209    }
1210    if {![info exists selectedline]} return
1211    foreach l $matchinglines {
1212        if {$l > $selectedline} {
1213            findselectline $l
1214            return
1215        }
1216    }
1217    bell
1218}
1219
1220proc findprev {} {
1221    global matchinglines selectedline
1222    if {![info exists matchinglines]} {
1223        dofind
1224        return
1225    }
1226    if {![info exists selectedline]} return
1227    set prev {}
1228    foreach l $matchinglines {
1229        if {$l >= $selectedline} break
1230        set prev $l
1231    }
1232    if {$prev != {}} {
1233        findselectline $prev
1234    } else {
1235        bell
1236    }
1237}
1238
1239proc markmatches {canv l str tag matches font} {
1240    set bbox [$canv bbox $tag]
1241    set x0 [lindex $bbox 0]
1242    set y0 [lindex $bbox 1]
1243    set y1 [lindex $bbox 3]
1244    foreach match $matches {
1245        set start [lindex $match 0]
1246        set end [lindex $match 1]
1247        if {$start > $end} continue
1248        set xoff [font measure $font [string range $str 0 [expr $start-1]]]
1249        set xlen [font measure $font [string range $str 0 [expr $end]]]
1250        set t [$canv create rect [expr $x0+$xoff] $y0 [expr $x0+$xlen+2] $y1 \
1251                   -outline {} -tags matches -fill yellow]
1252        $canv lower $t
1253    }
1254}
1255
1256proc unmarkmatches {} {
1257    global matchinglines
1258    allcanvs delete matches
1259    catch {unset matchinglines}
1260}
1261
1262proc selcanvline {w x y} {
1263    global canv canvy0 ctext linespc selectedline
1264    global lineid linehtag linentag linedtag rowtextx
1265    set ymax [lindex [$canv cget -scrollregion] 3]
1266    if {$ymax == {}} return
1267    set yfrac [lindex [$canv yview] 0]
1268    set y [expr {$y + $yfrac * $ymax}]
1269    set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
1270    if {$l < 0} {
1271        set l 0
1272    }
1273    if {$w eq $canv} {
1274        if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
1275    }
1276    unmarkmatches
1277    selectline $l
1278}
1279
1280proc selectline {l} {
1281    global canv canv2 canv3 ctext commitinfo selectedline
1282    global lineid linehtag linentag linedtag
1283    global canvy0 linespc parents nparents
1284    global cflist currentid sha1entry diffids
1285    global commentend seenfile idtags
1286    $canv delete hover
1287    if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
1288    $canv delete secsel
1289    set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
1290               -tags secsel -fill [$canv cget -selectbackground]]
1291    $canv lower $t
1292    $canv2 delete secsel
1293    set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
1294               -tags secsel -fill [$canv2 cget -selectbackground]]
1295    $canv2 lower $t
1296    $canv3 delete secsel
1297    set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
1298               -tags secsel -fill [$canv3 cget -selectbackground]]
1299    $canv3 lower $t
1300    set y [expr {$canvy0 + $l * $linespc}]
1301    set ymax [lindex [$canv cget -scrollregion] 3]
1302    set ytop [expr {$y - $linespc - 1}]
1303    set ybot [expr {$y + $linespc + 1}]
1304    set wnow [$canv yview]
1305    set wtop [expr [lindex $wnow 0] * $ymax]
1306    set wbot [expr [lindex $wnow 1] * $ymax]
1307    set wh [expr {$wbot - $wtop}]
1308    set newtop $wtop
1309    if {$ytop < $wtop} {
1310        if {$ybot < $wtop} {
1311            set newtop [expr {$y - $wh / 2.0}]
1312        } else {
1313            set newtop $ytop
1314            if {$newtop > $wtop - $linespc} {
1315                set newtop [expr {$wtop - $linespc}]
1316            }
1317        }
1318    } elseif {$ybot > $wbot} {
1319        if {$ytop > $wbot} {
1320            set newtop [expr {$y - $wh / 2.0}]
1321        } else {
1322            set newtop [expr {$ybot - $wh}]
1323            if {$newtop < $wtop + $linespc} {
1324                set newtop [expr {$wtop + $linespc}]
1325            }
1326        }
1327    }
1328    if {$newtop != $wtop} {
1329        if {$newtop < 0} {
1330            set newtop 0
1331        }
1332        allcanvs yview moveto [expr $newtop * 1.0 / $ymax]
1333    }
1334    set selectedline $l
1335
1336    set id $lineid($l)
1337    set currentid $id
1338    set diffids [concat $id $parents($id)]
1339    $sha1entry delete 0 end
1340    $sha1entry insert 0 $id
1341    $sha1entry selection from 0
1342    $sha1entry selection to end
1343
1344    $ctext conf -state normal
1345    $ctext delete 0.0 end
1346    $ctext mark set fmark.0 0.0
1347    $ctext mark gravity fmark.0 left
1348    set info $commitinfo($id)
1349    $ctext insert end "Author: [lindex $info 1]  [lindex $info 2]\n"
1350    $ctext insert end "Committer: [lindex $info 3]  [lindex $info 4]\n"
1351    if {[info exists idtags($id)]} {
1352        $ctext insert end "Tags:"
1353        foreach tag $idtags($id) {
1354            $ctext insert end " $tag"
1355        }
1356        $ctext insert end "\n"
1357    }
1358    $ctext insert end "\n"
1359    $ctext insert end [lindex $info 5]
1360    $ctext insert end "\n"
1361    $ctext tag delete Comments
1362    $ctext tag remove found 1.0 end
1363    $ctext conf -state disabled
1364    set commentend [$ctext index "end - 1c"]
1365
1366    $cflist delete 0 end
1367    $cflist insert end "Comments"
1368    if {$nparents($id) == 1} {
1369        startdiff
1370    }
1371    catch {unset seenfile}
1372}
1373
1374proc startdiff {} {
1375    global treediffs diffids treepending
1376
1377    if {![info exists treediffs($diffids)]} {
1378        if {![info exists treepending]} {
1379            gettreediffs $diffids
1380        }
1381    } else {
1382        addtocflist $diffids
1383    }
1384}
1385
1386proc selnextline {dir} {
1387    global selectedline
1388    if {![info exists selectedline]} return
1389    set l [expr $selectedline + $dir]
1390    unmarkmatches
1391    selectline $l
1392}
1393
1394proc addtocflist {ids} {
1395    global diffids treediffs cflist
1396    if {$ids != $diffids} {
1397        gettreediffs $diffids
1398        return
1399    }
1400    foreach f $treediffs($ids) {
1401        $cflist insert end $f
1402    }
1403    getblobdiffs $ids
1404}
1405
1406proc gettreediffs {ids} {
1407    global treediffs parents treepending
1408    set treepending $ids
1409    set treediffs($ids) {}
1410    set id [lindex $ids 0]
1411    set p [lindex $ids 1]
1412    if [catch {set gdtf [open "|git-diff-tree -r $p $id" r]}] return
1413    fconfigure $gdtf -blocking 0
1414    fileevent $gdtf readable "gettreediffline $gdtf {$ids}"
1415}
1416
1417proc gettreediffline {gdtf ids} {
1418    global treediffs treepending
1419    set n [gets $gdtf line]
1420    if {$n < 0} {
1421        if {![eof $gdtf]} return
1422        close $gdtf
1423        unset treepending
1424        addtocflist $ids
1425        return
1426    }
1427    set file [lindex $line 5]
1428    lappend treediffs($ids) $file
1429}
1430
1431proc getblobdiffs {ids} {
1432    global diffopts blobdifffd env curdifftag curtagstart
1433    global diffindex difffilestart nextupdate
1434
1435    set id [lindex $ids 0]
1436    set p [lindex $ids 1]
1437    set env(GIT_DIFF_OPTS) $diffopts
1438    if [catch {set bdf [open "|git-diff-tree -r -p $p $id" r]} err] {
1439        puts "error getting diffs: $err"
1440        return
1441    }
1442    fconfigure $bdf -blocking 0
1443    set blobdifffd($ids) $bdf
1444    set curdifftag Comments
1445    set curtagstart 0.0
1446    set diffindex 0
1447    catch {unset difffilestart}
1448    fileevent $bdf readable "getblobdiffline $bdf {$ids}"
1449    set nextupdate [expr {[clock clicks -milliseconds] + 100}]
1450}
1451
1452proc getblobdiffline {bdf ids} {
1453    global diffids blobdifffd ctext curdifftag curtagstart seenfile
1454    global diffnexthead diffnextnote diffindex difffilestart
1455    global nextupdate
1456
1457    set n [gets $bdf line]
1458    if {$n < 0} {
1459        if {[eof $bdf]} {
1460            close $bdf
1461            if {$ids == $diffids && $bdf == $blobdifffd($ids)} {
1462                $ctext tag add $curdifftag $curtagstart end
1463                set seenfile($curdifftag) 1
1464            }
1465        }
1466        return
1467    }
1468    if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
1469        return
1470    }
1471    $ctext conf -state normal
1472    if {[regexp {^---[ \t]+([^/])*/(.*)} $line match s1 fname]} {
1473        # start of a new file
1474        $ctext insert end "\n"
1475        $ctext tag add $curdifftag $curtagstart end
1476        set seenfile($curdifftag) 1
1477        set curtagstart [$ctext index "end - 1c"]
1478        set header $fname
1479        if {[info exists diffnexthead]} {
1480            set fname $diffnexthead
1481            set header "$diffnexthead ($diffnextnote)"
1482            unset diffnexthead
1483        }
1484        set here [$ctext index "end - 1c"]
1485        set difffilestart($diffindex) $here
1486        incr diffindex
1487        # start mark names at fmark.1 for first file
1488        $ctext mark set fmark.$diffindex $here
1489        $ctext mark gravity fmark.$diffindex left
1490        set curdifftag "f:$fname"
1491        $ctext tag delete $curdifftag
1492        set l [expr {(78 - [string length $header]) / 2}]
1493        set pad [string range "----------------------------------------" 1 $l]
1494        $ctext insert end "$pad $header $pad\n" filesep
1495    } elseif {[string range $line 0 2] == "+++"} {
1496        # no need to do anything with this
1497    } elseif {[regexp {^Created: (.*) \((mode: *[0-7]*)\)} $line match fn m]} {
1498        set diffnexthead $fn
1499        set diffnextnote "created, mode $m"
1500    } elseif {[string range $line 0 8] == "Deleted: "} {
1501        set diffnexthead [string range $line 9 end]
1502        set diffnextnote "deleted"
1503    } elseif {[regexp {^diff --git a/(.*) b/} $line match fn]} {
1504        # save the filename in case the next thing is "new file mode ..."
1505        set diffnexthead $fn
1506        set diffnextnote "modified"
1507    } elseif {[regexp {^new file mode ([0-7]+)} $line match m]} {
1508        set diffnextnote "new file, mode $m"
1509    } elseif {[string range $line 0 11] == "deleted file"} {
1510        set diffnextnote "deleted"
1511    } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
1512                   $line match f1l f1c f2l f2c rest]} {
1513        $ctext insert end "\t" hunksep
1514        $ctext insert end "    $f1l    " d0 "    $f2l    " d1
1515        $ctext insert end "    $rest \n" hunksep
1516    } else {
1517        set x [string range $line 0 0]
1518        if {$x == "-" || $x == "+"} {
1519            set tag [expr {$x == "+"}]
1520            set line [string range $line 1 end]
1521            $ctext insert end "$line\n" d$tag
1522        } elseif {$x == " "} {
1523            set line [string range $line 1 end]
1524            $ctext insert end "$line\n"
1525        } elseif {$x == "\\"} {
1526            # e.g. "\ No newline at end of file"
1527            $ctext insert end "$line\n" filesep
1528        } else {
1529            # Something else we don't recognize
1530            if {$curdifftag != "Comments"} {
1531                $ctext insert end "\n"
1532                $ctext tag add $curdifftag $curtagstart end
1533                set seenfile($curdifftag) 1
1534                set curtagstart [$ctext index "end - 1c"]
1535                set curdifftag Comments
1536            }
1537            $ctext insert end "$line\n" filesep
1538        }
1539    }
1540    $ctext conf -state disabled
1541    if {[clock clicks -milliseconds] >= $nextupdate} {
1542        incr nextupdate 100
1543        fileevent $bdf readable {}
1544        update
1545        fileevent $bdf readable "getblobdiffline $bdf {$ids}"
1546    }
1547}
1548
1549proc nextfile {} {
1550    global difffilestart ctext
1551    set here [$ctext index @0,0]
1552    for {set i 0} {[info exists difffilestart($i)]} {incr i} {
1553        if {[$ctext compare $difffilestart($i) > $here]} {
1554            $ctext yview $difffilestart($i)
1555            break
1556        }
1557    }
1558}
1559
1560proc listboxsel {} {
1561    global ctext cflist currentid treediffs seenfile
1562    if {![info exists currentid]} return
1563    set sel [lsort [$cflist curselection]]
1564    if {$sel eq {}} return
1565    set first [lindex $sel 0]
1566    catch {$ctext yview fmark.$first}
1567}
1568
1569proc setcoords {} {
1570    global linespc charspc canvx0 canvy0 mainfont
1571    set linespc [font metrics $mainfont -linespace]
1572    set charspc [font measure $mainfont "m"]
1573    set canvy0 [expr 3 + 0.5 * $linespc]
1574    set canvx0 [expr 3 + 0.5 * $linespc]
1575}
1576
1577proc redisplay {} {
1578    global selectedline stopped redisplaying phase
1579    if {$stopped > 1} return
1580    if {$phase == "getcommits"} return
1581    set redisplaying 1
1582    if {$phase == "drawgraph" || $phase == "incrdraw"} {
1583        set stopped 1
1584    } else {
1585        drawgraph
1586    }
1587}
1588
1589proc incrfont {inc} {
1590    global mainfont namefont textfont selectedline ctext canv phase
1591    global stopped entries
1592    unmarkmatches
1593    set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
1594    set namefont [lreplace $namefont 1 1 [expr {[lindex $namefont 1] + $inc}]]
1595    set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
1596    setcoords
1597    $ctext conf -font $textfont
1598    $ctext tag conf filesep -font [concat $textfont bold]
1599    foreach e $entries {
1600        $e conf -font $mainfont
1601    }
1602    if {$phase == "getcommits"} {
1603        $canv itemconf textitems -font $mainfont
1604    }
1605    redisplay
1606}
1607
1608proc clearsha1 {} {
1609    global sha1entry sha1string
1610    if {[string length $sha1string] == 40} {
1611        $sha1entry delete 0 end
1612    }
1613}
1614
1615proc sha1change {n1 n2 op} {
1616    global sha1string currentid sha1but
1617    if {$sha1string == {}
1618        || ([info exists currentid] && $sha1string == $currentid)} {
1619        set state disabled
1620    } else {
1621        set state normal
1622    }
1623    if {[$sha1but cget -state] == $state} return
1624    if {$state == "normal"} {
1625        $sha1but conf -state normal -relief raised -text "Goto: "
1626    } else {
1627        $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
1628    }
1629}
1630
1631proc gotocommit {} {
1632    global sha1string currentid idline tagids
1633    if {$sha1string == {}
1634        || ([info exists currentid] && $sha1string == $currentid)} return
1635    if {[info exists tagids($sha1string)]} {
1636        set id $tagids($sha1string)
1637    } else {
1638        set id [string tolower $sha1string]
1639    }
1640    if {[info exists idline($id)]} {
1641        selectline $idline($id)
1642        return
1643    }
1644    if {[regexp {^[0-9a-fA-F]{40}$} $sha1string]} {
1645        set type "SHA1 id"
1646    } else {
1647        set type "Tag"
1648    }
1649    error_popup "$type $sha1string is not known"
1650}
1651
1652proc lineenter {x y id} {
1653    global hoverx hovery hoverid hovertimer
1654    global commitinfo canv
1655
1656    if {![info exists commitinfo($id)]} return
1657    set hoverx $x
1658    set hovery $y
1659    set hoverid $id
1660    if {[info exists hovertimer]} {
1661        after cancel $hovertimer
1662    }
1663    set hovertimer [after 500 linehover]
1664    $canv delete hover
1665}
1666
1667proc linemotion {x y id} {
1668    global hoverx hovery hoverid hovertimer
1669
1670    if {[info exists hoverid] && $id == $hoverid} {
1671        set hoverx $x
1672        set hovery $y
1673        if {[info exists hovertimer]} {
1674            after cancel $hovertimer
1675        }
1676        set hovertimer [after 500 linehover]
1677    }
1678}
1679
1680proc lineleave {id} {
1681    global hoverid hovertimer canv
1682
1683    if {[info exists hoverid] && $id == $hoverid} {
1684        $canv delete hover
1685        if {[info exists hovertimer]} {
1686            after cancel $hovertimer
1687            unset hovertimer
1688        }
1689        unset hoverid
1690    }
1691}
1692
1693proc linehover {} {
1694    global hoverx hovery hoverid hovertimer
1695    global canv linespc lthickness
1696    global commitinfo mainfont
1697
1698    set text [lindex $commitinfo($hoverid) 0]
1699    set ymax [lindex [$canv cget -scrollregion] 3]
1700    if {$ymax == {}} return
1701    set yfrac [lindex [$canv yview] 0]
1702    set x [expr {$hoverx + 2 * $linespc}]
1703    set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
1704    set x0 [expr {$x - 2 * $lthickness}]
1705    set y0 [expr {$y - 2 * $lthickness}]
1706    set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}]
1707    set y1 [expr {$y + $linespc + 2 * $lthickness}]
1708    set t [$canv create rectangle $x0 $y0 $x1 $y1 \
1709               -fill \#ffff80 -outline black -width 1 -tags hover]
1710    $canv raise $t
1711    set t [$canv create text $x $y -anchor nw -text $text -tags hover]
1712    $canv raise $t
1713}
1714
1715proc lineclick {x y id} {
1716    global ctext commitinfo children cflist canv
1717
1718    unmarkmatches
1719    $canv delete hover
1720    # fill the details pane with info about this line
1721    $ctext conf -state normal
1722    $ctext delete 0.0 end
1723    $ctext insert end "Parent:\n "
1724    catch {destroy $ctext.$id}
1725    button $ctext.$id -text "Go:" -command "selbyid $id" \
1726        -padx 4 -pady 0
1727    $ctext window create end -window $ctext.$id -align center
1728    set info $commitinfo($id)
1729    $ctext insert end "\t[lindex $info 0]\n"
1730    $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
1731    $ctext insert end "\tDate:\t[lindex $info 2]\n"
1732    $ctext insert end "\tID:\t$id\n"
1733    if {[info exists children($id)]} {
1734        $ctext insert end "\nChildren:"
1735        foreach child $children($id) {
1736            $ctext insert end "\n "
1737            catch {destroy $ctext.$child}
1738            button $ctext.$child -text "Go:" -command "selbyid $child" \
1739                -padx 4 -pady 0
1740            $ctext window create end -window $ctext.$child -align center
1741            set info $commitinfo($child)
1742            $ctext insert end "\t[lindex $info 0]"
1743        }
1744    }
1745    $ctext conf -state disabled
1746
1747    $cflist delete 0 end
1748}
1749
1750proc selbyid {id} {
1751    global idline
1752    if {[info exists idline($id)]} {
1753        selectline $idline($id)
1754    }
1755}
1756
1757proc mstime {} {
1758    global startmstime
1759    if {![info exists startmstime]} {
1760        set startmstime [clock clicks -milliseconds]
1761    }
1762    return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
1763}
1764
1765proc rowmenu {x y id} {
1766    global rowctxmenu idline selectedline rowmenuid
1767
1768    if {![info exists selectedline] || $idline($id) eq $selectedline} {
1769        set state disabled
1770    } else {
1771        set state normal
1772    }
1773    $rowctxmenu entryconfigure 0 -state $state
1774    $rowctxmenu entryconfigure 1 -state $state
1775    $rowctxmenu entryconfigure 2 -state $state
1776    set rowmenuid $id
1777    tk_popup $rowctxmenu $x $y
1778}
1779
1780proc diffvssel {dirn} {
1781    global rowmenuid selectedline lineid
1782    global ctext cflist
1783    global diffids commitinfo
1784
1785    if {![info exists selectedline]} return
1786    if {$dirn} {
1787        set oldid $lineid($selectedline)
1788        set newid $rowmenuid
1789    } else {
1790        set oldid $rowmenuid
1791        set newid $lineid($selectedline)
1792    }
1793    $ctext conf -state normal
1794    $ctext delete 0.0 end
1795    $ctext mark set fmark.0 0.0
1796    $ctext mark gravity fmark.0 left
1797    $cflist delete 0 end
1798    $cflist insert end "Top"
1799    $ctext insert end "From $oldid\n     "
1800    $ctext insert end [lindex $commitinfo($oldid) 0]
1801    $ctext insert end "\n\nTo   $newid\n     "
1802    $ctext insert end [lindex $commitinfo($newid) 0]
1803    $ctext insert end "\n"
1804    $ctext conf -state disabled
1805    $ctext tag delete Comments
1806    $ctext tag remove found 1.0 end
1807    set diffids [list $newid $oldid]
1808    startdiff
1809}
1810
1811proc mkpatch {} {
1812    global rowmenuid currentid commitinfo patchtop patchnum
1813
1814    if {![info exists currentid]} return
1815    set oldid $currentid
1816    set oldhead [lindex $commitinfo($oldid) 0]
1817    set newid $rowmenuid
1818    set newhead [lindex $commitinfo($newid) 0]
1819    set top .patch
1820    set patchtop $top
1821    catch {destroy $top}
1822    toplevel $top
1823    label $top.title -text "Generate patch"
1824    grid $top.title -
1825    label $top.from -text "From:"
1826    entry $top.fromsha1 -width 40
1827    $top.fromsha1 insert 0 $oldid
1828    $top.fromsha1 conf -state readonly
1829    grid $top.from $top.fromsha1 -sticky w
1830    entry $top.fromhead -width 60
1831    $top.fromhead insert 0 $oldhead
1832    $top.fromhead conf -state readonly
1833    grid x $top.fromhead -sticky w
1834    label $top.to -text "To:"
1835    entry $top.tosha1 -width 40
1836    $top.tosha1 insert 0 $newid
1837    $top.tosha1 conf -state readonly
1838    grid $top.to $top.tosha1 -sticky w
1839    entry $top.tohead -width 60
1840    $top.tohead insert 0 $newhead
1841    $top.tohead conf -state readonly
1842    grid x $top.tohead -sticky w
1843    button $top.rev -text "Reverse" -command mkpatchrev -padx 5
1844    grid $top.rev x -pady 10
1845    label $top.flab -text "Output file:"
1846    entry $top.fname -width 60
1847    $top.fname insert 0 [file normalize "patch$patchnum.patch"]
1848    incr patchnum
1849    grid $top.flab $top.fname -sticky w
1850    frame $top.buts
1851    button $top.buts.gen -text "Generate" -command mkpatchgo
1852    button $top.buts.can -text "Cancel" -command mkpatchcan
1853    grid $top.buts.gen $top.buts.can
1854    grid columnconfigure $top.buts 0 -weight 1 -uniform a
1855    grid columnconfigure $top.buts 1 -weight 1 -uniform a
1856    grid $top.buts - -pady 10 -sticky ew
1857    focus $top.fname
1858}
1859
1860proc mkpatchrev {} {
1861    global patchtop
1862
1863    set oldid [$patchtop.fromsha1 get]
1864    set oldhead [$patchtop.fromhead get]
1865    set newid [$patchtop.tosha1 get]
1866    set newhead [$patchtop.tohead get]
1867    foreach e [list fromsha1 fromhead tosha1 tohead] \
1868            v [list $newid $newhead $oldid $oldhead] {
1869        $patchtop.$e conf -state normal
1870        $patchtop.$e delete 0 end
1871        $patchtop.$e insert 0 $v
1872        $patchtop.$e conf -state readonly
1873    }
1874}
1875
1876proc mkpatchgo {} {
1877    global patchtop
1878
1879    set oldid [$patchtop.fromsha1 get]
1880    set newid [$patchtop.tosha1 get]
1881    set fname [$patchtop.fname get]
1882    if {[catch {exec git-diff-tree -p $oldid $newid >$fname &} err]} {
1883        error_popup "Error creating patch: $err"
1884    }
1885    catch {destroy $patchtop}
1886    unset patchtop
1887}
1888
1889proc mkpatchcan {} {
1890    global patchtop
1891
1892    catch {destroy $patchtop}
1893    unset patchtop
1894}
1895
1896proc mktag {} {
1897    global rowmenuid mktagtop commitinfo
1898
1899    set top .maketag
1900    set mktagtop $top
1901    catch {destroy $top}
1902    toplevel $top
1903    label $top.title -text "Create tag"
1904    grid $top.title -
1905    label $top.id -text "ID:"
1906    entry $top.sha1 -width 40
1907    $top.sha1 insert 0 $rowmenuid
1908    $top.sha1 conf -state readonly
1909    grid $top.id $top.sha1 -sticky w
1910    entry $top.head -width 40
1911    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
1912    $top.head conf -state readonly
1913    grid x $top.head -sticky w
1914    label $top.tlab -text "Tag name:"
1915    entry $top.tag -width 40
1916    grid $top.tlab $top.tag -sticky w
1917    frame $top.buts
1918    button $top.buts.gen -text "Create" -command mktaggo
1919    button $top.buts.can -text "Cancel" -command mktagcan
1920    grid $top.buts.gen $top.buts.can
1921    grid columnconfigure $top.buts 0 -weight 1 -uniform a
1922    grid columnconfigure $top.buts 1 -weight 1 -uniform a
1923    grid $top.buts - -pady 10 -sticky ew
1924    focus $top.tag
1925}
1926
1927proc domktag {} {
1928    global mktagtop env tagids idtags
1929    global idpos idline linehtag canv selectedline
1930
1931    set id [$mktagtop.sha1 get]
1932    set tag [$mktagtop.tag get]
1933    if {$tag == {}} {
1934        error_popup "No tag name specified"
1935        return
1936    }
1937    if {[info exists tagids($tag)]} {
1938        error_popup "Tag \"$tag\" already exists"
1939        return
1940    }
1941    if {[catch {
1942        set dir ".git"
1943        if {[info exists env(GIT_DIR)]} {
1944            set dir $env(GIT_DIR)
1945        }
1946        set fname [file join $dir "refs/tags" $tag]
1947        set f [open $fname w]
1948        puts $f $id
1949        close $f
1950    } err]} {
1951        error_popup "Error creating tag: $err"
1952        return
1953    }
1954
1955    set tagids($tag) $id
1956    lappend idtags($id) $tag
1957    $canv delete tag.$id
1958    set xt [eval drawtags $id $idpos($id)]
1959    $canv coords $linehtag($idline($id)) $xt [lindex $idpos($id) 2]
1960    if {[info exists selectedline] && $selectedline == $idline($id)} {
1961        selectline $selectedline
1962    }
1963}
1964
1965proc mktagcan {} {
1966    global mktagtop
1967
1968    catch {destroy $mktagtop}
1969    unset mktagtop
1970}
1971
1972proc mktaggo {} {
1973    domktag
1974    mktagcan
1975}
1976
1977proc doquit {} {
1978    global stopped
1979    set stopped 100
1980    destroy .
1981}
1982
1983# defaults...
1984set datemode 0
1985set boldnames 0
1986set diffopts "-U 5 -p"
1987
1988set mainfont {Helvetica 9}
1989set textfont {Courier 9}
1990
1991set colors {green red blue magenta darkgrey brown orange}
1992
1993catch {source ~/.gitk}
1994
1995set namefont $mainfont
1996if {$boldnames} {
1997    lappend namefont bold
1998}
1999
2000set revtreeargs {}
2001foreach arg $argv {
2002    switch -regexp -- $arg {
2003        "^$" { }
2004        "^-b" { set boldnames 1 }
2005        "^-d" { set datemode 1 }
2006        default {
2007            lappend revtreeargs $arg
2008        }
2009    }
2010}
2011
2012set stopped 0
2013set redisplaying 0
2014set stuffsaved 0
2015set patchnum 0
2016setcoords
2017makewindow
2018readrefs
2019getcommits $revtreeargs