gitkon commit Handle the rename cases reported by git-diff-tree -C correctly. (7eab293)
   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        # set it blocking so we wait for the process to terminate
  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 findtypemenu findloc findstring fstring geometry
 274    global entries sha1entry sha1string sha1but
 275    global maincursor textcursor
 276    global rowctxmenu gaudydiff
 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    set findtypemenu [tk_optionMenu .ctop.top.bar.findtype \
 346                          findtype Exact IgnCase Regexp]
 347    set findloc "All fields"
 348    tk_optionMenu .ctop.top.bar.findloc findloc "All fields" Headline \
 349        Comments Author Committer Files Pickaxe
 350    pack .ctop.top.bar.findloc -side right
 351    pack .ctop.top.bar.findtype -side right
 352    # for making sure type==Exact whenever loc==Pickaxe
 353    trace add variable findloc write findlocchange
 354
 355    panedwindow .ctop.cdet -orient horizontal
 356    .ctop add .ctop.cdet
 357    frame .ctop.cdet.left
 358    set ctext .ctop.cdet.left.ctext
 359    text $ctext -bg white -state disabled -font $textfont \
 360        -width $geometry(ctextw) -height $geometry(ctexth) \
 361        -yscrollcommand ".ctop.cdet.left.sb set"
 362    scrollbar .ctop.cdet.left.sb -command "$ctext yview"
 363    pack .ctop.cdet.left.sb -side right -fill y
 364    pack $ctext -side left -fill both -expand 1
 365    .ctop.cdet add .ctop.cdet.left
 366
 367    $ctext tag conf filesep -font [concat $textfont bold] -back "#aaaaaa"
 368    if {$gaudydiff} {
 369        $ctext tag conf hunksep -back blue -fore white
 370        $ctext tag conf d0 -back "#ff8080"
 371        $ctext tag conf d1 -back green
 372    } else {
 373        $ctext tag conf hunksep -fore blue
 374        $ctext tag conf d0 -fore red
 375        $ctext tag conf d1 -fore "#00a000"
 376        $ctext tag conf found -back yellow
 377    }
 378
 379    frame .ctop.cdet.right
 380    set cflist .ctop.cdet.right.cfiles
 381    listbox $cflist -bg white -selectmode extended -width $geometry(cflistw) \
 382        -yscrollcommand ".ctop.cdet.right.sb set"
 383    scrollbar .ctop.cdet.right.sb -command "$cflist yview"
 384    pack .ctop.cdet.right.sb -side right -fill y
 385    pack $cflist -side left -fill both -expand 1
 386    .ctop.cdet add .ctop.cdet.right
 387    bind .ctop.cdet <Configure> {resizecdetpanes %W %w}
 388
 389    pack .ctop -side top -fill both -expand 1
 390
 391    bindall <1> {selcanvline %W %x %y}
 392    #bindall <B1-Motion> {selcanvline %W %x %y}
 393    bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
 394    bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
 395    bindall <2> "allcanvs scan mark 0 %y"
 396    bindall <B2-Motion> "allcanvs scan dragto 0 %y"
 397    bind . <Key-Up> "selnextline -1"
 398    bind . <Key-Down> "selnextline 1"
 399    bind . <Key-Prior> "allcanvs yview scroll -1 pages"
 400    bind . <Key-Next> "allcanvs yview scroll 1 pages"
 401    bindkey <Key-Delete> "$ctext yview scroll -1 pages"
 402    bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
 403    bindkey <Key-space> "$ctext yview scroll 1 pages"
 404    bindkey p "selnextline -1"
 405    bindkey n "selnextline 1"
 406    bindkey b "$ctext yview scroll -1 pages"
 407    bindkey d "$ctext yview scroll 18 units"
 408    bindkey u "$ctext yview scroll -18 units"
 409    bindkey / {findnext 1}
 410    bindkey <Key-Return> {findnext 0}
 411    bindkey ? findprev
 412    bindkey f nextfile
 413    bind . <Control-q> doquit
 414    bind . <Control-f> dofind
 415    bind . <Control-g> {findnext 0}
 416    bind . <Control-r> findprev
 417    bind . <Control-equal> {incrfont 1}
 418    bind . <Control-KP_Add> {incrfont 1}
 419    bind . <Control-minus> {incrfont -1}
 420    bind . <Control-KP_Subtract> {incrfont -1}
 421    bind $cflist <<ListboxSelect>> listboxsel
 422    bind . <Destroy> {savestuff %W}
 423    bind . <Button-1> "click %W"
 424    bind $fstring <Key-Return> dofind
 425    bind $sha1entry <Key-Return> gotocommit
 426    bind $sha1entry <<PasteSelection>> clearsha1
 427
 428    set maincursor [. cget -cursor]
 429    set textcursor [$ctext cget -cursor]
 430
 431    set rowctxmenu .rowctxmenu
 432    menu $rowctxmenu -tearoff 0
 433    $rowctxmenu add command -label "Diff this -> selected" \
 434        -command {diffvssel 0}
 435    $rowctxmenu add command -label "Diff selected -> this" \
 436        -command {diffvssel 1}
 437    $rowctxmenu add command -label "Make patch" -command mkpatch
 438    $rowctxmenu add command -label "Create tag" -command mktag
 439    $rowctxmenu add command -label "Write commit to file" -command writecommit
 440}
 441
 442# when we make a key binding for the toplevel, make sure
 443# it doesn't get triggered when that key is pressed in the
 444# find string entry widget.
 445proc bindkey {ev script} {
 446    global entries
 447    bind . $ev $script
 448    set escript [bind Entry $ev]
 449    if {$escript == {}} {
 450        set escript [bind Entry <Key>]
 451    }
 452    foreach e $entries {
 453        bind $e $ev "$escript; break"
 454    }
 455}
 456
 457# set the focus back to the toplevel for any click outside
 458# the entry widgets
 459proc click {w} {
 460    global entries
 461    foreach e $entries {
 462        if {$w == $e} return
 463    }
 464    focus .
 465}
 466
 467proc savestuff {w} {
 468    global canv canv2 canv3 ctext cflist mainfont textfont
 469    global stuffsaved
 470    if {$stuffsaved} return
 471    if {![winfo viewable .]} return
 472    catch {
 473        set f [open "~/.gitk-new" w]
 474        puts $f [list set mainfont $mainfont]
 475        puts $f [list set textfont $textfont]
 476        puts $f [list set findmergefiles $findmergefiles]
 477        puts $f [list set gaudydiff $gaudydiff]
 478        puts $f "set geometry(width) [winfo width .ctop]"
 479        puts $f "set geometry(height) [winfo height .ctop]"
 480        puts $f "set geometry(canv1) [expr [winfo width $canv]-2]"
 481        puts $f "set geometry(canv2) [expr [winfo width $canv2]-2]"
 482        puts $f "set geometry(canv3) [expr [winfo width $canv3]-2]"
 483        puts $f "set geometry(canvh) [expr [winfo height $canv]-2]"
 484        set wid [expr {([winfo width $ctext] - 8) \
 485                           / [font measure $textfont "0"]}]
 486        puts $f "set geometry(ctextw) $wid"
 487        set wid [expr {([winfo width $cflist] - 11) \
 488                           / [font measure [$cflist cget -font] "0"]}]
 489        puts $f "set geometry(cflistw) $wid"
 490        close $f
 491        file rename -force "~/.gitk-new" "~/.gitk"
 492    }
 493    set stuffsaved 1
 494}
 495
 496proc resizeclistpanes {win w} {
 497    global oldwidth
 498    if [info exists oldwidth($win)] {
 499        set s0 [$win sash coord 0]
 500        set s1 [$win sash coord 1]
 501        if {$w < 60} {
 502            set sash0 [expr {int($w/2 - 2)}]
 503            set sash1 [expr {int($w*5/6 - 2)}]
 504        } else {
 505            set factor [expr {1.0 * $w / $oldwidth($win)}]
 506            set sash0 [expr {int($factor * [lindex $s0 0])}]
 507            set sash1 [expr {int($factor * [lindex $s1 0])}]
 508            if {$sash0 < 30} {
 509                set sash0 30
 510            }
 511            if {$sash1 < $sash0 + 20} {
 512                set sash1 [expr $sash0 + 20]
 513            }
 514            if {$sash1 > $w - 10} {
 515                set sash1 [expr $w - 10]
 516                if {$sash0 > $sash1 - 20} {
 517                    set sash0 [expr $sash1 - 20]
 518                }
 519            }
 520        }
 521        $win sash place 0 $sash0 [lindex $s0 1]
 522        $win sash place 1 $sash1 [lindex $s1 1]
 523    }
 524    set oldwidth($win) $w
 525}
 526
 527proc resizecdetpanes {win w} {
 528    global oldwidth
 529    if [info exists oldwidth($win)] {
 530        set s0 [$win sash coord 0]
 531        if {$w < 60} {
 532            set sash0 [expr {int($w*3/4 - 2)}]
 533        } else {
 534            set factor [expr {1.0 * $w / $oldwidth($win)}]
 535            set sash0 [expr {int($factor * [lindex $s0 0])}]
 536            if {$sash0 < 45} {
 537                set sash0 45
 538            }
 539            if {$sash0 > $w - 15} {
 540                set sash0 [expr $w - 15]
 541            }
 542        }
 543        $win sash place 0 $sash0 [lindex $s0 1]
 544    }
 545    set oldwidth($win) $w
 546}
 547
 548proc allcanvs args {
 549    global canv canv2 canv3
 550    eval $canv $args
 551    eval $canv2 $args
 552    eval $canv3 $args
 553}
 554
 555proc bindall {event action} {
 556    global canv canv2 canv3
 557    bind $canv $event $action
 558    bind $canv2 $event $action
 559    bind $canv3 $event $action
 560}
 561
 562proc about {} {
 563    set w .about
 564    if {[winfo exists $w]} {
 565        raise $w
 566        return
 567    }
 568    toplevel $w
 569    wm title $w "About gitk"
 570    message $w.m -text {
 571Gitk version 1.2
 572
 573Copyright © 2005 Paul Mackerras
 574
 575Use and redistribute under the terms of the GNU General Public License} \
 576            -justify center -aspect 400
 577    pack $w.m -side top -fill x -padx 20 -pady 20
 578    button $w.ok -text Close -command "destroy $w"
 579    pack $w.ok -side bottom
 580}
 581
 582proc assigncolor {id} {
 583    global commitinfo colormap commcolors colors nextcolor
 584    global parents nparents children nchildren
 585    global cornercrossings crossings
 586
 587    if [info exists colormap($id)] return
 588    set ncolors [llength $colors]
 589    if {$nparents($id) <= 1 && $nchildren($id) == 1} {
 590        set child [lindex $children($id) 0]
 591        if {[info exists colormap($child)]
 592            && $nparents($child) == 1} {
 593            set colormap($id) $colormap($child)
 594            return
 595        }
 596    }
 597    set badcolors {}
 598    if {[info exists cornercrossings($id)]} {
 599        foreach x $cornercrossings($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 {}
 607        }
 608    }
 609    set origbad $badcolors
 610    if {[llength $badcolors] < $ncolors - 1} {
 611        if {[info exists crossings($id)]} {
 612            foreach x $crossings($id) {
 613                if {[info exists colormap($x)]
 614                    && [lsearch -exact $badcolors $colormap($x)] < 0} {
 615                    lappend badcolors $colormap($x)
 616                }
 617            }
 618            if {[llength $badcolors] >= $ncolors} {
 619                set badcolors $origbad
 620            }
 621        }
 622        set origbad $badcolors
 623    }
 624    if {[llength $badcolors] < $ncolors - 1} {
 625        foreach child $children($id) {
 626            if {[info exists colormap($child)]
 627                && [lsearch -exact $badcolors $colormap($child)] < 0} {
 628                lappend badcolors $colormap($child)
 629            }
 630            if {[info exists parents($child)]} {
 631                foreach p $parents($child) {
 632                    if {[info exists colormap($p)]
 633                        && [lsearch -exact $badcolors $colormap($p)] < 0} {
 634                        lappend badcolors $colormap($p)
 635                    }
 636                }
 637            }
 638        }
 639        if {[llength $badcolors] >= $ncolors} {
 640            set badcolors $origbad
 641        }
 642    }
 643    for {set i 0} {$i <= $ncolors} {incr i} {
 644        set c [lindex $colors $nextcolor]
 645        if {[incr nextcolor] >= $ncolors} {
 646            set nextcolor 0
 647        }
 648        if {[lsearch -exact $badcolors $c]} break
 649    }
 650    set colormap($id) $c
 651}
 652
 653proc initgraph {} {
 654    global canvy canvy0 lineno numcommits lthickness nextcolor linespc
 655    global mainline sidelines
 656    global nchildren ncleft
 657
 658    allcanvs delete all
 659    set nextcolor 0
 660    set canvy $canvy0
 661    set lineno -1
 662    set numcommits 0
 663    set lthickness [expr {int($linespc / 9) + 1}]
 664    catch {unset mainline}
 665    catch {unset sidelines}
 666    foreach id [array names nchildren] {
 667        set ncleft($id) $nchildren($id)
 668    }
 669}
 670
 671proc bindline {t id} {
 672    global canv
 673
 674    $canv bind $t <Enter> "lineenter %x %y $id"
 675    $canv bind $t <Motion> "linemotion %x %y $id"
 676    $canv bind $t <Leave> "lineleave $id"
 677    $canv bind $t <Button-1> "lineclick %x %y $id"
 678}
 679
 680proc drawcommitline {level} {
 681    global parents children nparents nchildren todo
 682    global canv canv2 canv3 mainfont namefont canvx0 canvy linespc
 683    global lineid linehtag linentag linedtag commitinfo
 684    global colormap numcommits currentparents dupparents
 685    global oldlevel oldnlines oldtodo
 686    global idtags idline idheads
 687    global lineno lthickness mainline sidelines
 688    global commitlisted rowtextx idpos
 689
 690    incr numcommits
 691    incr lineno
 692    set id [lindex $todo $level]
 693    set lineid($lineno) $id
 694    set idline($id) $lineno
 695    set ofill [expr {[info exists commitlisted($id)]? "blue": "white"}]
 696    if {![info exists commitinfo($id)]} {
 697        readcommit $id
 698        if {![info exists commitinfo($id)]} {
 699            set commitinfo($id) {"No commit information available"}
 700            set nparents($id) 0
 701        }
 702    }
 703    assigncolor $id
 704    set currentparents {}
 705    set dupparents {}
 706    if {[info exists commitlisted($id)] && [info exists parents($id)]} {
 707        foreach p $parents($id) {
 708            if {[lsearch -exact $currentparents $p] < 0} {
 709                lappend currentparents $p
 710            } else {
 711                # remember that this parent was listed twice
 712                lappend dupparents $p
 713            }
 714        }
 715    }
 716    set x [expr $canvx0 + $level * $linespc]
 717    set y1 $canvy
 718    set canvy [expr $canvy + $linespc]
 719    allcanvs conf -scrollregion \
 720        [list 0 0 0 [expr $y1 + 0.5 * $linespc + 2]]
 721    if {[info exists mainline($id)]} {
 722        lappend mainline($id) $x $y1
 723        set t [$canv create line $mainline($id) \
 724                   -width $lthickness -fill $colormap($id)]
 725        $canv lower $t
 726        bindline $t $id
 727    }
 728    if {[info exists sidelines($id)]} {
 729        foreach ls $sidelines($id) {
 730            set coords [lindex $ls 0]
 731            set thick [lindex $ls 1]
 732            set t [$canv create line $coords -fill $colormap($id) \
 733                       -width [expr {$thick * $lthickness}]]
 734            $canv lower $t
 735            bindline $t $id
 736        }
 737    }
 738    set orad [expr {$linespc / 3}]
 739    set t [$canv create oval [expr $x - $orad] [expr $y1 - $orad] \
 740               [expr $x + $orad - 1] [expr $y1 + $orad - 1] \
 741               -fill $ofill -outline black -width 1]
 742    $canv raise $t
 743    $canv bind $t <1> {selcanvline {} %x %y}
 744    set xt [expr $canvx0 + [llength $todo] * $linespc]
 745    if {[llength $currentparents] > 2} {
 746        set xt [expr {$xt + ([llength $currentparents] - 2) * $linespc}]
 747    }
 748    set rowtextx($lineno) $xt
 749    set idpos($id) [list $x $xt $y1]
 750    if {[info exists idtags($id)] || [info exists idheads($id)]} {
 751        set xt [drawtags $id $x $xt $y1]
 752    }
 753    set headline [lindex $commitinfo($id) 0]
 754    set name [lindex $commitinfo($id) 1]
 755    set date [lindex $commitinfo($id) 2]
 756    set linehtag($lineno) [$canv create text $xt $y1 -anchor w \
 757                               -text $headline -font $mainfont ]
 758    $canv bind $linehtag($lineno) <Button-3> "rowmenu %X %Y $id"
 759    set linentag($lineno) [$canv2 create text 3 $y1 -anchor w \
 760                               -text $name -font $namefont]
 761    set linedtag($lineno) [$canv3 create text 3 $y1 -anchor w \
 762                               -text $date -font $mainfont]
 763}
 764
 765proc drawtags {id x xt y1} {
 766    global idtags idheads
 767    global linespc lthickness
 768    global canv mainfont
 769
 770    set marks {}
 771    set ntags 0
 772    if {[info exists idtags($id)]} {
 773        set marks $idtags($id)
 774        set ntags [llength $marks]
 775    }
 776    if {[info exists idheads($id)]} {
 777        set marks [concat $marks $idheads($id)]
 778    }
 779    if {$marks eq {}} {
 780        return $xt
 781    }
 782
 783    set delta [expr {int(0.5 * ($linespc - $lthickness))}]
 784    set yt [expr $y1 - 0.5 * $linespc]
 785    set yb [expr $yt + $linespc - 1]
 786    set xvals {}
 787    set wvals {}
 788    foreach tag $marks {
 789        set wid [font measure $mainfont $tag]
 790        lappend xvals $xt
 791        lappend wvals $wid
 792        set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
 793    }
 794    set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
 795               -width $lthickness -fill black -tags tag.$id]
 796    $canv lower $t
 797    foreach tag $marks x $xvals wid $wvals {
 798        set xl [expr $x + $delta]
 799        set xr [expr $x + $delta + $wid + $lthickness]
 800        if {[incr ntags -1] >= 0} {
 801            # draw a tag
 802            $canv create polygon $x [expr $yt + $delta] $xl $yt\
 803                $xr $yt $xr $yb $xl $yb $x [expr $yb - $delta] \
 804                -width 1 -outline black -fill yellow -tags tag.$id
 805        } else {
 806            # draw a head
 807            set xl [expr $xl - $delta/2]
 808            $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
 809                -width 1 -outline black -fill green -tags tag.$id
 810        }
 811        $canv create text $xl $y1 -anchor w -text $tag \
 812            -font $mainfont -tags tag.$id
 813    }
 814    return $xt
 815}
 816
 817proc updatetodo {level noshortcut} {
 818    global currentparents ncleft todo
 819    global mainline oldlevel oldtodo oldnlines
 820    global canvx0 canvy linespc mainline
 821    global commitinfo
 822
 823    set oldlevel $level
 824    set oldtodo $todo
 825    set oldnlines [llength $todo]
 826    if {!$noshortcut && [llength $currentparents] == 1} {
 827        set p [lindex $currentparents 0]
 828        if {$ncleft($p) == 1 && [lsearch -exact $todo $p] < 0} {
 829            set ncleft($p) 0
 830            set x [expr $canvx0 + $level * $linespc]
 831            set y [expr $canvy - $linespc]
 832            set mainline($p) [list $x $y]
 833            set todo [lreplace $todo $level $level $p]
 834            return 0
 835        }
 836    }
 837
 838    set todo [lreplace $todo $level $level]
 839    set i $level
 840    foreach p $currentparents {
 841        incr ncleft($p) -1
 842        set k [lsearch -exact $todo $p]
 843        if {$k < 0} {
 844            set todo [linsert $todo $i $p]
 845            incr i
 846        }
 847    }
 848    return 1
 849}
 850
 851proc notecrossings {id lo hi corner} {
 852    global oldtodo crossings cornercrossings
 853
 854    for {set i $lo} {[incr i] < $hi} {} {
 855        set p [lindex $oldtodo $i]
 856        if {$p == {}} continue
 857        if {$i == $corner} {
 858            if {![info exists cornercrossings($id)]
 859                || [lsearch -exact $cornercrossings($id) $p] < 0} {
 860                lappend cornercrossings($id) $p
 861            }
 862            if {![info exists cornercrossings($p)]
 863                || [lsearch -exact $cornercrossings($p) $id] < 0} {
 864                lappend cornercrossings($p) $id
 865            }
 866        } else {
 867            if {![info exists crossings($id)]
 868                || [lsearch -exact $crossings($id) $p] < 0} {
 869                lappend crossings($id) $p
 870            }
 871            if {![info exists crossings($p)]
 872                || [lsearch -exact $crossings($p) $id] < 0} {
 873                lappend crossings($p) $id
 874            }
 875        }
 876    }
 877}
 878
 879proc drawslants {} {
 880    global canv mainline sidelines canvx0 canvy linespc
 881    global oldlevel oldtodo todo currentparents dupparents
 882    global lthickness linespc canvy colormap
 883
 884    set y1 [expr $canvy - $linespc]
 885    set y2 $canvy
 886    set i -1
 887    foreach id $oldtodo {
 888        incr i
 889        if {$id == {}} continue
 890        set xi [expr {$canvx0 + $i * $linespc}]
 891        if {$i == $oldlevel} {
 892            foreach p $currentparents {
 893                set j [lsearch -exact $todo $p]
 894                set coords [list $xi $y1]
 895                set xj [expr {$canvx0 + $j * $linespc}]
 896                if {$j < $i - 1} {
 897                    lappend coords [expr $xj + $linespc] $y1
 898                    notecrossings $p $j $i [expr {$j + 1}]
 899                } elseif {$j > $i + 1} {
 900                    lappend coords [expr $xj - $linespc] $y1
 901                    notecrossings $p $i $j [expr {$j - 1}]
 902                }
 903                if {[lsearch -exact $dupparents $p] >= 0} {
 904                    # draw a double-width line to indicate the doubled parent
 905                    lappend coords $xj $y2
 906                    lappend sidelines($p) [list $coords 2]
 907                    if {![info exists mainline($p)]} {
 908                        set mainline($p) [list $xj $y2]
 909                    }
 910                } else {
 911                    # normal case, no parent duplicated
 912                    if {![info exists mainline($p)]} {
 913                        if {$i != $j} {
 914                            lappend coords $xj $y2
 915                        }
 916                        set mainline($p) $coords
 917                    } else {
 918                        lappend coords $xj $y2
 919                        lappend sidelines($p) [list $coords 1]
 920                    }
 921                }
 922            }
 923        } elseif {[lindex $todo $i] != $id} {
 924            set j [lsearch -exact $todo $id]
 925            set xj [expr {$canvx0 + $j * $linespc}]
 926            lappend mainline($id) $xi $y1 $xj $y2
 927        }
 928    }
 929}
 930
 931proc decidenext {{noread 0}} {
 932    global parents children nchildren ncleft todo
 933    global canv canv2 canv3 mainfont namefont canvx0 canvy linespc
 934    global datemode cdate
 935    global commitinfo
 936    global currentparents oldlevel oldnlines oldtodo
 937    global lineno lthickness
 938
 939    # remove the null entry if present
 940    set nullentry [lsearch -exact $todo {}]
 941    if {$nullentry >= 0} {
 942        set todo [lreplace $todo $nullentry $nullentry]
 943    }
 944
 945    # choose which one to do next time around
 946    set todol [llength $todo]
 947    set level -1
 948    set latest {}
 949    for {set k $todol} {[incr k -1] >= 0} {} {
 950        set p [lindex $todo $k]
 951        if {$ncleft($p) == 0} {
 952            if {$datemode} {
 953                if {![info exists commitinfo($p)]} {
 954                    if {$noread} {
 955                        return {}
 956                    }
 957                    readcommit $p
 958                }
 959                if {$latest == {} || $cdate($p) > $latest} {
 960                    set level $k
 961                    set latest $cdate($p)
 962                }
 963            } else {
 964                set level $k
 965                break
 966            }
 967        }
 968    }
 969    if {$level < 0} {
 970        if {$todo != {}} {
 971            puts "ERROR: none of the pending commits can be done yet:"
 972            foreach p $todo {
 973                puts "  $p ($ncleft($p))"
 974            }
 975        }
 976        return -1
 977    }
 978
 979    # If we are reducing, put in a null entry
 980    if {$todol < $oldnlines} {
 981        if {$nullentry >= 0} {
 982            set i $nullentry
 983            while {$i < $todol
 984                   && [lindex $oldtodo $i] == [lindex $todo $i]} {
 985                incr i
 986            }
 987        } else {
 988            set i $oldlevel
 989            if {$level >= $i} {
 990                incr i
 991            }
 992        }
 993        if {$i < $todol} {
 994            set todo [linsert $todo $i {}]
 995            if {$level >= $i} {
 996                incr level
 997            }
 998        }
 999    }
1000    return $level
1001}
1002
1003proc drawcommit {id} {
1004    global phase todo nchildren datemode nextupdate
1005    global startcommits
1006
1007    if {$phase != "incrdraw"} {
1008        set phase incrdraw
1009        set todo $id
1010        set startcommits $id
1011        initgraph
1012        drawcommitline 0
1013        updatetodo 0 $datemode
1014    } else {
1015        if {$nchildren($id) == 0} {
1016            lappend todo $id
1017            lappend startcommits $id
1018        }
1019        set level [decidenext 1]
1020        if {$level == {} || $id != [lindex $todo $level]} {
1021            return
1022        }
1023        while 1 {
1024            drawslants
1025            drawcommitline $level
1026            if {[updatetodo $level $datemode]} {
1027                set level [decidenext 1]
1028                if {$level == {}} break
1029            }
1030            set id [lindex $todo $level]
1031            if {![info exists commitlisted($id)]} {
1032                break
1033            }
1034            if {[clock clicks -milliseconds] >= $nextupdate} {
1035                doupdate
1036                if {$stopped} break
1037            }
1038        }
1039    }
1040}
1041
1042proc finishcommits {} {
1043    global phase
1044    global startcommits
1045    global canv mainfont ctext maincursor textcursor
1046
1047    if {$phase != "incrdraw"} {
1048        $canv delete all
1049        $canv create text 3 3 -anchor nw -text "No commits selected" \
1050            -font $mainfont -tags textitems
1051        set phase {}
1052    } else {
1053        drawslants
1054        set level [decidenext]
1055        drawrest $level [llength $startcommits]
1056    }
1057    . config -cursor $maincursor
1058    $ctext config -cursor $textcursor
1059}
1060
1061proc drawgraph {} {
1062    global nextupdate startmsecs startcommits todo
1063
1064    if {$startcommits == {}} return
1065    set startmsecs [clock clicks -milliseconds]
1066    set nextupdate [expr $startmsecs + 100]
1067    initgraph
1068    set todo [lindex $startcommits 0]
1069    drawrest 0 1
1070}
1071
1072proc drawrest {level startix} {
1073    global phase stopped redisplaying selectedline
1074    global datemode currentparents todo
1075    global numcommits
1076    global nextupdate startmsecs startcommits idline
1077
1078    if {$level >= 0} {
1079        set phase drawgraph
1080        set startid [lindex $startcommits $startix]
1081        set startline -1
1082        if {$startid != {}} {
1083            set startline $idline($startid)
1084        }
1085        while 1 {
1086            if {$stopped} break
1087            drawcommitline $level
1088            set hard [updatetodo $level $datemode]
1089            if {$numcommits == $startline} {
1090                lappend todo $startid
1091                set hard 1
1092                incr startix
1093                set startid [lindex $startcommits $startix]
1094                set startline -1
1095                if {$startid != {}} {
1096                    set startline $idline($startid)
1097                }
1098            }
1099            if {$hard} {
1100                set level [decidenext]
1101                if {$level < 0} break
1102                drawslants
1103            }
1104            if {[clock clicks -milliseconds] >= $nextupdate} {
1105                update
1106                incr nextupdate 100
1107            }
1108        }
1109    }
1110    set phase {}
1111    set drawmsecs [expr [clock clicks -milliseconds] - $startmsecs]
1112    #puts "overall $drawmsecs ms for $numcommits commits"
1113    if {$redisplaying} {
1114        if {$stopped == 0 && [info exists selectedline]} {
1115            selectline $selectedline
1116        }
1117        if {$stopped == 1} {
1118            set stopped 0
1119            after idle drawgraph
1120        } else {
1121            set redisplaying 0
1122        }
1123    }
1124}
1125
1126proc findmatches {f} {
1127    global findtype foundstring foundstrlen
1128    if {$findtype == "Regexp"} {
1129        set matches [regexp -indices -all -inline $foundstring $f]
1130    } else {
1131        if {$findtype == "IgnCase"} {
1132            set str [string tolower $f]
1133        } else {
1134            set str $f
1135        }
1136        set matches {}
1137        set i 0
1138        while {[set j [string first $foundstring $str $i]] >= 0} {
1139            lappend matches [list $j [expr $j+$foundstrlen-1]]
1140            set i [expr $j + $foundstrlen]
1141        }
1142    }
1143    return $matches
1144}
1145
1146proc dofind {} {
1147    global findtype findloc findstring markedmatches commitinfo
1148    global numcommits lineid linehtag linentag linedtag
1149    global mainfont namefont canv canv2 canv3 selectedline
1150    global matchinglines foundstring foundstrlen
1151
1152    stopfindproc
1153    unmarkmatches
1154    focus .
1155    set matchinglines {}
1156    if {$findloc == "Pickaxe"} {
1157        findpatches
1158        return
1159    }
1160    if {$findtype == "IgnCase"} {
1161        set foundstring [string tolower $findstring]
1162    } else {
1163        set foundstring $findstring
1164    }
1165    set foundstrlen [string length $findstring]
1166    if {$foundstrlen == 0} return
1167    if {$findloc == "Files"} {
1168        findfiles
1169        return
1170    }
1171    if {![info exists selectedline]} {
1172        set oldsel -1
1173    } else {
1174        set oldsel $selectedline
1175    }
1176    set didsel 0
1177    set fldtypes {Headline Author Date Committer CDate Comment}
1178    for {set l 0} {$l < $numcommits} {incr l} {
1179        set id $lineid($l)
1180        set info $commitinfo($id)
1181        set doesmatch 0
1182        foreach f $info ty $fldtypes {
1183            if {$findloc != "All fields" && $findloc != $ty} {
1184                continue
1185            }
1186            set matches [findmatches $f]
1187            if {$matches == {}} continue
1188            set doesmatch 1
1189            if {$ty == "Headline"} {
1190                markmatches $canv $l $f $linehtag($l) $matches $mainfont
1191            } elseif {$ty == "Author"} {
1192                markmatches $canv2 $l $f $linentag($l) $matches $namefont
1193            } elseif {$ty == "Date"} {
1194                markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
1195            }
1196        }
1197        if {$doesmatch} {
1198            lappend matchinglines $l
1199            if {!$didsel && $l > $oldsel} {
1200                findselectline $l
1201                set didsel 1
1202            }
1203        }
1204    }
1205    if {$matchinglines == {}} {
1206        bell
1207    } elseif {!$didsel} {
1208        findselectline [lindex $matchinglines 0]
1209    }
1210}
1211
1212proc findselectline {l} {
1213    global findloc commentend ctext
1214    selectline $l
1215    if {$findloc == "All fields" || $findloc == "Comments"} {
1216        # highlight the matches in the comments
1217        set f [$ctext get 1.0 $commentend]
1218        set matches [findmatches $f]
1219        foreach match $matches {
1220            set start [lindex $match 0]
1221            set end [expr [lindex $match 1] + 1]
1222            $ctext tag add found "1.0 + $start c" "1.0 + $end c"
1223        }
1224    }
1225}
1226
1227proc findnext {restart} {
1228    global matchinglines selectedline
1229    if {![info exists matchinglines]} {
1230        if {$restart} {
1231            dofind
1232        }
1233        return
1234    }
1235    if {![info exists selectedline]} return
1236    foreach l $matchinglines {
1237        if {$l > $selectedline} {
1238            findselectline $l
1239            return
1240        }
1241    }
1242    bell
1243}
1244
1245proc findprev {} {
1246    global matchinglines selectedline
1247    if {![info exists matchinglines]} {
1248        dofind
1249        return
1250    }
1251    if {![info exists selectedline]} return
1252    set prev {}
1253    foreach l $matchinglines {
1254        if {$l >= $selectedline} break
1255        set prev $l
1256    }
1257    if {$prev != {}} {
1258        findselectline $prev
1259    } else {
1260        bell
1261    }
1262}
1263
1264proc findlocchange {name ix op} {
1265    global findloc findtype findtypemenu
1266    if {$findloc == "Pickaxe"} {
1267        set findtype Exact
1268        set state disabled
1269    } else {
1270        set state normal
1271    }
1272    $findtypemenu entryconf 1 -state $state
1273    $findtypemenu entryconf 2 -state $state
1274}
1275
1276proc stopfindproc {{done 0}} {
1277    global findprocpid findprocfile findids
1278    global ctext findoldcursor phase maincursor textcursor
1279    global findinprogress
1280
1281    catch {unset findids}
1282    if {[info exists findprocpid]} {
1283        if {!$done} {
1284            catch {exec kill $findprocpid}
1285        }
1286        catch {close $findprocfile}
1287        unset findprocpid
1288    }
1289    if {[info exists findinprogress]} {
1290        unset findinprogress
1291        if {$phase != "incrdraw"} {
1292            . config -cursor $maincursor
1293            $ctext config -cursor $textcursor
1294        }
1295    }
1296}
1297
1298proc findpatches {} {
1299    global findstring selectedline numcommits
1300    global findprocpid findprocfile
1301    global finddidsel ctext lineid findinprogress
1302    global findinsertpos
1303
1304    if {$numcommits == 0} return
1305
1306    # make a list of all the ids to search, starting at the one
1307    # after the selected line (if any)
1308    if {[info exists selectedline]} {
1309        set l $selectedline
1310    } else {
1311        set l -1
1312    }
1313    set inputids {}
1314    for {set i 0} {$i < $numcommits} {incr i} {
1315        if {[incr l] >= $numcommits} {
1316            set l 0
1317        }
1318        append inputids $lineid($l) "\n"
1319    }
1320
1321    if {[catch {
1322        set f [open [list | git-diff-tree --stdin -s -r -S$findstring \
1323                         << $inputids] r]
1324    } err]} {
1325        error_popup "Error starting search process: $err"
1326        return
1327    }
1328
1329    set findinsertpos end
1330    set findprocfile $f
1331    set findprocpid [pid $f]
1332    fconfigure $f -blocking 0
1333    fileevent $f readable readfindproc
1334    set finddidsel 0
1335    . config -cursor watch
1336    $ctext config -cursor watch
1337    set findinprogress 1
1338}
1339
1340proc readfindproc {} {
1341    global findprocfile finddidsel
1342    global idline matchinglines findinsertpos
1343
1344    set n [gets $findprocfile line]
1345    if {$n < 0} {
1346        if {[eof $findprocfile]} {
1347            stopfindproc 1
1348            if {!$finddidsel} {
1349                bell
1350            }
1351        }
1352        return
1353    }
1354    if {![regexp {^[0-9a-f]{40}} $line id]} {
1355        error_popup "Can't parse git-diff-tree output: $line"
1356        stopfindproc
1357        return
1358    }
1359    if {![info exists idline($id)]} {
1360        puts stderr "spurious id: $id"
1361        return
1362    }
1363    set l $idline($id)
1364    insertmatch $l $id
1365}
1366
1367proc insertmatch {l id} {
1368    global matchinglines findinsertpos finddidsel
1369
1370    if {$findinsertpos == "end"} {
1371        if {$matchinglines != {} && $l < [lindex $matchinglines 0]} {
1372            set matchinglines [linsert $matchinglines 0 $l]
1373            set findinsertpos 1
1374        } else {
1375            lappend matchinglines $l
1376        }
1377    } else {
1378        set matchinglines [linsert $matchinglines $findinsertpos $l]
1379        incr findinsertpos
1380    }
1381    markheadline $l $id
1382    if {!$finddidsel} {
1383        findselectline $l
1384        set finddidsel 1
1385    }
1386}
1387
1388proc findfiles {} {
1389    global selectedline numcommits lineid ctext
1390    global ffileline finddidsel parents nparents
1391    global findinprogress findstartline findinsertpos
1392    global treediffs fdiffids fdiffsneeded fdiffpos
1393    global findmergefiles
1394
1395    if {$numcommits == 0} return
1396
1397    if {[info exists selectedline]} {
1398        set l [expr {$selectedline + 1}]
1399    } else {
1400        set l 0
1401    }
1402    set ffileline $l
1403    set findstartline $l
1404    set diffsneeded {}
1405    set fdiffsneeded {}
1406    while 1 {
1407        set id $lineid($l)
1408        if {$findmergefiles || $nparents($id) == 1} {
1409            foreach p $parents($id) {
1410                if {![info exists treediffs([list $id $p])]} {
1411                    append diffsneeded "$id $p\n"
1412                    lappend fdiffsneeded [list $id $p]
1413                }
1414            }
1415        }
1416        if {[incr l] >= $numcommits} {
1417            set l 0
1418        }
1419        if {$l == $findstartline} break
1420    }
1421
1422    # start off a git-diff-tree process if needed
1423    if {$diffsneeded ne {}} {
1424        if {[catch {
1425            set df [open [list | git-diff-tree -r --stdin << $diffsneeded] r]
1426        } err ]} {
1427            error_popup "Error starting search process: $err"
1428            return
1429        }
1430        catch {unset fdiffids}
1431        set fdiffpos 0
1432        fconfigure $df -blocking 0
1433        fileevent $df readable [list readfilediffs $df]
1434    }
1435
1436    set finddidsel 0
1437    set findinsertpos end
1438    set id $lineid($l)
1439    set p [lindex $parents($id) 0]
1440    . config -cursor watch
1441    $ctext config -cursor watch
1442    set findinprogress 1
1443    findcont [list $id $p]
1444    update
1445}
1446
1447proc readfilediffs {df} {
1448    global findids fdiffids fdiffs
1449
1450    set n [gets $df line]
1451    if {$n < 0} {
1452        if {[eof $df]} {
1453            donefilediff
1454            if {[catch {close $df} err]} {
1455                stopfindproc
1456                bell
1457                error_popup "Error in git-diff-tree: $err"
1458            } elseif {[info exists findids]} {
1459                set ids $findids
1460                stopfindproc
1461                bell
1462                error_popup "Couldn't find diffs for {$ids}"
1463            }
1464        }
1465        return
1466    }
1467    if {[regexp {^([0-9a-f]{40}) \(from ([0-9a-f]{40})\)} $line match id p]} {
1468        # start of a new string of diffs
1469        donefilediff
1470        set fdiffids [list $id $p]
1471        set fdiffs {}
1472    } elseif {[string match ":*" $line]} {
1473        lappend fdiffs [lindex $line 5]
1474    }
1475}
1476
1477proc donefilediff {} {
1478    global fdiffids fdiffs treediffs findids
1479    global fdiffsneeded fdiffpos
1480
1481    if {[info exists fdiffids]} {
1482        while {[lindex $fdiffsneeded $fdiffpos] ne $fdiffids
1483               && $fdiffpos < [llength $fdiffsneeded]} {
1484            # git-diff-tree doesn't output anything for a commit
1485            # which doesn't change anything
1486            set nullids [lindex $fdiffsneeded $fdiffpos]
1487            set treediffs($nullids) {}
1488            if {[info exists findids] && $nullids eq $findids} {
1489                unset findids
1490                findcont $nullids
1491            }
1492            incr fdiffpos
1493        }
1494        incr fdiffpos
1495
1496        if {![info exists treediffs($fdiffids)]} {
1497            set treediffs($fdiffids) $fdiffs
1498        }
1499        if {[info exists findids] && $fdiffids eq $findids} {
1500            unset findids
1501            findcont $fdiffids
1502        }
1503    }
1504}
1505
1506proc findcont {ids} {
1507    global findids treediffs parents nparents
1508    global ffileline findstartline finddidsel
1509    global lineid numcommits matchinglines findinprogress
1510    global findmergefiles
1511
1512    set id [lindex $ids 0]
1513    set p [lindex $ids 1]
1514    set pi [lsearch -exact $parents($id) $p]
1515    set l $ffileline
1516    while 1 {
1517        if {$findmergefiles || $nparents($id) == 1} {
1518            if {![info exists treediffs($ids)]} {
1519                set findids $ids
1520                set ffileline $l
1521                return
1522            }
1523            set doesmatch 0
1524            foreach f $treediffs($ids) {
1525                set x [findmatches $f]
1526                if {$x != {}} {
1527                    set doesmatch 1
1528                    break
1529                }
1530            }
1531            if {$doesmatch} {
1532                insertmatch $l $id
1533                set pi $nparents($id)
1534            }
1535        } else {
1536            set pi $nparents($id)
1537        }
1538        if {[incr pi] >= $nparents($id)} {
1539            set pi 0
1540            if {[incr l] >= $numcommits} {
1541                set l 0
1542            }
1543            if {$l == $findstartline} break
1544            set id $lineid($l)
1545        }
1546        set p [lindex $parents($id) $pi]
1547        set ids [list $id $p]
1548    }
1549    stopfindproc
1550    if {!$finddidsel} {
1551        bell
1552    }
1553}
1554
1555# mark a commit as matching by putting a yellow background
1556# behind the headline
1557proc markheadline {l id} {
1558    global canv mainfont linehtag commitinfo
1559
1560    set bbox [$canv bbox $linehtag($l)]
1561    set t [$canv create rect $bbox -outline {} -tags matches -fill yellow]
1562    $canv lower $t
1563}
1564
1565# mark the bits of a headline, author or date that match a find string
1566proc markmatches {canv l str tag matches font} {
1567    set bbox [$canv bbox $tag]
1568    set x0 [lindex $bbox 0]
1569    set y0 [lindex $bbox 1]
1570    set y1 [lindex $bbox 3]
1571    foreach match $matches {
1572        set start [lindex $match 0]
1573        set end [lindex $match 1]
1574        if {$start > $end} continue
1575        set xoff [font measure $font [string range $str 0 [expr $start-1]]]
1576        set xlen [font measure $font [string range $str 0 [expr $end]]]
1577        set t [$canv create rect [expr $x0+$xoff] $y0 [expr $x0+$xlen+2] $y1 \
1578                   -outline {} -tags matches -fill yellow]
1579        $canv lower $t
1580    }
1581}
1582
1583proc unmarkmatches {} {
1584    global matchinglines findids
1585    allcanvs delete matches
1586    catch {unset matchinglines}
1587    catch {unset findids}
1588}
1589
1590proc selcanvline {w x y} {
1591    global canv canvy0 ctext linespc selectedline
1592    global lineid linehtag linentag linedtag rowtextx
1593    set ymax [lindex [$canv cget -scrollregion] 3]
1594    if {$ymax == {}} return
1595    set yfrac [lindex [$canv yview] 0]
1596    set y [expr {$y + $yfrac * $ymax}]
1597    set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
1598    if {$l < 0} {
1599        set l 0
1600    }
1601    if {$w eq $canv} {
1602        if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
1603    }
1604    unmarkmatches
1605    selectline $l
1606}
1607
1608proc selectline {l} {
1609    global canv canv2 canv3 ctext commitinfo selectedline
1610    global lineid linehtag linentag linedtag
1611    global canvy0 linespc parents nparents
1612    global cflist currentid sha1entry
1613    global commentend idtags
1614    $canv delete hover
1615    if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
1616    $canv delete secsel
1617    set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
1618               -tags secsel -fill [$canv cget -selectbackground]]
1619    $canv lower $t
1620    $canv2 delete secsel
1621    set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
1622               -tags secsel -fill [$canv2 cget -selectbackground]]
1623    $canv2 lower $t
1624    $canv3 delete secsel
1625    set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
1626               -tags secsel -fill [$canv3 cget -selectbackground]]
1627    $canv3 lower $t
1628    set y [expr {$canvy0 + $l * $linespc}]
1629    set ymax [lindex [$canv cget -scrollregion] 3]
1630    set ytop [expr {$y - $linespc - 1}]
1631    set ybot [expr {$y + $linespc + 1}]
1632    set wnow [$canv yview]
1633    set wtop [expr [lindex $wnow 0] * $ymax]
1634    set wbot [expr [lindex $wnow 1] * $ymax]
1635    set wh [expr {$wbot - $wtop}]
1636    set newtop $wtop
1637    if {$ytop < $wtop} {
1638        if {$ybot < $wtop} {
1639            set newtop [expr {$y - $wh / 2.0}]
1640        } else {
1641            set newtop $ytop
1642            if {$newtop > $wtop - $linespc} {
1643                set newtop [expr {$wtop - $linespc}]
1644            }
1645        }
1646    } elseif {$ybot > $wbot} {
1647        if {$ytop > $wbot} {
1648            set newtop [expr {$y - $wh / 2.0}]
1649        } else {
1650            set newtop [expr {$ybot - $wh}]
1651            if {$newtop < $wtop + $linespc} {
1652                set newtop [expr {$wtop + $linespc}]
1653            }
1654        }
1655    }
1656    if {$newtop != $wtop} {
1657        if {$newtop < 0} {
1658            set newtop 0
1659        }
1660        allcanvs yview moveto [expr $newtop * 1.0 / $ymax]
1661    }
1662    set selectedline $l
1663
1664    set id $lineid($l)
1665    set currentid $id
1666    $sha1entry delete 0 end
1667    $sha1entry insert 0 $id
1668    $sha1entry selection from 0
1669    $sha1entry selection to end
1670
1671    $ctext conf -state normal
1672    $ctext delete 0.0 end
1673    $ctext mark set fmark.0 0.0
1674    $ctext mark gravity fmark.0 left
1675    set info $commitinfo($id)
1676    $ctext insert end "Author: [lindex $info 1]  [lindex $info 2]\n"
1677    $ctext insert end "Committer: [lindex $info 3]  [lindex $info 4]\n"
1678    if {[info exists idtags($id)]} {
1679        $ctext insert end "Tags:"
1680        foreach tag $idtags($id) {
1681            $ctext insert end " $tag"
1682        }
1683        $ctext insert end "\n"
1684    }
1685    $ctext insert end "\n"
1686    $ctext insert end [lindex $info 5]
1687    $ctext insert end "\n"
1688    $ctext tag delete Comments
1689    $ctext tag remove found 1.0 end
1690    $ctext conf -state disabled
1691    set commentend [$ctext index "end - 1c"]
1692
1693    $cflist delete 0 end
1694    $cflist insert end "Comments"
1695    if {$nparents($id) == 1} {
1696        startdiff [concat $id $parents($id)]
1697    } elseif {$nparents($id) > 1} {
1698        mergediff $id
1699    }
1700}
1701
1702proc selnextline {dir} {
1703    global selectedline
1704    if {![info exists selectedline]} return
1705    set l [expr $selectedline + $dir]
1706    unmarkmatches
1707    selectline $l
1708}
1709
1710proc mergediff {id} {
1711    global parents diffmergeid diffmergegca mergefilelist diffpindex
1712
1713    set diffmergeid $id
1714    set diffpindex -1
1715    set diffmergegca [findgca $parents($id)]
1716    if {[info exists mergefilelist($id)]} {
1717        showmergediff
1718    } else {
1719        contmergediff {}
1720    }
1721}
1722
1723proc findgca {ids} {
1724    set gca {}
1725    foreach id $ids {
1726        if {$gca eq {}} {
1727            set gca $id
1728        } else {
1729            if {[catch {
1730                set gca [exec git-merge-base $gca $id]
1731            } err]} {
1732                return {}
1733            }
1734        }
1735    }
1736    return $gca
1737}
1738
1739proc contmergediff {ids} {
1740    global diffmergeid diffpindex parents nparents diffmergegca
1741    global treediffs mergefilelist diffids
1742
1743    # diff the child against each of the parents, and diff
1744    # each of the parents against the GCA.
1745    while 1 {
1746        if {[lindex $ids 0] == $diffmergeid && $diffmergegca ne {}} {
1747            set ids [list [lindex $ids 1] $diffmergegca]
1748        } else {
1749            if {[incr diffpindex] >= $nparents($diffmergeid)} break
1750            set p [lindex $parents($diffmergeid) $diffpindex]
1751            set ids [list $diffmergeid $p]
1752        }
1753        if {![info exists treediffs($ids)]} {
1754            set diffids $ids
1755            gettreediffs $ids
1756            return
1757        }
1758    }
1759
1760    # If a file in some parent is different from the child and also
1761    # different from the GCA, then it's interesting.
1762    # If we don't have a GCA, then a file is interesting if it is
1763    # different from the child in all the parents.
1764    if {$diffmergegca ne {}} {
1765        set files {}
1766        foreach p $parents($diffmergeid) {
1767            set gcadiffs $treediffs([list $p $diffmergegca])
1768            foreach f $treediffs([list $diffmergeid $p]) {
1769                if {[lsearch -exact $files $f] < 0
1770                    && [lsearch -exact $gcadiffs $f] >= 0} {
1771                    lappend files $f
1772                }
1773            }
1774        }
1775        set files [lsort $files]
1776    } else {
1777        set p [lindex $parents($diffmergeid) 0]
1778        set files $treediffs([list $diffmergeid $p])
1779        for {set i 1} {$i < $nparents($diffmergeid) && $files ne {}} {incr i} {
1780            set p [lindex $parents($diffmergeid) $i]
1781            set df $treediffs([list $diffmergeid $p])
1782            set nf {}
1783            foreach f $files {
1784                if {[lsearch -exact $df $f] >= 0} {
1785                    lappend nf $f
1786                }
1787            }
1788            set files $nf
1789        }
1790    }
1791
1792    set mergefilelist($diffmergeid) $files
1793    showmergediff
1794}
1795
1796proc showmergediff {} {
1797    global cflist diffmergeid mergefilelist
1798
1799    set files $mergefilelist($diffmergeid)
1800    foreach f $files {
1801        $cflist insert end $f
1802    }
1803}
1804
1805proc startdiff {ids} {
1806    global treediffs diffids treepending diffmergeid
1807
1808    set diffids $ids
1809    catch {unset diffmergeid}
1810    if {![info exists treediffs($ids)]} {
1811        if {![info exists treepending]} {
1812            gettreediffs $ids
1813        }
1814    } else {
1815        addtocflist $ids
1816    }
1817}
1818
1819proc addtocflist {ids} {
1820    global treediffs cflist
1821    foreach f $treediffs($ids) {
1822        $cflist insert end $f
1823    }
1824    getblobdiffs $ids
1825}
1826
1827proc gettreediffs {ids} {
1828    global treediff parents treepending
1829    set treepending $ids
1830    set treediff {}
1831    set id [lindex $ids 0]
1832    set p [lindex $ids 1]
1833    if [catch {set gdtf [open "|git-diff-tree -r $p $id" r]}] return
1834    fconfigure $gdtf -blocking 0
1835    fileevent $gdtf readable [list gettreediffline $gdtf $ids]
1836}
1837
1838proc gettreediffline {gdtf ids} {
1839    global treediff treediffs treepending diffids diffmergeid
1840
1841    set n [gets $gdtf line]
1842    if {$n < 0} {
1843        if {![eof $gdtf]} return
1844        close $gdtf
1845        set treediffs($ids) $treediff
1846        unset treepending
1847        if {$ids != $diffids} {
1848            gettreediffs $diffids
1849        } else {
1850            if {[info exists diffmergeid]} {
1851                contmergediff $ids
1852            } else {
1853                addtocflist $ids
1854            }
1855        }
1856        return
1857    }
1858    set file [lindex $line 5]
1859    lappend treediff $file
1860}
1861
1862proc getblobdiffs {ids} {
1863    global diffopts blobdifffd diffids env curdifftag curtagstart
1864    global difffilestart nextupdate diffinhdr treediffs
1865
1866    set id [lindex $ids 0]
1867    set p [lindex $ids 1]
1868    set env(GIT_DIFF_OPTS) $diffopts
1869    set cmd [list | git-diff-tree -r -p -C $p $id]
1870    if {[catch {set bdf [open $cmd r]} err]} {
1871        puts "error getting diffs: $err"
1872        return
1873    }
1874    set diffinhdr 0
1875    fconfigure $bdf -blocking 0
1876    set blobdifffd($ids) $bdf
1877    set curdifftag Comments
1878    set curtagstart 0.0
1879    catch {unset difffilestart}
1880    fileevent $bdf readable [list getblobdiffline $bdf $diffids]
1881    set nextupdate [expr {[clock clicks -milliseconds] + 100}]
1882}
1883
1884proc getblobdiffline {bdf ids} {
1885    global diffids blobdifffd ctext curdifftag curtagstart
1886    global diffnexthead diffnextnote difffilestart
1887    global nextupdate diffinhdr treediffs
1888    global gaudydiff
1889
1890    set n [gets $bdf line]
1891    if {$n < 0} {
1892        if {[eof $bdf]} {
1893            close $bdf
1894            if {$ids == $diffids && $bdf == $blobdifffd($ids)} {
1895                $ctext tag add $curdifftag $curtagstart end
1896            }
1897        }
1898        return
1899    }
1900    if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
1901        return
1902    }
1903    $ctext conf -state normal
1904    if {[regexp {^diff --git a/(.*) b/(.*)} $line match fname newname]} {
1905        # start of a new file
1906        $ctext insert end "\n"
1907        $ctext tag add $curdifftag $curtagstart end
1908        set curtagstart [$ctext index "end - 1c"]
1909        set header $newname
1910        set here [$ctext index "end - 1c"]
1911        set i [lsearch -exact $treediffs($diffids) $fname]
1912        if {$i >= 0} {
1913            set difffilestart($i) $here
1914            incr i
1915            $ctext mark set fmark.$i $here
1916            $ctext mark gravity fmark.$i left
1917        }
1918        if {$newname != $fname} {
1919            set i [lsearch -exact $treediffs($diffids) $newname]
1920            if {$i >= 0} {
1921                set difffilestart($i) $here
1922                incr i
1923                $ctext mark set fmark.$i $here
1924                $ctext mark gravity fmark.$i left
1925            }
1926        }
1927        set curdifftag "f:$fname"
1928        $ctext tag delete $curdifftag
1929        set l [expr {(78 - [string length $header]) / 2}]
1930        set pad [string range "----------------------------------------" 1 $l]
1931        $ctext insert end "$pad $header $pad\n" filesep
1932        set diffinhdr 1
1933    } elseif {[regexp {^(---|\+\+\+)} $line]} {
1934        set diffinhdr 0
1935    } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
1936                   $line match f1l f1c f2l f2c rest]} {
1937        if {$gaudydiff} {
1938            $ctext insert end "\t" hunksep
1939            $ctext insert end "    $f1l    " d0 "    $f2l    " d1
1940            $ctext insert end "    $rest \n" hunksep
1941        } else {
1942            $ctext insert end "$line\n" hunksep
1943        }
1944        set diffinhdr 0
1945    } else {
1946        set x [string range $line 0 0]
1947        if {$x == "-" || $x == "+"} {
1948            set tag [expr {$x == "+"}]
1949            if {$gaudydiff} {
1950                set line [string range $line 1 end]
1951            }
1952            $ctext insert end "$line\n" d$tag
1953        } elseif {$x == " "} {
1954            if {$gaudydiff} {
1955                set line [string range $line 1 end]
1956            }
1957            $ctext insert end "$line\n"
1958        } elseif {$diffinhdr || $x == "\\"} {
1959            # e.g. "\ No newline at end of file"
1960            $ctext insert end "$line\n" filesep
1961        } else {
1962            # Something else we don't recognize
1963            if {$curdifftag != "Comments"} {
1964                $ctext insert end "\n"
1965                $ctext tag add $curdifftag $curtagstart end
1966                set curtagstart [$ctext index "end - 1c"]
1967                set curdifftag Comments
1968            }
1969            $ctext insert end "$line\n" filesep
1970        }
1971    }
1972    $ctext conf -state disabled
1973    if {[clock clicks -milliseconds] >= $nextupdate} {
1974        incr nextupdate 100
1975        fileevent $bdf readable {}
1976        update
1977        fileevent $bdf readable "getblobdiffline $bdf {$ids}"
1978    }
1979}
1980
1981proc nextfile {} {
1982    global difffilestart ctext
1983    set here [$ctext index @0,0]
1984    for {set i 0} {[info exists difffilestart($i)]} {incr i} {
1985        if {[$ctext compare $difffilestart($i) > $here]} {
1986            if {![info exists pos]
1987                || [$ctext compare $difffilestart($i) < $pos]} {
1988                set pos $difffilestart($i)
1989            }
1990        }
1991    }
1992    if {[info exists pos]} {
1993        $ctext yview $pos
1994    }
1995}
1996
1997proc listboxsel {} {
1998    global ctext cflist currentid
1999    if {![info exists currentid]} return
2000    set sel [lsort [$cflist curselection]]
2001    if {$sel eq {}} return
2002    set first [lindex $sel 0]
2003    catch {$ctext yview fmark.$first}
2004}
2005
2006proc setcoords {} {
2007    global linespc charspc canvx0 canvy0 mainfont
2008    set linespc [font metrics $mainfont -linespace]
2009    set charspc [font measure $mainfont "m"]
2010    set canvy0 [expr 3 + 0.5 * $linespc]
2011    set canvx0 [expr 3 + 0.5 * $linespc]
2012}
2013
2014proc redisplay {} {
2015    global selectedline stopped redisplaying phase
2016    if {$stopped > 1} return
2017    if {$phase == "getcommits"} return
2018    set redisplaying 1
2019    if {$phase == "drawgraph" || $phase == "incrdraw"} {
2020        set stopped 1
2021    } else {
2022        drawgraph
2023    }
2024}
2025
2026proc incrfont {inc} {
2027    global mainfont namefont textfont selectedline ctext canv phase
2028    global stopped entries
2029    unmarkmatches
2030    set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
2031    set namefont [lreplace $namefont 1 1 [expr {[lindex $namefont 1] + $inc}]]
2032    set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
2033    setcoords
2034    $ctext conf -font $textfont
2035    $ctext tag conf filesep -font [concat $textfont bold]
2036    foreach e $entries {
2037        $e conf -font $mainfont
2038    }
2039    if {$phase == "getcommits"} {
2040        $canv itemconf textitems -font $mainfont
2041    }
2042    redisplay
2043}
2044
2045proc clearsha1 {} {
2046    global sha1entry sha1string
2047    if {[string length $sha1string] == 40} {
2048        $sha1entry delete 0 end
2049    }
2050}
2051
2052proc sha1change {n1 n2 op} {
2053    global sha1string currentid sha1but
2054    if {$sha1string == {}
2055        || ([info exists currentid] && $sha1string == $currentid)} {
2056        set state disabled
2057    } else {
2058        set state normal
2059    }
2060    if {[$sha1but cget -state] == $state} return
2061    if {$state == "normal"} {
2062        $sha1but conf -state normal -relief raised -text "Goto: "
2063    } else {
2064        $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
2065    }
2066}
2067
2068proc gotocommit {} {
2069    global sha1string currentid idline tagids
2070    global lineid numcommits
2071
2072    if {$sha1string == {}
2073        || ([info exists currentid] && $sha1string == $currentid)} return
2074    if {[info exists tagids($sha1string)]} {
2075        set id $tagids($sha1string)
2076    } else {
2077        set id [string tolower $sha1string]
2078        if {[regexp {^[0-9a-f]{4,39}$} $id]} {
2079            set matches {}
2080            for {set l 0} {$l < $numcommits} {incr l} {
2081                if {[string match $id* $lineid($l)]} {
2082                    lappend matches $lineid($l)
2083                }
2084            }
2085            if {$matches ne {}} {
2086                if {[llength $matches] > 1} {
2087                    error_popup "Short SHA1 id $id is ambiguous"
2088                    return
2089                }
2090                set id [lindex $matches 0]
2091            }
2092        }
2093    }
2094    if {[info exists idline($id)]} {
2095        selectline $idline($id)
2096        return
2097    }
2098    if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
2099        set type "SHA1 id"
2100    } else {
2101        set type "Tag"
2102    }
2103    error_popup "$type $sha1string is not known"
2104}
2105
2106proc lineenter {x y id} {
2107    global hoverx hovery hoverid hovertimer
2108    global commitinfo canv
2109
2110    if {![info exists commitinfo($id)]} return
2111    set hoverx $x
2112    set hovery $y
2113    set hoverid $id
2114    if {[info exists hovertimer]} {
2115        after cancel $hovertimer
2116    }
2117    set hovertimer [after 500 linehover]
2118    $canv delete hover
2119}
2120
2121proc linemotion {x y id} {
2122    global hoverx hovery hoverid hovertimer
2123
2124    if {[info exists hoverid] && $id == $hoverid} {
2125        set hoverx $x
2126        set hovery $y
2127        if {[info exists hovertimer]} {
2128            after cancel $hovertimer
2129        }
2130        set hovertimer [after 500 linehover]
2131    }
2132}
2133
2134proc lineleave {id} {
2135    global hoverid hovertimer canv
2136
2137    if {[info exists hoverid] && $id == $hoverid} {
2138        $canv delete hover
2139        if {[info exists hovertimer]} {
2140            after cancel $hovertimer
2141            unset hovertimer
2142        }
2143        unset hoverid
2144    }
2145}
2146
2147proc linehover {} {
2148    global hoverx hovery hoverid hovertimer
2149    global canv linespc lthickness
2150    global commitinfo mainfont
2151
2152    set text [lindex $commitinfo($hoverid) 0]
2153    set ymax [lindex [$canv cget -scrollregion] 3]
2154    if {$ymax == {}} return
2155    set yfrac [lindex [$canv yview] 0]
2156    set x [expr {$hoverx + 2 * $linespc}]
2157    set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
2158    set x0 [expr {$x - 2 * $lthickness}]
2159    set y0 [expr {$y - 2 * $lthickness}]
2160    set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}]
2161    set y1 [expr {$y + $linespc + 2 * $lthickness}]
2162    set t [$canv create rectangle $x0 $y0 $x1 $y1 \
2163               -fill \#ffff80 -outline black -width 1 -tags hover]
2164    $canv raise $t
2165    set t [$canv create text $x $y -anchor nw -text $text -tags hover]
2166    $canv raise $t
2167}
2168
2169proc lineclick {x y id} {
2170    global ctext commitinfo children cflist canv
2171
2172    unmarkmatches
2173    $canv delete hover
2174    # fill the details pane with info about this line
2175    $ctext conf -state normal
2176    $ctext delete 0.0 end
2177    $ctext insert end "Parent:\n "
2178    catch {destroy $ctext.$id}
2179    button $ctext.$id -text "Go:" -command "selbyid $id" \
2180        -padx 4 -pady 0
2181    $ctext window create end -window $ctext.$id -align center
2182    set info $commitinfo($id)
2183    $ctext insert end "\t[lindex $info 0]\n"
2184    $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
2185    $ctext insert end "\tDate:\t[lindex $info 2]\n"
2186    $ctext insert end "\tID:\t$id\n"
2187    if {[info exists children($id)]} {
2188        $ctext insert end "\nChildren:"
2189        foreach child $children($id) {
2190            $ctext insert end "\n "
2191            catch {destroy $ctext.$child}
2192            button $ctext.$child -text "Go:" -command "selbyid $child" \
2193                -padx 4 -pady 0
2194            $ctext window create end -window $ctext.$child -align center
2195            set info $commitinfo($child)
2196            $ctext insert end "\t[lindex $info 0]"
2197        }
2198    }
2199    $ctext conf -state disabled
2200
2201    $cflist delete 0 end
2202}
2203
2204proc selbyid {id} {
2205    global idline
2206    if {[info exists idline($id)]} {
2207        selectline $idline($id)
2208    }
2209}
2210
2211proc mstime {} {
2212    global startmstime
2213    if {![info exists startmstime]} {
2214        set startmstime [clock clicks -milliseconds]
2215    }
2216    return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
2217}
2218
2219proc rowmenu {x y id} {
2220    global rowctxmenu idline selectedline rowmenuid
2221
2222    if {![info exists selectedline] || $idline($id) eq $selectedline} {
2223        set state disabled
2224    } else {
2225        set state normal
2226    }
2227    $rowctxmenu entryconfigure 0 -state $state
2228    $rowctxmenu entryconfigure 1 -state $state
2229    $rowctxmenu entryconfigure 2 -state $state
2230    set rowmenuid $id
2231    tk_popup $rowctxmenu $x $y
2232}
2233
2234proc diffvssel {dirn} {
2235    global rowmenuid selectedline lineid
2236    global ctext cflist
2237    global commitinfo
2238
2239    if {![info exists selectedline]} return
2240    if {$dirn} {
2241        set oldid $lineid($selectedline)
2242        set newid $rowmenuid
2243    } else {
2244        set oldid $rowmenuid
2245        set newid $lineid($selectedline)
2246    }
2247    $ctext conf -state normal
2248    $ctext delete 0.0 end
2249    $ctext mark set fmark.0 0.0
2250    $ctext mark gravity fmark.0 left
2251    $cflist delete 0 end
2252    $cflist insert end "Top"
2253    $ctext insert end "From $oldid\n     "
2254    $ctext insert end [lindex $commitinfo($oldid) 0]
2255    $ctext insert end "\n\nTo   $newid\n     "
2256    $ctext insert end [lindex $commitinfo($newid) 0]
2257    $ctext insert end "\n"
2258    $ctext conf -state disabled
2259    $ctext tag delete Comments
2260    $ctext tag remove found 1.0 end
2261    startdiff $newid [list $oldid]
2262}
2263
2264proc mkpatch {} {
2265    global rowmenuid currentid commitinfo patchtop patchnum
2266
2267    if {![info exists currentid]} return
2268    set oldid $currentid
2269    set oldhead [lindex $commitinfo($oldid) 0]
2270    set newid $rowmenuid
2271    set newhead [lindex $commitinfo($newid) 0]
2272    set top .patch
2273    set patchtop $top
2274    catch {destroy $top}
2275    toplevel $top
2276    label $top.title -text "Generate patch"
2277    grid $top.title - -pady 10
2278    label $top.from -text "From:"
2279    entry $top.fromsha1 -width 40 -relief flat
2280    $top.fromsha1 insert 0 $oldid
2281    $top.fromsha1 conf -state readonly
2282    grid $top.from $top.fromsha1 -sticky w
2283    entry $top.fromhead -width 60 -relief flat
2284    $top.fromhead insert 0 $oldhead
2285    $top.fromhead conf -state readonly
2286    grid x $top.fromhead -sticky w
2287    label $top.to -text "To:"
2288    entry $top.tosha1 -width 40 -relief flat
2289    $top.tosha1 insert 0 $newid
2290    $top.tosha1 conf -state readonly
2291    grid $top.to $top.tosha1 -sticky w
2292    entry $top.tohead -width 60 -relief flat
2293    $top.tohead insert 0 $newhead
2294    $top.tohead conf -state readonly
2295    grid x $top.tohead -sticky w
2296    button $top.rev -text "Reverse" -command mkpatchrev -padx 5
2297    grid $top.rev x -pady 10
2298    label $top.flab -text "Output file:"
2299    entry $top.fname -width 60
2300    $top.fname insert 0 [file normalize "patch$patchnum.patch"]
2301    incr patchnum
2302    grid $top.flab $top.fname -sticky w
2303    frame $top.buts
2304    button $top.buts.gen -text "Generate" -command mkpatchgo
2305    button $top.buts.can -text "Cancel" -command mkpatchcan
2306    grid $top.buts.gen $top.buts.can
2307    grid columnconfigure $top.buts 0 -weight 1 -uniform a
2308    grid columnconfigure $top.buts 1 -weight 1 -uniform a
2309    grid $top.buts - -pady 10 -sticky ew
2310    focus $top.fname
2311}
2312
2313proc mkpatchrev {} {
2314    global patchtop
2315
2316    set oldid [$patchtop.fromsha1 get]
2317    set oldhead [$patchtop.fromhead get]
2318    set newid [$patchtop.tosha1 get]
2319    set newhead [$patchtop.tohead get]
2320    foreach e [list fromsha1 fromhead tosha1 tohead] \
2321            v [list $newid $newhead $oldid $oldhead] {
2322        $patchtop.$e conf -state normal
2323        $patchtop.$e delete 0 end
2324        $patchtop.$e insert 0 $v
2325        $patchtop.$e conf -state readonly
2326    }
2327}
2328
2329proc mkpatchgo {} {
2330    global patchtop
2331
2332    set oldid [$patchtop.fromsha1 get]
2333    set newid [$patchtop.tosha1 get]
2334    set fname [$patchtop.fname get]
2335    if {[catch {exec git-diff-tree -p $oldid $newid >$fname &} err]} {
2336        error_popup "Error creating patch: $err"
2337    }
2338    catch {destroy $patchtop}
2339    unset patchtop
2340}
2341
2342proc mkpatchcan {} {
2343    global patchtop
2344
2345    catch {destroy $patchtop}
2346    unset patchtop
2347}
2348
2349proc mktag {} {
2350    global rowmenuid mktagtop commitinfo
2351
2352    set top .maketag
2353    set mktagtop $top
2354    catch {destroy $top}
2355    toplevel $top
2356    label $top.title -text "Create tag"
2357    grid $top.title - -pady 10
2358    label $top.id -text "ID:"
2359    entry $top.sha1 -width 40 -relief flat
2360    $top.sha1 insert 0 $rowmenuid
2361    $top.sha1 conf -state readonly
2362    grid $top.id $top.sha1 -sticky w
2363    entry $top.head -width 60 -relief flat
2364    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
2365    $top.head conf -state readonly
2366    grid x $top.head -sticky w
2367    label $top.tlab -text "Tag name:"
2368    entry $top.tag -width 60
2369    grid $top.tlab $top.tag -sticky w
2370    frame $top.buts
2371    button $top.buts.gen -text "Create" -command mktaggo
2372    button $top.buts.can -text "Cancel" -command mktagcan
2373    grid $top.buts.gen $top.buts.can
2374    grid columnconfigure $top.buts 0 -weight 1 -uniform a
2375    grid columnconfigure $top.buts 1 -weight 1 -uniform a
2376    grid $top.buts - -pady 10 -sticky ew
2377    focus $top.tag
2378}
2379
2380proc domktag {} {
2381    global mktagtop env tagids idtags
2382    global idpos idline linehtag canv selectedline
2383
2384    set id [$mktagtop.sha1 get]
2385    set tag [$mktagtop.tag get]
2386    if {$tag == {}} {
2387        error_popup "No tag name specified"
2388        return
2389    }
2390    if {[info exists tagids($tag)]} {
2391        error_popup "Tag \"$tag\" already exists"
2392        return
2393    }
2394    if {[catch {
2395        set dir ".git"
2396        if {[info exists env(GIT_DIR)]} {
2397            set dir $env(GIT_DIR)
2398        }
2399        set fname [file join $dir "refs/tags" $tag]
2400        set f [open $fname w]
2401        puts $f $id
2402        close $f
2403    } err]} {
2404        error_popup "Error creating tag: $err"
2405        return
2406    }
2407
2408    set tagids($tag) $id
2409    lappend idtags($id) $tag
2410    $canv delete tag.$id
2411    set xt [eval drawtags $id $idpos($id)]
2412    $canv coords $linehtag($idline($id)) $xt [lindex $idpos($id) 2]
2413    if {[info exists selectedline] && $selectedline == $idline($id)} {
2414        selectline $selectedline
2415    }
2416}
2417
2418proc mktagcan {} {
2419    global mktagtop
2420
2421    catch {destroy $mktagtop}
2422    unset mktagtop
2423}
2424
2425proc mktaggo {} {
2426    domktag
2427    mktagcan
2428}
2429
2430proc writecommit {} {
2431    global rowmenuid wrcomtop commitinfo wrcomcmd
2432
2433    set top .writecommit
2434    set wrcomtop $top
2435    catch {destroy $top}
2436    toplevel $top
2437    label $top.title -text "Write commit to file"
2438    grid $top.title - -pady 10
2439    label $top.id -text "ID:"
2440    entry $top.sha1 -width 40 -relief flat
2441    $top.sha1 insert 0 $rowmenuid
2442    $top.sha1 conf -state readonly
2443    grid $top.id $top.sha1 -sticky w
2444    entry $top.head -width 60 -relief flat
2445    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
2446    $top.head conf -state readonly
2447    grid x $top.head -sticky w
2448    label $top.clab -text "Command:"
2449    entry $top.cmd -width 60 -textvariable wrcomcmd
2450    grid $top.clab $top.cmd -sticky w -pady 10
2451    label $top.flab -text "Output file:"
2452    entry $top.fname -width 60
2453    $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
2454    grid $top.flab $top.fname -sticky w
2455    frame $top.buts
2456    button $top.buts.gen -text "Write" -command wrcomgo
2457    button $top.buts.can -text "Cancel" -command wrcomcan
2458    grid $top.buts.gen $top.buts.can
2459    grid columnconfigure $top.buts 0 -weight 1 -uniform a
2460    grid columnconfigure $top.buts 1 -weight 1 -uniform a
2461    grid $top.buts - -pady 10 -sticky ew
2462    focus $top.fname
2463}
2464
2465proc wrcomgo {} {
2466    global wrcomtop
2467
2468    set id [$wrcomtop.sha1 get]
2469    set cmd "echo $id | [$wrcomtop.cmd get]"
2470    set fname [$wrcomtop.fname get]
2471    if {[catch {exec sh -c $cmd >$fname &} err]} {
2472        error_popup "Error writing commit: $err"
2473    }
2474    catch {destroy $wrcomtop}
2475    unset wrcomtop
2476}
2477
2478proc wrcomcan {} {
2479    global wrcomtop
2480
2481    catch {destroy $wrcomtop}
2482    unset wrcomtop
2483}
2484
2485proc doquit {} {
2486    global stopped
2487    set stopped 100
2488    destroy .
2489}
2490
2491# defaults...
2492set datemode 0
2493set boldnames 0
2494set diffopts "-U 5 -p"
2495set wrcomcmd "git-diff-tree --stdin -p --pretty"
2496
2497set mainfont {Helvetica 9}
2498set textfont {Courier 9}
2499set findmergefiles 0
2500set gaudydiff 0
2501
2502set colors {green red blue magenta darkgrey brown orange}
2503
2504catch {source ~/.gitk}
2505
2506set namefont $mainfont
2507if {$boldnames} {
2508    lappend namefont bold
2509}
2510
2511set revtreeargs {}
2512foreach arg $argv {
2513    switch -regexp -- $arg {
2514        "^$" { }
2515        "^-b" { set boldnames 1 }
2516        "^-d" { set datemode 1 }
2517        default {
2518            lappend revtreeargs $arg
2519        }
2520    }
2521}
2522
2523set stopped 0
2524set redisplaying 0
2525set stuffsaved 0
2526set patchnum 0
2527setcoords
2528makewindow
2529readrefs
2530getcommits $revtreeargs