gitkon commit gitk: Factored out some common code into a new start_rev_list procedure (38ad091)
   1#!/bin/sh
   2# Tcl ignores the next line -*- tcl -*- \
   3exec wish "$0" -- "$@"
   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 gitdir {} {
  11    global env
  12    if {[info exists env(GIT_DIR)]} {
  13        return $env(GIT_DIR)
  14    } else {
  15        return ".git"
  16    }
  17}
  18
  19proc parse_args {rargs} {
  20    global parsed_args
  21
  22    if [catch {
  23        set parse_args [concat --default HEAD $rargs]
  24        set parsed_args [split [eval exec git-rev-parse $parse_args] "\n"]
  25    }] {
  26        # if git-rev-parse failed for some reason...
  27        if {$rargs == {}} {
  28            set rargs HEAD
  29        }
  30        set parsed_args $rargs
  31    }
  32    return $parsed_args
  33}
  34
  35proc start_rev_list {rlargs} {
  36    global startmsecs nextupdate ncmupdate
  37    global commfd leftover gitencoding
  38
  39    set startmsecs [clock clicks -milliseconds]
  40    set nextupdate [expr {$startmsecs + 100}]
  41    set ncmupdate 1
  42    if [catch {
  43        set commfd [open [concat | git-rev-list --header --topo-order \
  44                              --parents $rlargs] r]
  45    } err] {
  46        puts stderr "Error executing git-rev-list: $err"
  47        exit 1
  48    }
  49    set leftover {}
  50    fconfigure $commfd -blocking 0 -translation lf -encoding $gitencoding
  51    fileevent $commfd readable [list getcommitlines $commfd]
  52    . config -cursor watch
  53    settextcursor watch
  54}
  55
  56proc getcommits {rargs} {
  57    global oldcommits commits phase canv mainfont env
  58
  59    # check that we can find a .git directory somewhere...
  60    set gitdir [gitdir]
  61    if {![file isdirectory $gitdir]} {
  62        error_popup "Cannot find the git directory \"$gitdir\"."
  63        exit 1
  64    }
  65    set oldcommits {}
  66    set commits {}
  67    set phase getcommits
  68    start_rev_list [parse_args $rargs]
  69    $canv delete all
  70    $canv create text 3 3 -anchor nw -text "Reading commits..." \
  71        -font $mainfont -tags textitems
  72}
  73
  74proc getcommitlines {commfd}  {
  75    global oldcommits commits parents cdate children nchildren
  76    global commitlisted phase nextupdate
  77    global stopped redisplaying leftover
  78    global canv
  79
  80    set stuff [read $commfd]
  81    if {$stuff == {}} {
  82        if {![eof $commfd]} return
  83        # set it blocking so we wait for the process to terminate
  84        fconfigure $commfd -blocking 1
  85        if {![catch {close $commfd} err]} {
  86            after idle finishcommits
  87            return
  88        }
  89        if {[string range $err 0 4] == "usage"} {
  90            set err \
  91                "Gitk: error reading commits: bad arguments to git-rev-list.\
  92                (Note: arguments to gitk are passed to git-rev-list\
  93                to allow selection of commits to be displayed.)"
  94        } else {
  95            set err "Error reading commits: $err"
  96        }
  97        error_popup $err
  98        exit 1
  99    }
 100    set start 0
 101    while 1 {
 102        set i [string first "\0" $stuff $start]
 103        if {$i < 0} {
 104            append leftover [string range $stuff $start end]
 105            return
 106        }
 107        set cmit [string range $stuff $start [expr {$i - 1}]]
 108        if {$start == 0} {
 109            set cmit "$leftover$cmit"
 110            set leftover {}
 111        }
 112        set start [expr {$i + 1}]
 113        set j [string first "\n" $cmit]
 114        set ok 0
 115        if {$j >= 0} {
 116            set ids [string range $cmit 0 [expr {$j - 1}]]
 117            set ok 1
 118            foreach id $ids {
 119                if {![regexp {^[0-9a-f]{40}$} $id]} {
 120                    set ok 0
 121                    break
 122                }
 123            }
 124        }
 125        if {!$ok} {
 126            set shortcmit $cmit
 127            if {[string length $shortcmit] > 80} {
 128                set shortcmit "[string range $shortcmit 0 80]..."
 129            }
 130            error_popup "Can't parse git-rev-list output: {$shortcmit}"
 131            exit 1
 132        }
 133        set id [lindex $ids 0]
 134        set olds [lrange $ids 1 end]
 135        set cmit [string range $cmit [expr {$j + 1}] end]
 136        if {$phase == "updatecommits"} {
 137            $canv delete all
 138            set oldcommits $commits
 139            set commits {}
 140            unset children
 141            unset nchildren
 142            set phase getcommits
 143        }
 144        lappend commits $id
 145        set commitlisted($id) 1
 146        parsecommit $id $cmit 1 [lrange $ids 1 end]
 147        drawcommit $id 1
 148        if {[clock clicks -milliseconds] >= $nextupdate} {
 149            doupdate 1
 150        }
 151        while {$redisplaying} {
 152            set redisplaying 0
 153            if {$stopped == 1} {
 154                set stopped 0
 155                set phase "getcommits"
 156                foreach id $commits {
 157                    drawcommit $id 1
 158                    if {$stopped} break
 159                    if {[clock clicks -milliseconds] >= $nextupdate} {
 160                        doupdate 1
 161                    }
 162                }
 163            }
 164        }
 165    }
 166}
 167
 168proc doupdate {reading} {
 169    global commfd nextupdate numcommits ncmupdate
 170
 171    if {$reading} {
 172        fileevent $commfd readable {}
 173    }
 174    update
 175    set nextupdate [expr {[clock clicks -milliseconds] + 100}]
 176    if {$numcommits < 100} {
 177        set ncmupdate [expr {$numcommits + 1}]
 178    } elseif {$numcommits < 10000} {
 179        set ncmupdate [expr {$numcommits + 10}]
 180    } else {
 181        set ncmupdate [expr {$numcommits + 100}]
 182    }
 183    if {$reading} {
 184        fileevent $commfd readable [list getcommitlines $commfd]
 185    }
 186}
 187
 188proc readcommit {id} {
 189    if [catch {set contents [exec git-cat-file commit $id]}] return
 190    parsecommit $id $contents 0 {}
 191}
 192
 193proc updatechildren {id olds} {
 194    global children nchildren parents nparents ncleft
 195
 196    if {![info exists nchildren($id)]} {
 197        set children($id) {}
 198        set nchildren($id) 0
 199        set ncleft($id) 0
 200    }
 201    set parents($id) $olds
 202    set nparents($id) [llength $olds]
 203    foreach p $olds {
 204        if {![info exists nchildren($p)]} {
 205            set children($p) [list $id]
 206            set nchildren($p) 1
 207            set ncleft($p) 1
 208        } elseif {[lsearch -exact $children($p) $id] < 0} {
 209            lappend children($p) $id
 210            incr nchildren($p)
 211            incr ncleft($p)
 212        }
 213    }
 214}
 215
 216proc parsecommit {id contents listed olds} {
 217    global commitinfo cdate
 218
 219    set inhdr 1
 220    set comment {}
 221    set headline {}
 222    set auname {}
 223    set audate {}
 224    set comname {}
 225    set comdate {}
 226    updatechildren $id $olds
 227    set hdrend [string first "\n\n" $contents]
 228    if {$hdrend < 0} {
 229        # should never happen...
 230        set hdrend [string length $contents]
 231    }
 232    set header [string range $contents 0 [expr {$hdrend - 1}]]
 233    set comment [string range $contents [expr {$hdrend + 2}] end]
 234    foreach line [split $header "\n"] {
 235        set tag [lindex $line 0]
 236        if {$tag == "author"} {
 237            set audate [lindex $line end-1]
 238            set auname [lrange $line 1 end-2]
 239        } elseif {$tag == "committer"} {
 240            set comdate [lindex $line end-1]
 241            set comname [lrange $line 1 end-2]
 242        }
 243    }
 244    set headline {}
 245    # take the first line of the comment as the headline
 246    set i [string first "\n" $comment]
 247    if {$i >= 0} {
 248        set headline [string trim [string range $comment 0 $i]]
 249    } else {
 250        set headline $comment
 251    }
 252    if {!$listed} {
 253        # git-rev-list indents the comment by 4 spaces;
 254        # if we got this via git-cat-file, add the indentation
 255        set newcomment {}
 256        foreach line [split $comment "\n"] {
 257            append newcomment "    "
 258            append newcomment $line
 259            append newcomment "\n"
 260        }
 261        set comment $newcomment
 262    }
 263    if {$comdate != {}} {
 264        set cdate($id) $comdate
 265    }
 266    set commitinfo($id) [list $headline $auname $audate \
 267                             $comname $comdate $comment]
 268}
 269
 270proc readrefs {} {
 271    global tagids idtags headids idheads tagcontents
 272    global otherrefids idotherrefs
 273
 274    foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
 275        catch {unset $v}
 276    }
 277    set refd [open [list | git-ls-remote [gitdir]] r]
 278    while {0 <= [set n [gets $refd line]]} {
 279        if {![regexp {^([0-9a-f]{40})   refs/([^^]*)$} $line \
 280            match id path]} {
 281            continue
 282        }
 283        if {![regexp {^(tags|heads)/(.*)$} $path match type name]} {
 284            set type others
 285            set name $path
 286        }
 287        if {$type == "tags"} {
 288            set tagids($name) $id
 289            lappend idtags($id) $name
 290            set obj {}
 291            set type {}
 292            set tag {}
 293            catch {
 294                set commit [exec git-rev-parse "$id^0"]
 295                if {"$commit" != "$id"} {
 296                    set tagids($name) $commit
 297                    lappend idtags($commit) $name
 298                }
 299            }           
 300            catch {
 301                set tagcontents($name) [exec git-cat-file tag "$id"]
 302            }
 303        } elseif { $type == "heads" } {
 304            set headids($name) $id
 305            lappend idheads($id) $name
 306        } else {
 307            set otherrefids($name) $id
 308            lappend idotherrefs($id) $name
 309        }
 310    }
 311    close $refd
 312}
 313
 314proc error_popup msg {
 315    set w .error
 316    toplevel $w
 317    wm transient $w .
 318    message $w.m -text $msg -justify center -aspect 400
 319    pack $w.m -side top -fill x -padx 20 -pady 20
 320    button $w.ok -text OK -command "destroy $w"
 321    pack $w.ok -side bottom -fill x
 322    bind $w <Visibility> "grab $w; focus $w"
 323    tkwait window $w
 324}
 325
 326proc makewindow {rargs} {
 327    global canv canv2 canv3 linespc charspc ctext cflist textfont
 328    global findtype findtypemenu findloc findstring fstring geometry
 329    global entries sha1entry sha1string sha1but
 330    global maincursor textcursor curtextcursor
 331    global rowctxmenu mergemax
 332
 333    menu .bar
 334    .bar add cascade -label "File" -menu .bar.file
 335    menu .bar.file
 336    .bar.file add command -label "Update" -command [list updatecommits $rargs]
 337    .bar.file add command -label "Reread references" -command rereadrefs
 338    .bar.file add command -label "Quit" -command doquit
 339    menu .bar.edit
 340    .bar add cascade -label "Edit" -menu .bar.edit
 341    .bar.edit add command -label "Preferences" -command doprefs
 342    menu .bar.help
 343    .bar add cascade -label "Help" -menu .bar.help
 344    .bar.help add command -label "About gitk" -command about
 345    . configure -menu .bar
 346
 347    if {![info exists geometry(canv1)]} {
 348        set geometry(canv1) [expr {45 * $charspc}]
 349        set geometry(canv2) [expr {30 * $charspc}]
 350        set geometry(canv3) [expr {15 * $charspc}]
 351        set geometry(canvh) [expr {25 * $linespc + 4}]
 352        set geometry(ctextw) 80
 353        set geometry(ctexth) 30
 354        set geometry(cflistw) 30
 355    }
 356    panedwindow .ctop -orient vertical
 357    if {[info exists geometry(width)]} {
 358        .ctop conf -width $geometry(width) -height $geometry(height)
 359        set texth [expr {$geometry(height) - $geometry(canvh) - 56}]
 360        set geometry(ctexth) [expr {($texth - 8) /
 361                                    [font metrics $textfont -linespace]}]
 362    }
 363    frame .ctop.top
 364    frame .ctop.top.bar
 365    pack .ctop.top.bar -side bottom -fill x
 366    set cscroll .ctop.top.csb
 367    scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
 368    pack $cscroll -side right -fill y
 369    panedwindow .ctop.top.clist -orient horizontal -sashpad 0 -handlesize 4
 370    pack .ctop.top.clist -side top -fill both -expand 1
 371    .ctop add .ctop.top
 372    set canv .ctop.top.clist.canv
 373    canvas $canv -height $geometry(canvh) -width $geometry(canv1) \
 374        -bg white -bd 0 \
 375        -yscrollincr $linespc -yscrollcommand "$cscroll set"
 376    .ctop.top.clist add $canv
 377    set canv2 .ctop.top.clist.canv2
 378    canvas $canv2 -height $geometry(canvh) -width $geometry(canv2) \
 379        -bg white -bd 0 -yscrollincr $linespc
 380    .ctop.top.clist add $canv2
 381    set canv3 .ctop.top.clist.canv3
 382    canvas $canv3 -height $geometry(canvh) -width $geometry(canv3) \
 383        -bg white -bd 0 -yscrollincr $linespc
 384    .ctop.top.clist add $canv3
 385    bind .ctop.top.clist <Configure> {resizeclistpanes %W %w}
 386
 387    set sha1entry .ctop.top.bar.sha1
 388    set entries $sha1entry
 389    set sha1but .ctop.top.bar.sha1label
 390    button $sha1but -text "SHA1 ID: " -state disabled -relief flat \
 391        -command gotocommit -width 8
 392    $sha1but conf -disabledforeground [$sha1but cget -foreground]
 393    pack .ctop.top.bar.sha1label -side left
 394    entry $sha1entry -width 40 -font $textfont -textvariable sha1string
 395    trace add variable sha1string write sha1change
 396    pack $sha1entry -side left -pady 2
 397
 398    image create bitmap bm-left -data {
 399        #define left_width 16
 400        #define left_height 16
 401        static unsigned char left_bits[] = {
 402        0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
 403        0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
 404        0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
 405    }
 406    image create bitmap bm-right -data {
 407        #define right_width 16
 408        #define right_height 16
 409        static unsigned char right_bits[] = {
 410        0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
 411        0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
 412        0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
 413    }
 414    button .ctop.top.bar.leftbut -image bm-left -command goback \
 415        -state disabled -width 26
 416    pack .ctop.top.bar.leftbut -side left -fill y
 417    button .ctop.top.bar.rightbut -image bm-right -command goforw \
 418        -state disabled -width 26
 419    pack .ctop.top.bar.rightbut -side left -fill y
 420
 421    button .ctop.top.bar.findbut -text "Find" -command dofind
 422    pack .ctop.top.bar.findbut -side left
 423    set findstring {}
 424    set fstring .ctop.top.bar.findstring
 425    lappend entries $fstring
 426    entry $fstring -width 30 -font $textfont -textvariable findstring
 427    pack $fstring -side left -expand 1 -fill x
 428    set findtype Exact
 429    set findtypemenu [tk_optionMenu .ctop.top.bar.findtype \
 430                          findtype Exact IgnCase Regexp]
 431    set findloc "All fields"
 432    tk_optionMenu .ctop.top.bar.findloc findloc "All fields" Headline \
 433        Comments Author Committer Files Pickaxe
 434    pack .ctop.top.bar.findloc -side right
 435    pack .ctop.top.bar.findtype -side right
 436    # for making sure type==Exact whenever loc==Pickaxe
 437    trace add variable findloc write findlocchange
 438
 439    panedwindow .ctop.cdet -orient horizontal
 440    .ctop add .ctop.cdet
 441    frame .ctop.cdet.left
 442    set ctext .ctop.cdet.left.ctext
 443    text $ctext -bg white -state disabled -font $textfont \
 444        -width $geometry(ctextw) -height $geometry(ctexth) \
 445        -yscrollcommand ".ctop.cdet.left.sb set" -wrap none
 446    scrollbar .ctop.cdet.left.sb -command "$ctext yview"
 447    pack .ctop.cdet.left.sb -side right -fill y
 448    pack $ctext -side left -fill both -expand 1
 449    .ctop.cdet add .ctop.cdet.left
 450
 451    $ctext tag conf filesep -font [concat $textfont bold] -back "#aaaaaa"
 452    $ctext tag conf hunksep -fore blue
 453    $ctext tag conf d0 -fore red
 454    $ctext tag conf d1 -fore "#00a000"
 455    $ctext tag conf m0 -fore red
 456    $ctext tag conf m1 -fore blue
 457    $ctext tag conf m2 -fore green
 458    $ctext tag conf m3 -fore purple
 459    $ctext tag conf m4 -fore brown
 460    $ctext tag conf mmax -fore darkgrey
 461    set mergemax 5
 462    $ctext tag conf mresult -font [concat $textfont bold]
 463    $ctext tag conf msep -font [concat $textfont bold]
 464    $ctext tag conf found -back yellow
 465
 466    frame .ctop.cdet.right
 467    set cflist .ctop.cdet.right.cfiles
 468    listbox $cflist -bg white -selectmode extended -width $geometry(cflistw) \
 469        -yscrollcommand ".ctop.cdet.right.sb set"
 470    scrollbar .ctop.cdet.right.sb -command "$cflist yview"
 471    pack .ctop.cdet.right.sb -side right -fill y
 472    pack $cflist -side left -fill both -expand 1
 473    .ctop.cdet add .ctop.cdet.right
 474    bind .ctop.cdet <Configure> {resizecdetpanes %W %w}
 475
 476    pack .ctop -side top -fill both -expand 1
 477
 478    bindall <1> {selcanvline %W %x %y}
 479    #bindall <B1-Motion> {selcanvline %W %x %y}
 480    bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
 481    bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
 482    bindall <2> "allcanvs scan mark 0 %y"
 483    bindall <B2-Motion> "allcanvs scan dragto 0 %y"
 484    bind . <Key-Up> "selnextline -1"
 485    bind . <Key-Down> "selnextline 1"
 486    bind . <Key-Right> "goforw"
 487    bind . <Key-Left> "goback"
 488    bind . <Key-Prior> "allcanvs yview scroll -1 pages"
 489    bind . <Key-Next> "allcanvs yview scroll 1 pages"
 490    bindkey <Key-Delete> "$ctext yview scroll -1 pages"
 491    bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
 492    bindkey <Key-space> "$ctext yview scroll 1 pages"
 493    bindkey p "selnextline -1"
 494    bindkey n "selnextline 1"
 495    bindkey z "goback"
 496    bindkey x "goforw"
 497    bindkey i "selnextline -1"
 498    bindkey k "selnextline 1"
 499    bindkey j "goback"
 500    bindkey l "goforw"
 501    bindkey b "$ctext yview scroll -1 pages"
 502    bindkey d "$ctext yview scroll 18 units"
 503    bindkey u "$ctext yview scroll -18 units"
 504    bindkey / {findnext 1}
 505    bindkey <Key-Return> {findnext 0}
 506    bindkey ? findprev
 507    bindkey f nextfile
 508    bind . <Control-q> doquit
 509    bind . <Control-f> dofind
 510    bind . <Control-g> {findnext 0}
 511    bind . <Control-r> findprev
 512    bind . <Control-equal> {incrfont 1}
 513    bind . <Control-KP_Add> {incrfont 1}
 514    bind . <Control-minus> {incrfont -1}
 515    bind . <Control-KP_Subtract> {incrfont -1}
 516    bind $cflist <<ListboxSelect>> listboxsel
 517    bind . <Destroy> {savestuff %W}
 518    bind . <Button-1> "click %W"
 519    bind $fstring <Key-Return> dofind
 520    bind $sha1entry <Key-Return> gotocommit
 521    bind $sha1entry <<PasteSelection>> clearsha1
 522
 523    set maincursor [. cget -cursor]
 524    set textcursor [$ctext cget -cursor]
 525    set curtextcursor $textcursor
 526
 527    set rowctxmenu .rowctxmenu
 528    menu $rowctxmenu -tearoff 0
 529    $rowctxmenu add command -label "Diff this -> selected" \
 530        -command {diffvssel 0}
 531    $rowctxmenu add command -label "Diff selected -> this" \
 532        -command {diffvssel 1}
 533    $rowctxmenu add command -label "Make patch" -command mkpatch
 534    $rowctxmenu add command -label "Create tag" -command mktag
 535    $rowctxmenu add command -label "Write commit to file" -command writecommit
 536}
 537
 538# when we make a key binding for the toplevel, make sure
 539# it doesn't get triggered when that key is pressed in the
 540# find string entry widget.
 541proc bindkey {ev script} {
 542    global entries
 543    bind . $ev $script
 544    set escript [bind Entry $ev]
 545    if {$escript == {}} {
 546        set escript [bind Entry <Key>]
 547    }
 548    foreach e $entries {
 549        bind $e $ev "$escript; break"
 550    }
 551}
 552
 553# set the focus back to the toplevel for any click outside
 554# the entry widgets
 555proc click {w} {
 556    global entries
 557    foreach e $entries {
 558        if {$w == $e} return
 559    }
 560    focus .
 561}
 562
 563proc savestuff {w} {
 564    global canv canv2 canv3 ctext cflist mainfont textfont
 565    global stuffsaved findmergefiles maxgraphpct
 566    global maxwidth
 567
 568    if {$stuffsaved} return
 569    if {![winfo viewable .]} return
 570    catch {
 571        set f [open "~/.gitk-new" w]
 572        puts $f [list set mainfont $mainfont]
 573        puts $f [list set textfont $textfont]
 574        puts $f [list set findmergefiles $findmergefiles]
 575        puts $f [list set maxgraphpct $maxgraphpct]
 576        puts $f [list set maxwidth $maxwidth]
 577        puts $f "set geometry(width) [winfo width .ctop]"
 578        puts $f "set geometry(height) [winfo height .ctop]"
 579        puts $f "set geometry(canv1) [expr {[winfo width $canv]-2}]"
 580        puts $f "set geometry(canv2) [expr {[winfo width $canv2]-2}]"
 581        puts $f "set geometry(canv3) [expr {[winfo width $canv3]-2}]"
 582        puts $f "set geometry(canvh) [expr {[winfo height $canv]-2}]"
 583        set wid [expr {([winfo width $ctext] - 8) \
 584                           / [font measure $textfont "0"]}]
 585        puts $f "set geometry(ctextw) $wid"
 586        set wid [expr {([winfo width $cflist] - 11) \
 587                           / [font measure [$cflist cget -font] "0"]}]
 588        puts $f "set geometry(cflistw) $wid"
 589        close $f
 590        file rename -force "~/.gitk-new" "~/.gitk"
 591    }
 592    set stuffsaved 1
 593}
 594
 595proc resizeclistpanes {win w} {
 596    global oldwidth
 597    if [info exists oldwidth($win)] {
 598        set s0 [$win sash coord 0]
 599        set s1 [$win sash coord 1]
 600        if {$w < 60} {
 601            set sash0 [expr {int($w/2 - 2)}]
 602            set sash1 [expr {int($w*5/6 - 2)}]
 603        } else {
 604            set factor [expr {1.0 * $w / $oldwidth($win)}]
 605            set sash0 [expr {int($factor * [lindex $s0 0])}]
 606            set sash1 [expr {int($factor * [lindex $s1 0])}]
 607            if {$sash0 < 30} {
 608                set sash0 30
 609            }
 610            if {$sash1 < $sash0 + 20} {
 611                set sash1 [expr {$sash0 + 20}]
 612            }
 613            if {$sash1 > $w - 10} {
 614                set sash1 [expr {$w - 10}]
 615                if {$sash0 > $sash1 - 20} {
 616                    set sash0 [expr {$sash1 - 20}]
 617                }
 618            }
 619        }
 620        $win sash place 0 $sash0 [lindex $s0 1]
 621        $win sash place 1 $sash1 [lindex $s1 1]
 622    }
 623    set oldwidth($win) $w
 624}
 625
 626proc resizecdetpanes {win w} {
 627    global oldwidth
 628    if [info exists oldwidth($win)] {
 629        set s0 [$win sash coord 0]
 630        if {$w < 60} {
 631            set sash0 [expr {int($w*3/4 - 2)}]
 632        } else {
 633            set factor [expr {1.0 * $w / $oldwidth($win)}]
 634            set sash0 [expr {int($factor * [lindex $s0 0])}]
 635            if {$sash0 < 45} {
 636                set sash0 45
 637            }
 638            if {$sash0 > $w - 15} {
 639                set sash0 [expr {$w - 15}]
 640            }
 641        }
 642        $win sash place 0 $sash0 [lindex $s0 1]
 643    }
 644    set oldwidth($win) $w
 645}
 646
 647proc allcanvs args {
 648    global canv canv2 canv3
 649    eval $canv $args
 650    eval $canv2 $args
 651    eval $canv3 $args
 652}
 653
 654proc bindall {event action} {
 655    global canv canv2 canv3
 656    bind $canv $event $action
 657    bind $canv2 $event $action
 658    bind $canv3 $event $action
 659}
 660
 661proc about {} {
 662    set w .about
 663    if {[winfo exists $w]} {
 664        raise $w
 665        return
 666    }
 667    toplevel $w
 668    wm title $w "About gitk"
 669    message $w.m -text {
 670Gitk version 1.2
 671
 672Copyright © 2005 Paul Mackerras
 673
 674Use and redistribute under the terms of the GNU General Public License} \
 675            -justify center -aspect 400
 676    pack $w.m -side top -fill x -padx 20 -pady 20
 677    button $w.ok -text Close -command "destroy $w"
 678    pack $w.ok -side bottom
 679}
 680
 681proc assigncolor {id} {
 682    global colormap commcolors colors nextcolor
 683    global parents nparents children nchildren
 684    global cornercrossings crossings
 685
 686    if [info exists colormap($id)] return
 687    set ncolors [llength $colors]
 688    if {$nparents($id) <= 1 && $nchildren($id) == 1} {
 689        set child [lindex $children($id) 0]
 690        if {[info exists colormap($child)]
 691            && $nparents($child) == 1} {
 692            set colormap($id) $colormap($child)
 693            return
 694        }
 695    }
 696    set badcolors {}
 697    if {[info exists cornercrossings($id)]} {
 698        foreach x $cornercrossings($id) {
 699            if {[info exists colormap($x)]
 700                && [lsearch -exact $badcolors $colormap($x)] < 0} {
 701                lappend badcolors $colormap($x)
 702            }
 703        }
 704        if {[llength $badcolors] >= $ncolors} {
 705            set badcolors {}
 706        }
 707    }
 708    set origbad $badcolors
 709    if {[llength $badcolors] < $ncolors - 1} {
 710        if {[info exists crossings($id)]} {
 711            foreach x $crossings($id) {
 712                if {[info exists colormap($x)]
 713                    && [lsearch -exact $badcolors $colormap($x)] < 0} {
 714                    lappend badcolors $colormap($x)
 715                }
 716            }
 717            if {[llength $badcolors] >= $ncolors} {
 718                set badcolors $origbad
 719            }
 720        }
 721        set origbad $badcolors
 722    }
 723    if {[llength $badcolors] < $ncolors - 1} {
 724        foreach child $children($id) {
 725            if {[info exists colormap($child)]
 726                && [lsearch -exact $badcolors $colormap($child)] < 0} {
 727                lappend badcolors $colormap($child)
 728            }
 729            if {[info exists parents($child)]} {
 730                foreach p $parents($child) {
 731                    if {[info exists colormap($p)]
 732                        && [lsearch -exact $badcolors $colormap($p)] < 0} {
 733                        lappend badcolors $colormap($p)
 734                    }
 735                }
 736            }
 737        }
 738        if {[llength $badcolors] >= $ncolors} {
 739            set badcolors $origbad
 740        }
 741    }
 742    for {set i 0} {$i <= $ncolors} {incr i} {
 743        set c [lindex $colors $nextcolor]
 744        if {[incr nextcolor] >= $ncolors} {
 745            set nextcolor 0
 746        }
 747        if {[lsearch -exact $badcolors $c]} break
 748    }
 749    set colormap($id) $c
 750}
 751
 752proc initgraph {} {
 753    global canvy canvy0 lineno numcommits nextcolor linespc
 754    global mainline mainlinearrow sidelines
 755    global nchildren ncleft
 756    global displist nhyperspace
 757
 758    allcanvs delete all
 759    set nextcolor 0
 760    set canvy $canvy0
 761    set lineno -1
 762    set numcommits 0
 763    catch {unset mainline}
 764    catch {unset mainlinearrow}
 765    catch {unset sidelines}
 766    foreach id [array names nchildren] {
 767        set ncleft($id) $nchildren($id)
 768    }
 769    set displist {}
 770    set nhyperspace 0
 771}
 772
 773proc bindline {t id} {
 774    global canv
 775
 776    $canv bind $t <Enter> "lineenter %x %y $id"
 777    $canv bind $t <Motion> "linemotion %x %y $id"
 778    $canv bind $t <Leave> "lineleave $id"
 779    $canv bind $t <Button-1> "lineclick %x %y $id 1"
 780}
 781
 782proc drawlines {id xtra delold} {
 783    global mainline mainlinearrow sidelines lthickness colormap canv
 784
 785    if {$delold} {
 786        $canv delete lines.$id
 787    }
 788    if {[info exists mainline($id)]} {
 789        set t [$canv create line $mainline($id) \
 790                   -width [expr {($xtra + 1) * $lthickness}] \
 791                   -fill $colormap($id) -tags lines.$id \
 792                   -arrow $mainlinearrow($id)]
 793        $canv lower $t
 794        bindline $t $id
 795    }
 796    if {[info exists sidelines($id)]} {
 797        foreach ls $sidelines($id) {
 798            set coords [lindex $ls 0]
 799            set thick [lindex $ls 1]
 800            set arrow [lindex $ls 2]
 801            set t [$canv create line $coords -fill $colormap($id) \
 802                       -width [expr {($thick + $xtra) * $lthickness}] \
 803                       -arrow $arrow -tags lines.$id]
 804            $canv lower $t
 805            bindline $t $id
 806        }
 807    }
 808}
 809
 810# level here is an index in displist
 811proc drawcommitline {level} {
 812    global parents children nparents displist
 813    global canv canv2 canv3 mainfont namefont canvy linespc
 814    global lineid linehtag linentag linedtag commitinfo
 815    global colormap numcommits currentparents dupparents
 816    global idtags idline idheads idotherrefs
 817    global lineno lthickness mainline mainlinearrow sidelines
 818    global commitlisted rowtextx idpos lastuse displist
 819    global oldnlines olddlevel olddisplist
 820
 821    incr numcommits
 822    incr lineno
 823    set id [lindex $displist $level]
 824    set lastuse($id) $lineno
 825    set lineid($lineno) $id
 826    set idline($id) $lineno
 827    set ofill [expr {[info exists commitlisted($id)]? "blue": "white"}]
 828    if {![info exists commitinfo($id)]} {
 829        readcommit $id
 830        if {![info exists commitinfo($id)]} {
 831            set commitinfo($id) {"No commit information available"}
 832            set nparents($id) 0
 833        }
 834    }
 835    assigncolor $id
 836    set currentparents {}
 837    set dupparents {}
 838    if {[info exists commitlisted($id)] && [info exists parents($id)]} {
 839        foreach p $parents($id) {
 840            if {[lsearch -exact $currentparents $p] < 0} {
 841                lappend currentparents $p
 842            } else {
 843                # remember that this parent was listed twice
 844                lappend dupparents $p
 845            }
 846        }
 847    }
 848    set x [xcoord $level $level $lineno]
 849    set y1 $canvy
 850    set canvy [expr {$canvy + $linespc}]
 851    allcanvs conf -scrollregion \
 852        [list 0 0 0 [expr {$y1 + 0.5 * $linespc + 2}]]
 853    if {[info exists mainline($id)]} {
 854        lappend mainline($id) $x $y1
 855        if {$mainlinearrow($id) ne "none"} {
 856            set mainline($id) [trimdiagstart $mainline($id)]
 857        }
 858    }
 859    drawlines $id 0 0
 860    set orad [expr {$linespc / 3}]
 861    set t [$canv create oval [expr {$x - $orad}] [expr {$y1 - $orad}] \
 862               [expr {$x + $orad - 1}] [expr {$y1 + $orad - 1}] \
 863               -fill $ofill -outline black -width 1]
 864    $canv raise $t
 865    $canv bind $t <1> {selcanvline {} %x %y}
 866    set xt [xcoord [llength $displist] $level $lineno]
 867    if {[llength $currentparents] > 2} {
 868        set xt [expr {$xt + ([llength $currentparents] - 2) * $linespc}]
 869    }
 870    set rowtextx($lineno) $xt
 871    set idpos($id) [list $x $xt $y1]
 872    if {[info exists idtags($id)] || [info exists idheads($id)]
 873        || [info exists idotherrefs($id)]} {
 874        set xt [drawtags $id $x $xt $y1]
 875    }
 876    set headline [lindex $commitinfo($id) 0]
 877    set name [lindex $commitinfo($id) 1]
 878    set date [lindex $commitinfo($id) 2]
 879    set date [formatdate $date]
 880    set linehtag($lineno) [$canv create text $xt $y1 -anchor w \
 881                               -text $headline -font $mainfont ]
 882    $canv bind $linehtag($lineno) <Button-3> "rowmenu %X %Y $id"
 883    set linentag($lineno) [$canv2 create text 3 $y1 -anchor w \
 884                               -text $name -font $namefont]
 885    set linedtag($lineno) [$canv3 create text 3 $y1 -anchor w \
 886                               -text $date -font $mainfont]
 887
 888    set olddlevel $level
 889    set olddisplist $displist
 890    set oldnlines [llength $displist]
 891}
 892
 893proc drawtags {id x xt y1} {
 894    global idtags idheads idotherrefs
 895    global linespc lthickness
 896    global canv mainfont idline rowtextx
 897
 898    set marks {}
 899    set ntags 0
 900    set nheads 0
 901    if {[info exists idtags($id)]} {
 902        set marks $idtags($id)
 903        set ntags [llength $marks]
 904    }
 905    if {[info exists idheads($id)]} {
 906        set marks [concat $marks $idheads($id)]
 907        set nheads [llength $idheads($id)]
 908    }
 909    if {[info exists idotherrefs($id)]} {
 910        set marks [concat $marks $idotherrefs($id)]
 911    }
 912    if {$marks eq {}} {
 913        return $xt
 914    }
 915
 916    set delta [expr {int(0.5 * ($linespc - $lthickness))}]
 917    set yt [expr {$y1 - 0.5 * $linespc}]
 918    set yb [expr {$yt + $linespc - 1}]
 919    set xvals {}
 920    set wvals {}
 921    foreach tag $marks {
 922        set wid [font measure $mainfont $tag]
 923        lappend xvals $xt
 924        lappend wvals $wid
 925        set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
 926    }
 927    set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
 928               -width $lthickness -fill black -tags tag.$id]
 929    $canv lower $t
 930    foreach tag $marks x $xvals wid $wvals {
 931        set xl [expr {$x + $delta}]
 932        set xr [expr {$x + $delta + $wid + $lthickness}]
 933        if {[incr ntags -1] >= 0} {
 934            # draw a tag
 935            set t [$canv create polygon $x [expr {$yt + $delta}] $xl $yt \
 936                       $xr $yt $xr $yb $xl $yb $x [expr {$yb - $delta}] \
 937                       -width 1 -outline black -fill yellow -tags tag.$id]
 938            $canv bind $t <1> [list showtag $tag 1]
 939            set rowtextx($idline($id)) [expr {$xr + $linespc}]
 940        } else {
 941            # draw a head or other ref
 942            if {[incr nheads -1] >= 0} {
 943                set col green
 944            } else {
 945                set col "#ddddff"
 946            }
 947            set xl [expr {$xl - $delta/2}]
 948            $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
 949                -width 1 -outline black -fill $col -tags tag.$id
 950        }
 951        set t [$canv create text $xl $y1 -anchor w -text $tag \
 952                   -font $mainfont -tags tag.$id]
 953        if {$ntags >= 0} {
 954            $canv bind $t <1> [list showtag $tag 1]
 955        }
 956    }
 957    return $xt
 958}
 959
 960proc notecrossings {id lo hi corner} {
 961    global olddisplist crossings cornercrossings
 962
 963    for {set i $lo} {[incr i] < $hi} {} {
 964        set p [lindex $olddisplist $i]
 965        if {$p == {}} continue
 966        if {$i == $corner} {
 967            if {![info exists cornercrossings($id)]
 968                || [lsearch -exact $cornercrossings($id) $p] < 0} {
 969                lappend cornercrossings($id) $p
 970            }
 971            if {![info exists cornercrossings($p)]
 972                || [lsearch -exact $cornercrossings($p) $id] < 0} {
 973                lappend cornercrossings($p) $id
 974            }
 975        } else {
 976            if {![info exists crossings($id)]
 977                || [lsearch -exact $crossings($id) $p] < 0} {
 978                lappend crossings($id) $p
 979            }
 980            if {![info exists crossings($p)]
 981                || [lsearch -exact $crossings($p) $id] < 0} {
 982                lappend crossings($p) $id
 983            }
 984        }
 985    }
 986}
 987
 988proc xcoord {i level ln} {
 989    global canvx0 xspc1 xspc2
 990
 991    set x [expr {$canvx0 + $i * $xspc1($ln)}]
 992    if {$i > 0 && $i == $level} {
 993        set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
 994    } elseif {$i > $level} {
 995        set x [expr {$x + $xspc2 - $xspc1($ln)}]
 996    }
 997    return $x
 998}
 999
1000# it seems Tk can't draw arrows on the end of diagonal line segments...
1001proc trimdiagend {line} {
1002    while {[llength $line] > 4} {
1003        set x1 [lindex $line end-3]
1004        set y1 [lindex $line end-2]
1005        set x2 [lindex $line end-1]
1006        set y2 [lindex $line end]
1007        if {($x1 == $x2) != ($y1 == $y2)} break
1008        set line [lreplace $line end-1 end]
1009    }
1010    return $line
1011}
1012
1013proc trimdiagstart {line} {
1014    while {[llength $line] > 4} {
1015        set x1 [lindex $line 0]
1016        set y1 [lindex $line 1]
1017        set x2 [lindex $line 2]
1018        set y2 [lindex $line 3]
1019        if {($x1 == $x2) != ($y1 == $y2)} break
1020        set line [lreplace $line 0 1]
1021    }
1022    return $line
1023}
1024
1025proc drawslants {id needonscreen nohs} {
1026    global canv mainline mainlinearrow sidelines
1027    global canvx0 canvy xspc1 xspc2 lthickness
1028    global currentparents dupparents
1029    global lthickness linespc canvy colormap lineno geometry
1030    global maxgraphpct maxwidth
1031    global displist onscreen lastuse
1032    global parents commitlisted
1033    global oldnlines olddlevel olddisplist
1034    global nhyperspace numcommits nnewparents
1035
1036    if {$lineno < 0} {
1037        lappend displist $id
1038        set onscreen($id) 1
1039        return 0
1040    }
1041
1042    set y1 [expr {$canvy - $linespc}]
1043    set y2 $canvy
1044
1045    # work out what we need to get back on screen
1046    set reins {}
1047    if {$onscreen($id) < 0} {
1048        # next to do isn't displayed, better get it on screen...
1049        lappend reins [list $id 0]
1050    }
1051    # make sure all the previous commits's parents are on the screen
1052    foreach p $currentparents {
1053        if {$onscreen($p) < 0} {
1054            lappend reins [list $p 0]
1055        }
1056    }
1057    # bring back anything requested by caller
1058    if {$needonscreen ne {}} {
1059        lappend reins $needonscreen
1060    }
1061
1062    # try the shortcut
1063    if {$currentparents == $id && $onscreen($id) == 0 && $reins eq {}} {
1064        set dlevel $olddlevel
1065        set x [xcoord $dlevel $dlevel $lineno]
1066        set mainline($id) [list $x $y1]
1067        set mainlinearrow($id) none
1068        set lastuse($id) $lineno
1069        set displist [lreplace $displist $dlevel $dlevel $id]
1070        set onscreen($id) 1
1071        set xspc1([expr {$lineno + 1}]) $xspc1($lineno)
1072        return $dlevel
1073    }
1074
1075    # update displist
1076    set displist [lreplace $displist $olddlevel $olddlevel]
1077    set j $olddlevel
1078    foreach p $currentparents {
1079        set lastuse($p) $lineno
1080        if {$onscreen($p) == 0} {
1081            set displist [linsert $displist $j $p]
1082            set onscreen($p) 1
1083            incr j
1084        }
1085    }
1086    if {$onscreen($id) == 0} {
1087        lappend displist $id
1088        set onscreen($id) 1
1089    }
1090
1091    # remove the null entry if present
1092    set nullentry [lsearch -exact $displist {}]
1093    if {$nullentry >= 0} {
1094        set displist [lreplace $displist $nullentry $nullentry]
1095    }
1096
1097    # bring back the ones we need now (if we did it earlier
1098    # it would change displist and invalidate olddlevel)
1099    foreach pi $reins {
1100        # test again in case of duplicates in reins
1101        set p [lindex $pi 0]
1102        if {$onscreen($p) < 0} {
1103            set onscreen($p) 1
1104            set lastuse($p) $lineno
1105            set displist [linsert $displist [lindex $pi 1] $p]
1106            incr nhyperspace -1
1107        }
1108    }
1109
1110    set lastuse($id) $lineno
1111
1112    # see if we need to make any lines jump off into hyperspace
1113    set displ [llength $displist]
1114    if {$displ > $maxwidth} {
1115        set ages {}
1116        foreach x $displist {
1117            lappend ages [list $lastuse($x) $x]
1118        }
1119        set ages [lsort -integer -index 0 $ages]
1120        set k 0
1121        while {$displ > $maxwidth} {
1122            set use [lindex $ages $k 0]
1123            set victim [lindex $ages $k 1]
1124            if {$use >= $lineno - 5} break
1125            incr k
1126            if {[lsearch -exact $nohs $victim] >= 0} continue
1127            set i [lsearch -exact $displist $victim]
1128            set displist [lreplace $displist $i $i]
1129            set onscreen($victim) -1
1130            incr nhyperspace
1131            incr displ -1
1132            if {$i < $nullentry} {
1133                incr nullentry -1
1134            }
1135            set x [lindex $mainline($victim) end-1]
1136            lappend mainline($victim) $x $y1
1137            set line [trimdiagend $mainline($victim)]
1138            set arrow "last"
1139            if {$mainlinearrow($victim) ne "none"} {
1140                set line [trimdiagstart $line]
1141                set arrow "both"
1142            }
1143            lappend sidelines($victim) [list $line 1 $arrow]
1144            unset mainline($victim)
1145        }
1146    }
1147
1148    set dlevel [lsearch -exact $displist $id]
1149
1150    # If we are reducing, put in a null entry
1151    if {$displ < $oldnlines} {
1152        # does the next line look like a merge?
1153        # i.e. does it have > 1 new parent?
1154        if {$nnewparents($id) > 1} {
1155            set i [expr {$dlevel + 1}]
1156        } elseif {$nnewparents([lindex $olddisplist $olddlevel]) == 0} {
1157            set i $olddlevel
1158            if {$nullentry >= 0 && $nullentry < $i} {
1159                incr i -1
1160            }
1161        } elseif {$nullentry >= 0} {
1162            set i $nullentry
1163            while {$i < $displ
1164                   && [lindex $olddisplist $i] == [lindex $displist $i]} {
1165                incr i
1166            }
1167        } else {
1168            set i $olddlevel
1169            if {$dlevel >= $i} {
1170                incr i
1171            }
1172        }
1173        if {$i < $displ} {
1174            set displist [linsert $displist $i {}]
1175            incr displ
1176            if {$dlevel >= $i} {
1177                incr dlevel
1178            }
1179        }
1180    }
1181
1182    # decide on the line spacing for the next line
1183    set lj [expr {$lineno + 1}]
1184    set maxw [expr {$maxgraphpct * $geometry(canv1) / 100}]
1185    if {$displ <= 1 || $canvx0 + $displ * $xspc2 <= $maxw} {
1186        set xspc1($lj) $xspc2
1187    } else {
1188        set xspc1($lj) [expr {($maxw - $canvx0 - $xspc2) / ($displ - 1)}]
1189        if {$xspc1($lj) < $lthickness} {
1190            set xspc1($lj) $lthickness
1191        }
1192    }
1193
1194    foreach idi $reins {
1195        set id [lindex $idi 0]
1196        set j [lsearch -exact $displist $id]
1197        set xj [xcoord $j $dlevel $lj]
1198        set mainline($id) [list $xj $y2]
1199        set mainlinearrow($id) first
1200    }
1201
1202    set i -1
1203    foreach id $olddisplist {
1204        incr i
1205        if {$id == {}} continue
1206        if {$onscreen($id) <= 0} continue
1207        set xi [xcoord $i $olddlevel $lineno]
1208        if {$i == $olddlevel} {
1209            foreach p $currentparents {
1210                set j [lsearch -exact $displist $p]
1211                set coords [list $xi $y1]
1212                set xj [xcoord $j $dlevel $lj]
1213                if {$xj < $xi - $linespc} {
1214                    lappend coords [expr {$xj + $linespc}] $y1
1215                    notecrossings $p $j $i [expr {$j + 1}]
1216                } elseif {$xj > $xi + $linespc} {
1217                    lappend coords [expr {$xj - $linespc}] $y1
1218                    notecrossings $p $i $j [expr {$j - 1}]
1219                }
1220                if {[lsearch -exact $dupparents $p] >= 0} {
1221                    # draw a double-width line to indicate the doubled parent
1222                    lappend coords $xj $y2
1223                    lappend sidelines($p) [list $coords 2 none]
1224                    if {![info exists mainline($p)]} {
1225                        set mainline($p) [list $xj $y2]
1226                        set mainlinearrow($p) none
1227                    }
1228                } else {
1229                    # normal case, no parent duplicated
1230                    set yb $y2
1231                    set dx [expr {abs($xi - $xj)}]
1232                    if {0 && $dx < $linespc} {
1233                        set yb [expr {$y1 + $dx}]
1234                    }
1235                    if {![info exists mainline($p)]} {
1236                        if {$xi != $xj} {
1237                            lappend coords $xj $yb
1238                        }
1239                        set mainline($p) $coords
1240                        set mainlinearrow($p) none
1241                    } else {
1242                        lappend coords $xj $yb
1243                        if {$yb < $y2} {
1244                            lappend coords $xj $y2
1245                        }
1246                        lappend sidelines($p) [list $coords 1 none]
1247                    }
1248                }
1249            }
1250        } else {
1251            set j $i
1252            if {[lindex $displist $i] != $id} {
1253                set j [lsearch -exact $displist $id]
1254            }
1255            if {$j != $i || $xspc1($lineno) != $xspc1($lj)
1256                || ($olddlevel < $i && $i < $dlevel)
1257                || ($dlevel < $i && $i < $olddlevel)} {
1258                set xj [xcoord $j $dlevel $lj]
1259                lappend mainline($id) $xi $y1 $xj $y2
1260            }
1261        }
1262    }
1263    return $dlevel
1264}
1265
1266# search for x in a list of lists
1267proc llsearch {llist x} {
1268    set i 0
1269    foreach l $llist {
1270        if {$l == $x || [lsearch -exact $l $x] >= 0} {
1271            return $i
1272        }
1273        incr i
1274    }
1275    return -1
1276}
1277
1278proc drawmore {reading} {
1279    global displayorder numcommits ncmupdate nextupdate
1280    global stopped nhyperspace parents commitlisted
1281    global maxwidth onscreen displist currentparents olddlevel
1282
1283    set n [llength $displayorder]
1284    while {$numcommits < $n} {
1285        set id [lindex $displayorder $numcommits]
1286        set ctxend [expr {$numcommits + 10}]
1287        if {!$reading && $ctxend > $n} {
1288            set ctxend $n
1289        }
1290        set dlist {}
1291        if {$numcommits > 0} {
1292            set dlist [lreplace $displist $olddlevel $olddlevel]
1293            set i $olddlevel
1294            foreach p $currentparents {
1295                if {$onscreen($p) == 0} {
1296                    set dlist [linsert $dlist $i $p]
1297                    incr i
1298                }
1299            }
1300        }
1301        set nohs {}
1302        set reins {}
1303        set isfat [expr {[llength $dlist] > $maxwidth}]
1304        if {$nhyperspace > 0 || $isfat} {
1305            if {$ctxend > $n} break
1306            # work out what to bring back and
1307            # what we want to don't want to send into hyperspace
1308            set room 1
1309            for {set k $numcommits} {$k < $ctxend} {incr k} {
1310                set x [lindex $displayorder $k]
1311                set i [llsearch $dlist $x]
1312                if {$i < 0} {
1313                    set i [llength $dlist]
1314                    lappend dlist $x
1315                }
1316                if {[lsearch -exact $nohs $x] < 0} {
1317                    lappend nohs $x
1318                }
1319                if {$reins eq {} && $onscreen($x) < 0 && $room} {
1320                    set reins [list $x $i]
1321                }
1322                set newp {}
1323                if {[info exists commitlisted($x)]} {
1324                    set right 0
1325                    foreach p $parents($x) {
1326                        if {[llsearch $dlist $p] < 0} {
1327                            lappend newp $p
1328                            if {[lsearch -exact $nohs $p] < 0} {
1329                                lappend nohs $p
1330                            }
1331                            if {$reins eq {} && $onscreen($p) < 0 && $room} {
1332                                set reins [list $p [expr {$i + $right}]]
1333                            }
1334                        }
1335                        set right 1
1336                    }
1337                }
1338                set l [lindex $dlist $i]
1339                if {[llength $l] == 1} {
1340                    set l $newp
1341                } else {
1342                    set j [lsearch -exact $l $x]
1343                    set l [concat [lreplace $l $j $j] $newp]
1344                }
1345                set dlist [lreplace $dlist $i $i $l]
1346                if {$room && $isfat && [llength $newp] <= 1} {
1347                    set room 0
1348                }
1349            }
1350        }
1351
1352        set dlevel [drawslants $id $reins $nohs]
1353        drawcommitline $dlevel
1354        if {[clock clicks -milliseconds] >= $nextupdate
1355            && $numcommits >= $ncmupdate} {
1356            doupdate $reading
1357            if {$stopped} break
1358        }
1359    }
1360}
1361
1362# level here is an index in todo
1363proc updatetodo {level noshortcut} {
1364    global ncleft todo nnewparents
1365    global commitlisted parents onscreen
1366
1367    set id [lindex $todo $level]
1368    set olds {}
1369    if {[info exists commitlisted($id)]} {
1370        foreach p $parents($id) {
1371            if {[lsearch -exact $olds $p] < 0} {
1372                lappend olds $p
1373            }
1374        }
1375    }
1376    if {!$noshortcut && [llength $olds] == 1} {
1377        set p [lindex $olds 0]
1378        if {$ncleft($p) == 1 && [lsearch -exact $todo $p] < 0} {
1379            set ncleft($p) 0
1380            set todo [lreplace $todo $level $level $p]
1381            set onscreen($p) 0
1382            set nnewparents($id) 1
1383            return 0
1384        }
1385    }
1386
1387    set todo [lreplace $todo $level $level]
1388    set i $level
1389    set n 0
1390    foreach p $olds {
1391        incr ncleft($p) -1
1392        set k [lsearch -exact $todo $p]
1393        if {$k < 0} {
1394            set todo [linsert $todo $i $p]
1395            set onscreen($p) 0
1396            incr i
1397            incr n
1398        }
1399    }
1400    set nnewparents($id) $n
1401
1402    return 1
1403}
1404
1405proc decidenext {{noread 0}} {
1406    global ncleft todo
1407    global datemode cdate
1408    global commitinfo
1409
1410    # choose which one to do next time around
1411    set todol [llength $todo]
1412    set level -1
1413    set latest {}
1414    for {set k $todol} {[incr k -1] >= 0} {} {
1415        set p [lindex $todo $k]
1416        if {$ncleft($p) == 0} {
1417            if {$datemode} {
1418                if {![info exists commitinfo($p)]} {
1419                    if {$noread} {
1420                        return {}
1421                    }
1422                    readcommit $p
1423                }
1424                if {$latest == {} || $cdate($p) > $latest} {
1425                    set level $k
1426                    set latest $cdate($p)
1427                }
1428            } else {
1429                set level $k
1430                break
1431            }
1432        }
1433    }
1434    if {$level < 0} {
1435        if {$todo != {}} {
1436            puts "ERROR: none of the pending commits can be done yet:"
1437            foreach p $todo {
1438                puts "  $p ($ncleft($p))"
1439            }
1440        }
1441        return -1
1442    }
1443
1444    return $level
1445}
1446
1447proc drawcommit {id reading} {
1448    global phase todo nchildren datemode nextupdate revlistorder
1449    global numcommits ncmupdate displayorder todo onscreen
1450    global numcommits ncmupdate displayorder todo onscreen parents
1451
1452    if {$phase != "incrdraw"} {
1453        set phase incrdraw
1454        set displayorder {}
1455        set todo {}
1456        initgraph
1457    }
1458    if {$nchildren($id) == 0} {
1459        lappend todo $id
1460        set onscreen($id) 0
1461    }
1462    if {$revlistorder} {
1463        set level [lsearch -exact $todo $id]
1464        if {$level < 0} {
1465            error_popup "oops, $id isn't in todo"
1466            return
1467        }
1468        lappend displayorder $id
1469        updatetodo $level 0
1470    } else {
1471        set level [decidenext 1]
1472        if {$level == {} || $id != [lindex $todo $level]} {
1473            return
1474        }
1475        while 1 {
1476            lappend displayorder [lindex $todo $level]
1477            if {[updatetodo $level $datemode]} {
1478                set level [decidenext 1]
1479                if {$level == {}} break
1480            }
1481            set id [lindex $todo $level]
1482            if {![info exists commitlisted($id)]} {
1483                break
1484            }
1485        }
1486    }
1487    drawmore $reading
1488}
1489
1490proc finishcommits {} {
1491    global phase oldcommits commits
1492    global canv mainfont ctext maincursor textcursor
1493    global parents
1494
1495    if {$phase == "incrdraw" || $phase == "removecommits"} {
1496        foreach id $oldcommits {
1497            lappend commits $id
1498            updatechildren $id $parents($id)
1499            drawcommit $id 0
1500        }
1501        set oldcommits {}
1502        drawrest
1503    } elseif {$phase == "updatecommits"} {
1504        set phase {}
1505    } else {
1506        $canv delete all
1507        $canv create text 3 3 -anchor nw -text "No commits selected" \
1508            -font $mainfont -tags textitems
1509        set phase {}
1510    }
1511    . config -cursor $maincursor
1512    settextcursor $textcursor
1513}
1514
1515# Don't change the text pane cursor if it is currently the hand cursor,
1516# showing that we are over a sha1 ID link.
1517proc settextcursor {c} {
1518    global ctext curtextcursor
1519
1520    if {[$ctext cget -cursor] == $curtextcursor} {
1521        $ctext config -cursor $c
1522    }
1523    set curtextcursor $c
1524}
1525
1526proc drawgraph {} {
1527    global nextupdate startmsecs ncmupdate
1528    global displayorder onscreen
1529
1530    if {$displayorder == {}} return
1531    set startmsecs [clock clicks -milliseconds]
1532    set nextupdate [expr {$startmsecs + 100}]
1533    set ncmupdate 1
1534    initgraph
1535    foreach id $displayorder {
1536        set onscreen($id) 0
1537    }
1538    drawmore 0
1539}
1540
1541proc drawrest {} {
1542    global phase stopped redisplaying selectedline
1543    global datemode todo displayorder
1544    global numcommits ncmupdate
1545    global nextupdate startmsecs revlistorder
1546
1547    set level [decidenext]
1548    if {$level >= 0} {
1549        set phase drawgraph
1550        while 1 {
1551            lappend displayorder [lindex $todo $level]
1552            set hard [updatetodo $level $datemode]
1553            if {$hard} {
1554                set level [decidenext]
1555                if {$level < 0} break
1556            }
1557        }
1558    }
1559    drawmore 0
1560    set phase {}
1561    set drawmsecs [expr {[clock clicks -milliseconds] - $startmsecs}]
1562    #puts "overall $drawmsecs ms for $numcommits commits"
1563    if {$redisplaying} {
1564        if {$stopped == 0 && [info exists selectedline]} {
1565            selectline $selectedline 0
1566        }
1567        if {$stopped == 1} {
1568            set stopped 0
1569            after idle drawgraph
1570        } else {
1571            set redisplaying 0
1572        }
1573    }
1574}
1575
1576proc findmatches {f} {
1577    global findtype foundstring foundstrlen
1578    if {$findtype == "Regexp"} {
1579        set matches [regexp -indices -all -inline $foundstring $f]
1580    } else {
1581        if {$findtype == "IgnCase"} {
1582            set str [string tolower $f]
1583        } else {
1584            set str $f
1585        }
1586        set matches {}
1587        set i 0
1588        while {[set j [string first $foundstring $str $i]] >= 0} {
1589            lappend matches [list $j [expr {$j+$foundstrlen-1}]]
1590            set i [expr {$j + $foundstrlen}]
1591        }
1592    }
1593    return $matches
1594}
1595
1596proc dofind {} {
1597    global findtype findloc findstring markedmatches commitinfo
1598    global numcommits lineid linehtag linentag linedtag
1599    global mainfont namefont canv canv2 canv3 selectedline
1600    global matchinglines foundstring foundstrlen
1601
1602    stopfindproc
1603    unmarkmatches
1604    focus .
1605    set matchinglines {}
1606    if {$findloc == "Pickaxe"} {
1607        findpatches
1608        return
1609    }
1610    if {$findtype == "IgnCase"} {
1611        set foundstring [string tolower $findstring]
1612    } else {
1613        set foundstring $findstring
1614    }
1615    set foundstrlen [string length $findstring]
1616    if {$foundstrlen == 0} return
1617    if {$findloc == "Files"} {
1618        findfiles
1619        return
1620    }
1621    if {![info exists selectedline]} {
1622        set oldsel -1
1623    } else {
1624        set oldsel $selectedline
1625    }
1626    set didsel 0
1627    set fldtypes {Headline Author Date Committer CDate Comment}
1628    for {set l 0} {$l < $numcommits} {incr l} {
1629        set id $lineid($l)
1630        set info $commitinfo($id)
1631        set doesmatch 0
1632        foreach f $info ty $fldtypes {
1633            if {$findloc != "All fields" && $findloc != $ty} {
1634                continue
1635            }
1636            set matches [findmatches $f]
1637            if {$matches == {}} continue
1638            set doesmatch 1
1639            if {$ty == "Headline"} {
1640                markmatches $canv $l $f $linehtag($l) $matches $mainfont
1641            } elseif {$ty == "Author"} {
1642                markmatches $canv2 $l $f $linentag($l) $matches $namefont
1643            } elseif {$ty == "Date"} {
1644                markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
1645            }
1646        }
1647        if {$doesmatch} {
1648            lappend matchinglines $l
1649            if {!$didsel && $l > $oldsel} {
1650                findselectline $l
1651                set didsel 1
1652            }
1653        }
1654    }
1655    if {$matchinglines == {}} {
1656        bell
1657    } elseif {!$didsel} {
1658        findselectline [lindex $matchinglines 0]
1659    }
1660}
1661
1662proc findselectline {l} {
1663    global findloc commentend ctext
1664    selectline $l 1
1665    if {$findloc == "All fields" || $findloc == "Comments"} {
1666        # highlight the matches in the comments
1667        set f [$ctext get 1.0 $commentend]
1668        set matches [findmatches $f]
1669        foreach match $matches {
1670            set start [lindex $match 0]
1671            set end [expr {[lindex $match 1] + 1}]
1672            $ctext tag add found "1.0 + $start c" "1.0 + $end c"
1673        }
1674    }
1675}
1676
1677proc findnext {restart} {
1678    global matchinglines selectedline
1679    if {![info exists matchinglines]} {
1680        if {$restart} {
1681            dofind
1682        }
1683        return
1684    }
1685    if {![info exists selectedline]} return
1686    foreach l $matchinglines {
1687        if {$l > $selectedline} {
1688            findselectline $l
1689            return
1690        }
1691    }
1692    bell
1693}
1694
1695proc findprev {} {
1696    global matchinglines selectedline
1697    if {![info exists matchinglines]} {
1698        dofind
1699        return
1700    }
1701    if {![info exists selectedline]} return
1702    set prev {}
1703    foreach l $matchinglines {
1704        if {$l >= $selectedline} break
1705        set prev $l
1706    }
1707    if {$prev != {}} {
1708        findselectline $prev
1709    } else {
1710        bell
1711    }
1712}
1713
1714proc findlocchange {name ix op} {
1715    global findloc findtype findtypemenu
1716    if {$findloc == "Pickaxe"} {
1717        set findtype Exact
1718        set state disabled
1719    } else {
1720        set state normal
1721    }
1722    $findtypemenu entryconf 1 -state $state
1723    $findtypemenu entryconf 2 -state $state
1724}
1725
1726proc stopfindproc {{done 0}} {
1727    global findprocpid findprocfile findids
1728    global ctext findoldcursor phase maincursor textcursor
1729    global findinprogress
1730
1731    catch {unset findids}
1732    if {[info exists findprocpid]} {
1733        if {!$done} {
1734            catch {exec kill $findprocpid}
1735        }
1736        catch {close $findprocfile}
1737        unset findprocpid
1738    }
1739    if {[info exists findinprogress]} {
1740        unset findinprogress
1741        if {$phase != "incrdraw"} {
1742            . config -cursor $maincursor
1743            settextcursor $textcursor
1744        }
1745    }
1746}
1747
1748proc findpatches {} {
1749    global findstring selectedline numcommits
1750    global findprocpid findprocfile
1751    global finddidsel ctext lineid findinprogress
1752    global findinsertpos
1753
1754    if {$numcommits == 0} return
1755
1756    # make a list of all the ids to search, starting at the one
1757    # after the selected line (if any)
1758    if {[info exists selectedline]} {
1759        set l $selectedline
1760    } else {
1761        set l -1
1762    }
1763    set inputids {}
1764    for {set i 0} {$i < $numcommits} {incr i} {
1765        if {[incr l] >= $numcommits} {
1766            set l 0
1767        }
1768        append inputids $lineid($l) "\n"
1769    }
1770
1771    if {[catch {
1772        set f [open [list | git-diff-tree --stdin -s -r -S$findstring \
1773                         << $inputids] r]
1774    } err]} {
1775        error_popup "Error starting search process: $err"
1776        return
1777    }
1778
1779    set findinsertpos end
1780    set findprocfile $f
1781    set findprocpid [pid $f]
1782    fconfigure $f -blocking 0
1783    fileevent $f readable readfindproc
1784    set finddidsel 0
1785    . config -cursor watch
1786    settextcursor watch
1787    set findinprogress 1
1788}
1789
1790proc readfindproc {} {
1791    global findprocfile finddidsel
1792    global idline matchinglines findinsertpos
1793
1794    set n [gets $findprocfile line]
1795    if {$n < 0} {
1796        if {[eof $findprocfile]} {
1797            stopfindproc 1
1798            if {!$finddidsel} {
1799                bell
1800            }
1801        }
1802        return
1803    }
1804    if {![regexp {^[0-9a-f]{40}} $line id]} {
1805        error_popup "Can't parse git-diff-tree output: $line"
1806        stopfindproc
1807        return
1808    }
1809    if {![info exists idline($id)]} {
1810        puts stderr "spurious id: $id"
1811        return
1812    }
1813    set l $idline($id)
1814    insertmatch $l $id
1815}
1816
1817proc insertmatch {l id} {
1818    global matchinglines findinsertpos finddidsel
1819
1820    if {$findinsertpos == "end"} {
1821        if {$matchinglines != {} && $l < [lindex $matchinglines 0]} {
1822            set matchinglines [linsert $matchinglines 0 $l]
1823            set findinsertpos 1
1824        } else {
1825            lappend matchinglines $l
1826        }
1827    } else {
1828        set matchinglines [linsert $matchinglines $findinsertpos $l]
1829        incr findinsertpos
1830    }
1831    markheadline $l $id
1832    if {!$finddidsel} {
1833        findselectline $l
1834        set finddidsel 1
1835    }
1836}
1837
1838proc findfiles {} {
1839    global selectedline numcommits lineid ctext
1840    global ffileline finddidsel parents nparents
1841    global findinprogress findstartline findinsertpos
1842    global treediffs fdiffids fdiffsneeded fdiffpos
1843    global findmergefiles
1844
1845    if {$numcommits == 0} return
1846
1847    if {[info exists selectedline]} {
1848        set l [expr {$selectedline + 1}]
1849    } else {
1850        set l 0
1851    }
1852    set ffileline $l
1853    set findstartline $l
1854    set diffsneeded {}
1855    set fdiffsneeded {}
1856    while 1 {
1857        set id $lineid($l)
1858        if {$findmergefiles || $nparents($id) == 1} {
1859            foreach p $parents($id) {
1860                if {![info exists treediffs([list $id $p])]} {
1861                    append diffsneeded "$id $p\n"
1862                    lappend fdiffsneeded [list $id $p]
1863                }
1864            }
1865        }
1866        if {[incr l] >= $numcommits} {
1867            set l 0
1868        }
1869        if {$l == $findstartline} break
1870    }
1871
1872    # start off a git-diff-tree process if needed
1873    if {$diffsneeded ne {}} {
1874        if {[catch {
1875            set df [open [list | git-diff-tree -r --stdin << $diffsneeded] r]
1876        } err ]} {
1877            error_popup "Error starting search process: $err"
1878            return
1879        }
1880        catch {unset fdiffids}
1881        set fdiffpos 0
1882        fconfigure $df -blocking 0
1883        fileevent $df readable [list readfilediffs $df]
1884    }
1885
1886    set finddidsel 0
1887    set findinsertpos end
1888    set id $lineid($l)
1889    set p [lindex $parents($id) 0]
1890    . config -cursor watch
1891    settextcursor watch
1892    set findinprogress 1
1893    findcont [list $id $p]
1894    update
1895}
1896
1897proc readfilediffs {df} {
1898    global findids fdiffids fdiffs
1899
1900    set n [gets $df line]
1901    if {$n < 0} {
1902        if {[eof $df]} {
1903            donefilediff
1904            if {[catch {close $df} err]} {
1905                stopfindproc
1906                bell
1907                error_popup "Error in git-diff-tree: $err"
1908            } elseif {[info exists findids]} {
1909                set ids $findids
1910                stopfindproc
1911                bell
1912                error_popup "Couldn't find diffs for {$ids}"
1913            }
1914        }
1915        return
1916    }
1917    if {[regexp {^([0-9a-f]{40}) \(from ([0-9a-f]{40})\)} $line match id p]} {
1918        # start of a new string of diffs
1919        donefilediff
1920        set fdiffids [list $id $p]
1921        set fdiffs {}
1922    } elseif {[string match ":*" $line]} {
1923        lappend fdiffs [lindex $line 5]
1924    }
1925}
1926
1927proc donefilediff {} {
1928    global fdiffids fdiffs treediffs findids
1929    global fdiffsneeded fdiffpos
1930
1931    if {[info exists fdiffids]} {
1932        while {[lindex $fdiffsneeded $fdiffpos] ne $fdiffids
1933               && $fdiffpos < [llength $fdiffsneeded]} {
1934            # git-diff-tree doesn't output anything for a commit
1935            # which doesn't change anything
1936            set nullids [lindex $fdiffsneeded $fdiffpos]
1937            set treediffs($nullids) {}
1938            if {[info exists findids] && $nullids eq $findids} {
1939                unset findids
1940                findcont $nullids
1941            }
1942            incr fdiffpos
1943        }
1944        incr fdiffpos
1945
1946        if {![info exists treediffs($fdiffids)]} {
1947            set treediffs($fdiffids) $fdiffs
1948        }
1949        if {[info exists findids] && $fdiffids eq $findids} {
1950            unset findids
1951            findcont $fdiffids
1952        }
1953    }
1954}
1955
1956proc findcont {ids} {
1957    global findids treediffs parents nparents
1958    global ffileline findstartline finddidsel
1959    global lineid numcommits matchinglines findinprogress
1960    global findmergefiles
1961
1962    set id [lindex $ids 0]
1963    set p [lindex $ids 1]
1964    set pi [lsearch -exact $parents($id) $p]
1965    set l $ffileline
1966    while 1 {
1967        if {$findmergefiles || $nparents($id) == 1} {
1968            if {![info exists treediffs($ids)]} {
1969                set findids $ids
1970                set ffileline $l
1971                return
1972            }
1973            set doesmatch 0
1974            foreach f $treediffs($ids) {
1975                set x [findmatches $f]
1976                if {$x != {}} {
1977                    set doesmatch 1
1978                    break
1979                }
1980            }
1981            if {$doesmatch} {
1982                insertmatch $l $id
1983                set pi $nparents($id)
1984            }
1985        } else {
1986            set pi $nparents($id)
1987        }
1988        if {[incr pi] >= $nparents($id)} {
1989            set pi 0
1990            if {[incr l] >= $numcommits} {
1991                set l 0
1992            }
1993            if {$l == $findstartline} break
1994            set id $lineid($l)
1995        }
1996        set p [lindex $parents($id) $pi]
1997        set ids [list $id $p]
1998    }
1999    stopfindproc
2000    if {!$finddidsel} {
2001        bell
2002    }
2003}
2004
2005# mark a commit as matching by putting a yellow background
2006# behind the headline
2007proc markheadline {l id} {
2008    global canv mainfont linehtag commitinfo
2009
2010    set bbox [$canv bbox $linehtag($l)]
2011    set t [$canv create rect $bbox -outline {} -tags matches -fill yellow]
2012    $canv lower $t
2013}
2014
2015# mark the bits of a headline, author or date that match a find string
2016proc markmatches {canv l str tag matches font} {
2017    set bbox [$canv bbox $tag]
2018    set x0 [lindex $bbox 0]
2019    set y0 [lindex $bbox 1]
2020    set y1 [lindex $bbox 3]
2021    foreach match $matches {
2022        set start [lindex $match 0]
2023        set end [lindex $match 1]
2024        if {$start > $end} continue
2025        set xoff [font measure $font [string range $str 0 [expr {$start-1}]]]
2026        set xlen [font measure $font [string range $str 0 [expr {$end}]]]
2027        set t [$canv create rect [expr {$x0+$xoff}] $y0 \
2028                   [expr {$x0+$xlen+2}] $y1 \
2029                   -outline {} -tags matches -fill yellow]
2030        $canv lower $t
2031    }
2032}
2033
2034proc unmarkmatches {} {
2035    global matchinglines findids
2036    allcanvs delete matches
2037    catch {unset matchinglines}
2038    catch {unset findids}
2039}
2040
2041proc selcanvline {w x y} {
2042    global canv canvy0 ctext linespc
2043    global lineid linehtag linentag linedtag rowtextx
2044    set ymax [lindex [$canv cget -scrollregion] 3]
2045    if {$ymax == {}} return
2046    set yfrac [lindex [$canv yview] 0]
2047    set y [expr {$y + $yfrac * $ymax}]
2048    set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
2049    if {$l < 0} {
2050        set l 0
2051    }
2052    if {$w eq $canv} {
2053        if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
2054    }
2055    unmarkmatches
2056    selectline $l 1
2057}
2058
2059proc commit_descriptor {p} {
2060    global commitinfo
2061    set l "..."
2062    if {[info exists commitinfo($p)]} {
2063        set l [lindex $commitinfo($p) 0]
2064    }
2065    return "$p ($l)"
2066}
2067
2068# append some text to the ctext widget, and make any SHA1 ID
2069# that we know about be a clickable link.
2070proc appendwithlinks {text} {
2071    global ctext idline linknum
2072
2073    set start [$ctext index "end - 1c"]
2074    $ctext insert end $text
2075    $ctext insert end "\n"
2076    set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
2077    foreach l $links {
2078        set s [lindex $l 0]
2079        set e [lindex $l 1]
2080        set linkid [string range $text $s $e]
2081        if {![info exists idline($linkid)]} continue
2082        incr e
2083        $ctext tag add link "$start + $s c" "$start + $e c"
2084        $ctext tag add link$linknum "$start + $s c" "$start + $e c"
2085        $ctext tag bind link$linknum <1> [list selectline $idline($linkid) 1]
2086        incr linknum
2087    }
2088    $ctext tag conf link -foreground blue -underline 1
2089    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
2090    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
2091}
2092
2093proc selectline {l isnew} {
2094    global canv canv2 canv3 ctext commitinfo selectedline
2095    global lineid linehtag linentag linedtag
2096    global canvy0 linespc parents nparents children
2097    global cflist currentid sha1entry
2098    global commentend idtags idline linknum
2099
2100    $canv delete hover
2101    normalline
2102    if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
2103    $canv delete secsel
2104    set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
2105               -tags secsel -fill [$canv cget -selectbackground]]
2106    $canv lower $t
2107    $canv2 delete secsel
2108    set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
2109               -tags secsel -fill [$canv2 cget -selectbackground]]
2110    $canv2 lower $t
2111    $canv3 delete secsel
2112    set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
2113               -tags secsel -fill [$canv3 cget -selectbackground]]
2114    $canv3 lower $t
2115    set y [expr {$canvy0 + $l * $linespc}]
2116    set ymax [lindex [$canv cget -scrollregion] 3]
2117    set ytop [expr {$y - $linespc - 1}]
2118    set ybot [expr {$y + $linespc + 1}]
2119    set wnow [$canv yview]
2120    set wtop [expr {[lindex $wnow 0] * $ymax}]
2121    set wbot [expr {[lindex $wnow 1] * $ymax}]
2122    set wh [expr {$wbot - $wtop}]
2123    set newtop $wtop
2124    if {$ytop < $wtop} {
2125        if {$ybot < $wtop} {
2126            set newtop [expr {$y - $wh / 2.0}]
2127        } else {
2128            set newtop $ytop
2129            if {$newtop > $wtop - $linespc} {
2130                set newtop [expr {$wtop - $linespc}]
2131            }
2132        }
2133    } elseif {$ybot > $wbot} {
2134        if {$ytop > $wbot} {
2135            set newtop [expr {$y - $wh / 2.0}]
2136        } else {
2137            set newtop [expr {$ybot - $wh}]
2138            if {$newtop < $wtop + $linespc} {
2139                set newtop [expr {$wtop + $linespc}]
2140            }
2141        }
2142    }
2143    if {$newtop != $wtop} {
2144        if {$newtop < 0} {
2145            set newtop 0
2146        }
2147        allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
2148    }
2149
2150    if {$isnew} {
2151        addtohistory [list selectline $l 0]
2152    }
2153
2154    set selectedline $l
2155
2156    set id $lineid($l)
2157    set currentid $id
2158    $sha1entry delete 0 end
2159    $sha1entry insert 0 $id
2160    $sha1entry selection from 0
2161    $sha1entry selection to end
2162
2163    $ctext conf -state normal
2164    $ctext delete 0.0 end
2165    set linknum 0
2166    $ctext mark set fmark.0 0.0
2167    $ctext mark gravity fmark.0 left
2168    set info $commitinfo($id)
2169    set date [formatdate [lindex $info 2]]
2170    $ctext insert end "Author: [lindex $info 1]  $date\n"
2171    set date [formatdate [lindex $info 4]]
2172    $ctext insert end "Committer: [lindex $info 3]  $date\n"
2173    if {[info exists idtags($id)]} {
2174        $ctext insert end "Tags:"
2175        foreach tag $idtags($id) {
2176            $ctext insert end " $tag"
2177        }
2178        $ctext insert end "\n"
2179    }
2180 
2181    set comment {}
2182    if {[info exists parents($id)]} {
2183        foreach p $parents($id) {
2184            append comment "Parent: [commit_descriptor $p]\n"
2185        }
2186    }
2187    if {[info exists children($id)]} {
2188        foreach c $children($id) {
2189            append comment "Child:  [commit_descriptor $c]\n"
2190        }
2191    }
2192    append comment "\n"
2193    append comment [lindex $info 5]
2194
2195    # make anything that looks like a SHA1 ID be a clickable link
2196    appendwithlinks $comment
2197
2198    $ctext tag delete Comments
2199    $ctext tag remove found 1.0 end
2200    $ctext conf -state disabled
2201    set commentend [$ctext index "end - 1c"]
2202
2203    $cflist delete 0 end
2204    $cflist insert end "Comments"
2205    if {$nparents($id) == 1} {
2206        startdiff $id
2207    } elseif {$nparents($id) > 1} {
2208        mergediff $id
2209    }
2210}
2211
2212proc selnextline {dir} {
2213    global selectedline
2214    if {![info exists selectedline]} return
2215    set l [expr {$selectedline + $dir}]
2216    unmarkmatches
2217    selectline $l 1
2218}
2219
2220proc unselectline {} {
2221    global selectedline
2222
2223    catch {unset selectedline}
2224    allcanvs delete secsel
2225}
2226
2227proc addtohistory {cmd} {
2228    global history historyindex
2229
2230    if {$historyindex > 0
2231        && [lindex $history [expr {$historyindex - 1}]] == $cmd} {
2232        return
2233    }
2234
2235    if {$historyindex < [llength $history]} {
2236        set history [lreplace $history $historyindex end $cmd]
2237    } else {
2238        lappend history $cmd
2239    }
2240    incr historyindex
2241    if {$historyindex > 1} {
2242        .ctop.top.bar.leftbut conf -state normal
2243    } else {
2244        .ctop.top.bar.leftbut conf -state disabled
2245    }
2246    .ctop.top.bar.rightbut conf -state disabled
2247}
2248
2249proc goback {} {
2250    global history historyindex
2251
2252    if {$historyindex > 1} {
2253        incr historyindex -1
2254        set cmd [lindex $history [expr {$historyindex - 1}]]
2255        eval $cmd
2256        .ctop.top.bar.rightbut conf -state normal
2257    }
2258    if {$historyindex <= 1} {
2259        .ctop.top.bar.leftbut conf -state disabled
2260    }
2261}
2262
2263proc goforw {} {
2264    global history historyindex
2265
2266    if {$historyindex < [llength $history]} {
2267        set cmd [lindex $history $historyindex]
2268        incr historyindex
2269        eval $cmd
2270        .ctop.top.bar.leftbut conf -state normal
2271    }
2272    if {$historyindex >= [llength $history]} {
2273        .ctop.top.bar.rightbut conf -state disabled
2274    }
2275}
2276
2277proc mergediff {id} {
2278    global parents diffmergeid diffmergegca mergefilelist diffpindex
2279
2280    set diffmergeid $id
2281    set diffpindex -1
2282    set diffmergegca [findgca $parents($id)]
2283    if {[info exists mergefilelist($id)]} {
2284        if {$mergefilelist($id) ne {}} {
2285            showmergediff
2286        }
2287    } else {
2288        contmergediff {}
2289    }
2290}
2291
2292proc findgca {ids} {
2293    set gca {}
2294    foreach id $ids {
2295        if {$gca eq {}} {
2296            set gca $id
2297        } else {
2298            if {[catch {
2299                set gca [exec git-merge-base $gca $id]
2300            } err]} {
2301                return {}
2302            }
2303        }
2304    }
2305    return $gca
2306}
2307
2308proc contmergediff {ids} {
2309    global diffmergeid diffpindex parents nparents diffmergegca
2310    global treediffs mergefilelist diffids treepending
2311
2312    # diff the child against each of the parents, and diff
2313    # each of the parents against the GCA.
2314    while 1 {
2315        if {[lindex $ids 1] == $diffmergeid && $diffmergegca ne {}} {
2316            set ids [list $diffmergegca [lindex $ids 0]]
2317        } else {
2318            if {[incr diffpindex] >= $nparents($diffmergeid)} break
2319            set p [lindex $parents($diffmergeid) $diffpindex]
2320            set ids [list $p $diffmergeid]
2321        }
2322        if {![info exists treediffs($ids)]} {
2323            set diffids $ids
2324            if {![info exists treepending]} {
2325                gettreediffs $ids
2326            }
2327            return
2328        }
2329    }
2330
2331    # If a file in some parent is different from the child and also
2332    # different from the GCA, then it's interesting.
2333    # If we don't have a GCA, then a file is interesting if it is
2334    # different from the child in all the parents.
2335    if {$diffmergegca ne {}} {
2336        set files {}
2337        foreach p $parents($diffmergeid) {
2338            set gcadiffs $treediffs([list $diffmergegca $p])
2339            foreach f $treediffs([list $p $diffmergeid]) {
2340                if {[lsearch -exact $files $f] < 0
2341                    && [lsearch -exact $gcadiffs $f] >= 0} {
2342                    lappend files $f
2343                }
2344            }
2345        }
2346        set files [lsort $files]
2347    } else {
2348        set p [lindex $parents($diffmergeid) 0]
2349        set files $treediffs([list $diffmergeid $p])
2350        for {set i 1} {$i < $nparents($diffmergeid) && $files ne {}} {incr i} {
2351            set p [lindex $parents($diffmergeid) $i]
2352            set df $treediffs([list $p $diffmergeid])
2353            set nf {}
2354            foreach f $files {
2355                if {[lsearch -exact $df $f] >= 0} {
2356                    lappend nf $f
2357                }
2358            }
2359            set files $nf
2360        }
2361    }
2362
2363    set mergefilelist($diffmergeid) $files
2364    if {$files ne {}} {
2365        showmergediff
2366    }
2367}
2368
2369proc showmergediff {} {
2370    global cflist diffmergeid mergefilelist parents
2371    global diffopts diffinhunk currentfile currenthunk filelines
2372    global diffblocked groupfilelast mergefds groupfilenum grouphunks
2373
2374    set files $mergefilelist($diffmergeid)
2375    foreach f $files {
2376        $cflist insert end $f
2377    }
2378    set env(GIT_DIFF_OPTS) $diffopts
2379    set flist {}
2380    catch {unset currentfile}
2381    catch {unset currenthunk}
2382    catch {unset filelines}
2383    catch {unset groupfilenum}
2384    catch {unset grouphunks}
2385    set groupfilelast -1
2386    foreach p $parents($diffmergeid) {
2387        set cmd [list | git-diff-tree -p $p $diffmergeid]
2388        set cmd [concat $cmd $mergefilelist($diffmergeid)]
2389        if {[catch {set f [open $cmd r]} err]} {
2390            error_popup "Error getting diffs: $err"
2391            foreach f $flist {
2392                catch {close $f}
2393            }
2394            return
2395        }
2396        lappend flist $f
2397        set ids [list $diffmergeid $p]
2398        set mergefds($ids) $f
2399        set diffinhunk($ids) 0
2400        set diffblocked($ids) 0
2401        fconfigure $f -blocking 0
2402        fileevent $f readable [list getmergediffline $f $ids $diffmergeid]
2403    }
2404}
2405
2406proc getmergediffline {f ids id} {
2407    global diffmergeid diffinhunk diffoldlines diffnewlines
2408    global currentfile currenthunk
2409    global diffoldstart diffnewstart diffoldlno diffnewlno
2410    global diffblocked mergefilelist
2411    global noldlines nnewlines difflcounts filelines
2412
2413    set n [gets $f line]
2414    if {$n < 0} {
2415        if {![eof $f]} return
2416    }
2417
2418    if {!([info exists diffmergeid] && $diffmergeid == $id)} {
2419        if {$n < 0} {
2420            close $f
2421        }
2422        return
2423    }
2424
2425    if {$diffinhunk($ids) != 0} {
2426        set fi $currentfile($ids)
2427        if {$n > 0 && [regexp {^[-+ \\]} $line match]} {
2428            # continuing an existing hunk
2429            set line [string range $line 1 end]
2430            set p [lindex $ids 1]
2431            if {$match eq "-" || $match eq " "} {
2432                set filelines($p,$fi,$diffoldlno($ids)) $line
2433                incr diffoldlno($ids)
2434            }
2435            if {$match eq "+" || $match eq " "} {
2436                set filelines($id,$fi,$diffnewlno($ids)) $line
2437                incr diffnewlno($ids)
2438            }
2439            if {$match eq " "} {
2440                if {$diffinhunk($ids) == 2} {
2441                    lappend difflcounts($ids) \
2442                        [list $noldlines($ids) $nnewlines($ids)]
2443                    set noldlines($ids) 0
2444                    set diffinhunk($ids) 1
2445                }
2446                incr noldlines($ids)
2447            } elseif {$match eq "-" || $match eq "+"} {
2448                if {$diffinhunk($ids) == 1} {
2449                    lappend difflcounts($ids) [list $noldlines($ids)]
2450                    set noldlines($ids) 0
2451                    set nnewlines($ids) 0
2452                    set diffinhunk($ids) 2
2453                }
2454                if {$match eq "-"} {
2455                    incr noldlines($ids)
2456                } else {
2457                    incr nnewlines($ids)
2458                }
2459            }
2460            # and if it's \ No newline at end of line, then what?
2461            return
2462        }
2463        # end of a hunk
2464        if {$diffinhunk($ids) == 1 && $noldlines($ids) != 0} {
2465            lappend difflcounts($ids) [list $noldlines($ids)]
2466        } elseif {$diffinhunk($ids) == 2
2467                  && ($noldlines($ids) != 0 || $nnewlines($ids) != 0)} {
2468            lappend difflcounts($ids) [list $noldlines($ids) $nnewlines($ids)]
2469        }
2470        set currenthunk($ids) [list $currentfile($ids) \
2471                                   $diffoldstart($ids) $diffnewstart($ids) \
2472                                   $diffoldlno($ids) $diffnewlno($ids) \
2473                                   $difflcounts($ids)]
2474        set diffinhunk($ids) 0
2475        # -1 = need to block, 0 = unblocked, 1 = is blocked
2476        set diffblocked($ids) -1
2477        processhunks
2478        if {$diffblocked($ids) == -1} {
2479            fileevent $f readable {}
2480            set diffblocked($ids) 1
2481        }
2482    }
2483
2484    if {$n < 0} {
2485        # eof
2486        if {!$diffblocked($ids)} {
2487            close $f
2488            set currentfile($ids) [llength $mergefilelist($diffmergeid)]
2489            set currenthunk($ids) [list $currentfile($ids) 0 0 0 0 {}]
2490            processhunks
2491        }
2492    } elseif {[regexp {^diff --git a/(.*) b/} $line match fname]} {
2493        # start of a new file
2494        set currentfile($ids) \
2495            [lsearch -exact $mergefilelist($diffmergeid) $fname]
2496    } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
2497                   $line match f1l f1c f2l f2c rest]} {
2498        if {[info exists currentfile($ids)] && $currentfile($ids) >= 0} {
2499            # start of a new hunk
2500            if {$f1l == 0 && $f1c == 0} {
2501                set f1l 1
2502            }
2503            if {$f2l == 0 && $f2c == 0} {
2504                set f2l 1
2505            }
2506            set diffinhunk($ids) 1
2507            set diffoldstart($ids) $f1l
2508            set diffnewstart($ids) $f2l
2509            set diffoldlno($ids) $f1l
2510            set diffnewlno($ids) $f2l
2511            set difflcounts($ids) {}
2512            set noldlines($ids) 0
2513            set nnewlines($ids) 0
2514        }
2515    }
2516}
2517
2518proc processhunks {} {
2519    global diffmergeid parents nparents currenthunk
2520    global mergefilelist diffblocked mergefds
2521    global grouphunks grouplinestart grouplineend groupfilenum
2522
2523    set nfiles [llength $mergefilelist($diffmergeid)]
2524    while 1 {
2525        set fi $nfiles
2526        set lno 0
2527        # look for the earliest hunk
2528        foreach p $parents($diffmergeid) {
2529            set ids [list $diffmergeid $p]
2530            if {![info exists currenthunk($ids)]} return
2531            set i [lindex $currenthunk($ids) 0]
2532            set l [lindex $currenthunk($ids) 2]
2533            if {$i < $fi || ($i == $fi && $l < $lno)} {
2534                set fi $i
2535                set lno $l
2536                set pi $p
2537            }
2538        }
2539
2540        if {$fi < $nfiles} {
2541            set ids [list $diffmergeid $pi]
2542            set hunk $currenthunk($ids)
2543            unset currenthunk($ids)
2544            if {$diffblocked($ids) > 0} {
2545                fileevent $mergefds($ids) readable \
2546                    [list getmergediffline $mergefds($ids) $ids $diffmergeid]
2547            }
2548            set diffblocked($ids) 0
2549
2550            if {[info exists groupfilenum] && $groupfilenum == $fi
2551                && $lno <= $grouplineend} {
2552                # add this hunk to the pending group
2553                lappend grouphunks($pi) $hunk
2554                set endln [lindex $hunk 4]
2555                if {$endln > $grouplineend} {
2556                    set grouplineend $endln
2557                }
2558                continue
2559            }
2560        }
2561
2562        # succeeding stuff doesn't belong in this group, so
2563        # process the group now
2564        if {[info exists groupfilenum]} {
2565            processgroup
2566            unset groupfilenum
2567            unset grouphunks
2568        }
2569
2570        if {$fi >= $nfiles} break
2571
2572        # start a new group
2573        set groupfilenum $fi
2574        set grouphunks($pi) [list $hunk]
2575        set grouplinestart $lno
2576        set grouplineend [lindex $hunk 4]
2577    }
2578}
2579
2580proc processgroup {} {
2581    global groupfilelast groupfilenum difffilestart
2582    global mergefilelist diffmergeid ctext filelines
2583    global parents diffmergeid diffoffset
2584    global grouphunks grouplinestart grouplineend nparents
2585    global mergemax
2586
2587    $ctext conf -state normal
2588    set id $diffmergeid
2589    set f $groupfilenum
2590    if {$groupfilelast != $f} {
2591        $ctext insert end "\n"
2592        set here [$ctext index "end - 1c"]
2593        set difffilestart($f) $here
2594        set mark fmark.[expr {$f + 1}]
2595        $ctext mark set $mark $here
2596        $ctext mark gravity $mark left
2597        set header [lindex $mergefilelist($id) $f]
2598        set l [expr {(78 - [string length $header]) / 2}]
2599        set pad [string range "----------------------------------------" 1 $l]
2600        $ctext insert end "$pad $header $pad\n" filesep
2601        set groupfilelast $f
2602        foreach p $parents($id) {
2603            set diffoffset($p) 0
2604        }
2605    }
2606
2607    $ctext insert end "@@" msep
2608    set nlines [expr {$grouplineend - $grouplinestart}]
2609    set events {}
2610    set pnum 0
2611    foreach p $parents($id) {
2612        set startline [expr {$grouplinestart + $diffoffset($p)}]
2613        set ol $startline
2614        set nl $grouplinestart
2615        if {[info exists grouphunks($p)]} {
2616            foreach h $grouphunks($p) {
2617                set l [lindex $h 2]
2618                if {$nl < $l} {
2619                    for {} {$nl < $l} {incr nl} {
2620                        set filelines($p,$f,$ol) $filelines($id,$f,$nl)
2621                        incr ol
2622                    }
2623                }
2624                foreach chunk [lindex $h 5] {
2625                    if {[llength $chunk] == 2} {
2626                        set olc [lindex $chunk 0]
2627                        set nlc [lindex $chunk 1]
2628                        set nnl [expr {$nl + $nlc}]
2629                        lappend events [list $nl $nnl $pnum $olc $nlc]
2630                        incr ol $olc
2631                        set nl $nnl
2632                    } else {
2633                        incr ol [lindex $chunk 0]
2634                        incr nl [lindex $chunk 0]
2635                    }
2636                }
2637            }
2638        }
2639        if {$nl < $grouplineend} {
2640            for {} {$nl < $grouplineend} {incr nl} {
2641                set filelines($p,$f,$ol) $filelines($id,$f,$nl)
2642                incr ol
2643            }
2644        }
2645        set nlines [expr {$ol - $startline}]
2646        $ctext insert end " -$startline,$nlines" msep
2647        incr pnum
2648    }
2649
2650    set nlines [expr {$grouplineend - $grouplinestart}]
2651    $ctext insert end " +$grouplinestart,$nlines @@\n" msep
2652
2653    set events [lsort -integer -index 0 $events]
2654    set nevents [llength $events]
2655    set nmerge $nparents($diffmergeid)
2656    set l $grouplinestart
2657    for {set i 0} {$i < $nevents} {set i $j} {
2658        set nl [lindex $events $i 0]
2659        while {$l < $nl} {
2660            $ctext insert end " $filelines($id,$f,$l)\n"
2661            incr l
2662        }
2663        set e [lindex $events $i]
2664        set enl [lindex $e 1]
2665        set j $i
2666        set active {}
2667        while 1 {
2668            set pnum [lindex $e 2]
2669            set olc [lindex $e 3]
2670            set nlc [lindex $e 4]
2671            if {![info exists delta($pnum)]} {
2672                set delta($pnum) [expr {$olc - $nlc}]
2673                lappend active $pnum
2674            } else {
2675                incr delta($pnum) [expr {$olc - $nlc}]
2676            }
2677            if {[incr j] >= $nevents} break
2678            set e [lindex $events $j]
2679            if {[lindex $e 0] >= $enl} break
2680            if {[lindex $e 1] > $enl} {
2681                set enl [lindex $e 1]
2682            }
2683        }
2684        set nlc [expr {$enl - $l}]
2685        set ncol mresult
2686        set bestpn -1
2687        if {[llength $active] == $nmerge - 1} {
2688            # no diff for one of the parents, i.e. it's identical
2689            for {set pnum 0} {$pnum < $nmerge} {incr pnum} {
2690                if {![info exists delta($pnum)]} {
2691                    if {$pnum < $mergemax} {
2692                        lappend ncol m$pnum
2693                    } else {
2694                        lappend ncol mmax
2695                    }
2696                    break
2697                }
2698            }
2699        } elseif {[llength $active] == $nmerge} {
2700            # all parents are different, see if one is very similar
2701            set bestsim 30
2702            for {set pnum 0} {$pnum < $nmerge} {incr pnum} {
2703                set sim [similarity $pnum $l $nlc $f \
2704                             [lrange $events $i [expr {$j-1}]]]
2705                if {$sim > $bestsim} {
2706                    set bestsim $sim
2707                    set bestpn $pnum
2708                }
2709            }
2710            if {$bestpn >= 0} {
2711                lappend ncol m$bestpn
2712            }
2713        }
2714        set pnum -1
2715        foreach p $parents($id) {
2716            incr pnum
2717            if {![info exists delta($pnum)] || $pnum == $bestpn} continue
2718            set olc [expr {$nlc + $delta($pnum)}]
2719            set ol [expr {$l + $diffoffset($p)}]
2720            incr diffoffset($p) $delta($pnum)
2721            unset delta($pnum)
2722            for {} {$olc > 0} {incr olc -1} {
2723                $ctext insert end "-$filelines($p,$f,$ol)\n" m$pnum
2724                incr ol
2725            }
2726        }
2727        set endl [expr {$l + $nlc}]
2728        if {$bestpn >= 0} {
2729            # show this pretty much as a normal diff
2730            set p [lindex $parents($id) $bestpn]
2731            set ol [expr {$l + $diffoffset($p)}]
2732            incr diffoffset($p) $delta($bestpn)
2733            unset delta($bestpn)
2734            for {set k $i} {$k < $j} {incr k} {
2735                set e [lindex $events $k]
2736                if {[lindex $e 2] != $bestpn} continue
2737                set nl [lindex $e 0]
2738                set ol [expr {$ol + $nl - $l}]
2739                for {} {$l < $nl} {incr l} {
2740                    $ctext insert end "+$filelines($id,$f,$l)\n" $ncol
2741                }
2742                set c [lindex $e 3]
2743                for {} {$c > 0} {incr c -1} {
2744                    $ctext insert end "-$filelines($p,$f,$ol)\n" m$bestpn
2745                    incr ol
2746                }
2747                set nl [lindex $e 1]
2748                for {} {$l < $nl} {incr l} {
2749                    $ctext insert end "+$filelines($id,$f,$l)\n" mresult
2750                }
2751            }
2752        }
2753        for {} {$l < $endl} {incr l} {
2754            $ctext insert end "+$filelines($id,$f,$l)\n" $ncol
2755        }
2756    }
2757    while {$l < $grouplineend} {
2758        $ctext insert end " $filelines($id,$f,$l)\n"
2759        incr l
2760    }
2761    $ctext conf -state disabled
2762}
2763
2764proc similarity {pnum l nlc f events} {
2765    global diffmergeid parents diffoffset filelines
2766
2767    set id $diffmergeid
2768    set p [lindex $parents($id) $pnum]
2769    set ol [expr {$l + $diffoffset($p)}]
2770    set endl [expr {$l + $nlc}]
2771    set same 0
2772    set diff 0
2773    foreach e $events {
2774        if {[lindex $e 2] != $pnum} continue
2775        set nl [lindex $e 0]
2776        set ol [expr {$ol + $nl - $l}]
2777        for {} {$l < $nl} {incr l} {
2778            incr same [string length $filelines($id,$f,$l)]
2779            incr same
2780        }
2781        set oc [lindex $e 3]
2782        for {} {$oc > 0} {incr oc -1} {
2783            incr diff [string length $filelines($p,$f,$ol)]
2784            incr diff
2785            incr ol
2786        }
2787        set nl [lindex $e 1]
2788        for {} {$l < $nl} {incr l} {
2789            incr diff [string length $filelines($id,$f,$l)]
2790            incr diff
2791        }
2792    }
2793    for {} {$l < $endl} {incr l} {
2794        incr same [string length $filelines($id,$f,$l)]
2795        incr same
2796    }
2797    if {$same == 0} {
2798        return 0
2799    }
2800    return [expr {200 * $same / (2 * $same + $diff)}]
2801}
2802
2803proc startdiff {ids} {
2804    global treediffs diffids treepending diffmergeid
2805
2806    set diffids $ids
2807    catch {unset diffmergeid}
2808    if {![info exists treediffs($ids)]} {
2809        if {![info exists treepending]} {
2810            gettreediffs $ids
2811        }
2812    } else {
2813        addtocflist $ids
2814    }
2815}
2816
2817proc addtocflist {ids} {
2818    global treediffs cflist
2819    foreach f $treediffs($ids) {
2820        $cflist insert end $f
2821    }
2822    getblobdiffs $ids
2823}
2824
2825proc gettreediffs {ids} {
2826    global treediff parents treepending
2827    set treepending $ids
2828    set treediff {}
2829    if [catch {set gdtf [open [concat | git-diff-tree --no-commit-id -r $ids] r]}] return
2830    fconfigure $gdtf -blocking 0
2831    fileevent $gdtf readable [list gettreediffline $gdtf $ids]
2832}
2833
2834proc gettreediffline {gdtf ids} {
2835    global treediff treediffs treepending diffids diffmergeid
2836
2837    set n [gets $gdtf line]
2838    if {$n < 0} {
2839        if {![eof $gdtf]} return
2840        close $gdtf
2841        set treediffs($ids) $treediff
2842        unset treepending
2843        if {$ids != $diffids} {
2844            gettreediffs $diffids
2845        } else {
2846            if {[info exists diffmergeid]} {
2847                contmergediff $ids
2848            } else {
2849                addtocflist $ids
2850            }
2851        }
2852        return
2853    }
2854    set file [lindex $line 5]
2855    lappend treediff $file
2856}
2857
2858proc getblobdiffs {ids} {
2859    global diffopts blobdifffd diffids env curdifftag curtagstart
2860    global difffilestart nextupdate diffinhdr treediffs
2861
2862    set env(GIT_DIFF_OPTS) $diffopts
2863    set cmd [concat | git-diff-tree --no-commit-id -r -p -C $ids]
2864    if {[catch {set bdf [open $cmd r]} err]} {
2865        puts "error getting diffs: $err"
2866        return
2867    }
2868    set diffinhdr 0
2869    fconfigure $bdf -blocking 0
2870    set blobdifffd($ids) $bdf
2871    set curdifftag Comments
2872    set curtagstart 0.0
2873    catch {unset difffilestart}
2874    fileevent $bdf readable [list getblobdiffline $bdf $diffids]
2875    set nextupdate [expr {[clock clicks -milliseconds] + 100}]
2876}
2877
2878proc getblobdiffline {bdf ids} {
2879    global diffids blobdifffd ctext curdifftag curtagstart
2880    global diffnexthead diffnextnote difffilestart
2881    global nextupdate diffinhdr treediffs
2882
2883    set n [gets $bdf line]
2884    if {$n < 0} {
2885        if {[eof $bdf]} {
2886            close $bdf
2887            if {$ids == $diffids && $bdf == $blobdifffd($ids)} {
2888                $ctext tag add $curdifftag $curtagstart end
2889            }
2890        }
2891        return
2892    }
2893    if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
2894        return
2895    }
2896    $ctext conf -state normal
2897    if {[regexp {^diff --git a/(.*) b/(.*)} $line match fname newname]} {
2898        # start of a new file
2899        $ctext insert end "\n"
2900        $ctext tag add $curdifftag $curtagstart end
2901        set curtagstart [$ctext index "end - 1c"]
2902        set header $newname
2903        set here [$ctext index "end - 1c"]
2904        set i [lsearch -exact $treediffs($diffids) $fname]
2905        if {$i >= 0} {
2906            set difffilestart($i) $here
2907            incr i
2908            $ctext mark set fmark.$i $here
2909            $ctext mark gravity fmark.$i left
2910        }
2911        if {$newname != $fname} {
2912            set i [lsearch -exact $treediffs($diffids) $newname]
2913            if {$i >= 0} {
2914                set difffilestart($i) $here
2915                incr i
2916                $ctext mark set fmark.$i $here
2917                $ctext mark gravity fmark.$i left
2918            }
2919        }
2920        set curdifftag "f:$fname"
2921        $ctext tag delete $curdifftag
2922        set l [expr {(78 - [string length $header]) / 2}]
2923        set pad [string range "----------------------------------------" 1 $l]
2924        $ctext insert end "$pad $header $pad\n" filesep
2925        set diffinhdr 1
2926    } elseif {[regexp {^(---|\+\+\+)} $line]} {
2927        set diffinhdr 0
2928    } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
2929                   $line match f1l f1c f2l f2c rest]} {
2930        $ctext insert end "$line\n" hunksep
2931        set diffinhdr 0
2932    } else {
2933        set x [string range $line 0 0]
2934        if {$x == "-" || $x == "+"} {
2935            set tag [expr {$x == "+"}]
2936            $ctext insert end "$line\n" d$tag
2937        } elseif {$x == " "} {
2938            $ctext insert end "$line\n"
2939        } elseif {$diffinhdr || $x == "\\"} {
2940            # e.g. "\ No newline at end of file"
2941            $ctext insert end "$line\n" filesep
2942        } else {
2943            # Something else we don't recognize
2944            if {$curdifftag != "Comments"} {
2945                $ctext insert end "\n"
2946                $ctext tag add $curdifftag $curtagstart end
2947                set curtagstart [$ctext index "end - 1c"]
2948                set curdifftag Comments
2949            }
2950            $ctext insert end "$line\n" filesep
2951        }
2952    }
2953    $ctext conf -state disabled
2954    if {[clock clicks -milliseconds] >= $nextupdate} {
2955        incr nextupdate 100
2956        fileevent $bdf readable {}
2957        update
2958        fileevent $bdf readable "getblobdiffline $bdf {$ids}"
2959    }
2960}
2961
2962proc nextfile {} {
2963    global difffilestart ctext
2964    set here [$ctext index @0,0]
2965    for {set i 0} {[info exists difffilestart($i)]} {incr i} {
2966        if {[$ctext compare $difffilestart($i) > $here]} {
2967            if {![info exists pos]
2968                || [$ctext compare $difffilestart($i) < $pos]} {
2969                set pos $difffilestart($i)
2970            }
2971        }
2972    }
2973    if {[info exists pos]} {
2974        $ctext yview $pos
2975    }
2976}
2977
2978proc listboxsel {} {
2979    global ctext cflist currentid
2980    if {![info exists currentid]} return
2981    set sel [lsort [$cflist curselection]]
2982    if {$sel eq {}} return
2983    set first [lindex $sel 0]
2984    catch {$ctext yview fmark.$first}
2985}
2986
2987proc setcoords {} {
2988    global linespc charspc canvx0 canvy0 mainfont
2989    global xspc1 xspc2 lthickness
2990
2991    set linespc [font metrics $mainfont -linespace]
2992    set charspc [font measure $mainfont "m"]
2993    set canvy0 [expr {3 + 0.5 * $linespc}]
2994    set canvx0 [expr {3 + 0.5 * $linespc}]
2995    set lthickness [expr {int($linespc / 9) + 1}]
2996    set xspc1(0) $linespc
2997    set xspc2 $linespc
2998}
2999
3000proc redisplay {} {
3001    global stopped redisplaying phase
3002    if {$stopped > 1} return
3003    if {$phase == "getcommits"} return
3004    set redisplaying 1
3005    if {$phase == "drawgraph" || $phase == "incrdraw"} {
3006        set stopped 1
3007    } else {
3008        drawgraph
3009    }
3010}
3011
3012proc incrfont {inc} {
3013    global mainfont namefont textfont ctext canv phase
3014    global stopped entries
3015    unmarkmatches
3016    set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
3017    set namefont [lreplace $namefont 1 1 [expr {[lindex $namefont 1] + $inc}]]
3018    set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
3019    setcoords
3020    $ctext conf -font $textfont
3021    $ctext tag conf filesep -font [concat $textfont bold]
3022    foreach e $entries {
3023        $e conf -font $mainfont
3024    }
3025    if {$phase == "getcommits"} {
3026        $canv itemconf textitems -font $mainfont
3027    }
3028    redisplay
3029}
3030
3031proc clearsha1 {} {
3032    global sha1entry sha1string
3033    if {[string length $sha1string] == 40} {
3034        $sha1entry delete 0 end
3035    }
3036}
3037
3038proc sha1change {n1 n2 op} {
3039    global sha1string currentid sha1but
3040    if {$sha1string == {}
3041        || ([info exists currentid] && $sha1string == $currentid)} {
3042        set state disabled
3043    } else {
3044        set state normal
3045    }
3046    if {[$sha1but cget -state] == $state} return
3047    if {$state == "normal"} {
3048        $sha1but conf -state normal -relief raised -text "Goto: "
3049    } else {
3050        $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
3051    }
3052}
3053
3054proc gotocommit {} {
3055    global sha1string currentid idline tagids
3056    global lineid numcommits
3057
3058    if {$sha1string == {}
3059        || ([info exists currentid] && $sha1string == $currentid)} return
3060    if {[info exists tagids($sha1string)]} {
3061        set id $tagids($sha1string)
3062    } else {
3063        set id [string tolower $sha1string]
3064        if {[regexp {^[0-9a-f]{4,39}$} $id]} {
3065            set matches {}
3066            for {set l 0} {$l < $numcommits} {incr l} {
3067                if {[string match $id* $lineid($l)]} {
3068                    lappend matches $lineid($l)
3069                }
3070            }
3071            if {$matches ne {}} {
3072                if {[llength $matches] > 1} {
3073                    error_popup "Short SHA1 id $id is ambiguous"
3074                    return
3075                }
3076                set id [lindex $matches 0]
3077            }
3078        }
3079    }
3080    if {[info exists idline($id)]} {
3081        selectline $idline($id) 1
3082        return
3083    }
3084    if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
3085        set type "SHA1 id"
3086    } else {
3087        set type "Tag"
3088    }
3089    error_popup "$type $sha1string is not known"
3090}
3091
3092proc lineenter {x y id} {
3093    global hoverx hovery hoverid hovertimer
3094    global commitinfo canv
3095
3096    if {![info exists commitinfo($id)]} return
3097    set hoverx $x
3098    set hovery $y
3099    set hoverid $id
3100    if {[info exists hovertimer]} {
3101        after cancel $hovertimer
3102    }
3103    set hovertimer [after 500 linehover]
3104    $canv delete hover
3105}
3106
3107proc linemotion {x y id} {
3108    global hoverx hovery hoverid hovertimer
3109
3110    if {[info exists hoverid] && $id == $hoverid} {
3111        set hoverx $x
3112        set hovery $y
3113        if {[info exists hovertimer]} {
3114            after cancel $hovertimer
3115        }
3116        set hovertimer [after 500 linehover]
3117    }
3118}
3119
3120proc lineleave {id} {
3121    global hoverid hovertimer canv
3122
3123    if {[info exists hoverid] && $id == $hoverid} {
3124        $canv delete hover
3125        if {[info exists hovertimer]} {
3126            after cancel $hovertimer
3127            unset hovertimer
3128        }
3129        unset hoverid
3130    }
3131}
3132
3133proc linehover {} {
3134    global hoverx hovery hoverid hovertimer
3135    global canv linespc lthickness
3136    global commitinfo mainfont
3137
3138    set text [lindex $commitinfo($hoverid) 0]
3139    set ymax [lindex [$canv cget -scrollregion] 3]
3140    if {$ymax == {}} return
3141    set yfrac [lindex [$canv yview] 0]
3142    set x [expr {$hoverx + 2 * $linespc}]
3143    set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
3144    set x0 [expr {$x - 2 * $lthickness}]
3145    set y0 [expr {$y - 2 * $lthickness}]
3146    set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}]
3147    set y1 [expr {$y + $linespc + 2 * $lthickness}]
3148    set t [$canv create rectangle $x0 $y0 $x1 $y1 \
3149               -fill \#ffff80 -outline black -width 1 -tags hover]
3150    $canv raise $t
3151    set t [$canv create text $x $y -anchor nw -text $text -tags hover -font $mainfont]
3152    $canv raise $t
3153}
3154
3155proc clickisonarrow {id y} {
3156    global mainline mainlinearrow sidelines lthickness
3157
3158    set thresh [expr {2 * $lthickness + 6}]
3159    if {[info exists mainline($id)]} {
3160        if {$mainlinearrow($id) ne "none"} {
3161            if {abs([lindex $mainline($id) 1] - $y) < $thresh} {
3162                return "up"
3163            }
3164        }
3165    }
3166    if {[info exists sidelines($id)]} {
3167        foreach ls $sidelines($id) {
3168            set coords [lindex $ls 0]
3169            set arrow [lindex $ls 2]
3170            if {$arrow eq "first" || $arrow eq "both"} {
3171                if {abs([lindex $coords 1] - $y) < $thresh} {
3172                    return "up"
3173                }
3174            }
3175            if {$arrow eq "last" || $arrow eq "both"} {
3176                if {abs([lindex $coords end] - $y) < $thresh} {
3177                    return "down"
3178                }
3179            }
3180        }
3181    }
3182    return {}
3183}
3184
3185proc arrowjump {id dirn y} {
3186    global mainline sidelines canv canv2 canv3
3187
3188    set yt {}
3189    if {$dirn eq "down"} {
3190        if {[info exists mainline($id)]} {
3191            set y1 [lindex $mainline($id) 1]
3192            if {$y1 > $y} {
3193                set yt $y1
3194            }
3195        }
3196        if {[info exists sidelines($id)]} {
3197            foreach ls $sidelines($id) {
3198                set y1 [lindex $ls 0 1]
3199                if {$y1 > $y && ($yt eq {} || $y1 < $yt)} {
3200                    set yt $y1
3201                }
3202            }
3203        }
3204    } else {
3205        if {[info exists sidelines($id)]} {
3206            foreach ls $sidelines($id) {
3207                set y1 [lindex $ls 0 end]
3208                if {$y1 < $y && ($yt eq {} || $y1 > $yt)} {
3209                    set yt $y1
3210                }
3211            }
3212        }
3213    }
3214    if {$yt eq {}} return
3215    set ymax [lindex [$canv cget -scrollregion] 3]
3216    if {$ymax eq {} || $ymax <= 0} return
3217    set view [$canv yview]
3218    set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
3219    set yfrac [expr {$yt / $ymax - $yspan / 2}]
3220    if {$yfrac < 0} {
3221        set yfrac 0
3222    }
3223    $canv yview moveto $yfrac
3224    $canv2 yview moveto $yfrac
3225    $canv3 yview moveto $yfrac
3226}
3227
3228proc lineclick {x y id isnew} {
3229    global ctext commitinfo children cflist canv thickerline
3230
3231    unmarkmatches
3232    unselectline
3233    normalline
3234    $canv delete hover
3235    # draw this line thicker than normal
3236    drawlines $id 1 1
3237    set thickerline $id
3238    if {$isnew} {
3239        set ymax [lindex [$canv cget -scrollregion] 3]
3240        if {$ymax eq {}} return
3241        set yfrac [lindex [$canv yview] 0]
3242        set y [expr {$y + $yfrac * $ymax}]
3243    }
3244    set dirn [clickisonarrow $id $y]
3245    if {$dirn ne {}} {
3246        arrowjump $id $dirn $y
3247        return
3248    }
3249
3250    if {$isnew} {
3251        addtohistory [list lineclick $x $y $id 0]
3252    }
3253    # fill the details pane with info about this line
3254    $ctext conf -state normal
3255    $ctext delete 0.0 end
3256    $ctext tag conf link -foreground blue -underline 1
3257    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
3258    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
3259    $ctext insert end "Parent:\t"
3260    $ctext insert end $id [list link link0]
3261    $ctext tag bind link0 <1> [list selbyid $id]
3262    set info $commitinfo($id)
3263    $ctext insert end "\n\t[lindex $info 0]\n"
3264    $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
3265    set date [formatdate [lindex $info 2]]
3266    $ctext insert end "\tDate:\t$date\n"
3267    if {[info exists children($id)]} {
3268        $ctext insert end "\nChildren:"
3269        set i 0
3270        foreach child $children($id) {
3271            incr i
3272            set info $commitinfo($child)
3273            $ctext insert end "\n\t"
3274            $ctext insert end $child [list link link$i]
3275            $ctext tag bind link$i <1> [list selbyid $child]
3276            $ctext insert end "\n\t[lindex $info 0]"
3277            $ctext insert end "\n\tAuthor:\t[lindex $info 1]"
3278            set date [formatdate [lindex $info 2]]
3279            $ctext insert end "\n\tDate:\t$date\n"
3280        }
3281    }
3282    $ctext conf -state disabled
3283
3284    $cflist delete 0 end
3285}
3286
3287proc normalline {} {
3288    global thickerline
3289    if {[info exists thickerline]} {
3290        drawlines $thickerline 0 1
3291        unset thickerline
3292    }
3293}
3294
3295proc selbyid {id} {
3296    global idline
3297    if {[info exists idline($id)]} {
3298        selectline $idline($id) 1
3299    }
3300}
3301
3302proc mstime {} {
3303    global startmstime
3304    if {![info exists startmstime]} {
3305        set startmstime [clock clicks -milliseconds]
3306    }
3307    return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
3308}
3309
3310proc rowmenu {x y id} {
3311    global rowctxmenu idline selectedline rowmenuid
3312
3313    if {![info exists selectedline] || $idline($id) eq $selectedline} {
3314        set state disabled
3315    } else {
3316        set state normal
3317    }
3318    $rowctxmenu entryconfigure 0 -state $state
3319    $rowctxmenu entryconfigure 1 -state $state
3320    $rowctxmenu entryconfigure 2 -state $state
3321    set rowmenuid $id
3322    tk_popup $rowctxmenu $x $y
3323}
3324
3325proc diffvssel {dirn} {
3326    global rowmenuid selectedline lineid
3327
3328    if {![info exists selectedline]} return
3329    if {$dirn} {
3330        set oldid $lineid($selectedline)
3331        set newid $rowmenuid
3332    } else {
3333        set oldid $rowmenuid
3334        set newid $lineid($selectedline)
3335    }
3336    addtohistory [list doseldiff $oldid $newid]
3337    doseldiff $oldid $newid
3338}
3339
3340proc doseldiff {oldid newid} {
3341    global ctext cflist
3342    global commitinfo
3343
3344    $ctext conf -state normal
3345    $ctext delete 0.0 end
3346    $ctext mark set fmark.0 0.0
3347    $ctext mark gravity fmark.0 left
3348    $cflist delete 0 end
3349    $cflist insert end "Top"
3350    $ctext insert end "From "
3351    $ctext tag conf link -foreground blue -underline 1
3352    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
3353    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
3354    $ctext tag bind link0 <1> [list selbyid $oldid]
3355    $ctext insert end $oldid [list link link0]
3356    $ctext insert end "\n     "
3357    $ctext insert end [lindex $commitinfo($oldid) 0]
3358    $ctext insert end "\n\nTo   "
3359    $ctext tag bind link1 <1> [list selbyid $newid]
3360    $ctext insert end $newid [list link link1]
3361    $ctext insert end "\n     "
3362    $ctext insert end [lindex $commitinfo($newid) 0]
3363    $ctext insert end "\n"
3364    $ctext conf -state disabled
3365    $ctext tag delete Comments
3366    $ctext tag remove found 1.0 end
3367    startdiff [list $oldid $newid]
3368}
3369
3370proc mkpatch {} {
3371    global rowmenuid currentid commitinfo patchtop patchnum
3372
3373    if {![info exists currentid]} return
3374    set oldid $currentid
3375    set oldhead [lindex $commitinfo($oldid) 0]
3376    set newid $rowmenuid
3377    set newhead [lindex $commitinfo($newid) 0]
3378    set top .patch
3379    set patchtop $top
3380    catch {destroy $top}
3381    toplevel $top
3382    label $top.title -text "Generate patch"
3383    grid $top.title - -pady 10
3384    label $top.from -text "From:"
3385    entry $top.fromsha1 -width 40 -relief flat
3386    $top.fromsha1 insert 0 $oldid
3387    $top.fromsha1 conf -state readonly
3388    grid $top.from $top.fromsha1 -sticky w
3389    entry $top.fromhead -width 60 -relief flat
3390    $top.fromhead insert 0 $oldhead
3391    $top.fromhead conf -state readonly
3392    grid x $top.fromhead -sticky w
3393    label $top.to -text "To:"
3394    entry $top.tosha1 -width 40 -relief flat
3395    $top.tosha1 insert 0 $newid
3396    $top.tosha1 conf -state readonly
3397    grid $top.to $top.tosha1 -sticky w
3398    entry $top.tohead -width 60 -relief flat
3399    $top.tohead insert 0 $newhead
3400    $top.tohead conf -state readonly
3401    grid x $top.tohead -sticky w
3402    button $top.rev -text "Reverse" -command mkpatchrev -padx 5
3403    grid $top.rev x -pady 10
3404    label $top.flab -text "Output file:"
3405    entry $top.fname -width 60
3406    $top.fname insert 0 [file normalize "patch$patchnum.patch"]
3407    incr patchnum
3408    grid $top.flab $top.fname -sticky w
3409    frame $top.buts
3410    button $top.buts.gen -text "Generate" -command mkpatchgo
3411    button $top.buts.can -text "Cancel" -command mkpatchcan
3412    grid $top.buts.gen $top.buts.can
3413    grid columnconfigure $top.buts 0 -weight 1 -uniform a
3414    grid columnconfigure $top.buts 1 -weight 1 -uniform a
3415    grid $top.buts - -pady 10 -sticky ew
3416    focus $top.fname
3417}
3418
3419proc mkpatchrev {} {
3420    global patchtop
3421
3422    set oldid [$patchtop.fromsha1 get]
3423    set oldhead [$patchtop.fromhead get]
3424    set newid [$patchtop.tosha1 get]
3425    set newhead [$patchtop.tohead get]
3426    foreach e [list fromsha1 fromhead tosha1 tohead] \
3427            v [list $newid $newhead $oldid $oldhead] {
3428        $patchtop.$e conf -state normal
3429        $patchtop.$e delete 0 end
3430        $patchtop.$e insert 0 $v
3431        $patchtop.$e conf -state readonly
3432    }
3433}
3434
3435proc mkpatchgo {} {
3436    global patchtop
3437
3438    set oldid [$patchtop.fromsha1 get]
3439    set newid [$patchtop.tosha1 get]
3440    set fname [$patchtop.fname get]
3441    if {[catch {exec git-diff-tree -p $oldid $newid >$fname &} err]} {
3442        error_popup "Error creating patch: $err"
3443    }
3444    catch {destroy $patchtop}
3445    unset patchtop
3446}
3447
3448proc mkpatchcan {} {
3449    global patchtop
3450
3451    catch {destroy $patchtop}
3452    unset patchtop
3453}
3454
3455proc mktag {} {
3456    global rowmenuid mktagtop commitinfo
3457
3458    set top .maketag
3459    set mktagtop $top
3460    catch {destroy $top}
3461    toplevel $top
3462    label $top.title -text "Create tag"
3463    grid $top.title - -pady 10
3464    label $top.id -text "ID:"
3465    entry $top.sha1 -width 40 -relief flat
3466    $top.sha1 insert 0 $rowmenuid
3467    $top.sha1 conf -state readonly
3468    grid $top.id $top.sha1 -sticky w
3469    entry $top.head -width 60 -relief flat
3470    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
3471    $top.head conf -state readonly
3472    grid x $top.head -sticky w
3473    label $top.tlab -text "Tag name:"
3474    entry $top.tag -width 60
3475    grid $top.tlab $top.tag -sticky w
3476    frame $top.buts
3477    button $top.buts.gen -text "Create" -command mktaggo
3478    button $top.buts.can -text "Cancel" -command mktagcan
3479    grid $top.buts.gen $top.buts.can
3480    grid columnconfigure $top.buts 0 -weight 1 -uniform a
3481    grid columnconfigure $top.buts 1 -weight 1 -uniform a
3482    grid $top.buts - -pady 10 -sticky ew
3483    focus $top.tag
3484}
3485
3486proc domktag {} {
3487    global mktagtop env tagids idtags
3488
3489    set id [$mktagtop.sha1 get]
3490    set tag [$mktagtop.tag get]
3491    if {$tag == {}} {
3492        error_popup "No tag name specified"
3493        return
3494    }
3495    if {[info exists tagids($tag)]} {
3496        error_popup "Tag \"$tag\" already exists"
3497        return
3498    }
3499    if {[catch {
3500        set dir [gitdir]
3501        set fname [file join $dir "refs/tags" $tag]
3502        set f [open $fname w]
3503        puts $f $id
3504        close $f
3505    } err]} {
3506        error_popup "Error creating tag: $err"
3507        return
3508    }
3509
3510    set tagids($tag) $id
3511    lappend idtags($id) $tag
3512    redrawtags $id
3513}
3514
3515proc redrawtags {id} {
3516    global canv linehtag idline idpos selectedline
3517
3518    if {![info exists idline($id)]} return
3519    $canv delete tag.$id
3520    set xt [eval drawtags $id $idpos($id)]
3521    $canv coords $linehtag($idline($id)) $xt [lindex $idpos($id) 2]
3522    if {[info exists selectedline] && $selectedline == $idline($id)} {
3523        selectline $selectedline 0
3524    }
3525}
3526
3527proc mktagcan {} {
3528    global mktagtop
3529
3530    catch {destroy $mktagtop}
3531    unset mktagtop
3532}
3533
3534proc mktaggo {} {
3535    domktag
3536    mktagcan
3537}
3538
3539proc writecommit {} {
3540    global rowmenuid wrcomtop commitinfo wrcomcmd
3541
3542    set top .writecommit
3543    set wrcomtop $top
3544    catch {destroy $top}
3545    toplevel $top
3546    label $top.title -text "Write commit to file"
3547    grid $top.title - -pady 10
3548    label $top.id -text "ID:"
3549    entry $top.sha1 -width 40 -relief flat
3550    $top.sha1 insert 0 $rowmenuid
3551    $top.sha1 conf -state readonly
3552    grid $top.id $top.sha1 -sticky w
3553    entry $top.head -width 60 -relief flat
3554    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
3555    $top.head conf -state readonly
3556    grid x $top.head -sticky w
3557    label $top.clab -text "Command:"
3558    entry $top.cmd -width 60 -textvariable wrcomcmd
3559    grid $top.clab $top.cmd -sticky w -pady 10
3560    label $top.flab -text "Output file:"
3561    entry $top.fname -width 60
3562    $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
3563    grid $top.flab $top.fname -sticky w
3564    frame $top.buts
3565    button $top.buts.gen -text "Write" -command wrcomgo
3566    button $top.buts.can -text "Cancel" -command wrcomcan
3567    grid $top.buts.gen $top.buts.can
3568    grid columnconfigure $top.buts 0 -weight 1 -uniform a
3569    grid columnconfigure $top.buts 1 -weight 1 -uniform a
3570    grid $top.buts - -pady 10 -sticky ew
3571    focus $top.fname
3572}
3573
3574proc wrcomgo {} {
3575    global wrcomtop
3576
3577    set id [$wrcomtop.sha1 get]
3578    set cmd "echo $id | [$wrcomtop.cmd get]"
3579    set fname [$wrcomtop.fname get]
3580    if {[catch {exec sh -c $cmd >$fname &} err]} {
3581        error_popup "Error writing commit: $err"
3582    }
3583    catch {destroy $wrcomtop}
3584    unset wrcomtop
3585}
3586
3587proc wrcomcan {} {
3588    global wrcomtop
3589
3590    catch {destroy $wrcomtop}
3591    unset wrcomtop
3592}
3593
3594proc listrefs {id} {
3595    global idtags idheads idotherrefs
3596
3597    set x {}
3598    if {[info exists idtags($id)]} {
3599        set x $idtags($id)
3600    }
3601    set y {}
3602    if {[info exists idheads($id)]} {
3603        set y $idheads($id)
3604    }
3605    set z {}
3606    if {[info exists idotherrefs($id)]} {
3607        set z $idotherrefs($id)
3608    }
3609    return [list $x $y $z]
3610}
3611
3612proc rereadrefs {} {
3613    global idtags idheads idotherrefs
3614    global tagids headids otherrefids
3615
3616    set refids [concat [array names idtags] \
3617                    [array names idheads] [array names idotherrefs]]
3618    foreach id $refids {
3619        if {![info exists ref($id)]} {
3620            set ref($id) [listrefs $id]
3621        }
3622    }
3623    readrefs
3624    set refids [lsort -unique [concat $refids [array names idtags] \
3625                        [array names idheads] [array names idotherrefs]]]
3626    foreach id $refids {
3627        set v [listrefs $id]
3628        if {![info exists ref($id)] || $ref($id) != $v} {
3629            redrawtags $id
3630        }
3631    }
3632}
3633
3634proc updatecommits {rargs} {
3635    global commitlisted commfd phase
3636    global startmsecs nextupdate ncmupdate
3637    global idtags idheads idotherrefs
3638    global leftover
3639    global parsed_args
3640    global canv
3641    global oldcommits commits
3642    global parents nchildren children ncleft
3643
3644    set old_args $parsed_args
3645    parse_args $rargs
3646
3647    foreach id $old_args {
3648        if {![regexp {^[0-9a-f]{40}$} $id]} continue
3649        if {[info exists oldref($id)]} continue
3650        set oldref($id) $id
3651        lappend ignoreold "^$id"
3652    }
3653    foreach id $parsed_args {
3654        if {![regexp {^[0-9a-f]{40}$} $id]} continue
3655        if {[info exists ref($id)]} continue
3656        set ref($id) $id
3657        lappend ignorenew "^$id"
3658    }
3659
3660    foreach a $old_args {
3661        if {![info exists ref($a)]} {
3662            lappend ignorenew $a
3663        }
3664    }
3665
3666    set phase updatecommits
3667    set removed_commits [split [eval exec git-rev-list $ignorenew] "\n" ]
3668    if {[llength $removed_commits] > 0} {
3669        $canv delete all
3670        set oldcommits {}
3671        foreach c $commits {
3672            if {[lsearch $c $removed_commits] < 0} {
3673                lappend oldcommits $c
3674            } else {
3675                unset commitlisted($c)
3676            }
3677        }
3678        set commits {}
3679        unset children
3680        unset nchildren
3681        set phase removecommits
3682    }
3683
3684    set args {}
3685    foreach a $parsed_args {
3686        if {![info exists oldref($a)]} {
3687            lappend args $a
3688        }
3689    }
3690
3691    readrefs
3692    start_rev_list [concat $ignoreold $args]
3693}
3694
3695proc showtag {tag isnew} {
3696    global ctext cflist tagcontents tagids linknum
3697
3698    if {$isnew} {
3699        addtohistory [list showtag $tag 0]
3700    }
3701    $ctext conf -state normal
3702    $ctext delete 0.0 end
3703    set linknum 0
3704    if {[info exists tagcontents($tag)]} {
3705        set text $tagcontents($tag)
3706    } else {
3707        set text "Tag: $tag\nId:  $tagids($tag)"
3708    }
3709    appendwithlinks $text
3710    $ctext conf -state disabled
3711    $cflist delete 0 end
3712}
3713
3714proc doquit {} {
3715    global stopped
3716    set stopped 100
3717    destroy .
3718}
3719
3720proc doprefs {} {
3721    global maxwidth maxgraphpct diffopts findmergefiles
3722    global oldprefs prefstop
3723
3724    set top .gitkprefs
3725    set prefstop $top
3726    if {[winfo exists $top]} {
3727        raise $top
3728        return
3729    }
3730    foreach v {maxwidth maxgraphpct diffopts findmergefiles} {
3731        set oldprefs($v) [set $v]
3732    }
3733    toplevel $top
3734    wm title $top "Gitk preferences"
3735    label $top.ldisp -text "Commit list display options"
3736    grid $top.ldisp - -sticky w -pady 10
3737    label $top.spacer -text " "
3738    label $top.maxwidthl -text "Maximum graph width (lines)" \
3739        -font optionfont
3740    spinbox $top.maxwidth -from 0 -to 100 -width 4 -textvariable maxwidth
3741    grid $top.spacer $top.maxwidthl $top.maxwidth -sticky w
3742    label $top.maxpctl -text "Maximum graph width (% of pane)" \
3743        -font optionfont
3744    spinbox $top.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct
3745    grid x $top.maxpctl $top.maxpct -sticky w
3746    checkbutton $top.findm -variable findmergefiles
3747    label $top.findml -text "Include merges for \"Find\" in \"Files\"" \
3748        -font optionfont
3749    grid $top.findm $top.findml - -sticky w
3750    label $top.ddisp -text "Diff display options"
3751    grid $top.ddisp - -sticky w -pady 10
3752    label $top.diffoptl -text "Options for diff program" \
3753        -font optionfont
3754    entry $top.diffopt -width 20 -textvariable diffopts
3755    grid x $top.diffoptl $top.diffopt -sticky w
3756    frame $top.buts
3757    button $top.buts.ok -text "OK" -command prefsok
3758    button $top.buts.can -text "Cancel" -command prefscan
3759    grid $top.buts.ok $top.buts.can
3760    grid columnconfigure $top.buts 0 -weight 1 -uniform a
3761    grid columnconfigure $top.buts 1 -weight 1 -uniform a
3762    grid $top.buts - - -pady 10 -sticky ew
3763}
3764
3765proc prefscan {} {
3766    global maxwidth maxgraphpct diffopts findmergefiles
3767    global oldprefs prefstop
3768
3769    foreach v {maxwidth maxgraphpct diffopts findmergefiles} {
3770        set $v $oldprefs($v)
3771    }
3772    catch {destroy $prefstop}
3773    unset prefstop
3774}
3775
3776proc prefsok {} {
3777    global maxwidth maxgraphpct
3778    global oldprefs prefstop
3779
3780    catch {destroy $prefstop}
3781    unset prefstop
3782    if {$maxwidth != $oldprefs(maxwidth)
3783        || $maxgraphpct != $oldprefs(maxgraphpct)} {
3784        redisplay
3785    }
3786}
3787
3788proc formatdate {d} {
3789    return [clock format $d -format "%Y-%m-%d %H:%M:%S"]
3790}
3791
3792# defaults...
3793set datemode 0
3794set diffopts "-U 5 -p"
3795set wrcomcmd "git-diff-tree --stdin -p --pretty"
3796
3797set gitencoding ""
3798catch {
3799    set gitencoding [exec git-repo-config --get i18n.commitencoding]
3800}
3801if {$gitencoding == ""} {
3802        set gitencoding "utf-8"
3803}
3804
3805set mainfont {Helvetica 9}
3806set textfont {Courier 9}
3807set findmergefiles 0
3808set maxgraphpct 50
3809set maxwidth 16
3810set revlistorder 0
3811set fastdate 0
3812
3813set colors {green red blue magenta darkgrey brown orange}
3814
3815catch {source ~/.gitk}
3816
3817set namefont $mainfont
3818
3819font create optionfont -family sans-serif -size -12
3820
3821set revtreeargs {}
3822foreach arg $argv {
3823    switch -regexp -- $arg {
3824        "^$" { }
3825        "^-d" { set datemode 1 }
3826        "^-r" { set revlistorder 1 }
3827        default {
3828            lappend revtreeargs $arg
3829        }
3830    }
3831}
3832
3833set history {}
3834set historyindex 0
3835
3836set stopped 0
3837set redisplaying 0
3838set stuffsaved 0
3839set patchnum 0
3840setcoords
3841makewindow $revtreeargs
3842readrefs
3843getcommits $revtreeargs