gitkon commit gitk: Fix bug introduced in commit 67a4f1a7 (7b459a1)
   1#!/bin/sh
   2# Tcl ignores the next line -*- tcl -*- \
   3exec wish "$0" -- "$@"
   4
   5# Copyright (C) 2005-2006 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 [exec git rev-parse --git-dir]
  16    }
  17}
  18
  19# A simple scheduler for compute-intensive stuff.
  20# The aim is to make sure that event handlers for GUI actions can
  21# run at least every 50-100 ms.  Unfortunately fileevent handlers are
  22# run before X event handlers, so reading from a fast source can
  23# make the GUI completely unresponsive.
  24proc run args {
  25    global isonrunq runq
  26
  27    set script $args
  28    if {[info exists isonrunq($script)]} return
  29    if {$runq eq {}} {
  30        after idle dorunq
  31    }
  32    lappend runq [list {} $script]
  33    set isonrunq($script) 1
  34}
  35
  36proc filerun {fd script} {
  37    fileevent $fd readable [list filereadable $fd $script]
  38}
  39
  40proc filereadable {fd script} {
  41    global runq
  42
  43    fileevent $fd readable {}
  44    if {$runq eq {}} {
  45        after idle dorunq
  46    }
  47    lappend runq [list $fd $script]
  48}
  49
  50proc dorunq {} {
  51    global isonrunq runq
  52
  53    set tstart [clock clicks -milliseconds]
  54    set t0 $tstart
  55    while {$runq ne {}} {
  56        set fd [lindex $runq 0 0]
  57        set script [lindex $runq 0 1]
  58        set repeat [eval $script]
  59        set t1 [clock clicks -milliseconds]
  60        set t [expr {$t1 - $t0}]
  61        set runq [lrange $runq 1 end]
  62        if {$repeat ne {} && $repeat} {
  63            if {$fd eq {} || $repeat == 2} {
  64                # script returns 1 if it wants to be readded
  65                # file readers return 2 if they could do more straight away
  66                lappend runq [list $fd $script]
  67            } else {
  68                fileevent $fd readable [list filereadable $fd $script]
  69            }
  70        } elseif {$fd eq {}} {
  71            unset isonrunq($script)
  72        }
  73        set t0 $t1
  74        if {$t1 - $tstart >= 80} break
  75    }
  76    if {$runq ne {}} {
  77        after idle dorunq
  78    }
  79}
  80
  81# Start off a git rev-list process and arrange to read its output
  82proc start_rev_list {view} {
  83    global startmsecs
  84    global commfd leftover tclencoding datemode
  85    global viewargs viewfiles commitidx
  86    global lookingforhead showlocalchanges
  87
  88    set startmsecs [clock clicks -milliseconds]
  89    set commitidx($view) 0
  90    set order "--topo-order"
  91    if {$datemode} {
  92        set order "--date-order"
  93    }
  94    if {[catch {
  95        set fd [open [concat | git log -z --pretty=raw $order --parents \
  96                         --boundary $viewargs($view) "--" $viewfiles($view)] r]
  97    } err]} {
  98        error_popup "Error executing git rev-list: $err"
  99        exit 1
 100    }
 101    set commfd($view) $fd
 102    set leftover($view) {}
 103    set lookingforhead $showlocalchanges
 104    fconfigure $fd -blocking 0 -translation lf -eofchar {}
 105    if {$tclencoding != {}} {
 106        fconfigure $fd -encoding $tclencoding
 107    }
 108    filerun $fd [list getcommitlines $fd $view]
 109    nowbusy $view
 110}
 111
 112proc stop_rev_list {} {
 113    global commfd curview
 114
 115    if {![info exists commfd($curview)]} return
 116    set fd $commfd($curview)
 117    catch {
 118        set pid [pid $fd]
 119        exec kill $pid
 120    }
 121    catch {close $fd}
 122    unset commfd($curview)
 123}
 124
 125proc getcommits {} {
 126    global phase canv mainfont curview
 127
 128    set phase getcommits
 129    initlayout
 130    start_rev_list $curview
 131    show_status "Reading commits..."
 132}
 133
 134proc getcommitlines {fd view}  {
 135    global commitlisted
 136    global leftover commfd
 137    global displayorder commitidx commitrow commitdata
 138    global parentlist children curview hlview
 139    global vparentlist vdisporder vcmitlisted
 140
 141    set stuff [read $fd 500000]
 142    # git log doesn't terminate the last commit with a null...
 143    if {$stuff == {} && $leftover($view) ne {} && [eof $fd]} {
 144        set stuff "\0"
 145    }
 146    if {$stuff == {}} {
 147        if {![eof $fd]} {
 148            return 1
 149        }
 150        global viewname
 151        unset commfd($view)
 152        notbusy $view
 153        # set it blocking so we wait for the process to terminate
 154        fconfigure $fd -blocking 1
 155        if {[catch {close $fd} err]} {
 156            set fv {}
 157            if {$view != $curview} {
 158                set fv " for the \"$viewname($view)\" view"
 159            }
 160            if {[string range $err 0 4] == "usage"} {
 161                set err "Gitk: error reading commits$fv:\
 162                        bad arguments to git rev-list."
 163                if {$viewname($view) eq "Command line"} {
 164                    append err \
 165                        "  (Note: arguments to gitk are passed to git rev-list\
 166                         to allow selection of commits to be displayed.)"
 167                }
 168            } else {
 169                set err "Error reading commits$fv: $err"
 170            }
 171            error_popup $err
 172        }
 173        if {$view == $curview} {
 174            run chewcommits $view
 175        }
 176        return 0
 177    }
 178    set start 0
 179    set gotsome 0
 180    while 1 {
 181        set i [string first "\0" $stuff $start]
 182        if {$i < 0} {
 183            append leftover($view) [string range $stuff $start end]
 184            break
 185        }
 186        if {$start == 0} {
 187            set cmit $leftover($view)
 188            append cmit [string range $stuff 0 [expr {$i - 1}]]
 189            set leftover($view) {}
 190        } else {
 191            set cmit [string range $stuff $start [expr {$i - 1}]]
 192        }
 193        set start [expr {$i + 1}]
 194        set j [string first "\n" $cmit]
 195        set ok 0
 196        set listed 1
 197        if {$j >= 0 && [string match "commit *" $cmit]} {
 198            set ids [string range $cmit 7 [expr {$j - 1}]]
 199            if {[string match {[-<>]*} $ids]} {
 200                switch -- [string index $ids 0] {
 201                    "-" {set listed 0}
 202                    "<" {set listed 2}
 203                    ">" {set listed 3}
 204                }
 205                set ids [string range $ids 1 end]
 206            }
 207            set ok 1
 208            foreach id $ids {
 209                if {[string length $id] != 40} {
 210                    set ok 0
 211                    break
 212                }
 213            }
 214        }
 215        if {!$ok} {
 216            set shortcmit $cmit
 217            if {[string length $shortcmit] > 80} {
 218                set shortcmit "[string range $shortcmit 0 80]..."
 219            }
 220            error_popup "Can't parse git log output: {$shortcmit}"
 221            exit 1
 222        }
 223        set id [lindex $ids 0]
 224        if {$listed} {
 225            set olds [lrange $ids 1 end]
 226            set i 0
 227            foreach p $olds {
 228                if {$i == 0 || [lsearch -exact $olds $p] >= $i} {
 229                    lappend children($view,$p) $id
 230                }
 231                incr i
 232            }
 233        } else {
 234            set olds {}
 235        }
 236        if {![info exists children($view,$id)]} {
 237            set children($view,$id) {}
 238        }
 239        set commitdata($id) [string range $cmit [expr {$j + 1}] end]
 240        set commitrow($view,$id) $commitidx($view)
 241        incr commitidx($view)
 242        if {$view == $curview} {
 243            lappend parentlist $olds
 244            lappend displayorder $id
 245            lappend commitlisted $listed
 246        } else {
 247            lappend vparentlist($view) $olds
 248            lappend vdisporder($view) $id
 249            lappend vcmitlisted($view) $listed
 250        }
 251        set gotsome 1
 252    }
 253    if {$gotsome} {
 254        run chewcommits $view
 255    }
 256    return 2
 257}
 258
 259proc chewcommits {view} {
 260    global curview hlview commfd
 261    global selectedline pending_select
 262
 263    set more 0
 264    if {$view == $curview} {
 265        set allread [expr {![info exists commfd($view)]}]
 266        set tlimit [expr {[clock clicks -milliseconds] + 50}]
 267        set more [layoutmore $tlimit $allread]
 268        if {$allread && !$more} {
 269            global displayorder commitidx phase
 270            global numcommits startmsecs
 271
 272            if {[info exists pending_select]} {
 273                set row [first_real_row]
 274                selectline $row 1
 275            }
 276            if {$commitidx($curview) > 0} {
 277                #set ms [expr {[clock clicks -milliseconds] - $startmsecs}]
 278                #puts "overall $ms ms for $numcommits commits"
 279            } else {
 280                show_status "No commits selected"
 281            }
 282            notbusy layout
 283            set phase {}
 284        }
 285    }
 286    if {[info exists hlview] && $view == $hlview} {
 287        vhighlightmore
 288    }
 289    return $more
 290}
 291
 292proc readcommit {id} {
 293    if {[catch {set contents [exec git cat-file commit $id]}]} return
 294    parsecommit $id $contents 0
 295}
 296
 297proc updatecommits {} {
 298    global viewdata curview phase displayorder
 299    global children commitrow selectedline thickerline
 300
 301    if {$phase ne {}} {
 302        stop_rev_list
 303        set phase {}
 304    }
 305    set n $curview
 306    foreach id $displayorder {
 307        catch {unset children($n,$id)}
 308        catch {unset commitrow($n,$id)}
 309    }
 310    set curview -1
 311    catch {unset selectedline}
 312    catch {unset thickerline}
 313    catch {unset viewdata($n)}
 314    readrefs
 315    changedrefs
 316    regetallcommits
 317    showview $n
 318}
 319
 320proc parsecommit {id contents listed} {
 321    global commitinfo cdate
 322
 323    set inhdr 1
 324    set comment {}
 325    set headline {}
 326    set auname {}
 327    set audate {}
 328    set comname {}
 329    set comdate {}
 330    set hdrend [string first "\n\n" $contents]
 331    if {$hdrend < 0} {
 332        # should never happen...
 333        set hdrend [string length $contents]
 334    }
 335    set header [string range $contents 0 [expr {$hdrend - 1}]]
 336    set comment [string range $contents [expr {$hdrend + 2}] end]
 337    foreach line [split $header "\n"] {
 338        set tag [lindex $line 0]
 339        if {$tag == "author"} {
 340            set audate [lindex $line end-1]
 341            set auname [lrange $line 1 end-2]
 342        } elseif {$tag == "committer"} {
 343            set comdate [lindex $line end-1]
 344            set comname [lrange $line 1 end-2]
 345        }
 346    }
 347    set headline {}
 348    # take the first non-blank line of the comment as the headline
 349    set headline [string trimleft $comment]
 350    set i [string first "\n" $headline]
 351    if {$i >= 0} {
 352        set headline [string range $headline 0 $i]
 353    }
 354    set headline [string trimright $headline]
 355    set i [string first "\r" $headline]
 356    if {$i >= 0} {
 357        set headline [string trimright [string range $headline 0 $i]]
 358    }
 359    if {!$listed} {
 360        # git rev-list indents the comment by 4 spaces;
 361        # if we got this via git cat-file, add the indentation
 362        set newcomment {}
 363        foreach line [split $comment "\n"] {
 364            append newcomment "    "
 365            append newcomment $line
 366            append newcomment "\n"
 367        }
 368        set comment $newcomment
 369    }
 370    if {$comdate != {}} {
 371        set cdate($id) $comdate
 372    }
 373    set commitinfo($id) [list $headline $auname $audate \
 374                             $comname $comdate $comment]
 375}
 376
 377proc getcommit {id} {
 378    global commitdata commitinfo
 379
 380    if {[info exists commitdata($id)]} {
 381        parsecommit $id $commitdata($id) 1
 382    } else {
 383        readcommit $id
 384        if {![info exists commitinfo($id)]} {
 385            set commitinfo($id) {"No commit information available"}
 386        }
 387    }
 388    return 1
 389}
 390
 391proc readrefs {} {
 392    global tagids idtags headids idheads tagobjid
 393    global otherrefids idotherrefs mainhead mainheadid
 394
 395    foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
 396        catch {unset $v}
 397    }
 398    set refd [open [list | git show-ref -d] r]
 399    while {[gets $refd line] >= 0} {
 400        if {[string index $line 40] ne " "} continue
 401        set id [string range $line 0 39]
 402        set ref [string range $line 41 end]
 403        if {![string match "refs/*" $ref]} continue
 404        set name [string range $ref 5 end]
 405        if {[string match "remotes/*" $name]} {
 406            if {![string match "*/HEAD" $name]} {
 407                set headids($name) $id
 408                lappend idheads($id) $name
 409            }
 410        } elseif {[string match "heads/*" $name]} {
 411            set name [string range $name 6 end]
 412            set headids($name) $id
 413            lappend idheads($id) $name
 414        } elseif {[string match "tags/*" $name]} {
 415            # this lets refs/tags/foo^{} overwrite refs/tags/foo,
 416            # which is what we want since the former is the commit ID
 417            set name [string range $name 5 end]
 418            if {[string match "*^{}" $name]} {
 419                set name [string range $name 0 end-3]
 420            } else {
 421                set tagobjid($name) $id
 422            }
 423            set tagids($name) $id
 424            lappend idtags($id) $name
 425        } else {
 426            set otherrefids($name) $id
 427            lappend idotherrefs($id) $name
 428        }
 429    }
 430    catch {close $refd}
 431    set mainhead {}
 432    set mainheadid {}
 433    catch {
 434        set thehead [exec git symbolic-ref HEAD]
 435        if {[string match "refs/heads/*" $thehead]} {
 436            set mainhead [string range $thehead 11 end]
 437            if {[info exists headids($mainhead)]} {
 438                set mainheadid $headids($mainhead)
 439            }
 440        }
 441    }
 442}
 443
 444# skip over fake commits
 445proc first_real_row {} {
 446    global nullid nullid2 displayorder numcommits
 447
 448    for {set row 0} {$row < $numcommits} {incr row} {
 449        set id [lindex $displayorder $row]
 450        if {$id ne $nullid && $id ne $nullid2} {
 451            break
 452        }
 453    }
 454    return $row
 455}
 456
 457# update things for a head moved to a child of its previous location
 458proc movehead {id name} {
 459    global headids idheads
 460
 461    removehead $headids($name) $name
 462    set headids($name) $id
 463    lappend idheads($id) $name
 464}
 465
 466# update things when a head has been removed
 467proc removehead {id name} {
 468    global headids idheads
 469
 470    if {$idheads($id) eq $name} {
 471        unset idheads($id)
 472    } else {
 473        set i [lsearch -exact $idheads($id) $name]
 474        if {$i >= 0} {
 475            set idheads($id) [lreplace $idheads($id) $i $i]
 476        }
 477    }
 478    unset headids($name)
 479}
 480
 481proc show_error {w top msg} {
 482    message $w.m -text $msg -justify center -aspect 400
 483    pack $w.m -side top -fill x -padx 20 -pady 20
 484    button $w.ok -text OK -command "destroy $top"
 485    pack $w.ok -side bottom -fill x
 486    bind $top <Visibility> "grab $top; focus $top"
 487    bind $top <Key-Return> "destroy $top"
 488    tkwait window $top
 489}
 490
 491proc error_popup msg {
 492    set w .error
 493    toplevel $w
 494    wm transient $w .
 495    show_error $w $w $msg
 496}
 497
 498proc confirm_popup msg {
 499    global confirm_ok
 500    set confirm_ok 0
 501    set w .confirm
 502    toplevel $w
 503    wm transient $w .
 504    message $w.m -text $msg -justify center -aspect 400
 505    pack $w.m -side top -fill x -padx 20 -pady 20
 506    button $w.ok -text OK -command "set confirm_ok 1; destroy $w"
 507    pack $w.ok -side left -fill x
 508    button $w.cancel -text Cancel -command "destroy $w"
 509    pack $w.cancel -side right -fill x
 510    bind $w <Visibility> "grab $w; focus $w"
 511    tkwait window $w
 512    return $confirm_ok
 513}
 514
 515proc makewindow {} {
 516    global canv canv2 canv3 linespc charspc ctext cflist
 517    global textfont mainfont uifont tabstop
 518    global findtype findtypemenu findloc findstring fstring geometry
 519    global entries sha1entry sha1string sha1but
 520    global maincursor textcursor curtextcursor
 521    global rowctxmenu fakerowmenu mergemax wrapcomment
 522    global highlight_files gdttype
 523    global searchstring sstring
 524    global bgcolor fgcolor bglist fglist diffcolors selectbgcolor
 525    global headctxmenu
 526
 527    menu .bar
 528    .bar add cascade -label "File" -menu .bar.file
 529    .bar configure -font $uifont
 530    menu .bar.file
 531    .bar.file add command -label "Update" -command updatecommits
 532    .bar.file add command -label "Reread references" -command rereadrefs
 533    .bar.file add command -label "Quit" -command doquit
 534    .bar.file configure -font $uifont
 535    menu .bar.edit
 536    .bar add cascade -label "Edit" -menu .bar.edit
 537    .bar.edit add command -label "Preferences" -command doprefs
 538    .bar.edit configure -font $uifont
 539
 540    menu .bar.view -font $uifont
 541    .bar add cascade -label "View" -menu .bar.view
 542    .bar.view add command -label "New view..." -command {newview 0}
 543    .bar.view add command -label "Edit view..." -command editview \
 544        -state disabled
 545    .bar.view add command -label "Delete view" -command delview -state disabled
 546    .bar.view add separator
 547    .bar.view add radiobutton -label "All files" -command {showview 0} \
 548        -variable selectedview -value 0
 549
 550    menu .bar.help
 551    .bar add cascade -label "Help" -menu .bar.help
 552    .bar.help add command -label "About gitk" -command about
 553    .bar.help add command -label "Key bindings" -command keys
 554    .bar.help configure -font $uifont
 555    . configure -menu .bar
 556
 557    # the gui has upper and lower half, parts of a paned window.
 558    panedwindow .ctop -orient vertical
 559
 560    # possibly use assumed geometry
 561    if {![info exists geometry(pwsash0)]} {
 562        set geometry(topheight) [expr {15 * $linespc}]
 563        set geometry(topwidth) [expr {80 * $charspc}]
 564        set geometry(botheight) [expr {15 * $linespc}]
 565        set geometry(botwidth) [expr {50 * $charspc}]
 566        set geometry(pwsash0) "[expr {40 * $charspc}] 2"
 567        set geometry(pwsash1) "[expr {60 * $charspc}] 2"
 568    }
 569
 570    # the upper half will have a paned window, a scroll bar to the right, and some stuff below
 571    frame .tf -height $geometry(topheight) -width $geometry(topwidth)
 572    frame .tf.histframe
 573    panedwindow .tf.histframe.pwclist -orient horizontal -sashpad 0 -handlesize 4
 574
 575    # create three canvases
 576    set cscroll .tf.histframe.csb
 577    set canv .tf.histframe.pwclist.canv
 578    canvas $canv \
 579        -selectbackground $selectbgcolor \
 580        -background $bgcolor -bd 0 \
 581        -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll"
 582    .tf.histframe.pwclist add $canv
 583    set canv2 .tf.histframe.pwclist.canv2
 584    canvas $canv2 \
 585        -selectbackground $selectbgcolor \
 586        -background $bgcolor -bd 0 -yscrollincr $linespc
 587    .tf.histframe.pwclist add $canv2
 588    set canv3 .tf.histframe.pwclist.canv3
 589    canvas $canv3 \
 590        -selectbackground $selectbgcolor \
 591        -background $bgcolor -bd 0 -yscrollincr $linespc
 592    .tf.histframe.pwclist add $canv3
 593    eval .tf.histframe.pwclist sash place 0 $geometry(pwsash0)
 594    eval .tf.histframe.pwclist sash place 1 $geometry(pwsash1)
 595
 596    # a scroll bar to rule them
 597    scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
 598    pack $cscroll -side right -fill y
 599    bind .tf.histframe.pwclist <Configure> {resizeclistpanes %W %w}
 600    lappend bglist $canv $canv2 $canv3
 601    pack .tf.histframe.pwclist -fill both -expand 1 -side left
 602
 603    # we have two button bars at bottom of top frame. Bar 1
 604    frame .tf.bar
 605    frame .tf.lbar -height 15
 606
 607    set sha1entry .tf.bar.sha1
 608    set entries $sha1entry
 609    set sha1but .tf.bar.sha1label
 610    button $sha1but -text "SHA1 ID: " -state disabled -relief flat \
 611        -command gotocommit -width 8 -font $uifont
 612    $sha1but conf -disabledforeground [$sha1but cget -foreground]
 613    pack .tf.bar.sha1label -side left
 614    entry $sha1entry -width 40 -font $textfont -textvariable sha1string
 615    trace add variable sha1string write sha1change
 616    pack $sha1entry -side left -pady 2
 617
 618    image create bitmap bm-left -data {
 619        #define left_width 16
 620        #define left_height 16
 621        static unsigned char left_bits[] = {
 622        0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
 623        0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
 624        0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
 625    }
 626    image create bitmap bm-right -data {
 627        #define right_width 16
 628        #define right_height 16
 629        static unsigned char right_bits[] = {
 630        0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
 631        0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
 632        0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
 633    }
 634    button .tf.bar.leftbut -image bm-left -command goback \
 635        -state disabled -width 26
 636    pack .tf.bar.leftbut -side left -fill y
 637    button .tf.bar.rightbut -image bm-right -command goforw \
 638        -state disabled -width 26
 639    pack .tf.bar.rightbut -side left -fill y
 640
 641    button .tf.bar.findbut -text "Find" -command dofind -font $uifont
 642    pack .tf.bar.findbut -side left
 643    set findstring {}
 644    set fstring .tf.bar.findstring
 645    lappend entries $fstring
 646    entry $fstring -width 30 -font $textfont -textvariable findstring
 647    trace add variable findstring write find_change
 648    pack $fstring -side left -expand 1 -fill x -in .tf.bar
 649    set findtype Exact
 650    set findtypemenu [tk_optionMenu .tf.bar.findtype \
 651                      findtype Exact IgnCase Regexp]
 652    trace add variable findtype write find_change
 653    .tf.bar.findtype configure -font $uifont
 654    .tf.bar.findtype.menu configure -font $uifont
 655    set findloc "All fields"
 656    tk_optionMenu .tf.bar.findloc findloc "All fields" Headline \
 657        Comments Author Committer
 658    trace add variable findloc write find_change
 659    .tf.bar.findloc configure -font $uifont
 660    .tf.bar.findloc.menu configure -font $uifont
 661    pack .tf.bar.findloc -side right
 662    pack .tf.bar.findtype -side right
 663
 664    # build up the bottom bar of upper window
 665    label .tf.lbar.flabel -text "Highlight:  Commits " \
 666    -font $uifont
 667    pack .tf.lbar.flabel -side left -fill y
 668    set gdttype "touching paths:"
 669    set gm [tk_optionMenu .tf.lbar.gdttype gdttype "touching paths:" \
 670        "adding/removing string:"]
 671    trace add variable gdttype write hfiles_change
 672    $gm conf -font $uifont
 673    .tf.lbar.gdttype conf -font $uifont
 674    pack .tf.lbar.gdttype -side left -fill y
 675    entry .tf.lbar.fent -width 25 -font $textfont \
 676        -textvariable highlight_files
 677    trace add variable highlight_files write hfiles_change
 678    lappend entries .tf.lbar.fent
 679    pack .tf.lbar.fent -side left -fill x -expand 1
 680    label .tf.lbar.vlabel -text " OR in view" -font $uifont
 681    pack .tf.lbar.vlabel -side left -fill y
 682    global viewhlmenu selectedhlview
 683    set viewhlmenu [tk_optionMenu .tf.lbar.vhl selectedhlview None]
 684    $viewhlmenu entryconf None -command delvhighlight
 685    $viewhlmenu conf -font $uifont
 686    .tf.lbar.vhl conf -font $uifont
 687    pack .tf.lbar.vhl -side left -fill y
 688    label .tf.lbar.rlabel -text " OR " -font $uifont
 689    pack .tf.lbar.rlabel -side left -fill y
 690    global highlight_related
 691    set m [tk_optionMenu .tf.lbar.relm highlight_related None \
 692        "Descendent" "Not descendent" "Ancestor" "Not ancestor"]
 693    $m conf -font $uifont
 694    .tf.lbar.relm conf -font $uifont
 695    trace add variable highlight_related write vrel_change
 696    pack .tf.lbar.relm -side left -fill y
 697
 698    # Finish putting the upper half of the viewer together
 699    pack .tf.lbar -in .tf -side bottom -fill x
 700    pack .tf.bar -in .tf -side bottom -fill x
 701    pack .tf.histframe -fill both -side top -expand 1
 702    .ctop add .tf
 703    .ctop paneconfigure .tf -height $geometry(topheight)
 704    .ctop paneconfigure .tf -width $geometry(topwidth)
 705
 706    # now build up the bottom
 707    panedwindow .pwbottom -orient horizontal
 708
 709    # lower left, a text box over search bar, scroll bar to the right
 710    # if we know window height, then that will set the lower text height, otherwise
 711    # we set lower text height which will drive window height
 712    if {[info exists geometry(main)]} {
 713        frame .bleft -width $geometry(botwidth)
 714    } else {
 715        frame .bleft -width $geometry(botwidth) -height $geometry(botheight)
 716    }
 717    frame .bleft.top
 718    frame .bleft.mid
 719
 720    button .bleft.top.search -text "Search" -command dosearch \
 721        -font $uifont
 722    pack .bleft.top.search -side left -padx 5
 723    set sstring .bleft.top.sstring
 724    entry $sstring -width 20 -font $textfont -textvariable searchstring
 725    lappend entries $sstring
 726    trace add variable searchstring write incrsearch
 727    pack $sstring -side left -expand 1 -fill x
 728    radiobutton .bleft.mid.diff -text "Diff" \
 729        -command changediffdisp -variable diffelide -value {0 0}
 730    radiobutton .bleft.mid.old -text "Old version" \
 731        -command changediffdisp -variable diffelide -value {0 1}
 732    radiobutton .bleft.mid.new -text "New version" \
 733        -command changediffdisp -variable diffelide -value {1 0}
 734    pack .bleft.mid.diff .bleft.mid.old .bleft.mid.new -side left
 735    set ctext .bleft.ctext
 736    text $ctext -background $bgcolor -foreground $fgcolor \
 737        -tabs "[expr {$tabstop * $charspc}]" \
 738        -state disabled -font $textfont \
 739        -yscrollcommand scrolltext -wrap none
 740    scrollbar .bleft.sb -command "$ctext yview"
 741    pack .bleft.top -side top -fill x
 742    pack .bleft.mid -side top -fill x
 743    pack .bleft.sb -side right -fill y
 744    pack $ctext -side left -fill both -expand 1
 745    lappend bglist $ctext
 746    lappend fglist $ctext
 747
 748    $ctext tag conf comment -wrap $wrapcomment
 749    $ctext tag conf filesep -font [concat $textfont bold] -back "#aaaaaa"
 750    $ctext tag conf hunksep -fore [lindex $diffcolors 2]
 751    $ctext tag conf d0 -fore [lindex $diffcolors 0]
 752    $ctext tag conf d1 -fore [lindex $diffcolors 1]
 753    $ctext tag conf m0 -fore red
 754    $ctext tag conf m1 -fore blue
 755    $ctext tag conf m2 -fore green
 756    $ctext tag conf m3 -fore purple
 757    $ctext tag conf m4 -fore brown
 758    $ctext tag conf m5 -fore "#009090"
 759    $ctext tag conf m6 -fore magenta
 760    $ctext tag conf m7 -fore "#808000"
 761    $ctext tag conf m8 -fore "#009000"
 762    $ctext tag conf m9 -fore "#ff0080"
 763    $ctext tag conf m10 -fore cyan
 764    $ctext tag conf m11 -fore "#b07070"
 765    $ctext tag conf m12 -fore "#70b0f0"
 766    $ctext tag conf m13 -fore "#70f0b0"
 767    $ctext tag conf m14 -fore "#f0b070"
 768    $ctext tag conf m15 -fore "#ff70b0"
 769    $ctext tag conf mmax -fore darkgrey
 770    set mergemax 16
 771    $ctext tag conf mresult -font [concat $textfont bold]
 772    $ctext tag conf msep -font [concat $textfont bold]
 773    $ctext tag conf found -back yellow
 774
 775    .pwbottom add .bleft
 776    .pwbottom paneconfigure .bleft -width $geometry(botwidth)
 777
 778    # lower right
 779    frame .bright
 780    frame .bright.mode
 781    radiobutton .bright.mode.patch -text "Patch" \
 782        -command reselectline -variable cmitmode -value "patch"
 783    .bright.mode.patch configure -font $uifont
 784    radiobutton .bright.mode.tree -text "Tree" \
 785        -command reselectline -variable cmitmode -value "tree"
 786    .bright.mode.tree configure -font $uifont
 787    grid .bright.mode.patch .bright.mode.tree -sticky ew
 788    pack .bright.mode -side top -fill x
 789    set cflist .bright.cfiles
 790    set indent [font measure $mainfont "nn"]
 791    text $cflist \
 792        -selectbackground $selectbgcolor \
 793        -background $bgcolor -foreground $fgcolor \
 794        -font $mainfont \
 795        -tabs [list $indent [expr {2 * $indent}]] \
 796        -yscrollcommand ".bright.sb set" \
 797        -cursor [. cget -cursor] \
 798        -spacing1 1 -spacing3 1
 799    lappend bglist $cflist
 800    lappend fglist $cflist
 801    scrollbar .bright.sb -command "$cflist yview"
 802    pack .bright.sb -side right -fill y
 803    pack $cflist -side left -fill both -expand 1
 804    $cflist tag configure highlight \
 805        -background [$cflist cget -selectbackground]
 806    $cflist tag configure bold -font [concat $mainfont bold]
 807
 808    .pwbottom add .bright
 809    .ctop add .pwbottom
 810
 811    # restore window position if known
 812    if {[info exists geometry(main)]} {
 813        wm geometry . "$geometry(main)"
 814    }
 815
 816    if {[tk windowingsystem] eq {aqua}} {
 817        set M1B M1
 818    } else {
 819        set M1B Control
 820    }
 821
 822    bind .pwbottom <Configure> {resizecdetpanes %W %w}
 823    pack .ctop -fill both -expand 1
 824    bindall <1> {selcanvline %W %x %y}
 825    #bindall <B1-Motion> {selcanvline %W %x %y}
 826    if {[tk windowingsystem] == "win32"} {
 827        bind . <MouseWheel> { windows_mousewheel_redirector %W %X %Y %D }
 828        bind $ctext <MouseWheel> { windows_mousewheel_redirector %W %X %Y %D ; break }
 829    } else {
 830        bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
 831        bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
 832    }
 833    bindall <2> "canvscan mark %W %x %y"
 834    bindall <B2-Motion> "canvscan dragto %W %x %y"
 835    bindkey <Home> selfirstline
 836    bindkey <End> sellastline
 837    bind . <Key-Up> "selnextline -1"
 838    bind . <Key-Down> "selnextline 1"
 839    bind . <Shift-Key-Up> "next_highlight -1"
 840    bind . <Shift-Key-Down> "next_highlight 1"
 841    bindkey <Key-Right> "goforw"
 842    bindkey <Key-Left> "goback"
 843    bind . <Key-Prior> "selnextpage -1"
 844    bind . <Key-Next> "selnextpage 1"
 845    bind . <$M1B-Home> "allcanvs yview moveto 0.0"
 846    bind . <$M1B-End> "allcanvs yview moveto 1.0"
 847    bind . <$M1B-Key-Up> "allcanvs yview scroll -1 units"
 848    bind . <$M1B-Key-Down> "allcanvs yview scroll 1 units"
 849    bind . <$M1B-Key-Prior> "allcanvs yview scroll -1 pages"
 850    bind . <$M1B-Key-Next> "allcanvs yview scroll 1 pages"
 851    bindkey <Key-Delete> "$ctext yview scroll -1 pages"
 852    bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
 853    bindkey <Key-space> "$ctext yview scroll 1 pages"
 854    bindkey p "selnextline -1"
 855    bindkey n "selnextline 1"
 856    bindkey z "goback"
 857    bindkey x "goforw"
 858    bindkey i "selnextline -1"
 859    bindkey k "selnextline 1"
 860    bindkey j "goback"
 861    bindkey l "goforw"
 862    bindkey b "$ctext yview scroll -1 pages"
 863    bindkey d "$ctext yview scroll 18 units"
 864    bindkey u "$ctext yview scroll -18 units"
 865    bindkey / {findnext 1}
 866    bindkey <Key-Return> {findnext 0}
 867    bindkey ? findprev
 868    bindkey f nextfile
 869    bindkey <F5> updatecommits
 870    bind . <$M1B-q> doquit
 871    bind . <$M1B-f> dofind
 872    bind . <$M1B-g> {findnext 0}
 873    bind . <$M1B-r> dosearchback
 874    bind . <$M1B-s> dosearch
 875    bind . <$M1B-equal> {incrfont 1}
 876    bind . <$M1B-KP_Add> {incrfont 1}
 877    bind . <$M1B-minus> {incrfont -1}
 878    bind . <$M1B-KP_Subtract> {incrfont -1}
 879    wm protocol . WM_DELETE_WINDOW doquit
 880    bind . <Button-1> "click %W"
 881    bind $fstring <Key-Return> dofind
 882    bind $sha1entry <Key-Return> gotocommit
 883    bind $sha1entry <<PasteSelection>> clearsha1
 884    bind $cflist <1> {sel_flist %W %x %y; break}
 885    bind $cflist <B1-Motion> {sel_flist %W %x %y; break}
 886    bind $cflist <ButtonRelease-1> {treeclick %W %x %y}
 887    bind $cflist <Button-3> {pop_flist_menu %W %X %Y %x %y}
 888
 889    set maincursor [. cget -cursor]
 890    set textcursor [$ctext cget -cursor]
 891    set curtextcursor $textcursor
 892
 893    set rowctxmenu .rowctxmenu
 894    menu $rowctxmenu -tearoff 0
 895    $rowctxmenu add command -label "Diff this -> selected" \
 896        -command {diffvssel 0}
 897    $rowctxmenu add command -label "Diff selected -> this" \
 898        -command {diffvssel 1}
 899    $rowctxmenu add command -label "Make patch" -command mkpatch
 900    $rowctxmenu add command -label "Create tag" -command mktag
 901    $rowctxmenu add command -label "Write commit to file" -command writecommit
 902    $rowctxmenu add command -label "Create new branch" -command mkbranch
 903    $rowctxmenu add command -label "Cherry-pick this commit" \
 904        -command cherrypick
 905    $rowctxmenu add command -label "Reset HEAD branch to here" \
 906        -command resethead
 907
 908    set fakerowmenu .fakerowmenu
 909    menu $fakerowmenu -tearoff 0
 910    $fakerowmenu add command -label "Diff this -> selected" \
 911        -command {diffvssel 0}
 912    $fakerowmenu add command -label "Diff selected -> this" \
 913        -command {diffvssel 1}
 914    $fakerowmenu add command -label "Make patch" -command mkpatch
 915#    $fakerowmenu add command -label "Commit" -command {mkcommit 0}
 916#    $fakerowmenu add command -label "Commit all" -command {mkcommit 1}
 917#    $fakerowmenu add command -label "Revert local changes" -command revertlocal
 918
 919    set headctxmenu .headctxmenu
 920    menu $headctxmenu -tearoff 0
 921    $headctxmenu add command -label "Check out this branch" \
 922        -command cobranch
 923    $headctxmenu add command -label "Remove this branch" \
 924        -command rmbranch
 925
 926    global flist_menu
 927    set flist_menu .flistctxmenu
 928    menu $flist_menu -tearoff 0
 929    $flist_menu add command -label "Highlight this too" \
 930        -command {flist_hl 0}
 931    $flist_menu add command -label "Highlight this only" \
 932        -command {flist_hl 1}
 933}
 934
 935# Windows sends all mouse wheel events to the current focused window, not
 936# the one where the mouse hovers, so bind those events here and redirect
 937# to the correct window
 938proc windows_mousewheel_redirector {W X Y D} {
 939    global canv canv2 canv3
 940    set w [winfo containing -displayof $W $X $Y]
 941    if {$w ne ""} {
 942        set u [expr {$D < 0 ? 5 : -5}]
 943        if {$w == $canv || $w == $canv2 || $w == $canv3} {
 944            allcanvs yview scroll $u units
 945        } else {
 946            catch {
 947                $w yview scroll $u units
 948            }
 949        }
 950    }
 951}
 952
 953# mouse-2 makes all windows scan vertically, but only the one
 954# the cursor is in scans horizontally
 955proc canvscan {op w x y} {
 956    global canv canv2 canv3
 957    foreach c [list $canv $canv2 $canv3] {
 958        if {$c == $w} {
 959            $c scan $op $x $y
 960        } else {
 961            $c scan $op 0 $y
 962        }
 963    }
 964}
 965
 966proc scrollcanv {cscroll f0 f1} {
 967    $cscroll set $f0 $f1
 968    drawfrac $f0 $f1
 969    flushhighlights
 970}
 971
 972# when we make a key binding for the toplevel, make sure
 973# it doesn't get triggered when that key is pressed in the
 974# find string entry widget.
 975proc bindkey {ev script} {
 976    global entries
 977    bind . $ev $script
 978    set escript [bind Entry $ev]
 979    if {$escript == {}} {
 980        set escript [bind Entry <Key>]
 981    }
 982    foreach e $entries {
 983        bind $e $ev "$escript; break"
 984    }
 985}
 986
 987# set the focus back to the toplevel for any click outside
 988# the entry widgets
 989proc click {w} {
 990    global ctext entries
 991    foreach e [concat $entries $ctext] {
 992        if {$w == $e} return
 993    }
 994    focus .
 995}
 996
 997proc savestuff {w} {
 998    global canv canv2 canv3 ctext cflist mainfont textfont uifont tabstop
 999    global stuffsaved findmergefiles maxgraphpct
1000    global maxwidth showneartags showlocalchanges
1001    global viewname viewfiles viewargs viewperm nextviewnum
1002    global cmitmode wrapcomment
1003    global colors bgcolor fgcolor diffcolors selectbgcolor
1004
1005    if {$stuffsaved} return
1006    if {![winfo viewable .]} return
1007    catch {
1008        set f [open "~/.gitk-new" w]
1009        puts $f [list set mainfont $mainfont]
1010        puts $f [list set textfont $textfont]
1011        puts $f [list set uifont $uifont]
1012        puts $f [list set tabstop $tabstop]
1013        puts $f [list set findmergefiles $findmergefiles]
1014        puts $f [list set maxgraphpct $maxgraphpct]
1015        puts $f [list set maxwidth $maxwidth]
1016        puts $f [list set cmitmode $cmitmode]
1017        puts $f [list set wrapcomment $wrapcomment]
1018        puts $f [list set showneartags $showneartags]
1019        puts $f [list set showlocalchanges $showlocalchanges]
1020        puts $f [list set bgcolor $bgcolor]
1021        puts $f [list set fgcolor $fgcolor]
1022        puts $f [list set colors $colors]
1023        puts $f [list set diffcolors $diffcolors]
1024        puts $f [list set selectbgcolor $selectbgcolor]
1025
1026        puts $f "set geometry(main) [wm geometry .]"
1027        puts $f "set geometry(topwidth) [winfo width .tf]"
1028        puts $f "set geometry(topheight) [winfo height .tf]"
1029        puts $f "set geometry(pwsash0) \"[.tf.histframe.pwclist sash coord 0]\""
1030        puts $f "set geometry(pwsash1) \"[.tf.histframe.pwclist sash coord 1]\""
1031        puts $f "set geometry(botwidth) [winfo width .bleft]"
1032        puts $f "set geometry(botheight) [winfo height .bleft]"
1033
1034        puts -nonewline $f "set permviews {"
1035        for {set v 0} {$v < $nextviewnum} {incr v} {
1036            if {$viewperm($v)} {
1037                puts $f "{[list $viewname($v) $viewfiles($v) $viewargs($v)]}"
1038            }
1039        }
1040        puts $f "}"
1041        close $f
1042        file rename -force "~/.gitk-new" "~/.gitk"
1043    }
1044    set stuffsaved 1
1045}
1046
1047proc resizeclistpanes {win w} {
1048    global oldwidth
1049    if {[info exists oldwidth($win)]} {
1050        set s0 [$win sash coord 0]
1051        set s1 [$win sash coord 1]
1052        if {$w < 60} {
1053            set sash0 [expr {int($w/2 - 2)}]
1054            set sash1 [expr {int($w*5/6 - 2)}]
1055        } else {
1056            set factor [expr {1.0 * $w / $oldwidth($win)}]
1057            set sash0 [expr {int($factor * [lindex $s0 0])}]
1058            set sash1 [expr {int($factor * [lindex $s1 0])}]
1059            if {$sash0 < 30} {
1060                set sash0 30
1061            }
1062            if {$sash1 < $sash0 + 20} {
1063                set sash1 [expr {$sash0 + 20}]
1064            }
1065            if {$sash1 > $w - 10} {
1066                set sash1 [expr {$w - 10}]
1067                if {$sash0 > $sash1 - 20} {
1068                    set sash0 [expr {$sash1 - 20}]
1069                }
1070            }
1071        }
1072        $win sash place 0 $sash0 [lindex $s0 1]
1073        $win sash place 1 $sash1 [lindex $s1 1]
1074    }
1075    set oldwidth($win) $w
1076}
1077
1078proc resizecdetpanes {win w} {
1079    global oldwidth
1080    if {[info exists oldwidth($win)]} {
1081        set s0 [$win sash coord 0]
1082        if {$w < 60} {
1083            set sash0 [expr {int($w*3/4 - 2)}]
1084        } else {
1085            set factor [expr {1.0 * $w / $oldwidth($win)}]
1086            set sash0 [expr {int($factor * [lindex $s0 0])}]
1087            if {$sash0 < 45} {
1088                set sash0 45
1089            }
1090            if {$sash0 > $w - 15} {
1091                set sash0 [expr {$w - 15}]
1092            }
1093        }
1094        $win sash place 0 $sash0 [lindex $s0 1]
1095    }
1096    set oldwidth($win) $w
1097}
1098
1099proc allcanvs args {
1100    global canv canv2 canv3
1101    eval $canv $args
1102    eval $canv2 $args
1103    eval $canv3 $args
1104}
1105
1106proc bindall {event action} {
1107    global canv canv2 canv3
1108    bind $canv $event $action
1109    bind $canv2 $event $action
1110    bind $canv3 $event $action
1111}
1112
1113proc about {} {
1114    global uifont
1115    set w .about
1116    if {[winfo exists $w]} {
1117        raise $w
1118        return
1119    }
1120    toplevel $w
1121    wm title $w "About gitk"
1122    message $w.m -text {
1123Gitk - a commit viewer for git
1124
1125Copyright © 2005-2006 Paul Mackerras
1126
1127Use and redistribute under the terms of the GNU General Public License} \
1128            -justify center -aspect 400 -border 2 -bg white -relief groove
1129    pack $w.m -side top -fill x -padx 2 -pady 2
1130    $w.m configure -font $uifont
1131    button $w.ok -text Close -command "destroy $w" -default active
1132    pack $w.ok -side bottom
1133    $w.ok configure -font $uifont
1134    bind $w <Visibility> "focus $w.ok"
1135    bind $w <Key-Escape> "destroy $w"
1136    bind $w <Key-Return> "destroy $w"
1137}
1138
1139proc keys {} {
1140    global uifont
1141    set w .keys
1142    if {[winfo exists $w]} {
1143        raise $w
1144        return
1145    }
1146    if {[tk windowingsystem] eq {aqua}} {
1147        set M1T Cmd
1148    } else {
1149        set M1T Ctrl
1150    }
1151    toplevel $w
1152    wm title $w "Gitk key bindings"
1153    message $w.m -text "
1154Gitk key bindings:
1155
1156<$M1T-Q>                Quit
1157<Home>          Move to first commit
1158<End>           Move to last commit
1159<Up>, p, i      Move up one commit
1160<Down>, n, k    Move down one commit
1161<Left>, z, j    Go back in history list
1162<Right>, x, l   Go forward in history list
1163<PageUp>        Move up one page in commit list
1164<PageDown>      Move down one page in commit list
1165<$M1T-Home>     Scroll to top of commit list
1166<$M1T-End>      Scroll to bottom of commit list
1167<$M1T-Up>       Scroll commit list up one line
1168<$M1T-Down>     Scroll commit list down one line
1169<$M1T-PageUp>   Scroll commit list up one page
1170<$M1T-PageDown> Scroll commit list down one page
1171<Shift-Up>      Move to previous highlighted line
1172<Shift-Down>    Move to next highlighted line
1173<Delete>, b     Scroll diff view up one page
1174<Backspace>     Scroll diff view up one page
1175<Space>         Scroll diff view down one page
1176u               Scroll diff view up 18 lines
1177d               Scroll diff view down 18 lines
1178<$M1T-F>                Find
1179<$M1T-G>                Move to next find hit
1180<Return>        Move to next find hit
1181/               Move to next find hit, or redo find
1182?               Move to previous find hit
1183f               Scroll diff view to next file
1184<$M1T-S>                Search for next hit in diff view
1185<$M1T-R>                Search for previous hit in diff view
1186<$M1T-KP+>      Increase font size
1187<$M1T-plus>     Increase font size
1188<$M1T-KP->      Decrease font size
1189<$M1T-minus>    Decrease font size
1190<F5>            Update
1191" \
1192            -justify left -bg white -border 2 -relief groove
1193    pack $w.m -side top -fill both -padx 2 -pady 2
1194    $w.m configure -font $uifont
1195    button $w.ok -text Close -command "destroy $w" -default active
1196    pack $w.ok -side bottom
1197    $w.ok configure -font $uifont
1198    bind $w <Visibility> "focus $w.ok"
1199    bind $w <Key-Escape> "destroy $w"
1200    bind $w <Key-Return> "destroy $w"
1201}
1202
1203# Procedures for manipulating the file list window at the
1204# bottom right of the overall window.
1205
1206proc treeview {w l openlevs} {
1207    global treecontents treediropen treeheight treeparent treeindex
1208
1209    set ix 0
1210    set treeindex() 0
1211    set lev 0
1212    set prefix {}
1213    set prefixend -1
1214    set prefendstack {}
1215    set htstack {}
1216    set ht 0
1217    set treecontents() {}
1218    $w conf -state normal
1219    foreach f $l {
1220        while {[string range $f 0 $prefixend] ne $prefix} {
1221            if {$lev <= $openlevs} {
1222                $w mark set e:$treeindex($prefix) "end -1c"
1223                $w mark gravity e:$treeindex($prefix) left
1224            }
1225            set treeheight($prefix) $ht
1226            incr ht [lindex $htstack end]
1227            set htstack [lreplace $htstack end end]
1228            set prefixend [lindex $prefendstack end]
1229            set prefendstack [lreplace $prefendstack end end]
1230            set prefix [string range $prefix 0 $prefixend]
1231            incr lev -1
1232        }
1233        set tail [string range $f [expr {$prefixend+1}] end]
1234        while {[set slash [string first "/" $tail]] >= 0} {
1235            lappend htstack $ht
1236            set ht 0
1237            lappend prefendstack $prefixend
1238            incr prefixend [expr {$slash + 1}]
1239            set d [string range $tail 0 $slash]
1240            lappend treecontents($prefix) $d
1241            set oldprefix $prefix
1242            append prefix $d
1243            set treecontents($prefix) {}
1244            set treeindex($prefix) [incr ix]
1245            set treeparent($prefix) $oldprefix
1246            set tail [string range $tail [expr {$slash+1}] end]
1247            if {$lev <= $openlevs} {
1248                set ht 1
1249                set treediropen($prefix) [expr {$lev < $openlevs}]
1250                set bm [expr {$lev == $openlevs? "tri-rt": "tri-dn"}]
1251                $w mark set d:$ix "end -1c"
1252                $w mark gravity d:$ix left
1253                set str "\n"
1254                for {set i 0} {$i < $lev} {incr i} {append str "\t"}
1255                $w insert end $str
1256                $w image create end -align center -image $bm -padx 1 \
1257                    -name a:$ix
1258                $w insert end $d [highlight_tag $prefix]
1259                $w mark set s:$ix "end -1c"
1260                $w mark gravity s:$ix left
1261            }
1262            incr lev
1263        }
1264        if {$tail ne {}} {
1265            if {$lev <= $openlevs} {
1266                incr ht
1267                set str "\n"
1268                for {set i 0} {$i < $lev} {incr i} {append str "\t"}
1269                $w insert end $str
1270                $w insert end $tail [highlight_tag $f]
1271            }
1272            lappend treecontents($prefix) $tail
1273        }
1274    }
1275    while {$htstack ne {}} {
1276        set treeheight($prefix) $ht
1277        incr ht [lindex $htstack end]
1278        set htstack [lreplace $htstack end end]
1279        set prefixend [lindex $prefendstack end]
1280        set prefendstack [lreplace $prefendstack end end]
1281        set prefix [string range $prefix 0 $prefixend]
1282    }
1283    $w conf -state disabled
1284}
1285
1286proc linetoelt {l} {
1287    global treeheight treecontents
1288
1289    set y 2
1290    set prefix {}
1291    while {1} {
1292        foreach e $treecontents($prefix) {
1293            if {$y == $l} {
1294                return "$prefix$e"
1295            }
1296            set n 1
1297            if {[string index $e end] eq "/"} {
1298                set n $treeheight($prefix$e)
1299                if {$y + $n > $l} {
1300                    append prefix $e
1301                    incr y
1302                    break
1303                }
1304            }
1305            incr y $n
1306        }
1307    }
1308}
1309
1310proc highlight_tree {y prefix} {
1311    global treeheight treecontents cflist
1312
1313    foreach e $treecontents($prefix) {
1314        set path $prefix$e
1315        if {[highlight_tag $path] ne {}} {
1316            $cflist tag add bold $y.0 "$y.0 lineend"
1317        }
1318        incr y
1319        if {[string index $e end] eq "/" && $treeheight($path) > 1} {
1320            set y [highlight_tree $y $path]
1321        }
1322    }
1323    return $y
1324}
1325
1326proc treeclosedir {w dir} {
1327    global treediropen treeheight treeparent treeindex
1328
1329    set ix $treeindex($dir)
1330    $w conf -state normal
1331    $w delete s:$ix e:$ix
1332    set treediropen($dir) 0
1333    $w image configure a:$ix -image tri-rt
1334    $w conf -state disabled
1335    set n [expr {1 - $treeheight($dir)}]
1336    while {$dir ne {}} {
1337        incr treeheight($dir) $n
1338        set dir $treeparent($dir)
1339    }
1340}
1341
1342proc treeopendir {w dir} {
1343    global treediropen treeheight treeparent treecontents treeindex
1344
1345    set ix $treeindex($dir)
1346    $w conf -state normal
1347    $w image configure a:$ix -image tri-dn
1348    $w mark set e:$ix s:$ix
1349    $w mark gravity e:$ix right
1350    set lev 0
1351    set str "\n"
1352    set n [llength $treecontents($dir)]
1353    for {set x $dir} {$x ne {}} {set x $treeparent($x)} {
1354        incr lev
1355        append str "\t"
1356        incr treeheight($x) $n
1357    }
1358    foreach e $treecontents($dir) {
1359        set de $dir$e
1360        if {[string index $e end] eq "/"} {
1361            set iy $treeindex($de)
1362            $w mark set d:$iy e:$ix
1363            $w mark gravity d:$iy left
1364            $w insert e:$ix $str
1365            set treediropen($de) 0
1366            $w image create e:$ix -align center -image tri-rt -padx 1 \
1367                -name a:$iy
1368            $w insert e:$ix $e [highlight_tag $de]
1369            $w mark set s:$iy e:$ix
1370            $w mark gravity s:$iy left
1371            set treeheight($de) 1
1372        } else {
1373            $w insert e:$ix $str
1374            $w insert e:$ix $e [highlight_tag $de]
1375        }
1376    }
1377    $w mark gravity e:$ix left
1378    $w conf -state disabled
1379    set treediropen($dir) 1
1380    set top [lindex [split [$w index @0,0] .] 0]
1381    set ht [$w cget -height]
1382    set l [lindex [split [$w index s:$ix] .] 0]
1383    if {$l < $top} {
1384        $w yview $l.0
1385    } elseif {$l + $n + 1 > $top + $ht} {
1386        set top [expr {$l + $n + 2 - $ht}]
1387        if {$l < $top} {
1388            set top $l
1389        }
1390        $w yview $top.0
1391    }
1392}
1393
1394proc treeclick {w x y} {
1395    global treediropen cmitmode ctext cflist cflist_top
1396
1397    if {$cmitmode ne "tree"} return
1398    if {![info exists cflist_top]} return
1399    set l [lindex [split [$w index "@$x,$y"] "."] 0]
1400    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
1401    $cflist tag add highlight $l.0 "$l.0 lineend"
1402    set cflist_top $l
1403    if {$l == 1} {
1404        $ctext yview 1.0
1405        return
1406    }
1407    set e [linetoelt $l]
1408    if {[string index $e end] ne "/"} {
1409        showfile $e
1410    } elseif {$treediropen($e)} {
1411        treeclosedir $w $e
1412    } else {
1413        treeopendir $w $e
1414    }
1415}
1416
1417proc setfilelist {id} {
1418    global treefilelist cflist
1419
1420    treeview $cflist $treefilelist($id) 0
1421}
1422
1423image create bitmap tri-rt -background black -foreground blue -data {
1424    #define tri-rt_width 13
1425    #define tri-rt_height 13
1426    static unsigned char tri-rt_bits[] = {
1427       0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x70, 0x00, 0xf0, 0x00,
1428       0xf0, 0x01, 0xf0, 0x00, 0x70, 0x00, 0x30, 0x00, 0x10, 0x00, 0x00, 0x00,
1429       0x00, 0x00};
1430} -maskdata {
1431    #define tri-rt-mask_width 13
1432    #define tri-rt-mask_height 13
1433    static unsigned char tri-rt-mask_bits[] = {
1434       0x08, 0x00, 0x18, 0x00, 0x38, 0x00, 0x78, 0x00, 0xf8, 0x00, 0xf8, 0x01,
1435       0xf8, 0x03, 0xf8, 0x01, 0xf8, 0x00, 0x78, 0x00, 0x38, 0x00, 0x18, 0x00,
1436       0x08, 0x00};
1437}
1438image create bitmap tri-dn -background black -foreground blue -data {
1439    #define tri-dn_width 13
1440    #define tri-dn_height 13
1441    static unsigned char tri-dn_bits[] = {
1442       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x07, 0xf8, 0x03,
1443       0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1444       0x00, 0x00};
1445} -maskdata {
1446    #define tri-dn-mask_width 13
1447    #define tri-dn-mask_height 13
1448    static unsigned char tri-dn-mask_bits[] = {
1449       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x1f, 0xfe, 0x0f, 0xfc, 0x07,
1450       0xf8, 0x03, 0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
1451       0x00, 0x00};
1452}
1453
1454proc init_flist {first} {
1455    global cflist cflist_top selectedline difffilestart
1456
1457    $cflist conf -state normal
1458    $cflist delete 0.0 end
1459    if {$first ne {}} {
1460        $cflist insert end $first
1461        set cflist_top 1
1462        $cflist tag add highlight 1.0 "1.0 lineend"
1463    } else {
1464        catch {unset cflist_top}
1465    }
1466    $cflist conf -state disabled
1467    set difffilestart {}
1468}
1469
1470proc highlight_tag {f} {
1471    global highlight_paths
1472
1473    foreach p $highlight_paths {
1474        if {[string match $p $f]} {
1475            return "bold"
1476        }
1477    }
1478    return {}
1479}
1480
1481proc highlight_filelist {} {
1482    global cmitmode cflist
1483
1484    $cflist conf -state normal
1485    if {$cmitmode ne "tree"} {
1486        set end [lindex [split [$cflist index end] .] 0]
1487        for {set l 2} {$l < $end} {incr l} {
1488            set line [$cflist get $l.0 "$l.0 lineend"]
1489            if {[highlight_tag $line] ne {}} {
1490                $cflist tag add bold $l.0 "$l.0 lineend"
1491            }
1492        }
1493    } else {
1494        highlight_tree 2 {}
1495    }
1496    $cflist conf -state disabled
1497}
1498
1499proc unhighlight_filelist {} {
1500    global cflist
1501
1502    $cflist conf -state normal
1503    $cflist tag remove bold 1.0 end
1504    $cflist conf -state disabled
1505}
1506
1507proc add_flist {fl} {
1508    global cflist
1509
1510    $cflist conf -state normal
1511    foreach f $fl {
1512        $cflist insert end "\n"
1513        $cflist insert end $f [highlight_tag $f]
1514    }
1515    $cflist conf -state disabled
1516}
1517
1518proc sel_flist {w x y} {
1519    global ctext difffilestart cflist cflist_top cmitmode
1520
1521    if {$cmitmode eq "tree"} return
1522    if {![info exists cflist_top]} return
1523    set l [lindex [split [$w index "@$x,$y"] "."] 0]
1524    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
1525    $cflist tag add highlight $l.0 "$l.0 lineend"
1526    set cflist_top $l
1527    if {$l == 1} {
1528        $ctext yview 1.0
1529    } else {
1530        catch {$ctext yview [lindex $difffilestart [expr {$l - 2}]]}
1531    }
1532}
1533
1534proc pop_flist_menu {w X Y x y} {
1535    global ctext cflist cmitmode flist_menu flist_menu_file
1536    global treediffs diffids
1537
1538    set l [lindex [split [$w index "@$x,$y"] "."] 0]
1539    if {$l <= 1} return
1540    if {$cmitmode eq "tree"} {
1541        set e [linetoelt $l]
1542        if {[string index $e end] eq "/"} return
1543    } else {
1544        set e [lindex $treediffs($diffids) [expr {$l-2}]]
1545    }
1546    set flist_menu_file $e
1547    tk_popup $flist_menu $X $Y
1548}
1549
1550proc flist_hl {only} {
1551    global flist_menu_file highlight_files
1552
1553    set x [shellquote $flist_menu_file]
1554    if {$only || $highlight_files eq {}} {
1555        set highlight_files $x
1556    } else {
1557        append highlight_files " " $x
1558    }
1559}
1560
1561# Functions for adding and removing shell-type quoting
1562
1563proc shellquote {str} {
1564    if {![string match "*\['\"\\ \t]*" $str]} {
1565        return $str
1566    }
1567    if {![string match "*\['\"\\]*" $str]} {
1568        return "\"$str\""
1569    }
1570    if {![string match "*'*" $str]} {
1571        return "'$str'"
1572    }
1573    return "\"[string map {\" \\\" \\ \\\\} $str]\""
1574}
1575
1576proc shellarglist {l} {
1577    set str {}
1578    foreach a $l {
1579        if {$str ne {}} {
1580            append str " "
1581        }
1582        append str [shellquote $a]
1583    }
1584    return $str
1585}
1586
1587proc shelldequote {str} {
1588    set ret {}
1589    set used -1
1590    while {1} {
1591        incr used
1592        if {![regexp -start $used -indices "\['\"\\\\ \t]" $str first]} {
1593            append ret [string range $str $used end]
1594            set used [string length $str]
1595            break
1596        }
1597        set first [lindex $first 0]
1598        set ch [string index $str $first]
1599        if {$first > $used} {
1600            append ret [string range $str $used [expr {$first - 1}]]
1601            set used $first
1602        }
1603        if {$ch eq " " || $ch eq "\t"} break
1604        incr used
1605        if {$ch eq "'"} {
1606            set first [string first "'" $str $used]
1607            if {$first < 0} {
1608                error "unmatched single-quote"
1609            }
1610            append ret [string range $str $used [expr {$first - 1}]]
1611            set used $first
1612            continue
1613        }
1614        if {$ch eq "\\"} {
1615            if {$used >= [string length $str]} {
1616                error "trailing backslash"
1617            }
1618            append ret [string index $str $used]
1619            continue
1620        }
1621        # here ch == "\""
1622        while {1} {
1623            if {![regexp -start $used -indices "\[\"\\\\]" $str first]} {
1624                error "unmatched double-quote"
1625            }
1626            set first [lindex $first 0]
1627            set ch [string index $str $first]
1628            if {$first > $used} {
1629                append ret [string range $str $used [expr {$first - 1}]]
1630                set used $first
1631            }
1632            if {$ch eq "\""} break
1633            incr used
1634            append ret [string index $str $used]
1635            incr used
1636        }
1637    }
1638    return [list $used $ret]
1639}
1640
1641proc shellsplit {str} {
1642    set l {}
1643    while {1} {
1644        set str [string trimleft $str]
1645        if {$str eq {}} break
1646        set dq [shelldequote $str]
1647        set n [lindex $dq 0]
1648        set word [lindex $dq 1]
1649        set str [string range $str $n end]
1650        lappend l $word
1651    }
1652    return $l
1653}
1654
1655# Code to implement multiple views
1656
1657proc newview {ishighlight} {
1658    global nextviewnum newviewname newviewperm uifont newishighlight
1659    global newviewargs revtreeargs
1660
1661    set newishighlight $ishighlight
1662    set top .gitkview
1663    if {[winfo exists $top]} {
1664        raise $top
1665        return
1666    }
1667    set newviewname($nextviewnum) "View $nextviewnum"
1668    set newviewperm($nextviewnum) 0
1669    set newviewargs($nextviewnum) [shellarglist $revtreeargs]
1670    vieweditor $top $nextviewnum "Gitk view definition"
1671}
1672
1673proc editview {} {
1674    global curview
1675    global viewname viewperm newviewname newviewperm
1676    global viewargs newviewargs
1677
1678    set top .gitkvedit-$curview
1679    if {[winfo exists $top]} {
1680        raise $top
1681        return
1682    }
1683    set newviewname($curview) $viewname($curview)
1684    set newviewperm($curview) $viewperm($curview)
1685    set newviewargs($curview) [shellarglist $viewargs($curview)]
1686    vieweditor $top $curview "Gitk: edit view $viewname($curview)"
1687}
1688
1689proc vieweditor {top n title} {
1690    global newviewname newviewperm viewfiles
1691    global uifont
1692
1693    toplevel $top
1694    wm title $top $title
1695    label $top.nl -text "Name" -font $uifont
1696    entry $top.name -width 20 -textvariable newviewname($n) -font $uifont
1697    grid $top.nl $top.name -sticky w -pady 5
1698    checkbutton $top.perm -text "Remember this view" -variable newviewperm($n) \
1699        -font $uifont
1700    grid $top.perm - -pady 5 -sticky w
1701    message $top.al -aspect 1000 -font $uifont \
1702        -text "Commits to include (arguments to git rev-list):"
1703    grid $top.al - -sticky w -pady 5
1704    entry $top.args -width 50 -textvariable newviewargs($n) \
1705        -background white -font $uifont
1706    grid $top.args - -sticky ew -padx 5
1707    message $top.l -aspect 1000 -font $uifont \
1708        -text "Enter files and directories to include, one per line:"
1709    grid $top.l - -sticky w
1710    text $top.t -width 40 -height 10 -background white -font $uifont
1711    if {[info exists viewfiles($n)]} {
1712        foreach f $viewfiles($n) {
1713            $top.t insert end $f
1714            $top.t insert end "\n"
1715        }
1716        $top.t delete {end - 1c} end
1717        $top.t mark set insert 0.0
1718    }
1719    grid $top.t - -sticky ew -padx 5
1720    frame $top.buts
1721    button $top.buts.ok -text "OK" -command [list newviewok $top $n] \
1722        -font $uifont
1723    button $top.buts.can -text "Cancel" -command [list destroy $top] \
1724        -font $uifont
1725    grid $top.buts.ok $top.buts.can
1726    grid columnconfigure $top.buts 0 -weight 1 -uniform a
1727    grid columnconfigure $top.buts 1 -weight 1 -uniform a
1728    grid $top.buts - -pady 10 -sticky ew
1729    focus $top.t
1730}
1731
1732proc doviewmenu {m first cmd op argv} {
1733    set nmenu [$m index end]
1734    for {set i $first} {$i <= $nmenu} {incr i} {
1735        if {[$m entrycget $i -command] eq $cmd} {
1736            eval $m $op $i $argv
1737            break
1738        }
1739    }
1740}
1741
1742proc allviewmenus {n op args} {
1743    global viewhlmenu
1744
1745    doviewmenu .bar.view 5 [list showview $n] $op $args
1746    doviewmenu $viewhlmenu 1 [list addvhighlight $n] $op $args
1747}
1748
1749proc newviewok {top n} {
1750    global nextviewnum newviewperm newviewname newishighlight
1751    global viewname viewfiles viewperm selectedview curview
1752    global viewargs newviewargs viewhlmenu
1753
1754    if {[catch {
1755        set newargs [shellsplit $newviewargs($n)]
1756    } err]} {
1757        error_popup "Error in commit selection arguments: $err"
1758        wm raise $top
1759        focus $top
1760        return
1761    }
1762    set files {}
1763    foreach f [split [$top.t get 0.0 end] "\n"] {
1764        set ft [string trim $f]
1765        if {$ft ne {}} {
1766            lappend files $ft
1767        }
1768    }
1769    if {![info exists viewfiles($n)]} {
1770        # creating a new view
1771        incr nextviewnum
1772        set viewname($n) $newviewname($n)
1773        set viewperm($n) $newviewperm($n)
1774        set viewfiles($n) $files
1775        set viewargs($n) $newargs
1776        addviewmenu $n
1777        if {!$newishighlight} {
1778            run showview $n
1779        } else {
1780            run addvhighlight $n
1781        }
1782    } else {
1783        # editing an existing view
1784        set viewperm($n) $newviewperm($n)
1785        if {$newviewname($n) ne $viewname($n)} {
1786            set viewname($n) $newviewname($n)
1787            doviewmenu .bar.view 5 [list showview $n] \
1788                entryconf [list -label $viewname($n)]
1789            doviewmenu $viewhlmenu 1 [list addvhighlight $n] \
1790                entryconf [list -label $viewname($n) -value $viewname($n)]
1791        }
1792        if {$files ne $viewfiles($n) || $newargs ne $viewargs($n)} {
1793            set viewfiles($n) $files
1794            set viewargs($n) $newargs
1795            if {$curview == $n} {
1796                run updatecommits
1797            }
1798        }
1799    }
1800    catch {destroy $top}
1801}
1802
1803proc delview {} {
1804    global curview viewdata viewperm hlview selectedhlview
1805
1806    if {$curview == 0} return
1807    if {[info exists hlview] && $hlview == $curview} {
1808        set selectedhlview None
1809        unset hlview
1810    }
1811    allviewmenus $curview delete
1812    set viewdata($curview) {}
1813    set viewperm($curview) 0
1814    showview 0
1815}
1816
1817proc addviewmenu {n} {
1818    global viewname viewhlmenu
1819
1820    .bar.view add radiobutton -label $viewname($n) \
1821        -command [list showview $n] -variable selectedview -value $n
1822    $viewhlmenu add radiobutton -label $viewname($n) \
1823        -command [list addvhighlight $n] -variable selectedhlview
1824}
1825
1826proc flatten {var} {
1827    global $var
1828
1829    set ret {}
1830    foreach i [array names $var] {
1831        lappend ret $i [set $var\($i\)]
1832    }
1833    return $ret
1834}
1835
1836proc unflatten {var l} {
1837    global $var
1838
1839    catch {unset $var}
1840    foreach {i v} $l {
1841        set $var\($i\) $v
1842    }
1843}
1844
1845proc showview {n} {
1846    global curview viewdata viewfiles
1847    global displayorder parentlist rowidlist rowoffsets
1848    global colormap rowtextx commitrow nextcolor canvxmax
1849    global numcommits rowrangelist commitlisted idrowranges rowchk
1850    global selectedline currentid canv canvy0
1851    global treediffs
1852    global pending_select phase
1853    global commitidx rowlaidout rowoptim
1854    global commfd
1855    global selectedview selectfirst
1856    global vparentlist vdisporder vcmitlisted
1857    global hlview selectedhlview
1858
1859    if {$n == $curview} return
1860    set selid {}
1861    if {[info exists selectedline]} {
1862        set selid $currentid
1863        set y [yc $selectedline]
1864        set ymax [lindex [$canv cget -scrollregion] 3]
1865        set span [$canv yview]
1866        set ytop [expr {[lindex $span 0] * $ymax}]
1867        set ybot [expr {[lindex $span 1] * $ymax}]
1868        if {$ytop < $y && $y < $ybot} {
1869            set yscreen [expr {$y - $ytop}]
1870        } else {
1871            set yscreen [expr {($ybot - $ytop) / 2}]
1872        }
1873    } elseif {[info exists pending_select]} {
1874        set selid $pending_select
1875        unset pending_select
1876    }
1877    unselectline
1878    normalline
1879    if {$curview >= 0} {
1880        set vparentlist($curview) $parentlist
1881        set vdisporder($curview) $displayorder
1882        set vcmitlisted($curview) $commitlisted
1883        if {$phase ne {}} {
1884            set viewdata($curview) \
1885                [list $phase $rowidlist $rowoffsets $rowrangelist \
1886                     [flatten idrowranges] [flatten idinlist] \
1887                     $rowlaidout $rowoptim $numcommits]
1888        } elseif {![info exists viewdata($curview)]
1889                  || [lindex $viewdata($curview) 0] ne {}} {
1890            set viewdata($curview) \
1891                [list {} $rowidlist $rowoffsets $rowrangelist]
1892        }
1893    }
1894    catch {unset treediffs}
1895    clear_display
1896    if {[info exists hlview] && $hlview == $n} {
1897        unset hlview
1898        set selectedhlview None
1899    }
1900
1901    set curview $n
1902    set selectedview $n
1903    .bar.view entryconf Edit* -state [expr {$n == 0? "disabled": "normal"}]
1904    .bar.view entryconf Delete* -state [expr {$n == 0? "disabled": "normal"}]
1905
1906    if {![info exists viewdata($n)]} {
1907        if {$selid ne {}} {
1908            set pending_select $selid
1909        }
1910        getcommits
1911        return
1912    }
1913
1914    set v $viewdata($n)
1915    set phase [lindex $v 0]
1916    set displayorder $vdisporder($n)
1917    set parentlist $vparentlist($n)
1918    set commitlisted $vcmitlisted($n)
1919    set rowidlist [lindex $v 1]
1920    set rowoffsets [lindex $v 2]
1921    set rowrangelist [lindex $v 3]
1922    if {$phase eq {}} {
1923        set numcommits [llength $displayorder]
1924        catch {unset idrowranges}
1925    } else {
1926        unflatten idrowranges [lindex $v 4]
1927        unflatten idinlist [lindex $v 5]
1928        set rowlaidout [lindex $v 6]
1929        set rowoptim [lindex $v 7]
1930        set numcommits [lindex $v 8]
1931        catch {unset rowchk}
1932    }
1933
1934    catch {unset colormap}
1935    catch {unset rowtextx}
1936    set nextcolor 0
1937    set canvxmax [$canv cget -width]
1938    set curview $n
1939    set row 0
1940    setcanvscroll
1941    set yf 0
1942    set row {}
1943    set selectfirst 0
1944    if {$selid ne {} && [info exists commitrow($n,$selid)]} {
1945        set row $commitrow($n,$selid)
1946        # try to get the selected row in the same position on the screen
1947        set ymax [lindex [$canv cget -scrollregion] 3]
1948        set ytop [expr {[yc $row] - $yscreen}]
1949        if {$ytop < 0} {
1950            set ytop 0
1951        }
1952        set yf [expr {$ytop * 1.0 / $ymax}]
1953    }
1954    allcanvs yview moveto $yf
1955    drawvisible
1956    if {$row ne {}} {
1957        selectline $row 0
1958    } elseif {$selid ne {}} {
1959        set pending_select $selid
1960    } else {
1961        set row [first_real_row]
1962        if {$row < $numcommits} {
1963            selectline $row 0
1964        } else {
1965            set selectfirst 1
1966        }
1967    }
1968    if {$phase ne {}} {
1969        if {$phase eq "getcommits"} {
1970            show_status "Reading commits..."
1971        }
1972        run chewcommits $n
1973    } elseif {$numcommits == 0} {
1974        show_status "No commits selected"
1975    }
1976}
1977
1978# Stuff relating to the highlighting facility
1979
1980proc ishighlighted {row} {
1981    global vhighlights fhighlights nhighlights rhighlights
1982
1983    if {[info exists nhighlights($row)] && $nhighlights($row) > 0} {
1984        return $nhighlights($row)
1985    }
1986    if {[info exists vhighlights($row)] && $vhighlights($row) > 0} {
1987        return $vhighlights($row)
1988    }
1989    if {[info exists fhighlights($row)] && $fhighlights($row) > 0} {
1990        return $fhighlights($row)
1991    }
1992    if {[info exists rhighlights($row)] && $rhighlights($row) > 0} {
1993        return $rhighlights($row)
1994    }
1995    return 0
1996}
1997
1998proc bolden {row font} {
1999    global canv linehtag selectedline boldrows
2000
2001    lappend boldrows $row
2002    $canv itemconf $linehtag($row) -font $font
2003    if {[info exists selectedline] && $row == $selectedline} {
2004        $canv delete secsel
2005        set t [eval $canv create rect [$canv bbox $linehtag($row)] \
2006                   -outline {{}} -tags secsel \
2007                   -fill [$canv cget -selectbackground]]
2008        $canv lower $t
2009    }
2010}
2011
2012proc bolden_name {row font} {
2013    global canv2 linentag selectedline boldnamerows
2014
2015    lappend boldnamerows $row
2016    $canv2 itemconf $linentag($row) -font $font
2017    if {[info exists selectedline] && $row == $selectedline} {
2018        $canv2 delete secsel
2019        set t [eval $canv2 create rect [$canv2 bbox $linentag($row)] \
2020                   -outline {{}} -tags secsel \
2021                   -fill [$canv2 cget -selectbackground]]
2022        $canv2 lower $t
2023    }
2024}
2025
2026proc unbolden {} {
2027    global mainfont boldrows
2028
2029    set stillbold {}
2030    foreach row $boldrows {
2031        if {![ishighlighted $row]} {
2032            bolden $row $mainfont
2033        } else {
2034            lappend stillbold $row
2035        }
2036    }
2037    set boldrows $stillbold
2038}
2039
2040proc addvhighlight {n} {
2041    global hlview curview viewdata vhl_done vhighlights commitidx
2042
2043    if {[info exists hlview]} {
2044        delvhighlight
2045    }
2046    set hlview $n
2047    if {$n != $curview && ![info exists viewdata($n)]} {
2048        set viewdata($n) [list getcommits {{}} {{}} {} {} {} 0 0 0 {}]
2049        set vparentlist($n) {}
2050        set vdisporder($n) {}
2051        set vcmitlisted($n) {}
2052        start_rev_list $n
2053    }
2054    set vhl_done $commitidx($hlview)
2055    if {$vhl_done > 0} {
2056        drawvisible
2057    }
2058}
2059
2060proc delvhighlight {} {
2061    global hlview vhighlights
2062
2063    if {![info exists hlview]} return
2064    unset hlview
2065    catch {unset vhighlights}
2066    unbolden
2067}
2068
2069proc vhighlightmore {} {
2070    global hlview vhl_done commitidx vhighlights
2071    global displayorder vdisporder curview mainfont
2072
2073    set font [concat $mainfont bold]
2074    set max $commitidx($hlview)
2075    if {$hlview == $curview} {
2076        set disp $displayorder
2077    } else {
2078        set disp $vdisporder($hlview)
2079    }
2080    set vr [visiblerows]
2081    set r0 [lindex $vr 0]
2082    set r1 [lindex $vr 1]
2083    for {set i $vhl_done} {$i < $max} {incr i} {
2084        set id [lindex $disp $i]
2085        if {[info exists commitrow($curview,$id)]} {
2086            set row $commitrow($curview,$id)
2087            if {$r0 <= $row && $row <= $r1} {
2088                if {![highlighted $row]} {
2089                    bolden $row $font
2090                }
2091                set vhighlights($row) 1
2092            }
2093        }
2094    }
2095    set vhl_done $max
2096}
2097
2098proc askvhighlight {row id} {
2099    global hlview vhighlights commitrow iddrawn mainfont
2100
2101    if {[info exists commitrow($hlview,$id)]} {
2102        if {[info exists iddrawn($id)] && ![ishighlighted $row]} {
2103            bolden $row [concat $mainfont bold]
2104        }
2105        set vhighlights($row) 1
2106    } else {
2107        set vhighlights($row) 0
2108    }
2109}
2110
2111proc hfiles_change {name ix op} {
2112    global highlight_files filehighlight fhighlights fh_serial
2113    global mainfont highlight_paths
2114
2115    if {[info exists filehighlight]} {
2116        # delete previous highlights
2117        catch {close $filehighlight}
2118        unset filehighlight
2119        catch {unset fhighlights}
2120        unbolden
2121        unhighlight_filelist
2122    }
2123    set highlight_paths {}
2124    after cancel do_file_hl $fh_serial
2125    incr fh_serial
2126    if {$highlight_files ne {}} {
2127        after 300 do_file_hl $fh_serial
2128    }
2129}
2130
2131proc makepatterns {l} {
2132    set ret {}
2133    foreach e $l {
2134        set ee [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} $e]
2135        if {[string index $ee end] eq "/"} {
2136            lappend ret "$ee*"
2137        } else {
2138            lappend ret $ee
2139            lappend ret "$ee/*"
2140        }
2141    }
2142    return $ret
2143}
2144
2145proc do_file_hl {serial} {
2146    global highlight_files filehighlight highlight_paths gdttype fhl_list
2147
2148    if {$gdttype eq "touching paths:"} {
2149        if {[catch {set paths [shellsplit $highlight_files]}]} return
2150        set highlight_paths [makepatterns $paths]
2151        highlight_filelist
2152        set gdtargs [concat -- $paths]
2153    } else {
2154        set gdtargs [list "-S$highlight_files"]
2155    }
2156    set cmd [concat | git diff-tree -r -s --stdin $gdtargs]
2157    set filehighlight [open $cmd r+]
2158    fconfigure $filehighlight -blocking 0
2159    filerun $filehighlight readfhighlight
2160    set fhl_list {}
2161    drawvisible
2162    flushhighlights
2163}
2164
2165proc flushhighlights {} {
2166    global filehighlight fhl_list
2167
2168    if {[info exists filehighlight]} {
2169        lappend fhl_list {}
2170        puts $filehighlight ""
2171        flush $filehighlight
2172    }
2173}
2174
2175proc askfilehighlight {row id} {
2176    global filehighlight fhighlights fhl_list
2177
2178    lappend fhl_list $id
2179    set fhighlights($row) -1
2180    puts $filehighlight $id
2181}
2182
2183proc readfhighlight {} {
2184    global filehighlight fhighlights commitrow curview mainfont iddrawn
2185    global fhl_list
2186
2187    if {![info exists filehighlight]} {
2188        return 0
2189    }
2190    set nr 0
2191    while {[incr nr] <= 100 && [gets $filehighlight line] >= 0} {
2192        set line [string trim $line]
2193        set i [lsearch -exact $fhl_list $line]
2194        if {$i < 0} continue
2195        for {set j 0} {$j < $i} {incr j} {
2196            set id [lindex $fhl_list $j]
2197            if {[info exists commitrow($curview,$id)]} {
2198                set fhighlights($commitrow($curview,$id)) 0
2199            }
2200        }
2201        set fhl_list [lrange $fhl_list [expr {$i+1}] end]
2202        if {$line eq {}} continue
2203        if {![info exists commitrow($curview,$line)]} continue
2204        set row $commitrow($curview,$line)
2205        if {[info exists iddrawn($line)] && ![ishighlighted $row]} {
2206            bolden $row [concat $mainfont bold]
2207        }
2208        set fhighlights($row) 1
2209    }
2210    if {[eof $filehighlight]} {
2211        # strange...
2212        puts "oops, git diff-tree died"
2213        catch {close $filehighlight}
2214        unset filehighlight
2215        return 0
2216    }
2217    next_hlcont
2218    return 1
2219}
2220
2221proc find_change {name ix op} {
2222    global nhighlights mainfont boldnamerows
2223    global findstring findpattern findtype
2224
2225    # delete previous highlights, if any
2226    foreach row $boldnamerows {
2227        bolden_name $row $mainfont
2228    }
2229    set boldnamerows {}
2230    catch {unset nhighlights}
2231    unbolden
2232    unmarkmatches
2233    if {$findtype ne "Regexp"} {
2234        set e [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} \
2235                   $findstring]
2236        set findpattern "*$e*"
2237    }
2238    drawvisible
2239}
2240
2241proc doesmatch {f} {
2242    global findtype findstring findpattern
2243
2244    if {$findtype eq "Regexp"} {
2245        return [regexp $findstring $f]
2246    } elseif {$findtype eq "IgnCase"} {
2247        return [string match -nocase $findpattern $f]
2248    } else {
2249        return [string match $findpattern $f]
2250    }
2251}
2252
2253proc askfindhighlight {row id} {
2254    global nhighlights commitinfo iddrawn mainfont
2255    global findloc
2256    global markingmatches
2257
2258    if {![info exists commitinfo($id)]} {
2259        getcommit $id
2260    }
2261    set info $commitinfo($id)
2262    set isbold 0
2263    set fldtypes {Headline Author Date Committer CDate Comments}
2264    foreach f $info ty $fldtypes {
2265        if {($findloc eq "All fields" || $findloc eq $ty) &&
2266            [doesmatch $f]} {
2267            if {$ty eq "Author"} {
2268                set isbold 2
2269                break
2270            }
2271            set isbold 1
2272        }
2273    }
2274    if {$isbold && [info exists iddrawn($id)]} {
2275        set f [concat $mainfont bold]
2276        if {![ishighlighted $row]} {
2277            bolden $row $f
2278            if {$isbold > 1} {
2279                bolden_name $row $f
2280            }
2281        }
2282        if {$markingmatches} {
2283            markrowmatches $row $id
2284        }
2285    }
2286    set nhighlights($row) $isbold
2287}
2288
2289proc markrowmatches {row id} {
2290    global canv canv2 linehtag linentag commitinfo findloc
2291
2292    set headline [lindex $commitinfo($id) 0]
2293    set author [lindex $commitinfo($id) 1]
2294    $canv delete match$row
2295    $canv2 delete match$row
2296    if {$findloc eq "All fields" || $findloc eq "Headline"} {
2297        set m [findmatches $headline]
2298        if {$m ne {}} {
2299            markmatches $canv $row $headline $linehtag($row) $m \
2300                [$canv itemcget $linehtag($row) -font] $row
2301        }
2302    }
2303    if {$findloc eq "All fields" || $findloc eq "Author"} {
2304        set m [findmatches $author]
2305        if {$m ne {}} {
2306            markmatches $canv2 $row $author $linentag($row) $m \
2307                [$canv2 itemcget $linentag($row) -font] $row
2308        }
2309    }
2310}
2311
2312proc vrel_change {name ix op} {
2313    global highlight_related
2314
2315    rhighlight_none
2316    if {$highlight_related ne "None"} {
2317        run drawvisible
2318    }
2319}
2320
2321# prepare for testing whether commits are descendents or ancestors of a
2322proc rhighlight_sel {a} {
2323    global descendent desc_todo ancestor anc_todo
2324    global highlight_related rhighlights
2325
2326    catch {unset descendent}
2327    set desc_todo [list $a]
2328    catch {unset ancestor}
2329    set anc_todo [list $a]
2330    if {$highlight_related ne "None"} {
2331        rhighlight_none
2332        run drawvisible
2333    }
2334}
2335
2336proc rhighlight_none {} {
2337    global rhighlights
2338
2339    catch {unset rhighlights}
2340    unbolden
2341}
2342
2343proc is_descendent {a} {
2344    global curview children commitrow descendent desc_todo
2345
2346    set v $curview
2347    set la $commitrow($v,$a)
2348    set todo $desc_todo
2349    set leftover {}
2350    set done 0
2351    for {set i 0} {$i < [llength $todo]} {incr i} {
2352        set do [lindex $todo $i]
2353        if {$commitrow($v,$do) < $la} {
2354            lappend leftover $do
2355            continue
2356        }
2357        foreach nk $children($v,$do) {
2358            if {![info exists descendent($nk)]} {
2359                set descendent($nk) 1
2360                lappend todo $nk
2361                if {$nk eq $a} {
2362                    set done 1
2363                }
2364            }
2365        }
2366        if {$done} {
2367            set desc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
2368            return
2369        }
2370    }
2371    set descendent($a) 0
2372    set desc_todo $leftover
2373}
2374
2375proc is_ancestor {a} {
2376    global curview parentlist commitrow ancestor anc_todo
2377
2378    set v $curview
2379    set la $commitrow($v,$a)
2380    set todo $anc_todo
2381    set leftover {}
2382    set done 0
2383    for {set i 0} {$i < [llength $todo]} {incr i} {
2384        set do [lindex $todo $i]
2385        if {![info exists commitrow($v,$do)] || $commitrow($v,$do) > $la} {
2386            lappend leftover $do
2387            continue
2388        }
2389        foreach np [lindex $parentlist $commitrow($v,$do)] {
2390            if {![info exists ancestor($np)]} {
2391                set ancestor($np) 1
2392                lappend todo $np
2393                if {$np eq $a} {
2394                    set done 1
2395                }
2396            }
2397        }
2398        if {$done} {
2399            set anc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
2400            return
2401        }
2402    }
2403    set ancestor($a) 0
2404    set anc_todo $leftover
2405}
2406
2407proc askrelhighlight {row id} {
2408    global descendent highlight_related iddrawn mainfont rhighlights
2409    global selectedline ancestor
2410
2411    if {![info exists selectedline]} return
2412    set isbold 0
2413    if {$highlight_related eq "Descendent" ||
2414        $highlight_related eq "Not descendent"} {
2415        if {![info exists descendent($id)]} {
2416            is_descendent $id
2417        }
2418        if {$descendent($id) == ($highlight_related eq "Descendent")} {
2419            set isbold 1
2420        }
2421    } elseif {$highlight_related eq "Ancestor" ||
2422              $highlight_related eq "Not ancestor"} {
2423        if {![info exists ancestor($id)]} {
2424            is_ancestor $id
2425        }
2426        if {$ancestor($id) == ($highlight_related eq "Ancestor")} {
2427            set isbold 1
2428        }
2429    }
2430    if {[info exists iddrawn($id)]} {
2431        if {$isbold && ![ishighlighted $row]} {
2432            bolden $row [concat $mainfont bold]
2433        }
2434    }
2435    set rhighlights($row) $isbold
2436}
2437
2438proc next_hlcont {} {
2439    global fhl_row fhl_dirn displayorder numcommits
2440    global vhighlights fhighlights nhighlights rhighlights
2441    global hlview filehighlight findstring highlight_related
2442
2443    if {![info exists fhl_dirn] || $fhl_dirn == 0} return
2444    set row $fhl_row
2445    while {1} {
2446        if {$row < 0 || $row >= $numcommits} {
2447            bell
2448            set fhl_dirn 0
2449            return
2450        }
2451        set id [lindex $displayorder $row]
2452        if {[info exists hlview]} {
2453            if {![info exists vhighlights($row)]} {
2454                askvhighlight $row $id
2455            }
2456            if {$vhighlights($row) > 0} break
2457        }
2458        if {$findstring ne {}} {
2459            if {![info exists nhighlights($row)]} {
2460                askfindhighlight $row $id
2461            }
2462            if {$nhighlights($row) > 0} break
2463        }
2464        if {$highlight_related ne "None"} {
2465            if {![info exists rhighlights($row)]} {
2466                askrelhighlight $row $id
2467            }
2468            if {$rhighlights($row) > 0} break
2469        }
2470        if {[info exists filehighlight]} {
2471            if {![info exists fhighlights($row)]} {
2472                # ask for a few more while we're at it...
2473                set r $row
2474                for {set n 0} {$n < 100} {incr n} {
2475                    if {![info exists fhighlights($r)]} {
2476                        askfilehighlight $r [lindex $displayorder $r]
2477                    }
2478                    incr r $fhl_dirn
2479                    if {$r < 0 || $r >= $numcommits} break
2480                }
2481                flushhighlights
2482            }
2483            if {$fhighlights($row) < 0} {
2484                set fhl_row $row
2485                return
2486            }
2487            if {$fhighlights($row) > 0} break
2488        }
2489        incr row $fhl_dirn
2490    }
2491    set fhl_dirn 0
2492    selectline $row 1
2493}
2494
2495proc next_highlight {dirn} {
2496    global selectedline fhl_row fhl_dirn
2497    global hlview filehighlight findstring highlight_related
2498
2499    if {![info exists selectedline]} return
2500    if {!([info exists hlview] || $findstring ne {} ||
2501          $highlight_related ne "None" || [info exists filehighlight])} return
2502    set fhl_row [expr {$selectedline + $dirn}]
2503    set fhl_dirn $dirn
2504    next_hlcont
2505}
2506
2507proc cancel_next_highlight {} {
2508    global fhl_dirn
2509
2510    set fhl_dirn 0
2511}
2512
2513# Graph layout functions
2514
2515proc shortids {ids} {
2516    set res {}
2517    foreach id $ids {
2518        if {[llength $id] > 1} {
2519            lappend res [shortids $id]
2520        } elseif {[regexp {^[0-9a-f]{40}$} $id]} {
2521            lappend res [string range $id 0 7]
2522        } else {
2523            lappend res $id
2524        }
2525    }
2526    return $res
2527}
2528
2529proc incrange {l x o} {
2530    set n [llength $l]
2531    while {$x < $n} {
2532        set e [lindex $l $x]
2533        if {$e ne {}} {
2534            lset l $x [expr {$e + $o}]
2535        }
2536        incr x
2537    }
2538    return $l
2539}
2540
2541proc ntimes {n o} {
2542    set ret {}
2543    for {} {$n > 0} {incr n -1} {
2544        lappend ret $o
2545    }
2546    return $ret
2547}
2548
2549proc usedinrange {id l1 l2} {
2550    global children commitrow curview
2551
2552    if {[info exists commitrow($curview,$id)]} {
2553        set r $commitrow($curview,$id)
2554        if {$l1 <= $r && $r <= $l2} {
2555            return [expr {$r - $l1 + 1}]
2556        }
2557    }
2558    set kids $children($curview,$id)
2559    foreach c $kids {
2560        set r $commitrow($curview,$c)
2561        if {$l1 <= $r && $r <= $l2} {
2562            return [expr {$r - $l1 + 1}]
2563        }
2564    }
2565    return 0
2566}
2567
2568proc sanity {row {full 0}} {
2569    global rowidlist rowoffsets
2570
2571    set col -1
2572    set ids [lindex $rowidlist $row]
2573    foreach id $ids {
2574        incr col
2575        if {$id eq {}} continue
2576        if {$col < [llength $ids] - 1 &&
2577            [lsearch -exact -start [expr {$col+1}] $ids $id] >= 0} {
2578            puts "oops: [shortids $id] repeated in row $row col $col: {[shortids [lindex $rowidlist $row]]}"
2579        }
2580        set o [lindex $rowoffsets $row $col]
2581        set y $row
2582        set x $col
2583        while {$o ne {}} {
2584            incr y -1
2585            incr x $o
2586            if {[lindex $rowidlist $y $x] != $id} {
2587                puts "oops: rowoffsets wrong at row [expr {$y+1}] col [expr {$x-$o}]"
2588                puts "  id=[shortids $id] check started at row $row"
2589                for {set i $row} {$i >= $y} {incr i -1} {
2590                    puts "  row $i ids={[shortids [lindex $rowidlist $i]]} offs={[lindex $rowoffsets $i]}"
2591                }
2592                break
2593            }
2594            if {!$full} break
2595            set o [lindex $rowoffsets $y $x]
2596        }
2597    }
2598}
2599
2600proc makeuparrow {oid x y z} {
2601    global rowidlist rowoffsets uparrowlen idrowranges displayorder
2602
2603    for {set i 1} {$i < $uparrowlen && $y > 1} {incr i} {
2604        incr y -1
2605        incr x $z
2606        set off0 [lindex $rowoffsets $y]
2607        for {set x0 $x} {1} {incr x0} {
2608            if {$x0 >= [llength $off0]} {
2609                set x0 [llength [lindex $rowoffsets [expr {$y-1}]]]
2610                break
2611            }
2612            set z [lindex $off0 $x0]
2613            if {$z ne {}} {
2614                incr x0 $z
2615                break
2616            }
2617        }
2618        set z [expr {$x0 - $x}]
2619        lset rowidlist $y [linsert [lindex $rowidlist $y] $x $oid]
2620        lset rowoffsets $y [linsert [lindex $rowoffsets $y] $x $z]
2621    }
2622    set tmp [lreplace [lindex $rowoffsets $y] $x $x {}]
2623    lset rowoffsets $y [incrange $tmp [expr {$x+1}] -1]
2624    lappend idrowranges($oid) [lindex $displayorder $y]
2625}
2626
2627proc initlayout {} {
2628    global rowidlist rowoffsets displayorder commitlisted
2629    global rowlaidout rowoptim
2630    global idinlist rowchk rowrangelist idrowranges
2631    global numcommits canvxmax canv
2632    global nextcolor
2633    global parentlist
2634    global colormap rowtextx
2635    global selectfirst
2636
2637    set numcommits 0
2638    set displayorder {}
2639    set commitlisted {}
2640    set parentlist {}
2641    set rowrangelist {}
2642    set nextcolor 0
2643    set rowidlist {{}}
2644    set rowoffsets {{}}
2645    catch {unset idinlist}
2646    catch {unset rowchk}
2647    set rowlaidout 0
2648    set rowoptim 0
2649    set canvxmax [$canv cget -width]
2650    catch {unset colormap}
2651    catch {unset rowtextx}
2652    catch {unset idrowranges}
2653    set selectfirst 1
2654}
2655
2656proc setcanvscroll {} {
2657    global canv canv2 canv3 numcommits linespc canvxmax canvy0
2658
2659    set ymax [expr {$canvy0 + ($numcommits - 0.5) * $linespc + 2}]
2660    $canv conf -scrollregion [list 0 0 $canvxmax $ymax]
2661    $canv2 conf -scrollregion [list 0 0 0 $ymax]
2662    $canv3 conf -scrollregion [list 0 0 0 $ymax]
2663}
2664
2665proc visiblerows {} {
2666    global canv numcommits linespc
2667
2668    set ymax [lindex [$canv cget -scrollregion] 3]
2669    if {$ymax eq {} || $ymax == 0} return
2670    set f [$canv yview]
2671    set y0 [expr {int([lindex $f 0] * $ymax)}]
2672    set r0 [expr {int(($y0 - 3) / $linespc) - 1}]
2673    if {$r0 < 0} {
2674        set r0 0
2675    }
2676    set y1 [expr {int([lindex $f 1] * $ymax)}]
2677    set r1 [expr {int(($y1 - 3) / $linespc) + 1}]
2678    if {$r1 >= $numcommits} {
2679        set r1 [expr {$numcommits - 1}]
2680    }
2681    return [list $r0 $r1]
2682}
2683
2684proc layoutmore {tmax allread} {
2685    global rowlaidout rowoptim commitidx numcommits optim_delay
2686    global uparrowlen curview rowidlist idinlist
2687
2688    set showlast 0
2689    set showdelay $optim_delay
2690    set optdelay [expr {$uparrowlen + 1}]
2691    while {1} {
2692        if {$rowoptim - $showdelay > $numcommits} {
2693            showstuff [expr {$rowoptim - $showdelay}] $showlast
2694        } elseif {$rowlaidout - $optdelay > $rowoptim} {
2695            set nr [expr {$rowlaidout - $optdelay - $rowoptim}]
2696            if {$nr > 100} {
2697                set nr 100
2698            }
2699            optimize_rows $rowoptim 0 [expr {$rowoptim + $nr}]
2700            incr rowoptim $nr
2701        } elseif {$commitidx($curview) > $rowlaidout} {
2702            set nr [expr {$commitidx($curview) - $rowlaidout}]
2703            # may need to increase this threshold if uparrowlen or
2704            # mingaplen are increased...
2705            if {$nr > 150} {
2706                set nr 150
2707            }
2708            set row $rowlaidout
2709            set rowlaidout [layoutrows $row [expr {$row + $nr}] $allread]
2710            if {$rowlaidout == $row} {
2711                return 0
2712            }
2713        } elseif {$allread} {
2714            set optdelay 0
2715            set nrows $commitidx($curview)
2716            if {[lindex $rowidlist $nrows] ne {} ||
2717                [array names idinlist] ne {}} {
2718                layouttail
2719                set rowlaidout $commitidx($curview)
2720            } elseif {$rowoptim == $nrows} {
2721                set showdelay 0
2722                set showlast 1
2723                if {$numcommits == $nrows} {
2724                    return 0
2725                }
2726            }
2727        } else {
2728            return 0
2729        }
2730        if {$tmax ne {} && [clock clicks -milliseconds] >= $tmax} {
2731            return 1
2732        }
2733    }
2734}
2735
2736proc showstuff {canshow last} {
2737    global numcommits commitrow pending_select selectedline curview
2738    global lookingforhead mainheadid displayorder selectfirst
2739    global lastscrollset
2740
2741    if {$numcommits == 0} {
2742        global phase
2743        set phase "incrdraw"
2744        allcanvs delete all
2745    }
2746    set r0 $numcommits
2747    set prev $numcommits
2748    set numcommits $canshow
2749    set t [clock clicks -milliseconds]
2750    if {$prev < 100 || $last || $t - $lastscrollset > 500} {
2751        set lastscrollset $t
2752        setcanvscroll
2753    }
2754    set rows [visiblerows]
2755    set r1 [lindex $rows 1]
2756    if {$r1 >= $canshow} {
2757        set r1 [expr {$canshow - 1}]
2758    }
2759    if {$r0 <= $r1} {
2760        drawcommits $r0 $r1
2761    }
2762    if {[info exists pending_select] &&
2763        [info exists commitrow($curview,$pending_select)] &&
2764        $commitrow($curview,$pending_select) < $numcommits} {
2765        selectline $commitrow($curview,$pending_select) 1
2766    }
2767    if {$selectfirst} {
2768        if {[info exists selectedline] || [info exists pending_select]} {
2769            set selectfirst 0
2770        } else {
2771            set l [first_real_row]
2772            selectline $l 1
2773            set selectfirst 0
2774        }
2775    }
2776    if {$lookingforhead && [info exists commitrow($curview,$mainheadid)]
2777        && ($last || $commitrow($curview,$mainheadid) < $numcommits - 1)} {
2778        set lookingforhead 0
2779        dodiffindex
2780    }
2781}
2782
2783proc doshowlocalchanges {} {
2784    global lookingforhead curview mainheadid phase commitrow
2785
2786    if {[info exists commitrow($curview,$mainheadid)] &&
2787        ($phase eq {} || $commitrow($curview,$mainheadid) < $numcommits - 1)} {
2788        dodiffindex
2789    } elseif {$phase ne {}} {
2790        set lookingforhead 1
2791    }
2792}
2793
2794proc dohidelocalchanges {} {
2795    global lookingforhead localfrow localirow lserial
2796
2797    set lookingforhead 0
2798    if {$localfrow >= 0} {
2799        removerow $localfrow
2800        set localfrow -1
2801        if {$localirow > 0} {
2802            incr localirow -1
2803        }
2804    }
2805    if {$localirow >= 0} {
2806        removerow $localirow
2807        set localirow -1
2808    }
2809    incr lserial
2810}
2811
2812# spawn off a process to do git diff-index --cached HEAD
2813proc dodiffindex {} {
2814    global localirow localfrow lserial
2815
2816    incr lserial
2817    set localfrow -1
2818    set localirow -1
2819    set fd [open "|git diff-index --cached HEAD" r]
2820    fconfigure $fd -blocking 0
2821    filerun $fd [list readdiffindex $fd $lserial]
2822}
2823
2824proc readdiffindex {fd serial} {
2825    global localirow commitrow mainheadid nullid2 curview
2826    global commitinfo commitdata lserial
2827
2828    set isdiff 1
2829    if {[gets $fd line] < 0} {
2830        if {![eof $fd]} {
2831            return 1
2832        }
2833        set isdiff 0
2834    }
2835    # we only need to see one line and we don't really care what it says...
2836    close $fd
2837
2838    # now see if there are any local changes not checked in to the index
2839    if {$serial == $lserial} {
2840        set fd [open "|git diff-files" r]
2841        fconfigure $fd -blocking 0
2842        filerun $fd [list readdifffiles $fd $serial]
2843    }
2844
2845    if {$isdiff && $serial == $lserial && $localirow == -1} {
2846        # add the line for the changes in the index to the graph
2847        set localirow $commitrow($curview,$mainheadid)
2848        set hl "Local changes checked in to index but not committed"
2849        set commitinfo($nullid2) [list  $hl {} {} {} {} "    $hl\n"]
2850        set commitdata($nullid2) "\n    $hl\n"
2851        insertrow $localirow $nullid2
2852    }
2853    return 0
2854}
2855
2856proc readdifffiles {fd serial} {
2857    global localirow localfrow commitrow mainheadid nullid curview
2858    global commitinfo commitdata lserial
2859
2860    set isdiff 1
2861    if {[gets $fd line] < 0} {
2862        if {![eof $fd]} {
2863            return 1
2864        }
2865        set isdiff 0
2866    }
2867    # we only need to see one line and we don't really care what it says...
2868    close $fd
2869
2870    if {$isdiff && $serial == $lserial && $localfrow == -1} {
2871        # add the line for the local diff to the graph
2872        if {$localirow >= 0} {
2873            set localfrow $localirow
2874            incr localirow
2875        } else {
2876            set localfrow $commitrow($curview,$mainheadid)
2877        }
2878        set hl "Local uncommitted changes, not checked in to index"
2879        set commitinfo($nullid) [list  $hl {} {} {} {} "    $hl\n"]
2880        set commitdata($nullid) "\n    $hl\n"
2881        insertrow $localfrow $nullid
2882    }
2883    return 0
2884}
2885
2886proc layoutrows {row endrow last} {
2887    global rowidlist rowoffsets displayorder
2888    global uparrowlen downarrowlen maxwidth mingaplen
2889    global children parentlist
2890    global idrowranges
2891    global commitidx curview
2892    global idinlist rowchk rowrangelist
2893
2894    set idlist [lindex $rowidlist $row]
2895    set offs [lindex $rowoffsets $row]
2896    while {$row < $endrow} {
2897        set id [lindex $displayorder $row]
2898        set nev [expr {[llength $idlist] - $maxwidth + 1}]
2899        foreach p [lindex $parentlist $row] {
2900            if {![info exists idinlist($p)] || !$idinlist($p)} {
2901                incr nev
2902            }
2903        }
2904        if {$nev > 0} {
2905            if {!$last &&
2906                $row + $uparrowlen + $mingaplen >= $commitidx($curview)} break
2907            for {set x [llength $idlist]} {[incr x -1] >= 0} {} {
2908                set i [lindex $idlist $x]
2909                if {![info exists rowchk($i)] || $row >= $rowchk($i)} {
2910                    set r [usedinrange $i [expr {$row - $downarrowlen}] \
2911                               [expr {$row + $uparrowlen + $mingaplen}]]
2912                    if {$r == 0} {
2913                        set idlist [lreplace $idlist $x $x]
2914                        set offs [lreplace $offs $x $x]
2915                        set offs [incrange $offs $x 1]
2916                        set idinlist($i) 0
2917                        set rm1 [expr {$row - 1}]
2918                        lappend idrowranges($i) [lindex $displayorder $rm1]
2919                        if {[incr nev -1] <= 0} break
2920                        continue
2921                    }
2922                    set rowchk($i) [expr {$row + $r}]
2923                }
2924            }
2925            lset rowidlist $row $idlist
2926            lset rowoffsets $row $offs
2927        }
2928        set oldolds {}
2929        set newolds {}
2930        foreach p [lindex $parentlist $row] {
2931            if {![info exists idinlist($p)]} {
2932                lappend newolds $p
2933            } elseif {!$idinlist($p)} {
2934                lappend oldolds $p
2935            }
2936            set idinlist($p) 1
2937        }
2938        set col [lsearch -exact $idlist $id]
2939        if {$col < 0} {
2940            set col [llength $idlist]
2941            lappend idlist $id
2942            lset rowidlist $row $idlist
2943            set z {}
2944            if {$children($curview,$id) ne {}} {
2945                set z [expr {[llength [lindex $rowidlist [expr {$row-1}]]] - $col}]
2946                unset idinlist($id)
2947            }
2948            lappend offs $z
2949            lset rowoffsets $row $offs
2950            if {$z ne {}} {
2951                makeuparrow $id $col $row $z
2952            }
2953        } else {
2954            unset idinlist($id)
2955        }
2956        set ranges {}
2957        if {[info exists idrowranges($id)]} {
2958            set ranges $idrowranges($id)
2959            lappend ranges $id
2960            unset idrowranges($id)
2961        }
2962        lappend rowrangelist $ranges
2963        incr row
2964        set offs [ntimes [llength $idlist] 0]
2965        set l [llength $newolds]
2966        set idlist [eval lreplace \$idlist $col $col $newolds]
2967        set o 0
2968        if {$l != 1} {
2969            set offs [lrange $offs 0 [expr {$col - 1}]]
2970            foreach x $newolds {
2971                lappend offs {}
2972                incr o -1
2973            }
2974            incr o
2975            set tmp [expr {[llength $idlist] - [llength $offs]}]
2976            if {$tmp > 0} {
2977                set offs [concat $offs [ntimes $tmp $o]]
2978            }
2979        } else {
2980            lset offs $col {}
2981        }
2982        foreach i $newolds {
2983            set idrowranges($i) $id
2984        }
2985        incr col $l
2986        foreach oid $oldolds {
2987            set idlist [linsert $idlist $col $oid]
2988            set offs [linsert $offs $col $o]
2989            makeuparrow $oid $col $row $o
2990            incr col
2991        }
2992        lappend rowidlist $idlist
2993        lappend rowoffsets $offs
2994    }
2995    return $row
2996}
2997
2998proc addextraid {id row} {
2999    global displayorder commitrow commitinfo
3000    global commitidx commitlisted
3001    global parentlist children curview
3002
3003    incr commitidx($curview)
3004    lappend displayorder $id
3005    lappend commitlisted 0
3006    lappend parentlist {}
3007    set commitrow($curview,$id) $row
3008    readcommit $id
3009    if {![info exists commitinfo($id)]} {
3010        set commitinfo($id) {"No commit information available"}
3011    }
3012    if {![info exists children($curview,$id)]} {
3013        set children($curview,$id) {}
3014    }
3015}
3016
3017proc layouttail {} {
3018    global rowidlist rowoffsets idinlist commitidx curview
3019    global idrowranges rowrangelist
3020
3021    set row $commitidx($curview)
3022    set idlist [lindex $rowidlist $row]
3023    while {$idlist ne {}} {
3024        set col [expr {[llength $idlist] - 1}]
3025        set id [lindex $idlist $col]
3026        addextraid $id $row
3027        catch {unset idinlist($id)}
3028        lappend idrowranges($id) $id
3029        lappend rowrangelist $idrowranges($id)
3030        unset idrowranges($id)
3031        incr row
3032        set offs [ntimes $col 0]
3033        set idlist [lreplace $idlist $col $col]
3034        lappend rowidlist $idlist
3035        lappend rowoffsets $offs
3036    }
3037
3038    foreach id [array names idinlist] {
3039        unset idinlist($id)
3040        addextraid $id $row
3041        lset rowidlist $row [list $id]
3042        lset rowoffsets $row 0
3043        makeuparrow $id 0 $row 0
3044        lappend idrowranges($id) $id
3045        lappend rowrangelist $idrowranges($id)
3046        unset idrowranges($id)
3047        incr row
3048        lappend rowidlist {}
3049        lappend rowoffsets {}
3050    }
3051}
3052
3053proc insert_pad {row col npad} {
3054    global rowidlist rowoffsets
3055
3056    set pad [ntimes $npad {}]
3057    lset rowidlist $row [eval linsert [list [lindex $rowidlist $row]] $col $pad]
3058    set tmp [eval linsert [list [lindex $rowoffsets $row]] $col $pad]
3059    lset rowoffsets $row [incrange $tmp [expr {$col + $npad}] [expr {-$npad}]]
3060}
3061
3062proc optimize_rows {row col endrow} {
3063    global rowidlist rowoffsets displayorder
3064
3065    for {} {$row < $endrow} {incr row} {
3066        set idlist [lindex $rowidlist $row]
3067        set offs [lindex $rowoffsets $row]
3068        set haspad 0
3069        for {} {$col < [llength $offs]} {incr col} {
3070            if {[lindex $idlist $col] eq {}} {
3071                set haspad 1
3072                continue
3073            }
3074            set z [lindex $offs $col]
3075            if {$z eq {}} continue
3076            set isarrow 0
3077            set x0 [expr {$col + $z}]
3078            set y0 [expr {$row - 1}]
3079            set z0 [lindex $rowoffsets $y0 $x0]
3080            if {$z0 eq {}} {
3081                set id [lindex $idlist $col]
3082                set ranges [rowranges $id]
3083                if {$ranges ne {} && $y0 > [lindex $ranges 0]} {
3084                    set isarrow 1
3085                }
3086            }
3087            # Looking at lines from this row to the previous row,
3088            # make them go straight up if they end in an arrow on
3089            # the previous row; otherwise make them go straight up
3090            # or at 45 degrees.
3091            if {$z < -1 || ($z < 0 && $isarrow)} {
3092                # Line currently goes left too much;
3093                # insert pads in the previous row, then optimize it
3094                set npad [expr {-1 - $z + $isarrow}]
3095                set offs [incrange $offs $col $npad]
3096                insert_pad $y0 $x0 $npad
3097                if {$y0 > 0} {
3098                    optimize_rows $y0 $x0 $row
3099                }
3100                set z [lindex $offs $col]
3101                set x0 [expr {$col + $z}]
3102                set z0 [lindex $rowoffsets $y0 $x0]
3103            } elseif {$z > 1 || ($z > 0 && $isarrow)} {
3104                # Line currently goes right too much;
3105                # insert pads in this line and adjust the next's rowoffsets
3106                set npad [expr {$z - 1 + $isarrow}]
3107                set y1 [expr {$row + 1}]
3108                set offs2 [lindex $rowoffsets $y1]
3109                set x1 -1
3110                foreach z $offs2 {
3111                    incr x1
3112                    if {$z eq {} || $x1 + $z < $col} continue
3113                    if {$x1 + $z > $col} {
3114                        incr npad
3115                    }
3116                    lset rowoffsets $y1 [incrange $offs2 $x1 $npad]
3117                    break
3118                }
3119                set pad [ntimes $npad {}]
3120                set idlist [eval linsert \$idlist $col $pad]
3121                set tmp [eval linsert \$offs $col $pad]
3122                incr col $npad
3123                set offs [incrange $tmp $col [expr {-$npad}]]
3124                set z [lindex $offs $col]
3125                set haspad 1
3126            }
3127            if {$z0 eq {} && !$isarrow} {
3128                # this line links to its first child on row $row-2
3129                set rm2 [expr {$row - 2}]
3130                set id [lindex $displayorder $rm2]
3131                set xc [lsearch -exact [lindex $rowidlist $rm2] $id]
3132                if {$xc >= 0} {
3133                    set z0 [expr {$xc - $x0}]
3134                }
3135            }
3136            # avoid lines jigging left then immediately right
3137            if {$z0 ne {} && $z < 0 && $z0 > 0} {
3138                insert_pad $y0 $x0 1
3139                set offs [incrange $offs $col 1]
3140                optimize_rows $y0 [expr {$x0 + 1}] $row
3141            }
3142        }
3143        if {!$haspad} {
3144            set o {}
3145            # Find the first column that doesn't have a line going right
3146            for {set col [llength $idlist]} {[incr col -1] >= 0} {} {
3147                set o [lindex $offs $col]
3148                if {$o eq {}} {
3149                    # check if this is the link to the first child
3150                    set id [lindex $idlist $col]
3151                    set ranges [rowranges $id]
3152                    if {$ranges ne {} && $row == [lindex $ranges 0]} {
3153                        # it is, work out offset to child
3154                        set y0 [expr {$row - 1}]
3155                        set id [lindex $displayorder $y0]
3156                        set x0 [lsearch -exact [lindex $rowidlist $y0] $id]
3157                        if {$x0 >= 0} {
3158                            set o [expr {$x0 - $col}]
3159                        }
3160                    }
3161                }
3162                if {$o eq {} || $o <= 0} break
3163            }
3164            # Insert a pad at that column as long as it has a line and
3165            # isn't the last column, and adjust the next row' offsets
3166            if {$o ne {} && [incr col] < [llength $idlist]} {
3167                set y1 [expr {$row + 1}]
3168                set offs2 [lindex $rowoffsets $y1]
3169                set x1 -1
3170                foreach z $offs2 {
3171                    incr x1
3172                    if {$z eq {} || $x1 + $z < $col} continue
3173                    lset rowoffsets $y1 [incrange $offs2 $x1 1]
3174                    break
3175                }
3176                set idlist [linsert $idlist $col {}]
3177                set tmp [linsert $offs $col {}]
3178                incr col
3179                set offs [incrange $tmp $col -1]
3180            }
3181        }
3182        lset rowidlist $row $idlist
3183        lset rowoffsets $row $offs
3184        set col 0
3185    }
3186}
3187
3188proc xc {row col} {
3189    global canvx0 linespc
3190    return [expr {$canvx0 + $col * $linespc}]
3191}
3192
3193proc yc {row} {
3194    global canvy0 linespc
3195    return [expr {$canvy0 + $row * $linespc}]
3196}
3197
3198proc linewidth {id} {
3199    global thickerline lthickness
3200
3201    set wid $lthickness
3202    if {[info exists thickerline] && $id eq $thickerline} {
3203        set wid [expr {2 * $lthickness}]
3204    }
3205    return $wid
3206}
3207
3208proc rowranges {id} {
3209    global phase idrowranges commitrow rowlaidout rowrangelist curview
3210
3211    set ranges {}
3212    if {$phase eq {} ||
3213        ([info exists commitrow($curview,$id)]
3214         && $commitrow($curview,$id) < $rowlaidout)} {
3215        set ranges [lindex $rowrangelist $commitrow($curview,$id)]
3216    } elseif {[info exists idrowranges($id)]} {
3217        set ranges $idrowranges($id)
3218    }
3219    set linenos {}
3220    foreach rid $ranges {
3221        lappend linenos $commitrow($curview,$rid)
3222    }
3223    if {$linenos ne {}} {
3224        lset linenos 0 [expr {[lindex $linenos 0] + 1}]
3225    }
3226    return $linenos
3227}
3228
3229# work around tk8.4 refusal to draw arrows on diagonal segments
3230proc adjarrowhigh {coords} {
3231    global linespc
3232
3233    set x0 [lindex $coords 0]
3234    set x1 [lindex $coords 2]
3235    if {$x0 != $x1} {
3236        set y0 [lindex $coords 1]
3237        set y1 [lindex $coords 3]
3238        if {$y0 - $y1 <= 2 * $linespc && $x1 == [lindex $coords 4]} {
3239            # we have a nearby vertical segment, just trim off the diag bit
3240            set coords [lrange $coords 2 end]
3241        } else {
3242            set slope [expr {($x0 - $x1) / ($y0 - $y1)}]
3243            set xi [expr {$x0 - $slope * $linespc / 2}]
3244            set yi [expr {$y0 - $linespc / 2}]
3245            set coords [lreplace $coords 0 1 $xi $y0 $xi $yi]
3246        }
3247    }
3248    return $coords
3249}
3250
3251proc drawlineseg {id row endrow arrowlow} {
3252    global rowidlist displayorder iddrawn linesegs
3253    global canv colormap linespc curview maxlinelen
3254
3255    set cols [list [lsearch -exact [lindex $rowidlist $row] $id]]
3256    set le [expr {$row + 1}]
3257    set arrowhigh 1
3258    while {1} {
3259        set c [lsearch -exact [lindex $rowidlist $le] $id]
3260        if {$c < 0} {
3261            incr le -1
3262            break
3263        }
3264        lappend cols $c
3265        set x [lindex $displayorder $le]
3266        if {$x eq $id} {
3267            set arrowhigh 0
3268            break
3269        }
3270        if {[info exists iddrawn($x)] || $le == $endrow} {
3271            set c [lsearch -exact [lindex $rowidlist [expr {$le+1}]] $id]
3272            if {$c >= 0} {
3273                lappend cols $c
3274                set arrowhigh 0
3275            }
3276            break
3277        }
3278        incr le
3279    }
3280    if {$le <= $row} {
3281        return $row
3282    }
3283
3284    set lines {}
3285    set i 0
3286    set joinhigh 0
3287    if {[info exists linesegs($id)]} {
3288        set lines $linesegs($id)
3289        foreach li $lines {
3290            set r0 [lindex $li 0]
3291            if {$r0 > $row} {
3292                if {$r0 == $le && [lindex $li 1] - $row <= $maxlinelen} {
3293                    set joinhigh 1
3294                }
3295                break
3296            }
3297            incr i
3298        }
3299    }
3300    set joinlow 0
3301    if {$i > 0} {
3302        set li [lindex $lines [expr {$i-1}]]
3303        set r1 [lindex $li 1]
3304        if {$r1 == $row && $le - [lindex $li 0] <= $maxlinelen} {
3305            set joinlow 1
3306        }
3307    }
3308
3309    set x [lindex $cols [expr {$le - $row}]]
3310    set xp [lindex $cols [expr {$le - 1 - $row}]]
3311    set dir [expr {$xp - $x}]
3312    if {$joinhigh} {
3313        set ith [lindex $lines $i 2]
3314        set coords [$canv coords $ith]
3315        set ah [$canv itemcget $ith -arrow]
3316        set arrowhigh [expr {$ah eq "first" || $ah eq "both"}]
3317        set x2 [lindex $cols [expr {$le + 1 - $row}]]
3318        if {$x2 ne {} && $x - $x2 == $dir} {
3319            set coords [lrange $coords 0 end-2]
3320        }
3321    } else {
3322        set coords [list [xc $le $x] [yc $le]]
3323    }
3324    if {$joinlow} {
3325        set itl [lindex $lines [expr {$i-1}] 2]
3326        set al [$canv itemcget $itl -arrow]
3327        set arrowlow [expr {$al eq "last" || $al eq "both"}]
3328    } elseif {$arrowlow &&
3329              [lsearch -exact [lindex $rowidlist [expr {$row-1}]] $id] >= 0} {
3330        set arrowlow 0
3331    }
3332    set arrow [lindex {none first last both} [expr {$arrowhigh + 2*$arrowlow}]]
3333    for {set y $le} {[incr y -1] > $row} {} {
3334        set x $xp
3335        set xp [lindex $cols [expr {$y - 1 - $row}]]
3336        set ndir [expr {$xp - $x}]
3337        if {$dir != $ndir || $xp < 0} {
3338            lappend coords [xc $y $x] [yc $y]
3339        }
3340        set dir $ndir
3341    }
3342    if {!$joinlow} {
3343        if {$xp < 0} {
3344            # join parent line to first child
3345            set ch [lindex $displayorder $row]
3346            set xc [lsearch -exact [lindex $rowidlist $row] $ch]
3347            if {$xc < 0} {
3348                puts "oops: drawlineseg: child $ch not on row $row"
3349            } else {
3350                if {$xc < $x - 1} {
3351                    lappend coords [xc $row [expr {$x-1}]] [yc $row]
3352                } elseif {$xc > $x + 1} {
3353                    lappend coords [xc $row [expr {$x+1}]] [yc $row]
3354                }
3355                set x $xc
3356            }
3357            lappend coords [xc $row $x] [yc $row]
3358        } else {
3359            set xn [xc $row $xp]
3360            set yn [yc $row]
3361            # work around tk8.4 refusal to draw arrows on diagonal segments
3362            if {$arrowlow && $xn != [lindex $coords end-1]} {
3363                if {[llength $coords] < 4 ||
3364                    [lindex $coords end-3] != [lindex $coords end-1] ||
3365                    [lindex $coords end] - $yn > 2 * $linespc} {
3366                    set xn [xc $row [expr {$xp - 0.5 * $dir}]]
3367                    set yo [yc [expr {$row + 0.5}]]
3368                    lappend coords $xn $yo $xn $yn
3369                }
3370            } else {
3371                lappend coords $xn $yn
3372            }
3373        }
3374        if {!$joinhigh} {
3375            if {$arrowhigh} {
3376                set coords [adjarrowhigh $coords]
3377            }
3378            assigncolor $id
3379            set t [$canv create line $coords -width [linewidth $id] \
3380                       -fill $colormap($id) -tags lines.$id -arrow $arrow]
3381            $canv lower $t
3382            bindline $t $id
3383            set lines [linsert $lines $i [list $row $le $t]]
3384        } else {
3385            $canv coords $ith $coords
3386            if {$arrow ne $ah} {
3387                $canv itemconf $ith -arrow $arrow
3388            }
3389            lset lines $i 0 $row
3390        }
3391    } else {
3392        set xo [lsearch -exact [lindex $rowidlist [expr {$row - 1}]] $id]
3393        set ndir [expr {$xo - $xp}]
3394        set clow [$canv coords $itl]
3395        if {$dir == $ndir} {
3396            set clow [lrange $clow 2 end]
3397        }
3398        set coords [concat $coords $clow]
3399        if {!$joinhigh} {
3400            lset lines [expr {$i-1}] 1 $le
3401            if {$arrowhigh} {
3402                set coords [adjarrowhigh $coords]
3403            }
3404        } else {
3405            # coalesce two pieces
3406            $canv delete $ith
3407            set b [lindex $lines [expr {$i-1}] 0]
3408            set e [lindex $lines $i 1]
3409            set lines [lreplace $lines [expr {$i-1}] $i [list $b $e $itl]]
3410        }
3411        $canv coords $itl $coords
3412        if {$arrow ne $al} {
3413            $canv itemconf $itl -arrow $arrow
3414        }
3415    }
3416
3417    set linesegs($id) $lines
3418    return $le
3419}
3420
3421proc drawparentlinks {id row} {
3422    global rowidlist canv colormap curview parentlist
3423    global idpos
3424
3425    set rowids [lindex $rowidlist $row]
3426    set col [lsearch -exact $rowids $id]
3427    if {$col < 0} return
3428    set olds [lindex $parentlist $row]
3429    set row2 [expr {$row + 1}]
3430    set x [xc $row $col]
3431    set y [yc $row]
3432    set y2 [yc $row2]
3433    set ids [lindex $rowidlist $row2]
3434    # rmx = right-most X coord used
3435    set rmx 0
3436    foreach p $olds {
3437        set i [lsearch -exact $ids $p]
3438        if {$i < 0} {
3439            puts "oops, parent $p of $id not in list"
3440            continue
3441        }
3442        set x2 [xc $row2 $i]
3443        if {$x2 > $rmx} {
3444            set rmx $x2
3445        }
3446        if {[lsearch -exact $rowids $p] < 0} {
3447            # drawlineseg will do this one for us
3448            continue
3449        }
3450        assigncolor $p
3451        # should handle duplicated parents here...
3452        set coords [list $x $y]
3453        if {$i < $col - 1} {
3454            lappend coords [xc $row [expr {$i + 1}]] $y
3455        } elseif {$i > $col + 1} {
3456            lappend coords [xc $row [expr {$i - 1}]] $y
3457        }
3458        lappend coords $x2 $y2
3459        set t [$canv create line $coords -width [linewidth $p] \
3460                   -fill $colormap($p) -tags lines.$p]
3461        $canv lower $t
3462        bindline $t $p
3463    }
3464    if {$rmx > [lindex $idpos($id) 1]} {
3465        lset idpos($id) 1 $rmx
3466        redrawtags $id
3467    }
3468}
3469
3470proc drawlines {id} {
3471    global canv
3472
3473    $canv itemconf lines.$id -width [linewidth $id]
3474}
3475
3476proc drawcmittext {id row col} {
3477    global linespc canv canv2 canv3 canvy0 fgcolor curview
3478    global commitlisted commitinfo rowidlist parentlist
3479    global rowtextx idpos idtags idheads idotherrefs
3480    global linehtag linentag linedtag
3481    global mainfont canvxmax boldrows boldnamerows fgcolor nullid nullid2
3482
3483    # listed is 0 for boundary, 1 for normal, 2 for left, 3 for right
3484    set listed [lindex $commitlisted $row]
3485    if {$id eq $nullid} {
3486        set ofill red
3487    } elseif {$id eq $nullid2} {
3488        set ofill green
3489    } else {
3490        set ofill [expr {$listed != 0? "blue": "white"}]
3491    }
3492    set x [xc $row $col]
3493    set y [yc $row]
3494    set orad [expr {$linespc / 3}]
3495    if {$listed <= 1} {
3496        set t [$canv create oval [expr {$x - $orad}] [expr {$y - $orad}] \
3497                   [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
3498                   -fill $ofill -outline $fgcolor -width 1 -tags circle]
3499    } elseif {$listed == 2} {
3500        # triangle pointing left for left-side commits
3501        set t [$canv create polygon \
3502                   [expr {$x - $orad}] $y \
3503                   [expr {$x + $orad - 1}] [expr {$y - $orad}] \
3504                   [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
3505                   -fill $ofill -outline $fgcolor -width 1 -tags circle]
3506    } else {
3507        # triangle pointing right for right-side commits
3508        set t [$canv create polygon \
3509                   [expr {$x + $orad - 1}] $y \
3510                   [expr {$x - $orad}] [expr {$y - $orad}] \
3511                   [expr {$x - $orad}] [expr {$y + $orad - 1}] \
3512                   -fill $ofill -outline $fgcolor -width 1 -tags circle]
3513    }
3514    $canv raise $t
3515    $canv bind $t <1> {selcanvline {} %x %y}
3516    set rmx [llength [lindex $rowidlist $row]]
3517    set olds [lindex $parentlist $row]
3518    if {$olds ne {}} {
3519        set nextids [lindex $rowidlist [expr {$row + 1}]]
3520        foreach p $olds {
3521            set i [lsearch -exact $nextids $p]
3522            if {$i > $rmx} {
3523                set rmx $i
3524            }
3525        }
3526    }
3527    set xt [xc $row $rmx]
3528    set rowtextx($row) $xt
3529    set idpos($id) [list $x $xt $y]
3530    if {[info exists idtags($id)] || [info exists idheads($id)]
3531        || [info exists idotherrefs($id)]} {
3532        set xt [drawtags $id $x $xt $y]
3533    }
3534    set headline [lindex $commitinfo($id) 0]
3535    set name [lindex $commitinfo($id) 1]
3536    set date [lindex $commitinfo($id) 2]
3537    set date [formatdate $date]
3538    set font $mainfont
3539    set nfont $mainfont
3540    set isbold [ishighlighted $row]
3541    if {$isbold > 0} {
3542        lappend boldrows $row
3543        lappend font bold
3544        if {$isbold > 1} {
3545            lappend boldnamerows $row
3546            lappend nfont bold
3547        }
3548    }
3549    set linehtag($row) [$canv create text $xt $y -anchor w -fill $fgcolor \
3550                            -text $headline -font $font -tags text]
3551    $canv bind $linehtag($row) <Button-3> "rowmenu %X %Y $id"
3552    set linentag($row) [$canv2 create text 3 $y -anchor w -fill $fgcolor \
3553                            -text $name -font $nfont -tags text]
3554    set linedtag($row) [$canv3 create text 3 $y -anchor w -fill $fgcolor \
3555                            -text $date -font $mainfont -tags text]
3556    set xr [expr {$xt + [font measure $mainfont $headline]}]
3557    if {$xr > $canvxmax} {
3558        set canvxmax $xr
3559        setcanvscroll
3560    }
3561}
3562
3563proc drawcmitrow {row} {
3564    global displayorder rowidlist
3565    global iddrawn markingmatches
3566    global commitinfo parentlist numcommits
3567    global filehighlight fhighlights findstring nhighlights
3568    global hlview vhighlights
3569    global highlight_related rhighlights
3570
3571    if {$row >= $numcommits} return
3572
3573    set id [lindex $displayorder $row]
3574    if {[info exists hlview] && ![info exists vhighlights($row)]} {
3575        askvhighlight $row $id
3576    }
3577    if {[info exists filehighlight] && ![info exists fhighlights($row)]} {
3578        askfilehighlight $row $id
3579    }
3580    if {$findstring ne {} && ![info exists nhighlights($row)]} {
3581        askfindhighlight $row $id
3582    }
3583    if {$highlight_related ne "None" && ![info exists rhighlights($row)]} {
3584        askrelhighlight $row $id
3585    }
3586    if {![info exists iddrawn($id)]} {
3587        set col [lsearch -exact [lindex $rowidlist $row] $id]
3588        if {$col < 0} {
3589            puts "oops, row $row id $id not in list"
3590            return
3591        }
3592        if {![info exists commitinfo($id)]} {
3593            getcommit $id
3594        }
3595        assigncolor $id
3596        drawcmittext $id $row $col
3597        set iddrawn($id) 1
3598    }
3599    if {$markingmatches} {
3600        markrowmatches $row $id
3601    }
3602}
3603
3604proc drawcommits {row {endrow {}}} {
3605    global numcommits iddrawn displayorder curview
3606    global parentlist rowidlist
3607
3608    if {$row < 0} {
3609        set row 0
3610    }
3611    if {$endrow eq {}} {
3612        set endrow $row
3613    }
3614    if {$endrow >= $numcommits} {
3615        set endrow [expr {$numcommits - 1}]
3616    }
3617
3618    # make the lines join to already-drawn rows either side
3619    set r [expr {$row - 1}]
3620    if {$r < 0 || ![info exists iddrawn([lindex $displayorder $r])]} {
3621        set r $row
3622    }
3623    set er [expr {$endrow + 1}]
3624    if {$er >= $numcommits ||
3625        ![info exists iddrawn([lindex $displayorder $er])]} {
3626        set er $endrow
3627    }
3628    for {} {$r <= $er} {incr r} {
3629        set id [lindex $displayorder $r]
3630        set wasdrawn [info exists iddrawn($id)]
3631        drawcmitrow $r
3632        if {$r == $er} break
3633        set nextid [lindex $displayorder [expr {$r + 1}]]
3634        if {$wasdrawn && [info exists iddrawn($nextid)]} {
3635            catch {unset prevlines}
3636            continue
3637        }
3638        drawparentlinks $id $r
3639
3640        if {[info exists lineends($r)]} {
3641            foreach lid $lineends($r) {
3642                unset prevlines($lid)
3643            }
3644        }
3645        set rowids [lindex $rowidlist $r]
3646        foreach lid $rowids {
3647            if {$lid eq {}} continue
3648            if {$lid eq $id} {
3649                # see if this is the first child of any of its parents
3650                foreach p [lindex $parentlist $r] {
3651                    if {[lsearch -exact $rowids $p] < 0} {
3652                        # make this line extend up to the child
3653                        set le [drawlineseg $p $r $er 0]
3654                        lappend lineends($le) $p
3655                        set prevlines($p) 1
3656                    }
3657                }
3658            } elseif {![info exists prevlines($lid)]} {
3659                set le [drawlineseg $lid $r $er 1]
3660                lappend lineends($le) $lid
3661                set prevlines($lid) 1
3662            }
3663        }
3664    }
3665}
3666
3667proc drawfrac {f0 f1} {
3668    global canv linespc
3669
3670    set ymax [lindex [$canv cget -scrollregion] 3]
3671    if {$ymax eq {} || $ymax == 0} return
3672    set y0 [expr {int($f0 * $ymax)}]
3673    set row [expr {int(($y0 - 3) / $linespc) - 1}]
3674    set y1 [expr {int($f1 * $ymax)}]
3675    set endrow [expr {int(($y1 - 3) / $linespc) + 1}]
3676    drawcommits $row $endrow
3677}
3678
3679proc drawvisible {} {
3680    global canv
3681    eval drawfrac [$canv yview]
3682}
3683
3684proc clear_display {} {
3685    global iddrawn linesegs
3686    global vhighlights fhighlights nhighlights rhighlights
3687
3688    allcanvs delete all
3689    catch {unset iddrawn}
3690    catch {unset linesegs}
3691    catch {unset vhighlights}
3692    catch {unset fhighlights}
3693    catch {unset nhighlights}
3694    catch {unset rhighlights}
3695}
3696
3697proc findcrossings {id} {
3698    global rowidlist parentlist numcommits rowoffsets displayorder
3699
3700    set cross {}
3701    set ccross {}
3702    foreach {s e} [rowranges $id] {
3703        if {$e >= $numcommits} {
3704            set e [expr {$numcommits - 1}]
3705        }
3706        if {$e <= $s} continue
3707        set x [lsearch -exact [lindex $rowidlist $e] $id]
3708        if {$x < 0} {
3709            puts "findcrossings: oops, no [shortids $id] in row $e"
3710            continue
3711        }
3712        for {set row $e} {[incr row -1] >= $s} {} {
3713            set olds [lindex $parentlist $row]
3714            set kid [lindex $displayorder $row]
3715            set kidx [lsearch -exact [lindex $rowidlist $row] $kid]
3716            if {$kidx < 0} continue
3717            set nextrow [lindex $rowidlist [expr {$row + 1}]]
3718            foreach p $olds {
3719                set px [lsearch -exact $nextrow $p]
3720                if {$px < 0} continue
3721                if {($kidx < $x && $x < $px) || ($px < $x && $x < $kidx)} {
3722                    if {[lsearch -exact $ccross $p] >= 0} continue
3723                    if {$x == $px + ($kidx < $px? -1: 1)} {
3724                        lappend ccross $p
3725                    } elseif {[lsearch -exact $cross $p] < 0} {
3726                        lappend cross $p
3727                    }
3728                }
3729            }
3730            set inc [lindex $rowoffsets $row $x]
3731            if {$inc eq {}} break
3732            incr x $inc
3733        }
3734    }
3735    return [concat $ccross {{}} $cross]
3736}
3737
3738proc assigncolor {id} {
3739    global colormap colors nextcolor
3740    global commitrow parentlist children children curview
3741
3742    if {[info exists colormap($id)]} return
3743    set ncolors [llength $colors]
3744    if {[info exists children($curview,$id)]} {
3745        set kids $children($curview,$id)
3746    } else {
3747        set kids {}
3748    }
3749    if {[llength $kids] == 1} {
3750        set child [lindex $kids 0]
3751        if {[info exists colormap($child)]
3752            && [llength [lindex $parentlist $commitrow($curview,$child)]] == 1} {
3753            set colormap($id) $colormap($child)
3754            return
3755        }
3756    }
3757    set badcolors {}
3758    set origbad {}
3759    foreach x [findcrossings $id] {
3760        if {$x eq {}} {
3761            # delimiter between corner crossings and other crossings
3762            if {[llength $badcolors] >= $ncolors - 1} break
3763            set origbad $badcolors
3764        }
3765        if {[info exists colormap($x)]
3766            && [lsearch -exact $badcolors $colormap($x)] < 0} {
3767            lappend badcolors $colormap($x)
3768        }
3769    }
3770    if {[llength $badcolors] >= $ncolors} {
3771        set badcolors $origbad
3772    }
3773    set origbad $badcolors
3774    if {[llength $badcolors] < $ncolors - 1} {
3775        foreach child $kids {
3776            if {[info exists colormap($child)]
3777                && [lsearch -exact $badcolors $colormap($child)] < 0} {
3778                lappend badcolors $colormap($child)
3779            }
3780            foreach p [lindex $parentlist $commitrow($curview,$child)] {
3781                if {[info exists colormap($p)]
3782                    && [lsearch -exact $badcolors $colormap($p)] < 0} {
3783                    lappend badcolors $colormap($p)
3784                }
3785            }
3786        }
3787        if {[llength $badcolors] >= $ncolors} {
3788            set badcolors $origbad
3789        }
3790    }
3791    for {set i 0} {$i <= $ncolors} {incr i} {
3792        set c [lindex $colors $nextcolor]
3793        if {[incr nextcolor] >= $ncolors} {
3794            set nextcolor 0
3795        }
3796        if {[lsearch -exact $badcolors $c]} break
3797    }
3798    set colormap($id) $c
3799}
3800
3801proc bindline {t id} {
3802    global canv
3803
3804    $canv bind $t <Enter> "lineenter %x %y $id"
3805    $canv bind $t <Motion> "linemotion %x %y $id"
3806    $canv bind $t <Leave> "lineleave $id"
3807    $canv bind $t <Button-1> "lineclick %x %y $id 1"
3808}
3809
3810proc drawtags {id x xt y1} {
3811    global idtags idheads idotherrefs mainhead
3812    global linespc lthickness
3813    global canv mainfont commitrow rowtextx curview fgcolor bgcolor
3814
3815    set marks {}
3816    set ntags 0
3817    set nheads 0
3818    if {[info exists idtags($id)]} {
3819        set marks $idtags($id)
3820        set ntags [llength $marks]
3821    }
3822    if {[info exists idheads($id)]} {
3823        set marks [concat $marks $idheads($id)]
3824        set nheads [llength $idheads($id)]
3825    }
3826    if {[info exists idotherrefs($id)]} {
3827        set marks [concat $marks $idotherrefs($id)]
3828    }
3829    if {$marks eq {}} {
3830        return $xt
3831    }
3832
3833    set delta [expr {int(0.5 * ($linespc - $lthickness))}]
3834    set yt [expr {$y1 - 0.5 * $linespc}]
3835    set yb [expr {$yt + $linespc - 1}]
3836    set xvals {}
3837    set wvals {}
3838    set i -1
3839    foreach tag $marks {
3840        incr i
3841        if {$i >= $ntags && $i < $ntags + $nheads && $tag eq $mainhead} {
3842            set wid [font measure [concat $mainfont bold] $tag]
3843        } else {
3844            set wid [font measure $mainfont $tag]
3845        }
3846        lappend xvals $xt
3847        lappend wvals $wid
3848        set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
3849    }
3850    set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
3851               -width $lthickness -fill black -tags tag.$id]
3852    $canv lower $t
3853    foreach tag $marks x $xvals wid $wvals {
3854        set xl [expr {$x + $delta}]
3855        set xr [expr {$x + $delta + $wid + $lthickness}]
3856        set font $mainfont
3857        if {[incr ntags -1] >= 0} {
3858            # draw a tag
3859            set t [$canv create polygon $x [expr {$yt + $delta}] $xl $yt \
3860                       $xr $yt $xr $yb $xl $yb $x [expr {$yb - $delta}] \
3861                       -width 1 -outline black -fill yellow -tags tag.$id]
3862            $canv bind $t <1> [list showtag $tag 1]
3863            set rowtextx($commitrow($curview,$id)) [expr {$xr + $linespc}]
3864        } else {
3865            # draw a head or other ref
3866            if {[incr nheads -1] >= 0} {
3867                set col green
3868                if {$tag eq $mainhead} {
3869                    lappend font bold
3870                }
3871            } else {
3872                set col "#ddddff"
3873            }
3874            set xl [expr {$xl - $delta/2}]
3875            $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
3876                -width 1 -outline black -fill $col -tags tag.$id
3877            if {[regexp {^(remotes/.*/|remotes/)} $tag match remoteprefix]} {
3878                set rwid [font measure $mainfont $remoteprefix]
3879                set xi [expr {$x + 1}]
3880                set yti [expr {$yt + 1}]
3881                set xri [expr {$x + $rwid}]
3882                $canv create polygon $xi $yti $xri $yti $xri $yb $xi $yb \
3883                        -width 0 -fill "#ffddaa" -tags tag.$id
3884            }
3885        }
3886        set t [$canv create text $xl $y1 -anchor w -text $tag -fill $fgcolor \
3887                   -font $font -tags [list tag.$id text]]
3888        if {$ntags >= 0} {
3889            $canv bind $t <1> [list showtag $tag 1]
3890        } elseif {$nheads >= 0} {
3891            $canv bind $t <Button-3> [list headmenu %X %Y $id $tag]
3892        }
3893    }
3894    return $xt
3895}
3896
3897proc xcoord {i level ln} {
3898    global canvx0 xspc1 xspc2
3899
3900    set x [expr {$canvx0 + $i * $xspc1($ln)}]
3901    if {$i > 0 && $i == $level} {
3902        set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
3903    } elseif {$i > $level} {
3904        set x [expr {$x + $xspc2 - $xspc1($ln)}]
3905    }
3906    return $x
3907}
3908
3909proc show_status {msg} {
3910    global canv mainfont fgcolor
3911
3912    clear_display
3913    $canv create text 3 3 -anchor nw -text $msg -font $mainfont \
3914        -tags text -fill $fgcolor
3915}
3916
3917# Insert a new commit as the child of the commit on row $row.
3918# The new commit will be displayed on row $row and the commits
3919# on that row and below will move down one row.
3920proc insertrow {row newcmit} {
3921    global displayorder parentlist commitlisted children
3922    global commitrow curview rowidlist rowoffsets numcommits
3923    global rowrangelist rowlaidout rowoptim numcommits
3924    global selectedline rowchk commitidx
3925
3926    if {$row >= $numcommits} {
3927        puts "oops, inserting new row $row but only have $numcommits rows"
3928        return
3929    }
3930    set p [lindex $displayorder $row]
3931    set displayorder [linsert $displayorder $row $newcmit]
3932    set parentlist [linsert $parentlist $row $p]
3933    set kids $children($curview,$p)
3934    lappend kids $newcmit
3935    set children($curview,$p) $kids
3936    set children($curview,$newcmit) {}
3937    set commitlisted [linsert $commitlisted $row 1]
3938    set l [llength $displayorder]
3939    for {set r $row} {$r < $l} {incr r} {
3940        set id [lindex $displayorder $r]
3941        set commitrow($curview,$id) $r
3942    }
3943    incr commitidx($curview)
3944
3945    set idlist [lindex $rowidlist $row]
3946    set offs [lindex $rowoffsets $row]
3947    set newoffs {}
3948    foreach x $idlist {
3949        if {$x eq {} || ($x eq $p && [llength $kids] == 1)} {
3950            lappend newoffs {}
3951        } else {
3952            lappend newoffs 0
3953        }
3954    }
3955    if {[llength $kids] == 1} {
3956        set col [lsearch -exact $idlist $p]
3957        lset idlist $col $newcmit
3958    } else {
3959        set col [llength $idlist]
3960        lappend idlist $newcmit
3961        lappend offs {}
3962        lset rowoffsets $row $offs
3963    }
3964    set rowidlist [linsert $rowidlist $row $idlist]
3965    set rowoffsets [linsert $rowoffsets [expr {$row+1}] $newoffs]
3966
3967    set rowrangelist [linsert $rowrangelist $row {}]
3968    if {[llength $kids] > 1} {
3969        set rp1 [expr {$row + 1}]
3970        set ranges [lindex $rowrangelist $rp1]
3971        if {$ranges eq {}} {
3972            set ranges [list $newcmit $p]
3973        } elseif {[lindex $ranges end-1] eq $p} {
3974            lset ranges end-1 $newcmit
3975        }
3976        lset rowrangelist $rp1 $ranges
3977    }
3978
3979    catch {unset rowchk}
3980
3981    incr rowlaidout
3982    incr rowoptim
3983    incr numcommits
3984
3985    if {[info exists selectedline] && $selectedline >= $row} {
3986        incr selectedline
3987    }
3988    redisplay
3989}
3990
3991# Remove a commit that was inserted with insertrow on row $row.
3992proc removerow {row} {
3993    global displayorder parentlist commitlisted children
3994    global commitrow curview rowidlist rowoffsets numcommits
3995    global rowrangelist idrowranges rowlaidout rowoptim numcommits
3996    global linesegends selectedline rowchk commitidx
3997
3998    if {$row >= $numcommits} {
3999        puts "oops, removing row $row but only have $numcommits rows"
4000        return
4001    }
4002    set rp1 [expr {$row + 1}]
4003    set id [lindex $displayorder $row]
4004    set p [lindex $parentlist $row]
4005    set displayorder [lreplace $displayorder $row $row]
4006    set parentlist [lreplace $parentlist $row $row]
4007    set commitlisted [lreplace $commitlisted $row $row]
4008    set kids $children($curview,$p)
4009    set i [lsearch -exact $kids $id]
4010    if {$i >= 0} {
4011        set kids [lreplace $kids $i $i]
4012        set children($curview,$p) $kids
4013    }
4014    set l [llength $displayorder]
4015    for {set r $row} {$r < $l} {incr r} {
4016        set id [lindex $displayorder $r]
4017        set commitrow($curview,$id) $r
4018    }
4019    incr commitidx($curview) -1
4020
4021    set rowidlist [lreplace $rowidlist $row $row]
4022    set rowoffsets [lreplace $rowoffsets $rp1 $rp1]
4023    if {$kids ne {}} {
4024        set offs [lindex $rowoffsets $row]
4025        set offs [lreplace $offs end end]
4026        lset rowoffsets $row $offs
4027    }
4028
4029    set rowrangelist [lreplace $rowrangelist $row $row]
4030    if {[llength $kids] > 0} {
4031        set ranges [lindex $rowrangelist $row]
4032        if {[lindex $ranges end-1] eq $id} {
4033            set ranges [lreplace $ranges end-1 end]
4034            lset rowrangelist $row $ranges
4035        }
4036    }
4037
4038    catch {unset rowchk}
4039
4040    incr rowlaidout -1
4041    incr rowoptim -1
4042    incr numcommits -1
4043
4044    if {[info exists selectedline] && $selectedline > $row} {
4045        incr selectedline -1
4046    }
4047    redisplay
4048}
4049
4050# Don't change the text pane cursor if it is currently the hand cursor,
4051# showing that we are over a sha1 ID link.
4052proc settextcursor {c} {
4053    global ctext curtextcursor
4054
4055    if {[$ctext cget -cursor] == $curtextcursor} {
4056        $ctext config -cursor $c
4057    }
4058    set curtextcursor $c
4059}
4060
4061proc nowbusy {what} {
4062    global isbusy
4063
4064    if {[array names isbusy] eq {}} {
4065        . config -cursor watch
4066        settextcursor watch
4067    }
4068    set isbusy($what) 1
4069}
4070
4071proc notbusy {what} {
4072    global isbusy maincursor textcursor
4073
4074    catch {unset isbusy($what)}
4075    if {[array names isbusy] eq {}} {
4076        . config -cursor $maincursor
4077        settextcursor $textcursor
4078    }
4079}
4080
4081proc findmatches {f} {
4082    global findtype findstring
4083    if {$findtype == "Regexp"} {
4084        set matches [regexp -indices -all -inline $findstring $f]
4085    } else {
4086        set fs $findstring
4087        if {$findtype == "IgnCase"} {
4088            set f [string tolower $f]
4089            set fs [string tolower $fs]
4090        }
4091        set matches {}
4092        set i 0
4093        set l [string length $fs]
4094        while {[set j [string first $fs $f $i]] >= 0} {
4095            lappend matches [list $j [expr {$j+$l-1}]]
4096            set i [expr {$j + $l}]
4097        }
4098    }
4099    return $matches
4100}
4101
4102proc dofind {{rev 0}} {
4103    global findstring findstartline findcurline selectedline numcommits
4104
4105    unmarkmatches
4106    cancel_next_highlight
4107    focus .
4108    if {$findstring eq {} || $numcommits == 0} return
4109    if {![info exists selectedline]} {
4110        set findstartline [lindex [visiblerows] $rev]
4111    } else {
4112        set findstartline $selectedline
4113    }
4114    set findcurline $findstartline
4115    nowbusy finding
4116    if {!$rev} {
4117        run findmore
4118    } else {
4119        if {$findcurline == 0} {
4120            set findcurline $numcommits
4121        }
4122        incr findcurline -1
4123        run findmorerev
4124    }
4125}
4126
4127proc findnext {restart} {
4128    global findcurline
4129    if {![info exists findcurline]} {
4130        if {$restart} {
4131            dofind
4132        } else {
4133            bell
4134        }
4135    } else {
4136        run findmore
4137        nowbusy finding
4138    }
4139}
4140
4141proc findprev {} {
4142    global findcurline
4143    if {![info exists findcurline]} {
4144        dofind 1
4145    } else {
4146        run findmorerev
4147        nowbusy finding
4148    }
4149}
4150
4151proc findmore {} {
4152    global commitdata commitinfo numcommits findstring findpattern findloc
4153    global findstartline findcurline displayorder
4154
4155    set fldtypes {Headline Author Date Committer CDate Comments}
4156    set l [expr {$findcurline + 1}]
4157    if {$l >= $numcommits} {
4158        set l 0
4159    }
4160    if {$l <= $findstartline} {
4161        set lim [expr {$findstartline + 1}]
4162    } else {
4163        set lim $numcommits
4164    }
4165    if {$lim - $l > 500} {
4166        set lim [expr {$l + 500}]
4167    }
4168    set last 0
4169    for {} {$l < $lim} {incr l} {
4170        set id [lindex $displayorder $l]
4171        # shouldn't happen unless git log doesn't give all the commits...
4172        if {![info exists commitdata($id)]} continue
4173        if {![doesmatch $commitdata($id)]} continue
4174        if {![info exists commitinfo($id)]} {
4175            getcommit $id
4176        }
4177        set info $commitinfo($id)
4178        foreach f $info ty $fldtypes {
4179            if {($findloc eq "All fields" || $findloc eq $ty) &&
4180                [doesmatch $f]} {
4181                findselectline $l
4182                notbusy finding
4183                return 0
4184            }
4185        }
4186    }
4187    if {$l == $findstartline + 1} {
4188        bell
4189        unset findcurline
4190        notbusy finding
4191        return 0
4192    }
4193    set findcurline [expr {$l - 1}]
4194    return 1
4195}
4196
4197proc findmorerev {} {
4198    global commitdata commitinfo numcommits findstring findpattern findloc
4199    global findstartline findcurline displayorder
4200
4201    set fldtypes {Headline Author Date Committer CDate Comments}
4202    set l $findcurline
4203    if {$l == 0} {
4204        set l $numcommits
4205    }
4206    incr l -1
4207    if {$l >= $findstartline} {
4208        set lim [expr {$findstartline - 1}]
4209    } else {
4210        set lim -1
4211    }
4212    if {$l - $lim > 500} {
4213        set lim [expr {$l - 500}]
4214    }
4215    set last 0
4216    for {} {$l > $lim} {incr l -1} {
4217        set id [lindex $displayorder $l]
4218        if {![doesmatch $commitdata($id)]} continue
4219        if {![info exists commitinfo($id)]} {
4220            getcommit $id
4221        }
4222        set info $commitinfo($id)
4223        foreach f $info ty $fldtypes {
4224            if {($findloc eq "All fields" || $findloc eq $ty) &&
4225                [doesmatch $f]} {
4226                findselectline $l
4227                notbusy finding
4228                return 0
4229            }
4230        }
4231    }
4232    if {$l == -1} {
4233        bell
4234        unset findcurline
4235        notbusy finding
4236        return 0
4237    }
4238    set findcurline [expr {$l + 1}]
4239    return 1
4240}
4241
4242proc findselectline {l} {
4243    global findloc commentend ctext findcurline markingmatches
4244
4245    set markingmatches 1
4246    set findcurline $l
4247    selectline $l 1
4248    if {$findloc == "All fields" || $findloc == "Comments"} {
4249        # highlight the matches in the comments
4250        set f [$ctext get 1.0 $commentend]
4251        set matches [findmatches $f]
4252        foreach match $matches {
4253            set start [lindex $match 0]
4254            set end [expr {[lindex $match 1] + 1}]
4255            $ctext tag add found "1.0 + $start c" "1.0 + $end c"
4256        }
4257    }
4258    drawvisible
4259}
4260
4261# mark the bits of a headline or author that match a find string
4262proc markmatches {canv l str tag matches font row} {
4263    global selectedline
4264
4265    set bbox [$canv bbox $tag]
4266    set x0 [lindex $bbox 0]
4267    set y0 [lindex $bbox 1]
4268    set y1 [lindex $bbox 3]
4269    foreach match $matches {
4270        set start [lindex $match 0]
4271        set end [lindex $match 1]
4272        if {$start > $end} continue
4273        set xoff [font measure $font [string range $str 0 [expr {$start-1}]]]
4274        set xlen [font measure $font [string range $str 0 [expr {$end}]]]
4275        set t [$canv create rect [expr {$x0+$xoff}] $y0 \
4276                   [expr {$x0+$xlen+2}] $y1 \
4277                   -outline {} -tags [list match$l matches] -fill yellow]
4278        $canv lower $t
4279        if {[info exists selectedline] && $row == $selectedline} {
4280            $canv raise $t secsel
4281        }
4282    }
4283}
4284
4285proc unmarkmatches {} {
4286    global findids markingmatches findcurline
4287
4288    allcanvs delete matches
4289    catch {unset findids}
4290    set markingmatches 0
4291    catch {unset findcurline}
4292}
4293
4294proc selcanvline {w x y} {
4295    global canv canvy0 ctext linespc
4296    global rowtextx
4297    set ymax [lindex [$canv cget -scrollregion] 3]
4298    if {$ymax == {}} return
4299    set yfrac [lindex [$canv yview] 0]
4300    set y [expr {$y + $yfrac * $ymax}]
4301    set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
4302    if {$l < 0} {
4303        set l 0
4304    }
4305    if {$w eq $canv} {
4306        if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
4307    }
4308    unmarkmatches
4309    selectline $l 1
4310}
4311
4312proc commit_descriptor {p} {
4313    global commitinfo
4314    if {![info exists commitinfo($p)]} {
4315        getcommit $p
4316    }
4317    set l "..."
4318    if {[llength $commitinfo($p)] > 1} {
4319        set l [lindex $commitinfo($p) 0]
4320    }
4321    return "$p ($l)\n"
4322}
4323
4324# append some text to the ctext widget, and make any SHA1 ID
4325# that we know about be a clickable link.
4326proc appendwithlinks {text tags} {
4327    global ctext commitrow linknum curview
4328
4329    set start [$ctext index "end - 1c"]
4330    $ctext insert end $text $tags
4331    set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
4332    foreach l $links {
4333        set s [lindex $l 0]
4334        set e [lindex $l 1]
4335        set linkid [string range $text $s $e]
4336        if {![info exists commitrow($curview,$linkid)]} continue
4337        incr e
4338        $ctext tag add link "$start + $s c" "$start + $e c"
4339        $ctext tag add link$linknum "$start + $s c" "$start + $e c"
4340        $ctext tag bind link$linknum <1> \
4341            [list selectline $commitrow($curview,$linkid) 1]
4342        incr linknum
4343    }
4344    $ctext tag conf link -foreground blue -underline 1
4345    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
4346    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
4347}
4348
4349proc viewnextline {dir} {
4350    global canv linespc
4351
4352    $canv delete hover
4353    set ymax [lindex [$canv cget -scrollregion] 3]
4354    set wnow [$canv yview]
4355    set wtop [expr {[lindex $wnow 0] * $ymax}]
4356    set newtop [expr {$wtop + $dir * $linespc}]
4357    if {$newtop < 0} {
4358        set newtop 0
4359    } elseif {$newtop > $ymax} {
4360        set newtop $ymax
4361    }
4362    allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
4363}
4364
4365# add a list of tag or branch names at position pos
4366# returns the number of names inserted
4367proc appendrefs {pos ids var} {
4368    global ctext commitrow linknum curview $var maxrefs
4369
4370    if {[catch {$ctext index $pos}]} {
4371        return 0
4372    }
4373    $ctext conf -state normal
4374    $ctext delete $pos "$pos lineend"
4375    set tags {}
4376    foreach id $ids {
4377        foreach tag [set $var\($id\)] {
4378            lappend tags [list $tag $id]
4379        }
4380    }
4381    if {[llength $tags] > $maxrefs} {
4382        $ctext insert $pos "many ([llength $tags])"
4383    } else {
4384        set tags [lsort -index 0 -decreasing $tags]
4385        set sep {}
4386        foreach ti $tags {
4387            set id [lindex $ti 1]
4388            set lk link$linknum
4389            incr linknum
4390            $ctext tag delete $lk
4391            $ctext insert $pos $sep
4392            $ctext insert $pos [lindex $ti 0] $lk
4393            if {[info exists commitrow($curview,$id)]} {
4394                $ctext tag conf $lk -foreground blue
4395                $ctext tag bind $lk <1> \
4396                    [list selectline $commitrow($curview,$id) 1]
4397                $ctext tag conf $lk -underline 1
4398                $ctext tag bind $lk <Enter> { %W configure -cursor hand2 }
4399                $ctext tag bind $lk <Leave> \
4400                    { %W configure -cursor $curtextcursor }
4401            }
4402            set sep ", "
4403        }
4404    }
4405    $ctext conf -state disabled
4406    return [llength $tags]
4407}
4408
4409# called when we have finished computing the nearby tags
4410proc dispneartags {delay} {
4411    global selectedline currentid showneartags tagphase
4412
4413    if {![info exists selectedline] || !$showneartags} return
4414    after cancel dispnexttag
4415    if {$delay} {
4416        after 200 dispnexttag
4417        set tagphase -1
4418    } else {
4419        after idle dispnexttag
4420        set tagphase 0
4421    }
4422}
4423
4424proc dispnexttag {} {
4425    global selectedline currentid showneartags tagphase ctext
4426
4427    if {![info exists selectedline] || !$showneartags} return
4428    switch -- $tagphase {
4429        0 {
4430            set dtags [desctags $currentid]
4431            if {$dtags ne {}} {
4432                appendrefs precedes $dtags idtags
4433            }
4434        }
4435        1 {
4436            set atags [anctags $currentid]
4437            if {$atags ne {}} {
4438                appendrefs follows $atags idtags
4439            }
4440        }
4441        2 {
4442            set dheads [descheads $currentid]
4443            if {$dheads ne {}} {
4444                if {[appendrefs branch $dheads idheads] > 1
4445                    && [$ctext get "branch -3c"] eq "h"} {
4446                    # turn "Branch" into "Branches"
4447                    $ctext conf -state normal
4448                    $ctext insert "branch -2c" "es"
4449                    $ctext conf -state disabled
4450                }
4451            }
4452        }
4453    }
4454    if {[incr tagphase] <= 2} {
4455        after idle dispnexttag
4456    }
4457}
4458
4459proc selectline {l isnew} {
4460    global canv canv2 canv3 ctext commitinfo selectedline
4461    global displayorder linehtag linentag linedtag
4462    global canvy0 linespc parentlist children curview
4463    global currentid sha1entry
4464    global commentend idtags linknum
4465    global mergemax numcommits pending_select
4466    global cmitmode showneartags allcommits
4467
4468    catch {unset pending_select}
4469    $canv delete hover
4470    normalline
4471    cancel_next_highlight
4472    if {$l < 0 || $l >= $numcommits} return
4473    set y [expr {$canvy0 + $l * $linespc}]
4474    set ymax [lindex [$canv cget -scrollregion] 3]
4475    set ytop [expr {$y - $linespc - 1}]
4476    set ybot [expr {$y + $linespc + 1}]
4477    set wnow [$canv yview]
4478    set wtop [expr {[lindex $wnow 0] * $ymax}]
4479    set wbot [expr {[lindex $wnow 1] * $ymax}]
4480    set wh [expr {$wbot - $wtop}]
4481    set newtop $wtop
4482    if {$ytop < $wtop} {
4483        if {$ybot < $wtop} {
4484            set newtop [expr {$y - $wh / 2.0}]
4485        } else {
4486            set newtop $ytop
4487            if {$newtop > $wtop - $linespc} {
4488                set newtop [expr {$wtop - $linespc}]
4489            }
4490        }
4491    } elseif {$ybot > $wbot} {
4492        if {$ytop > $wbot} {
4493            set newtop [expr {$y - $wh / 2.0}]
4494        } else {
4495            set newtop [expr {$ybot - $wh}]
4496            if {$newtop < $wtop + $linespc} {
4497                set newtop [expr {$wtop + $linespc}]
4498            }
4499        }
4500    }
4501    if {$newtop != $wtop} {
4502        if {$newtop < 0} {
4503            set newtop 0
4504        }
4505        allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
4506        drawvisible
4507    }
4508
4509    if {![info exists linehtag($l)]} return
4510    $canv delete secsel
4511    set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
4512               -tags secsel -fill [$canv cget -selectbackground]]
4513    $canv lower $t
4514    $canv2 delete secsel
4515    set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
4516               -tags secsel -fill [$canv2 cget -selectbackground]]
4517    $canv2 lower $t
4518    $canv3 delete secsel
4519    set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
4520               -tags secsel -fill [$canv3 cget -selectbackground]]
4521    $canv3 lower $t
4522
4523    if {$isnew} {
4524        addtohistory [list selectline $l 0]
4525    }
4526
4527    set selectedline $l
4528
4529    set id [lindex $displayorder $l]
4530    set currentid $id
4531    $sha1entry delete 0 end
4532    $sha1entry insert 0 $id
4533    $sha1entry selection from 0
4534    $sha1entry selection to end
4535    rhighlight_sel $id
4536
4537    $ctext conf -state normal
4538    clear_ctext
4539    set linknum 0
4540    set info $commitinfo($id)
4541    set date [formatdate [lindex $info 2]]
4542    $ctext insert end "Author: [lindex $info 1]  $date\n"
4543    set date [formatdate [lindex $info 4]]
4544    $ctext insert end "Committer: [lindex $info 3]  $date\n"
4545    if {[info exists idtags($id)]} {
4546        $ctext insert end "Tags:"
4547        foreach tag $idtags($id) {
4548            $ctext insert end " $tag"
4549        }
4550        $ctext insert end "\n"
4551    }
4552
4553    set headers {}
4554    set olds [lindex $parentlist $l]
4555    if {[llength $olds] > 1} {
4556        set np 0
4557        foreach p $olds {
4558            if {$np >= $mergemax} {
4559                set tag mmax
4560            } else {
4561                set tag m$np
4562            }
4563            $ctext insert end "Parent: " $tag
4564            appendwithlinks [commit_descriptor $p] {}
4565            incr np
4566        }
4567    } else {
4568        foreach p $olds {
4569            append headers "Parent: [commit_descriptor $p]"
4570        }
4571    }
4572
4573    foreach c $children($curview,$id) {
4574        append headers "Child:  [commit_descriptor $c]"
4575    }
4576
4577    # make anything that looks like a SHA1 ID be a clickable link
4578    appendwithlinks $headers {}
4579    if {$showneartags} {
4580        if {![info exists allcommits]} {
4581            getallcommits
4582        }
4583        $ctext insert end "Branch: "
4584        $ctext mark set branch "end -1c"
4585        $ctext mark gravity branch left
4586        $ctext insert end "\nFollows: "
4587        $ctext mark set follows "end -1c"
4588        $ctext mark gravity follows left
4589        $ctext insert end "\nPrecedes: "
4590        $ctext mark set precedes "end -1c"
4591        $ctext mark gravity precedes left
4592        $ctext insert end "\n"
4593        dispneartags 1
4594    }
4595    $ctext insert end "\n"
4596    set comment [lindex $info 5]
4597    if {[string first "\r" $comment] >= 0} {
4598        set comment [string map {"\r" "\n    "} $comment]
4599    }
4600    appendwithlinks $comment {comment}
4601
4602    $ctext tag remove found 1.0 end
4603    $ctext conf -state disabled
4604    set commentend [$ctext index "end - 1c"]
4605
4606    init_flist "Comments"
4607    if {$cmitmode eq "tree"} {
4608        gettree $id
4609    } elseif {[llength $olds] <= 1} {
4610        startdiff $id
4611    } else {
4612        mergediff $id $l
4613    }
4614}
4615
4616proc selfirstline {} {
4617    unmarkmatches
4618    selectline 0 1
4619}
4620
4621proc sellastline {} {
4622    global numcommits
4623    unmarkmatches
4624    set l [expr {$numcommits - 1}]
4625    selectline $l 1
4626}
4627
4628proc selnextline {dir} {
4629    global selectedline
4630    focus .
4631    if {![info exists selectedline]} return
4632    set l [expr {$selectedline + $dir}]
4633    unmarkmatches
4634    selectline $l 1
4635}
4636
4637proc selnextpage {dir} {
4638    global canv linespc selectedline numcommits
4639
4640    set lpp [expr {([winfo height $canv] - 2) / $linespc}]
4641    if {$lpp < 1} {
4642        set lpp 1
4643    }
4644    allcanvs yview scroll [expr {$dir * $lpp}] units
4645    drawvisible
4646    if {![info exists selectedline]} return
4647    set l [expr {$selectedline + $dir * $lpp}]
4648    if {$l < 0} {
4649        set l 0
4650    } elseif {$l >= $numcommits} {
4651        set l [expr $numcommits - 1]
4652    }
4653    unmarkmatches
4654    selectline $l 1
4655}
4656
4657proc unselectline {} {
4658    global selectedline currentid
4659
4660    catch {unset selectedline}
4661    catch {unset currentid}
4662    allcanvs delete secsel
4663    rhighlight_none
4664    cancel_next_highlight
4665}
4666
4667proc reselectline {} {
4668    global selectedline
4669
4670    if {[info exists selectedline]} {
4671        selectline $selectedline 0
4672    }
4673}
4674
4675proc addtohistory {cmd} {
4676    global history historyindex curview
4677
4678    set elt [list $curview $cmd]
4679    if {$historyindex > 0
4680        && [lindex $history [expr {$historyindex - 1}]] == $elt} {
4681        return
4682    }
4683
4684    if {$historyindex < [llength $history]} {
4685        set history [lreplace $history $historyindex end $elt]
4686    } else {
4687        lappend history $elt
4688    }
4689    incr historyindex
4690    if {$historyindex > 1} {
4691        .tf.bar.leftbut conf -state normal
4692    } else {
4693        .tf.bar.leftbut conf -state disabled
4694    }
4695    .tf.bar.rightbut conf -state disabled
4696}
4697
4698proc godo {elt} {
4699    global curview
4700
4701    set view [lindex $elt 0]
4702    set cmd [lindex $elt 1]
4703    if {$curview != $view} {
4704        showview $view
4705    }
4706    eval $cmd
4707}
4708
4709proc goback {} {
4710    global history historyindex
4711    focus .
4712
4713    if {$historyindex > 1} {
4714        incr historyindex -1
4715        godo [lindex $history [expr {$historyindex - 1}]]
4716        .tf.bar.rightbut conf -state normal
4717    }
4718    if {$historyindex <= 1} {
4719        .tf.bar.leftbut conf -state disabled
4720    }
4721}
4722
4723proc goforw {} {
4724    global history historyindex
4725    focus .
4726
4727    if {$historyindex < [llength $history]} {
4728        set cmd [lindex $history $historyindex]
4729        incr historyindex
4730        godo $cmd
4731        .tf.bar.leftbut conf -state normal
4732    }
4733    if {$historyindex >= [llength $history]} {
4734        .tf.bar.rightbut conf -state disabled
4735    }
4736}
4737
4738proc gettree {id} {
4739    global treefilelist treeidlist diffids diffmergeid treepending
4740    global nullid nullid2
4741
4742    set diffids $id
4743    catch {unset diffmergeid}
4744    if {![info exists treefilelist($id)]} {
4745        if {![info exists treepending]} {
4746            if {$id eq $nullid} {
4747                set cmd [list | git ls-files]
4748            } elseif {$id eq $nullid2} {
4749                set cmd [list | git ls-files --stage -t]
4750            } else {
4751                set cmd [list | git ls-tree -r $id]
4752            }
4753            if {[catch {set gtf [open $cmd r]}]} {
4754                return
4755            }
4756            set treepending $id
4757            set treefilelist($id) {}
4758            set treeidlist($id) {}
4759            fconfigure $gtf -blocking 0
4760            filerun $gtf [list gettreeline $gtf $id]
4761        }
4762    } else {
4763        setfilelist $id
4764    }
4765}
4766
4767proc gettreeline {gtf id} {
4768    global treefilelist treeidlist treepending cmitmode diffids nullid nullid2
4769
4770    set nl 0
4771    while {[incr nl] <= 1000 && [gets $gtf line] >= 0} {
4772        if {$diffids eq $nullid} {
4773            set fname $line
4774        } else {
4775            if {$diffids ne $nullid2 && [lindex $line 1] ne "blob"} continue
4776            set i [string first "\t" $line]
4777            if {$i < 0} continue
4778            set sha1 [lindex $line 2]
4779            set fname [string range $line [expr {$i+1}] end]
4780            if {[string index $fname 0] eq "\""} {
4781                set fname [lindex $fname 0]
4782            }
4783            lappend treeidlist($id) $sha1
4784        }
4785        lappend treefilelist($id) $fname
4786    }
4787    if {![eof $gtf]} {
4788        return [expr {$nl >= 1000? 2: 1}]
4789    }
4790    close $gtf
4791    unset treepending
4792    if {$cmitmode ne "tree"} {
4793        if {![info exists diffmergeid]} {
4794            gettreediffs $diffids
4795        }
4796    } elseif {$id ne $diffids} {
4797        gettree $diffids
4798    } else {
4799        setfilelist $id
4800    }
4801    return 0
4802}
4803
4804proc showfile {f} {
4805    global treefilelist treeidlist diffids nullid nullid2
4806    global ctext commentend
4807
4808    set i [lsearch -exact $treefilelist($diffids) $f]
4809    if {$i < 0} {
4810        puts "oops, $f not in list for id $diffids"
4811        return
4812    }
4813    if {$diffids eq $nullid} {
4814        if {[catch {set bf [open $f r]} err]} {
4815            puts "oops, can't read $f: $err"
4816            return
4817        }
4818    } else {
4819        set blob [lindex $treeidlist($diffids) $i]
4820        if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} {
4821            puts "oops, error reading blob $blob: $err"
4822            return
4823        }
4824    }
4825    fconfigure $bf -blocking 0
4826    filerun $bf [list getblobline $bf $diffids]
4827    $ctext config -state normal
4828    clear_ctext $commentend
4829    $ctext insert end "\n"
4830    $ctext insert end "$f\n" filesep
4831    $ctext config -state disabled
4832    $ctext yview $commentend
4833}
4834
4835proc getblobline {bf id} {
4836    global diffids cmitmode ctext
4837
4838    if {$id ne $diffids || $cmitmode ne "tree"} {
4839        catch {close $bf}
4840        return 0
4841    }
4842    $ctext config -state normal
4843    set nl 0
4844    while {[incr nl] <= 1000 && [gets $bf line] >= 0} {
4845        $ctext insert end "$line\n"
4846    }
4847    if {[eof $bf]} {
4848        # delete last newline
4849        $ctext delete "end - 2c" "end - 1c"
4850        close $bf
4851        return 0
4852    }
4853    $ctext config -state disabled
4854    return [expr {$nl >= 1000? 2: 1}]
4855}
4856
4857proc mergediff {id l} {
4858    global diffmergeid diffopts mdifffd
4859    global diffids
4860    global parentlist
4861
4862    set diffmergeid $id
4863    set diffids $id
4864    # this doesn't seem to actually affect anything...
4865    set env(GIT_DIFF_OPTS) $diffopts
4866    set cmd [concat | git diff-tree --no-commit-id --cc $id]
4867    if {[catch {set mdf [open $cmd r]} err]} {
4868        error_popup "Error getting merge diffs: $err"
4869        return
4870    }
4871    fconfigure $mdf -blocking 0
4872    set mdifffd($id) $mdf
4873    set np [llength [lindex $parentlist $l]]
4874    filerun $mdf [list getmergediffline $mdf $id $np]
4875}
4876
4877proc getmergediffline {mdf id np} {
4878    global diffmergeid ctext cflist mergemax
4879    global difffilestart mdifffd
4880
4881    $ctext conf -state normal
4882    set nr 0
4883    while {[incr nr] <= 1000 && [gets $mdf line] >= 0} {
4884        if {![info exists diffmergeid] || $id != $diffmergeid
4885            || $mdf != $mdifffd($id)} {
4886            close $mdf
4887            return 0
4888        }
4889        if {[regexp {^diff --cc (.*)} $line match fname]} {
4890            # start of a new file
4891            $ctext insert end "\n"
4892            set here [$ctext index "end - 1c"]
4893            lappend difffilestart $here
4894            add_flist [list $fname]
4895            set l [expr {(78 - [string length $fname]) / 2}]
4896            set pad [string range "----------------------------------------" 1 $l]
4897            $ctext insert end "$pad $fname $pad\n" filesep
4898        } elseif {[regexp {^@@} $line]} {
4899            $ctext insert end "$line\n" hunksep
4900        } elseif {[regexp {^[0-9a-f]{40}$} $line] || [regexp {^index} $line]} {
4901            # do nothing
4902        } else {
4903            # parse the prefix - one ' ', '-' or '+' for each parent
4904            set spaces {}
4905            set minuses {}
4906            set pluses {}
4907            set isbad 0
4908            for {set j 0} {$j < $np} {incr j} {
4909                set c [string range $line $j $j]
4910                if {$c == " "} {
4911                    lappend spaces $j
4912                } elseif {$c == "-"} {
4913                    lappend minuses $j
4914                } elseif {$c == "+"} {
4915                    lappend pluses $j
4916                } else {
4917                    set isbad 1
4918                    break
4919                }
4920            }
4921            set tags {}
4922            set num {}
4923            if {!$isbad && $minuses ne {} && $pluses eq {}} {
4924                # line doesn't appear in result, parents in $minuses have the line
4925                set num [lindex $minuses 0]
4926            } elseif {!$isbad && $pluses ne {} && $minuses eq {}} {
4927                # line appears in result, parents in $pluses don't have the line
4928                lappend tags mresult
4929                set num [lindex $spaces 0]
4930            }
4931            if {$num ne {}} {
4932                if {$num >= $mergemax} {
4933                    set num "max"
4934                }
4935                lappend tags m$num
4936            }
4937            $ctext insert end "$line\n" $tags
4938        }
4939    }
4940    $ctext conf -state disabled
4941    if {[eof $mdf]} {
4942        close $mdf
4943        return 0
4944    }
4945    return [expr {$nr >= 1000? 2: 1}]
4946}
4947
4948proc startdiff {ids} {
4949    global treediffs diffids treepending diffmergeid nullid nullid2
4950
4951    set diffids $ids
4952    catch {unset diffmergeid}
4953    if {![info exists treediffs($ids)] ||
4954        [lsearch -exact $ids $nullid] >= 0 ||
4955        [lsearch -exact $ids $nullid2] >= 0} {
4956        if {![info exists treepending]} {
4957            gettreediffs $ids
4958        }
4959    } else {
4960        addtocflist $ids
4961    }
4962}
4963
4964proc addtocflist {ids} {
4965    global treediffs cflist
4966    add_flist $treediffs($ids)
4967    getblobdiffs $ids
4968}
4969
4970proc diffcmd {ids flags} {
4971    global nullid nullid2
4972
4973    set i [lsearch -exact $ids $nullid]
4974    set j [lsearch -exact $ids $nullid2]
4975    if {$i >= 0} {
4976        if {[llength $ids] > 1 && $j < 0} {
4977            # comparing working directory with some specific revision
4978            set cmd [concat | git diff-index $flags]
4979            if {$i == 0} {
4980                lappend cmd -R [lindex $ids 1]
4981            } else {
4982                lappend cmd [lindex $ids 0]
4983            }
4984        } else {
4985            # comparing working directory with index
4986            set cmd [concat | git diff-files $flags]
4987            if {$j == 1} {
4988                lappend cmd -R
4989            }
4990        }
4991    } elseif {$j >= 0} {
4992        set cmd [concat | git diff-index --cached $flags]
4993        if {[llength $ids] > 1} {
4994            # comparing index with specific revision
4995            if {$i == 0} {
4996                lappend cmd -R [lindex $ids 1]
4997            } else {
4998                lappend cmd [lindex $ids 0]
4999            }
5000        } else {
5001            # comparing index with HEAD
5002            lappend cmd HEAD
5003        }
5004    } else {
5005        set cmd [concat | git diff-tree -r $flags $ids]
5006    }
5007    return $cmd
5008}
5009
5010proc gettreediffs {ids} {
5011    global treediff treepending
5012
5013    set treepending $ids
5014    set treediff {}
5015    if {[catch {set gdtf [open [diffcmd $ids {--no-commit-id}] r]}]} return
5016    fconfigure $gdtf -blocking 0
5017    filerun $gdtf [list gettreediffline $gdtf $ids]
5018}
5019
5020proc gettreediffline {gdtf ids} {
5021    global treediff treediffs treepending diffids diffmergeid
5022    global cmitmode
5023
5024    set nr 0
5025    while {[incr nr] <= 1000 && [gets $gdtf line] >= 0} {
5026        set i [string first "\t" $line]
5027        if {$i >= 0} {
5028            set file [string range $line [expr {$i+1}] end]
5029            if {[string index $file 0] eq "\""} {
5030                set file [lindex $file 0]
5031            }
5032            lappend treediff $file
5033        }
5034    }
5035    if {![eof $gdtf]} {
5036        return [expr {$nr >= 1000? 2: 1}]
5037    }
5038    close $gdtf
5039    set treediffs($ids) $treediff
5040    unset treepending
5041    if {$cmitmode eq "tree"} {
5042        gettree $diffids
5043    } elseif {$ids != $diffids} {
5044        if {![info exists diffmergeid]} {
5045            gettreediffs $diffids
5046        }
5047    } else {
5048        addtocflist $ids
5049    }
5050    return 0
5051}
5052
5053proc getblobdiffs {ids} {
5054    global diffopts blobdifffd diffids env
5055    global diffinhdr treediffs
5056
5057    set env(GIT_DIFF_OPTS) $diffopts
5058    if {[catch {set bdf [open [diffcmd $ids {-p -C --no-commit-id}] r]} err]} {
5059        puts "error getting diffs: $err"
5060        return
5061    }
5062    set diffinhdr 0
5063    fconfigure $bdf -blocking 0
5064    set blobdifffd($ids) $bdf
5065    filerun $bdf [list getblobdiffline $bdf $diffids]
5066}
5067
5068proc setinlist {var i val} {
5069    global $var
5070
5071    while {[llength [set $var]] < $i} {
5072        lappend $var {}
5073    }
5074    if {[llength [set $var]] == $i} {
5075        lappend $var $val
5076    } else {
5077        lset $var $i $val
5078    }
5079}
5080
5081proc makediffhdr {fname ids} {
5082    global ctext curdiffstart treediffs
5083
5084    set i [lsearch -exact $treediffs($ids) $fname]
5085    if {$i >= 0} {
5086        setinlist difffilestart $i $curdiffstart
5087    }
5088    set l [expr {(78 - [string length $fname]) / 2}]
5089    set pad [string range "----------------------------------------" 1 $l]
5090    $ctext insert $curdiffstart "$pad $fname $pad" filesep
5091}
5092
5093proc getblobdiffline {bdf ids} {
5094    global diffids blobdifffd ctext curdiffstart
5095    global diffnexthead diffnextnote difffilestart
5096    global diffinhdr treediffs
5097
5098    set nr 0
5099    $ctext conf -state normal
5100    while {[incr nr] <= 1000 && [gets $bdf line] >= 0} {
5101        if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
5102            close $bdf
5103            return 0
5104        }
5105        if {![string compare -length 11 "diff --git " $line]} {
5106            # trim off "diff --git "
5107            set line [string range $line 11 end]
5108            set diffinhdr 1
5109            # start of a new file
5110            $ctext insert end "\n"
5111            set curdiffstart [$ctext index "end - 1c"]
5112            $ctext insert end "\n" filesep
5113            # If the name hasn't changed the length will be odd,
5114            # the middle char will be a space, and the two bits either
5115            # side will be a/name and b/name, or "a/name" and "b/name".
5116            # If the name has changed we'll get "rename from" and
5117            # "rename to" lines following this, and we'll use them
5118            # to get the filenames.
5119            # This complexity is necessary because spaces in the filename(s)
5120            # don't get escaped.
5121            set l [string length $line]
5122            set i [expr {$l / 2}]
5123            if {!(($l & 1) && [string index $line $i] eq " " &&
5124                  [string range $line 2 [expr {$i - 1}]] eq \
5125                      [string range $line [expr {$i + 3}] end])} {
5126                continue
5127            }
5128            # unescape if quoted and chop off the a/ from the front
5129            if {[string index $line 0] eq "\""} {
5130                set fname [string range [lindex $line 0] 2 end]
5131            } else {
5132                set fname [string range $line 2 [expr {$i - 1}]]
5133            }
5134            makediffhdr $fname $ids
5135
5136        } elseif {[regexp {^@@ -([0-9]+)(,[0-9]+)? \+([0-9]+)(,[0-9]+)? @@(.*)} \
5137                       $line match f1l f1c f2l f2c rest]} {
5138            $ctext insert end "$line\n" hunksep
5139            set diffinhdr 0
5140
5141        } elseif {$diffinhdr} {
5142            if {![string compare -length 12 "rename from " $line]} {
5143                set fname [string range $line 12 end]
5144                if {[string index $fname 0] eq "\""} {
5145                    set fname [lindex $fname 0]
5146                }
5147                set i [lsearch -exact $treediffs($ids) $fname]
5148                if {$i >= 0} {
5149                    setinlist difffilestart $i $curdiffstart
5150                }
5151            } elseif {![string compare -length 10 $line "rename to "]} {
5152                set fname [string range $line 10 end]
5153                if {[string index $fname 0] eq "\""} {
5154                    set fname [lindex $fname 0]
5155                }
5156                makediffhdr $fname $ids
5157            } elseif {[string compare -length 3 $line "---"] == 0} {
5158                # do nothing
5159                continue
5160            } elseif {[string compare -length 3 $line "+++"] == 0} {
5161                set diffinhdr 0
5162                continue
5163            }
5164            $ctext insert end "$line\n" filesep
5165
5166        } else {
5167            set x [string range $line 0 0]
5168            if {$x == "-" || $x == "+"} {
5169                set tag [expr {$x == "+"}]
5170                $ctext insert end "$line\n" d$tag
5171            } elseif {$x == " "} {
5172                $ctext insert end "$line\n"
5173            } else {
5174                # "\ No newline at end of file",
5175                # or something else we don't recognize
5176                $ctext insert end "$line\n" hunksep
5177            }
5178        }
5179    }
5180    $ctext conf -state disabled
5181    if {[eof $bdf]} {
5182        close $bdf
5183        return 0
5184    }
5185    return [expr {$nr >= 1000? 2: 1}]
5186}
5187
5188proc changediffdisp {} {
5189    global ctext diffelide
5190
5191    $ctext tag conf d0 -elide [lindex $diffelide 0]
5192    $ctext tag conf d1 -elide [lindex $diffelide 1]
5193}
5194
5195proc prevfile {} {
5196    global difffilestart ctext
5197    set prev [lindex $difffilestart 0]
5198    set here [$ctext index @0,0]
5199    foreach loc $difffilestart {
5200        if {[$ctext compare $loc >= $here]} {
5201            $ctext yview $prev
5202            return
5203        }
5204        set prev $loc
5205    }
5206    $ctext yview $prev
5207}
5208
5209proc nextfile {} {
5210    global difffilestart ctext
5211    set here [$ctext index @0,0]
5212    foreach loc $difffilestart {
5213        if {[$ctext compare $loc > $here]} {
5214            $ctext yview $loc
5215            return
5216        }
5217    }
5218}
5219
5220proc clear_ctext {{first 1.0}} {
5221    global ctext smarktop smarkbot
5222
5223    set l [lindex [split $first .] 0]
5224    if {![info exists smarktop] || [$ctext compare $first < $smarktop.0]} {
5225        set smarktop $l
5226    }
5227    if {![info exists smarkbot] || [$ctext compare $first < $smarkbot.0]} {
5228        set smarkbot $l
5229    }
5230    $ctext delete $first end
5231}
5232
5233proc incrsearch {name ix op} {
5234    global ctext searchstring searchdirn
5235
5236    $ctext tag remove found 1.0 end
5237    if {[catch {$ctext index anchor}]} {
5238        # no anchor set, use start of selection, or of visible area
5239        set sel [$ctext tag ranges sel]
5240        if {$sel ne {}} {
5241            $ctext mark set anchor [lindex $sel 0]
5242        } elseif {$searchdirn eq "-forwards"} {
5243            $ctext mark set anchor @0,0
5244        } else {
5245            $ctext mark set anchor @0,[winfo height $ctext]
5246        }
5247    }
5248    if {$searchstring ne {}} {
5249        set here [$ctext search $searchdirn -- $searchstring anchor]
5250        if {$here ne {}} {
5251            $ctext see $here
5252        }
5253        searchmarkvisible 1
5254    }
5255}
5256
5257proc dosearch {} {
5258    global sstring ctext searchstring searchdirn
5259
5260    focus $sstring
5261    $sstring icursor end
5262    set searchdirn -forwards
5263    if {$searchstring ne {}} {
5264        set sel [$ctext tag ranges sel]
5265        if {$sel ne {}} {
5266            set start "[lindex $sel 0] + 1c"
5267        } elseif {[catch {set start [$ctext index anchor]}]} {
5268            set start "@0,0"
5269        }
5270        set match [$ctext search -count mlen -- $searchstring $start]
5271        $ctext tag remove sel 1.0 end
5272        if {$match eq {}} {
5273            bell
5274            return
5275        }
5276        $ctext see $match
5277        set mend "$match + $mlen c"
5278        $ctext tag add sel $match $mend
5279        $ctext mark unset anchor
5280    }
5281}
5282
5283proc dosearchback {} {
5284    global sstring ctext searchstring searchdirn
5285
5286    focus $sstring
5287    $sstring icursor end
5288    set searchdirn -backwards
5289    if {$searchstring ne {}} {
5290        set sel [$ctext tag ranges sel]
5291        if {$sel ne {}} {
5292            set start [lindex $sel 0]
5293        } elseif {[catch {set start [$ctext index anchor]}]} {
5294            set start @0,[winfo height $ctext]
5295        }
5296        set match [$ctext search -backwards -count ml -- $searchstring $start]
5297        $ctext tag remove sel 1.0 end
5298        if {$match eq {}} {
5299            bell
5300            return
5301        }
5302        $ctext see $match
5303        set mend "$match + $ml c"
5304        $ctext tag add sel $match $mend
5305        $ctext mark unset anchor
5306    }
5307}
5308
5309proc searchmark {first last} {
5310    global ctext searchstring
5311
5312    set mend $first.0
5313    while {1} {
5314        set match [$ctext search -count mlen -- $searchstring $mend $last.end]
5315        if {$match eq {}} break
5316        set mend "$match + $mlen c"
5317        $ctext tag add found $match $mend
5318    }
5319}
5320
5321proc searchmarkvisible {doall} {
5322    global ctext smarktop smarkbot
5323
5324    set topline [lindex [split [$ctext index @0,0] .] 0]
5325    set botline [lindex [split [$ctext index @0,[winfo height $ctext]] .] 0]
5326    if {$doall || $botline < $smarktop || $topline > $smarkbot} {
5327        # no overlap with previous
5328        searchmark $topline $botline
5329        set smarktop $topline
5330        set smarkbot $botline
5331    } else {
5332        if {$topline < $smarktop} {
5333            searchmark $topline [expr {$smarktop-1}]
5334            set smarktop $topline
5335        }
5336        if {$botline > $smarkbot} {
5337            searchmark [expr {$smarkbot+1}] $botline
5338            set smarkbot $botline
5339        }
5340    }
5341}
5342
5343proc scrolltext {f0 f1} {
5344    global searchstring
5345
5346    .bleft.sb set $f0 $f1
5347    if {$searchstring ne {}} {
5348        searchmarkvisible 0
5349    }
5350}
5351
5352proc setcoords {} {
5353    global linespc charspc canvx0 canvy0 mainfont
5354    global xspc1 xspc2 lthickness
5355
5356    set linespc [font metrics $mainfont -linespace]
5357    set charspc [font measure $mainfont "m"]
5358    set canvy0 [expr {int(3 + 0.5 * $linespc)}]
5359    set canvx0 [expr {int(3 + 0.5 * $linespc)}]
5360    set lthickness [expr {int($linespc / 9) + 1}]
5361    set xspc1(0) $linespc
5362    set xspc2 $linespc
5363}
5364
5365proc redisplay {} {
5366    global canv
5367    global selectedline
5368
5369    set ymax [lindex [$canv cget -scrollregion] 3]
5370    if {$ymax eq {} || $ymax == 0} return
5371    set span [$canv yview]
5372    clear_display
5373    setcanvscroll
5374    allcanvs yview moveto [lindex $span 0]
5375    drawvisible
5376    if {[info exists selectedline]} {
5377        selectline $selectedline 0
5378        allcanvs yview moveto [lindex $span 0]
5379    }
5380}
5381
5382proc incrfont {inc} {
5383    global mainfont textfont ctext canv phase cflist
5384    global charspc tabstop
5385    global stopped entries
5386    unmarkmatches
5387    set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
5388    set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
5389    setcoords
5390    $ctext conf -font $textfont -tabs "[expr {$tabstop * $charspc}]"
5391    $cflist conf -font $textfont
5392    $ctext tag conf filesep -font [concat $textfont bold]
5393    foreach e $entries {
5394        $e conf -font $mainfont
5395    }
5396    if {$phase eq "getcommits"} {
5397        $canv itemconf textitems -font $mainfont
5398    }
5399    redisplay
5400}
5401
5402proc clearsha1 {} {
5403    global sha1entry sha1string
5404    if {[string length $sha1string] == 40} {
5405        $sha1entry delete 0 end
5406    }
5407}
5408
5409proc sha1change {n1 n2 op} {
5410    global sha1string currentid sha1but
5411    if {$sha1string == {}
5412        || ([info exists currentid] && $sha1string == $currentid)} {
5413        set state disabled
5414    } else {
5415        set state normal
5416    }
5417    if {[$sha1but cget -state] == $state} return
5418    if {$state == "normal"} {
5419        $sha1but conf -state normal -relief raised -text "Goto: "
5420    } else {
5421        $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
5422    }
5423}
5424
5425proc gotocommit {} {
5426    global sha1string currentid commitrow tagids headids
5427    global displayorder numcommits curview
5428
5429    if {$sha1string == {}
5430        || ([info exists currentid] && $sha1string == $currentid)} return
5431    if {[info exists tagids($sha1string)]} {
5432        set id $tagids($sha1string)
5433    } elseif {[info exists headids($sha1string)]} {
5434        set id $headids($sha1string)
5435    } else {
5436        set id [string tolower $sha1string]
5437        if {[regexp {^[0-9a-f]{4,39}$} $id]} {
5438            set matches {}
5439            foreach i $displayorder {
5440                if {[string match $id* $i]} {
5441                    lappend matches $i
5442                }
5443            }
5444            if {$matches ne {}} {
5445                if {[llength $matches] > 1} {
5446                    error_popup "Short SHA1 id $id is ambiguous"
5447                    return
5448                }
5449                set id [lindex $matches 0]
5450            }
5451        }
5452    }
5453    if {[info exists commitrow($curview,$id)]} {
5454        selectline $commitrow($curview,$id) 1
5455        return
5456    }
5457    if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
5458        set type "SHA1 id"
5459    } else {
5460        set type "Tag/Head"
5461    }
5462    error_popup "$type $sha1string is not known"
5463}
5464
5465proc lineenter {x y id} {
5466    global hoverx hovery hoverid hovertimer
5467    global commitinfo canv
5468
5469    if {![info exists commitinfo($id)] && ![getcommit $id]} return
5470    set hoverx $x
5471    set hovery $y
5472    set hoverid $id
5473    if {[info exists hovertimer]} {
5474        after cancel $hovertimer
5475    }
5476    set hovertimer [after 500 linehover]
5477    $canv delete hover
5478}
5479
5480proc linemotion {x y id} {
5481    global hoverx hovery hoverid hovertimer
5482
5483    if {[info exists hoverid] && $id == $hoverid} {
5484        set hoverx $x
5485        set hovery $y
5486        if {[info exists hovertimer]} {
5487            after cancel $hovertimer
5488        }
5489        set hovertimer [after 500 linehover]
5490    }
5491}
5492
5493proc lineleave {id} {
5494    global hoverid hovertimer canv
5495
5496    if {[info exists hoverid] && $id == $hoverid} {
5497        $canv delete hover
5498        if {[info exists hovertimer]} {
5499            after cancel $hovertimer
5500            unset hovertimer
5501        }
5502        unset hoverid
5503    }
5504}
5505
5506proc linehover {} {
5507    global hoverx hovery hoverid hovertimer
5508    global canv linespc lthickness
5509    global commitinfo mainfont
5510
5511    set text [lindex $commitinfo($hoverid) 0]
5512    set ymax [lindex [$canv cget -scrollregion] 3]
5513    if {$ymax == {}} return
5514    set yfrac [lindex [$canv yview] 0]
5515    set x [expr {$hoverx + 2 * $linespc}]
5516    set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
5517    set x0 [expr {$x - 2 * $lthickness}]
5518    set y0 [expr {$y - 2 * $lthickness}]
5519    set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}]
5520    set y1 [expr {$y + $linespc + 2 * $lthickness}]
5521    set t [$canv create rectangle $x0 $y0 $x1 $y1 \
5522               -fill \#ffff80 -outline black -width 1 -tags hover]
5523    $canv raise $t
5524    set t [$canv create text $x $y -anchor nw -text $text -tags hover \
5525               -font $mainfont]
5526    $canv raise $t
5527}
5528
5529proc clickisonarrow {id y} {
5530    global lthickness
5531
5532    set ranges [rowranges $id]
5533    set thresh [expr {2 * $lthickness + 6}]
5534    set n [expr {[llength $ranges] - 1}]
5535    for {set i 1} {$i < $n} {incr i} {
5536        set row [lindex $ranges $i]
5537        if {abs([yc $row] - $y) < $thresh} {
5538            return $i
5539        }
5540    }
5541    return {}
5542}
5543
5544proc arrowjump {id n y} {
5545    global canv
5546
5547    # 1 <-> 2, 3 <-> 4, etc...
5548    set n [expr {(($n - 1) ^ 1) + 1}]
5549    set row [lindex [rowranges $id] $n]
5550    set yt [yc $row]
5551    set ymax [lindex [$canv cget -scrollregion] 3]
5552    if {$ymax eq {} || $ymax <= 0} return
5553    set view [$canv yview]
5554    set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
5555    set yfrac [expr {$yt / $ymax - $yspan / 2}]
5556    if {$yfrac < 0} {
5557        set yfrac 0
5558    }
5559    allcanvs yview moveto $yfrac
5560}
5561
5562proc lineclick {x y id isnew} {
5563    global ctext commitinfo children canv thickerline curview
5564
5565    if {![info exists commitinfo($id)] && ![getcommit $id]} return
5566    unmarkmatches
5567    unselectline
5568    normalline
5569    $canv delete hover
5570    # draw this line thicker than normal
5571    set thickerline $id
5572    drawlines $id
5573    if {$isnew} {
5574        set ymax [lindex [$canv cget -scrollregion] 3]
5575        if {$ymax eq {}} return
5576        set yfrac [lindex [$canv yview] 0]
5577        set y [expr {$y + $yfrac * $ymax}]
5578    }
5579    set dirn [clickisonarrow $id $y]
5580    if {$dirn ne {}} {
5581        arrowjump $id $dirn $y
5582        return
5583    }
5584
5585    if {$isnew} {
5586        addtohistory [list lineclick $x $y $id 0]
5587    }
5588    # fill the details pane with info about this line
5589    $ctext conf -state normal
5590    clear_ctext
5591    $ctext tag conf link -foreground blue -underline 1
5592    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
5593    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
5594    $ctext insert end "Parent:\t"
5595    $ctext insert end $id [list link link0]
5596    $ctext tag bind link0 <1> [list selbyid $id]
5597    set info $commitinfo($id)
5598    $ctext insert end "\n\t[lindex $info 0]\n"
5599    $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
5600    set date [formatdate [lindex $info 2]]
5601    $ctext insert end "\tDate:\t$date\n"
5602    set kids $children($curview,$id)
5603    if {$kids ne {}} {
5604        $ctext insert end "\nChildren:"
5605        set i 0
5606        foreach child $kids {
5607            incr i
5608            if {![info exists commitinfo($child)] && ![getcommit $child]} continue
5609            set info $commitinfo($child)
5610            $ctext insert end "\n\t"
5611            $ctext insert end $child [list link link$i]
5612            $ctext tag bind link$i <1> [list selbyid $child]
5613            $ctext insert end "\n\t[lindex $info 0]"
5614            $ctext insert end "\n\tAuthor:\t[lindex $info 1]"
5615            set date [formatdate [lindex $info 2]]
5616            $ctext insert end "\n\tDate:\t$date\n"
5617        }
5618    }
5619    $ctext conf -state disabled
5620    init_flist {}
5621}
5622
5623proc normalline {} {
5624    global thickerline
5625    if {[info exists thickerline]} {
5626        set id $thickerline
5627        unset thickerline
5628        drawlines $id
5629    }
5630}
5631
5632proc selbyid {id} {
5633    global commitrow curview
5634    if {[info exists commitrow($curview,$id)]} {
5635        selectline $commitrow($curview,$id) 1
5636    }
5637}
5638
5639proc mstime {} {
5640    global startmstime
5641    if {![info exists startmstime]} {
5642        set startmstime [clock clicks -milliseconds]
5643    }
5644    return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
5645}
5646
5647proc rowmenu {x y id} {
5648    global rowctxmenu commitrow selectedline rowmenuid curview
5649    global nullid nullid2 fakerowmenu mainhead
5650
5651    set rowmenuid $id
5652    if {![info exists selectedline]
5653        || $commitrow($curview,$id) eq $selectedline} {
5654        set state disabled
5655    } else {
5656        set state normal
5657    }
5658    if {$id ne $nullid && $id ne $nullid2} {
5659        set menu $rowctxmenu
5660        $menu entryconfigure 7 -label "Reset $mainhead branch to here"
5661    } else {
5662        set menu $fakerowmenu
5663    }
5664    $menu entryconfigure "Diff this*" -state $state
5665    $menu entryconfigure "Diff selected*" -state $state
5666    $menu entryconfigure "Make patch" -state $state
5667    tk_popup $menu $x $y
5668}
5669
5670proc diffvssel {dirn} {
5671    global rowmenuid selectedline displayorder
5672
5673    if {![info exists selectedline]} return
5674    if {$dirn} {
5675        set oldid [lindex $displayorder $selectedline]
5676        set newid $rowmenuid
5677    } else {
5678        set oldid $rowmenuid
5679        set newid [lindex $displayorder $selectedline]
5680    }
5681    addtohistory [list doseldiff $oldid $newid]
5682    doseldiff $oldid $newid
5683}
5684
5685proc doseldiff {oldid newid} {
5686    global ctext
5687    global commitinfo
5688
5689    $ctext conf -state normal
5690    clear_ctext
5691    init_flist "Top"
5692    $ctext insert end "From "
5693    $ctext tag conf link -foreground blue -underline 1
5694    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
5695    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
5696    $ctext tag bind link0 <1> [list selbyid $oldid]
5697    $ctext insert end $oldid [list link link0]
5698    $ctext insert end "\n     "
5699    $ctext insert end [lindex $commitinfo($oldid) 0]
5700    $ctext insert end "\n\nTo   "
5701    $ctext tag bind link1 <1> [list selbyid $newid]
5702    $ctext insert end $newid [list link link1]
5703    $ctext insert end "\n     "
5704    $ctext insert end [lindex $commitinfo($newid) 0]
5705    $ctext insert end "\n"
5706    $ctext conf -state disabled
5707    $ctext tag remove found 1.0 end
5708    startdiff [list $oldid $newid]
5709}
5710
5711proc mkpatch {} {
5712    global rowmenuid currentid commitinfo patchtop patchnum
5713
5714    if {![info exists currentid]} return
5715    set oldid $currentid
5716    set oldhead [lindex $commitinfo($oldid) 0]
5717    set newid $rowmenuid
5718    set newhead [lindex $commitinfo($newid) 0]
5719    set top .patch
5720    set patchtop $top
5721    catch {destroy $top}
5722    toplevel $top
5723    label $top.title -text "Generate patch"
5724    grid $top.title - -pady 10
5725    label $top.from -text "From:"
5726    entry $top.fromsha1 -width 40 -relief flat
5727    $top.fromsha1 insert 0 $oldid
5728    $top.fromsha1 conf -state readonly
5729    grid $top.from $top.fromsha1 -sticky w
5730    entry $top.fromhead -width 60 -relief flat
5731    $top.fromhead insert 0 $oldhead
5732    $top.fromhead conf -state readonly
5733    grid x $top.fromhead -sticky w
5734    label $top.to -text "To:"
5735    entry $top.tosha1 -width 40 -relief flat
5736    $top.tosha1 insert 0 $newid
5737    $top.tosha1 conf -state readonly
5738    grid $top.to $top.tosha1 -sticky w
5739    entry $top.tohead -width 60 -relief flat
5740    $top.tohead insert 0 $newhead
5741    $top.tohead conf -state readonly
5742    grid x $top.tohead -sticky w
5743    button $top.rev -text "Reverse" -command mkpatchrev -padx 5
5744    grid $top.rev x -pady 10
5745    label $top.flab -text "Output file:"
5746    entry $top.fname -width 60
5747    $top.fname insert 0 [file normalize "patch$patchnum.patch"]
5748    incr patchnum
5749    grid $top.flab $top.fname -sticky w
5750    frame $top.buts
5751    button $top.buts.gen -text "Generate" -command mkpatchgo
5752    button $top.buts.can -text "Cancel" -command mkpatchcan
5753    grid $top.buts.gen $top.buts.can
5754    grid columnconfigure $top.buts 0 -weight 1 -uniform a
5755    grid columnconfigure $top.buts 1 -weight 1 -uniform a
5756    grid $top.buts - -pady 10 -sticky ew
5757    focus $top.fname
5758}
5759
5760proc mkpatchrev {} {
5761    global patchtop
5762
5763    set oldid [$patchtop.fromsha1 get]
5764    set oldhead [$patchtop.fromhead get]
5765    set newid [$patchtop.tosha1 get]
5766    set newhead [$patchtop.tohead get]
5767    foreach e [list fromsha1 fromhead tosha1 tohead] \
5768            v [list $newid $newhead $oldid $oldhead] {
5769        $patchtop.$e conf -state normal
5770        $patchtop.$e delete 0 end
5771        $patchtop.$e insert 0 $v
5772        $patchtop.$e conf -state readonly
5773    }
5774}
5775
5776proc mkpatchgo {} {
5777    global patchtop nullid nullid2
5778
5779    set oldid [$patchtop.fromsha1 get]
5780    set newid [$patchtop.tosha1 get]
5781    set fname [$patchtop.fname get]
5782    set cmd [diffcmd [list $oldid $newid] -p]
5783    lappend cmd >$fname &
5784    if {[catch {eval exec $cmd} err]} {
5785        error_popup "Error creating patch: $err"
5786    }
5787    catch {destroy $patchtop}
5788    unset patchtop
5789}
5790
5791proc mkpatchcan {} {
5792    global patchtop
5793
5794    catch {destroy $patchtop}
5795    unset patchtop
5796}
5797
5798proc mktag {} {
5799    global rowmenuid mktagtop commitinfo
5800
5801    set top .maketag
5802    set mktagtop $top
5803    catch {destroy $top}
5804    toplevel $top
5805    label $top.title -text "Create tag"
5806    grid $top.title - -pady 10
5807    label $top.id -text "ID:"
5808    entry $top.sha1 -width 40 -relief flat
5809    $top.sha1 insert 0 $rowmenuid
5810    $top.sha1 conf -state readonly
5811    grid $top.id $top.sha1 -sticky w
5812    entry $top.head -width 60 -relief flat
5813    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
5814    $top.head conf -state readonly
5815    grid x $top.head -sticky w
5816    label $top.tlab -text "Tag name:"
5817    entry $top.tag -width 60
5818    grid $top.tlab $top.tag -sticky w
5819    frame $top.buts
5820    button $top.buts.gen -text "Create" -command mktaggo
5821    button $top.buts.can -text "Cancel" -command mktagcan
5822    grid $top.buts.gen $top.buts.can
5823    grid columnconfigure $top.buts 0 -weight 1 -uniform a
5824    grid columnconfigure $top.buts 1 -weight 1 -uniform a
5825    grid $top.buts - -pady 10 -sticky ew
5826    focus $top.tag
5827}
5828
5829proc domktag {} {
5830    global mktagtop env tagids idtags
5831
5832    set id [$mktagtop.sha1 get]
5833    set tag [$mktagtop.tag get]
5834    if {$tag == {}} {
5835        error_popup "No tag name specified"
5836        return
5837    }
5838    if {[info exists tagids($tag)]} {
5839        error_popup "Tag \"$tag\" already exists"
5840        return
5841    }
5842    if {[catch {
5843        set dir [gitdir]
5844        set fname [file join $dir "refs/tags" $tag]
5845        set f [open $fname w]
5846        puts $f $id
5847        close $f
5848    } err]} {
5849        error_popup "Error creating tag: $err"
5850        return
5851    }
5852
5853    set tagids($tag) $id
5854    lappend idtags($id) $tag
5855    redrawtags $id
5856    addedtag $id
5857}
5858
5859proc redrawtags {id} {
5860    global canv linehtag commitrow idpos selectedline curview
5861    global mainfont canvxmax iddrawn
5862
5863    if {![info exists commitrow($curview,$id)]} return
5864    if {![info exists iddrawn($id)]} return
5865    drawcommits $commitrow($curview,$id)
5866    $canv delete tag.$id
5867    set xt [eval drawtags $id $idpos($id)]
5868    $canv coords $linehtag($commitrow($curview,$id)) $xt [lindex $idpos($id) 2]
5869    set text [$canv itemcget $linehtag($commitrow($curview,$id)) -text]
5870    set xr [expr {$xt + [font measure $mainfont $text]}]
5871    if {$xr > $canvxmax} {
5872        set canvxmax $xr
5873        setcanvscroll
5874    }
5875    if {[info exists selectedline]
5876        && $selectedline == $commitrow($curview,$id)} {
5877        selectline $selectedline 0
5878    }
5879}
5880
5881proc mktagcan {} {
5882    global mktagtop
5883
5884    catch {destroy $mktagtop}
5885    unset mktagtop
5886}
5887
5888proc mktaggo {} {
5889    domktag
5890    mktagcan
5891}
5892
5893proc writecommit {} {
5894    global rowmenuid wrcomtop commitinfo wrcomcmd
5895
5896    set top .writecommit
5897    set wrcomtop $top
5898    catch {destroy $top}
5899    toplevel $top
5900    label $top.title -text "Write commit to file"
5901    grid $top.title - -pady 10
5902    label $top.id -text "ID:"
5903    entry $top.sha1 -width 40 -relief flat
5904    $top.sha1 insert 0 $rowmenuid
5905    $top.sha1 conf -state readonly
5906    grid $top.id $top.sha1 -sticky w
5907    entry $top.head -width 60 -relief flat
5908    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
5909    $top.head conf -state readonly
5910    grid x $top.head -sticky w
5911    label $top.clab -text "Command:"
5912    entry $top.cmd -width 60 -textvariable wrcomcmd
5913    grid $top.clab $top.cmd -sticky w -pady 10
5914    label $top.flab -text "Output file:"
5915    entry $top.fname -width 60
5916    $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
5917    grid $top.flab $top.fname -sticky w
5918    frame $top.buts
5919    button $top.buts.gen -text "Write" -command wrcomgo
5920    button $top.buts.can -text "Cancel" -command wrcomcan
5921    grid $top.buts.gen $top.buts.can
5922    grid columnconfigure $top.buts 0 -weight 1 -uniform a
5923    grid columnconfigure $top.buts 1 -weight 1 -uniform a
5924    grid $top.buts - -pady 10 -sticky ew
5925    focus $top.fname
5926}
5927
5928proc wrcomgo {} {
5929    global wrcomtop
5930
5931    set id [$wrcomtop.sha1 get]
5932    set cmd "echo $id | [$wrcomtop.cmd get]"
5933    set fname [$wrcomtop.fname get]
5934    if {[catch {exec sh -c $cmd >$fname &} err]} {
5935        error_popup "Error writing commit: $err"
5936    }
5937    catch {destroy $wrcomtop}
5938    unset wrcomtop
5939}
5940
5941proc wrcomcan {} {
5942    global wrcomtop
5943
5944    catch {destroy $wrcomtop}
5945    unset wrcomtop
5946}
5947
5948proc mkbranch {} {
5949    global rowmenuid mkbrtop
5950
5951    set top .makebranch
5952    catch {destroy $top}
5953    toplevel $top
5954    label $top.title -text "Create new branch"
5955    grid $top.title - -pady 10
5956    label $top.id -text "ID:"
5957    entry $top.sha1 -width 40 -relief flat
5958    $top.sha1 insert 0 $rowmenuid
5959    $top.sha1 conf -state readonly
5960    grid $top.id $top.sha1 -sticky w
5961    label $top.nlab -text "Name:"
5962    entry $top.name -width 40
5963    grid $top.nlab $top.name -sticky w
5964    frame $top.buts
5965    button $top.buts.go -text "Create" -command [list mkbrgo $top]
5966    button $top.buts.can -text "Cancel" -command "catch {destroy $top}"
5967    grid $top.buts.go $top.buts.can
5968    grid columnconfigure $top.buts 0 -weight 1 -uniform a
5969    grid columnconfigure $top.buts 1 -weight 1 -uniform a
5970    grid $top.buts - -pady 10 -sticky ew
5971    focus $top.name
5972}
5973
5974proc mkbrgo {top} {
5975    global headids idheads
5976
5977    set name [$top.name get]
5978    set id [$top.sha1 get]
5979    if {$name eq {}} {
5980        error_popup "Please specify a name for the new branch"
5981        return
5982    }
5983    catch {destroy $top}
5984    nowbusy newbranch
5985    update
5986    if {[catch {
5987        exec git branch $name $id
5988    } err]} {
5989        notbusy newbranch
5990        error_popup $err
5991    } else {
5992        set headids($name) $id
5993        lappend idheads($id) $name
5994        addedhead $id $name
5995        notbusy newbranch
5996        redrawtags $id
5997        dispneartags 0
5998    }
5999}
6000
6001proc cherrypick {} {
6002    global rowmenuid curview commitrow
6003    global mainhead
6004
6005    set oldhead [exec git rev-parse HEAD]
6006    set dheads [descheads $rowmenuid]
6007    if {$dheads ne {} && [lsearch -exact $dheads $oldhead] >= 0} {
6008        set ok [confirm_popup "Commit [string range $rowmenuid 0 7] is already\
6009                        included in branch $mainhead -- really re-apply it?"]
6010        if {!$ok} return
6011    }
6012    nowbusy cherrypick
6013    update
6014    # Unfortunately git-cherry-pick writes stuff to stderr even when
6015    # no error occurs, and exec takes that as an indication of error...
6016    if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} {
6017        notbusy cherrypick
6018        error_popup $err
6019        return
6020    }
6021    set newhead [exec git rev-parse HEAD]
6022    if {$newhead eq $oldhead} {
6023        notbusy cherrypick
6024        error_popup "No changes committed"
6025        return
6026    }
6027    addnewchild $newhead $oldhead
6028    if {[info exists commitrow($curview,$oldhead)]} {
6029        insertrow $commitrow($curview,$oldhead) $newhead
6030        if {$mainhead ne {}} {
6031            movehead $newhead $mainhead
6032            movedhead $newhead $mainhead
6033        }
6034        redrawtags $oldhead
6035        redrawtags $newhead
6036    }
6037    notbusy cherrypick
6038}
6039
6040proc resethead {} {
6041    global mainheadid mainhead rowmenuid confirm_ok resettype
6042    global showlocalchanges
6043
6044    set confirm_ok 0
6045    set w ".confirmreset"
6046    toplevel $w
6047    wm transient $w .
6048    wm title $w "Confirm reset"
6049    message $w.m -text \
6050        "Reset branch $mainhead to [string range $rowmenuid 0 7]?" \
6051        -justify center -aspect 1000
6052    pack $w.m -side top -fill x -padx 20 -pady 20
6053    frame $w.f -relief sunken -border 2
6054    message $w.f.rt -text "Reset type:" -aspect 1000
6055    grid $w.f.rt -sticky w
6056    set resettype mixed
6057    radiobutton $w.f.soft -value soft -variable resettype -justify left \
6058        -text "Soft: Leave working tree and index untouched"
6059    grid $w.f.soft -sticky w
6060    radiobutton $w.f.mixed -value mixed -variable resettype -justify left \
6061        -text "Mixed: Leave working tree untouched, reset index"
6062    grid $w.f.mixed -sticky w
6063    radiobutton $w.f.hard -value hard -variable resettype -justify left \
6064        -text "Hard: Reset working tree and index\n(discard ALL local changes)"
6065    grid $w.f.hard -sticky w
6066    pack $w.f -side top -fill x
6067    button $w.ok -text OK -command "set confirm_ok 1; destroy $w"
6068    pack $w.ok -side left -fill x -padx 20 -pady 20
6069    button $w.cancel -text Cancel -command "destroy $w"
6070    pack $w.cancel -side right -fill x -padx 20 -pady 20
6071    bind $w <Visibility> "grab $w; focus $w"
6072    tkwait window $w
6073    if {!$confirm_ok} return
6074    if {[catch {set fd [open \
6075            [list | sh -c "git reset --$resettype $rowmenuid 2>&1"] r]} err]} {
6076        error_popup $err
6077    } else {
6078        dohidelocalchanges
6079        set w ".resetprogress"
6080        filerun $fd [list readresetstat $fd $w]
6081        toplevel $w
6082        wm transient $w
6083        wm title $w "Reset progress"
6084        message $w.m -text "Reset in progress, please wait..." \
6085            -justify center -aspect 1000
6086        pack $w.m -side top -fill x -padx 20 -pady 5
6087        canvas $w.c -width 150 -height 20 -bg white
6088        $w.c create rect 0 0 0 20 -fill green -tags rect
6089        pack $w.c -side top -fill x -padx 20 -pady 5 -expand 1
6090        nowbusy reset
6091    }
6092}
6093
6094proc readresetstat {fd w} {
6095    global mainhead mainheadid showlocalchanges
6096
6097    if {[gets $fd line] >= 0} {
6098        if {[regexp {([0-9]+)% \(([0-9]+)/([0-9]+)\)} $line match p m n]} {
6099            set x [expr {($m * 150) / $n}]
6100            $w.c coords rect 0 0 $x 20
6101        }
6102        return 1
6103    }
6104    destroy $w
6105    notbusy reset
6106    if {[catch {close $fd} err]} {
6107        error_popup $err
6108    }
6109    set oldhead $mainheadid
6110    set newhead [exec git rev-parse HEAD]
6111    if {$newhead ne $oldhead} {
6112        movehead $newhead $mainhead
6113        movedhead $newhead $mainhead
6114        set mainheadid $newhead
6115        redrawtags $oldhead
6116        redrawtags $newhead
6117    }
6118    if {$showlocalchanges} {
6119        doshowlocalchanges
6120    }
6121    return 0
6122}
6123
6124# context menu for a head
6125proc headmenu {x y id head} {
6126    global headmenuid headmenuhead headctxmenu mainhead
6127
6128    set headmenuid $id
6129    set headmenuhead $head
6130    set state normal
6131    if {$head eq $mainhead} {
6132        set state disabled
6133    }
6134    $headctxmenu entryconfigure 0 -state $state
6135    $headctxmenu entryconfigure 1 -state $state
6136    tk_popup $headctxmenu $x $y
6137}
6138
6139proc cobranch {} {
6140    global headmenuid headmenuhead mainhead headids
6141    global showlocalchanges mainheadid
6142
6143    # check the tree is clean first??
6144    set oldmainhead $mainhead
6145    nowbusy checkout
6146    update
6147    dohidelocalchanges
6148    if {[catch {
6149        exec git checkout -q $headmenuhead
6150    } err]} {
6151        notbusy checkout
6152        error_popup $err
6153    } else {
6154        notbusy checkout
6155        set mainhead $headmenuhead
6156        set mainheadid $headmenuid
6157        if {[info exists headids($oldmainhead)]} {
6158            redrawtags $headids($oldmainhead)
6159        }
6160        redrawtags $headmenuid
6161    }
6162    if {$showlocalchanges} {
6163        dodiffindex
6164    }
6165}
6166
6167proc rmbranch {} {
6168    global headmenuid headmenuhead mainhead
6169    global headids idheads
6170
6171    set head $headmenuhead
6172    set id $headmenuid
6173    # this check shouldn't be needed any more...
6174    if {$head eq $mainhead} {
6175        error_popup "Cannot delete the currently checked-out branch"
6176        return
6177    }
6178    set dheads [descheads $id]
6179    if {$dheads eq $headids($head)} {
6180        # the stuff on this branch isn't on any other branch
6181        if {![confirm_popup "The commits on branch $head aren't on any other\
6182                        branch.\nReally delete branch $head?"]} return
6183    }
6184    nowbusy rmbranch
6185    update
6186    if {[catch {exec git branch -D $head} err]} {
6187        notbusy rmbranch
6188        error_popup $err
6189        return
6190    }
6191    removehead $id $head
6192    removedhead $id $head
6193    redrawtags $id
6194    notbusy rmbranch
6195    dispneartags 0
6196}
6197
6198# Stuff for finding nearby tags
6199proc getallcommits {} {
6200    global allcommits allids nbmp nextarc seeds
6201
6202    set allids {}
6203    set nbmp 0
6204    set nextarc 0
6205    set allcommits 0
6206    set seeds {}
6207    regetallcommits
6208}
6209
6210# Called when the graph might have changed
6211proc regetallcommits {} {
6212    global allcommits seeds
6213
6214    set cmd [concat | git rev-list --all --parents]
6215    foreach id $seeds {
6216        lappend cmd "^$id"
6217    }
6218    set fd [open $cmd r]
6219    fconfigure $fd -blocking 0
6220    incr allcommits
6221    nowbusy allcommits
6222    filerun $fd [list getallclines $fd]
6223}
6224
6225# Since most commits have 1 parent and 1 child, we group strings of
6226# such commits into "arcs" joining branch/merge points (BMPs), which
6227# are commits that either don't have 1 parent or don't have 1 child.
6228#
6229# arcnos(id) - incoming arcs for BMP, arc we're on for other nodes
6230# arcout(id) - outgoing arcs for BMP
6231# arcids(a) - list of IDs on arc including end but not start
6232# arcstart(a) - BMP ID at start of arc
6233# arcend(a) - BMP ID at end of arc
6234# growing(a) - arc a is still growing
6235# arctags(a) - IDs out of arcids (excluding end) that have tags
6236# archeads(a) - IDs out of arcids (excluding end) that have heads
6237# The start of an arc is at the descendent end, so "incoming" means
6238# coming from descendents, and "outgoing" means going towards ancestors.
6239
6240proc getallclines {fd} {
6241    global allids allparents allchildren idtags idheads nextarc nbmp
6242    global arcnos arcids arctags arcout arcend arcstart archeads growing
6243    global seeds allcommits
6244
6245    set nid 0
6246    while {[incr nid] <= 1000 && [gets $fd line] >= 0} {
6247        set id [lindex $line 0]
6248        if {[info exists allparents($id)]} {
6249            # seen it already
6250            continue
6251        }
6252        lappend allids $id
6253        set olds [lrange $line 1 end]
6254        set allparents($id) $olds
6255        if {![info exists allchildren($id)]} {
6256            set allchildren($id) {}
6257            set arcnos($id) {}
6258            lappend seeds $id
6259        } else {
6260            set a $arcnos($id)
6261            if {[llength $olds] == 1 && [llength $a] == 1} {
6262                lappend arcids($a) $id
6263                if {[info exists idtags($id)]} {
6264                    lappend arctags($a) $id
6265                }
6266                if {[info exists idheads($id)]} {
6267                    lappend archeads($a) $id
6268                }
6269                if {[info exists allparents($olds)]} {
6270                    # seen parent already
6271                    if {![info exists arcout($olds)]} {
6272                        splitarc $olds
6273                    }
6274                    lappend arcids($a) $olds
6275                    set arcend($a) $olds
6276                    unset growing($a)
6277                }
6278                lappend allchildren($olds) $id
6279                lappend arcnos($olds) $a
6280                continue
6281            }
6282        }
6283        incr nbmp
6284        foreach a $arcnos($id) {
6285            lappend arcids($a) $id
6286            set arcend($a) $id
6287            unset growing($a)
6288        }
6289
6290        set ao {}
6291        foreach p $olds {
6292            lappend allchildren($p) $id
6293            set a [incr nextarc]
6294            set arcstart($a) $id
6295            set archeads($a) {}
6296            set arctags($a) {}
6297            set archeads($a) {}
6298            set arcids($a) {}
6299            lappend ao $a
6300            set growing($a) 1
6301            if {[info exists allparents($p)]} {
6302                # seen it already, may need to make a new branch
6303                if {![info exists arcout($p)]} {
6304                    splitarc $p
6305                }
6306                lappend arcids($a) $p
6307                set arcend($a) $p
6308                unset growing($a)
6309            }
6310            lappend arcnos($p) $a
6311        }
6312        set arcout($id) $ao
6313    }
6314    if {$nid > 0} {
6315        global cached_dheads cached_dtags cached_atags
6316        catch {unset cached_dheads}
6317        catch {unset cached_dtags}
6318        catch {unset cached_atags}
6319    }
6320    if {![eof $fd]} {
6321        return [expr {$nid >= 1000? 2: 1}]
6322    }
6323    close $fd
6324    if {[incr allcommits -1] == 0} {
6325        notbusy allcommits
6326    }
6327    dispneartags 0
6328    return 0
6329}
6330
6331proc recalcarc {a} {
6332    global arctags archeads arcids idtags idheads
6333
6334    set at {}
6335    set ah {}
6336    foreach id [lrange $arcids($a) 0 end-1] {
6337        if {[info exists idtags($id)]} {
6338            lappend at $id
6339        }
6340        if {[info exists idheads($id)]} {
6341            lappend ah $id
6342        }
6343    }
6344    set arctags($a) $at
6345    set archeads($a) $ah
6346}
6347
6348proc splitarc {p} {
6349    global arcnos arcids nextarc nbmp arctags archeads idtags idheads
6350    global arcstart arcend arcout allparents growing
6351
6352    set a $arcnos($p)
6353    if {[llength $a] != 1} {
6354        puts "oops splitarc called but [llength $a] arcs already"
6355        return
6356    }
6357    set a [lindex $a 0]
6358    set i [lsearch -exact $arcids($a) $p]
6359    if {$i < 0} {
6360        puts "oops splitarc $p not in arc $a"
6361        return
6362    }
6363    set na [incr nextarc]
6364    if {[info exists arcend($a)]} {
6365        set arcend($na) $arcend($a)
6366    } else {
6367        set l [lindex $allparents([lindex $arcids($a) end]) 0]
6368        set j [lsearch -exact $arcnos($l) $a]
6369        set arcnos($l) [lreplace $arcnos($l) $j $j $na]
6370    }
6371    set tail [lrange $arcids($a) [expr {$i+1}] end]
6372    set arcids($a) [lrange $arcids($a) 0 $i]
6373    set arcend($a) $p
6374    set arcstart($na) $p
6375    set arcout($p) $na
6376    set arcids($na) $tail
6377    if {[info exists growing($a)]} {
6378        set growing($na) 1
6379        unset growing($a)
6380    }
6381    incr nbmp
6382
6383    foreach id $tail {
6384        if {[llength $arcnos($id)] == 1} {
6385            set arcnos($id) $na
6386        } else {
6387            set j [lsearch -exact $arcnos($id) $a]
6388            set arcnos($id) [lreplace $arcnos($id) $j $j $na]
6389        }
6390    }
6391
6392    # reconstruct tags and heads lists
6393    if {$arctags($a) ne {} || $archeads($a) ne {}} {
6394        recalcarc $a
6395        recalcarc $na
6396    } else {
6397        set arctags($na) {}
6398        set archeads($na) {}
6399    }
6400}
6401
6402# Update things for a new commit added that is a child of one
6403# existing commit.  Used when cherry-picking.
6404proc addnewchild {id p} {
6405    global allids allparents allchildren idtags nextarc nbmp
6406    global arcnos arcids arctags arcout arcend arcstart archeads growing
6407    global seeds
6408
6409    lappend allids $id
6410    set allparents($id) [list $p]
6411    set allchildren($id) {}
6412    set arcnos($id) {}
6413    lappend seeds $id
6414    incr nbmp
6415    lappend allchildren($p) $id
6416    set a [incr nextarc]
6417    set arcstart($a) $id
6418    set archeads($a) {}
6419    set arctags($a) {}
6420    set arcids($a) [list $p]
6421    set arcend($a) $p
6422    if {![info exists arcout($p)]} {
6423        splitarc $p
6424    }
6425    lappend arcnos($p) $a
6426    set arcout($id) [list $a]
6427}
6428
6429# Returns 1 if a is an ancestor of b, -1 if b is an ancestor of a,
6430# or 0 if neither is true.
6431proc anc_or_desc {a b} {
6432    global arcout arcstart arcend arcnos cached_isanc
6433
6434    if {$arcnos($a) eq $arcnos($b)} {
6435        # Both are on the same arc(s); either both are the same BMP,
6436        # or if one is not a BMP, the other is also not a BMP or is
6437        # the BMP at end of the arc (and it only has 1 incoming arc).
6438        # Or both can be BMPs with no incoming arcs.
6439        if {$a eq $b || $arcnos($a) eq {}} {
6440            return 0
6441        }
6442        # assert {[llength $arcnos($a)] == 1}
6443        set arc [lindex $arcnos($a) 0]
6444        set i [lsearch -exact $arcids($arc) $a]
6445        set j [lsearch -exact $arcids($arc) $b]
6446        if {$i < 0 || $i > $j} {
6447            return 1
6448        } else {
6449            return -1
6450        }
6451    }
6452
6453    if {![info exists arcout($a)]} {
6454        set arc [lindex $arcnos($a) 0]
6455        if {[info exists arcend($arc)]} {
6456            set aend $arcend($arc)
6457        } else {
6458            set aend {}
6459        }
6460        set a $arcstart($arc)
6461    } else {
6462        set aend $a
6463    }
6464    if {![info exists arcout($b)]} {
6465        set arc [lindex $arcnos($b) 0]
6466        if {[info exists arcend($arc)]} {
6467            set bend $arcend($arc)
6468        } else {
6469            set bend {}
6470        }
6471        set b $arcstart($arc)
6472    } else {
6473        set bend $b
6474    }
6475    if {$a eq $bend} {
6476        return 1
6477    }
6478    if {$b eq $aend} {
6479        return -1
6480    }
6481    if {[info exists cached_isanc($a,$bend)]} {
6482        if {$cached_isanc($a,$bend)} {
6483            return 1
6484        }
6485    }
6486    if {[info exists cached_isanc($b,$aend)]} {
6487        if {$cached_isanc($b,$aend)} {
6488            return -1
6489        }
6490        if {[info exists cached_isanc($a,$bend)]} {
6491            return 0
6492        }
6493    }
6494
6495    set todo [list $a $b]
6496    set anc($a) a
6497    set anc($b) b
6498    for {set i 0} {$i < [llength $todo]} {incr i} {
6499        set x [lindex $todo $i]
6500        if {$anc($x) eq {}} {
6501            continue
6502        }
6503        foreach arc $arcnos($x) {
6504            set xd $arcstart($arc)
6505            if {$xd eq $bend} {
6506                set cached_isanc($a,$bend) 1
6507                set cached_isanc($b,$aend) 0
6508                return 1
6509            } elseif {$xd eq $aend} {
6510                set cached_isanc($b,$aend) 1
6511                set cached_isanc($a,$bend) 0
6512                return -1
6513            }
6514            if {![info exists anc($xd)]} {
6515                set anc($xd) $anc($x)
6516                lappend todo $xd
6517            } elseif {$anc($xd) ne $anc($x)} {
6518                set anc($xd) {}
6519            }
6520        }
6521    }
6522    set cached_isanc($a,$bend) 0
6523    set cached_isanc($b,$aend) 0
6524    return 0
6525}
6526
6527# This identifies whether $desc has an ancestor that is
6528# a growing tip of the graph and which is not an ancestor of $anc
6529# and returns 0 if so and 1 if not.
6530# If we subsequently discover a tag on such a growing tip, and that
6531# turns out to be a descendent of $anc (which it could, since we
6532# don't necessarily see children before parents), then $desc
6533# isn't a good choice to display as a descendent tag of
6534# $anc (since it is the descendent of another tag which is
6535# a descendent of $anc).  Similarly, $anc isn't a good choice to
6536# display as a ancestor tag of $desc.
6537#
6538proc is_certain {desc anc} {
6539    global arcnos arcout arcstart arcend growing problems
6540
6541    set certain {}
6542    if {[llength $arcnos($anc)] == 1} {
6543        # tags on the same arc are certain
6544        if {$arcnos($desc) eq $arcnos($anc)} {
6545            return 1
6546        }
6547        if {![info exists arcout($anc)]} {
6548            # if $anc is partway along an arc, use the start of the arc instead
6549            set a [lindex $arcnos($anc) 0]
6550            set anc $arcstart($a)
6551        }
6552    }
6553    if {[llength $arcnos($desc)] > 1 || [info exists arcout($desc)]} {
6554        set x $desc
6555    } else {
6556        set a [lindex $arcnos($desc) 0]
6557        set x $arcend($a)
6558    }
6559    if {$x == $anc} {
6560        return 1
6561    }
6562    set anclist [list $x]
6563    set dl($x) 1
6564    set nnh 1
6565    set ngrowanc 0
6566    for {set i 0} {$i < [llength $anclist] && ($nnh > 0 || $ngrowanc > 0)} {incr i} {
6567        set x [lindex $anclist $i]
6568        if {$dl($x)} {
6569            incr nnh -1
6570        }
6571        set done($x) 1
6572        foreach a $arcout($x) {
6573            if {[info exists growing($a)]} {
6574                if {![info exists growanc($x)] && $dl($x)} {
6575                    set growanc($x) 1
6576                    incr ngrowanc
6577                }
6578            } else {
6579                set y $arcend($a)
6580                if {[info exists dl($y)]} {
6581                    if {$dl($y)} {
6582                        if {!$dl($x)} {
6583                            set dl($y) 0
6584                            if {![info exists done($y)]} {
6585                                incr nnh -1
6586                            }
6587                            if {[info exists growanc($x)]} {
6588                                incr ngrowanc -1
6589                            }
6590                            set xl [list $y]
6591                            for {set k 0} {$k < [llength $xl]} {incr k} {
6592                                set z [lindex $xl $k]
6593                                foreach c $arcout($z) {
6594                                    if {[info exists arcend($c)]} {
6595                                        set v $arcend($c)
6596                                        if {[info exists dl($v)] && $dl($v)} {
6597                                            set dl($v) 0
6598                                            if {![info exists done($v)]} {
6599                                                incr nnh -1
6600                                            }
6601                                            if {[info exists growanc($v)]} {
6602                                                incr ngrowanc -1
6603                                            }
6604                                            lappend xl $v
6605                                        }
6606                                    }
6607                                }
6608                            }
6609                        }
6610                    }
6611                } elseif {$y eq $anc || !$dl($x)} {
6612                    set dl($y) 0
6613                    lappend anclist $y
6614                } else {
6615                    set dl($y) 1
6616                    lappend anclist $y
6617                    incr nnh
6618                }
6619            }
6620        }
6621    }
6622    foreach x [array names growanc] {
6623        if {$dl($x)} {
6624            return 0
6625        }
6626        return 0
6627    }
6628    return 1
6629}
6630
6631proc validate_arctags {a} {
6632    global arctags idtags
6633
6634    set i -1
6635    set na $arctags($a)
6636    foreach id $arctags($a) {
6637        incr i
6638        if {![info exists idtags($id)]} {
6639            set na [lreplace $na $i $i]
6640            incr i -1
6641        }
6642    }
6643    set arctags($a) $na
6644}
6645
6646proc validate_archeads {a} {
6647    global archeads idheads
6648
6649    set i -1
6650    set na $archeads($a)
6651    foreach id $archeads($a) {
6652        incr i
6653        if {![info exists idheads($id)]} {
6654            set na [lreplace $na $i $i]
6655            incr i -1
6656        }
6657    }
6658    set archeads($a) $na
6659}
6660
6661# Return the list of IDs that have tags that are descendents of id,
6662# ignoring IDs that are descendents of IDs already reported.
6663proc desctags {id} {
6664    global arcnos arcstart arcids arctags idtags allparents
6665    global growing cached_dtags
6666
6667    if {![info exists allparents($id)]} {
6668        return {}
6669    }
6670    set t1 [clock clicks -milliseconds]
6671    set argid $id
6672    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
6673        # part-way along an arc; check that arc first
6674        set a [lindex $arcnos($id) 0]
6675        if {$arctags($a) ne {}} {
6676            validate_arctags $a
6677            set i [lsearch -exact $arcids($a) $id]
6678            set tid {}
6679            foreach t $arctags($a) {
6680                set j [lsearch -exact $arcids($a) $t]
6681                if {$j >= $i} break
6682                set tid $t
6683            }
6684            if {$tid ne {}} {
6685                return $tid
6686            }
6687        }
6688        set id $arcstart($a)
6689        if {[info exists idtags($id)]} {
6690            return $id
6691        }
6692    }
6693    if {[info exists cached_dtags($id)]} {
6694        return $cached_dtags($id)
6695    }
6696
6697    set origid $id
6698    set todo [list $id]
6699    set queued($id) 1
6700    set nc 1
6701    for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
6702        set id [lindex $todo $i]
6703        set done($id) 1
6704        set ta [info exists hastaggedancestor($id)]
6705        if {!$ta} {
6706            incr nc -1
6707        }
6708        # ignore tags on starting node
6709        if {!$ta && $i > 0} {
6710            if {[info exists idtags($id)]} {
6711                set tagloc($id) $id
6712                set ta 1
6713            } elseif {[info exists cached_dtags($id)]} {
6714                set tagloc($id) $cached_dtags($id)
6715                set ta 1
6716            }
6717        }
6718        foreach a $arcnos($id) {
6719            set d $arcstart($a)
6720            if {!$ta && $arctags($a) ne {}} {
6721                validate_arctags $a
6722                if {$arctags($a) ne {}} {
6723                    lappend tagloc($id) [lindex $arctags($a) end]
6724                }
6725            }
6726            if {$ta || $arctags($a) ne {}} {
6727                set tomark [list $d]
6728                for {set j 0} {$j < [llength $tomark]} {incr j} {
6729                    set dd [lindex $tomark $j]
6730                    if {![info exists hastaggedancestor($dd)]} {
6731                        if {[info exists done($dd)]} {
6732                            foreach b $arcnos($dd) {
6733                                lappend tomark $arcstart($b)
6734                            }
6735                            if {[info exists tagloc($dd)]} {
6736                                unset tagloc($dd)
6737                            }
6738                        } elseif {[info exists queued($dd)]} {
6739                            incr nc -1
6740                        }
6741                        set hastaggedancestor($dd) 1
6742                    }
6743                }
6744            }
6745            if {![info exists queued($d)]} {
6746                lappend todo $d
6747                set queued($d) 1
6748                if {![info exists hastaggedancestor($d)]} {
6749                    incr nc
6750                }
6751            }
6752        }
6753    }
6754    set tags {}
6755    foreach id [array names tagloc] {
6756        if {![info exists hastaggedancestor($id)]} {
6757            foreach t $tagloc($id) {
6758                if {[lsearch -exact $tags $t] < 0} {
6759                    lappend tags $t
6760                }
6761            }
6762        }
6763    }
6764    set t2 [clock clicks -milliseconds]
6765    set loopix $i
6766
6767    # remove tags that are descendents of other tags
6768    for {set i 0} {$i < [llength $tags]} {incr i} {
6769        set a [lindex $tags $i]
6770        for {set j 0} {$j < $i} {incr j} {
6771            set b [lindex $tags $j]
6772            set r [anc_or_desc $a $b]
6773            if {$r == 1} {
6774                set tags [lreplace $tags $j $j]
6775                incr j -1
6776                incr i -1
6777            } elseif {$r == -1} {
6778                set tags [lreplace $tags $i $i]
6779                incr i -1
6780                break
6781            }
6782        }
6783    }
6784
6785    if {[array names growing] ne {}} {
6786        # graph isn't finished, need to check if any tag could get
6787        # eclipsed by another tag coming later.  Simply ignore any
6788        # tags that could later get eclipsed.
6789        set ctags {}
6790        foreach t $tags {
6791            if {[is_certain $t $origid]} {
6792                lappend ctags $t
6793            }
6794        }
6795        if {$tags eq $ctags} {
6796            set cached_dtags($origid) $tags
6797        } else {
6798            set tags $ctags
6799        }
6800    } else {
6801        set cached_dtags($origid) $tags
6802    }
6803    set t3 [clock clicks -milliseconds]
6804    if {0 && $t3 - $t1 >= 100} {
6805        puts "iterating descendents ($loopix/[llength $todo] nodes) took\
6806            [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
6807    }
6808    return $tags
6809}
6810
6811proc anctags {id} {
6812    global arcnos arcids arcout arcend arctags idtags allparents
6813    global growing cached_atags
6814
6815    if {![info exists allparents($id)]} {
6816        return {}
6817    }
6818    set t1 [clock clicks -milliseconds]
6819    set argid $id
6820    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
6821        # part-way along an arc; check that arc first
6822        set a [lindex $arcnos($id) 0]
6823        if {$arctags($a) ne {}} {
6824            validate_arctags $a
6825            set i [lsearch -exact $arcids($a) $id]
6826            foreach t $arctags($a) {
6827                set j [lsearch -exact $arcids($a) $t]
6828                if {$j > $i} {
6829                    return $t
6830                }
6831            }
6832        }
6833        if {![info exists arcend($a)]} {
6834            return {}
6835        }
6836        set id $arcend($a)
6837        if {[info exists idtags($id)]} {
6838            return $id
6839        }
6840    }
6841    if {[info exists cached_atags($id)]} {
6842        return $cached_atags($id)
6843    }
6844
6845    set origid $id
6846    set todo [list $id]
6847    set queued($id) 1
6848    set taglist {}
6849    set nc 1
6850    for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
6851        set id [lindex $todo $i]
6852        set done($id) 1
6853        set td [info exists hastaggeddescendent($id)]
6854        if {!$td} {
6855            incr nc -1
6856        }
6857        # ignore tags on starting node
6858        if {!$td && $i > 0} {
6859            if {[info exists idtags($id)]} {
6860                set tagloc($id) $id
6861                set td 1
6862            } elseif {[info exists cached_atags($id)]} {
6863                set tagloc($id) $cached_atags($id)
6864                set td 1
6865            }
6866        }
6867        foreach a $arcout($id) {
6868            if {!$td && $arctags($a) ne {}} {
6869                validate_arctags $a
6870                if {$arctags($a) ne {}} {
6871                    lappend tagloc($id) [lindex $arctags($a) 0]
6872                }
6873            }
6874            if {![info exists arcend($a)]} continue
6875            set d $arcend($a)
6876            if {$td || $arctags($a) ne {}} {
6877                set tomark [list $d]
6878                for {set j 0} {$j < [llength $tomark]} {incr j} {
6879                    set dd [lindex $tomark $j]
6880                    if {![info exists hastaggeddescendent($dd)]} {
6881                        if {[info exists done($dd)]} {
6882                            foreach b $arcout($dd) {
6883                                if {[info exists arcend($b)]} {
6884                                    lappend tomark $arcend($b)
6885                                }
6886                            }
6887                            if {[info exists tagloc($dd)]} {
6888                                unset tagloc($dd)
6889                            }
6890                        } elseif {[info exists queued($dd)]} {
6891                            incr nc -1
6892                        }
6893                        set hastaggeddescendent($dd) 1
6894                    }
6895                }
6896            }
6897            if {![info exists queued($d)]} {
6898                lappend todo $d
6899                set queued($d) 1
6900                if {![info exists hastaggeddescendent($d)]} {
6901                    incr nc
6902                }
6903            }
6904        }
6905    }
6906    set t2 [clock clicks -milliseconds]
6907    set loopix $i
6908    set tags {}
6909    foreach id [array names tagloc] {
6910        if {![info exists hastaggeddescendent($id)]} {
6911            foreach t $tagloc($id) {
6912                if {[lsearch -exact $tags $t] < 0} {
6913                    lappend tags $t
6914                }
6915            }
6916        }
6917    }
6918
6919    # remove tags that are ancestors of other tags
6920    for {set i 0} {$i < [llength $tags]} {incr i} {
6921        set a [lindex $tags $i]
6922        for {set j 0} {$j < $i} {incr j} {
6923            set b [lindex $tags $j]
6924            set r [anc_or_desc $a $b]
6925            if {$r == -1} {
6926                set tags [lreplace $tags $j $j]
6927                incr j -1
6928                incr i -1
6929            } elseif {$r == 1} {
6930                set tags [lreplace $tags $i $i]
6931                incr i -1
6932                break
6933            }
6934        }
6935    }
6936
6937    if {[array names growing] ne {}} {
6938        # graph isn't finished, need to check if any tag could get
6939        # eclipsed by another tag coming later.  Simply ignore any
6940        # tags that could later get eclipsed.
6941        set ctags {}
6942        foreach t $tags {
6943            if {[is_certain $origid $t]} {
6944                lappend ctags $t
6945            }
6946        }
6947        if {$tags eq $ctags} {
6948            set cached_atags($origid) $tags
6949        } else {
6950            set tags $ctags
6951        }
6952    } else {
6953        set cached_atags($origid) $tags
6954    }
6955    set t3 [clock clicks -milliseconds]
6956    if {0 && $t3 - $t1 >= 100} {
6957        puts "iterating ancestors ($loopix/[llength $todo] nodes) took\
6958            [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
6959    }
6960    return $tags
6961}
6962
6963# Return the list of IDs that have heads that are descendents of id,
6964# including id itself if it has a head.
6965proc descheads {id} {
6966    global arcnos arcstart arcids archeads idheads cached_dheads
6967    global allparents
6968
6969    if {![info exists allparents($id)]} {
6970        return {}
6971    }
6972    set aret {}
6973    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
6974        # part-way along an arc; check it first
6975        set a [lindex $arcnos($id) 0]
6976        if {$archeads($a) ne {}} {
6977            validate_archeads $a
6978            set i [lsearch -exact $arcids($a) $id]
6979            foreach t $archeads($a) {
6980                set j [lsearch -exact $arcids($a) $t]
6981                if {$j > $i} break
6982                lappend aret $t
6983            }
6984        }
6985        set id $arcstart($a)
6986    }
6987    set origid $id
6988    set todo [list $id]
6989    set seen($id) 1
6990    set ret {}
6991    for {set i 0} {$i < [llength $todo]} {incr i} {
6992        set id [lindex $todo $i]
6993        if {[info exists cached_dheads($id)]} {
6994            set ret [concat $ret $cached_dheads($id)]
6995        } else {
6996            if {[info exists idheads($id)]} {
6997                lappend ret $id
6998            }
6999            foreach a $arcnos($id) {
7000                if {$archeads($a) ne {}} {
7001                    validate_archeads $a
7002                    if {$archeads($a) ne {}} {
7003                        set ret [concat $ret $archeads($a)]
7004                    }
7005                }
7006                set d $arcstart($a)
7007                if {![info exists seen($d)]} {
7008                    lappend todo $d
7009                    set seen($d) 1
7010                }
7011            }
7012        }
7013    }
7014    set ret [lsort -unique $ret]
7015    set cached_dheads($origid) $ret
7016    return [concat $ret $aret]
7017}
7018
7019proc addedtag {id} {
7020    global arcnos arcout cached_dtags cached_atags
7021
7022    if {![info exists arcnos($id)]} return
7023    if {![info exists arcout($id)]} {
7024        recalcarc [lindex $arcnos($id) 0]
7025    }
7026    catch {unset cached_dtags}
7027    catch {unset cached_atags}
7028}
7029
7030proc addedhead {hid head} {
7031    global arcnos arcout cached_dheads
7032
7033    if {![info exists arcnos($hid)]} return
7034    if {![info exists arcout($hid)]} {
7035        recalcarc [lindex $arcnos($hid) 0]
7036    }
7037    catch {unset cached_dheads}
7038}
7039
7040proc removedhead {hid head} {
7041    global cached_dheads
7042
7043    catch {unset cached_dheads}
7044}
7045
7046proc movedhead {hid head} {
7047    global arcnos arcout cached_dheads
7048
7049    if {![info exists arcnos($hid)]} return
7050    if {![info exists arcout($hid)]} {
7051        recalcarc [lindex $arcnos($hid) 0]
7052    }
7053    catch {unset cached_dheads}
7054}
7055
7056proc changedrefs {} {
7057    global cached_dheads cached_dtags cached_atags
7058    global arctags archeads arcnos arcout idheads idtags
7059
7060    foreach id [concat [array names idheads] [array names idtags]] {
7061        if {[info exists arcnos($id)] && ![info exists arcout($id)]} {
7062            set a [lindex $arcnos($id) 0]
7063            if {![info exists donearc($a)]} {
7064                recalcarc $a
7065                set donearc($a) 1
7066            }
7067        }
7068    }
7069    catch {unset cached_dtags}
7070    catch {unset cached_atags}
7071    catch {unset cached_dheads}
7072}
7073
7074proc rereadrefs {} {
7075    global idtags idheads idotherrefs mainhead
7076
7077    set refids [concat [array names idtags] \
7078                    [array names idheads] [array names idotherrefs]]
7079    foreach id $refids {
7080        if {![info exists ref($id)]} {
7081            set ref($id) [listrefs $id]
7082        }
7083    }
7084    set oldmainhead $mainhead
7085    readrefs
7086    changedrefs
7087    set refids [lsort -unique [concat $refids [array names idtags] \
7088                        [array names idheads] [array names idotherrefs]]]
7089    foreach id $refids {
7090        set v [listrefs $id]
7091        if {![info exists ref($id)] || $ref($id) != $v ||
7092            ($id eq $oldmainhead && $id ne $mainhead) ||
7093            ($id eq $mainhead && $id ne $oldmainhead)} {
7094            redrawtags $id
7095        }
7096    }
7097}
7098
7099proc listrefs {id} {
7100    global idtags idheads idotherrefs
7101
7102    set x {}
7103    if {[info exists idtags($id)]} {
7104        set x $idtags($id)
7105    }
7106    set y {}
7107    if {[info exists idheads($id)]} {
7108        set y $idheads($id)
7109    }
7110    set z {}
7111    if {[info exists idotherrefs($id)]} {
7112        set z $idotherrefs($id)
7113    }
7114    return [list $x $y $z]
7115}
7116
7117proc showtag {tag isnew} {
7118    global ctext tagcontents tagids linknum tagobjid
7119
7120    if {$isnew} {
7121        addtohistory [list showtag $tag 0]
7122    }
7123    $ctext conf -state normal
7124    clear_ctext
7125    set linknum 0
7126    if {![info exists tagcontents($tag)]} {
7127        catch {
7128            set tagcontents($tag) [exec git cat-file tag $tagobjid($tag)]
7129        }
7130    }
7131    if {[info exists tagcontents($tag)]} {
7132        set text $tagcontents($tag)
7133    } else {
7134        set text "Tag: $tag\nId:  $tagids($tag)"
7135    }
7136    appendwithlinks $text {}
7137    $ctext conf -state disabled
7138    init_flist {}
7139}
7140
7141proc doquit {} {
7142    global stopped
7143    set stopped 100
7144    savestuff .
7145    destroy .
7146}
7147
7148proc doprefs {} {
7149    global maxwidth maxgraphpct diffopts
7150    global oldprefs prefstop showneartags showlocalchanges
7151    global bgcolor fgcolor ctext diffcolors selectbgcolor
7152    global uifont tabstop
7153
7154    set top .gitkprefs
7155    set prefstop $top
7156    if {[winfo exists $top]} {
7157        raise $top
7158        return
7159    }
7160    foreach v {maxwidth maxgraphpct diffopts showneartags showlocalchanges} {
7161        set oldprefs($v) [set $v]
7162    }
7163    toplevel $top
7164    wm title $top "Gitk preferences"
7165    label $top.ldisp -text "Commit list display options"
7166    $top.ldisp configure -font $uifont
7167    grid $top.ldisp - -sticky w -pady 10
7168    label $top.spacer -text " "
7169    label $top.maxwidthl -text "Maximum graph width (lines)" \
7170        -font optionfont
7171    spinbox $top.maxwidth -from 0 -to 100 -width 4 -textvariable maxwidth
7172    grid $top.spacer $top.maxwidthl $top.maxwidth -sticky w
7173    label $top.maxpctl -text "Maximum graph width (% of pane)" \
7174        -font optionfont
7175    spinbox $top.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct
7176    grid x $top.maxpctl $top.maxpct -sticky w
7177    frame $top.showlocal
7178    label $top.showlocal.l -text "Show local changes" -font optionfont
7179    checkbutton $top.showlocal.b -variable showlocalchanges
7180    pack $top.showlocal.b $top.showlocal.l -side left
7181    grid x $top.showlocal -sticky w
7182
7183    label $top.ddisp -text "Diff display options"
7184    $top.ddisp configure -font $uifont
7185    grid $top.ddisp - -sticky w -pady 10
7186    label $top.diffoptl -text "Options for diff program" \
7187        -font optionfont
7188    entry $top.diffopt -width 20 -textvariable diffopts
7189    grid x $top.diffoptl $top.diffopt -sticky w
7190    frame $top.ntag
7191    label $top.ntag.l -text "Display nearby tags" -font optionfont
7192    checkbutton $top.ntag.b -variable showneartags
7193    pack $top.ntag.b $top.ntag.l -side left
7194    grid x $top.ntag -sticky w
7195    label $top.tabstopl -text "tabstop" -font optionfont
7196    spinbox $top.tabstop -from 1 -to 20 -width 4 -textvariable tabstop
7197    grid x $top.tabstopl $top.tabstop -sticky w
7198
7199    label $top.cdisp -text "Colors: press to choose"
7200    $top.cdisp configure -font $uifont
7201    grid $top.cdisp - -sticky w -pady 10
7202    label $top.bg -padx 40 -relief sunk -background $bgcolor
7203    button $top.bgbut -text "Background" -font optionfont \
7204        -command [list choosecolor bgcolor 0 $top.bg background setbg]
7205    grid x $top.bgbut $top.bg -sticky w
7206    label $top.fg -padx 40 -relief sunk -background $fgcolor
7207    button $top.fgbut -text "Foreground" -font optionfont \
7208        -command [list choosecolor fgcolor 0 $top.fg foreground setfg]
7209    grid x $top.fgbut $top.fg -sticky w
7210    label $top.diffold -padx 40 -relief sunk -background [lindex $diffcolors 0]
7211    button $top.diffoldbut -text "Diff: old lines" -font optionfont \
7212        -command [list choosecolor diffcolors 0 $top.diffold "diff old lines" \
7213                      [list $ctext tag conf d0 -foreground]]
7214    grid x $top.diffoldbut $top.diffold -sticky w
7215    label $top.diffnew -padx 40 -relief sunk -background [lindex $diffcolors 1]
7216    button $top.diffnewbut -text "Diff: new lines" -font optionfont \
7217        -command [list choosecolor diffcolors 1 $top.diffnew "diff new lines" \
7218                      [list $ctext tag conf d1 -foreground]]
7219    grid x $top.diffnewbut $top.diffnew -sticky w
7220    label $top.hunksep -padx 40 -relief sunk -background [lindex $diffcolors 2]
7221    button $top.hunksepbut -text "Diff: hunk header" -font optionfont \
7222        -command [list choosecolor diffcolors 2 $top.hunksep \
7223                      "diff hunk header" \
7224                      [list $ctext tag conf hunksep -foreground]]
7225    grid x $top.hunksepbut $top.hunksep -sticky w
7226    label $top.selbgsep -padx 40 -relief sunk -background $selectbgcolor
7227    button $top.selbgbut -text "Select bg" -font optionfont \
7228        -command [list choosecolor selectbgcolor 0 $top.selbgsep background setselbg]
7229    grid x $top.selbgbut $top.selbgsep -sticky w
7230
7231    frame $top.buts
7232    button $top.buts.ok -text "OK" -command prefsok -default active
7233    $top.buts.ok configure -font $uifont
7234    button $top.buts.can -text "Cancel" -command prefscan -default normal
7235    $top.buts.can configure -font $uifont
7236    grid $top.buts.ok $top.buts.can
7237    grid columnconfigure $top.buts 0 -weight 1 -uniform a
7238    grid columnconfigure $top.buts 1 -weight 1 -uniform a
7239    grid $top.buts - - -pady 10 -sticky ew
7240    bind $top <Visibility> "focus $top.buts.ok"
7241}
7242
7243proc choosecolor {v vi w x cmd} {
7244    global $v
7245
7246    set c [tk_chooseColor -initialcolor [lindex [set $v] $vi] \
7247               -title "Gitk: choose color for $x"]
7248    if {$c eq {}} return
7249    $w conf -background $c
7250    lset $v $vi $c
7251    eval $cmd $c
7252}
7253
7254proc setselbg {c} {
7255    global bglist cflist
7256    foreach w $bglist {
7257        $w configure -selectbackground $c
7258    }
7259    $cflist tag configure highlight \
7260        -background [$cflist cget -selectbackground]
7261    allcanvs itemconf secsel -fill $c
7262}
7263
7264proc setbg {c} {
7265    global bglist
7266
7267    foreach w $bglist {
7268        $w conf -background $c
7269    }
7270}
7271
7272proc setfg {c} {
7273    global fglist canv
7274
7275    foreach w $fglist {
7276        $w conf -foreground $c
7277    }
7278    allcanvs itemconf text -fill $c
7279    $canv itemconf circle -outline $c
7280}
7281
7282proc prefscan {} {
7283    global maxwidth maxgraphpct diffopts
7284    global oldprefs prefstop showneartags showlocalchanges
7285
7286    foreach v {maxwidth maxgraphpct diffopts showneartags showlocalchanges} {
7287        set $v $oldprefs($v)
7288    }
7289    catch {destroy $prefstop}
7290    unset prefstop
7291}
7292
7293proc prefsok {} {
7294    global maxwidth maxgraphpct
7295    global oldprefs prefstop showneartags showlocalchanges
7296    global charspc ctext tabstop
7297
7298    catch {destroy $prefstop}
7299    unset prefstop
7300    $ctext configure -tabs "[expr {$tabstop * $charspc}]"
7301    if {$showlocalchanges != $oldprefs(showlocalchanges)} {
7302        if {$showlocalchanges} {
7303            doshowlocalchanges
7304        } else {
7305            dohidelocalchanges
7306        }
7307    }
7308    if {$maxwidth != $oldprefs(maxwidth)
7309        || $maxgraphpct != $oldprefs(maxgraphpct)} {
7310        redisplay
7311    } elseif {$showneartags != $oldprefs(showneartags)} {
7312        reselectline
7313    }
7314}
7315
7316proc formatdate {d} {
7317    if {$d ne {}} {
7318        set d [clock format $d -format "%Y-%m-%d %H:%M:%S"]
7319    }
7320    return $d
7321}
7322
7323# This list of encoding names and aliases is distilled from
7324# http://www.iana.org/assignments/character-sets.
7325# Not all of them are supported by Tcl.
7326set encoding_aliases {
7327    { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII
7328      ISO646-US US-ASCII us IBM367 cp367 csASCII }
7329    { ISO-10646-UTF-1 csISO10646UTF1 }
7330    { ISO_646.basic:1983 ref csISO646basic1983 }
7331    { INVARIANT csINVARIANT }
7332    { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion }
7333    { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom }
7334    { NATS-SEFI iso-ir-8-1 csNATSSEFI }
7335    { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD }
7336    { NATS-DANO iso-ir-9-1 csNATSDANO }
7337    { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD }
7338    { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish }
7339    { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames }
7340    { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 }
7341    { ISO-2022-KR csISO2022KR }
7342    { EUC-KR csEUCKR }
7343    { ISO-2022-JP csISO2022JP }
7344    { ISO-2022-JP-2 csISO2022JP2 }
7345    { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7
7346      csISO13JISC6220jp }
7347    { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro }
7348    { IT iso-ir-15 ISO646-IT csISO15Italian }
7349    { PT iso-ir-16 ISO646-PT csISO16Portuguese }
7350    { ES iso-ir-17 ISO646-ES csISO17Spanish }
7351    { greek7-old iso-ir-18 csISO18Greek7Old }
7352    { latin-greek iso-ir-19 csISO19LatinGreek }
7353    { DIN_66003 iso-ir-21 de ISO646-DE csISO21German }
7354    { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French }
7355    { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 }
7356    { ISO_5427 iso-ir-37 csISO5427Cyrillic }
7357    { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 }
7358    { BS_viewdata iso-ir-47 csISO47BSViewdata }
7359    { INIS iso-ir-49 csISO49INIS }
7360    { INIS-8 iso-ir-50 csISO50INIS8 }
7361    { INIS-cyrillic iso-ir-51 csISO51INISCyrillic }
7362    { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 }
7363    { ISO_5428:1980 iso-ir-55 csISO5428Greek }
7364    { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 }
7365    { GB_2312-80 iso-ir-58 chinese csISO58GB231280 }
7366    { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian
7367      csISO60Norwegian1 }
7368    { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 }
7369    { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French }
7370    { videotex-suppl iso-ir-70 csISO70VideotexSupp1 }
7371    { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 }
7372    { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 }
7373    { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian }
7374    { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 }
7375    { greek7 iso-ir-88 csISO88Greek7 }
7376    { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 }
7377    { iso-ir-90 csISO90 }
7378    { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a }
7379    { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b
7380      csISO92JISC62991984b }
7381    { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd }
7382    { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand }
7383    { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add
7384      csISO95JIS62291984handadd }
7385    { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana }
7386    { ISO_2033-1983 iso-ir-98 e13b csISO2033 }
7387    { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS }
7388    { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819
7389      CP819 csISOLatin1 }
7390    { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 }
7391    { T.61-7bit iso-ir-102 csISO102T617bit }
7392    { T.61-8bit T.61 iso-ir-103 csISO103T618bit }
7393    { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 }
7394    { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 }
7395    { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic }
7396    { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 }
7397    { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 }
7398    { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr }
7399    { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708
7400      arabic csISOLatinArabic }
7401    { ISO_8859-6-E csISO88596E ISO-8859-6-E }
7402    { ISO_8859-6-I csISO88596I ISO-8859-6-I }
7403    { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118
7404      greek greek8 csISOLatinGreek }
7405    { T.101-G2 iso-ir-128 csISO128T101G2 }
7406    { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew
7407      csISOLatinHebrew }
7408    { ISO_8859-8-E csISO88598E ISO-8859-8-E }
7409    { ISO_8859-8-I csISO88598I ISO-8859-8-I }
7410    { CSN_369103 iso-ir-139 csISO139CSN369103 }
7411    { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 }
7412    { ISO_6937-2-add iso-ir-142 csISOTextComm }
7413    { IEC_P27-1 iso-ir-143 csISO143IECP271 }
7414    { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic
7415      csISOLatinCyrillic }
7416    { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian }
7417    { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian }
7418    { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 }
7419    { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT }
7420    { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba }
7421    { ISO_6937-2-25 iso-ir-152 csISO6937Add }
7422    { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 }
7423    { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp }
7424    { ISO_10367-box iso-ir-155 csISO10367Box }
7425    { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 }
7426    { latin-lap lap iso-ir-158 csISO158Lap }
7427    { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 }
7428    { DS_2089 DS2089 ISO646-DK dk csISO646Danish }
7429    { us-dk csUSDK }
7430    { dk-us csDKUS }
7431    { JIS_X0201 X0201 csHalfWidthKatakana }
7432    { KSC5636 ISO646-KR csKSC5636 }
7433    { ISO-10646-UCS-2 csUnicode }
7434    { ISO-10646-UCS-4 csUCS4 }
7435    { DEC-MCS dec csDECMCS }
7436    { hp-roman8 roman8 r8 csHPRoman8 }
7437    { macintosh mac csMacintosh }
7438    { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl
7439      csIBM037 }
7440    { IBM038 EBCDIC-INT cp038 csIBM038 }
7441    { IBM273 CP273 csIBM273 }
7442    { IBM274 EBCDIC-BE CP274 csIBM274 }
7443    { IBM275 EBCDIC-BR cp275 csIBM275 }
7444    { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 }
7445    { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 }
7446    { IBM280 CP280 ebcdic-cp-it csIBM280 }
7447    { IBM281 EBCDIC-JP-E cp281 csIBM281 }
7448    { IBM284 CP284 ebcdic-cp-es csIBM284 }
7449    { IBM285 CP285 ebcdic-cp-gb csIBM285 }
7450    { IBM290 cp290 EBCDIC-JP-kana csIBM290 }
7451    { IBM297 cp297 ebcdic-cp-fr csIBM297 }
7452    { IBM420 cp420 ebcdic-cp-ar1 csIBM420 }
7453    { IBM423 cp423 ebcdic-cp-gr csIBM423 }
7454    { IBM424 cp424 ebcdic-cp-he csIBM424 }
7455    { IBM437 cp437 437 csPC8CodePage437 }
7456    { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 }
7457    { IBM775 cp775 csPC775Baltic }
7458    { IBM850 cp850 850 csPC850Multilingual }
7459    { IBM851 cp851 851 csIBM851 }
7460    { IBM852 cp852 852 csPCp852 }
7461    { IBM855 cp855 855 csIBM855 }
7462    { IBM857 cp857 857 csIBM857 }
7463    { IBM860 cp860 860 csIBM860 }
7464    { IBM861 cp861 861 cp-is csIBM861 }
7465    { IBM862 cp862 862 csPC862LatinHebrew }
7466    { IBM863 cp863 863 csIBM863 }
7467    { IBM864 cp864 csIBM864 }
7468    { IBM865 cp865 865 csIBM865 }
7469    { IBM866 cp866 866 csIBM866 }
7470    { IBM868 CP868 cp-ar csIBM868 }
7471    { IBM869 cp869 869 cp-gr csIBM869 }
7472    { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 }
7473    { IBM871 CP871 ebcdic-cp-is csIBM871 }
7474    { IBM880 cp880 EBCDIC-Cyrillic csIBM880 }
7475    { IBM891 cp891 csIBM891 }
7476    { IBM903 cp903 csIBM903 }
7477    { IBM904 cp904 904 csIBBM904 }
7478    { IBM905 CP905 ebcdic-cp-tr csIBM905 }
7479    { IBM918 CP918 ebcdic-cp-ar2 csIBM918 }
7480    { IBM1026 CP1026 csIBM1026 }
7481    { EBCDIC-AT-DE csIBMEBCDICATDE }
7482    { EBCDIC-AT-DE-A csEBCDICATDEA }
7483    { EBCDIC-CA-FR csEBCDICCAFR }
7484    { EBCDIC-DK-NO csEBCDICDKNO }
7485    { EBCDIC-DK-NO-A csEBCDICDKNOA }
7486    { EBCDIC-FI-SE csEBCDICFISE }
7487    { EBCDIC-FI-SE-A csEBCDICFISEA }
7488    { EBCDIC-FR csEBCDICFR }
7489    { EBCDIC-IT csEBCDICIT }
7490    { EBCDIC-PT csEBCDICPT }
7491    { EBCDIC-ES csEBCDICES }
7492    { EBCDIC-ES-A csEBCDICESA }
7493    { EBCDIC-ES-S csEBCDICESS }
7494    { EBCDIC-UK csEBCDICUK }
7495    { EBCDIC-US csEBCDICUS }
7496    { UNKNOWN-8BIT csUnknown8BiT }
7497    { MNEMONIC csMnemonic }
7498    { MNEM csMnem }
7499    { VISCII csVISCII }
7500    { VIQR csVIQR }
7501    { KOI8-R csKOI8R }
7502    { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro }
7503    { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro }
7504    { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro }
7505    { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro }
7506    { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro }
7507    { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro }
7508    { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro }
7509    { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro }
7510    { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro }
7511    { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro }
7512    { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro }
7513    { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro }
7514    { IBM1047 IBM-1047 }
7515    { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian }
7516    { Amiga-1251 Ami1251 Amiga1251 Ami-1251 }
7517    { UNICODE-1-1 csUnicode11 }
7518    { CESU-8 csCESU-8 }
7519    { BOCU-1 csBOCU-1 }
7520    { UNICODE-1-1-UTF-7 csUnicode11UTF7 }
7521    { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic
7522      l8 }
7523    { ISO-8859-15 ISO_8859-15 Latin-9 }
7524    { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 }
7525    { GBK CP936 MS936 windows-936 }
7526    { JIS_Encoding csJISEncoding }
7527    { Shift_JIS MS_Kanji csShiftJIS }
7528    { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese
7529      EUC-JP }
7530    { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese }
7531    { ISO-10646-UCS-Basic csUnicodeASCII }
7532    { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 }
7533    { ISO-Unicode-IBM-1261 csUnicodeIBM1261 }
7534    { ISO-Unicode-IBM-1268 csUnicodeIBM1268 }
7535    { ISO-Unicode-IBM-1276 csUnicodeIBM1276 }
7536    { ISO-Unicode-IBM-1264 csUnicodeIBM1264 }
7537    { ISO-Unicode-IBM-1265 csUnicodeIBM1265 }
7538    { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 }
7539    { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 }
7540    { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 }
7541    { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 }
7542    { Adobe-Standard-Encoding csAdobeStandardEncoding }
7543    { Ventura-US csVenturaUS }
7544    { Ventura-International csVenturaInternational }
7545    { PC8-Danish-Norwegian csPC8DanishNorwegian }
7546    { PC8-Turkish csPC8Turkish }
7547    { IBM-Symbols csIBMSymbols }
7548    { IBM-Thai csIBMThai }
7549    { HP-Legal csHPLegal }
7550    { HP-Pi-font csHPPiFont }
7551    { HP-Math8 csHPMath8 }
7552    { Adobe-Symbol-Encoding csHPPSMath }
7553    { HP-DeskTop csHPDesktop }
7554    { Ventura-Math csVenturaMath }
7555    { Microsoft-Publishing csMicrosoftPublishing }
7556    { Windows-31J csWindows31J }
7557    { GB2312 csGB2312 }
7558    { Big5 csBig5 }
7559}
7560
7561proc tcl_encoding {enc} {
7562    global encoding_aliases
7563    set names [encoding names]
7564    set lcnames [string tolower $names]
7565    set enc [string tolower $enc]
7566    set i [lsearch -exact $lcnames $enc]
7567    if {$i < 0} {
7568        # look for "isonnn" instead of "iso-nnn" or "iso_nnn"
7569        if {[regsub {^iso[-_]} $enc iso encx]} {
7570            set i [lsearch -exact $lcnames $encx]
7571        }
7572    }
7573    if {$i < 0} {
7574        foreach l $encoding_aliases {
7575            set ll [string tolower $l]
7576            if {[lsearch -exact $ll $enc] < 0} continue
7577            # look through the aliases for one that tcl knows about
7578            foreach e $ll {
7579                set i [lsearch -exact $lcnames $e]
7580                if {$i < 0} {
7581                    if {[regsub {^iso[-_]} $e iso ex]} {
7582                        set i [lsearch -exact $lcnames $ex]
7583                    }
7584                }
7585                if {$i >= 0} break
7586            }
7587            break
7588        }
7589    }
7590    if {$i >= 0} {
7591        return [lindex $names $i]
7592    }
7593    return {}
7594}
7595
7596# defaults...
7597set datemode 0
7598set diffopts "-U 5 -p"
7599set wrcomcmd "git diff-tree --stdin -p --pretty"
7600
7601set gitencoding {}
7602catch {
7603    set gitencoding [exec git config --get i18n.commitencoding]
7604}
7605if {$gitencoding == ""} {
7606    set gitencoding "utf-8"
7607}
7608set tclencoding [tcl_encoding $gitencoding]
7609if {$tclencoding == {}} {
7610    puts stderr "Warning: encoding $gitencoding is not supported by Tcl/Tk"
7611}
7612
7613set mainfont {Helvetica 9}
7614set textfont {Courier 9}
7615set uifont {Helvetica 9 bold}
7616set tabstop 8
7617set findmergefiles 0
7618set maxgraphpct 50
7619set maxwidth 16
7620set revlistorder 0
7621set fastdate 0
7622set uparrowlen 7
7623set downarrowlen 7
7624set mingaplen 30
7625set cmitmode "patch"
7626set wrapcomment "none"
7627set showneartags 1
7628set maxrefs 20
7629set maxlinelen 200
7630set showlocalchanges 1
7631
7632set colors {green red blue magenta darkgrey brown orange}
7633set bgcolor white
7634set fgcolor black
7635set diffcolors {red "#00a000" blue}
7636set selectbgcolor gray85
7637
7638catch {source ~/.gitk}
7639
7640font create optionfont -family sans-serif -size -12
7641
7642# check that we can find a .git directory somewhere...
7643if {[catch {set gitdir [gitdir]}]} {
7644    show_error {} . "Cannot find a git repository here."
7645    exit 1
7646}
7647if {![file isdirectory $gitdir]} {
7648    show_error {} . "Cannot find the git directory \"$gitdir\"."
7649    exit 1
7650}
7651
7652set revtreeargs {}
7653set cmdline_files {}
7654set i 0
7655foreach arg $argv {
7656    switch -- $arg {
7657        "" { }
7658        "-d" { set datemode 1 }
7659        "--" {
7660            set cmdline_files [lrange $argv [expr {$i + 1}] end]
7661            break
7662        }
7663        default {
7664            lappend revtreeargs $arg
7665        }
7666    }
7667    incr i
7668}
7669
7670if {$i >= [llength $argv] && $revtreeargs ne {}} {
7671    # no -- on command line, but some arguments (other than -d)
7672    if {[catch {
7673        set f [eval exec git rev-parse --no-revs --no-flags $revtreeargs]
7674        set cmdline_files [split $f "\n"]
7675        set n [llength $cmdline_files]
7676        set revtreeargs [lrange $revtreeargs 0 end-$n]
7677        # Unfortunately git rev-parse doesn't produce an error when
7678        # something is both a revision and a filename.  To be consistent
7679        # with git log and git rev-list, check revtreeargs for filenames.
7680        foreach arg $revtreeargs {
7681            if {[file exists $arg]} {
7682                show_error {} . "Ambiguous argument '$arg': both revision\
7683                                 and filename"
7684                exit 1
7685            }
7686        }
7687    } err]} {
7688        # unfortunately we get both stdout and stderr in $err,
7689        # so look for "fatal:".
7690        set i [string first "fatal:" $err]
7691        if {$i > 0} {
7692            set err [string range $err [expr {$i + 6}] end]
7693        }
7694        show_error {} . "Bad arguments to gitk:\n$err"
7695        exit 1
7696    }
7697}
7698
7699set nullid "0000000000000000000000000000000000000000"
7700set nullid2 "0000000000000000000000000000000000000001"
7701
7702
7703set runq {}
7704set history {}
7705set historyindex 0
7706set fh_serial 0
7707set nhl_names {}
7708set highlight_paths {}
7709set searchdirn -forwards
7710set boldrows {}
7711set boldnamerows {}
7712set diffelide {0 0}
7713set markingmatches 0
7714
7715set optim_delay 16
7716
7717set nextviewnum 1
7718set curview 0
7719set selectedview 0
7720set selectedhlview None
7721set viewfiles(0) {}
7722set viewperm(0) 0
7723set viewargs(0) {}
7724
7725set cmdlineok 0
7726set stopped 0
7727set stuffsaved 0
7728set patchnum 0
7729set lookingforhead 0
7730set localirow -1
7731set localfrow -1
7732set lserial 0
7733setcoords
7734makewindow
7735# wait for the window to become visible
7736tkwait visibility .
7737wm title . "[file tail $argv0]: [file tail [pwd]]"
7738readrefs
7739
7740if {$cmdline_files ne {} || $revtreeargs ne {}} {
7741    # create a view for the files/dirs specified on the command line
7742    set curview 1
7743    set selectedview 1
7744    set nextviewnum 2
7745    set viewname(1) "Command line"
7746    set viewfiles(1) $cmdline_files
7747    set viewargs(1) $revtreeargs
7748    set viewperm(1) 0
7749    addviewmenu 1
7750    .bar.view entryconf Edit* -state normal
7751    .bar.view entryconf Delete* -state normal
7752}
7753
7754if {[info exists permviews]} {
7755    foreach v $permviews {
7756        set n $nextviewnum
7757        incr nextviewnum
7758        set viewname($n) [lindex $v 0]
7759        set viewfiles($n) [lindex $v 1]
7760        set viewargs($n) [lindex $v 2]
7761        set viewperm($n) 1
7762        addviewmenu $n
7763    }
7764}
7765getcommits