gitk-git / gitkon commit Merge branch 'db/maint-checkout-b' (666599d)
   1#!/bin/sh
   2# Tcl ignores the next line -*- tcl -*- \
   3exec wish "$0" -- "$@"
   4
   5# Copyright © 2005-2008 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 currunq
  26
  27    set script $args
  28    if {[info exists isonrunq($script)]} return
  29    if {$runq eq {} && ![info exists currunq]} {
  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 currunq
  42
  43    fileevent $fd readable {}
  44    if {$runq eq {} && ![info exists currunq]} {
  45        after idle dorunq
  46    }
  47    lappend runq [list $fd $script]
  48}
  49
  50proc nukefile {fd} {
  51    global runq
  52
  53    for {set i 0} {$i < [llength $runq]} {} {
  54        if {[lindex $runq $i 0] eq $fd} {
  55            set runq [lreplace $runq $i $i]
  56        } else {
  57            incr i
  58        }
  59    }
  60}
  61
  62proc dorunq {} {
  63    global isonrunq runq currunq
  64
  65    set tstart [clock clicks -milliseconds]
  66    set t0 $tstart
  67    while {[llength $runq] > 0} {
  68        set fd [lindex $runq 0 0]
  69        set script [lindex $runq 0 1]
  70        set currunq [lindex $runq 0]
  71        set runq [lrange $runq 1 end]
  72        set repeat [eval $script]
  73        unset currunq
  74        set t1 [clock clicks -milliseconds]
  75        set t [expr {$t1 - $t0}]
  76        if {$repeat ne {} && $repeat} {
  77            if {$fd eq {} || $repeat == 2} {
  78                # script returns 1 if it wants to be readded
  79                # file readers return 2 if they could do more straight away
  80                lappend runq [list $fd $script]
  81            } else {
  82                fileevent $fd readable [list filereadable $fd $script]
  83            }
  84        } elseif {$fd eq {}} {
  85            unset isonrunq($script)
  86        }
  87        set t0 $t1
  88        if {$t1 - $tstart >= 80} break
  89    }
  90    if {$runq ne {}} {
  91        after idle dorunq
  92    }
  93}
  94
  95proc reg_instance {fd} {
  96    global commfd leftover loginstance
  97
  98    set i [incr loginstance]
  99    set commfd($i) $fd
 100    set leftover($i) {}
 101    return $i
 102}
 103
 104proc unmerged_files {files} {
 105    global nr_unmerged
 106
 107    # find the list of unmerged files
 108    set mlist {}
 109    set nr_unmerged 0
 110    if {[catch {
 111        set fd [open "| git ls-files -u" r]
 112    } err]} {
 113        show_error {} . "[mc "Couldn't get list of unmerged files:"] $err"
 114        exit 1
 115    }
 116    while {[gets $fd line] >= 0} {
 117        set i [string first "\t" $line]
 118        if {$i < 0} continue
 119        set fname [string range $line [expr {$i+1}] end]
 120        if {[lsearch -exact $mlist $fname] >= 0} continue
 121        incr nr_unmerged
 122        if {$files eq {} || [path_filter $files $fname]} {
 123            lappend mlist $fname
 124        }
 125    }
 126    catch {close $fd}
 127    return $mlist
 128}
 129
 130proc parseviewargs {n arglist} {
 131    global vdatemode vmergeonly vflags vdflags vrevs vfiltered vorigargs
 132
 133    set vdatemode($n) 0
 134    set vmergeonly($n) 0
 135    set glflags {}
 136    set diffargs {}
 137    set nextisval 0
 138    set revargs {}
 139    set origargs $arglist
 140    set allknown 1
 141    set filtered 0
 142    set i -1
 143    foreach arg $arglist {
 144        incr i
 145        if {$nextisval} {
 146            lappend glflags $arg
 147            set nextisval 0
 148            continue
 149        }
 150        switch -glob -- $arg {
 151            "-d" -
 152            "--date-order" {
 153                set vdatemode($n) 1
 154                # remove from origargs in case we hit an unknown option
 155                set origargs [lreplace $origargs $i $i]
 156                incr i -1
 157            }
 158            # These request or affect diff output, which we don't want.
 159            # Some could be used to set our defaults for diff display.
 160            "-[puabwcrRBMC]" -
 161            "--no-renames" - "--full-index" - "--binary" - "--abbrev=*" -
 162            "--find-copies-harder" - "-l*" - "--ext-diff" - "--no-ext-diff" -
 163            "--src-prefix=*" - "--dst-prefix=*" - "--no-prefix" -
 164            "-O*" - "--text" - "--full-diff" - "--ignore-space-at-eol" -
 165            "--ignore-space-change" - "-U*" - "--unified=*" {
 166                lappend diffargs $arg
 167            }
 168            # These cause our parsing of git log's output to fail, or else
 169            # they're options we want to set ourselves, so ignore them.
 170            "--raw" - "--patch-with-raw" - "--patch-with-stat" -
 171            "--name-only" - "--name-status" - "--color" - "--color-words" -
 172            "--log-size" - "--pretty=*" - "--decorate" - "--abbrev-commit" -
 173            "--cc" - "-z" - "--header" - "--parents" - "--boundary" -
 174            "--no-color" - "-g" - "--walk-reflogs" - "--no-walk" -
 175            "--timestamp" - "relative-date" - "--date=*" - "--stdin" -
 176            "--objects" - "--objects-edge" - "--reverse" {
 177            }
 178            # These are harmless, and some are even useful
 179            "--stat=*" - "--numstat" - "--shortstat" - "--summary" -
 180            "--check" - "--exit-code" - "--quiet" - "--topo-order" -
 181            "--full-history" - "--dense" - "--sparse" -
 182            "--follow" - "--left-right" - "--encoding=*" {
 183                lappend glflags $arg
 184            }
 185            # These mean that we get a subset of the commits
 186            "--diff-filter=*" - "--no-merges" - "--unpacked" -
 187            "--max-count=*" - "--skip=*" - "--since=*" - "--after=*" -
 188            "--until=*" - "--before=*" - "--max-age=*" - "--min-age=*" -
 189            "--author=*" - "--committer=*" - "--grep=*" - "-[iE]" -
 190            "--remove-empty" - "--first-parent" - "--cherry-pick" -
 191            "-S*" - "--pickaxe-all" - "--pickaxe-regex" - {
 192                set filtered 1
 193                lappend glflags $arg
 194            }
 195            # This appears to be the only one that has a value as a
 196            # separate word following it
 197            "-n" {
 198                set filtered 1
 199                set nextisval 1
 200                lappend glflags $arg
 201            }
 202            "--not" {
 203                set notflag [expr {!$notflag}]
 204                lappend revargs $arg
 205            }
 206            "--all" {
 207                lappend revargs $arg
 208            }
 209            "--merge" {
 210                set vmergeonly($n) 1
 211                # git rev-parse doesn't understand --merge
 212                lappend revargs --gitk-symmetric-diff-marker MERGE_HEAD...HEAD
 213            }
 214            # Other flag arguments including -<n>
 215            "-*" {
 216                if {[string is digit -strict [string range $arg 1 end]]} {
 217                    set filtered 1
 218                } else {
 219                    # a flag argument that we don't recognize;
 220                    # that means we can't optimize
 221                    set allknown 0
 222                }
 223                lappend glflags $arg
 224            }
 225            # Non-flag arguments specify commits or ranges of commits
 226            default {
 227                if {[string match "*...*" $arg]} {
 228                    lappend revargs --gitk-symmetric-diff-marker
 229                }
 230                lappend revargs $arg
 231            }
 232        }
 233    }
 234    set vdflags($n) $diffargs
 235    set vflags($n) $glflags
 236    set vrevs($n) $revargs
 237    set vfiltered($n) $filtered
 238    set vorigargs($n) $origargs
 239    return $allknown
 240}
 241
 242proc parseviewrevs {view revs} {
 243    global vposids vnegids
 244
 245    if {$revs eq {}} {
 246        set revs HEAD
 247    }
 248    if {[catch {set ids [eval exec git rev-parse $revs]} err]} {
 249        # we get stdout followed by stderr in $err
 250        # for an unknown rev, git rev-parse echoes it and then errors out
 251        set errlines [split $err "\n"]
 252        set badrev {}
 253        for {set l 0} {$l < [llength $errlines]} {incr l} {
 254            set line [lindex $errlines $l]
 255            if {!([string length $line] == 40 && [string is xdigit $line])} {
 256                if {[string match "fatal:*" $line]} {
 257                    if {[string match "fatal: ambiguous argument*" $line]
 258                        && $badrev ne {}} {
 259                        if {[llength $badrev] == 1} {
 260                            set err "unknown revision $badrev"
 261                        } else {
 262                            set err "unknown revisions: [join $badrev ", "]"
 263                        }
 264                    } else {
 265                        set err [join [lrange $errlines $l end] "\n"]
 266                    }
 267                    break
 268                }
 269                lappend badrev $line
 270            }
 271        }                   
 272        error_popup "Error parsing revisions: $err"
 273        return {}
 274    }
 275    set ret {}
 276    set pos {}
 277    set neg {}
 278    set sdm 0
 279    foreach id [split $ids "\n"] {
 280        if {$id eq "--gitk-symmetric-diff-marker"} {
 281            set sdm 4
 282        } elseif {[string match "^*" $id]} {
 283            if {$sdm != 1} {
 284                lappend ret $id
 285                if {$sdm == 3} {
 286                    set sdm 0
 287                }
 288            }
 289            lappend neg [string range $id 1 end]
 290        } else {
 291            if {$sdm != 2} {
 292                lappend ret $id
 293            } else {
 294                lset ret end [lindex $ret end]...$id
 295            }
 296            lappend pos $id
 297        }
 298        incr sdm -1
 299    }
 300    set vposids($view) $pos
 301    set vnegids($view) $neg
 302    return $ret
 303}
 304
 305# Start off a git log process and arrange to read its output
 306proc start_rev_list {view} {
 307    global startmsecs commitidx viewcomplete curview
 308    global tclencoding
 309    global viewargs viewargscmd viewfiles vfilelimit
 310    global showlocalchanges commitinterest
 311    global viewactive viewinstances vmergeonly
 312    global mainheadid
 313    global vcanopt vflags vrevs vorigargs
 314
 315    set startmsecs [clock clicks -milliseconds]
 316    set commitidx($view) 0
 317    # these are set this way for the error exits
 318    set viewcomplete($view) 1
 319    set viewactive($view) 0
 320    varcinit $view
 321
 322    set args $viewargs($view)
 323    if {$viewargscmd($view) ne {}} {
 324        if {[catch {
 325            set str [exec sh -c $viewargscmd($view)]
 326        } err]} {
 327            error_popup "Error executing --argscmd command: $err"
 328            return 0
 329        }
 330        set args [concat $args [split $str "\n"]]
 331    }
 332    set vcanopt($view) [parseviewargs $view $args]
 333
 334    set files $viewfiles($view)
 335    if {$vmergeonly($view)} {
 336        set files [unmerged_files $files]
 337        if {$files eq {}} {
 338            global nr_unmerged
 339            if {$nr_unmerged == 0} {
 340                error_popup [mc "No files selected: --merge specified but\
 341                             no files are unmerged."]
 342            } else {
 343                error_popup [mc "No files selected: --merge specified but\
 344                             no unmerged files are within file limit."]
 345            }
 346            return 0
 347        }
 348    }
 349    set vfilelimit($view) $files
 350
 351    if {$vcanopt($view)} {
 352        set revs [parseviewrevs $view $vrevs($view)]
 353        if {$revs eq {}} {
 354            return 0
 355        }
 356        set args [concat $vflags($view) $revs]
 357    } else {
 358        set args $vorigargs($view)
 359    }
 360
 361    if {[catch {
 362        set fd [open [concat | git log --no-color -z --pretty=raw --parents \
 363                         --boundary $args "--" $files] r]
 364    } err]} {
 365        error_popup "[mc "Error executing git log:"] $err"
 366        return 0
 367    }
 368    set i [reg_instance $fd]
 369    set viewinstances($view) [list $i]
 370    if {$showlocalchanges && $mainheadid ne {}} {
 371        lappend commitinterest($mainheadid) {dodiffindex}
 372    }
 373    fconfigure $fd -blocking 0 -translation lf -eofchar {}
 374    if {$tclencoding != {}} {
 375        fconfigure $fd -encoding $tclencoding
 376    }
 377    filerun $fd [list getcommitlines $fd $i $view 0]
 378    nowbusy $view [mc "Reading"]
 379    set viewcomplete($view) 0
 380    set viewactive($view) 1
 381    return 1
 382}
 383
 384proc stop_instance {inst} {
 385    global commfd leftover
 386
 387    set fd $commfd($inst)
 388    catch {
 389        set pid [pid $fd]
 390
 391        if {$::tcl_platform(platform) eq {windows}} {
 392            exec kill -f $pid
 393        } else {
 394            exec kill $pid
 395        }
 396    }
 397    catch {close $fd}
 398    nukefile $fd
 399    unset commfd($inst)
 400    unset leftover($inst)
 401}
 402
 403proc stop_backends {} {
 404    global commfd
 405
 406    foreach inst [array names commfd] {
 407        stop_instance $inst
 408    }
 409}
 410
 411proc stop_rev_list {view} {
 412    global viewinstances
 413
 414    foreach inst $viewinstances($view) {
 415        stop_instance $inst
 416    }
 417    set viewinstances($view) {}
 418}
 419
 420proc reset_pending_select {selid} {
 421    global pending_select mainheadid selectheadid
 422
 423    if {$selid ne {}} {
 424        set pending_select $selid
 425    } elseif {$selectheadid ne {}} {
 426        set pending_select $selectheadid
 427    } else {
 428        set pending_select $mainheadid
 429    }
 430}
 431
 432proc getcommits {selid} {
 433    global canv curview need_redisplay viewactive
 434
 435    initlayout
 436    if {[start_rev_list $curview]} {
 437        reset_pending_select $selid
 438        show_status [mc "Reading commits..."]
 439        set need_redisplay 1
 440    } else {
 441        show_status [mc "No commits selected"]
 442    }
 443}
 444
 445proc updatecommits {} {
 446    global curview vcanopt vorigargs vfilelimit viewinstances
 447    global viewactive viewcomplete tclencoding
 448    global startmsecs showneartags showlocalchanges
 449    global mainheadid pending_select
 450    global isworktree
 451    global varcid vposids vnegids vflags vrevs
 452
 453    set isworktree [expr {[exec git rev-parse --is-inside-work-tree] == "true"}]
 454    set oldmainid $mainheadid
 455    rereadrefs
 456    if {$showlocalchanges} {
 457        if {$mainheadid ne $oldmainid} {
 458            dohidelocalchanges
 459        }
 460        if {[commitinview $mainheadid $curview]} {
 461            dodiffindex
 462        }
 463    }
 464    set view $curview
 465    if {$vcanopt($view)} {
 466        set oldpos $vposids($view)
 467        set oldneg $vnegids($view)
 468        set revs [parseviewrevs $view $vrevs($view)]
 469        if {$revs eq {}} {
 470            return
 471        }
 472        # note: getting the delta when negative refs change is hard,
 473        # and could require multiple git log invocations, so in that
 474        # case we ask git log for all the commits (not just the delta)
 475        if {$oldneg eq $vnegids($view)} {
 476            set newrevs {}
 477            set npos 0
 478            # take out positive refs that we asked for before or
 479            # that we have already seen
 480            foreach rev $revs {
 481                if {[string length $rev] == 40} {
 482                    if {[lsearch -exact $oldpos $rev] < 0
 483                        && ![info exists varcid($view,$rev)]} {
 484                        lappend newrevs $rev
 485                        incr npos
 486                    }
 487                } else {
 488                    lappend $newrevs $rev
 489                }
 490            }
 491            if {$npos == 0} return
 492            set revs $newrevs
 493            set vposids($view) [lsort -unique [concat $oldpos $vposids($view)]]
 494        }
 495        set args [concat $vflags($view) $revs --not $oldpos]
 496    } else {
 497        set args $vorigargs($view)
 498    }
 499    if {[catch {
 500        set fd [open [concat | git log --no-color -z --pretty=raw --parents \
 501                          --boundary $args "--" $vfilelimit($view)] r]
 502    } err]} {
 503        error_popup "Error executing git log: $err"
 504        return
 505    }
 506    if {$viewactive($view) == 0} {
 507        set startmsecs [clock clicks -milliseconds]
 508    }
 509    set i [reg_instance $fd]
 510    lappend viewinstances($view) $i
 511    fconfigure $fd -blocking 0 -translation lf -eofchar {}
 512    if {$tclencoding != {}} {
 513        fconfigure $fd -encoding $tclencoding
 514    }
 515    filerun $fd [list getcommitlines $fd $i $view 1]
 516    incr viewactive($view)
 517    set viewcomplete($view) 0
 518    reset_pending_select {}
 519    nowbusy $view "Reading"
 520    if {$showneartags} {
 521        getallcommits
 522    }
 523}
 524
 525proc reloadcommits {} {
 526    global curview viewcomplete selectedline currentid thickerline
 527    global showneartags treediffs commitinterest cached_commitrow
 528    global targetid
 529
 530    set selid {}
 531    if {$selectedline ne {}} {
 532        set selid $currentid
 533    }
 534
 535    if {!$viewcomplete($curview)} {
 536        stop_rev_list $curview
 537    }
 538    resetvarcs $curview
 539    set selectedline {}
 540    catch {unset currentid}
 541    catch {unset thickerline}
 542    catch {unset treediffs}
 543    readrefs
 544    changedrefs
 545    if {$showneartags} {
 546        getallcommits
 547    }
 548    clear_display
 549    catch {unset commitinterest}
 550    catch {unset cached_commitrow}
 551    catch {unset targetid}
 552    setcanvscroll
 553    getcommits $selid
 554    return 0
 555}
 556
 557# This makes a string representation of a positive integer which
 558# sorts as a string in numerical order
 559proc strrep {n} {
 560    if {$n < 16} {
 561        return [format "%x" $n]
 562    } elseif {$n < 256} {
 563        return [format "x%.2x" $n]
 564    } elseif {$n < 65536} {
 565        return [format "y%.4x" $n]
 566    }
 567    return [format "z%.8x" $n]
 568}
 569
 570# Procedures used in reordering commits from git log (without
 571# --topo-order) into the order for display.
 572
 573proc varcinit {view} {
 574    global varcstart vupptr vdownptr vleftptr vbackptr varctok varcrow
 575    global vtokmod varcmod vrowmod varcix vlastins
 576
 577    set varcstart($view) {{}}
 578    set vupptr($view) {0}
 579    set vdownptr($view) {0}
 580    set vleftptr($view) {0}
 581    set vbackptr($view) {0}
 582    set varctok($view) {{}}
 583    set varcrow($view) {{}}
 584    set vtokmod($view) {}
 585    set varcmod($view) 0
 586    set vrowmod($view) 0
 587    set varcix($view) {{}}
 588    set vlastins($view) {0}
 589}
 590
 591proc resetvarcs {view} {
 592    global varcid varccommits parents children vseedcount ordertok
 593
 594    foreach vid [array names varcid $view,*] {
 595        unset varcid($vid)
 596        unset children($vid)
 597        unset parents($vid)
 598    }
 599    # some commits might have children but haven't been seen yet
 600    foreach vid [array names children $view,*] {
 601        unset children($vid)
 602    }
 603    foreach va [array names varccommits $view,*] {
 604        unset varccommits($va)
 605    }
 606    foreach vd [array names vseedcount $view,*] {
 607        unset vseedcount($vd)
 608    }
 609    catch {unset ordertok}
 610}
 611
 612# returns a list of the commits with no children
 613proc seeds {v} {
 614    global vdownptr vleftptr varcstart
 615
 616    set ret {}
 617    set a [lindex $vdownptr($v) 0]
 618    while {$a != 0} {
 619        lappend ret [lindex $varcstart($v) $a]
 620        set a [lindex $vleftptr($v) $a]
 621    }
 622    return $ret
 623}
 624
 625proc newvarc {view id} {
 626    global varcid varctok parents children vdatemode
 627    global vupptr vdownptr vleftptr vbackptr varcrow varcix varcstart
 628    global commitdata commitinfo vseedcount varccommits vlastins
 629
 630    set a [llength $varctok($view)]
 631    set vid $view,$id
 632    if {[llength $children($vid)] == 0 || $vdatemode($view)} {
 633        if {![info exists commitinfo($id)]} {
 634            parsecommit $id $commitdata($id) 1
 635        }
 636        set cdate [lindex $commitinfo($id) 4]
 637        if {![string is integer -strict $cdate]} {
 638            set cdate 0
 639        }
 640        if {![info exists vseedcount($view,$cdate)]} {
 641            set vseedcount($view,$cdate) -1
 642        }
 643        set c [incr vseedcount($view,$cdate)]
 644        set cdate [expr {$cdate ^ 0xffffffff}]
 645        set tok "s[strrep $cdate][strrep $c]"
 646    } else {
 647        set tok {}
 648    }
 649    set ka 0
 650    if {[llength $children($vid)] > 0} {
 651        set kid [lindex $children($vid) end]
 652        set k $varcid($view,$kid)
 653        if {[string compare [lindex $varctok($view) $k] $tok] > 0} {
 654            set ki $kid
 655            set ka $k
 656            set tok [lindex $varctok($view) $k]
 657        }
 658    }
 659    if {$ka != 0} {
 660        set i [lsearch -exact $parents($view,$ki) $id]
 661        set j [expr {[llength $parents($view,$ki)] - 1 - $i}]
 662        append tok [strrep $j]
 663    }
 664    set c [lindex $vlastins($view) $ka]
 665    if {$c == 0 || [string compare $tok [lindex $varctok($view) $c]] < 0} {
 666        set c $ka
 667        set b [lindex $vdownptr($view) $ka]
 668    } else {
 669        set b [lindex $vleftptr($view) $c]
 670    }
 671    while {$b != 0 && [string compare $tok [lindex $varctok($view) $b]] >= 0} {
 672        set c $b
 673        set b [lindex $vleftptr($view) $c]
 674    }
 675    if {$c == $ka} {
 676        lset vdownptr($view) $ka $a
 677        lappend vbackptr($view) 0
 678    } else {
 679        lset vleftptr($view) $c $a
 680        lappend vbackptr($view) $c
 681    }
 682    lset vlastins($view) $ka $a
 683    lappend vupptr($view) $ka
 684    lappend vleftptr($view) $b
 685    if {$b != 0} {
 686        lset vbackptr($view) $b $a
 687    }
 688    lappend varctok($view) $tok
 689    lappend varcstart($view) $id
 690    lappend vdownptr($view) 0
 691    lappend varcrow($view) {}
 692    lappend varcix($view) {}
 693    set varccommits($view,$a) {}
 694    lappend vlastins($view) 0
 695    return $a
 696}
 697
 698proc splitvarc {p v} {
 699    global varcid varcstart varccommits varctok
 700    global vupptr vdownptr vleftptr vbackptr varcix varcrow vlastins
 701
 702    set oa $varcid($v,$p)
 703    set ac $varccommits($v,$oa)
 704    set i [lsearch -exact $varccommits($v,$oa) $p]
 705    if {$i <= 0} return
 706    set na [llength $varctok($v)]
 707    # "%" sorts before "0"...
 708    set tok "[lindex $varctok($v) $oa]%[strrep $i]"
 709    lappend varctok($v) $tok
 710    lappend varcrow($v) {}
 711    lappend varcix($v) {}
 712    set varccommits($v,$oa) [lrange $ac 0 [expr {$i - 1}]]
 713    set varccommits($v,$na) [lrange $ac $i end]
 714    lappend varcstart($v) $p
 715    foreach id $varccommits($v,$na) {
 716        set varcid($v,$id) $na
 717    }
 718    lappend vdownptr($v) [lindex $vdownptr($v) $oa]
 719    lappend vlastins($v) [lindex $vlastins($v) $oa]
 720    lset vdownptr($v) $oa $na
 721    lset vlastins($v) $oa 0
 722    lappend vupptr($v) $oa
 723    lappend vleftptr($v) 0
 724    lappend vbackptr($v) 0
 725    for {set b [lindex $vdownptr($v) $na]} {$b != 0} {set b [lindex $vleftptr($v) $b]} {
 726        lset vupptr($v) $b $na
 727    }
 728}
 729
 730proc renumbervarc {a v} {
 731    global parents children varctok varcstart varccommits
 732    global vupptr vdownptr vleftptr vbackptr vlastins varcid vtokmod vdatemode
 733
 734    set t1 [clock clicks -milliseconds]
 735    set todo {}
 736    set isrelated($a) 1
 737    set kidchanged($a) 1
 738    set ntot 0
 739    while {$a != 0} {
 740        if {[info exists isrelated($a)]} {
 741            lappend todo $a
 742            set id [lindex $varccommits($v,$a) end]
 743            foreach p $parents($v,$id) {
 744                if {[info exists varcid($v,$p)]} {
 745                    set isrelated($varcid($v,$p)) 1
 746                }
 747            }
 748        }
 749        incr ntot
 750        set b [lindex $vdownptr($v) $a]
 751        if {$b == 0} {
 752            while {$a != 0} {
 753                set b [lindex $vleftptr($v) $a]
 754                if {$b != 0} break
 755                set a [lindex $vupptr($v) $a]
 756            }
 757        }
 758        set a $b
 759    }
 760    foreach a $todo {
 761        if {![info exists kidchanged($a)]} continue
 762        set id [lindex $varcstart($v) $a]
 763        if {[llength $children($v,$id)] > 1} {
 764            set children($v,$id) [lsort -command [list vtokcmp $v] \
 765                                      $children($v,$id)]
 766        }
 767        set oldtok [lindex $varctok($v) $a]
 768        if {!$vdatemode($v)} {
 769            set tok {}
 770        } else {
 771            set tok $oldtok
 772        }
 773        set ka 0
 774        set kid [last_real_child $v,$id]
 775        if {$kid ne {}} {
 776            set k $varcid($v,$kid)
 777            if {[string compare [lindex $varctok($v) $k] $tok] > 0} {
 778                set ki $kid
 779                set ka $k
 780                set tok [lindex $varctok($v) $k]
 781            }
 782        }
 783        if {$ka != 0} {
 784            set i [lsearch -exact $parents($v,$ki) $id]
 785            set j [expr {[llength $parents($v,$ki)] - 1 - $i}]
 786            append tok [strrep $j]
 787        }
 788        if {$tok eq $oldtok} {
 789            continue
 790        }
 791        set id [lindex $varccommits($v,$a) end]
 792        foreach p $parents($v,$id) {
 793            if {[info exists varcid($v,$p)]} {
 794                set kidchanged($varcid($v,$p)) 1
 795            } else {
 796                set sortkids($p) 1
 797            }
 798        }
 799        lset varctok($v) $a $tok
 800        set b [lindex $vupptr($v) $a]
 801        if {$b != $ka} {
 802            if {[string compare [lindex $varctok($v) $ka] $vtokmod($v)] < 0} {
 803                modify_arc $v $ka
 804            }
 805            if {[string compare [lindex $varctok($v) $b] $vtokmod($v)] < 0} {
 806                modify_arc $v $b
 807            }
 808            set c [lindex $vbackptr($v) $a]
 809            set d [lindex $vleftptr($v) $a]
 810            if {$c == 0} {
 811                lset vdownptr($v) $b $d
 812            } else {
 813                lset vleftptr($v) $c $d
 814            }
 815            if {$d != 0} {
 816                lset vbackptr($v) $d $c
 817            }
 818            if {[lindex $vlastins($v) $b] == $a} {
 819                lset vlastins($v) $b $c
 820            }
 821            lset vupptr($v) $a $ka
 822            set c [lindex $vlastins($v) $ka]
 823            if {$c == 0 || \
 824                    [string compare $tok [lindex $varctok($v) $c]] < 0} {
 825                set c $ka
 826                set b [lindex $vdownptr($v) $ka]
 827            } else {
 828                set b [lindex $vleftptr($v) $c]
 829            }
 830            while {$b != 0 && \
 831                      [string compare $tok [lindex $varctok($v) $b]] >= 0} {
 832                set c $b
 833                set b [lindex $vleftptr($v) $c]
 834            }
 835            if {$c == $ka} {
 836                lset vdownptr($v) $ka $a
 837                lset vbackptr($v) $a 0
 838            } else {
 839                lset vleftptr($v) $c $a
 840                lset vbackptr($v) $a $c
 841            }
 842            lset vleftptr($v) $a $b
 843            if {$b != 0} {
 844                lset vbackptr($v) $b $a
 845            }
 846            lset vlastins($v) $ka $a
 847        }
 848    }
 849    foreach id [array names sortkids] {
 850        if {[llength $children($v,$id)] > 1} {
 851            set children($v,$id) [lsort -command [list vtokcmp $v] \
 852                                      $children($v,$id)]
 853        }
 854    }
 855    set t2 [clock clicks -milliseconds]
 856    #puts "renumbervarc did [llength $todo] of $ntot arcs in [expr {$t2-$t1}]ms"
 857}
 858
 859# Fix up the graph after we have found out that in view $v,
 860# $p (a commit that we have already seen) is actually the parent
 861# of the last commit in arc $a.
 862proc fix_reversal {p a v} {
 863    global varcid varcstart varctok vupptr
 864
 865    set pa $varcid($v,$p)
 866    if {$p ne [lindex $varcstart($v) $pa]} {
 867        splitvarc $p $v
 868        set pa $varcid($v,$p)
 869    }
 870    # seeds always need to be renumbered
 871    if {[lindex $vupptr($v) $pa] == 0 ||
 872        [string compare [lindex $varctok($v) $a] \
 873             [lindex $varctok($v) $pa]] > 0} {
 874        renumbervarc $pa $v
 875    }
 876}
 877
 878proc insertrow {id p v} {
 879    global cmitlisted children parents varcid varctok vtokmod
 880    global varccommits ordertok commitidx numcommits curview
 881    global targetid targetrow
 882
 883    readcommit $id
 884    set vid $v,$id
 885    set cmitlisted($vid) 1
 886    set children($vid) {}
 887    set parents($vid) [list $p]
 888    set a [newvarc $v $id]
 889    set varcid($vid) $a
 890    if {[string compare [lindex $varctok($v) $a] $vtokmod($v)] < 0} {
 891        modify_arc $v $a
 892    }
 893    lappend varccommits($v,$a) $id
 894    set vp $v,$p
 895    if {[llength [lappend children($vp) $id]] > 1} {
 896        set children($vp) [lsort -command [list vtokcmp $v] $children($vp)]
 897        catch {unset ordertok}
 898    }
 899    fix_reversal $p $a $v
 900    incr commitidx($v)
 901    if {$v == $curview} {
 902        set numcommits $commitidx($v)
 903        setcanvscroll
 904        if {[info exists targetid]} {
 905            if {![comes_before $targetid $p]} {
 906                incr targetrow
 907            }
 908        }
 909    }
 910}
 911
 912proc insertfakerow {id p} {
 913    global varcid varccommits parents children cmitlisted
 914    global commitidx varctok vtokmod targetid targetrow curview numcommits
 915
 916    set v $curview
 917    set a $varcid($v,$p)
 918    set i [lsearch -exact $varccommits($v,$a) $p]
 919    if {$i < 0} {
 920        puts "oops: insertfakerow can't find [shortids $p] on arc $a"
 921        return
 922    }
 923    set children($v,$id) {}
 924    set parents($v,$id) [list $p]
 925    set varcid($v,$id) $a
 926    lappend children($v,$p) $id
 927    set cmitlisted($v,$id) 1
 928    set numcommits [incr commitidx($v)]
 929    # note we deliberately don't update varcstart($v) even if $i == 0
 930    set varccommits($v,$a) [linsert $varccommits($v,$a) $i $id]
 931    modify_arc $v $a $i
 932    if {[info exists targetid]} {
 933        if {![comes_before $targetid $p]} {
 934            incr targetrow
 935        }
 936    }
 937    setcanvscroll
 938    drawvisible
 939}
 940
 941proc removefakerow {id} {
 942    global varcid varccommits parents children commitidx
 943    global varctok vtokmod cmitlisted currentid selectedline
 944    global targetid curview numcommits
 945
 946    set v $curview
 947    if {[llength $parents($v,$id)] != 1} {
 948        puts "oops: removefakerow [shortids $id] has [llength $parents($v,$id)] parents"
 949        return
 950    }
 951    set p [lindex $parents($v,$id) 0]
 952    set a $varcid($v,$id)
 953    set i [lsearch -exact $varccommits($v,$a) $id]
 954    if {$i < 0} {
 955        puts "oops: removefakerow can't find [shortids $id] on arc $a"
 956        return
 957    }
 958    unset varcid($v,$id)
 959    set varccommits($v,$a) [lreplace $varccommits($v,$a) $i $i]
 960    unset parents($v,$id)
 961    unset children($v,$id)
 962    unset cmitlisted($v,$id)
 963    set numcommits [incr commitidx($v) -1]
 964    set j [lsearch -exact $children($v,$p) $id]
 965    if {$j >= 0} {
 966        set children($v,$p) [lreplace $children($v,$p) $j $j]
 967    }
 968    modify_arc $v $a $i
 969    if {[info exist currentid] && $id eq $currentid} {
 970        unset currentid
 971        set selectedline {}
 972    }
 973    if {[info exists targetid] && $targetid eq $id} {
 974        set targetid $p
 975    }
 976    setcanvscroll
 977    drawvisible
 978}
 979
 980proc first_real_child {vp} {
 981    global children nullid nullid2
 982
 983    foreach id $children($vp) {
 984        if {$id ne $nullid && $id ne $nullid2} {
 985            return $id
 986        }
 987    }
 988    return {}
 989}
 990
 991proc last_real_child {vp} {
 992    global children nullid nullid2
 993
 994    set kids $children($vp)
 995    for {set i [llength $kids]} {[incr i -1] >= 0} {} {
 996        set id [lindex $kids $i]
 997        if {$id ne $nullid && $id ne $nullid2} {
 998            return $id
 999        }
1000    }
1001    return {}
1002}
1003
1004proc vtokcmp {v a b} {
1005    global varctok varcid
1006
1007    return [string compare [lindex $varctok($v) $varcid($v,$a)] \
1008                [lindex $varctok($v) $varcid($v,$b)]]
1009}
1010
1011# This assumes that if lim is not given, the caller has checked that
1012# arc a's token is less than $vtokmod($v)
1013proc modify_arc {v a {lim {}}} {
1014    global varctok vtokmod varcmod varcrow vupptr curview vrowmod varccommits
1015
1016    if {$lim ne {}} {
1017        set c [string compare [lindex $varctok($v) $a] $vtokmod($v)]
1018        if {$c > 0} return
1019        if {$c == 0} {
1020            set r [lindex $varcrow($v) $a]
1021            if {$r ne {} && $vrowmod($v) <= $r + $lim} return
1022        }
1023    }
1024    set vtokmod($v) [lindex $varctok($v) $a]
1025    set varcmod($v) $a
1026    if {$v == $curview} {
1027        while {$a != 0 && [lindex $varcrow($v) $a] eq {}} {
1028            set a [lindex $vupptr($v) $a]
1029            set lim {}
1030        }
1031        set r 0
1032        if {$a != 0} {
1033            if {$lim eq {}} {
1034                set lim [llength $varccommits($v,$a)]
1035            }
1036            set r [expr {[lindex $varcrow($v) $a] + $lim}]
1037        }
1038        set vrowmod($v) $r
1039        undolayout $r
1040    }
1041}
1042
1043proc update_arcrows {v} {
1044    global vtokmod varcmod vrowmod varcrow commitidx currentid selectedline
1045    global varcid vrownum varcorder varcix varccommits
1046    global vupptr vdownptr vleftptr varctok
1047    global displayorder parentlist curview cached_commitrow
1048
1049    if {$vrowmod($v) == $commitidx($v)} return
1050    if {$v == $curview} {
1051        if {[llength $displayorder] > $vrowmod($v)} {
1052            set displayorder [lrange $displayorder 0 [expr {$vrowmod($v) - 1}]]
1053            set parentlist [lrange $parentlist 0 [expr {$vrowmod($v) - 1}]]
1054        }
1055        catch {unset cached_commitrow}
1056    }
1057    set narctot [expr {[llength $varctok($v)] - 1}]
1058    set a $varcmod($v)
1059    while {$a != 0 && [lindex $varcix($v) $a] eq {}} {
1060        # go up the tree until we find something that has a row number,
1061        # or we get to a seed
1062        set a [lindex $vupptr($v) $a]
1063    }
1064    if {$a == 0} {
1065        set a [lindex $vdownptr($v) 0]
1066        if {$a == 0} return
1067        set vrownum($v) {0}
1068        set varcorder($v) [list $a]
1069        lset varcix($v) $a 0
1070        lset varcrow($v) $a 0
1071        set arcn 0
1072        set row 0
1073    } else {
1074        set arcn [lindex $varcix($v) $a]
1075        if {[llength $vrownum($v)] > $arcn + 1} {
1076            set vrownum($v) [lrange $vrownum($v) 0 $arcn]
1077            set varcorder($v) [lrange $varcorder($v) 0 $arcn]
1078        }
1079        set row [lindex $varcrow($v) $a]
1080    }
1081    while {1} {
1082        set p $a
1083        incr row [llength $varccommits($v,$a)]
1084        # go down if possible
1085        set b [lindex $vdownptr($v) $a]
1086        if {$b == 0} {
1087            # if not, go left, or go up until we can go left
1088            while {$a != 0} {
1089                set b [lindex $vleftptr($v) $a]
1090                if {$b != 0} break
1091                set a [lindex $vupptr($v) $a]
1092            }
1093            if {$a == 0} break
1094        }
1095        set a $b
1096        incr arcn
1097        lappend vrownum($v) $row
1098        lappend varcorder($v) $a
1099        lset varcix($v) $a $arcn
1100        lset varcrow($v) $a $row
1101    }
1102    set vtokmod($v) [lindex $varctok($v) $p]
1103    set varcmod($v) $p
1104    set vrowmod($v) $row
1105    if {[info exists currentid]} {
1106        set selectedline [rowofcommit $currentid]
1107    }
1108}
1109
1110# Test whether view $v contains commit $id
1111proc commitinview {id v} {
1112    global varcid
1113
1114    return [info exists varcid($v,$id)]
1115}
1116
1117# Return the row number for commit $id in the current view
1118proc rowofcommit {id} {
1119    global varcid varccommits varcrow curview cached_commitrow
1120    global varctok vtokmod
1121
1122    set v $curview
1123    if {![info exists varcid($v,$id)]} {
1124        puts "oops rowofcommit no arc for [shortids $id]"
1125        return {}
1126    }
1127    set a $varcid($v,$id)
1128    if {[string compare [lindex $varctok($v) $a] $vtokmod($v)] >= 0} {
1129        update_arcrows $v
1130    }
1131    if {[info exists cached_commitrow($id)]} {
1132        return $cached_commitrow($id)
1133    }
1134    set i [lsearch -exact $varccommits($v,$a) $id]
1135    if {$i < 0} {
1136        puts "oops didn't find commit [shortids $id] in arc $a"
1137        return {}
1138    }
1139    incr i [lindex $varcrow($v) $a]
1140    set cached_commitrow($id) $i
1141    return $i
1142}
1143
1144# Returns 1 if a is on an earlier row than b, otherwise 0
1145proc comes_before {a b} {
1146    global varcid varctok curview
1147
1148    set v $curview
1149    if {$a eq $b || ![info exists varcid($v,$a)] || \
1150            ![info exists varcid($v,$b)]} {
1151        return 0
1152    }
1153    if {$varcid($v,$a) != $varcid($v,$b)} {
1154        return [expr {[string compare [lindex $varctok($v) $varcid($v,$a)] \
1155                           [lindex $varctok($v) $varcid($v,$b)]] < 0}]
1156    }
1157    return [expr {[rowofcommit $a] < [rowofcommit $b]}]
1158}
1159
1160proc bsearch {l elt} {
1161    if {[llength $l] == 0 || $elt <= [lindex $l 0]} {
1162        return 0
1163    }
1164    set lo 0
1165    set hi [llength $l]
1166    while {$hi - $lo > 1} {
1167        set mid [expr {int(($lo + $hi) / 2)}]
1168        set t [lindex $l $mid]
1169        if {$elt < $t} {
1170            set hi $mid
1171        } elseif {$elt > $t} {
1172            set lo $mid
1173        } else {
1174            return $mid
1175        }
1176    }
1177    return $lo
1178}
1179
1180# Make sure rows $start..$end-1 are valid in displayorder and parentlist
1181proc make_disporder {start end} {
1182    global vrownum curview commitidx displayorder parentlist
1183    global varccommits varcorder parents vrowmod varcrow
1184    global d_valid_start d_valid_end
1185
1186    if {$end > $vrowmod($curview)} {
1187        update_arcrows $curview
1188    }
1189    set ai [bsearch $vrownum($curview) $start]
1190    set start [lindex $vrownum($curview) $ai]
1191    set narc [llength $vrownum($curview)]
1192    for {set r $start} {$ai < $narc && $r < $end} {incr ai} {
1193        set a [lindex $varcorder($curview) $ai]
1194        set l [llength $displayorder]
1195        set al [llength $varccommits($curview,$a)]
1196        if {$l < $r + $al} {
1197            if {$l < $r} {
1198                set pad [ntimes [expr {$r - $l}] {}]
1199                set displayorder [concat $displayorder $pad]
1200                set parentlist [concat $parentlist $pad]
1201            } elseif {$l > $r} {
1202                set displayorder [lrange $displayorder 0 [expr {$r - 1}]]
1203                set parentlist [lrange $parentlist 0 [expr {$r - 1}]]
1204            }
1205            foreach id $varccommits($curview,$a) {
1206                lappend displayorder $id
1207                lappend parentlist $parents($curview,$id)
1208            }
1209        } elseif {[lindex $displayorder [expr {$r + $al - 1}]] eq {}} {
1210            set i $r
1211            foreach id $varccommits($curview,$a) {
1212                lset displayorder $i $id
1213                lset parentlist $i $parents($curview,$id)
1214                incr i
1215            }
1216        }
1217        incr r $al
1218    }
1219}
1220
1221proc commitonrow {row} {
1222    global displayorder
1223
1224    set id [lindex $displayorder $row]
1225    if {$id eq {}} {
1226        make_disporder $row [expr {$row + 1}]
1227        set id [lindex $displayorder $row]
1228    }
1229    return $id
1230}
1231
1232proc closevarcs {v} {
1233    global varctok varccommits varcid parents children
1234    global cmitlisted commitidx commitinterest vtokmod
1235
1236    set missing_parents 0
1237    set scripts {}
1238    set narcs [llength $varctok($v)]
1239    for {set a 1} {$a < $narcs} {incr a} {
1240        set id [lindex $varccommits($v,$a) end]
1241        foreach p $parents($v,$id) {
1242            if {[info exists varcid($v,$p)]} continue
1243            # add p as a new commit
1244            incr missing_parents
1245            set cmitlisted($v,$p) 0
1246            set parents($v,$p) {}
1247            if {[llength $children($v,$p)] == 1 &&
1248                [llength $parents($v,$id)] == 1} {
1249                set b $a
1250            } else {
1251                set b [newvarc $v $p]
1252            }
1253            set varcid($v,$p) $b
1254            if {[string compare [lindex $varctok($v) $b] $vtokmod($v)] < 0} {
1255                modify_arc $v $b
1256            }
1257            lappend varccommits($v,$b) $p
1258            incr commitidx($v)
1259            if {[info exists commitinterest($p)]} {
1260                foreach script $commitinterest($p) {
1261                    lappend scripts [string map [list "%I" $p] $script]
1262                }
1263                unset commitinterest($id)
1264            }
1265        }
1266    }
1267    if {$missing_parents > 0} {
1268        foreach s $scripts {
1269            eval $s
1270        }
1271    }
1272}
1273
1274# Use $rwid as a substitute for $id, i.e. reparent $id's children to $rwid
1275# Assumes we already have an arc for $rwid.
1276proc rewrite_commit {v id rwid} {
1277    global children parents varcid varctok vtokmod varccommits
1278
1279    foreach ch $children($v,$id) {
1280        # make $rwid be $ch's parent in place of $id
1281        set i [lsearch -exact $parents($v,$ch) $id]
1282        if {$i < 0} {
1283            puts "oops rewrite_commit didn't find $id in parent list for $ch"
1284        }
1285        set parents($v,$ch) [lreplace $parents($v,$ch) $i $i $rwid]
1286        # add $ch to $rwid's children and sort the list if necessary
1287        if {[llength [lappend children($v,$rwid) $ch]] > 1} {
1288            set children($v,$rwid) [lsort -command [list vtokcmp $v] \
1289                                        $children($v,$rwid)]
1290        }
1291        # fix the graph after joining $id to $rwid
1292        set a $varcid($v,$ch)
1293        fix_reversal $rwid $a $v
1294        # parentlist is wrong for the last element of arc $a
1295        # even if displayorder is right, hence the 3rd arg here
1296        modify_arc $v $a [expr {[llength $varccommits($v,$a)] - 1}]
1297    }
1298}
1299
1300proc getcommitlines {fd inst view updating}  {
1301    global cmitlisted commitinterest leftover
1302    global commitidx commitdata vdatemode
1303    global parents children curview hlview
1304    global idpending ordertok
1305    global varccommits varcid varctok vtokmod vfilelimit
1306
1307    set stuff [read $fd 500000]
1308    # git log doesn't terminate the last commit with a null...
1309    if {$stuff == {} && $leftover($inst) ne {} && [eof $fd]} {
1310        set stuff "\0"
1311    }
1312    if {$stuff == {}} {
1313        if {![eof $fd]} {
1314            return 1
1315        }
1316        global commfd viewcomplete viewactive viewname
1317        global viewinstances
1318        unset commfd($inst)
1319        set i [lsearch -exact $viewinstances($view) $inst]
1320        if {$i >= 0} {
1321            set viewinstances($view) [lreplace $viewinstances($view) $i $i]
1322        }
1323        # set it blocking so we wait for the process to terminate
1324        fconfigure $fd -blocking 1
1325        if {[catch {close $fd} err]} {
1326            set fv {}
1327            if {$view != $curview} {
1328                set fv " for the \"$viewname($view)\" view"
1329            }
1330            if {[string range $err 0 4] == "usage"} {
1331                set err "Gitk: error reading commits$fv:\
1332                        bad arguments to git log."
1333                if {$viewname($view) eq "Command line"} {
1334                    append err \
1335                        "  (Note: arguments to gitk are passed to git log\
1336                         to allow selection of commits to be displayed.)"
1337                }
1338            } else {
1339                set err "Error reading commits$fv: $err"
1340            }
1341            error_popup $err
1342        }
1343        if {[incr viewactive($view) -1] <= 0} {
1344            set viewcomplete($view) 1
1345            # Check if we have seen any ids listed as parents that haven't
1346            # appeared in the list
1347            closevarcs $view
1348            notbusy $view
1349        }
1350        if {$view == $curview} {
1351            run chewcommits
1352        }
1353        return 0
1354    }
1355    set start 0
1356    set gotsome 0
1357    set scripts {}
1358    while 1 {
1359        set i [string first "\0" $stuff $start]
1360        if {$i < 0} {
1361            append leftover($inst) [string range $stuff $start end]
1362            break
1363        }
1364        if {$start == 0} {
1365            set cmit $leftover($inst)
1366            append cmit [string range $stuff 0 [expr {$i - 1}]]
1367            set leftover($inst) {}
1368        } else {
1369            set cmit [string range $stuff $start [expr {$i - 1}]]
1370        }
1371        set start [expr {$i + 1}]
1372        set j [string first "\n" $cmit]
1373        set ok 0
1374        set listed 1
1375        if {$j >= 0 && [string match "commit *" $cmit]} {
1376            set ids [string range $cmit 7 [expr {$j - 1}]]
1377            if {[string match {[-^<>]*} $ids]} {
1378                switch -- [string index $ids 0] {
1379                    "-" {set listed 0}
1380                    "^" {set listed 2}
1381                    "<" {set listed 3}
1382                    ">" {set listed 4}
1383                }
1384                set ids [string range $ids 1 end]
1385            }
1386            set ok 1
1387            foreach id $ids {
1388                if {[string length $id] != 40} {
1389                    set ok 0
1390                    break
1391                }
1392            }
1393        }
1394        if {!$ok} {
1395            set shortcmit $cmit
1396            if {[string length $shortcmit] > 80} {
1397                set shortcmit "[string range $shortcmit 0 80]..."
1398            }
1399            error_popup "[mc "Can't parse git log output:"] {$shortcmit}"
1400            exit 1
1401        }
1402        set id [lindex $ids 0]
1403        set vid $view,$id
1404
1405        if {!$listed && $updating && ![info exists varcid($vid)] &&
1406            $vfilelimit($view) ne {}} {
1407            # git log doesn't rewrite parents for unlisted commits
1408            # when doing path limiting, so work around that here
1409            # by working out the rewritten parent with git rev-list
1410            # and if we already know about it, using the rewritten
1411            # parent as a substitute parent for $id's children.
1412            if {![catch {
1413                set rwid [exec git rev-list --first-parent --max-count=1 \
1414                              $id -- $vfilelimit($view)]
1415            }]} {
1416                if {$rwid ne {} && [info exists varcid($view,$rwid)]} {
1417                    # use $rwid in place of $id
1418                    rewrite_commit $view $id $rwid
1419                    continue
1420                }
1421            }
1422        }
1423
1424        set a 0
1425        if {[info exists varcid($vid)]} {
1426            if {$cmitlisted($vid) || !$listed} continue
1427            set a $varcid($vid)
1428        }
1429        if {$listed} {
1430            set olds [lrange $ids 1 end]
1431        } else {
1432            set olds {}
1433        }
1434        set commitdata($id) [string range $cmit [expr {$j + 1}] end]
1435        set cmitlisted($vid) $listed
1436        set parents($vid) $olds
1437        if {![info exists children($vid)]} {
1438            set children($vid) {}
1439        } elseif {$a == 0 && [llength $children($vid)] == 1} {
1440            set k [lindex $children($vid) 0]
1441            if {[llength $parents($view,$k)] == 1 &&
1442                (!$vdatemode($view) ||
1443                 $varcid($view,$k) == [llength $varctok($view)] - 1)} {
1444                set a $varcid($view,$k)
1445            }
1446        }
1447        if {$a == 0} {
1448            # new arc
1449            set a [newvarc $view $id]
1450        }
1451        if {[string compare [lindex $varctok($view) $a] $vtokmod($view)] < 0} {
1452            modify_arc $view $a
1453        }
1454        if {![info exists varcid($vid)]} {
1455            set varcid($vid) $a
1456            lappend varccommits($view,$a) $id
1457            incr commitidx($view)
1458        }
1459
1460        set i 0
1461        foreach p $olds {
1462            if {$i == 0 || [lsearch -exact $olds $p] >= $i} {
1463                set vp $view,$p
1464                if {[llength [lappend children($vp) $id]] > 1 &&
1465                    [vtokcmp $view [lindex $children($vp) end-1] $id] > 0} {
1466                    set children($vp) [lsort -command [list vtokcmp $view] \
1467                                           $children($vp)]
1468                    catch {unset ordertok}
1469                }
1470                if {[info exists varcid($view,$p)]} {
1471                    fix_reversal $p $a $view
1472                }
1473            }
1474            incr i
1475        }
1476
1477        if {[info exists commitinterest($id)]} {
1478            foreach script $commitinterest($id) {
1479                lappend scripts [string map [list "%I" $id] $script]
1480            }
1481            unset commitinterest($id)
1482        }
1483        set gotsome 1
1484    }
1485    if {$gotsome} {
1486        global numcommits hlview
1487
1488        if {$view == $curview} {
1489            set numcommits $commitidx($view)
1490            run chewcommits
1491        }
1492        if {[info exists hlview] && $view == $hlview} {
1493            # we never actually get here...
1494            run vhighlightmore
1495        }
1496        foreach s $scripts {
1497            eval $s
1498        }
1499    }
1500    return 2
1501}
1502
1503proc chewcommits {} {
1504    global curview hlview viewcomplete
1505    global pending_select
1506
1507    layoutmore
1508    if {$viewcomplete($curview)} {
1509        global commitidx varctok
1510        global numcommits startmsecs
1511
1512        if {[info exists pending_select]} {
1513            update
1514            reset_pending_select {}
1515
1516            if {[commitinview $pending_select $curview]} {
1517                selectline [rowofcommit $pending_select] 1
1518            } else {
1519                set row [first_real_row]
1520                selectline $row 1
1521            }
1522        }
1523        if {$commitidx($curview) > 0} {
1524            #set ms [expr {[clock clicks -milliseconds] - $startmsecs}]
1525            #puts "overall $ms ms for $numcommits commits"
1526            #puts "[llength $varctok($view)] arcs, $commitidx($view) commits"
1527        } else {
1528            show_status [mc "No commits selected"]
1529        }
1530        notbusy layout
1531    }
1532    return 0
1533}
1534
1535proc readcommit {id} {
1536    if {[catch {set contents [exec git cat-file commit $id]}]} return
1537    parsecommit $id $contents 0
1538}
1539
1540proc parsecommit {id contents listed} {
1541    global commitinfo cdate
1542
1543    set inhdr 1
1544    set comment {}
1545    set headline {}
1546    set auname {}
1547    set audate {}
1548    set comname {}
1549    set comdate {}
1550    set hdrend [string first "\n\n" $contents]
1551    if {$hdrend < 0} {
1552        # should never happen...
1553        set hdrend [string length $contents]
1554    }
1555    set header [string range $contents 0 [expr {$hdrend - 1}]]
1556    set comment [string range $contents [expr {$hdrend + 2}] end]
1557    foreach line [split $header "\n"] {
1558        set tag [lindex $line 0]
1559        if {$tag == "author"} {
1560            set audate [lindex $line end-1]
1561            set auname [lrange $line 1 end-2]
1562        } elseif {$tag == "committer"} {
1563            set comdate [lindex $line end-1]
1564            set comname [lrange $line 1 end-2]
1565        }
1566    }
1567    set headline {}
1568    # take the first non-blank line of the comment as the headline
1569    set headline [string trimleft $comment]
1570    set i [string first "\n" $headline]
1571    if {$i >= 0} {
1572        set headline [string range $headline 0 $i]
1573    }
1574    set headline [string trimright $headline]
1575    set i [string first "\r" $headline]
1576    if {$i >= 0} {
1577        set headline [string trimright [string range $headline 0 $i]]
1578    }
1579    if {!$listed} {
1580        # git log indents the comment by 4 spaces;
1581        # if we got this via git cat-file, add the indentation
1582        set newcomment {}
1583        foreach line [split $comment "\n"] {
1584            append newcomment "    "
1585            append newcomment $line
1586            append newcomment "\n"
1587        }
1588        set comment $newcomment
1589    }
1590    if {$comdate != {}} {
1591        set cdate($id) $comdate
1592    }
1593    set commitinfo($id) [list $headline $auname $audate \
1594                             $comname $comdate $comment]
1595}
1596
1597proc getcommit {id} {
1598    global commitdata commitinfo
1599
1600    if {[info exists commitdata($id)]} {
1601        parsecommit $id $commitdata($id) 1
1602    } else {
1603        readcommit $id
1604        if {![info exists commitinfo($id)]} {
1605            set commitinfo($id) [list [mc "No commit information available"]]
1606        }
1607    }
1608    return 1
1609}
1610
1611proc readrefs {} {
1612    global tagids idtags headids idheads tagobjid
1613    global otherrefids idotherrefs mainhead mainheadid
1614    global selecthead selectheadid
1615
1616    foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
1617        catch {unset $v}
1618    }
1619    set refd [open [list | git show-ref -d] r]
1620    while {[gets $refd line] >= 0} {
1621        if {[string index $line 40] ne " "} continue
1622        set id [string range $line 0 39]
1623        set ref [string range $line 41 end]
1624        if {![string match "refs/*" $ref]} continue
1625        set name [string range $ref 5 end]
1626        if {[string match "remotes/*" $name]} {
1627            if {![string match "*/HEAD" $name]} {
1628                set headids($name) $id
1629                lappend idheads($id) $name
1630            }
1631        } elseif {[string match "heads/*" $name]} {
1632            set name [string range $name 6 end]
1633            set headids($name) $id
1634            lappend idheads($id) $name
1635        } elseif {[string match "tags/*" $name]} {
1636            # this lets refs/tags/foo^{} overwrite refs/tags/foo,
1637            # which is what we want since the former is the commit ID
1638            set name [string range $name 5 end]
1639            if {[string match "*^{}" $name]} {
1640                set name [string range $name 0 end-3]
1641            } else {
1642                set tagobjid($name) $id
1643            }
1644            set tagids($name) $id
1645            lappend idtags($id) $name
1646        } else {
1647            set otherrefids($name) $id
1648            lappend idotherrefs($id) $name
1649        }
1650    }
1651    catch {close $refd}
1652    set mainhead {}
1653    set mainheadid {}
1654    catch {
1655        set mainheadid [exec git rev-parse HEAD]
1656        set thehead [exec git symbolic-ref HEAD]
1657        if {[string match "refs/heads/*" $thehead]} {
1658            set mainhead [string range $thehead 11 end]
1659        }
1660    }
1661    set selectheadid {}
1662    if {$selecthead ne {}} {
1663        catch {
1664            set selectheadid [exec git rev-parse --verify $selecthead]
1665        }
1666    }
1667}
1668
1669# skip over fake commits
1670proc first_real_row {} {
1671    global nullid nullid2 numcommits
1672
1673    for {set row 0} {$row < $numcommits} {incr row} {
1674        set id [commitonrow $row]
1675        if {$id ne $nullid && $id ne $nullid2} {
1676            break
1677        }
1678    }
1679    return $row
1680}
1681
1682# update things for a head moved to a child of its previous location
1683proc movehead {id name} {
1684    global headids idheads
1685
1686    removehead $headids($name) $name
1687    set headids($name) $id
1688    lappend idheads($id) $name
1689}
1690
1691# update things when a head has been removed
1692proc removehead {id name} {
1693    global headids idheads
1694
1695    if {$idheads($id) eq $name} {
1696        unset idheads($id)
1697    } else {
1698        set i [lsearch -exact $idheads($id) $name]
1699        if {$i >= 0} {
1700            set idheads($id) [lreplace $idheads($id) $i $i]
1701        }
1702    }
1703    unset headids($name)
1704}
1705
1706proc show_error {w top msg} {
1707    message $w.m -text $msg -justify center -aspect 400
1708    pack $w.m -side top -fill x -padx 20 -pady 20
1709    button $w.ok -text [mc OK] -command "destroy $top"
1710    pack $w.ok -side bottom -fill x
1711    bind $top <Visibility> "grab $top; focus $top"
1712    bind $top <Key-Return> "destroy $top"
1713    tkwait window $top
1714}
1715
1716proc error_popup msg {
1717    set w .error
1718    toplevel $w
1719    wm transient $w .
1720    show_error $w $w $msg
1721}
1722
1723proc confirm_popup msg {
1724    global confirm_ok
1725    set confirm_ok 0
1726    set w .confirm
1727    toplevel $w
1728    wm transient $w .
1729    message $w.m -text $msg -justify center -aspect 400
1730    pack $w.m -side top -fill x -padx 20 -pady 20
1731    button $w.ok -text [mc OK] -command "set confirm_ok 1; destroy $w"
1732    pack $w.ok -side left -fill x
1733    button $w.cancel -text [mc Cancel] -command "destroy $w"
1734    pack $w.cancel -side right -fill x
1735    bind $w <Visibility> "grab $w; focus $w"
1736    tkwait window $w
1737    return $confirm_ok
1738}
1739
1740proc setoptions {} {
1741    option add *Panedwindow.showHandle 1 startupFile
1742    option add *Panedwindow.sashRelief raised startupFile
1743    option add *Button.font uifont startupFile
1744    option add *Checkbutton.font uifont startupFile
1745    option add *Radiobutton.font uifont startupFile
1746    option add *Menu.font uifont startupFile
1747    option add *Menubutton.font uifont startupFile
1748    option add *Label.font uifont startupFile
1749    option add *Message.font uifont startupFile
1750    option add *Entry.font uifont startupFile
1751}
1752
1753proc makewindow {} {
1754    global canv canv2 canv3 linespc charspc ctext cflist cscroll
1755    global tabstop
1756    global findtype findtypemenu findloc findstring fstring geometry
1757    global entries sha1entry sha1string sha1but
1758    global diffcontextstring diffcontext
1759    global ignorespace
1760    global maincursor textcursor curtextcursor
1761    global rowctxmenu fakerowmenu mergemax wrapcomment
1762    global highlight_files gdttype
1763    global searchstring sstring
1764    global bgcolor fgcolor bglist fglist diffcolors selectbgcolor
1765    global headctxmenu progresscanv progressitem progresscoords statusw
1766    global fprogitem fprogcoord lastprogupdate progupdatepending
1767    global rprogitem rprogcoord rownumsel numcommits
1768    global have_tk85
1769
1770    menu .bar
1771    .bar add cascade -label [mc "File"] -menu .bar.file
1772    menu .bar.file
1773    .bar.file add command -label [mc "Update"] -command updatecommits
1774    .bar.file add command -label [mc "Reload"] -command reloadcommits
1775    .bar.file add command -label [mc "Reread references"] -command rereadrefs
1776    .bar.file add command -label [mc "List references"] -command showrefs
1777    .bar.file add command -label [mc "Quit"] -command doquit
1778    menu .bar.edit
1779    .bar add cascade -label [mc "Edit"] -menu .bar.edit
1780    .bar.edit add command -label [mc "Preferences"] -command doprefs
1781
1782    menu .bar.view
1783    .bar add cascade -label [mc "View"] -menu .bar.view
1784    .bar.view add command -label [mc "New view..."] -command {newview 0}
1785    .bar.view add command -label [mc "Edit view..."] -command editview \
1786        -state disabled
1787    .bar.view add command -label [mc "Delete view"] -command delview -state disabled
1788    .bar.view add separator
1789    .bar.view add radiobutton -label [mc "All files"] -command {showview 0} \
1790        -variable selectedview -value 0
1791
1792    menu .bar.help
1793    .bar add cascade -label [mc "Help"] -menu .bar.help
1794    .bar.help add command -label [mc "About gitk"] -command about
1795    .bar.help add command -label [mc "Key bindings"] -command keys
1796    .bar.help configure
1797    . configure -menu .bar
1798
1799    # the gui has upper and lower half, parts of a paned window.
1800    panedwindow .ctop -orient vertical
1801
1802    # possibly use assumed geometry
1803    if {![info exists geometry(pwsash0)]} {
1804        set geometry(topheight) [expr {15 * $linespc}]
1805        set geometry(topwidth) [expr {80 * $charspc}]
1806        set geometry(botheight) [expr {15 * $linespc}]
1807        set geometry(botwidth) [expr {50 * $charspc}]
1808        set geometry(pwsash0) "[expr {40 * $charspc}] 2"
1809        set geometry(pwsash1) "[expr {60 * $charspc}] 2"
1810    }
1811
1812    # the upper half will have a paned window, a scroll bar to the right, and some stuff below
1813    frame .tf -height $geometry(topheight) -width $geometry(topwidth)
1814    frame .tf.histframe
1815    panedwindow .tf.histframe.pwclist -orient horizontal -sashpad 0 -handlesize 4
1816
1817    # create three canvases
1818    set cscroll .tf.histframe.csb
1819    set canv .tf.histframe.pwclist.canv
1820    canvas $canv \
1821        -selectbackground $selectbgcolor \
1822        -background $bgcolor -bd 0 \
1823        -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll"
1824    .tf.histframe.pwclist add $canv
1825    set canv2 .tf.histframe.pwclist.canv2
1826    canvas $canv2 \
1827        -selectbackground $selectbgcolor \
1828        -background $bgcolor -bd 0 -yscrollincr $linespc
1829    .tf.histframe.pwclist add $canv2
1830    set canv3 .tf.histframe.pwclist.canv3
1831    canvas $canv3 \
1832        -selectbackground $selectbgcolor \
1833        -background $bgcolor -bd 0 -yscrollincr $linespc
1834    .tf.histframe.pwclist add $canv3
1835    eval .tf.histframe.pwclist sash place 0 $geometry(pwsash0)
1836    eval .tf.histframe.pwclist sash place 1 $geometry(pwsash1)
1837
1838    # a scroll bar to rule them
1839    scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
1840    pack $cscroll -side right -fill y
1841    bind .tf.histframe.pwclist <Configure> {resizeclistpanes %W %w}
1842    lappend bglist $canv $canv2 $canv3
1843    pack .tf.histframe.pwclist -fill both -expand 1 -side left
1844
1845    # we have two button bars at bottom of top frame. Bar 1
1846    frame .tf.bar
1847    frame .tf.lbar -height 15
1848
1849    set sha1entry .tf.bar.sha1
1850    set entries $sha1entry
1851    set sha1but .tf.bar.sha1label
1852    button $sha1but -text [mc "SHA1 ID: "] -state disabled -relief flat \
1853        -command gotocommit -width 8
1854    $sha1but conf -disabledforeground [$sha1but cget -foreground]
1855    pack .tf.bar.sha1label -side left
1856    entry $sha1entry -width 40 -font textfont -textvariable sha1string
1857    trace add variable sha1string write sha1change
1858    pack $sha1entry -side left -pady 2
1859
1860    image create bitmap bm-left -data {
1861        #define left_width 16
1862        #define left_height 16
1863        static unsigned char left_bits[] = {
1864        0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
1865        0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
1866        0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
1867    }
1868    image create bitmap bm-right -data {
1869        #define right_width 16
1870        #define right_height 16
1871        static unsigned char right_bits[] = {
1872        0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
1873        0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
1874        0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
1875    }
1876    button .tf.bar.leftbut -image bm-left -command goback \
1877        -state disabled -width 26
1878    pack .tf.bar.leftbut -side left -fill y
1879    button .tf.bar.rightbut -image bm-right -command goforw \
1880        -state disabled -width 26
1881    pack .tf.bar.rightbut -side left -fill y
1882
1883    label .tf.bar.rowlabel -text [mc "Row"]
1884    set rownumsel {}
1885    label .tf.bar.rownum -width 7 -font textfont -textvariable rownumsel \
1886        -relief sunken -anchor e
1887    label .tf.bar.rowlabel2 -text "/"
1888    label .tf.bar.numcommits -width 7 -font textfont -textvariable numcommits \
1889        -relief sunken -anchor e
1890    pack .tf.bar.rowlabel .tf.bar.rownum .tf.bar.rowlabel2 .tf.bar.numcommits \
1891        -side left
1892    global selectedline
1893    trace add variable selectedline write selectedline_change
1894
1895    # Status label and progress bar
1896    set statusw .tf.bar.status
1897    label $statusw -width 15 -relief sunken
1898    pack $statusw -side left -padx 5
1899    set h [expr {[font metrics uifont -linespace] + 2}]
1900    set progresscanv .tf.bar.progress
1901    canvas $progresscanv -relief sunken -height $h -borderwidth 2
1902    set progressitem [$progresscanv create rect -1 0 0 $h -fill green]
1903    set fprogitem [$progresscanv create rect -1 0 0 $h -fill yellow]
1904    set rprogitem [$progresscanv create rect -1 0 0 $h -fill red]
1905    pack $progresscanv -side right -expand 1 -fill x
1906    set progresscoords {0 0}
1907    set fprogcoord 0
1908    set rprogcoord 0
1909    bind $progresscanv <Configure> adjustprogress
1910    set lastprogupdate [clock clicks -milliseconds]
1911    set progupdatepending 0
1912
1913    # build up the bottom bar of upper window
1914    label .tf.lbar.flabel -text "[mc "Find"] "
1915    button .tf.lbar.fnext -text [mc "next"] -command {dofind 1 1}
1916    button .tf.lbar.fprev -text [mc "prev"] -command {dofind -1 1}
1917    label .tf.lbar.flab2 -text " [mc "commit"] "
1918    pack .tf.lbar.flabel .tf.lbar.fnext .tf.lbar.fprev .tf.lbar.flab2 \
1919        -side left -fill y
1920    set gdttype [mc "containing:"]
1921    set gm [tk_optionMenu .tf.lbar.gdttype gdttype \
1922                [mc "containing:"] \
1923                [mc "touching paths:"] \
1924                [mc "adding/removing string:"]]
1925    trace add variable gdttype write gdttype_change
1926    pack .tf.lbar.gdttype -side left -fill y
1927
1928    set findstring {}
1929    set fstring .tf.lbar.findstring
1930    lappend entries $fstring
1931    entry $fstring -width 30 -font textfont -textvariable findstring
1932    trace add variable findstring write find_change
1933    set findtype [mc "Exact"]
1934    set findtypemenu [tk_optionMenu .tf.lbar.findtype \
1935                      findtype [mc "Exact"] [mc "IgnCase"] [mc "Regexp"]]
1936    trace add variable findtype write findcom_change
1937    set findloc [mc "All fields"]
1938    tk_optionMenu .tf.lbar.findloc findloc [mc "All fields"] [mc "Headline"] \
1939        [mc "Comments"] [mc "Author"] [mc "Committer"]
1940    trace add variable findloc write find_change
1941    pack .tf.lbar.findloc -side right
1942    pack .tf.lbar.findtype -side right
1943    pack $fstring -side left -expand 1 -fill x
1944
1945    # Finish putting the upper half of the viewer together
1946    pack .tf.lbar -in .tf -side bottom -fill x
1947    pack .tf.bar -in .tf -side bottom -fill x
1948    pack .tf.histframe -fill both -side top -expand 1
1949    .ctop add .tf
1950    .ctop paneconfigure .tf -height $geometry(topheight)
1951    .ctop paneconfigure .tf -width $geometry(topwidth)
1952
1953    # now build up the bottom
1954    panedwindow .pwbottom -orient horizontal
1955
1956    # lower left, a text box over search bar, scroll bar to the right
1957    # if we know window height, then that will set the lower text height, otherwise
1958    # we set lower text height which will drive window height
1959    if {[info exists geometry(main)]} {
1960        frame .bleft -width $geometry(botwidth)
1961    } else {
1962        frame .bleft -width $geometry(botwidth) -height $geometry(botheight)
1963    }
1964    frame .bleft.top
1965    frame .bleft.mid
1966    frame .bleft.bottom
1967
1968    button .bleft.top.search -text [mc "Search"] -command dosearch
1969    pack .bleft.top.search -side left -padx 5
1970    set sstring .bleft.top.sstring
1971    entry $sstring -width 20 -font textfont -textvariable searchstring
1972    lappend entries $sstring
1973    trace add variable searchstring write incrsearch
1974    pack $sstring -side left -expand 1 -fill x
1975    radiobutton .bleft.mid.diff -text [mc "Diff"] \
1976        -command changediffdisp -variable diffelide -value {0 0}
1977    radiobutton .bleft.mid.old -text [mc "Old version"] \
1978        -command changediffdisp -variable diffelide -value {0 1}
1979    radiobutton .bleft.mid.new -text [mc "New version"] \
1980        -command changediffdisp -variable diffelide -value {1 0}
1981    label .bleft.mid.labeldiffcontext -text "      [mc "Lines of context"]: "
1982    pack .bleft.mid.diff .bleft.mid.old .bleft.mid.new -side left
1983    spinbox .bleft.mid.diffcontext -width 5 -font textfont \
1984        -from 1 -increment 1 -to 10000000 \
1985        -validate all -validatecommand "diffcontextvalidate %P" \
1986        -textvariable diffcontextstring
1987    .bleft.mid.diffcontext set $diffcontext
1988    trace add variable diffcontextstring write diffcontextchange
1989    lappend entries .bleft.mid.diffcontext
1990    pack .bleft.mid.labeldiffcontext .bleft.mid.diffcontext -side left
1991    checkbutton .bleft.mid.ignspace -text [mc "Ignore space change"] \
1992        -command changeignorespace -variable ignorespace
1993    pack .bleft.mid.ignspace -side left -padx 5
1994    set ctext .bleft.bottom.ctext
1995    text $ctext -background $bgcolor -foreground $fgcolor \
1996        -state disabled -font textfont \
1997        -yscrollcommand scrolltext -wrap none \
1998        -xscrollcommand ".bleft.bottom.sbhorizontal set"
1999    if {$have_tk85} {
2000        $ctext conf -tabstyle wordprocessor
2001    }
2002    scrollbar .bleft.bottom.sb -command "$ctext yview"
2003    scrollbar .bleft.bottom.sbhorizontal -command "$ctext xview" -orient h \
2004        -width 10
2005    pack .bleft.top -side top -fill x
2006    pack .bleft.mid -side top -fill x
2007    grid $ctext .bleft.bottom.sb -sticky nsew
2008    grid .bleft.bottom.sbhorizontal -sticky ew
2009    grid columnconfigure .bleft.bottom 0 -weight 1
2010    grid rowconfigure .bleft.bottom 0 -weight 1
2011    grid rowconfigure .bleft.bottom 1 -weight 0
2012    pack .bleft.bottom -side top -fill both -expand 1
2013    lappend bglist $ctext
2014    lappend fglist $ctext
2015
2016    $ctext tag conf comment -wrap $wrapcomment
2017    $ctext tag conf filesep -font textfontbold -back "#aaaaaa"
2018    $ctext tag conf hunksep -fore [lindex $diffcolors 2]
2019    $ctext tag conf d0 -fore [lindex $diffcolors 0]
2020    $ctext tag conf d1 -fore [lindex $diffcolors 1]
2021    $ctext tag conf m0 -fore red
2022    $ctext tag conf m1 -fore blue
2023    $ctext tag conf m2 -fore green
2024    $ctext tag conf m3 -fore purple
2025    $ctext tag conf m4 -fore brown
2026    $ctext tag conf m5 -fore "#009090"
2027    $ctext tag conf m6 -fore magenta
2028    $ctext tag conf m7 -fore "#808000"
2029    $ctext tag conf m8 -fore "#009000"
2030    $ctext tag conf m9 -fore "#ff0080"
2031    $ctext tag conf m10 -fore cyan
2032    $ctext tag conf m11 -fore "#b07070"
2033    $ctext tag conf m12 -fore "#70b0f0"
2034    $ctext tag conf m13 -fore "#70f0b0"
2035    $ctext tag conf m14 -fore "#f0b070"
2036    $ctext tag conf m15 -fore "#ff70b0"
2037    $ctext tag conf mmax -fore darkgrey
2038    set mergemax 16
2039    $ctext tag conf mresult -font textfontbold
2040    $ctext tag conf msep -font textfontbold
2041    $ctext tag conf found -back yellow
2042
2043    .pwbottom add .bleft
2044    .pwbottom paneconfigure .bleft -width $geometry(botwidth)
2045
2046    # lower right
2047    frame .bright
2048    frame .bright.mode
2049    radiobutton .bright.mode.patch -text [mc "Patch"] \
2050        -command reselectline -variable cmitmode -value "patch"
2051    radiobutton .bright.mode.tree -text [mc "Tree"] \
2052        -command reselectline -variable cmitmode -value "tree"
2053    grid .bright.mode.patch .bright.mode.tree -sticky ew
2054    pack .bright.mode -side top -fill x
2055    set cflist .bright.cfiles
2056    set indent [font measure mainfont "nn"]
2057    text $cflist \
2058        -selectbackground $selectbgcolor \
2059        -background $bgcolor -foreground $fgcolor \
2060        -font mainfont \
2061        -tabs [list $indent [expr {2 * $indent}]] \
2062        -yscrollcommand ".bright.sb set" \
2063        -cursor [. cget -cursor] \
2064        -spacing1 1 -spacing3 1
2065    lappend bglist $cflist
2066    lappend fglist $cflist
2067    scrollbar .bright.sb -command "$cflist yview"
2068    pack .bright.sb -side right -fill y
2069    pack $cflist -side left -fill both -expand 1
2070    $cflist tag configure highlight \
2071        -background [$cflist cget -selectbackground]
2072    $cflist tag configure bold -font mainfontbold
2073
2074    .pwbottom add .bright
2075    .ctop add .pwbottom
2076
2077    # restore window width & height if known
2078    if {[info exists geometry(main)]} {
2079        if {[scan $geometry(main) "%dx%d" w h] >= 2} {
2080            if {$w > [winfo screenwidth .]} {
2081                set w [winfo screenwidth .]
2082            }
2083            if {$h > [winfo screenheight .]} {
2084                set h [winfo screenheight .]
2085            }
2086            wm geometry . "${w}x$h"
2087        }
2088    }
2089
2090    if {[tk windowingsystem] eq {aqua}} {
2091        set M1B M1
2092    } else {
2093        set M1B Control
2094    }
2095
2096    bind .pwbottom <Configure> {resizecdetpanes %W %w}
2097    pack .ctop -fill both -expand 1
2098    bindall <1> {selcanvline %W %x %y}
2099    #bindall <B1-Motion> {selcanvline %W %x %y}
2100    if {[tk windowingsystem] == "win32"} {
2101        bind . <MouseWheel> { windows_mousewheel_redirector %W %X %Y %D }
2102        bind $ctext <MouseWheel> { windows_mousewheel_redirector %W %X %Y %D ; break }
2103    } else {
2104        bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
2105        bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
2106        if {[tk windowingsystem] eq "aqua"} {
2107            bindall <MouseWheel> {
2108                set delta [expr {- (%D)}]
2109                allcanvs yview scroll $delta units
2110            }
2111        }
2112    }
2113    bindall <2> "canvscan mark %W %x %y"
2114    bindall <B2-Motion> "canvscan dragto %W %x %y"
2115    bindkey <Home> selfirstline
2116    bindkey <End> sellastline
2117    bind . <Key-Up> "selnextline -1"
2118    bind . <Key-Down> "selnextline 1"
2119    bind . <Shift-Key-Up> "dofind -1 0"
2120    bind . <Shift-Key-Down> "dofind 1 0"
2121    bindkey <Key-Right> "goforw"
2122    bindkey <Key-Left> "goback"
2123    bind . <Key-Prior> "selnextpage -1"
2124    bind . <Key-Next> "selnextpage 1"
2125    bind . <$M1B-Home> "allcanvs yview moveto 0.0"
2126    bind . <$M1B-End> "allcanvs yview moveto 1.0"
2127    bind . <$M1B-Key-Up> "allcanvs yview scroll -1 units"
2128    bind . <$M1B-Key-Down> "allcanvs yview scroll 1 units"
2129    bind . <$M1B-Key-Prior> "allcanvs yview scroll -1 pages"
2130    bind . <$M1B-Key-Next> "allcanvs yview scroll 1 pages"
2131    bindkey <Key-Delete> "$ctext yview scroll -1 pages"
2132    bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
2133    bindkey <Key-space> "$ctext yview scroll 1 pages"
2134    bindkey p "selnextline -1"
2135    bindkey n "selnextline 1"
2136    bindkey z "goback"
2137    bindkey x "goforw"
2138    bindkey i "selnextline -1"
2139    bindkey k "selnextline 1"
2140    bindkey j "goback"
2141    bindkey l "goforw"
2142    bindkey b prevfile
2143    bindkey d "$ctext yview scroll 18 units"
2144    bindkey u "$ctext yview scroll -18 units"
2145    bindkey / {dofind 1 1}
2146    bindkey <Key-Return> {dofind 1 1}
2147    bindkey ? {dofind -1 1}
2148    bindkey f nextfile
2149    bindkey <F5> updatecommits
2150    bind . <$M1B-q> doquit
2151    bind . <$M1B-f> {dofind 1 1}
2152    bind . <$M1B-g> {dofind 1 0}
2153    bind . <$M1B-r> dosearchback
2154    bind . <$M1B-s> dosearch
2155    bind . <$M1B-equal> {incrfont 1}
2156    bind . <$M1B-plus> {incrfont 1}
2157    bind . <$M1B-KP_Add> {incrfont 1}
2158    bind . <$M1B-minus> {incrfont -1}
2159    bind . <$M1B-KP_Subtract> {incrfont -1}
2160    wm protocol . WM_DELETE_WINDOW doquit
2161    bind . <Destroy> {stop_backends}
2162    bind . <Button-1> "click %W"
2163    bind $fstring <Key-Return> {dofind 1 1}
2164    bind $sha1entry <Key-Return> gotocommit
2165    bind $sha1entry <<PasteSelection>> clearsha1
2166    bind $cflist <1> {sel_flist %W %x %y; break}
2167    bind $cflist <B1-Motion> {sel_flist %W %x %y; break}
2168    bind $cflist <ButtonRelease-1> {treeclick %W %x %y}
2169    bind $cflist <Button-3> {pop_flist_menu %W %X %Y %x %y}
2170
2171    set maincursor [. cget -cursor]
2172    set textcursor [$ctext cget -cursor]
2173    set curtextcursor $textcursor
2174
2175    set rowctxmenu .rowctxmenu
2176    menu $rowctxmenu -tearoff 0
2177    $rowctxmenu add command -label [mc "Diff this -> selected"] \
2178        -command {diffvssel 0}
2179    $rowctxmenu add command -label [mc "Diff selected -> this"] \
2180        -command {diffvssel 1}
2181    $rowctxmenu add command -label [mc "Make patch"] -command mkpatch
2182    $rowctxmenu add command -label [mc "Create tag"] -command mktag
2183    $rowctxmenu add command -label [mc "Write commit to file"] -command writecommit
2184    $rowctxmenu add command -label [mc "Create new branch"] -command mkbranch
2185    $rowctxmenu add command -label [mc "Cherry-pick this commit"] \
2186        -command cherrypick
2187    $rowctxmenu add command -label [mc "Reset HEAD branch to here"] \
2188        -command resethead
2189
2190    set fakerowmenu .fakerowmenu
2191    menu $fakerowmenu -tearoff 0
2192    $fakerowmenu add command -label [mc "Diff this -> selected"] \
2193        -command {diffvssel 0}
2194    $fakerowmenu add command -label [mc "Diff selected -> this"] \
2195        -command {diffvssel 1}
2196    $fakerowmenu add command -label [mc "Make patch"] -command mkpatch
2197#    $fakerowmenu add command -label [mc "Commit"] -command {mkcommit 0}
2198#    $fakerowmenu add command -label [mc "Commit all"] -command {mkcommit 1}
2199#    $fakerowmenu add command -label [mc "Revert local changes"] -command revertlocal
2200
2201    set headctxmenu .headctxmenu
2202    menu $headctxmenu -tearoff 0
2203    $headctxmenu add command -label [mc "Check out this branch"] \
2204        -command cobranch
2205    $headctxmenu add command -label [mc "Remove this branch"] \
2206        -command rmbranch
2207
2208    global flist_menu
2209    set flist_menu .flistctxmenu
2210    menu $flist_menu -tearoff 0
2211    $flist_menu add command -label [mc "Highlight this too"] \
2212        -command {flist_hl 0}
2213    $flist_menu add command -label [mc "Highlight this only"] \
2214        -command {flist_hl 1}
2215    $flist_menu add command -label [mc "External diff"] \
2216        -command {external_diff}
2217    $flist_menu add command -label [mc "Blame parent commit"] \
2218        -command {external_blame 1}
2219}
2220
2221# Windows sends all mouse wheel events to the current focused window, not
2222# the one where the mouse hovers, so bind those events here and redirect
2223# to the correct window
2224proc windows_mousewheel_redirector {W X Y D} {
2225    global canv canv2 canv3
2226    set w [winfo containing -displayof $W $X $Y]
2227    if {$w ne ""} {
2228        set u [expr {$D < 0 ? 5 : -5}]
2229        if {$w == $canv || $w == $canv2 || $w == $canv3} {
2230            allcanvs yview scroll $u units
2231        } else {
2232            catch {
2233                $w yview scroll $u units
2234            }
2235        }
2236    }
2237}
2238
2239# Update row number label when selectedline changes
2240proc selectedline_change {n1 n2 op} {
2241    global selectedline rownumsel
2242
2243    if {$selectedline eq {}} {
2244        set rownumsel {}
2245    } else {
2246        set rownumsel [expr {$selectedline + 1}]
2247    }
2248}
2249
2250# mouse-2 makes all windows scan vertically, but only the one
2251# the cursor is in scans horizontally
2252proc canvscan {op w x y} {
2253    global canv canv2 canv3
2254    foreach c [list $canv $canv2 $canv3] {
2255        if {$c == $w} {
2256            $c scan $op $x $y
2257        } else {
2258            $c scan $op 0 $y
2259        }
2260    }
2261}
2262
2263proc scrollcanv {cscroll f0 f1} {
2264    $cscroll set $f0 $f1
2265    drawvisible
2266    flushhighlights
2267}
2268
2269# when we make a key binding for the toplevel, make sure
2270# it doesn't get triggered when that key is pressed in the
2271# find string entry widget.
2272proc bindkey {ev script} {
2273    global entries
2274    bind . $ev $script
2275    set escript [bind Entry $ev]
2276    if {$escript == {}} {
2277        set escript [bind Entry <Key>]
2278    }
2279    foreach e $entries {
2280        bind $e $ev "$escript; break"
2281    }
2282}
2283
2284# set the focus back to the toplevel for any click outside
2285# the entry widgets
2286proc click {w} {
2287    global ctext entries
2288    foreach e [concat $entries $ctext] {
2289        if {$w == $e} return
2290    }
2291    focus .
2292}
2293
2294# Adjust the progress bar for a change in requested extent or canvas size
2295proc adjustprogress {} {
2296    global progresscanv progressitem progresscoords
2297    global fprogitem fprogcoord lastprogupdate progupdatepending
2298    global rprogitem rprogcoord
2299
2300    set w [expr {[winfo width $progresscanv] - 4}]
2301    set x0 [expr {$w * [lindex $progresscoords 0]}]
2302    set x1 [expr {$w * [lindex $progresscoords 1]}]
2303    set h [winfo height $progresscanv]
2304    $progresscanv coords $progressitem $x0 0 $x1 $h
2305    $progresscanv coords $fprogitem 0 0 [expr {$w * $fprogcoord}] $h
2306    $progresscanv coords $rprogitem 0 0 [expr {$w * $rprogcoord}] $h
2307    set now [clock clicks -milliseconds]
2308    if {$now >= $lastprogupdate + 100} {
2309        set progupdatepending 0
2310        update
2311    } elseif {!$progupdatepending} {
2312        set progupdatepending 1
2313        after [expr {$lastprogupdate + 100 - $now}] doprogupdate
2314    }
2315}
2316
2317proc doprogupdate {} {
2318    global lastprogupdate progupdatepending
2319
2320    if {$progupdatepending} {
2321        set progupdatepending 0
2322        set lastprogupdate [clock clicks -milliseconds]
2323        update
2324    }
2325}
2326
2327proc savestuff {w} {
2328    global canv canv2 canv3 mainfont textfont uifont tabstop
2329    global stuffsaved findmergefiles maxgraphpct
2330    global maxwidth showneartags showlocalchanges
2331    global viewname viewfiles viewargs viewargscmd viewperm nextviewnum
2332    global cmitmode wrapcomment datetimeformat limitdiffs
2333    global colors bgcolor fgcolor diffcolors diffcontext selectbgcolor
2334    global autoselect extdifftool
2335
2336    if {$stuffsaved} return
2337    if {![winfo viewable .]} return
2338    catch {
2339        set f [open "~/.gitk-new" w]
2340        puts $f [list set mainfont $mainfont]
2341        puts $f [list set textfont $textfont]
2342        puts $f [list set uifont $uifont]
2343        puts $f [list set tabstop $tabstop]
2344        puts $f [list set findmergefiles $findmergefiles]
2345        puts $f [list set maxgraphpct $maxgraphpct]
2346        puts $f [list set maxwidth $maxwidth]
2347        puts $f [list set cmitmode $cmitmode]
2348        puts $f [list set wrapcomment $wrapcomment]
2349        puts $f [list set autoselect $autoselect]
2350        puts $f [list set showneartags $showneartags]
2351        puts $f [list set showlocalchanges $showlocalchanges]
2352        puts $f [list set datetimeformat $datetimeformat]
2353        puts $f [list set limitdiffs $limitdiffs]
2354        puts $f [list set bgcolor $bgcolor]
2355        puts $f [list set fgcolor $fgcolor]
2356        puts $f [list set colors $colors]
2357        puts $f [list set diffcolors $diffcolors]
2358        puts $f [list set diffcontext $diffcontext]
2359        puts $f [list set selectbgcolor $selectbgcolor]
2360        puts $f [list set extdifftool $extdifftool]
2361
2362        puts $f "set geometry(main) [wm geometry .]"
2363        puts $f "set geometry(topwidth) [winfo width .tf]"
2364        puts $f "set geometry(topheight) [winfo height .tf]"
2365        puts $f "set geometry(pwsash0) \"[.tf.histframe.pwclist sash coord 0]\""
2366        puts $f "set geometry(pwsash1) \"[.tf.histframe.pwclist sash coord 1]\""
2367        puts $f "set geometry(botwidth) [winfo width .bleft]"
2368        puts $f "set geometry(botheight) [winfo height .bleft]"
2369
2370        puts -nonewline $f "set permviews {"
2371        for {set v 0} {$v < $nextviewnum} {incr v} {
2372            if {$viewperm($v)} {
2373                puts $f "{[list $viewname($v) $viewfiles($v) $viewargs($v) $viewargscmd($v)]}"
2374            }
2375        }
2376        puts $f "}"
2377        close $f
2378        file rename -force "~/.gitk-new" "~/.gitk"
2379    }
2380    set stuffsaved 1
2381}
2382
2383proc resizeclistpanes {win w} {
2384    global oldwidth
2385    if {[info exists oldwidth($win)]} {
2386        set s0 [$win sash coord 0]
2387        set s1 [$win sash coord 1]
2388        if {$w < 60} {
2389            set sash0 [expr {int($w/2 - 2)}]
2390            set sash1 [expr {int($w*5/6 - 2)}]
2391        } else {
2392            set factor [expr {1.0 * $w / $oldwidth($win)}]
2393            set sash0 [expr {int($factor * [lindex $s0 0])}]
2394            set sash1 [expr {int($factor * [lindex $s1 0])}]
2395            if {$sash0 < 30} {
2396                set sash0 30
2397            }
2398            if {$sash1 < $sash0 + 20} {
2399                set sash1 [expr {$sash0 + 20}]
2400            }
2401            if {$sash1 > $w - 10} {
2402                set sash1 [expr {$w - 10}]
2403                if {$sash0 > $sash1 - 20} {
2404                    set sash0 [expr {$sash1 - 20}]
2405                }
2406            }
2407        }
2408        $win sash place 0 $sash0 [lindex $s0 1]
2409        $win sash place 1 $sash1 [lindex $s1 1]
2410    }
2411    set oldwidth($win) $w
2412}
2413
2414proc resizecdetpanes {win w} {
2415    global oldwidth
2416    if {[info exists oldwidth($win)]} {
2417        set s0 [$win sash coord 0]
2418        if {$w < 60} {
2419            set sash0 [expr {int($w*3/4 - 2)}]
2420        } else {
2421            set factor [expr {1.0 * $w / $oldwidth($win)}]
2422            set sash0 [expr {int($factor * [lindex $s0 0])}]
2423            if {$sash0 < 45} {
2424                set sash0 45
2425            }
2426            if {$sash0 > $w - 15} {
2427                set sash0 [expr {$w - 15}]
2428            }
2429        }
2430        $win sash place 0 $sash0 [lindex $s0 1]
2431    }
2432    set oldwidth($win) $w
2433}
2434
2435proc allcanvs args {
2436    global canv canv2 canv3
2437    eval $canv $args
2438    eval $canv2 $args
2439    eval $canv3 $args
2440}
2441
2442proc bindall {event action} {
2443    global canv canv2 canv3
2444    bind $canv $event $action
2445    bind $canv2 $event $action
2446    bind $canv3 $event $action
2447}
2448
2449proc about {} {
2450    global uifont
2451    set w .about
2452    if {[winfo exists $w]} {
2453        raise $w
2454        return
2455    }
2456    toplevel $w
2457    wm title $w [mc "About gitk"]
2458    message $w.m -text [mc "
2459Gitk - a commit viewer for git
2460
2461Copyright © 2005-2008 Paul Mackerras
2462
2463Use and redistribute under the terms of the GNU General Public License"] \
2464            -justify center -aspect 400 -border 2 -bg white -relief groove
2465    pack $w.m -side top -fill x -padx 2 -pady 2
2466    button $w.ok -text [mc "Close"] -command "destroy $w" -default active
2467    pack $w.ok -side bottom
2468    bind $w <Visibility> "focus $w.ok"
2469    bind $w <Key-Escape> "destroy $w"
2470    bind $w <Key-Return> "destroy $w"
2471}
2472
2473proc keys {} {
2474    set w .keys
2475    if {[winfo exists $w]} {
2476        raise $w
2477        return
2478    }
2479    if {[tk windowingsystem] eq {aqua}} {
2480        set M1T Cmd
2481    } else {
2482        set M1T Ctrl
2483    }
2484    toplevel $w
2485    wm title $w [mc "Gitk key bindings"]
2486    message $w.m -text "
2487[mc "Gitk key bindings:"]
2488
2489[mc "<%s-Q>             Quit" $M1T]
2490[mc "<Home>             Move to first commit"]
2491[mc "<End>              Move to last commit"]
2492[mc "<Up>, p, i Move up one commit"]
2493[mc "<Down>, n, k       Move down one commit"]
2494[mc "<Left>, z, j       Go back in history list"]
2495[mc "<Right>, x, l      Go forward in history list"]
2496[mc "<PageUp>   Move up one page in commit list"]
2497[mc "<PageDown> Move down one page in commit list"]
2498[mc "<%s-Home>  Scroll to top of commit list" $M1T]
2499[mc "<%s-End>   Scroll to bottom of commit list" $M1T]
2500[mc "<%s-Up>    Scroll commit list up one line" $M1T]
2501[mc "<%s-Down>  Scroll commit list down one line" $M1T]
2502[mc "<%s-PageUp>        Scroll commit list up one page" $M1T]
2503[mc "<%s-PageDown>      Scroll commit list down one page" $M1T]
2504[mc "<Shift-Up> Find backwards (upwards, later commits)"]
2505[mc "<Shift-Down>       Find forwards (downwards, earlier commits)"]
2506[mc "<Delete>, b        Scroll diff view up one page"]
2507[mc "<Backspace>        Scroll diff view up one page"]
2508[mc "<Space>            Scroll diff view down one page"]
2509[mc "u          Scroll diff view up 18 lines"]
2510[mc "d          Scroll diff view down 18 lines"]
2511[mc "<%s-F>             Find" $M1T]
2512[mc "<%s-G>             Move to next find hit" $M1T]
2513[mc "<Return>   Move to next find hit"]
2514[mc "/          Move to next find hit, or redo find"]
2515[mc "?          Move to previous find hit"]
2516[mc "f          Scroll diff view to next file"]
2517[mc "<%s-S>             Search for next hit in diff view" $M1T]
2518[mc "<%s-R>             Search for previous hit in diff view" $M1T]
2519[mc "<%s-KP+>   Increase font size" $M1T]
2520[mc "<%s-plus>  Increase font size" $M1T]
2521[mc "<%s-KP->   Decrease font size" $M1T]
2522[mc "<%s-minus> Decrease font size" $M1T]
2523[mc "<F5>               Update"]
2524" \
2525            -justify left -bg white -border 2 -relief groove
2526    pack $w.m -side top -fill both -padx 2 -pady 2
2527    button $w.ok -text [mc "Close"] -command "destroy $w" -default active
2528    pack $w.ok -side bottom
2529    bind $w <Visibility> "focus $w.ok"
2530    bind $w <Key-Escape> "destroy $w"
2531    bind $w <Key-Return> "destroy $w"
2532}
2533
2534# Procedures for manipulating the file list window at the
2535# bottom right of the overall window.
2536
2537proc treeview {w l openlevs} {
2538    global treecontents treediropen treeheight treeparent treeindex
2539
2540    set ix 0
2541    set treeindex() 0
2542    set lev 0
2543    set prefix {}
2544    set prefixend -1
2545    set prefendstack {}
2546    set htstack {}
2547    set ht 0
2548    set treecontents() {}
2549    $w conf -state normal
2550    foreach f $l {
2551        while {[string range $f 0 $prefixend] ne $prefix} {
2552            if {$lev <= $openlevs} {
2553                $w mark set e:$treeindex($prefix) "end -1c"
2554                $w mark gravity e:$treeindex($prefix) left
2555            }
2556            set treeheight($prefix) $ht
2557            incr ht [lindex $htstack end]
2558            set htstack [lreplace $htstack end end]
2559            set prefixend [lindex $prefendstack end]
2560            set prefendstack [lreplace $prefendstack end end]
2561            set prefix [string range $prefix 0 $prefixend]
2562            incr lev -1
2563        }
2564        set tail [string range $f [expr {$prefixend+1}] end]
2565        while {[set slash [string first "/" $tail]] >= 0} {
2566            lappend htstack $ht
2567            set ht 0
2568            lappend prefendstack $prefixend
2569            incr prefixend [expr {$slash + 1}]
2570            set d [string range $tail 0 $slash]
2571            lappend treecontents($prefix) $d
2572            set oldprefix $prefix
2573            append prefix $d
2574            set treecontents($prefix) {}
2575            set treeindex($prefix) [incr ix]
2576            set treeparent($prefix) $oldprefix
2577            set tail [string range $tail [expr {$slash+1}] end]
2578            if {$lev <= $openlevs} {
2579                set ht 1
2580                set treediropen($prefix) [expr {$lev < $openlevs}]
2581                set bm [expr {$lev == $openlevs? "tri-rt": "tri-dn"}]
2582                $w mark set d:$ix "end -1c"
2583                $w mark gravity d:$ix left
2584                set str "\n"
2585                for {set i 0} {$i < $lev} {incr i} {append str "\t"}
2586                $w insert end $str
2587                $w image create end -align center -image $bm -padx 1 \
2588                    -name a:$ix
2589                $w insert end $d [highlight_tag $prefix]
2590                $w mark set s:$ix "end -1c"
2591                $w mark gravity s:$ix left
2592            }
2593            incr lev
2594        }
2595        if {$tail ne {}} {
2596            if {$lev <= $openlevs} {
2597                incr ht
2598                set str "\n"
2599                for {set i 0} {$i < $lev} {incr i} {append str "\t"}
2600                $w insert end $str
2601                $w insert end $tail [highlight_tag $f]
2602            }
2603            lappend treecontents($prefix) $tail
2604        }
2605    }
2606    while {$htstack ne {}} {
2607        set treeheight($prefix) $ht
2608        incr ht [lindex $htstack end]
2609        set htstack [lreplace $htstack end end]
2610        set prefixend [lindex $prefendstack end]
2611        set prefendstack [lreplace $prefendstack end end]
2612        set prefix [string range $prefix 0 $prefixend]
2613    }
2614    $w conf -state disabled
2615}
2616
2617proc linetoelt {l} {
2618    global treeheight treecontents
2619
2620    set y 2
2621    set prefix {}
2622    while {1} {
2623        foreach e $treecontents($prefix) {
2624            if {$y == $l} {
2625                return "$prefix$e"
2626            }
2627            set n 1
2628            if {[string index $e end] eq "/"} {
2629                set n $treeheight($prefix$e)
2630                if {$y + $n > $l} {
2631                    append prefix $e
2632                    incr y
2633                    break
2634                }
2635            }
2636            incr y $n
2637        }
2638    }
2639}
2640
2641proc highlight_tree {y prefix} {
2642    global treeheight treecontents cflist
2643
2644    foreach e $treecontents($prefix) {
2645        set path $prefix$e
2646        if {[highlight_tag $path] ne {}} {
2647            $cflist tag add bold $y.0 "$y.0 lineend"
2648        }
2649        incr y
2650        if {[string index $e end] eq "/" && $treeheight($path) > 1} {
2651            set y [highlight_tree $y $path]
2652        }
2653    }
2654    return $y
2655}
2656
2657proc treeclosedir {w dir} {
2658    global treediropen treeheight treeparent treeindex
2659
2660    set ix $treeindex($dir)
2661    $w conf -state normal
2662    $w delete s:$ix e:$ix
2663    set treediropen($dir) 0
2664    $w image configure a:$ix -image tri-rt
2665    $w conf -state disabled
2666    set n [expr {1 - $treeheight($dir)}]
2667    while {$dir ne {}} {
2668        incr treeheight($dir) $n
2669        set dir $treeparent($dir)
2670    }
2671}
2672
2673proc treeopendir {w dir} {
2674    global treediropen treeheight treeparent treecontents treeindex
2675
2676    set ix $treeindex($dir)
2677    $w conf -state normal
2678    $w image configure a:$ix -image tri-dn
2679    $w mark set e:$ix s:$ix
2680    $w mark gravity e:$ix right
2681    set lev 0
2682    set str "\n"
2683    set n [llength $treecontents($dir)]
2684    for {set x $dir} {$x ne {}} {set x $treeparent($x)} {
2685        incr lev
2686        append str "\t"
2687        incr treeheight($x) $n
2688    }
2689    foreach e $treecontents($dir) {
2690        set de $dir$e
2691        if {[string index $e end] eq "/"} {
2692            set iy $treeindex($de)
2693            $w mark set d:$iy e:$ix
2694            $w mark gravity d:$iy left
2695            $w insert e:$ix $str
2696            set treediropen($de) 0
2697            $w image create e:$ix -align center -image tri-rt -padx 1 \
2698                -name a:$iy
2699            $w insert e:$ix $e [highlight_tag $de]
2700            $w mark set s:$iy e:$ix
2701            $w mark gravity s:$iy left
2702            set treeheight($de) 1
2703        } else {
2704            $w insert e:$ix $str
2705            $w insert e:$ix $e [highlight_tag $de]
2706        }
2707    }
2708    $w mark gravity e:$ix left
2709    $w conf -state disabled
2710    set treediropen($dir) 1
2711    set top [lindex [split [$w index @0,0] .] 0]
2712    set ht [$w cget -height]
2713    set l [lindex [split [$w index s:$ix] .] 0]
2714    if {$l < $top} {
2715        $w yview $l.0
2716    } elseif {$l + $n + 1 > $top + $ht} {
2717        set top [expr {$l + $n + 2 - $ht}]
2718        if {$l < $top} {
2719            set top $l
2720        }
2721        $w yview $top.0
2722    }
2723}
2724
2725proc treeclick {w x y} {
2726    global treediropen cmitmode ctext cflist cflist_top
2727
2728    if {$cmitmode ne "tree"} return
2729    if {![info exists cflist_top]} return
2730    set l [lindex [split [$w index "@$x,$y"] "."] 0]
2731    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
2732    $cflist tag add highlight $l.0 "$l.0 lineend"
2733    set cflist_top $l
2734    if {$l == 1} {
2735        $ctext yview 1.0
2736        return
2737    }
2738    set e [linetoelt $l]
2739    if {[string index $e end] ne "/"} {
2740        showfile $e
2741    } elseif {$treediropen($e)} {
2742        treeclosedir $w $e
2743    } else {
2744        treeopendir $w $e
2745    }
2746}
2747
2748proc setfilelist {id} {
2749    global treefilelist cflist
2750
2751    treeview $cflist $treefilelist($id) 0
2752}
2753
2754image create bitmap tri-rt -background black -foreground blue -data {
2755    #define tri-rt_width 13
2756    #define tri-rt_height 13
2757    static unsigned char tri-rt_bits[] = {
2758       0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x70, 0x00, 0xf0, 0x00,
2759       0xf0, 0x01, 0xf0, 0x00, 0x70, 0x00, 0x30, 0x00, 0x10, 0x00, 0x00, 0x00,
2760       0x00, 0x00};
2761} -maskdata {
2762    #define tri-rt-mask_width 13
2763    #define tri-rt-mask_height 13
2764    static unsigned char tri-rt-mask_bits[] = {
2765       0x08, 0x00, 0x18, 0x00, 0x38, 0x00, 0x78, 0x00, 0xf8, 0x00, 0xf8, 0x01,
2766       0xf8, 0x03, 0xf8, 0x01, 0xf8, 0x00, 0x78, 0x00, 0x38, 0x00, 0x18, 0x00,
2767       0x08, 0x00};
2768}
2769image create bitmap tri-dn -background black -foreground blue -data {
2770    #define tri-dn_width 13
2771    #define tri-dn_height 13
2772    static unsigned char tri-dn_bits[] = {
2773       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x07, 0xf8, 0x03,
2774       0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2775       0x00, 0x00};
2776} -maskdata {
2777    #define tri-dn-mask_width 13
2778    #define tri-dn-mask_height 13
2779    static unsigned char tri-dn-mask_bits[] = {
2780       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x1f, 0xfe, 0x0f, 0xfc, 0x07,
2781       0xf8, 0x03, 0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
2782       0x00, 0x00};
2783}
2784
2785image create bitmap reficon-T -background black -foreground yellow -data {
2786    #define tagicon_width 13
2787    #define tagicon_height 9
2788    static unsigned char tagicon_bits[] = {
2789       0x00, 0x00, 0x00, 0x00, 0xf0, 0x07, 0xf8, 0x07,
2790       0xfc, 0x07, 0xf8, 0x07, 0xf0, 0x07, 0x00, 0x00, 0x00, 0x00};
2791} -maskdata {
2792    #define tagicon-mask_width 13
2793    #define tagicon-mask_height 9
2794    static unsigned char tagicon-mask_bits[] = {
2795       0x00, 0x00, 0xf0, 0x0f, 0xf8, 0x0f, 0xfc, 0x0f,
2796       0xfe, 0x0f, 0xfc, 0x0f, 0xf8, 0x0f, 0xf0, 0x0f, 0x00, 0x00};
2797}
2798set rectdata {
2799    #define headicon_width 13
2800    #define headicon_height 9
2801    static unsigned char headicon_bits[] = {
2802       0x00, 0x00, 0x00, 0x00, 0xf8, 0x07, 0xf8, 0x07,
2803       0xf8, 0x07, 0xf8, 0x07, 0xf8, 0x07, 0x00, 0x00, 0x00, 0x00};
2804}
2805set rectmask {
2806    #define headicon-mask_width 13
2807    #define headicon-mask_height 9
2808    static unsigned char headicon-mask_bits[] = {
2809       0x00, 0x00, 0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f,
2810       0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f, 0x00, 0x00};
2811}
2812image create bitmap reficon-H -background black -foreground green \
2813    -data $rectdata -maskdata $rectmask
2814image create bitmap reficon-o -background black -foreground "#ddddff" \
2815    -data $rectdata -maskdata $rectmask
2816
2817proc init_flist {first} {
2818    global cflist cflist_top difffilestart
2819
2820    $cflist conf -state normal
2821    $cflist delete 0.0 end
2822    if {$first ne {}} {
2823        $cflist insert end $first
2824        set cflist_top 1
2825        $cflist tag add highlight 1.0 "1.0 lineend"
2826    } else {
2827        catch {unset cflist_top}
2828    }
2829    $cflist conf -state disabled
2830    set difffilestart {}
2831}
2832
2833proc highlight_tag {f} {
2834    global highlight_paths
2835
2836    foreach p $highlight_paths {
2837        if {[string match $p $f]} {
2838            return "bold"
2839        }
2840    }
2841    return {}
2842}
2843
2844proc highlight_filelist {} {
2845    global cmitmode cflist
2846
2847    $cflist conf -state normal
2848    if {$cmitmode ne "tree"} {
2849        set end [lindex [split [$cflist index end] .] 0]
2850        for {set l 2} {$l < $end} {incr l} {
2851            set line [$cflist get $l.0 "$l.0 lineend"]
2852            if {[highlight_tag $line] ne {}} {
2853                $cflist tag add bold $l.0 "$l.0 lineend"
2854            }
2855        }
2856    } else {
2857        highlight_tree 2 {}
2858    }
2859    $cflist conf -state disabled
2860}
2861
2862proc unhighlight_filelist {} {
2863    global cflist
2864
2865    $cflist conf -state normal
2866    $cflist tag remove bold 1.0 end
2867    $cflist conf -state disabled
2868}
2869
2870proc add_flist {fl} {
2871    global cflist
2872
2873    $cflist conf -state normal
2874    foreach f $fl {
2875        $cflist insert end "\n"
2876        $cflist insert end $f [highlight_tag $f]
2877    }
2878    $cflist conf -state disabled
2879}
2880
2881proc sel_flist {w x y} {
2882    global ctext difffilestart cflist cflist_top cmitmode
2883
2884    if {$cmitmode eq "tree"} return
2885    if {![info exists cflist_top]} return
2886    set l [lindex [split [$w index "@$x,$y"] "."] 0]
2887    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
2888    $cflist tag add highlight $l.0 "$l.0 lineend"
2889    set cflist_top $l
2890    if {$l == 1} {
2891        $ctext yview 1.0
2892    } else {
2893        catch {$ctext yview [lindex $difffilestart [expr {$l - 2}]]}
2894    }
2895}
2896
2897proc pop_flist_menu {w X Y x y} {
2898    global ctext cflist cmitmode flist_menu flist_menu_file
2899    global treediffs diffids
2900
2901    stopfinding
2902    set l [lindex [split [$w index "@$x,$y"] "."] 0]
2903    if {$l <= 1} return
2904    if {$cmitmode eq "tree"} {
2905        set e [linetoelt $l]
2906        if {[string index $e end] eq "/"} return
2907    } else {
2908        set e [lindex $treediffs($diffids) [expr {$l-2}]]
2909    }
2910    set flist_menu_file $e
2911    set xdiffstate "normal"
2912    if {$cmitmode eq "tree"} {
2913        set xdiffstate "disabled"
2914    }
2915    # Disable "External diff" item in tree mode
2916    $flist_menu entryconf 2 -state $xdiffstate
2917    tk_popup $flist_menu $X $Y
2918}
2919
2920proc flist_hl {only} {
2921    global flist_menu_file findstring gdttype
2922
2923    set x [shellquote $flist_menu_file]
2924    if {$only || $findstring eq {} || $gdttype ne [mc "touching paths:"]} {
2925        set findstring $x
2926    } else {
2927        append findstring " " $x
2928    }
2929    set gdttype [mc "touching paths:"]
2930}
2931
2932proc save_file_from_commit {filename output what} {
2933    global nullfile
2934
2935    if {[catch {exec git show $filename -- > $output} err]} {
2936        if {[string match "fatal: bad revision *" $err]} {
2937            return $nullfile
2938        }
2939        error_popup "Error getting \"$filename\" from $what: $err"
2940        return {}
2941    }
2942    return $output
2943}
2944
2945proc external_diff_get_one_file {diffid filename diffdir} {
2946    global nullid nullid2 nullfile
2947    global gitdir
2948
2949    if {$diffid == $nullid} {
2950        set difffile [file join [file dirname $gitdir] $filename]
2951        if {[file exists $difffile]} {
2952            return $difffile
2953        }
2954        return $nullfile
2955    }
2956    if {$diffid == $nullid2} {
2957        set difffile [file join $diffdir "\[index\] [file tail $filename]"]
2958        return [save_file_from_commit :$filename $difffile index]
2959    }
2960    set difffile [file join $diffdir "\[$diffid\] [file tail $filename]"]
2961    return [save_file_from_commit $diffid:$filename $difffile \
2962               "revision $diffid"]
2963}
2964
2965proc external_diff {} {
2966    global gitktmpdir nullid nullid2
2967    global flist_menu_file
2968    global diffids
2969    global diffnum
2970    global gitdir extdifftool
2971
2972    if {[llength $diffids] == 1} {
2973        # no reference commit given
2974        set diffidto [lindex $diffids 0]
2975        if {$diffidto eq $nullid} {
2976            # diffing working copy with index
2977            set diffidfrom $nullid2
2978        } elseif {$diffidto eq $nullid2} {
2979            # diffing index with HEAD
2980            set diffidfrom "HEAD"
2981        } else {
2982            # use first parent commit
2983            global parentlist selectedline
2984            set diffidfrom [lindex $parentlist $selectedline 0]
2985        }
2986    } else {
2987        set diffidfrom [lindex $diffids 0]
2988        set diffidto [lindex $diffids 1]
2989    }
2990
2991    # make sure that several diffs wont collide
2992    if {![info exists gitktmpdir]} {
2993        set gitktmpdir [file join [file dirname $gitdir] \
2994                            [format ".gitk-tmp.%s" [pid]]]
2995        if {[catch {file mkdir $gitktmpdir} err]} {
2996            error_popup "Error creating temporary directory $gitktmpdir: $err"
2997            unset gitktmpdir
2998            return
2999        }
3000        set diffnum 0
3001    }
3002    incr diffnum
3003    set diffdir [file join $gitktmpdir $diffnum]
3004    if {[catch {file mkdir $diffdir} err]} {
3005        error_popup "Error creating temporary directory $diffdir: $err"
3006        return
3007    }
3008
3009    # gather files to diff
3010    set difffromfile [external_diff_get_one_file $diffidfrom $flist_menu_file $diffdir]
3011    set difftofile [external_diff_get_one_file $diffidto $flist_menu_file $diffdir]
3012
3013    if {$difffromfile ne {} && $difftofile ne {}} {
3014        set cmd [concat | [shellsplit $extdifftool] \
3015                     [list $difffromfile $difftofile]]
3016        if {[catch {set fl [open $cmd r]} err]} {
3017            file delete -force $diffdir
3018            error_popup [mc "$extdifftool: command failed: $err"]
3019        } else {
3020            fconfigure $fl -blocking 0
3021            filerun $fl [list delete_at_eof $fl $diffdir]
3022        }
3023    }
3024}
3025
3026proc external_blame {parent_idx} {
3027    global flist_menu_file
3028    global nullid nullid2
3029    global parentlist selectedline currentid
3030
3031    if {$parent_idx > 0} {
3032        set base_commit [lindex $parentlist $selectedline [expr {$parent_idx-1}]]
3033    } else {
3034        set base_commit $currentid
3035    }
3036
3037    if {$base_commit eq {} || $base_commit eq $nullid || $base_commit eq $nullid2} {
3038        error_popup [mc "No such commit"]
3039        return
3040    }
3041
3042    if {[catch {exec git gui blame $base_commit $flist_menu_file &} err]} {
3043        error_popup [mc "git gui blame: command failed: $err"]
3044    }
3045}
3046
3047# delete $dir when we see eof on $f (presumably because the child has exited)
3048proc delete_at_eof {f dir} {
3049    while {[gets $f line] >= 0} {}
3050    if {[eof $f]} {
3051        if {[catch {close $f} err]} {
3052            error_popup "External diff viewer failed: $err"
3053        }
3054        file delete -force $dir
3055        return 0
3056    }
3057    return 1
3058}
3059
3060# Functions for adding and removing shell-type quoting
3061
3062proc shellquote {str} {
3063    if {![string match "*\['\"\\ \t]*" $str]} {
3064        return $str
3065    }
3066    if {![string match "*\['\"\\]*" $str]} {
3067        return "\"$str\""
3068    }
3069    if {![string match "*'*" $str]} {
3070        return "'$str'"
3071    }
3072    return "\"[string map {\" \\\" \\ \\\\} $str]\""
3073}
3074
3075proc shellarglist {l} {
3076    set str {}
3077    foreach a $l {
3078        if {$str ne {}} {
3079            append str " "
3080        }
3081        append str [shellquote $a]
3082    }
3083    return $str
3084}
3085
3086proc shelldequote {str} {
3087    set ret {}
3088    set used -1
3089    while {1} {
3090        incr used
3091        if {![regexp -start $used -indices "\['\"\\\\ \t]" $str first]} {
3092            append ret [string range $str $used end]
3093            set used [string length $str]
3094            break
3095        }
3096        set first [lindex $first 0]
3097        set ch [string index $str $first]
3098        if {$first > $used} {
3099            append ret [string range $str $used [expr {$first - 1}]]
3100            set used $first
3101        }
3102        if {$ch eq " " || $ch eq "\t"} break
3103        incr used
3104        if {$ch eq "'"} {
3105            set first [string first "'" $str $used]
3106            if {$first < 0} {
3107                error "unmatched single-quote"
3108            }
3109            append ret [string range $str $used [expr {$first - 1}]]
3110            set used $first
3111            continue
3112        }
3113        if {$ch eq "\\"} {
3114            if {$used >= [string length $str]} {
3115                error "trailing backslash"
3116            }
3117            append ret [string index $str $used]
3118            continue
3119        }
3120        # here ch == "\""
3121        while {1} {
3122            if {![regexp -start $used -indices "\[\"\\\\]" $str first]} {
3123                error "unmatched double-quote"
3124            }
3125            set first [lindex $first 0]
3126            set ch [string index $str $first]
3127            if {$first > $used} {
3128                append ret [string range $str $used [expr {$first - 1}]]
3129                set used $first
3130            }
3131            if {$ch eq "\""} break
3132            incr used
3133            append ret [string index $str $used]
3134            incr used
3135        }
3136    }
3137    return [list $used $ret]
3138}
3139
3140proc shellsplit {str} {
3141    set l {}
3142    while {1} {
3143        set str [string trimleft $str]
3144        if {$str eq {}} break
3145        set dq [shelldequote $str]
3146        set n [lindex $dq 0]
3147        set word [lindex $dq 1]
3148        set str [string range $str $n end]
3149        lappend l $word
3150    }
3151    return $l
3152}
3153
3154# Code to implement multiple views
3155
3156proc newview {ishighlight} {
3157    global nextviewnum newviewname newviewperm newishighlight
3158    global newviewargs revtreeargs viewargscmd newviewargscmd curview
3159
3160    set newishighlight $ishighlight
3161    set top .gitkview
3162    if {[winfo exists $top]} {
3163        raise $top
3164        return
3165    }
3166    set newviewname($nextviewnum) "[mc "View"] $nextviewnum"
3167    set newviewperm($nextviewnum) 0
3168    set newviewargs($nextviewnum) [shellarglist $revtreeargs]
3169    set newviewargscmd($nextviewnum) $viewargscmd($curview)
3170    vieweditor $top $nextviewnum [mc "Gitk view definition"]
3171}
3172
3173proc editview {} {
3174    global curview
3175    global viewname viewperm newviewname newviewperm
3176    global viewargs newviewargs viewargscmd newviewargscmd
3177
3178    set top .gitkvedit-$curview
3179    if {[winfo exists $top]} {
3180        raise $top
3181        return
3182    }
3183    set newviewname($curview) $viewname($curview)
3184    set newviewperm($curview) $viewperm($curview)
3185    set newviewargs($curview) [shellarglist $viewargs($curview)]
3186    set newviewargscmd($curview) $viewargscmd($curview)
3187    vieweditor $top $curview "Gitk: edit view $viewname($curview)"
3188}
3189
3190proc vieweditor {top n title} {
3191    global newviewname newviewperm viewfiles bgcolor
3192
3193    toplevel $top
3194    wm title $top $title
3195    label $top.nl -text [mc "Name"]
3196    entry $top.name -width 20 -textvariable newviewname($n)
3197    grid $top.nl $top.name -sticky w -pady 5
3198    checkbutton $top.perm -text [mc "Remember this view"] \
3199        -variable newviewperm($n)
3200    grid $top.perm - -pady 5 -sticky w
3201    message $top.al -aspect 1000 \
3202        -text [mc "Commits to include (arguments to git log):"]
3203    grid $top.al - -sticky w -pady 5
3204    entry $top.args -width 50 -textvariable newviewargs($n) \
3205        -background $bgcolor
3206    grid $top.args - -sticky ew -padx 5
3207
3208    message $top.ac -aspect 1000 \
3209        -text [mc "Command to generate more commits to include:"]
3210    grid $top.ac - -sticky w -pady 5
3211    entry $top.argscmd -width 50 -textvariable newviewargscmd($n) \
3212        -background white
3213    grid $top.argscmd - -sticky ew -padx 5
3214
3215    message $top.l -aspect 1000 \
3216        -text [mc "Enter files and directories to include, one per line:"]
3217    grid $top.l - -sticky w
3218    text $top.t -width 40 -height 10 -background $bgcolor -font uifont
3219    if {[info exists viewfiles($n)]} {
3220        foreach f $viewfiles($n) {
3221            $top.t insert end $f
3222            $top.t insert end "\n"
3223        }
3224        $top.t delete {end - 1c} end
3225        $top.t mark set insert 0.0
3226    }
3227    grid $top.t - -sticky ew -padx 5
3228    frame $top.buts
3229    button $top.buts.ok -text [mc "OK"] -command [list newviewok $top $n]
3230    button $top.buts.can -text [mc "Cancel"] -command [list destroy $top]
3231    grid $top.buts.ok $top.buts.can
3232    grid columnconfigure $top.buts 0 -weight 1 -uniform a
3233    grid columnconfigure $top.buts 1 -weight 1 -uniform a
3234    grid $top.buts - -pady 10 -sticky ew
3235    focus $top.t
3236}
3237
3238proc doviewmenu {m first cmd op argv} {
3239    set nmenu [$m index end]
3240    for {set i $first} {$i <= $nmenu} {incr i} {
3241        if {[$m entrycget $i -command] eq $cmd} {
3242            eval $m $op $i $argv
3243            break
3244        }
3245    }
3246}
3247
3248proc allviewmenus {n op args} {
3249    # global viewhlmenu
3250
3251    doviewmenu .bar.view 5 [list showview $n] $op $args
3252    # doviewmenu $viewhlmenu 1 [list addvhighlight $n] $op $args
3253}
3254
3255proc newviewok {top n} {
3256    global nextviewnum newviewperm newviewname newishighlight
3257    global viewname viewfiles viewperm selectedview curview
3258    global viewargs newviewargs viewargscmd newviewargscmd viewhlmenu
3259
3260    if {[catch {
3261        set newargs [shellsplit $newviewargs($n)]
3262    } err]} {
3263        error_popup "[mc "Error in commit selection arguments:"] $err"
3264        wm raise $top
3265        focus $top
3266        return
3267    }
3268    set files {}
3269    foreach f [split [$top.t get 0.0 end] "\n"] {
3270        set ft [string trim $f]
3271        if {$ft ne {}} {
3272            lappend files $ft
3273        }
3274    }
3275    if {![info exists viewfiles($n)]} {
3276        # creating a new view
3277        incr nextviewnum
3278        set viewname($n) $newviewname($n)
3279        set viewperm($n) $newviewperm($n)
3280        set viewfiles($n) $files
3281        set viewargs($n) $newargs
3282        set viewargscmd($n) $newviewargscmd($n)
3283        addviewmenu $n
3284        if {!$newishighlight} {
3285            run showview $n
3286        } else {
3287            run addvhighlight $n
3288        }
3289    } else {
3290        # editing an existing view
3291        set viewperm($n) $newviewperm($n)
3292        if {$newviewname($n) ne $viewname($n)} {
3293            set viewname($n) $newviewname($n)
3294            doviewmenu .bar.view 5 [list showview $n] \
3295                entryconf [list -label $viewname($n)]
3296            # doviewmenu $viewhlmenu 1 [list addvhighlight $n] \
3297                # entryconf [list -label $viewname($n) -value $viewname($n)]
3298        }
3299        if {$files ne $viewfiles($n) || $newargs ne $viewargs($n) || \
3300                $newviewargscmd($n) ne $viewargscmd($n)} {
3301            set viewfiles($n) $files
3302            set viewargs($n) $newargs
3303            set viewargscmd($n) $newviewargscmd($n)
3304            if {$curview == $n} {
3305                run reloadcommits
3306            }
3307        }
3308    }
3309    catch {destroy $top}
3310}
3311
3312proc delview {} {
3313    global curview viewperm hlview selectedhlview
3314
3315    if {$curview == 0} return
3316    if {[info exists hlview] && $hlview == $curview} {
3317        set selectedhlview [mc "None"]
3318        unset hlview
3319    }
3320    allviewmenus $curview delete
3321    set viewperm($curview) 0
3322    showview 0
3323}
3324
3325proc addviewmenu {n} {
3326    global viewname viewhlmenu
3327
3328    .bar.view add radiobutton -label $viewname($n) \
3329        -command [list showview $n] -variable selectedview -value $n
3330    #$viewhlmenu add radiobutton -label $viewname($n) \
3331    #   -command [list addvhighlight $n] -variable selectedhlview
3332}
3333
3334proc showview {n} {
3335    global curview cached_commitrow ordertok
3336    global displayorder parentlist rowidlist rowisopt rowfinal
3337    global colormap rowtextx nextcolor canvxmax
3338    global numcommits viewcomplete
3339    global selectedline currentid canv canvy0
3340    global treediffs
3341    global pending_select mainheadid
3342    global commitidx
3343    global selectedview
3344    global hlview selectedhlview commitinterest
3345
3346    if {$n == $curview} return
3347    set selid {}
3348    set ymax [lindex [$canv cget -scrollregion] 3]
3349    set span [$canv yview]
3350    set ytop [expr {[lindex $span 0] * $ymax}]
3351    set ybot [expr {[lindex $span 1] * $ymax}]
3352    set yscreen [expr {($ybot - $ytop) / 2}]
3353    if {$selectedline ne {}} {
3354        set selid $currentid
3355        set y [yc $selectedline]
3356        if {$ytop < $y && $y < $ybot} {
3357            set yscreen [expr {$y - $ytop}]
3358        }
3359    } elseif {[info exists pending_select]} {
3360        set selid $pending_select
3361        unset pending_select
3362    }
3363    unselectline
3364    normalline
3365    catch {unset treediffs}
3366    clear_display
3367    if {[info exists hlview] && $hlview == $n} {
3368        unset hlview
3369        set selectedhlview [mc "None"]
3370    }
3371    catch {unset commitinterest}
3372    catch {unset cached_commitrow}
3373    catch {unset ordertok}
3374
3375    set curview $n
3376    set selectedview $n
3377    .bar.view entryconf [mc "Edit view..."] -state [expr {$n == 0? "disabled": "normal"}]
3378    .bar.view entryconf [mc "Delete view"] -state [expr {$n == 0? "disabled": "normal"}]
3379
3380    run refill_reflist
3381    if {![info exists viewcomplete($n)]} {
3382        getcommits $selid
3383        return
3384    }
3385
3386    set displayorder {}
3387    set parentlist {}
3388    set rowidlist {}
3389    set rowisopt {}
3390    set rowfinal {}
3391    set numcommits $commitidx($n)
3392
3393    catch {unset colormap}
3394    catch {unset rowtextx}
3395    set nextcolor 0
3396    set canvxmax [$canv cget -width]
3397    set curview $n
3398    set row 0
3399    setcanvscroll
3400    set yf 0
3401    set row {}
3402    if {$selid ne {} && [commitinview $selid $n]} {
3403        set row [rowofcommit $selid]
3404        # try to get the selected row in the same position on the screen
3405        set ymax [lindex [$canv cget -scrollregion] 3]
3406        set ytop [expr {[yc $row] - $yscreen}]
3407        if {$ytop < 0} {
3408            set ytop 0
3409        }
3410        set yf [expr {$ytop * 1.0 / $ymax}]
3411    }
3412    allcanvs yview moveto $yf
3413    drawvisible
3414    if {$row ne {}} {
3415        selectline $row 0
3416    } elseif {!$viewcomplete($n)} {
3417        reset_pending_select $selid
3418    } else {
3419        reset_pending_select {}
3420
3421        if {[commitinview $pending_select $curview]} {
3422            selectline [rowofcommit $pending_select] 1
3423        } else {
3424            set row [first_real_row]
3425            if {$row < $numcommits} {
3426                selectline $row 0
3427            }
3428        }
3429    }
3430    if {!$viewcomplete($n)} {
3431        if {$numcommits == 0} {
3432            show_status [mc "Reading commits..."]
3433        }
3434    } elseif {$numcommits == 0} {
3435        show_status [mc "No commits selected"]
3436    }
3437}
3438
3439# Stuff relating to the highlighting facility
3440
3441proc ishighlighted {id} {
3442    global vhighlights fhighlights nhighlights rhighlights
3443
3444    if {[info exists nhighlights($id)] && $nhighlights($id) > 0} {
3445        return $nhighlights($id)
3446    }
3447    if {[info exists vhighlights($id)] && $vhighlights($id) > 0} {
3448        return $vhighlights($id)
3449    }
3450    if {[info exists fhighlights($id)] && $fhighlights($id) > 0} {
3451        return $fhighlights($id)
3452    }
3453    if {[info exists rhighlights($id)] && $rhighlights($id) > 0} {
3454        return $rhighlights($id)
3455    }
3456    return 0
3457}
3458
3459proc bolden {row font} {
3460    global canv linehtag selectedline boldrows
3461
3462    lappend boldrows $row
3463    $canv itemconf $linehtag($row) -font $font
3464    if {$row == $selectedline} {
3465        $canv delete secsel
3466        set t [eval $canv create rect [$canv bbox $linehtag($row)] \
3467                   -outline {{}} -tags secsel \
3468                   -fill [$canv cget -selectbackground]]
3469        $canv lower $t
3470    }
3471}
3472
3473proc bolden_name {row font} {
3474    global canv2 linentag selectedline boldnamerows
3475
3476    lappend boldnamerows $row
3477    $canv2 itemconf $linentag($row) -font $font
3478    if {$row == $selectedline} {
3479        $canv2 delete secsel
3480        set t [eval $canv2 create rect [$canv2 bbox $linentag($row)] \
3481                   -outline {{}} -tags secsel \
3482                   -fill [$canv2 cget -selectbackground]]
3483        $canv2 lower $t
3484    }
3485}
3486
3487proc unbolden {} {
3488    global boldrows
3489
3490    set stillbold {}
3491    foreach row $boldrows {
3492        if {![ishighlighted [commitonrow $row]]} {
3493            bolden $row mainfont
3494        } else {
3495            lappend stillbold $row
3496        }
3497    }
3498    set boldrows $stillbold
3499}
3500
3501proc addvhighlight {n} {
3502    global hlview viewcomplete curview vhl_done commitidx
3503
3504    if {[info exists hlview]} {
3505        delvhighlight
3506    }
3507    set hlview $n
3508    if {$n != $curview && ![info exists viewcomplete($n)]} {
3509        start_rev_list $n
3510    }
3511    set vhl_done $commitidx($hlview)
3512    if {$vhl_done > 0} {
3513        drawvisible
3514    }
3515}
3516
3517proc delvhighlight {} {
3518    global hlview vhighlights
3519
3520    if {![info exists hlview]} return
3521    unset hlview
3522    catch {unset vhighlights}
3523    unbolden
3524}
3525
3526proc vhighlightmore {} {
3527    global hlview vhl_done commitidx vhighlights curview
3528
3529    set max $commitidx($hlview)
3530    set vr [visiblerows]
3531    set r0 [lindex $vr 0]
3532    set r1 [lindex $vr 1]
3533    for {set i $vhl_done} {$i < $max} {incr i} {
3534        set id [commitonrow $i $hlview]
3535        if {[commitinview $id $curview]} {
3536            set row [rowofcommit $id]
3537            if {$r0 <= $row && $row <= $r1} {
3538                if {![highlighted $row]} {
3539                    bolden $row mainfontbold
3540                }
3541                set vhighlights($id) 1
3542            }
3543        }
3544    }
3545    set vhl_done $max
3546    return 0
3547}
3548
3549proc askvhighlight {row id} {
3550    global hlview vhighlights iddrawn
3551
3552    if {[commitinview $id $hlview]} {
3553        if {[info exists iddrawn($id)] && ![ishighlighted $id]} {
3554            bolden $row mainfontbold
3555        }
3556        set vhighlights($id) 1
3557    } else {
3558        set vhighlights($id) 0
3559    }
3560}
3561
3562proc hfiles_change {} {
3563    global highlight_files filehighlight fhighlights fh_serial
3564    global highlight_paths gdttype
3565
3566    if {[info exists filehighlight]} {
3567        # delete previous highlights
3568        catch {close $filehighlight}
3569        unset filehighlight
3570        catch {unset fhighlights}
3571        unbolden
3572        unhighlight_filelist
3573    }
3574    set highlight_paths {}
3575    after cancel do_file_hl $fh_serial
3576    incr fh_serial
3577    if {$highlight_files ne {}} {
3578        after 300 do_file_hl $fh_serial
3579    }
3580}
3581
3582proc gdttype_change {name ix op} {
3583    global gdttype highlight_files findstring findpattern
3584
3585    stopfinding
3586    if {$findstring ne {}} {
3587        if {$gdttype eq [mc "containing:"]} {
3588            if {$highlight_files ne {}} {
3589                set highlight_files {}
3590                hfiles_change
3591            }
3592            findcom_change
3593        } else {
3594            if {$findpattern ne {}} {
3595                set findpattern {}
3596                findcom_change
3597            }
3598            set highlight_files $findstring
3599            hfiles_change
3600        }
3601        drawvisible
3602    }
3603    # enable/disable findtype/findloc menus too
3604}
3605
3606proc find_change {name ix op} {
3607    global gdttype findstring highlight_files
3608
3609    stopfinding
3610    if {$gdttype eq [mc "containing:"]} {
3611        findcom_change
3612    } else {
3613        if {$highlight_files ne $findstring} {
3614            set highlight_files $findstring
3615            hfiles_change
3616        }
3617    }
3618    drawvisible
3619}
3620
3621proc findcom_change args {
3622    global nhighlights boldnamerows
3623    global findpattern findtype findstring gdttype
3624
3625    stopfinding
3626    # delete previous highlights, if any
3627    foreach row $boldnamerows {
3628        bolden_name $row mainfont
3629    }
3630    set boldnamerows {}
3631    catch {unset nhighlights}
3632    unbolden
3633    unmarkmatches
3634    if {$gdttype ne [mc "containing:"] || $findstring eq {}} {
3635        set findpattern {}
3636    } elseif {$findtype eq [mc "Regexp"]} {
3637        set findpattern $findstring
3638    } else {
3639        set e [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} \
3640                   $findstring]
3641        set findpattern "*$e*"
3642    }
3643}
3644
3645proc makepatterns {l} {
3646    set ret {}
3647    foreach e $l {
3648        set ee [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} $e]
3649        if {[string index $ee end] eq "/"} {
3650            lappend ret "$ee*"
3651        } else {
3652            lappend ret $ee
3653            lappend ret "$ee/*"
3654        }
3655    }
3656    return $ret
3657}
3658
3659proc do_file_hl {serial} {
3660    global highlight_files filehighlight highlight_paths gdttype fhl_list
3661
3662    if {$gdttype eq [mc "touching paths:"]} {
3663        if {[catch {set paths [shellsplit $highlight_files]}]} return
3664        set highlight_paths [makepatterns $paths]
3665        highlight_filelist
3666        set gdtargs [concat -- $paths]
3667    } elseif {$gdttype eq [mc "adding/removing string:"]} {
3668        set gdtargs [list "-S$highlight_files"]
3669    } else {
3670        # must be "containing:", i.e. we're searching commit info
3671        return
3672    }
3673    set cmd [concat | git diff-tree -r -s --stdin $gdtargs]
3674    set filehighlight [open $cmd r+]
3675    fconfigure $filehighlight -blocking 0
3676    filerun $filehighlight readfhighlight
3677    set fhl_list {}
3678    drawvisible
3679    flushhighlights
3680}
3681
3682proc flushhighlights {} {
3683    global filehighlight fhl_list
3684
3685    if {[info exists filehighlight]} {
3686        lappend fhl_list {}
3687        puts $filehighlight ""
3688        flush $filehighlight
3689    }
3690}
3691
3692proc askfilehighlight {row id} {
3693    global filehighlight fhighlights fhl_list
3694
3695    lappend fhl_list $id
3696    set fhighlights($id) -1
3697    puts $filehighlight $id
3698}
3699
3700proc readfhighlight {} {
3701    global filehighlight fhighlights curview iddrawn
3702    global fhl_list find_dirn
3703
3704    if {![info exists filehighlight]} {
3705        return 0
3706    }
3707    set nr 0
3708    while {[incr nr] <= 100 && [gets $filehighlight line] >= 0} {
3709        set line [string trim $line]
3710        set i [lsearch -exact $fhl_list $line]
3711        if {$i < 0} continue
3712        for {set j 0} {$j < $i} {incr j} {
3713            set id [lindex $fhl_list $j]
3714            set fhighlights($id) 0
3715        }
3716        set fhl_list [lrange $fhl_list [expr {$i+1}] end]
3717        if {$line eq {}} continue
3718        if {![commitinview $line $curview]} continue
3719        set row [rowofcommit $line]
3720        if {[info exists iddrawn($line)] && ![ishighlighted $line]} {
3721            bolden $row mainfontbold
3722        }
3723        set fhighlights($line) 1
3724    }
3725    if {[eof $filehighlight]} {
3726        # strange...
3727        puts "oops, git diff-tree died"
3728        catch {close $filehighlight}
3729        unset filehighlight
3730        return 0
3731    }
3732    if {[info exists find_dirn]} {
3733        run findmore
3734    }
3735    return 1
3736}
3737
3738proc doesmatch {f} {
3739    global findtype findpattern
3740
3741    if {$findtype eq [mc "Regexp"]} {
3742        return [regexp $findpattern $f]
3743    } elseif {$findtype eq [mc "IgnCase"]} {
3744        return [string match -nocase $findpattern $f]
3745    } else {
3746        return [string match $findpattern $f]
3747    }
3748}
3749
3750proc askfindhighlight {row id} {
3751    global nhighlights commitinfo iddrawn
3752    global findloc
3753    global markingmatches
3754
3755    if {![info exists commitinfo($id)]} {
3756        getcommit $id
3757    }
3758    set info $commitinfo($id)
3759    set isbold 0
3760    set fldtypes [list [mc Headline] [mc Author] [mc Date] [mc Committer] [mc CDate] [mc Comments]]
3761    foreach f $info ty $fldtypes {
3762        if {($findloc eq [mc "All fields"] || $findloc eq $ty) &&
3763            [doesmatch $f]} {
3764            if {$ty eq [mc "Author"]} {
3765                set isbold 2
3766                break
3767            }
3768            set isbold 1
3769        }
3770    }
3771    if {$isbold && [info exists iddrawn($id)]} {
3772        if {![ishighlighted $id]} {
3773            bolden $row mainfontbold
3774            if {$isbold > 1} {
3775                bolden_name $row mainfontbold
3776            }
3777        }
3778        if {$markingmatches} {
3779            markrowmatches $row $id
3780        }
3781    }
3782    set nhighlights($id) $isbold
3783}
3784
3785proc markrowmatches {row id} {
3786    global canv canv2 linehtag linentag commitinfo findloc
3787
3788    set headline [lindex $commitinfo($id) 0]
3789    set author [lindex $commitinfo($id) 1]
3790    $canv delete match$row
3791    $canv2 delete match$row
3792    if {$findloc eq [mc "All fields"] || $findloc eq [mc "Headline"]} {
3793        set m [findmatches $headline]
3794        if {$m ne {}} {
3795            markmatches $canv $row $headline $linehtag($row) $m \
3796                [$canv itemcget $linehtag($row) -font] $row
3797        }
3798    }
3799    if {$findloc eq [mc "All fields"] || $findloc eq [mc "Author"]} {
3800        set m [findmatches $author]
3801        if {$m ne {}} {
3802            markmatches $canv2 $row $author $linentag($row) $m \
3803                [$canv2 itemcget $linentag($row) -font] $row
3804        }
3805    }
3806}
3807
3808proc vrel_change {name ix op} {
3809    global highlight_related
3810
3811    rhighlight_none
3812    if {$highlight_related ne [mc "None"]} {
3813        run drawvisible
3814    }
3815}
3816
3817# prepare for testing whether commits are descendents or ancestors of a
3818proc rhighlight_sel {a} {
3819    global descendent desc_todo ancestor anc_todo
3820    global highlight_related
3821
3822    catch {unset descendent}
3823    set desc_todo [list $a]
3824    catch {unset ancestor}
3825    set anc_todo [list $a]
3826    if {$highlight_related ne [mc "None"]} {
3827        rhighlight_none
3828        run drawvisible
3829    }
3830}
3831
3832proc rhighlight_none {} {
3833    global rhighlights
3834
3835    catch {unset rhighlights}
3836    unbolden
3837}
3838
3839proc is_descendent {a} {
3840    global curview children descendent desc_todo
3841
3842    set v $curview
3843    set la [rowofcommit $a]
3844    set todo $desc_todo
3845    set leftover {}
3846    set done 0
3847    for {set i 0} {$i < [llength $todo]} {incr i} {
3848        set do [lindex $todo $i]
3849        if {[rowofcommit $do] < $la} {
3850            lappend leftover $do
3851            continue
3852        }
3853        foreach nk $children($v,$do) {
3854            if {![info exists descendent($nk)]} {
3855                set descendent($nk) 1
3856                lappend todo $nk
3857                if {$nk eq $a} {
3858                    set done 1
3859                }
3860            }
3861        }
3862        if {$done} {
3863            set desc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
3864            return
3865        }
3866    }
3867    set descendent($a) 0
3868    set desc_todo $leftover
3869}
3870
3871proc is_ancestor {a} {
3872    global curview parents ancestor anc_todo
3873
3874    set v $curview
3875    set la [rowofcommit $a]
3876    set todo $anc_todo
3877    set leftover {}
3878    set done 0
3879    for {set i 0} {$i < [llength $todo]} {incr i} {
3880        set do [lindex $todo $i]
3881        if {![commitinview $do $v] || [rowofcommit $do] > $la} {
3882            lappend leftover $do
3883            continue
3884        }
3885        foreach np $parents($v,$do) {
3886            if {![info exists ancestor($np)]} {
3887                set ancestor($np) 1
3888                lappend todo $np
3889                if {$np eq $a} {
3890                    set done 1
3891                }
3892            }
3893        }
3894        if {$done} {
3895            set anc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
3896            return
3897        }
3898    }
3899    set ancestor($a) 0
3900    set anc_todo $leftover
3901}
3902
3903proc askrelhighlight {row id} {
3904    global descendent highlight_related iddrawn rhighlights
3905    global selectedline ancestor
3906
3907    if {$selectedline eq {}} return
3908    set isbold 0
3909    if {$highlight_related eq [mc "Descendant"] ||
3910        $highlight_related eq [mc "Not descendant"]} {
3911        if {![info exists descendent($id)]} {
3912            is_descendent $id
3913        }
3914        if {$descendent($id) == ($highlight_related eq [mc "Descendant"])} {
3915            set isbold 1
3916        }
3917    } elseif {$highlight_related eq [mc "Ancestor"] ||
3918              $highlight_related eq [mc "Not ancestor"]} {
3919        if {![info exists ancestor($id)]} {
3920            is_ancestor $id
3921        }
3922        if {$ancestor($id) == ($highlight_related eq [mc "Ancestor"])} {
3923            set isbold 1
3924        }
3925    }
3926    if {[info exists iddrawn($id)]} {
3927        if {$isbold && ![ishighlighted $id]} {
3928            bolden $row mainfontbold
3929        }
3930    }
3931    set rhighlights($id) $isbold
3932}
3933
3934# Graph layout functions
3935
3936proc shortids {ids} {
3937    set res {}
3938    foreach id $ids {
3939        if {[llength $id] > 1} {
3940            lappend res [shortids $id]
3941        } elseif {[regexp {^[0-9a-f]{40}$} $id]} {
3942            lappend res [string range $id 0 7]
3943        } else {
3944            lappend res $id
3945        }
3946    }
3947    return $res
3948}
3949
3950proc ntimes {n o} {
3951    set ret {}
3952    set o [list $o]
3953    for {set mask 1} {$mask <= $n} {incr mask $mask} {
3954        if {($n & $mask) != 0} {
3955            set ret [concat $ret $o]
3956        }
3957        set o [concat $o $o]
3958    }
3959    return $ret
3960}
3961
3962proc ordertoken {id} {
3963    global ordertok curview varcid varcstart varctok curview parents children
3964    global nullid nullid2
3965
3966    if {[info exists ordertok($id)]} {
3967        return $ordertok($id)
3968    }
3969    set origid $id
3970    set todo {}
3971    while {1} {
3972        if {[info exists varcid($curview,$id)]} {
3973            set a $varcid($curview,$id)
3974            set p [lindex $varcstart($curview) $a]
3975        } else {
3976            set p [lindex $children($curview,$id) 0]
3977        }
3978        if {[info exists ordertok($p)]} {
3979            set tok $ordertok($p)
3980            break
3981        }
3982        set id [first_real_child $curview,$p]
3983        if {$id eq {}} {
3984            # it's a root
3985            set tok [lindex $varctok($curview) $varcid($curview,$p)]
3986            break
3987        }
3988        if {[llength $parents($curview,$id)] == 1} {
3989            lappend todo [list $p {}]
3990        } else {
3991            set j [lsearch -exact $parents($curview,$id) $p]
3992            if {$j < 0} {
3993                puts "oops didn't find [shortids $p] in parents of [shortids $id]"
3994            }
3995            lappend todo [list $p [strrep $j]]
3996        }
3997    }
3998    for {set i [llength $todo]} {[incr i -1] >= 0} {} {
3999        set p [lindex $todo $i 0]
4000        append tok [lindex $todo $i 1]
4001        set ordertok($p) $tok
4002    }
4003    set ordertok($origid) $tok
4004    return $tok
4005}
4006
4007# Work out where id should go in idlist so that order-token
4008# values increase from left to right
4009proc idcol {idlist id {i 0}} {
4010    set t [ordertoken $id]
4011    if {$i < 0} {
4012        set i 0
4013    }
4014    if {$i >= [llength $idlist] || $t < [ordertoken [lindex $idlist $i]]} {
4015        if {$i > [llength $idlist]} {
4016            set i [llength $idlist]
4017        }
4018        while {[incr i -1] >= 0 && $t < [ordertoken [lindex $idlist $i]]} {}
4019        incr i
4020    } else {
4021        if {$t > [ordertoken [lindex $idlist $i]]} {
4022            while {[incr i] < [llength $idlist] &&
4023                   $t >= [ordertoken [lindex $idlist $i]]} {}
4024        }
4025    }
4026    return $i
4027}
4028
4029proc initlayout {} {
4030    global rowidlist rowisopt rowfinal displayorder parentlist
4031    global numcommits canvxmax canv
4032    global nextcolor
4033    global colormap rowtextx
4034
4035    set numcommits 0
4036    set displayorder {}
4037    set parentlist {}
4038    set nextcolor 0
4039    set rowidlist {}
4040    set rowisopt {}
4041    set rowfinal {}
4042    set canvxmax [$canv cget -width]
4043    catch {unset colormap}
4044    catch {unset rowtextx}
4045    setcanvscroll
4046}
4047
4048proc setcanvscroll {} {
4049    global canv canv2 canv3 numcommits linespc canvxmax canvy0
4050    global lastscrollset lastscrollrows
4051
4052    set ymax [expr {$canvy0 + ($numcommits - 0.5) * $linespc + 2}]
4053    $canv conf -scrollregion [list 0 0 $canvxmax $ymax]
4054    $canv2 conf -scrollregion [list 0 0 0 $ymax]
4055    $canv3 conf -scrollregion [list 0 0 0 $ymax]
4056    set lastscrollset [clock clicks -milliseconds]
4057    set lastscrollrows $numcommits
4058}
4059
4060proc visiblerows {} {
4061    global canv numcommits linespc
4062
4063    set ymax [lindex [$canv cget -scrollregion] 3]
4064    if {$ymax eq {} || $ymax == 0} return
4065    set f [$canv yview]
4066    set y0 [expr {int([lindex $f 0] * $ymax)}]
4067    set r0 [expr {int(($y0 - 3) / $linespc) - 1}]
4068    if {$r0 < 0} {
4069        set r0 0
4070    }
4071    set y1 [expr {int([lindex $f 1] * $ymax)}]
4072    set r1 [expr {int(($y1 - 3) / $linespc) + 1}]
4073    if {$r1 >= $numcommits} {
4074        set r1 [expr {$numcommits - 1}]
4075    }
4076    return [list $r0 $r1]
4077}
4078
4079proc layoutmore {} {
4080    global commitidx viewcomplete curview
4081    global numcommits pending_select curview
4082    global lastscrollset lastscrollrows commitinterest
4083
4084    if {$lastscrollrows < 100 || $viewcomplete($curview) ||
4085        [clock clicks -milliseconds] - $lastscrollset > 500} {
4086        setcanvscroll
4087    }
4088    if {[info exists pending_select] &&
4089        [commitinview $pending_select $curview]} {
4090        update
4091        selectline [rowofcommit $pending_select] 1
4092    }
4093    drawvisible
4094}
4095
4096proc doshowlocalchanges {} {
4097    global curview mainheadid
4098
4099    if {$mainheadid eq {}} return
4100    if {[commitinview $mainheadid $curview]} {
4101        dodiffindex
4102    } else {
4103        lappend commitinterest($mainheadid) {dodiffindex}
4104    }
4105}
4106
4107proc dohidelocalchanges {} {
4108    global nullid nullid2 lserial curview
4109
4110    if {[commitinview $nullid $curview]} {
4111        removefakerow $nullid
4112    }
4113    if {[commitinview $nullid2 $curview]} {
4114        removefakerow $nullid2
4115    }
4116    incr lserial
4117}
4118
4119# spawn off a process to do git diff-index --cached HEAD
4120proc dodiffindex {} {
4121    global lserial showlocalchanges
4122    global isworktree
4123
4124    if {!$showlocalchanges || !$isworktree} return
4125    incr lserial
4126    set fd [open "|git diff-index --cached HEAD" r]
4127    fconfigure $fd -blocking 0
4128    set i [reg_instance $fd]
4129    filerun $fd [list readdiffindex $fd $lserial $i]
4130}
4131
4132proc readdiffindex {fd serial inst} {
4133    global mainheadid nullid nullid2 curview commitinfo commitdata lserial
4134
4135    set isdiff 1
4136    if {[gets $fd line] < 0} {
4137        if {![eof $fd]} {
4138            return 1
4139        }
4140        set isdiff 0
4141    }
4142    # we only need to see one line and we don't really care what it says...
4143    stop_instance $inst
4144
4145    if {$serial != $lserial} {
4146        return 0
4147    }
4148
4149    # now see if there are any local changes not checked in to the index
4150    set fd [open "|git diff-files" r]
4151    fconfigure $fd -blocking 0
4152    set i [reg_instance $fd]
4153    filerun $fd [list readdifffiles $fd $serial $i]
4154
4155    if {$isdiff && ![commitinview $nullid2 $curview]} {
4156        # add the line for the changes in the index to the graph
4157        set hl [mc "Local changes checked in to index but not committed"]
4158        set commitinfo($nullid2) [list  $hl {} {} {} {} "    $hl\n"]
4159        set commitdata($nullid2) "\n    $hl\n"
4160        if {[commitinview $nullid $curview]} {
4161            removefakerow $nullid
4162        }
4163        insertfakerow $nullid2 $mainheadid
4164    } elseif {!$isdiff && [commitinview $nullid2 $curview]} {
4165        removefakerow $nullid2
4166    }
4167    return 0
4168}
4169
4170proc readdifffiles {fd serial inst} {
4171    global mainheadid nullid nullid2 curview
4172    global commitinfo commitdata lserial
4173
4174    set isdiff 1
4175    if {[gets $fd line] < 0} {
4176        if {![eof $fd]} {
4177            return 1
4178        }
4179        set isdiff 0
4180    }
4181    # we only need to see one line and we don't really care what it says...
4182    stop_instance $inst
4183
4184    if {$serial != $lserial} {
4185        return 0
4186    }
4187
4188    if {$isdiff && ![commitinview $nullid $curview]} {
4189        # add the line for the local diff to the graph
4190        set hl [mc "Local uncommitted changes, not checked in to index"]
4191        set commitinfo($nullid) [list  $hl {} {} {} {} "    $hl\n"]
4192        set commitdata($nullid) "\n    $hl\n"
4193        if {[commitinview $nullid2 $curview]} {
4194            set p $nullid2
4195        } else {
4196            set p $mainheadid
4197        }
4198        insertfakerow $nullid $p
4199    } elseif {!$isdiff && [commitinview $nullid $curview]} {
4200        removefakerow $nullid
4201    }
4202    return 0
4203}
4204
4205proc nextuse {id row} {
4206    global curview children
4207
4208    if {[info exists children($curview,$id)]} {
4209        foreach kid $children($curview,$id) {
4210            if {![commitinview $kid $curview]} {
4211                return -1
4212            }
4213            if {[rowofcommit $kid] > $row} {
4214                return [rowofcommit $kid]
4215            }
4216        }
4217    }
4218    if {[commitinview $id $curview]} {
4219        return [rowofcommit $id]
4220    }
4221    return -1
4222}
4223
4224proc prevuse {id row} {
4225    global curview children
4226
4227    set ret -1
4228    if {[info exists children($curview,$id)]} {
4229        foreach kid $children($curview,$id) {
4230            if {![commitinview $kid $curview]} break
4231            if {[rowofcommit $kid] < $row} {
4232                set ret [rowofcommit $kid]
4233            }
4234        }
4235    }
4236    return $ret
4237}
4238
4239proc make_idlist {row} {
4240    global displayorder parentlist uparrowlen downarrowlen mingaplen
4241    global commitidx curview children
4242
4243    set r [expr {$row - $mingaplen - $downarrowlen - 1}]
4244    if {$r < 0} {
4245        set r 0
4246    }
4247    set ra [expr {$row - $downarrowlen}]
4248    if {$ra < 0} {
4249        set ra 0
4250    }
4251    set rb [expr {$row + $uparrowlen}]
4252    if {$rb > $commitidx($curview)} {
4253        set rb $commitidx($curview)
4254    }
4255    make_disporder $r [expr {$rb + 1}]
4256    set ids {}
4257    for {} {$r < $ra} {incr r} {
4258        set nextid [lindex $displayorder [expr {$r + 1}]]
4259        foreach p [lindex $parentlist $r] {
4260            if {$p eq $nextid} continue
4261            set rn [nextuse $p $r]
4262            if {$rn >= $row &&
4263                $rn <= $r + $downarrowlen + $mingaplen + $uparrowlen} {
4264                lappend ids [list [ordertoken $p] $p]
4265            }
4266        }
4267    }
4268    for {} {$r < $row} {incr r} {
4269        set nextid [lindex $displayorder [expr {$r + 1}]]
4270        foreach p [lindex $parentlist $r] {
4271            if {$p eq $nextid} continue
4272            set rn [nextuse $p $r]
4273            if {$rn < 0 || $rn >= $row} {
4274                lappend ids [list [ordertoken $p] $p]
4275            }
4276        }
4277    }
4278    set id [lindex $displayorder $row]
4279    lappend ids [list [ordertoken $id] $id]
4280    while {$r < $rb} {
4281        foreach p [lindex $parentlist $r] {
4282            set firstkid [lindex $children($curview,$p) 0]
4283            if {[rowofcommit $firstkid] < $row} {
4284                lappend ids [list [ordertoken $p] $p]
4285            }
4286        }
4287        incr r
4288        set id [lindex $displayorder $r]
4289        if {$id ne {}} {
4290            set firstkid [lindex $children($curview,$id) 0]
4291            if {$firstkid ne {} && [rowofcommit $firstkid] < $row} {
4292                lappend ids [list [ordertoken $id] $id]
4293            }
4294        }
4295    }
4296    set idlist {}
4297    foreach idx [lsort -unique $ids] {
4298        lappend idlist [lindex $idx 1]
4299    }
4300    return $idlist
4301}
4302
4303proc rowsequal {a b} {
4304    while {[set i [lsearch -exact $a {}]] >= 0} {
4305        set a [lreplace $a $i $i]
4306    }
4307    while {[set i [lsearch -exact $b {}]] >= 0} {
4308        set b [lreplace $b $i $i]
4309    }
4310    return [expr {$a eq $b}]
4311}
4312
4313proc makeupline {id row rend col} {
4314    global rowidlist uparrowlen downarrowlen mingaplen
4315
4316    for {set r $rend} {1} {set r $rstart} {
4317        set rstart [prevuse $id $r]
4318        if {$rstart < 0} return
4319        if {$rstart < $row} break
4320    }
4321    if {$rstart + $uparrowlen + $mingaplen + $downarrowlen < $rend} {
4322        set rstart [expr {$rend - $uparrowlen - 1}]
4323    }
4324    for {set r $rstart} {[incr r] <= $row} {} {
4325        set idlist [lindex $rowidlist $r]
4326        if {$idlist ne {} && [lsearch -exact $idlist $id] < 0} {
4327            set col [idcol $idlist $id $col]
4328            lset rowidlist $r [linsert $idlist $col $id]
4329            changedrow $r
4330        }
4331    }
4332}
4333
4334proc layoutrows {row endrow} {
4335    global rowidlist rowisopt rowfinal displayorder
4336    global uparrowlen downarrowlen maxwidth mingaplen
4337    global children parentlist
4338    global commitidx viewcomplete curview
4339
4340    make_disporder [expr {$row - 1}] [expr {$endrow + $uparrowlen}]
4341    set idlist {}
4342    if {$row > 0} {
4343        set rm1 [expr {$row - 1}]
4344        foreach id [lindex $rowidlist $rm1] {
4345            if {$id ne {}} {
4346                lappend idlist $id
4347            }
4348        }
4349        set final [lindex $rowfinal $rm1]
4350    }
4351    for {} {$row < $endrow} {incr row} {
4352        set rm1 [expr {$row - 1}]
4353        if {$rm1 < 0 || $idlist eq {}} {
4354            set idlist [make_idlist $row]
4355            set final 1
4356        } else {
4357            set id [lindex $displayorder $rm1]
4358            set col [lsearch -exact $idlist $id]
4359            set idlist [lreplace $idlist $col $col]
4360            foreach p [lindex $parentlist $rm1] {
4361                if {[lsearch -exact $idlist $p] < 0} {
4362                    set col [idcol $idlist $p $col]
4363                    set idlist [linsert $idlist $col $p]
4364                    # if not the first child, we have to insert a line going up
4365                    if {$id ne [lindex $children($curview,$p) 0]} {
4366                        makeupline $p $rm1 $row $col
4367                    }
4368                }
4369            }
4370            set id [lindex $displayorder $row]
4371            if {$row > $downarrowlen} {
4372                set termrow [expr {$row - $downarrowlen - 1}]
4373                foreach p [lindex $parentlist $termrow] {
4374                    set i [lsearch -exact $idlist $p]
4375                    if {$i < 0} continue
4376                    set nr [nextuse $p $termrow]
4377                    if {$nr < 0 || $nr >= $row + $mingaplen + $uparrowlen} {
4378                        set idlist [lreplace $idlist $i $i]
4379                    }
4380                }
4381            }
4382            set col [lsearch -exact $idlist $id]
4383            if {$col < 0} {
4384                set col [idcol $idlist $id]
4385                set idlist [linsert $idlist $col $id]
4386                if {$children($curview,$id) ne {}} {
4387                    makeupline $id $rm1 $row $col
4388                }
4389            }
4390            set r [expr {$row + $uparrowlen - 1}]
4391            if {$r < $commitidx($curview)} {
4392                set x $col
4393                foreach p [lindex $parentlist $r] {
4394                    if {[lsearch -exact $idlist $p] >= 0} continue
4395                    set fk [lindex $children($curview,$p) 0]
4396                    if {[rowofcommit $fk] < $row} {
4397                        set x [idcol $idlist $p $x]
4398                        set idlist [linsert $idlist $x $p]
4399                    }
4400                }
4401                if {[incr r] < $commitidx($curview)} {
4402                    set p [lindex $displayorder $r]
4403                    if {[lsearch -exact $idlist $p] < 0} {
4404                        set fk [lindex $children($curview,$p) 0]
4405                        if {$fk ne {} && [rowofcommit $fk] < $row} {
4406                            set x [idcol $idlist $p $x]
4407                            set idlist [linsert $idlist $x $p]
4408                        }
4409                    }
4410                }
4411            }
4412        }
4413        if {$final && !$viewcomplete($curview) &&
4414            $row + $uparrowlen + $mingaplen + $downarrowlen
4415                >= $commitidx($curview)} {
4416            set final 0
4417        }
4418        set l [llength $rowidlist]
4419        if {$row == $l} {
4420            lappend rowidlist $idlist
4421            lappend rowisopt 0
4422            lappend rowfinal $final
4423        } elseif {$row < $l} {
4424            if {![rowsequal $idlist [lindex $rowidlist $row]]} {
4425                lset rowidlist $row $idlist
4426                changedrow $row
4427            }
4428            lset rowfinal $row $final
4429        } else {
4430            set pad [ntimes [expr {$row - $l}] {}]
4431            set rowidlist [concat $rowidlist $pad]
4432            lappend rowidlist $idlist
4433            set rowfinal [concat $rowfinal $pad]
4434            lappend rowfinal $final
4435            set rowisopt [concat $rowisopt [ntimes [expr {$row - $l + 1}] 0]]
4436        }
4437    }
4438    return $row
4439}
4440
4441proc changedrow {row} {
4442    global displayorder iddrawn rowisopt need_redisplay
4443
4444    set l [llength $rowisopt]
4445    if {$row < $l} {
4446        lset rowisopt $row 0
4447        if {$row + 1 < $l} {
4448            lset rowisopt [expr {$row + 1}] 0
4449            if {$row + 2 < $l} {
4450                lset rowisopt [expr {$row + 2}] 0
4451            }
4452        }
4453    }
4454    set id [lindex $displayorder $row]
4455    if {[info exists iddrawn($id)]} {
4456        set need_redisplay 1
4457    }
4458}
4459
4460proc insert_pad {row col npad} {
4461    global rowidlist
4462
4463    set pad [ntimes $npad {}]
4464    set idlist [lindex $rowidlist $row]
4465    set bef [lrange $idlist 0 [expr {$col - 1}]]
4466    set aft [lrange $idlist $col end]
4467    set i [lsearch -exact $aft {}]
4468    if {$i > 0} {
4469        set aft [lreplace $aft $i $i]
4470    }
4471    lset rowidlist $row [concat $bef $pad $aft]
4472    changedrow $row
4473}
4474
4475proc optimize_rows {row col endrow} {
4476    global rowidlist rowisopt displayorder curview children
4477
4478    if {$row < 1} {
4479        set row 1
4480    }
4481    for {} {$row < $endrow} {incr row; set col 0} {
4482        if {[lindex $rowisopt $row]} continue
4483        set haspad 0
4484        set y0 [expr {$row - 1}]
4485        set ym [expr {$row - 2}]
4486        set idlist [lindex $rowidlist $row]
4487        set previdlist [lindex $rowidlist $y0]
4488        if {$idlist eq {} || $previdlist eq {}} continue
4489        if {$ym >= 0} {
4490            set pprevidlist [lindex $rowidlist $ym]
4491            if {$pprevidlist eq {}} continue
4492        } else {
4493            set pprevidlist {}
4494        }
4495        set x0 -1
4496        set xm -1
4497        for {} {$col < [llength $idlist]} {incr col} {
4498            set id [lindex $idlist $col]
4499            if {[lindex $previdlist $col] eq $id} continue
4500            if {$id eq {}} {
4501                set haspad 1
4502                continue
4503            }
4504            set x0 [lsearch -exact $previdlist $id]
4505            if {$x0 < 0} continue
4506            set z [expr {$x0 - $col}]
4507            set isarrow 0
4508            set z0 {}
4509            if {$ym >= 0} {
4510                set xm [lsearch -exact $pprevidlist $id]
4511                if {$xm >= 0} {
4512                    set z0 [expr {$xm - $x0}]
4513                }
4514            }
4515            if {$z0 eq {}} {
4516                # if row y0 is the first child of $id then it's not an arrow
4517                if {[lindex $children($curview,$id) 0] ne
4518                    [lindex $displayorder $y0]} {
4519                    set isarrow 1
4520                }
4521            }
4522            if {!$isarrow && $id ne [lindex $displayorder $row] &&
4523                [lsearch -exact [lindex $rowidlist [expr {$row+1}]] $id] < 0} {
4524                set isarrow 1
4525            }
4526            # Looking at lines from this row to the previous row,
4527            # make them go straight up if they end in an arrow on
4528            # the previous row; otherwise make them go straight up
4529            # or at 45 degrees.
4530            if {$z < -1 || ($z < 0 && $isarrow)} {
4531                # Line currently goes left too much;
4532                # insert pads in the previous row, then optimize it
4533                set npad [expr {-1 - $z + $isarrow}]
4534                insert_pad $y0 $x0 $npad
4535                if {$y0 > 0} {
4536                    optimize_rows $y0 $x0 $row
4537                }
4538                set previdlist [lindex $rowidlist $y0]
4539                set x0 [lsearch -exact $previdlist $id]
4540                set z [expr {$x0 - $col}]
4541                if {$z0 ne {}} {
4542                    set pprevidlist [lindex $rowidlist $ym]
4543                    set xm [lsearch -exact $pprevidlist $id]
4544                    set z0 [expr {$xm - $x0}]
4545                }
4546            } elseif {$z > 1 || ($z > 0 && $isarrow)} {
4547                # Line currently goes right too much;
4548                # insert pads in this line
4549                set npad [expr {$z - 1 + $isarrow}]
4550                insert_pad $row $col $npad
4551                set idlist [lindex $rowidlist $row]
4552                incr col $npad
4553                set z [expr {$x0 - $col}]
4554                set haspad 1
4555            }
4556            if {$z0 eq {} && !$isarrow && $ym >= 0} {
4557                # this line links to its first child on row $row-2
4558                set id [lindex $displayorder $ym]
4559                set xc [lsearch -exact $pprevidlist $id]
4560                if {$xc >= 0} {
4561                    set z0 [expr {$xc - $x0}]
4562                }
4563            }
4564            # avoid lines jigging left then immediately right
4565            if {$z0 ne {} && $z < 0 && $z0 > 0} {
4566                insert_pad $y0 $x0 1
4567                incr x0
4568                optimize_rows $y0 $x0 $row
4569                set previdlist [lindex $rowidlist $y0]
4570            }
4571        }
4572        if {!$haspad} {
4573            # Find the first column that doesn't have a line going right
4574            for {set col [llength $idlist]} {[incr col -1] >= 0} {} {
4575                set id [lindex $idlist $col]
4576                if {$id eq {}} break
4577                set x0 [lsearch -exact $previdlist $id]
4578                if {$x0 < 0} {
4579                    # check if this is the link to the first child
4580                    set kid [lindex $displayorder $y0]
4581                    if {[lindex $children($curview,$id) 0] eq $kid} {
4582                        # it is, work out offset to child
4583                        set x0 [lsearch -exact $previdlist $kid]
4584                    }
4585                }
4586                if {$x0 <= $col} break
4587            }
4588            # Insert a pad at that column as long as it has a line and
4589            # isn't the last column
4590            if {$x0 >= 0 && [incr col] < [llength $idlist]} {
4591                set idlist [linsert $idlist $col {}]
4592                lset rowidlist $row $idlist
4593                changedrow $row
4594            }
4595        }
4596    }
4597}
4598
4599proc xc {row col} {
4600    global canvx0 linespc
4601    return [expr {$canvx0 + $col * $linespc}]
4602}
4603
4604proc yc {row} {
4605    global canvy0 linespc
4606    return [expr {$canvy0 + $row * $linespc}]
4607}
4608
4609proc linewidth {id} {
4610    global thickerline lthickness
4611
4612    set wid $lthickness
4613    if {[info exists thickerline] && $id eq $thickerline} {
4614        set wid [expr {2 * $lthickness}]
4615    }
4616    return $wid
4617}
4618
4619proc rowranges {id} {
4620    global curview children uparrowlen downarrowlen
4621    global rowidlist
4622
4623    set kids $children($curview,$id)
4624    if {$kids eq {}} {
4625        return {}
4626    }
4627    set ret {}
4628    lappend kids $id
4629    foreach child $kids {
4630        if {![commitinview $child $curview]} break
4631        set row [rowofcommit $child]
4632        if {![info exists prev]} {
4633            lappend ret [expr {$row + 1}]
4634        } else {
4635            if {$row <= $prevrow} {
4636                puts "oops children of [shortids $id] out of order [shortids $child] $row <= [shortids $prev] $prevrow"
4637            }
4638            # see if the line extends the whole way from prevrow to row
4639            if {$row > $prevrow + $uparrowlen + $downarrowlen &&
4640                [lsearch -exact [lindex $rowidlist \
4641                            [expr {int(($row + $prevrow) / 2)}]] $id] < 0} {
4642                # it doesn't, see where it ends
4643                set r [expr {$prevrow + $downarrowlen}]
4644                if {[lsearch -exact [lindex $rowidlist $r] $id] < 0} {
4645                    while {[incr r -1] > $prevrow &&
4646                           [lsearch -exact [lindex $rowidlist $r] $id] < 0} {}
4647                } else {
4648                    while {[incr r] <= $row &&
4649                           [lsearch -exact [lindex $rowidlist $r] $id] >= 0} {}
4650                    incr r -1
4651                }
4652                lappend ret $r
4653                # see where it starts up again
4654                set r [expr {$row - $uparrowlen}]
4655                if {[lsearch -exact [lindex $rowidlist $r] $id] < 0} {
4656                    while {[incr r] < $row &&
4657                           [lsearch -exact [lindex $rowidlist $r] $id] < 0} {}
4658                } else {
4659                    while {[incr r -1] >= $prevrow &&
4660                           [lsearch -exact [lindex $rowidlist $r] $id] >= 0} {}
4661                    incr r
4662                }
4663                lappend ret $r
4664            }
4665        }
4666        if {$child eq $id} {
4667            lappend ret $row
4668        }
4669        set prev $child
4670        set prevrow $row
4671    }
4672    return $ret
4673}
4674
4675proc drawlineseg {id row endrow arrowlow} {
4676    global rowidlist displayorder iddrawn linesegs
4677    global canv colormap linespc curview maxlinelen parentlist
4678
4679    set cols [list [lsearch -exact [lindex $rowidlist $row] $id]]
4680    set le [expr {$row + 1}]
4681    set arrowhigh 1
4682    while {1} {
4683        set c [lsearch -exact [lindex $rowidlist $le] $id]
4684        if {$c < 0} {
4685            incr le -1
4686            break
4687        }
4688        lappend cols $c
4689        set x [lindex $displayorder $le]
4690        if {$x eq $id} {
4691            set arrowhigh 0
4692            break
4693        }
4694        if {[info exists iddrawn($x)] || $le == $endrow} {
4695            set c [lsearch -exact [lindex $rowidlist [expr {$le+1}]] $id]
4696            if {$c >= 0} {
4697                lappend cols $c
4698                set arrowhigh 0
4699            }
4700            break
4701        }
4702        incr le
4703    }
4704    if {$le <= $row} {
4705        return $row
4706    }
4707
4708    set lines {}
4709    set i 0
4710    set joinhigh 0
4711    if {[info exists linesegs($id)]} {
4712        set lines $linesegs($id)
4713        foreach li $lines {
4714            set r0 [lindex $li 0]
4715            if {$r0 > $row} {
4716                if {$r0 == $le && [lindex $li 1] - $row <= $maxlinelen} {
4717                    set joinhigh 1
4718                }
4719                break
4720            }
4721            incr i
4722        }
4723    }
4724    set joinlow 0
4725    if {$i > 0} {
4726        set li [lindex $lines [expr {$i-1}]]
4727        set r1 [lindex $li 1]
4728        if {$r1 == $row && $le - [lindex $li 0] <= $maxlinelen} {
4729            set joinlow 1
4730        }
4731    }
4732
4733    set x [lindex $cols [expr {$le - $row}]]
4734    set xp [lindex $cols [expr {$le - 1 - $row}]]
4735    set dir [expr {$xp - $x}]
4736    if {$joinhigh} {
4737        set ith [lindex $lines $i 2]
4738        set coords [$canv coords $ith]
4739        set ah [$canv itemcget $ith -arrow]
4740        set arrowhigh [expr {$ah eq "first" || $ah eq "both"}]
4741        set x2 [lindex $cols [expr {$le + 1 - $row}]]
4742        if {$x2 ne {} && $x - $x2 == $dir} {
4743            set coords [lrange $coords 0 end-2]
4744        }
4745    } else {
4746        set coords [list [xc $le $x] [yc $le]]
4747    }
4748    if {$joinlow} {
4749        set itl [lindex $lines [expr {$i-1}] 2]
4750        set al [$canv itemcget $itl -arrow]
4751        set arrowlow [expr {$al eq "last" || $al eq "both"}]
4752    } elseif {$arrowlow} {
4753        if {[lsearch -exact [lindex $rowidlist [expr {$row-1}]] $id] >= 0 ||
4754            [lsearch -exact [lindex $parentlist [expr {$row-1}]] $id] >= 0} {
4755            set arrowlow 0
4756        }
4757    }
4758    set arrow [lindex {none first last both} [expr {$arrowhigh + 2*$arrowlow}]]
4759    for {set y $le} {[incr y -1] > $row} {} {
4760        set x $xp
4761        set xp [lindex $cols [expr {$y - 1 - $row}]]
4762        set ndir [expr {$xp - $x}]
4763        if {$dir != $ndir || $xp < 0} {
4764            lappend coords [xc $y $x] [yc $y]
4765        }
4766        set dir $ndir
4767    }
4768    if {!$joinlow} {
4769        if {$xp < 0} {
4770            # join parent line to first child
4771            set ch [lindex $displayorder $row]
4772            set xc [lsearch -exact [lindex $rowidlist $row] $ch]
4773            if {$xc < 0} {
4774                puts "oops: drawlineseg: child $ch not on row $row"
4775            } elseif {$xc != $x} {
4776                if {($arrowhigh && $le == $row + 1) || $dir == 0} {
4777                    set d [expr {int(0.5 * $linespc)}]
4778                    set x1 [xc $row $x]
4779                    if {$xc < $x} {
4780                        set x2 [expr {$x1 - $d}]
4781                    } else {
4782                        set x2 [expr {$x1 + $d}]
4783                    }
4784                    set y2 [yc $row]
4785                    set y1 [expr {$y2 + $d}]
4786                    lappend coords $x1 $y1 $x2 $y2
4787                } elseif {$xc < $x - 1} {
4788                    lappend coords [xc $row [expr {$x-1}]] [yc $row]
4789                } elseif {$xc > $x + 1} {
4790                    lappend coords [xc $row [expr {$x+1}]] [yc $row]
4791                }
4792                set x $xc
4793            }
4794            lappend coords [xc $row $x] [yc $row]
4795        } else {
4796            set xn [xc $row $xp]
4797            set yn [yc $row]
4798            lappend coords $xn $yn
4799        }
4800        if {!$joinhigh} {
4801            assigncolor $id
4802            set t [$canv create line $coords -width [linewidth $id] \
4803                       -fill $colormap($id) -tags lines.$id -arrow $arrow]
4804            $canv lower $t
4805            bindline $t $id
4806            set lines [linsert $lines $i [list $row $le $t]]
4807        } else {
4808            $canv coords $ith $coords
4809            if {$arrow ne $ah} {
4810                $canv itemconf $ith -arrow $arrow
4811            }
4812            lset lines $i 0 $row
4813        }
4814    } else {
4815        set xo [lsearch -exact [lindex $rowidlist [expr {$row - 1}]] $id]
4816        set ndir [expr {$xo - $xp}]
4817        set clow [$canv coords $itl]
4818        if {$dir == $ndir} {
4819            set clow [lrange $clow 2 end]
4820        }
4821        set coords [concat $coords $clow]
4822        if {!$joinhigh} {
4823            lset lines [expr {$i-1}] 1 $le
4824        } else {
4825            # coalesce two pieces
4826            $canv delete $ith
4827            set b [lindex $lines [expr {$i-1}] 0]
4828            set e [lindex $lines $i 1]
4829            set lines [lreplace $lines [expr {$i-1}] $i [list $b $e $itl]]
4830        }
4831        $canv coords $itl $coords
4832        if {$arrow ne $al} {
4833            $canv itemconf $itl -arrow $arrow
4834        }
4835    }
4836
4837    set linesegs($id) $lines
4838    return $le
4839}
4840
4841proc drawparentlinks {id row} {
4842    global rowidlist canv colormap curview parentlist
4843    global idpos linespc
4844
4845    set rowids [lindex $rowidlist $row]
4846    set col [lsearch -exact $rowids $id]
4847    if {$col < 0} return
4848    set olds [lindex $parentlist $row]
4849    set row2 [expr {$row + 1}]
4850    set x [xc $row $col]
4851    set y [yc $row]
4852    set y2 [yc $row2]
4853    set d [expr {int(0.5 * $linespc)}]
4854    set ymid [expr {$y + $d}]
4855    set ids [lindex $rowidlist $row2]
4856    # rmx = right-most X coord used
4857    set rmx 0
4858    foreach p $olds {
4859        set i [lsearch -exact $ids $p]
4860        if {$i < 0} {
4861            puts "oops, parent $p of $id not in list"
4862            continue
4863        }
4864        set x2 [xc $row2 $i]
4865        if {$x2 > $rmx} {
4866            set rmx $x2
4867        }
4868        set j [lsearch -exact $rowids $p]
4869        if {$j < 0} {
4870            # drawlineseg will do this one for us
4871            continue
4872        }
4873        assigncolor $p
4874        # should handle duplicated parents here...
4875        set coords [list $x $y]
4876        if {$i != $col} {
4877            # if attaching to a vertical segment, draw a smaller
4878            # slant for visual distinctness
4879            if {$i == $j} {
4880                if {$i < $col} {
4881                    lappend coords [expr {$x2 + $d}] $y $x2 $ymid
4882                } else {
4883                    lappend coords [expr {$x2 - $d}] $y $x2 $ymid
4884                }
4885            } elseif {$i < $col && $i < $j} {
4886                # segment slants towards us already
4887                lappend coords [xc $row $j] $y
4888            } else {
4889                if {$i < $col - 1} {
4890                    lappend coords [expr {$x2 + $linespc}] $y
4891                } elseif {$i > $col + 1} {
4892                    lappend coords [expr {$x2 - $linespc}] $y
4893                }
4894                lappend coords $x2 $y2
4895            }
4896        } else {
4897            lappend coords $x2 $y2
4898        }
4899        set t [$canv create line $coords -width [linewidth $p] \
4900                   -fill $colormap($p) -tags lines.$p]
4901        $canv lower $t
4902        bindline $t $p
4903    }
4904    if {$rmx > [lindex $idpos($id) 1]} {
4905        lset idpos($id) 1 $rmx
4906        redrawtags $id
4907    }
4908}
4909
4910proc drawlines {id} {
4911    global canv
4912
4913    $canv itemconf lines.$id -width [linewidth $id]
4914}
4915
4916proc drawcmittext {id row col} {
4917    global linespc canv canv2 canv3 fgcolor curview
4918    global cmitlisted commitinfo rowidlist parentlist
4919    global rowtextx idpos idtags idheads idotherrefs
4920    global linehtag linentag linedtag selectedline
4921    global canvxmax boldrows boldnamerows fgcolor
4922    global mainheadid nullid nullid2 circleitem circlecolors
4923
4924    # listed is 0 for boundary, 1 for normal, 2 for negative, 3 for left, 4 for right
4925    set listed $cmitlisted($curview,$id)
4926    if {$id eq $nullid} {
4927        set ofill red
4928    } elseif {$id eq $nullid2} {
4929        set ofill green
4930    } elseif {$id eq $mainheadid} {
4931        set ofill yellow
4932    } else {
4933        set ofill [lindex $circlecolors $listed]
4934    }
4935    set x [xc $row $col]
4936    set y [yc $row]
4937    set orad [expr {$linespc / 3}]
4938    if {$listed <= 2} {
4939        set t [$canv create oval [expr {$x - $orad}] [expr {$y - $orad}] \
4940                   [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
4941                   -fill $ofill -outline $fgcolor -width 1 -tags circle]
4942    } elseif {$listed == 3} {
4943        # triangle pointing left for left-side commits
4944        set t [$canv create polygon \
4945                   [expr {$x - $orad}] $y \
4946                   [expr {$x + $orad - 1}] [expr {$y - $orad}] \
4947                   [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
4948                   -fill $ofill -outline $fgcolor -width 1 -tags circle]
4949    } else {
4950        # triangle pointing right for right-side commits
4951        set t [$canv create polygon \
4952                   [expr {$x + $orad - 1}] $y \
4953                   [expr {$x - $orad}] [expr {$y - $orad}] \
4954                   [expr {$x - $orad}] [expr {$y + $orad - 1}] \
4955                   -fill $ofill -outline $fgcolor -width 1 -tags circle]
4956    }
4957    set circleitem($row) $t
4958    $canv raise $t
4959    $canv bind $t <1> {selcanvline {} %x %y}
4960    set rmx [llength [lindex $rowidlist $row]]
4961    set olds [lindex $parentlist $row]
4962    if {$olds ne {}} {
4963        set nextids [lindex $rowidlist [expr {$row + 1}]]
4964        foreach p $olds {
4965            set i [lsearch -exact $nextids $p]
4966            if {$i > $rmx} {
4967                set rmx $i
4968            }
4969        }
4970    }
4971    set xt [xc $row $rmx]
4972    set rowtextx($row) $xt
4973    set idpos($id) [list $x $xt $y]
4974    if {[info exists idtags($id)] || [info exists idheads($id)]
4975        || [info exists idotherrefs($id)]} {
4976        set xt [drawtags $id $x $xt $y]
4977    }
4978    set headline [lindex $commitinfo($id) 0]
4979    set name [lindex $commitinfo($id) 1]
4980    set date [lindex $commitinfo($id) 2]
4981    set date [formatdate $date]
4982    set font mainfont
4983    set nfont mainfont
4984    set isbold [ishighlighted $id]
4985    if {$isbold > 0} {
4986        lappend boldrows $row
4987        set font mainfontbold
4988        if {$isbold > 1} {
4989            lappend boldnamerows $row
4990            set nfont mainfontbold
4991        }
4992    }
4993    set linehtag($row) [$canv create text $xt $y -anchor w -fill $fgcolor \
4994                            -text $headline -font $font -tags text]
4995    $canv bind $linehtag($row) <Button-3> "rowmenu %X %Y $id"
4996    set linentag($row) [$canv2 create text 3 $y -anchor w -fill $fgcolor \
4997                            -text $name -font $nfont -tags text]
4998    set linedtag($row) [$canv3 create text 3 $y -anchor w -fill $fgcolor \
4999                            -text $date -font mainfont -tags text]
5000    if {$selectedline == $row} {
5001        make_secsel $row
5002    }
5003    set xr [expr {$xt + [font measure $font $headline]}]
5004    if {$xr > $canvxmax} {
5005        set canvxmax $xr
5006        setcanvscroll
5007    }
5008}
5009
5010proc drawcmitrow {row} {
5011    global displayorder rowidlist nrows_drawn
5012    global iddrawn markingmatches
5013    global commitinfo numcommits
5014    global filehighlight fhighlights findpattern nhighlights
5015    global hlview vhighlights
5016    global highlight_related rhighlights
5017
5018    if {$row >= $numcommits} return
5019
5020    set id [lindex $displayorder $row]
5021    if {[info exists hlview] && ![info exists vhighlights($id)]} {
5022        askvhighlight $row $id
5023    }
5024    if {[info exists filehighlight] && ![info exists fhighlights($id)]} {
5025        askfilehighlight $row $id
5026    }
5027    if {$findpattern ne {} && ![info exists nhighlights($id)]} {
5028        askfindhighlight $row $id
5029    }
5030    if {$highlight_related ne [mc "None"] && ![info exists rhighlights($id)]} {
5031        askrelhighlight $row $id
5032    }
5033    if {![info exists iddrawn($id)]} {
5034        set col [lsearch -exact [lindex $rowidlist $row] $id]
5035        if {$col < 0} {
5036            puts "oops, row $row id $id not in list"
5037            return
5038        }
5039        if {![info exists commitinfo($id)]} {
5040            getcommit $id
5041        }
5042        assigncolor $id
5043        drawcmittext $id $row $col
5044        set iddrawn($id) 1
5045        incr nrows_drawn
5046    }
5047    if {$markingmatches} {
5048        markrowmatches $row $id
5049    }
5050}
5051
5052proc drawcommits {row {endrow {}}} {
5053    global numcommits iddrawn displayorder curview need_redisplay
5054    global parentlist rowidlist rowfinal uparrowlen downarrowlen nrows_drawn
5055
5056    if {$row < 0} {
5057        set row 0
5058    }
5059    if {$endrow eq {}} {
5060        set endrow $row
5061    }
5062    if {$endrow >= $numcommits} {
5063        set endrow [expr {$numcommits - 1}]
5064    }
5065
5066    set rl1 [expr {$row - $downarrowlen - 3}]
5067    if {$rl1 < 0} {
5068        set rl1 0
5069    }
5070    set ro1 [expr {$row - 3}]
5071    if {$ro1 < 0} {
5072        set ro1 0
5073    }
5074    set r2 [expr {$endrow + $uparrowlen + 3}]
5075    if {$r2 > $numcommits} {
5076        set r2 $numcommits
5077    }
5078    for {set r $rl1} {$r < $r2} {incr r} {
5079        if {[lindex $rowidlist $r] ne {} && [lindex $rowfinal $r]} {
5080            if {$rl1 < $r} {
5081                layoutrows $rl1 $r
5082            }
5083            set rl1 [expr {$r + 1}]
5084        }
5085    }
5086    if {$rl1 < $r} {
5087        layoutrows $rl1 $r
5088    }
5089    optimize_rows $ro1 0 $r2
5090    if {$need_redisplay || $nrows_drawn > 2000} {
5091        clear_display
5092        drawvisible
5093    }
5094
5095    # make the lines join to already-drawn rows either side
5096    set r [expr {$row - 1}]
5097    if {$r < 0 || ![info exists iddrawn([lindex $displayorder $r])]} {
5098        set r $row
5099    }
5100    set er [expr {$endrow + 1}]
5101    if {$er >= $numcommits ||
5102        ![info exists iddrawn([lindex $displayorder $er])]} {
5103        set er $endrow
5104    }
5105    for {} {$r <= $er} {incr r} {
5106        set id [lindex $displayorder $r]
5107        set wasdrawn [info exists iddrawn($id)]
5108        drawcmitrow $r
5109        if {$r == $er} break
5110        set nextid [lindex $displayorder [expr {$r + 1}]]
5111        if {$wasdrawn && [info exists iddrawn($nextid)]} continue
5112        drawparentlinks $id $r
5113
5114        set rowids [lindex $rowidlist $r]
5115        foreach lid $rowids {
5116            if {$lid eq {}} continue
5117            if {[info exists lineend($lid)] && $lineend($lid) > $r} continue
5118            if {$lid eq $id} {
5119                # see if this is the first child of any of its parents
5120                foreach p [lindex $parentlist $r] {
5121                    if {[lsearch -exact $rowids $p] < 0} {
5122                        # make this line extend up to the child
5123                        set lineend($p) [drawlineseg $p $r $er 0]
5124                    }
5125                }
5126            } else {
5127                set lineend($lid) [drawlineseg $lid $r $er 1]
5128            }
5129        }
5130    }
5131}
5132
5133proc undolayout {row} {
5134    global uparrowlen mingaplen downarrowlen
5135    global rowidlist rowisopt rowfinal need_redisplay
5136
5137    set r [expr {$row - ($uparrowlen + $mingaplen + $downarrowlen)}]
5138    if {$r < 0} {
5139        set r 0
5140    }
5141    if {[llength $rowidlist] > $r} {
5142        incr r -1
5143        set rowidlist [lrange $rowidlist 0 $r]
5144        set rowfinal [lrange $rowfinal 0 $r]
5145        set rowisopt [lrange $rowisopt 0 $r]
5146        set need_redisplay 1
5147        run drawvisible
5148    }
5149}
5150
5151proc drawvisible {} {
5152    global canv linespc curview vrowmod selectedline targetrow targetid
5153    global need_redisplay cscroll numcommits
5154
5155    set fs [$canv yview]
5156    set ymax [lindex [$canv cget -scrollregion] 3]
5157    if {$ymax eq {} || $ymax == 0 || $numcommits == 0} return
5158    set f0 [lindex $fs 0]
5159    set f1 [lindex $fs 1]
5160    set y0 [expr {int($f0 * $ymax)}]
5161    set y1 [expr {int($f1 * $ymax)}]
5162
5163    if {[info exists targetid]} {
5164        if {[commitinview $targetid $curview]} {
5165            set r [rowofcommit $targetid]
5166            if {$r != $targetrow} {
5167                # Fix up the scrollregion and change the scrolling position
5168                # now that our target row has moved.
5169                set diff [expr {($r - $targetrow) * $linespc}]
5170                set targetrow $r
5171                setcanvscroll
5172                set ymax [lindex [$canv cget -scrollregion] 3]
5173                incr y0 $diff
5174                incr y1 $diff
5175                set f0 [expr {$y0 / $ymax}]
5176                set f1 [expr {$y1 / $ymax}]
5177                allcanvs yview moveto $f0
5178                $cscroll set $f0 $f1
5179                set need_redisplay 1
5180            }
5181        } else {
5182            unset targetid
5183        }
5184    }
5185
5186    set row [expr {int(($y0 - 3) / $linespc) - 1}]
5187    set endrow [expr {int(($y1 - 3) / $linespc) + 1}]
5188    if {$endrow >= $vrowmod($curview)} {
5189        update_arcrows $curview
5190    }
5191    if {$selectedline ne {} &&
5192        $row <= $selectedline && $selectedline <= $endrow} {
5193        set targetrow $selectedline
5194    } elseif {[info exists targetid]} {
5195        set targetrow [expr {int(($row + $endrow) / 2)}]
5196    }
5197    if {[info exists targetrow]} {
5198        if {$targetrow >= $numcommits} {
5199            set targetrow [expr {$numcommits - 1}]
5200        }
5201        set targetid [commitonrow $targetrow]
5202    }
5203    drawcommits $row $endrow
5204}
5205
5206proc clear_display {} {
5207    global iddrawn linesegs need_redisplay nrows_drawn
5208    global vhighlights fhighlights nhighlights rhighlights
5209    global linehtag linentag linedtag boldrows boldnamerows
5210
5211    allcanvs delete all
5212    catch {unset iddrawn}
5213    catch {unset linesegs}
5214    catch {unset linehtag}
5215    catch {unset linentag}
5216    catch {unset linedtag}
5217    set boldrows {}
5218    set boldnamerows {}
5219    catch {unset vhighlights}
5220    catch {unset fhighlights}
5221    catch {unset nhighlights}
5222    catch {unset rhighlights}
5223    set need_redisplay 0
5224    set nrows_drawn 0
5225}
5226
5227proc findcrossings {id} {
5228    global rowidlist parentlist numcommits displayorder
5229
5230    set cross {}
5231    set ccross {}
5232    foreach {s e} [rowranges $id] {
5233        if {$e >= $numcommits} {
5234            set e [expr {$numcommits - 1}]
5235        }
5236        if {$e <= $s} continue
5237        for {set row $e} {[incr row -1] >= $s} {} {
5238            set x [lsearch -exact [lindex $rowidlist $row] $id]
5239            if {$x < 0} break
5240            set olds [lindex $parentlist $row]
5241            set kid [lindex $displayorder $row]
5242            set kidx [lsearch -exact [lindex $rowidlist $row] $kid]
5243            if {$kidx < 0} continue
5244            set nextrow [lindex $rowidlist [expr {$row + 1}]]
5245            foreach p $olds {
5246                set px [lsearch -exact $nextrow $p]
5247                if {$px < 0} continue
5248                if {($kidx < $x && $x < $px) || ($px < $x && $x < $kidx)} {
5249                    if {[lsearch -exact $ccross $p] >= 0} continue
5250                    if {$x == $px + ($kidx < $px? -1: 1)} {
5251                        lappend ccross $p
5252                    } elseif {[lsearch -exact $cross $p] < 0} {
5253                        lappend cross $p
5254                    }
5255                }
5256            }
5257        }
5258    }
5259    return [concat $ccross {{}} $cross]
5260}
5261
5262proc assigncolor {id} {
5263    global colormap colors nextcolor
5264    global parents children children curview
5265
5266    if {[info exists colormap($id)]} return
5267    set ncolors [llength $colors]
5268    if {[info exists children($curview,$id)]} {
5269        set kids $children($curview,$id)
5270    } else {
5271        set kids {}
5272    }
5273    if {[llength $kids] == 1} {
5274        set child [lindex $kids 0]
5275        if {[info exists colormap($child)]
5276            && [llength $parents($curview,$child)] == 1} {
5277            set colormap($id) $colormap($child)
5278            return
5279        }
5280    }
5281    set badcolors {}
5282    set origbad {}
5283    foreach x [findcrossings $id] {
5284        if {$x eq {}} {
5285            # delimiter between corner crossings and other crossings
5286            if {[llength $badcolors] >= $ncolors - 1} break
5287            set origbad $badcolors
5288        }
5289        if {[info exists colormap($x)]
5290            && [lsearch -exact $badcolors $colormap($x)] < 0} {
5291            lappend badcolors $colormap($x)
5292        }
5293    }
5294    if {[llength $badcolors] >= $ncolors} {
5295        set badcolors $origbad
5296    }
5297    set origbad $badcolors
5298    if {[llength $badcolors] < $ncolors - 1} {
5299        foreach child $kids {
5300            if {[info exists colormap($child)]
5301                && [lsearch -exact $badcolors $colormap($child)] < 0} {
5302                lappend badcolors $colormap($child)
5303            }
5304            foreach p $parents($curview,$child) {
5305                if {[info exists colormap($p)]
5306                    && [lsearch -exact $badcolors $colormap($p)] < 0} {
5307                    lappend badcolors $colormap($p)
5308                }
5309            }
5310        }
5311        if {[llength $badcolors] >= $ncolors} {
5312            set badcolors $origbad
5313        }
5314    }
5315    for {set i 0} {$i <= $ncolors} {incr i} {
5316        set c [lindex $colors $nextcolor]
5317        if {[incr nextcolor] >= $ncolors} {
5318            set nextcolor 0
5319        }
5320        if {[lsearch -exact $badcolors $c]} break
5321    }
5322    set colormap($id) $c
5323}
5324
5325proc bindline {t id} {
5326    global canv
5327
5328    $canv bind $t <Enter> "lineenter %x %y $id"
5329    $canv bind $t <Motion> "linemotion %x %y $id"
5330    $canv bind $t <Leave> "lineleave $id"
5331    $canv bind $t <Button-1> "lineclick %x %y $id 1"
5332}
5333
5334proc drawtags {id x xt y1} {
5335    global idtags idheads idotherrefs mainhead
5336    global linespc lthickness
5337    global canv rowtextx curview fgcolor bgcolor
5338
5339    set marks {}
5340    set ntags 0
5341    set nheads 0
5342    if {[info exists idtags($id)]} {
5343        set marks $idtags($id)
5344        set ntags [llength $marks]
5345    }
5346    if {[info exists idheads($id)]} {
5347        set marks [concat $marks $idheads($id)]
5348        set nheads [llength $idheads($id)]
5349    }
5350    if {[info exists idotherrefs($id)]} {
5351        set marks [concat $marks $idotherrefs($id)]
5352    }
5353    if {$marks eq {}} {
5354        return $xt
5355    }
5356
5357    set delta [expr {int(0.5 * ($linespc - $lthickness))}]
5358    set yt [expr {$y1 - 0.5 * $linespc}]
5359    set yb [expr {$yt + $linespc - 1}]
5360    set xvals {}
5361    set wvals {}
5362    set i -1
5363    foreach tag $marks {
5364        incr i
5365        if {$i >= $ntags && $i < $ntags + $nheads && $tag eq $mainhead} {
5366            set wid [font measure mainfontbold $tag]
5367        } else {
5368            set wid [font measure mainfont $tag]
5369        }
5370        lappend xvals $xt
5371        lappend wvals $wid
5372        set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
5373    }
5374    set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
5375               -width $lthickness -fill black -tags tag.$id]
5376    $canv lower $t
5377    foreach tag $marks x $xvals wid $wvals {
5378        set xl [expr {$x + $delta}]
5379        set xr [expr {$x + $delta + $wid + $lthickness}]
5380        set font mainfont
5381        if {[incr ntags -1] >= 0} {
5382            # draw a tag
5383            set t [$canv create polygon $x [expr {$yt + $delta}] $xl $yt \
5384                       $xr $yt $xr $yb $xl $yb $x [expr {$yb - $delta}] \
5385                       -width 1 -outline black -fill yellow -tags tag.$id]
5386            $canv bind $t <1> [list showtag $tag 1]
5387            set rowtextx([rowofcommit $id]) [expr {$xr + $linespc}]
5388        } else {
5389            # draw a head or other ref
5390            if {[incr nheads -1] >= 0} {
5391                set col green
5392                if {$tag eq $mainhead} {
5393                    set font mainfontbold
5394                }
5395            } else {
5396                set col "#ddddff"
5397            }
5398            set xl [expr {$xl - $delta/2}]
5399            $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
5400                -width 1 -outline black -fill $col -tags tag.$id
5401            if {[regexp {^(remotes/.*/|remotes/)} $tag match remoteprefix]} {
5402                set rwid [font measure mainfont $remoteprefix]
5403                set xi [expr {$x + 1}]
5404                set yti [expr {$yt + 1}]
5405                set xri [expr {$x + $rwid}]
5406                $canv create polygon $xi $yti $xri $yti $xri $yb $xi $yb \
5407                        -width 0 -fill "#ffddaa" -tags tag.$id
5408            }
5409        }
5410        set t [$canv create text $xl $y1 -anchor w -text $tag -fill $fgcolor \
5411                   -font $font -tags [list tag.$id text]]
5412        if {$ntags >= 0} {
5413            $canv bind $t <1> [list showtag $tag 1]
5414        } elseif {$nheads >= 0} {
5415            $canv bind $t <Button-3> [list headmenu %X %Y $id $tag]
5416        }
5417    }
5418    return $xt
5419}
5420
5421proc xcoord {i level ln} {
5422    global canvx0 xspc1 xspc2
5423
5424    set x [expr {$canvx0 + $i * $xspc1($ln)}]
5425    if {$i > 0 && $i == $level} {
5426        set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
5427    } elseif {$i > $level} {
5428        set x [expr {$x + $xspc2 - $xspc1($ln)}]
5429    }
5430    return $x
5431}
5432
5433proc show_status {msg} {
5434    global canv fgcolor
5435
5436    clear_display
5437    $canv create text 3 3 -anchor nw -text $msg -font mainfont \
5438        -tags text -fill $fgcolor
5439}
5440
5441# Don't change the text pane cursor if it is currently the hand cursor,
5442# showing that we are over a sha1 ID link.
5443proc settextcursor {c} {
5444    global ctext curtextcursor
5445
5446    if {[$ctext cget -cursor] == $curtextcursor} {
5447        $ctext config -cursor $c
5448    }
5449    set curtextcursor $c
5450}
5451
5452proc nowbusy {what {name {}}} {
5453    global isbusy busyname statusw
5454
5455    if {[array names isbusy] eq {}} {
5456        . config -cursor watch
5457        settextcursor watch
5458    }
5459    set isbusy($what) 1
5460    set busyname($what) $name
5461    if {$name ne {}} {
5462        $statusw conf -text $name
5463    }
5464}
5465
5466proc notbusy {what} {
5467    global isbusy maincursor textcursor busyname statusw
5468
5469    catch {
5470        unset isbusy($what)
5471        if {$busyname($what) ne {} &&
5472            [$statusw cget -text] eq $busyname($what)} {
5473            $statusw conf -text {}
5474        }
5475    }
5476    if {[array names isbusy] eq {}} {
5477        . config -cursor $maincursor
5478        settextcursor $textcursor
5479    }
5480}
5481
5482proc findmatches {f} {
5483    global findtype findstring
5484    if {$findtype == [mc "Regexp"]} {
5485        set matches [regexp -indices -all -inline $findstring $f]
5486    } else {
5487        set fs $findstring
5488        if {$findtype == [mc "IgnCase"]} {
5489            set f [string tolower $f]
5490            set fs [string tolower $fs]
5491        }
5492        set matches {}
5493        set i 0
5494        set l [string length $fs]
5495        while {[set j [string first $fs $f $i]] >= 0} {
5496            lappend matches [list $j [expr {$j+$l-1}]]
5497            set i [expr {$j + $l}]
5498        }
5499    }
5500    return $matches
5501}
5502
5503proc dofind {{dirn 1} {wrap 1}} {
5504    global findstring findstartline findcurline selectedline numcommits
5505    global gdttype filehighlight fh_serial find_dirn findallowwrap
5506
5507    if {[info exists find_dirn]} {
5508        if {$find_dirn == $dirn} return
5509        stopfinding
5510    }
5511    focus .
5512    if {$findstring eq {} || $numcommits == 0} return
5513    if {$selectedline eq {}} {
5514        set findstartline [lindex [visiblerows] [expr {$dirn < 0}]]
5515    } else {
5516        set findstartline $selectedline
5517    }
5518    set findcurline $findstartline
5519    nowbusy finding [mc "Searching"]
5520    if {$gdttype ne [mc "containing:"] && ![info exists filehighlight]} {
5521        after cancel do_file_hl $fh_serial
5522        do_file_hl $fh_serial
5523    }
5524    set find_dirn $dirn
5525    set findallowwrap $wrap
5526    run findmore
5527}
5528
5529proc stopfinding {} {
5530    global find_dirn findcurline fprogcoord
5531
5532    if {[info exists find_dirn]} {
5533        unset find_dirn
5534        unset findcurline
5535        notbusy finding
5536        set fprogcoord 0
5537        adjustprogress
5538    }
5539}
5540
5541proc findmore {} {
5542    global commitdata commitinfo numcommits findpattern findloc
5543    global findstartline findcurline findallowwrap
5544    global find_dirn gdttype fhighlights fprogcoord
5545    global curview varcorder vrownum varccommits vrowmod
5546
5547    if {![info exists find_dirn]} {
5548        return 0
5549    }
5550    set fldtypes [list [mc "Headline"] [mc "Author"] [mc "Date"] [mc "Committer"] [mc "CDate"] [mc "Comments"]]
5551    set l $findcurline
5552    set moretodo 0
5553    if {$find_dirn > 0} {
5554        incr l
5555        if {$l >= $numcommits} {
5556            set l 0
5557        }
5558        if {$l <= $findstartline} {
5559            set lim [expr {$findstartline + 1}]
5560        } else {
5561            set lim $numcommits
5562            set moretodo $findallowwrap
5563        }
5564    } else {
5565        if {$l == 0} {
5566            set l $numcommits
5567        }
5568        incr l -1
5569        if {$l >= $findstartline} {
5570            set lim [expr {$findstartline - 1}]
5571        } else {
5572            set lim -1
5573            set moretodo $findallowwrap
5574        }
5575    }
5576    set n [expr {($lim - $l) * $find_dirn}]
5577    if {$n > 500} {
5578        set n 500
5579        set moretodo 1
5580    }
5581    if {$l + ($find_dirn > 0? $n: 1) > $vrowmod($curview)} {
5582        update_arcrows $curview
5583    }
5584    set found 0
5585    set domore 1
5586    set ai [bsearch $vrownum($curview) $l]
5587    set a [lindex $varcorder($curview) $ai]
5588    set arow [lindex $vrownum($curview) $ai]
5589    set ids [lindex $varccommits($curview,$a)]
5590    set arowend [expr {$arow + [llength $ids]}]
5591    if {$gdttype eq [mc "containing:"]} {
5592        for {} {$n > 0} {incr n -1; incr l $find_dirn} {
5593            if {$l < $arow || $l >= $arowend} {
5594                incr ai $find_dirn
5595                set a [lindex $varcorder($curview) $ai]
5596                set arow [lindex $vrownum($curview) $ai]
5597                set ids [lindex $varccommits($curview,$a)]
5598                set arowend [expr {$arow + [llength $ids]}]
5599            }
5600            set id [lindex $ids [expr {$l - $arow}]]
5601            # shouldn't happen unless git log doesn't give all the commits...
5602            if {![info exists commitdata($id)] ||
5603                ![doesmatch $commitdata($id)]} {
5604                continue
5605            }
5606            if {![info exists commitinfo($id)]} {
5607                getcommit $id
5608            }
5609            set info $commitinfo($id)
5610            foreach f $info ty $fldtypes {
5611                if {($findloc eq [mc "All fields"] || $findloc eq $ty) &&
5612                    [doesmatch $f]} {
5613                    set found 1
5614                    break
5615                }
5616            }
5617            if {$found} break
5618        }
5619    } else {
5620        for {} {$n > 0} {incr n -1; incr l $find_dirn} {
5621            if {$l < $arow || $l >= $arowend} {
5622                incr ai $find_dirn
5623                set a [lindex $varcorder($curview) $ai]
5624                set arow [lindex $vrownum($curview) $ai]
5625                set ids [lindex $varccommits($curview,$a)]
5626                set arowend [expr {$arow + [llength $ids]}]
5627            }
5628            set id [lindex $ids [expr {$l - $arow}]]
5629            if {![info exists fhighlights($id)]} {
5630                # this sets fhighlights($id) to -1
5631                askfilehighlight $l $id
5632            }
5633            if {$fhighlights($id) > 0} {
5634                set found $domore
5635                break
5636            }
5637            if {$fhighlights($id) < 0} {
5638                if {$domore} {
5639                    set domore 0
5640                    set findcurline [expr {$l - $find_dirn}]
5641                }
5642            }
5643        }
5644    }
5645    if {$found || ($domore && !$moretodo)} {
5646        unset findcurline
5647        unset find_dirn
5648        notbusy finding
5649        set fprogcoord 0
5650        adjustprogress
5651        if {$found} {
5652            findselectline $l
5653        } else {
5654            bell
5655        }
5656        return 0
5657    }
5658    if {!$domore} {
5659        flushhighlights
5660    } else {
5661        set findcurline [expr {$l - $find_dirn}]
5662    }
5663    set n [expr {($findcurline - $findstartline) * $find_dirn - 1}]
5664    if {$n < 0} {
5665        incr n $numcommits
5666    }
5667    set fprogcoord [expr {$n * 1.0 / $numcommits}]
5668    adjustprogress
5669    return $domore
5670}
5671
5672proc findselectline {l} {
5673    global findloc commentend ctext findcurline markingmatches gdttype
5674
5675    set markingmatches 1
5676    set findcurline $l
5677    selectline $l 1
5678    if {$findloc == [mc "All fields"] || $findloc == [mc "Comments"]} {
5679        # highlight the matches in the comments
5680        set f [$ctext get 1.0 $commentend]
5681        set matches [findmatches $f]
5682        foreach match $matches {
5683            set start [lindex $match 0]
5684            set end [expr {[lindex $match 1] + 1}]
5685            $ctext tag add found "1.0 + $start c" "1.0 + $end c"
5686        }
5687    }
5688    drawvisible
5689}
5690
5691# mark the bits of a headline or author that match a find string
5692proc markmatches {canv l str tag matches font row} {
5693    global selectedline
5694
5695    set bbox [$canv bbox $tag]
5696    set x0 [lindex $bbox 0]
5697    set y0 [lindex $bbox 1]
5698    set y1 [lindex $bbox 3]
5699    foreach match $matches {
5700        set start [lindex $match 0]
5701        set end [lindex $match 1]
5702        if {$start > $end} continue
5703        set xoff [font measure $font [string range $str 0 [expr {$start-1}]]]
5704        set xlen [font measure $font [string range $str 0 [expr {$end}]]]
5705        set t [$canv create rect [expr {$x0+$xoff}] $y0 \
5706                   [expr {$x0+$xlen+2}] $y1 \
5707                   -outline {} -tags [list match$l matches] -fill yellow]
5708        $canv lower $t
5709        if {$row == $selectedline} {
5710            $canv raise $t secsel
5711        }
5712    }
5713}
5714
5715proc unmarkmatches {} {
5716    global markingmatches
5717
5718    allcanvs delete matches
5719    set markingmatches 0
5720    stopfinding
5721}
5722
5723proc selcanvline {w x y} {
5724    global canv canvy0 ctext linespc
5725    global rowtextx
5726    set ymax [lindex [$canv cget -scrollregion] 3]
5727    if {$ymax == {}} return
5728    set yfrac [lindex [$canv yview] 0]
5729    set y [expr {$y + $yfrac * $ymax}]
5730    set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
5731    if {$l < 0} {
5732        set l 0
5733    }
5734    if {$w eq $canv} {
5735        set xmax [lindex [$canv cget -scrollregion] 2]
5736        set xleft [expr {[lindex [$canv xview] 0] * $xmax}]
5737        if {![info exists rowtextx($l)] || $xleft + $x < $rowtextx($l)} return
5738    }
5739    unmarkmatches
5740    selectline $l 1
5741}
5742
5743proc commit_descriptor {p} {
5744    global commitinfo
5745    if {![info exists commitinfo($p)]} {
5746        getcommit $p
5747    }
5748    set l "..."
5749    if {[llength $commitinfo($p)] > 1} {
5750        set l [lindex $commitinfo($p) 0]
5751    }
5752    return "$p ($l)\n"
5753}
5754
5755# append some text to the ctext widget, and make any SHA1 ID
5756# that we know about be a clickable link.
5757proc appendwithlinks {text tags} {
5758    global ctext linknum curview pendinglinks
5759
5760    set start [$ctext index "end - 1c"]
5761    $ctext insert end $text $tags
5762    set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
5763    foreach l $links {
5764        set s [lindex $l 0]
5765        set e [lindex $l 1]
5766        set linkid [string range $text $s $e]
5767        incr e
5768        $ctext tag delete link$linknum
5769        $ctext tag add link$linknum "$start + $s c" "$start + $e c"
5770        setlink $linkid link$linknum
5771        incr linknum
5772    }
5773}
5774
5775proc setlink {id lk} {
5776    global curview ctext pendinglinks commitinterest
5777
5778    if {[commitinview $id $curview]} {
5779        $ctext tag conf $lk -foreground blue -underline 1
5780        $ctext tag bind $lk <1> [list selectline [rowofcommit $id] 1]
5781        $ctext tag bind $lk <Enter> {linkcursor %W 1}
5782        $ctext tag bind $lk <Leave> {linkcursor %W -1}
5783    } else {
5784        lappend pendinglinks($id) $lk
5785        lappend commitinterest($id) {makelink %I}
5786    }
5787}
5788
5789proc makelink {id} {
5790    global pendinglinks
5791
5792    if {![info exists pendinglinks($id)]} return
5793    foreach lk $pendinglinks($id) {
5794        setlink $id $lk
5795    }
5796    unset pendinglinks($id)
5797}
5798
5799proc linkcursor {w inc} {
5800    global linkentercount curtextcursor
5801
5802    if {[incr linkentercount $inc] > 0} {
5803        $w configure -cursor hand2
5804    } else {
5805        $w configure -cursor $curtextcursor
5806        if {$linkentercount < 0} {
5807            set linkentercount 0
5808        }
5809    }
5810}
5811
5812proc viewnextline {dir} {
5813    global canv linespc
5814
5815    $canv delete hover
5816    set ymax [lindex [$canv cget -scrollregion] 3]
5817    set wnow [$canv yview]
5818    set wtop [expr {[lindex $wnow 0] * $ymax}]
5819    set newtop [expr {$wtop + $dir * $linespc}]
5820    if {$newtop < 0} {
5821        set newtop 0
5822    } elseif {$newtop > $ymax} {
5823        set newtop $ymax
5824    }
5825    allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
5826}
5827
5828# add a list of tag or branch names at position pos
5829# returns the number of names inserted
5830proc appendrefs {pos ids var} {
5831    global ctext linknum curview $var maxrefs
5832
5833    if {[catch {$ctext index $pos}]} {
5834        return 0
5835    }
5836    $ctext conf -state normal
5837    $ctext delete $pos "$pos lineend"
5838    set tags {}
5839    foreach id $ids {
5840        foreach tag [set $var\($id\)] {
5841            lappend tags [list $tag $id]
5842        }
5843    }
5844    if {[llength $tags] > $maxrefs} {
5845        $ctext insert $pos "many ([llength $tags])"
5846    } else {
5847        set tags [lsort -index 0 -decreasing $tags]
5848        set sep {}
5849        foreach ti $tags {
5850            set id [lindex $ti 1]
5851            set lk link$linknum
5852            incr linknum
5853            $ctext tag delete $lk
5854            $ctext insert $pos $sep
5855            $ctext insert $pos [lindex $ti 0] $lk
5856            setlink $id $lk
5857            set sep ", "
5858        }
5859    }
5860    $ctext conf -state disabled
5861    return [llength $tags]
5862}
5863
5864# called when we have finished computing the nearby tags
5865proc dispneartags {delay} {
5866    global selectedline currentid showneartags tagphase
5867
5868    if {$selectedline eq {} || !$showneartags} return
5869    after cancel dispnexttag
5870    if {$delay} {
5871        after 200 dispnexttag
5872        set tagphase -1
5873    } else {
5874        after idle dispnexttag
5875        set tagphase 0
5876    }
5877}
5878
5879proc dispnexttag {} {
5880    global selectedline currentid showneartags tagphase ctext
5881
5882    if {$selectedline eq {} || !$showneartags} return
5883    switch -- $tagphase {
5884        0 {
5885            set dtags [desctags $currentid]
5886            if {$dtags ne {}} {
5887                appendrefs precedes $dtags idtags
5888            }
5889        }
5890        1 {
5891            set atags [anctags $currentid]
5892            if {$atags ne {}} {
5893                appendrefs follows $atags idtags
5894            }
5895        }
5896        2 {
5897            set dheads [descheads $currentid]
5898            if {$dheads ne {}} {
5899                if {[appendrefs branch $dheads idheads] > 1
5900                    && [$ctext get "branch -3c"] eq "h"} {
5901                    # turn "Branch" into "Branches"
5902                    $ctext conf -state normal
5903                    $ctext insert "branch -2c" "es"
5904                    $ctext conf -state disabled
5905                }
5906            }
5907        }
5908    }
5909    if {[incr tagphase] <= 2} {
5910        after idle dispnexttag
5911    }
5912}
5913
5914proc make_secsel {l} {
5915    global linehtag linentag linedtag canv canv2 canv3
5916
5917    if {![info exists linehtag($l)]} return
5918    $canv delete secsel
5919    set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
5920               -tags secsel -fill [$canv cget -selectbackground]]
5921    $canv lower $t
5922    $canv2 delete secsel
5923    set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
5924               -tags secsel -fill [$canv2 cget -selectbackground]]
5925    $canv2 lower $t
5926    $canv3 delete secsel
5927    set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
5928               -tags secsel -fill [$canv3 cget -selectbackground]]
5929    $canv3 lower $t
5930}
5931
5932proc selectline {l isnew} {
5933    global canv ctext commitinfo selectedline
5934    global canvy0 linespc parents children curview
5935    global currentid sha1entry
5936    global commentend idtags linknum
5937    global mergemax numcommits pending_select
5938    global cmitmode showneartags allcommits
5939    global targetrow targetid lastscrollrows
5940    global autoselect
5941
5942    catch {unset pending_select}
5943    $canv delete hover
5944    normalline
5945    unsel_reflist
5946    stopfinding
5947    if {$l < 0 || $l >= $numcommits} return
5948    set id [commitonrow $l]
5949    set targetid $id
5950    set targetrow $l
5951    set selectedline $l
5952    set currentid $id
5953    if {$lastscrollrows < $numcommits} {
5954        setcanvscroll
5955    }
5956
5957    set y [expr {$canvy0 + $l * $linespc}]
5958    set ymax [lindex [$canv cget -scrollregion] 3]
5959    set ytop [expr {$y - $linespc - 1}]
5960    set ybot [expr {$y + $linespc + 1}]
5961    set wnow [$canv yview]
5962    set wtop [expr {[lindex $wnow 0] * $ymax}]
5963    set wbot [expr {[lindex $wnow 1] * $ymax}]
5964    set wh [expr {$wbot - $wtop}]
5965    set newtop $wtop
5966    if {$ytop < $wtop} {
5967        if {$ybot < $wtop} {
5968            set newtop [expr {$y - $wh / 2.0}]
5969        } else {
5970            set newtop $ytop
5971            if {$newtop > $wtop - $linespc} {
5972                set newtop [expr {$wtop - $linespc}]
5973            }
5974        }
5975    } elseif {$ybot > $wbot} {
5976        if {$ytop > $wbot} {
5977            set newtop [expr {$y - $wh / 2.0}]
5978        } else {
5979            set newtop [expr {$ybot - $wh}]
5980            if {$newtop < $wtop + $linespc} {
5981                set newtop [expr {$wtop + $linespc}]
5982            }
5983        }
5984    }
5985    if {$newtop != $wtop} {
5986        if {$newtop < 0} {
5987            set newtop 0
5988        }
5989        allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
5990        drawvisible
5991    }
5992
5993    make_secsel $l
5994
5995    if {$isnew} {
5996        addtohistory [list selbyid $id]
5997    }
5998
5999    $sha1entry delete 0 end
6000    $sha1entry insert 0 $id
6001    if {$autoselect} {
6002        $sha1entry selection from 0
6003        $sha1entry selection to end
6004    }
6005    rhighlight_sel $id
6006
6007    $ctext conf -state normal
6008    clear_ctext
6009    set linknum 0
6010    if {![info exists commitinfo($id)]} {
6011        getcommit $id
6012    }
6013    set info $commitinfo($id)
6014    set date [formatdate [lindex $info 2]]
6015    $ctext insert end "[mc "Author"]: [lindex $info 1]  $date\n"
6016    set date [formatdate [lindex $info 4]]
6017    $ctext insert end "[mc "Committer"]: [lindex $info 3]  $date\n"
6018    if {[info exists idtags($id)]} {
6019        $ctext insert end [mc "Tags:"]
6020        foreach tag $idtags($id) {
6021            $ctext insert end " $tag"
6022        }
6023        $ctext insert end "\n"
6024    }
6025
6026    set headers {}
6027    set olds $parents($curview,$id)
6028    if {[llength $olds] > 1} {
6029        set np 0
6030        foreach p $olds {
6031            if {$np >= $mergemax} {
6032                set tag mmax
6033            } else {
6034                set tag m$np
6035            }
6036            $ctext insert end "[mc "Parent"]: " $tag
6037            appendwithlinks [commit_descriptor $p] {}
6038            incr np
6039        }
6040    } else {
6041        foreach p $olds {
6042            append headers "[mc "Parent"]: [commit_descriptor $p]"
6043        }
6044    }
6045
6046    foreach c $children($curview,$id) {
6047        append headers "[mc "Child"]:  [commit_descriptor $c]"
6048    }
6049
6050    # make anything that looks like a SHA1 ID be a clickable link
6051    appendwithlinks $headers {}
6052    if {$showneartags} {
6053        if {![info exists allcommits]} {
6054            getallcommits
6055        }
6056        $ctext insert end "[mc "Branch"]: "
6057        $ctext mark set branch "end -1c"
6058        $ctext mark gravity branch left
6059        $ctext insert end "\n[mc "Follows"]: "
6060        $ctext mark set follows "end -1c"
6061        $ctext mark gravity follows left
6062        $ctext insert end "\n[mc "Precedes"]: "
6063        $ctext mark set precedes "end -1c"
6064        $ctext mark gravity precedes left
6065        $ctext insert end "\n"
6066        dispneartags 1
6067    }
6068    $ctext insert end "\n"
6069    set comment [lindex $info 5]
6070    if {[string first "\r" $comment] >= 0} {
6071        set comment [string map {"\r" "\n    "} $comment]
6072    }
6073    appendwithlinks $comment {comment}
6074
6075    $ctext tag remove found 1.0 end
6076    $ctext conf -state disabled
6077    set commentend [$ctext index "end - 1c"]
6078
6079    init_flist [mc "Comments"]
6080    if {$cmitmode eq "tree"} {
6081        gettree $id
6082    } elseif {[llength $olds] <= 1} {
6083        startdiff $id
6084    } else {
6085        mergediff $id
6086    }
6087}
6088
6089proc selfirstline {} {
6090    unmarkmatches
6091    selectline 0 1
6092}
6093
6094proc sellastline {} {
6095    global numcommits
6096    unmarkmatches
6097    set l [expr {$numcommits - 1}]
6098    selectline $l 1
6099}
6100
6101proc selnextline {dir} {
6102    global selectedline
6103    focus .
6104    if {$selectedline eq {}} return
6105    set l [expr {$selectedline + $dir}]
6106    unmarkmatches
6107    selectline $l 1
6108}
6109
6110proc selnextpage {dir} {
6111    global canv linespc selectedline numcommits
6112
6113    set lpp [expr {([winfo height $canv] - 2) / $linespc}]
6114    if {$lpp < 1} {
6115        set lpp 1
6116    }
6117    allcanvs yview scroll [expr {$dir * $lpp}] units
6118    drawvisible
6119    if {$selectedline eq {}} return
6120    set l [expr {$selectedline + $dir * $lpp}]
6121    if {$l < 0} {
6122        set l 0
6123    } elseif {$l >= $numcommits} {
6124        set l [expr $numcommits - 1]
6125    }
6126    unmarkmatches
6127    selectline $l 1
6128}
6129
6130proc unselectline {} {
6131    global selectedline currentid
6132
6133    set selectedline {}
6134    catch {unset currentid}
6135    allcanvs delete secsel
6136    rhighlight_none
6137}
6138
6139proc reselectline {} {
6140    global selectedline
6141
6142    if {$selectedline ne {}} {
6143        selectline $selectedline 0
6144    }
6145}
6146
6147proc addtohistory {cmd} {
6148    global history historyindex curview
6149
6150    set elt [list $curview $cmd]
6151    if {$historyindex > 0
6152        && [lindex $history [expr {$historyindex - 1}]] == $elt} {
6153        return
6154    }
6155
6156    if {$historyindex < [llength $history]} {
6157        set history [lreplace $history $historyindex end $elt]
6158    } else {
6159        lappend history $elt
6160    }
6161    incr historyindex
6162    if {$historyindex > 1} {
6163        .tf.bar.leftbut conf -state normal
6164    } else {
6165        .tf.bar.leftbut conf -state disabled
6166    }
6167    .tf.bar.rightbut conf -state disabled
6168}
6169
6170proc godo {elt} {
6171    global curview
6172
6173    set view [lindex $elt 0]
6174    set cmd [lindex $elt 1]
6175    if {$curview != $view} {
6176        showview $view
6177    }
6178    eval $cmd
6179}
6180
6181proc goback {} {
6182    global history historyindex
6183    focus .
6184
6185    if {$historyindex > 1} {
6186        incr historyindex -1
6187        godo [lindex $history [expr {$historyindex - 1}]]
6188        .tf.bar.rightbut conf -state normal
6189    }
6190    if {$historyindex <= 1} {
6191        .tf.bar.leftbut conf -state disabled
6192    }
6193}
6194
6195proc goforw {} {
6196    global history historyindex
6197    focus .
6198
6199    if {$historyindex < [llength $history]} {
6200        set cmd [lindex $history $historyindex]
6201        incr historyindex
6202        godo $cmd
6203        .tf.bar.leftbut conf -state normal
6204    }
6205    if {$historyindex >= [llength $history]} {
6206        .tf.bar.rightbut conf -state disabled
6207    }
6208}
6209
6210proc gettree {id} {
6211    global treefilelist treeidlist diffids diffmergeid treepending
6212    global nullid nullid2
6213
6214    set diffids $id
6215    catch {unset diffmergeid}
6216    if {![info exists treefilelist($id)]} {
6217        if {![info exists treepending]} {
6218            if {$id eq $nullid} {
6219                set cmd [list | git ls-files]
6220            } elseif {$id eq $nullid2} {
6221                set cmd [list | git ls-files --stage -t]
6222            } else {
6223                set cmd [list | git ls-tree -r $id]
6224            }
6225            if {[catch {set gtf [open $cmd r]}]} {
6226                return
6227            }
6228            set treepending $id
6229            set treefilelist($id) {}
6230            set treeidlist($id) {}
6231            fconfigure $gtf -blocking 0
6232            filerun $gtf [list gettreeline $gtf $id]
6233        }
6234    } else {
6235        setfilelist $id
6236    }
6237}
6238
6239proc gettreeline {gtf id} {
6240    global treefilelist treeidlist treepending cmitmode diffids nullid nullid2
6241
6242    set nl 0
6243    while {[incr nl] <= 1000 && [gets $gtf line] >= 0} {
6244        if {$diffids eq $nullid} {
6245            set fname $line
6246        } else {
6247            set i [string first "\t" $line]
6248            if {$i < 0} continue
6249            set fname [string range $line [expr {$i+1}] end]
6250            set line [string range $line 0 [expr {$i-1}]]
6251            if {$diffids ne $nullid2 && [lindex $line 1] ne "blob"} continue
6252            set sha1 [lindex $line 2]
6253            if {[string index $fname 0] eq "\""} {
6254                set fname [lindex $fname 0]
6255            }
6256            lappend treeidlist($id) $sha1
6257        }
6258        lappend treefilelist($id) $fname
6259    }
6260    if {![eof $gtf]} {
6261        return [expr {$nl >= 1000? 2: 1}]
6262    }
6263    close $gtf
6264    unset treepending
6265    if {$cmitmode ne "tree"} {
6266        if {![info exists diffmergeid]} {
6267            gettreediffs $diffids
6268        }
6269    } elseif {$id ne $diffids} {
6270        gettree $diffids
6271    } else {
6272        setfilelist $id
6273    }
6274    return 0
6275}
6276
6277proc showfile {f} {
6278    global treefilelist treeidlist diffids nullid nullid2
6279    global ctext commentend
6280
6281    set i [lsearch -exact $treefilelist($diffids) $f]
6282    if {$i < 0} {
6283        puts "oops, $f not in list for id $diffids"
6284        return
6285    }
6286    if {$diffids eq $nullid} {
6287        if {[catch {set bf [open $f r]} err]} {
6288            puts "oops, can't read $f: $err"
6289            return
6290        }
6291    } else {
6292        set blob [lindex $treeidlist($diffids) $i]
6293        if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} {
6294            puts "oops, error reading blob $blob: $err"
6295            return
6296        }
6297    }
6298    fconfigure $bf -blocking 0
6299    filerun $bf [list getblobline $bf $diffids]
6300    $ctext config -state normal
6301    clear_ctext $commentend
6302    $ctext insert end "\n"
6303    $ctext insert end "$f\n" filesep
6304    $ctext config -state disabled
6305    $ctext yview $commentend
6306    settabs 0
6307}
6308
6309proc getblobline {bf id} {
6310    global diffids cmitmode ctext
6311
6312    if {$id ne $diffids || $cmitmode ne "tree"} {
6313        catch {close $bf}
6314        return 0
6315    }
6316    $ctext config -state normal
6317    set nl 0
6318    while {[incr nl] <= 1000 && [gets $bf line] >= 0} {
6319        $ctext insert end "$line\n"
6320    }
6321    if {[eof $bf]} {
6322        # delete last newline
6323        $ctext delete "end - 2c" "end - 1c"
6324        close $bf
6325        return 0
6326    }
6327    $ctext config -state disabled
6328    return [expr {$nl >= 1000? 2: 1}]
6329}
6330
6331proc mergediff {id} {
6332    global diffmergeid mdifffd
6333    global diffids
6334    global parents
6335    global diffcontext
6336    global limitdiffs vfilelimit curview
6337
6338    set diffmergeid $id
6339    set diffids $id
6340    # this doesn't seem to actually affect anything...
6341    set cmd [concat | git diff-tree --no-commit-id --cc -U$diffcontext $id]
6342    if {$limitdiffs && $vfilelimit($curview) ne {}} {
6343        set cmd [concat $cmd -- $vfilelimit($curview)]
6344    }
6345    if {[catch {set mdf [open $cmd r]} err]} {
6346        error_popup "[mc "Error getting merge diffs:"] $err"
6347        return
6348    }
6349    fconfigure $mdf -blocking 0
6350    set mdifffd($id) $mdf
6351    set np [llength $parents($curview,$id)]
6352    settabs $np
6353    filerun $mdf [list getmergediffline $mdf $id $np]
6354}
6355
6356proc getmergediffline {mdf id np} {
6357    global diffmergeid ctext cflist mergemax
6358    global difffilestart mdifffd
6359
6360    $ctext conf -state normal
6361    set nr 0
6362    while {[incr nr] <= 1000 && [gets $mdf line] >= 0} {
6363        if {![info exists diffmergeid] || $id != $diffmergeid
6364            || $mdf != $mdifffd($id)} {
6365            close $mdf
6366            return 0
6367        }
6368        if {[regexp {^diff --cc (.*)} $line match fname]} {
6369            # start of a new file
6370            $ctext insert end "\n"
6371            set here [$ctext index "end - 1c"]
6372            lappend difffilestart $here
6373            add_flist [list $fname]
6374            set l [expr {(78 - [string length $fname]) / 2}]
6375            set pad [string range "----------------------------------------" 1 $l]
6376            $ctext insert end "$pad $fname $pad\n" filesep
6377        } elseif {[regexp {^@@} $line]} {
6378            $ctext insert end "$line\n" hunksep
6379        } elseif {[regexp {^[0-9a-f]{40}$} $line] || [regexp {^index} $line]} {
6380            # do nothing
6381        } else {
6382            # parse the prefix - one ' ', '-' or '+' for each parent
6383            set spaces {}
6384            set minuses {}
6385            set pluses {}
6386            set isbad 0
6387            for {set j 0} {$j < $np} {incr j} {
6388                set c [string range $line $j $j]
6389                if {$c == " "} {
6390                    lappend spaces $j
6391                } elseif {$c == "-"} {
6392                    lappend minuses $j
6393                } elseif {$c == "+"} {
6394                    lappend pluses $j
6395                } else {
6396                    set isbad 1
6397                    break
6398                }
6399            }
6400            set tags {}
6401            set num {}
6402            if {!$isbad && $minuses ne {} && $pluses eq {}} {
6403                # line doesn't appear in result, parents in $minuses have the line
6404                set num [lindex $minuses 0]
6405            } elseif {!$isbad && $pluses ne {} && $minuses eq {}} {
6406                # line appears in result, parents in $pluses don't have the line
6407                lappend tags mresult
6408                set num [lindex $spaces 0]
6409            }
6410            if {$num ne {}} {
6411                if {$num >= $mergemax} {
6412                    set num "max"
6413                }
6414                lappend tags m$num
6415            }
6416            $ctext insert end "$line\n" $tags
6417        }
6418    }
6419    $ctext conf -state disabled
6420    if {[eof $mdf]} {
6421        close $mdf
6422        return 0
6423    }
6424    return [expr {$nr >= 1000? 2: 1}]
6425}
6426
6427proc startdiff {ids} {
6428    global treediffs diffids treepending diffmergeid nullid nullid2
6429
6430    settabs 1
6431    set diffids $ids
6432    catch {unset diffmergeid}
6433    if {![info exists treediffs($ids)] ||
6434        [lsearch -exact $ids $nullid] >= 0 ||
6435        [lsearch -exact $ids $nullid2] >= 0} {
6436        if {![info exists treepending]} {
6437            gettreediffs $ids
6438        }
6439    } else {
6440        addtocflist $ids
6441    }
6442}
6443
6444proc path_filter {filter name} {
6445    foreach p $filter {
6446        set l [string length $p]
6447        if {[string index $p end] eq "/"} {
6448            if {[string compare -length $l $p $name] == 0} {
6449                return 1
6450            }
6451        } else {
6452            if {[string compare -length $l $p $name] == 0 &&
6453                ([string length $name] == $l ||
6454                 [string index $name $l] eq "/")} {
6455                return 1
6456            }
6457        }
6458    }
6459    return 0
6460}
6461
6462proc addtocflist {ids} {
6463    global treediffs
6464
6465    add_flist $treediffs($ids)
6466    getblobdiffs $ids
6467}
6468
6469proc diffcmd {ids flags} {
6470    global nullid nullid2
6471
6472    set i [lsearch -exact $ids $nullid]
6473    set j [lsearch -exact $ids $nullid2]
6474    if {$i >= 0} {
6475        if {[llength $ids] > 1 && $j < 0} {
6476            # comparing working directory with some specific revision
6477            set cmd [concat | git diff-index $flags]
6478            if {$i == 0} {
6479                lappend cmd -R [lindex $ids 1]
6480            } else {
6481                lappend cmd [lindex $ids 0]
6482            }
6483        } else {
6484            # comparing working directory with index
6485            set cmd [concat | git diff-files $flags]
6486            if {$j == 1} {
6487                lappend cmd -R
6488            }
6489        }
6490    } elseif {$j >= 0} {
6491        set cmd [concat | git diff-index --cached $flags]
6492        if {[llength $ids] > 1} {
6493            # comparing index with specific revision
6494            if {$i == 0} {
6495                lappend cmd -R [lindex $ids 1]
6496            } else {
6497                lappend cmd [lindex $ids 0]
6498            }
6499        } else {
6500            # comparing index with HEAD
6501            lappend cmd HEAD
6502        }
6503    } else {
6504        set cmd [concat | git diff-tree -r $flags $ids]
6505    }
6506    return $cmd
6507}
6508
6509proc gettreediffs {ids} {
6510    global treediff treepending
6511
6512    if {[catch {set gdtf [open [diffcmd $ids {--no-commit-id}] r]}]} return
6513
6514    set treepending $ids
6515    set treediff {}
6516    fconfigure $gdtf -blocking 0
6517    filerun $gdtf [list gettreediffline $gdtf $ids]
6518}
6519
6520proc gettreediffline {gdtf ids} {
6521    global treediff treediffs treepending diffids diffmergeid
6522    global cmitmode vfilelimit curview limitdiffs
6523
6524    set nr 0
6525    while {[incr nr] <= 1000 && [gets $gdtf line] >= 0} {
6526        set i [string first "\t" $line]
6527        if {$i >= 0} {
6528            set file [string range $line [expr {$i+1}] end]
6529            if {[string index $file 0] eq "\""} {
6530                set file [lindex $file 0]
6531            }
6532            lappend treediff $file
6533        }
6534    }
6535    if {![eof $gdtf]} {
6536        return [expr {$nr >= 1000? 2: 1}]
6537    }
6538    close $gdtf
6539    if {$limitdiffs && $vfilelimit($curview) ne {}} {
6540        set flist {}
6541        foreach f $treediff {
6542            if {[path_filter $vfilelimit($curview) $f]} {
6543                lappend flist $f
6544            }
6545        }
6546        set treediffs($ids) $flist
6547    } else {
6548        set treediffs($ids) $treediff
6549    }
6550    unset treepending
6551    if {$cmitmode eq "tree"} {
6552        gettree $diffids
6553    } elseif {$ids != $diffids} {
6554        if {![info exists diffmergeid]} {
6555            gettreediffs $diffids
6556        }
6557    } else {
6558        addtocflist $ids
6559    }
6560    return 0
6561}
6562
6563# empty string or positive integer
6564proc diffcontextvalidate {v} {
6565    return [regexp {^(|[1-9][0-9]*)$} $v]
6566}
6567
6568proc diffcontextchange {n1 n2 op} {
6569    global diffcontextstring diffcontext
6570
6571    if {[string is integer -strict $diffcontextstring]} {
6572        if {$diffcontextstring > 0} {
6573            set diffcontext $diffcontextstring
6574            reselectline
6575        }
6576    }
6577}
6578
6579proc changeignorespace {} {
6580    reselectline
6581}
6582
6583proc getblobdiffs {ids} {
6584    global blobdifffd diffids env
6585    global diffinhdr treediffs
6586    global diffcontext
6587    global ignorespace
6588    global limitdiffs vfilelimit curview
6589
6590    set cmd [diffcmd $ids "-p -C --no-commit-id -U$diffcontext"]
6591    if {$ignorespace} {
6592        append cmd " -w"
6593    }
6594    if {$limitdiffs && $vfilelimit($curview) ne {}} {
6595        set cmd [concat $cmd -- $vfilelimit($curview)]
6596    }
6597    if {[catch {set bdf [open $cmd r]} err]} {
6598        puts "error getting diffs: $err"
6599        return
6600    }
6601    set diffinhdr 0
6602    fconfigure $bdf -blocking 0
6603    set blobdifffd($ids) $bdf
6604    filerun $bdf [list getblobdiffline $bdf $diffids]
6605}
6606
6607proc setinlist {var i val} {
6608    global $var
6609
6610    while {[llength [set $var]] < $i} {
6611        lappend $var {}
6612    }
6613    if {[llength [set $var]] == $i} {
6614        lappend $var $val
6615    } else {
6616        lset $var $i $val
6617    }
6618}
6619
6620proc makediffhdr {fname ids} {
6621    global ctext curdiffstart treediffs
6622
6623    set i [lsearch -exact $treediffs($ids) $fname]
6624    if {$i >= 0} {
6625        setinlist difffilestart $i $curdiffstart
6626    }
6627    set l [expr {(78 - [string length $fname]) / 2}]
6628    set pad [string range "----------------------------------------" 1 $l]
6629    $ctext insert $curdiffstart "$pad $fname $pad" filesep
6630}
6631
6632proc getblobdiffline {bdf ids} {
6633    global diffids blobdifffd ctext curdiffstart
6634    global diffnexthead diffnextnote difffilestart
6635    global diffinhdr treediffs
6636
6637    set nr 0
6638    $ctext conf -state normal
6639    while {[incr nr] <= 1000 && [gets $bdf line] >= 0} {
6640        if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
6641            close $bdf
6642            return 0
6643        }
6644        if {![string compare -length 11 "diff --git " $line]} {
6645            # trim off "diff --git "
6646            set line [string range $line 11 end]
6647            set diffinhdr 1
6648            # start of a new file
6649            $ctext insert end "\n"
6650            set curdiffstart [$ctext index "end - 1c"]
6651            $ctext insert end "\n" filesep
6652            # If the name hasn't changed the length will be odd,
6653            # the middle char will be a space, and the two bits either
6654            # side will be a/name and b/name, or "a/name" and "b/name".
6655            # If the name has changed we'll get "rename from" and
6656            # "rename to" or "copy from" and "copy to" lines following this,
6657            # and we'll use them to get the filenames.
6658            # This complexity is necessary because spaces in the filename(s)
6659            # don't get escaped.
6660            set l [string length $line]
6661            set i [expr {$l / 2}]
6662            if {!(($l & 1) && [string index $line $i] eq " " &&
6663                  [string range $line 2 [expr {$i - 1}]] eq \
6664                      [string range $line [expr {$i + 3}] end])} {
6665                continue
6666            }
6667            # unescape if quoted and chop off the a/ from the front
6668            if {[string index $line 0] eq "\""} {
6669                set fname [string range [lindex $line 0] 2 end]
6670            } else {
6671                set fname [string range $line 2 [expr {$i - 1}]]
6672            }
6673            makediffhdr $fname $ids
6674
6675        } elseif {[regexp {^@@ -([0-9]+)(,[0-9]+)? \+([0-9]+)(,[0-9]+)? @@(.*)} \
6676                       $line match f1l f1c f2l f2c rest]} {
6677            $ctext insert end "$line\n" hunksep
6678            set diffinhdr 0
6679
6680        } elseif {$diffinhdr} {
6681            if {![string compare -length 12 "rename from " $line]} {
6682                set fname [string range $line [expr 6 + [string first " from " $line] ] end]
6683                if {[string index $fname 0] eq "\""} {
6684                    set fname [lindex $fname 0]
6685                }
6686                set i [lsearch -exact $treediffs($ids) $fname]
6687                if {$i >= 0} {
6688                    setinlist difffilestart $i $curdiffstart
6689                }
6690            } elseif {![string compare -length 10 $line "rename to "] ||
6691                      ![string compare -length 8 $line "copy to "]} {
6692                set fname [string range $line [expr 4 + [string first " to " $line] ] end]
6693                if {[string index $fname 0] eq "\""} {
6694                    set fname [lindex $fname 0]
6695                }
6696                makediffhdr $fname $ids
6697            } elseif {[string compare -length 3 $line "---"] == 0} {
6698                # do nothing
6699                continue
6700            } elseif {[string compare -length 3 $line "+++"] == 0} {
6701                set diffinhdr 0
6702                continue
6703            }
6704            $ctext insert end "$line\n" filesep
6705
6706        } else {
6707            set x [string range $line 0 0]
6708            if {$x == "-" || $x == "+"} {
6709                set tag [expr {$x == "+"}]
6710                $ctext insert end "$line\n" d$tag
6711            } elseif {$x == " "} {
6712                $ctext insert end "$line\n"
6713            } else {
6714                # "\ No newline at end of file",
6715                # or something else we don't recognize
6716                $ctext insert end "$line\n" hunksep
6717            }
6718        }
6719    }
6720    $ctext conf -state disabled
6721    if {[eof $bdf]} {
6722        close $bdf
6723        return 0
6724    }
6725    return [expr {$nr >= 1000? 2: 1}]
6726}
6727
6728proc changediffdisp {} {
6729    global ctext diffelide
6730
6731    $ctext tag conf d0 -elide [lindex $diffelide 0]
6732    $ctext tag conf d1 -elide [lindex $diffelide 1]
6733}
6734
6735proc highlightfile {loc cline} {
6736    global ctext cflist cflist_top
6737
6738    $ctext yview $loc
6739    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
6740    $cflist tag add highlight $cline.0 "$cline.0 lineend"
6741    $cflist see $cline.0
6742    set cflist_top $cline
6743}
6744
6745proc prevfile {} {
6746    global difffilestart ctext cmitmode
6747
6748    if {$cmitmode eq "tree"} return
6749    set prev 0.0
6750    set prevline 1
6751    set here [$ctext index @0,0]
6752    foreach loc $difffilestart {
6753        if {[$ctext compare $loc >= $here]} {
6754            highlightfile $prev $prevline
6755            return
6756        }
6757        set prev $loc
6758        incr prevline
6759    }
6760    highlightfile $prev $prevline
6761}
6762
6763proc nextfile {} {
6764    global difffilestart ctext cmitmode
6765
6766    if {$cmitmode eq "tree"} return
6767    set here [$ctext index @0,0]
6768    set line 1
6769    foreach loc $difffilestart {
6770        incr line
6771        if {[$ctext compare $loc > $here]} {
6772            highlightfile $loc $line
6773            return
6774        }
6775    }
6776}
6777
6778proc clear_ctext {{first 1.0}} {
6779    global ctext smarktop smarkbot
6780    global pendinglinks
6781
6782    set l [lindex [split $first .] 0]
6783    if {![info exists smarktop] || [$ctext compare $first < $smarktop.0]} {
6784        set smarktop $l
6785    }
6786    if {![info exists smarkbot] || [$ctext compare $first < $smarkbot.0]} {
6787        set smarkbot $l
6788    }
6789    $ctext delete $first end
6790    if {$first eq "1.0"} {
6791        catch {unset pendinglinks}
6792    }
6793}
6794
6795proc settabs {{firstab {}}} {
6796    global firsttabstop tabstop ctext have_tk85
6797
6798    if {$firstab ne {} && $have_tk85} {
6799        set firsttabstop $firstab
6800    }
6801    set w [font measure textfont "0"]
6802    if {$firsttabstop != 0} {
6803        $ctext conf -tabs [list [expr {($firsttabstop + $tabstop) * $w}] \
6804                               [expr {($firsttabstop + 2 * $tabstop) * $w}]]
6805    } elseif {$have_tk85 || $tabstop != 8} {
6806        $ctext conf -tabs [expr {$tabstop * $w}]
6807    } else {
6808        $ctext conf -tabs {}
6809    }
6810}
6811
6812proc incrsearch {name ix op} {
6813    global ctext searchstring searchdirn
6814
6815    $ctext tag remove found 1.0 end
6816    if {[catch {$ctext index anchor}]} {
6817        # no anchor set, use start of selection, or of visible area
6818        set sel [$ctext tag ranges sel]
6819        if {$sel ne {}} {
6820            $ctext mark set anchor [lindex $sel 0]
6821        } elseif {$searchdirn eq "-forwards"} {
6822            $ctext mark set anchor @0,0
6823        } else {
6824            $ctext mark set anchor @0,[winfo height $ctext]
6825        }
6826    }
6827    if {$searchstring ne {}} {
6828        set here [$ctext search $searchdirn -- $searchstring anchor]
6829        if {$here ne {}} {
6830            $ctext see $here
6831        }
6832        searchmarkvisible 1
6833    }
6834}
6835
6836proc dosearch {} {
6837    global sstring ctext searchstring searchdirn
6838
6839    focus $sstring
6840    $sstring icursor end
6841    set searchdirn -forwards
6842    if {$searchstring ne {}} {
6843        set sel [$ctext tag ranges sel]
6844        if {$sel ne {}} {
6845            set start "[lindex $sel 0] + 1c"
6846        } elseif {[catch {set start [$ctext index anchor]}]} {
6847            set start "@0,0"
6848        }
6849        set match [$ctext search -count mlen -- $searchstring $start]
6850        $ctext tag remove sel 1.0 end
6851        if {$match eq {}} {
6852            bell
6853            return
6854        }
6855        $ctext see $match
6856        set mend "$match + $mlen c"
6857        $ctext tag add sel $match $mend
6858        $ctext mark unset anchor
6859    }
6860}
6861
6862proc dosearchback {} {
6863    global sstring ctext searchstring searchdirn
6864
6865    focus $sstring
6866    $sstring icursor end
6867    set searchdirn -backwards
6868    if {$searchstring ne {}} {
6869        set sel [$ctext tag ranges sel]
6870        if {$sel ne {}} {
6871            set start [lindex $sel 0]
6872        } elseif {[catch {set start [$ctext index anchor]}]} {
6873            set start @0,[winfo height $ctext]
6874        }
6875        set match [$ctext search -backwards -count ml -- $searchstring $start]
6876        $ctext tag remove sel 1.0 end
6877        if {$match eq {}} {
6878            bell
6879            return
6880        }
6881        $ctext see $match
6882        set mend "$match + $ml c"
6883        $ctext tag add sel $match $mend
6884        $ctext mark unset anchor
6885    }
6886}
6887
6888proc searchmark {first last} {
6889    global ctext searchstring
6890
6891    set mend $first.0
6892    while {1} {
6893        set match [$ctext search -count mlen -- $searchstring $mend $last.end]
6894        if {$match eq {}} break
6895        set mend "$match + $mlen c"
6896        $ctext tag add found $match $mend
6897    }
6898}
6899
6900proc searchmarkvisible {doall} {
6901    global ctext smarktop smarkbot
6902
6903    set topline [lindex [split [$ctext index @0,0] .] 0]
6904    set botline [lindex [split [$ctext index @0,[winfo height $ctext]] .] 0]
6905    if {$doall || $botline < $smarktop || $topline > $smarkbot} {
6906        # no overlap with previous
6907        searchmark $topline $botline
6908        set smarktop $topline
6909        set smarkbot $botline
6910    } else {
6911        if {$topline < $smarktop} {
6912            searchmark $topline [expr {$smarktop-1}]
6913            set smarktop $topline
6914        }
6915        if {$botline > $smarkbot} {
6916            searchmark [expr {$smarkbot+1}] $botline
6917            set smarkbot $botline
6918        }
6919    }
6920}
6921
6922proc scrolltext {f0 f1} {
6923    global searchstring
6924
6925    .bleft.bottom.sb set $f0 $f1
6926    if {$searchstring ne {}} {
6927        searchmarkvisible 0
6928    }
6929}
6930
6931proc setcoords {} {
6932    global linespc charspc canvx0 canvy0
6933    global xspc1 xspc2 lthickness
6934
6935    set linespc [font metrics mainfont -linespace]
6936    set charspc [font measure mainfont "m"]
6937    set canvy0 [expr {int(3 + 0.5 * $linespc)}]
6938    set canvx0 [expr {int(3 + 0.5 * $linespc)}]
6939    set lthickness [expr {int($linespc / 9) + 1}]
6940    set xspc1(0) $linespc
6941    set xspc2 $linespc
6942}
6943
6944proc redisplay {} {
6945    global canv
6946    global selectedline
6947
6948    set ymax [lindex [$canv cget -scrollregion] 3]
6949    if {$ymax eq {} || $ymax == 0} return
6950    set span [$canv yview]
6951    clear_display
6952    setcanvscroll
6953    allcanvs yview moveto [lindex $span 0]
6954    drawvisible
6955    if {$selectedline ne {}} {
6956        selectline $selectedline 0
6957        allcanvs yview moveto [lindex $span 0]
6958    }
6959}
6960
6961proc parsefont {f n} {
6962    global fontattr
6963
6964    set fontattr($f,family) [lindex $n 0]
6965    set s [lindex $n 1]
6966    if {$s eq {} || $s == 0} {
6967        set s 10
6968    } elseif {$s < 0} {
6969        set s [expr {int(-$s / [winfo fpixels . 1p] + 0.5)}]
6970    }
6971    set fontattr($f,size) $s
6972    set fontattr($f,weight) normal
6973    set fontattr($f,slant) roman
6974    foreach style [lrange $n 2 end] {
6975        switch -- $style {
6976            "normal" -
6977            "bold"   {set fontattr($f,weight) $style}
6978            "roman" -
6979            "italic" {set fontattr($f,slant) $style}
6980        }
6981    }
6982}
6983
6984proc fontflags {f {isbold 0}} {
6985    global fontattr
6986
6987    return [list -family $fontattr($f,family) -size $fontattr($f,size) \
6988                -weight [expr {$isbold? "bold": $fontattr($f,weight)}] \
6989                -slant $fontattr($f,slant)]
6990}
6991
6992proc fontname {f} {
6993    global fontattr
6994
6995    set n [list $fontattr($f,family) $fontattr($f,size)]
6996    if {$fontattr($f,weight) eq "bold"} {
6997        lappend n "bold"
6998    }
6999    if {$fontattr($f,slant) eq "italic"} {
7000        lappend n "italic"
7001    }
7002    return $n
7003}
7004
7005proc incrfont {inc} {
7006    global mainfont textfont ctext canv cflist showrefstop
7007    global stopped entries fontattr
7008
7009    unmarkmatches
7010    set s $fontattr(mainfont,size)
7011    incr s $inc
7012    if {$s < 1} {
7013        set s 1
7014    }
7015    set fontattr(mainfont,size) $s
7016    font config mainfont -size $s
7017    font config mainfontbold -size $s
7018    set mainfont [fontname mainfont]
7019    set s $fontattr(textfont,size)
7020    incr s $inc
7021    if {$s < 1} {
7022        set s 1
7023    }
7024    set fontattr(textfont,size) $s
7025    font config textfont -size $s
7026    font config textfontbold -size $s
7027    set textfont [fontname textfont]
7028    setcoords
7029    settabs
7030    redisplay
7031}
7032
7033proc clearsha1 {} {
7034    global sha1entry sha1string
7035    if {[string length $sha1string] == 40} {
7036        $sha1entry delete 0 end
7037    }
7038}
7039
7040proc sha1change {n1 n2 op} {
7041    global sha1string currentid sha1but
7042    if {$sha1string == {}
7043        || ([info exists currentid] && $sha1string == $currentid)} {
7044        set state disabled
7045    } else {
7046        set state normal
7047    }
7048    if {[$sha1but cget -state] == $state} return
7049    if {$state == "normal"} {
7050        $sha1but conf -state normal -relief raised -text "[mc "Goto:"] "
7051    } else {
7052        $sha1but conf -state disabled -relief flat -text "[mc "SHA1 ID:"] "
7053    }
7054}
7055
7056proc gotocommit {} {
7057    global sha1string tagids headids curview varcid
7058
7059    if {$sha1string == {}
7060        || ([info exists currentid] && $sha1string == $currentid)} return
7061    if {[info exists tagids($sha1string)]} {
7062        set id $tagids($sha1string)
7063    } elseif {[info exists headids($sha1string)]} {
7064        set id $headids($sha1string)
7065    } else {
7066        set id [string tolower $sha1string]
7067        if {[regexp {^[0-9a-f]{4,39}$} $id]} {
7068            set matches [array names varcid "$curview,$id*"]
7069            if {$matches ne {}} {
7070                if {[llength $matches] > 1} {
7071                    error_popup [mc "Short SHA1 id %s is ambiguous" $id]
7072                    return
7073                }
7074                set id [lindex [split [lindex $matches 0] ","] 1]
7075            }
7076        }
7077    }
7078    if {[commitinview $id $curview]} {
7079        selectline [rowofcommit $id] 1
7080        return
7081    }
7082    if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
7083        set msg [mc "SHA1 id %s is not known" $sha1string]
7084    } else {
7085        set msg [mc "Tag/Head %s is not known" $sha1string]
7086    }
7087    error_popup $msg
7088}
7089
7090proc lineenter {x y id} {
7091    global hoverx hovery hoverid hovertimer
7092    global commitinfo canv
7093
7094    if {![info exists commitinfo($id)] && ![getcommit $id]} return
7095    set hoverx $x
7096    set hovery $y
7097    set hoverid $id
7098    if {[info exists hovertimer]} {
7099        after cancel $hovertimer
7100    }
7101    set hovertimer [after 500 linehover]
7102    $canv delete hover
7103}
7104
7105proc linemotion {x y id} {
7106    global hoverx hovery hoverid hovertimer
7107
7108    if {[info exists hoverid] && $id == $hoverid} {
7109        set hoverx $x
7110        set hovery $y
7111        if {[info exists hovertimer]} {
7112            after cancel $hovertimer
7113        }
7114        set hovertimer [after 500 linehover]
7115    }
7116}
7117
7118proc lineleave {id} {
7119    global hoverid hovertimer canv
7120
7121    if {[info exists hoverid] && $id == $hoverid} {
7122        $canv delete hover
7123        if {[info exists hovertimer]} {
7124            after cancel $hovertimer
7125            unset hovertimer
7126        }
7127        unset hoverid
7128    }
7129}
7130
7131proc linehover {} {
7132    global hoverx hovery hoverid hovertimer
7133    global canv linespc lthickness
7134    global commitinfo
7135
7136    set text [lindex $commitinfo($hoverid) 0]
7137    set ymax [lindex [$canv cget -scrollregion] 3]
7138    if {$ymax == {}} return
7139    set yfrac [lindex [$canv yview] 0]
7140    set x [expr {$hoverx + 2 * $linespc}]
7141    set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
7142    set x0 [expr {$x - 2 * $lthickness}]
7143    set y0 [expr {$y - 2 * $lthickness}]
7144    set x1 [expr {$x + [font measure mainfont $text] + 2 * $lthickness}]
7145    set y1 [expr {$y + $linespc + 2 * $lthickness}]
7146    set t [$canv create rectangle $x0 $y0 $x1 $y1 \
7147               -fill \#ffff80 -outline black -width 1 -tags hover]
7148    $canv raise $t
7149    set t [$canv create text $x $y -anchor nw -text $text -tags hover \
7150               -font mainfont]
7151    $canv raise $t
7152}
7153
7154proc clickisonarrow {id y} {
7155    global lthickness
7156
7157    set ranges [rowranges $id]
7158    set thresh [expr {2 * $lthickness + 6}]
7159    set n [expr {[llength $ranges] - 1}]
7160    for {set i 1} {$i < $n} {incr i} {
7161        set row [lindex $ranges $i]
7162        if {abs([yc $row] - $y) < $thresh} {
7163            return $i
7164        }
7165    }
7166    return {}
7167}
7168
7169proc arrowjump {id n y} {
7170    global canv
7171
7172    # 1 <-> 2, 3 <-> 4, etc...
7173    set n [expr {(($n - 1) ^ 1) + 1}]
7174    set row [lindex [rowranges $id] $n]
7175    set yt [yc $row]
7176    set ymax [lindex [$canv cget -scrollregion] 3]
7177    if {$ymax eq {} || $ymax <= 0} return
7178    set view [$canv yview]
7179    set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
7180    set yfrac [expr {$yt / $ymax - $yspan / 2}]
7181    if {$yfrac < 0} {
7182        set yfrac 0
7183    }
7184    allcanvs yview moveto $yfrac
7185}
7186
7187proc lineclick {x y id isnew} {
7188    global ctext commitinfo children canv thickerline curview
7189
7190    if {![info exists commitinfo($id)] && ![getcommit $id]} return
7191    unmarkmatches
7192    unselectline
7193    normalline
7194    $canv delete hover
7195    # draw this line thicker than normal
7196    set thickerline $id
7197    drawlines $id
7198    if {$isnew} {
7199        set ymax [lindex [$canv cget -scrollregion] 3]
7200        if {$ymax eq {}} return
7201        set yfrac [lindex [$canv yview] 0]
7202        set y [expr {$y + $yfrac * $ymax}]
7203    }
7204    set dirn [clickisonarrow $id $y]
7205    if {$dirn ne {}} {
7206        arrowjump $id $dirn $y
7207        return
7208    }
7209
7210    if {$isnew} {
7211        addtohistory [list lineclick $x $y $id 0]
7212    }
7213    # fill the details pane with info about this line
7214    $ctext conf -state normal
7215    clear_ctext
7216    settabs 0
7217    $ctext insert end "[mc "Parent"]:\t"
7218    $ctext insert end $id link0
7219    setlink $id link0
7220    set info $commitinfo($id)
7221    $ctext insert end "\n\t[lindex $info 0]\n"
7222    $ctext insert end "\t[mc "Author"]:\t[lindex $info 1]\n"
7223    set date [formatdate [lindex $info 2]]
7224    $ctext insert end "\t[mc "Date"]:\t$date\n"
7225    set kids $children($curview,$id)
7226    if {$kids ne {}} {
7227        $ctext insert end "\n[mc "Children"]:"
7228        set i 0
7229        foreach child $kids {
7230            incr i
7231            if {![info exists commitinfo($child)] && ![getcommit $child]} continue
7232            set info $commitinfo($child)
7233            $ctext insert end "\n\t"
7234            $ctext insert end $child link$i
7235            setlink $child link$i
7236            $ctext insert end "\n\t[lindex $info 0]"
7237            $ctext insert end "\n\t[mc "Author"]:\t[lindex $info 1]"
7238            set date [formatdate [lindex $info 2]]
7239            $ctext insert end "\n\t[mc "Date"]:\t$date\n"
7240        }
7241    }
7242    $ctext conf -state disabled
7243    init_flist {}
7244}
7245
7246proc normalline {} {
7247    global thickerline
7248    if {[info exists thickerline]} {
7249        set id $thickerline
7250        unset thickerline
7251        drawlines $id
7252    }
7253}
7254
7255proc selbyid {id} {
7256    global curview
7257    if {[commitinview $id $curview]} {
7258        selectline [rowofcommit $id] 1
7259    }
7260}
7261
7262proc mstime {} {
7263    global startmstime
7264    if {![info exists startmstime]} {
7265        set startmstime [clock clicks -milliseconds]
7266    }
7267    return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
7268}
7269
7270proc rowmenu {x y id} {
7271    global rowctxmenu selectedline rowmenuid curview
7272    global nullid nullid2 fakerowmenu mainhead
7273
7274    stopfinding
7275    set rowmenuid $id
7276    if {$selectedline eq {} || [rowofcommit $id] eq $selectedline} {
7277        set state disabled
7278    } else {
7279        set state normal
7280    }
7281    if {$id ne $nullid && $id ne $nullid2} {
7282        set menu $rowctxmenu
7283        if {$mainhead ne {}} {
7284            $menu entryconfigure 7 -label [mc "Reset %s branch to here" $mainhead]
7285        } else {
7286            $menu entryconfigure 7 -label [mc "Detached head: can't reset" $mainhead] -state disabled
7287        }
7288    } else {
7289        set menu $fakerowmenu
7290    }
7291    $menu entryconfigure [mc "Diff this -> selected"] -state $state
7292    $menu entryconfigure [mc "Diff selected -> this"] -state $state
7293    $menu entryconfigure [mc "Make patch"] -state $state
7294    tk_popup $menu $x $y
7295}
7296
7297proc diffvssel {dirn} {
7298    global rowmenuid selectedline
7299
7300    if {$selectedline eq {}} return
7301    if {$dirn} {
7302        set oldid [commitonrow $selectedline]
7303        set newid $rowmenuid
7304    } else {
7305        set oldid $rowmenuid
7306        set newid [commitonrow $selectedline]
7307    }
7308    addtohistory [list doseldiff $oldid $newid]
7309    doseldiff $oldid $newid
7310}
7311
7312proc doseldiff {oldid newid} {
7313    global ctext
7314    global commitinfo
7315
7316    $ctext conf -state normal
7317    clear_ctext
7318    init_flist [mc "Top"]
7319    $ctext insert end "[mc "From"] "
7320    $ctext insert end $oldid link0
7321    setlink $oldid link0
7322    $ctext insert end "\n     "
7323    $ctext insert end [lindex $commitinfo($oldid) 0]
7324    $ctext insert end "\n\n[mc "To"]   "
7325    $ctext insert end $newid link1
7326    setlink $newid link1
7327    $ctext insert end "\n     "
7328    $ctext insert end [lindex $commitinfo($newid) 0]
7329    $ctext insert end "\n"
7330    $ctext conf -state disabled
7331    $ctext tag remove found 1.0 end
7332    startdiff [list $oldid $newid]
7333}
7334
7335proc mkpatch {} {
7336    global rowmenuid currentid commitinfo patchtop patchnum
7337
7338    if {![info exists currentid]} return
7339    set oldid $currentid
7340    set oldhead [lindex $commitinfo($oldid) 0]
7341    set newid $rowmenuid
7342    set newhead [lindex $commitinfo($newid) 0]
7343    set top .patch
7344    set patchtop $top
7345    catch {destroy $top}
7346    toplevel $top
7347    label $top.title -text [mc "Generate patch"]
7348    grid $top.title - -pady 10
7349    label $top.from -text [mc "From:"]
7350    entry $top.fromsha1 -width 40 -relief flat
7351    $top.fromsha1 insert 0 $oldid
7352    $top.fromsha1 conf -state readonly
7353    grid $top.from $top.fromsha1 -sticky w
7354    entry $top.fromhead -width 60 -relief flat
7355    $top.fromhead insert 0 $oldhead
7356    $top.fromhead conf -state readonly
7357    grid x $top.fromhead -sticky w
7358    label $top.to -text [mc "To:"]
7359    entry $top.tosha1 -width 40 -relief flat
7360    $top.tosha1 insert 0 $newid
7361    $top.tosha1 conf -state readonly
7362    grid $top.to $top.tosha1 -sticky w
7363    entry $top.tohead -width 60 -relief flat
7364    $top.tohead insert 0 $newhead
7365    $top.tohead conf -state readonly
7366    grid x $top.tohead -sticky w
7367    button $top.rev -text [mc "Reverse"] -command mkpatchrev -padx 5
7368    grid $top.rev x -pady 10
7369    label $top.flab -text [mc "Output file:"]
7370    entry $top.fname -width 60
7371    $top.fname insert 0 [file normalize "patch$patchnum.patch"]
7372    incr patchnum
7373    grid $top.flab $top.fname -sticky w
7374    frame $top.buts
7375    button $top.buts.gen -text [mc "Generate"] -command mkpatchgo
7376    button $top.buts.can -text [mc "Cancel"] -command mkpatchcan
7377    grid $top.buts.gen $top.buts.can
7378    grid columnconfigure $top.buts 0 -weight 1 -uniform a
7379    grid columnconfigure $top.buts 1 -weight 1 -uniform a
7380    grid $top.buts - -pady 10 -sticky ew
7381    focus $top.fname
7382}
7383
7384proc mkpatchrev {} {
7385    global patchtop
7386
7387    set oldid [$patchtop.fromsha1 get]
7388    set oldhead [$patchtop.fromhead get]
7389    set newid [$patchtop.tosha1 get]
7390    set newhead [$patchtop.tohead get]
7391    foreach e [list fromsha1 fromhead tosha1 tohead] \
7392            v [list $newid $newhead $oldid $oldhead] {
7393        $patchtop.$e conf -state normal
7394        $patchtop.$e delete 0 end
7395        $patchtop.$e insert 0 $v
7396        $patchtop.$e conf -state readonly
7397    }
7398}
7399
7400proc mkpatchgo {} {
7401    global patchtop nullid nullid2
7402
7403    set oldid [$patchtop.fromsha1 get]
7404    set newid [$patchtop.tosha1 get]
7405    set fname [$patchtop.fname get]
7406    set cmd [diffcmd [list $oldid $newid] -p]
7407    # trim off the initial "|"
7408    set cmd [lrange $cmd 1 end]
7409    lappend cmd >$fname &
7410    if {[catch {eval exec $cmd} err]} {
7411        error_popup "[mc "Error creating patch:"] $err"
7412    }
7413    catch {destroy $patchtop}
7414    unset patchtop
7415}
7416
7417proc mkpatchcan {} {
7418    global patchtop
7419
7420    catch {destroy $patchtop}
7421    unset patchtop
7422}
7423
7424proc mktag {} {
7425    global rowmenuid mktagtop commitinfo
7426
7427    set top .maketag
7428    set mktagtop $top
7429    catch {destroy $top}
7430    toplevel $top
7431    label $top.title -text [mc "Create tag"]
7432    grid $top.title - -pady 10
7433    label $top.id -text [mc "ID:"]
7434    entry $top.sha1 -width 40 -relief flat
7435    $top.sha1 insert 0 $rowmenuid
7436    $top.sha1 conf -state readonly
7437    grid $top.id $top.sha1 -sticky w
7438    entry $top.head -width 60 -relief flat
7439    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
7440    $top.head conf -state readonly
7441    grid x $top.head -sticky w
7442    label $top.tlab -text [mc "Tag name:"]
7443    entry $top.tag -width 60
7444    grid $top.tlab $top.tag -sticky w
7445    frame $top.buts
7446    button $top.buts.gen -text [mc "Create"] -command mktaggo
7447    button $top.buts.can -text [mc "Cancel"] -command mktagcan
7448    grid $top.buts.gen $top.buts.can
7449    grid columnconfigure $top.buts 0 -weight 1 -uniform a
7450    grid columnconfigure $top.buts 1 -weight 1 -uniform a
7451    grid $top.buts - -pady 10 -sticky ew
7452    focus $top.tag
7453}
7454
7455proc domktag {} {
7456    global mktagtop env tagids idtags
7457
7458    set id [$mktagtop.sha1 get]
7459    set tag [$mktagtop.tag get]
7460    if {$tag == {}} {
7461        error_popup [mc "No tag name specified"]
7462        return
7463    }
7464    if {[info exists tagids($tag)]} {
7465        error_popup [mc "Tag \"%s\" already exists" $tag]
7466        return
7467    }
7468    if {[catch {
7469        exec git tag $tag $id
7470    } err]} {
7471        error_popup "[mc "Error creating tag:"] $err"
7472        return
7473    }
7474
7475    set tagids($tag) $id
7476    lappend idtags($id) $tag
7477    redrawtags $id
7478    addedtag $id
7479    dispneartags 0
7480    run refill_reflist
7481}
7482
7483proc redrawtags {id} {
7484    global canv linehtag idpos currentid curview cmitlisted
7485    global canvxmax iddrawn circleitem mainheadid circlecolors
7486
7487    if {![commitinview $id $curview]} return
7488    if {![info exists iddrawn($id)]} return
7489    set row [rowofcommit $id]
7490    if {$id eq $mainheadid} {
7491        set ofill yellow
7492    } else {
7493        set ofill [lindex $circlecolors $cmitlisted($curview,$id)]
7494    }
7495    $canv itemconf $circleitem($row) -fill $ofill
7496    $canv delete tag.$id
7497    set xt [eval drawtags $id $idpos($id)]
7498    $canv coords $linehtag($row) $xt [lindex $idpos($id) 2]
7499    set text [$canv itemcget $linehtag($row) -text]
7500    set font [$canv itemcget $linehtag($row) -font]
7501    set xr [expr {$xt + [font measure $font $text]}]
7502    if {$xr > $canvxmax} {
7503        set canvxmax $xr
7504        setcanvscroll
7505    }
7506    if {[info exists currentid] && $currentid == $id} {
7507        make_secsel $row
7508    }
7509}
7510
7511proc mktagcan {} {
7512    global mktagtop
7513
7514    catch {destroy $mktagtop}
7515    unset mktagtop
7516}
7517
7518proc mktaggo {} {
7519    domktag
7520    mktagcan
7521}
7522
7523proc writecommit {} {
7524    global rowmenuid wrcomtop commitinfo wrcomcmd
7525
7526    set top .writecommit
7527    set wrcomtop $top
7528    catch {destroy $top}
7529    toplevel $top
7530    label $top.title -text [mc "Write commit to file"]
7531    grid $top.title - -pady 10
7532    label $top.id -text [mc "ID:"]
7533    entry $top.sha1 -width 40 -relief flat
7534    $top.sha1 insert 0 $rowmenuid
7535    $top.sha1 conf -state readonly
7536    grid $top.id $top.sha1 -sticky w
7537    entry $top.head -width 60 -relief flat
7538    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
7539    $top.head conf -state readonly
7540    grid x $top.head -sticky w
7541    label $top.clab -text [mc "Command:"]
7542    entry $top.cmd -width 60 -textvariable wrcomcmd
7543    grid $top.clab $top.cmd -sticky w -pady 10
7544    label $top.flab -text [mc "Output file:"]
7545    entry $top.fname -width 60
7546    $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
7547    grid $top.flab $top.fname -sticky w
7548    frame $top.buts
7549    button $top.buts.gen -text [mc "Write"] -command wrcomgo
7550    button $top.buts.can -text [mc "Cancel"] -command wrcomcan
7551    grid $top.buts.gen $top.buts.can
7552    grid columnconfigure $top.buts 0 -weight 1 -uniform a
7553    grid columnconfigure $top.buts 1 -weight 1 -uniform a
7554    grid $top.buts - -pady 10 -sticky ew
7555    focus $top.fname
7556}
7557
7558proc wrcomgo {} {
7559    global wrcomtop
7560
7561    set id [$wrcomtop.sha1 get]
7562    set cmd "echo $id | [$wrcomtop.cmd get]"
7563    set fname [$wrcomtop.fname get]
7564    if {[catch {exec sh -c $cmd >$fname &} err]} {
7565        error_popup "[mc "Error writing commit:"] $err"
7566    }
7567    catch {destroy $wrcomtop}
7568    unset wrcomtop
7569}
7570
7571proc wrcomcan {} {
7572    global wrcomtop
7573
7574    catch {destroy $wrcomtop}
7575    unset wrcomtop
7576}
7577
7578proc mkbranch {} {
7579    global rowmenuid mkbrtop
7580
7581    set top .makebranch
7582    catch {destroy $top}
7583    toplevel $top
7584    label $top.title -text [mc "Create new branch"]
7585    grid $top.title - -pady 10
7586    label $top.id -text [mc "ID:"]
7587    entry $top.sha1 -width 40 -relief flat
7588    $top.sha1 insert 0 $rowmenuid
7589    $top.sha1 conf -state readonly
7590    grid $top.id $top.sha1 -sticky w
7591    label $top.nlab -text [mc "Name:"]
7592    entry $top.name -width 40
7593    grid $top.nlab $top.name -sticky w
7594    frame $top.buts
7595    button $top.buts.go -text [mc "Create"] -command [list mkbrgo $top]
7596    button $top.buts.can -text [mc "Cancel"] -command "catch {destroy $top}"
7597    grid $top.buts.go $top.buts.can
7598    grid columnconfigure $top.buts 0 -weight 1 -uniform a
7599    grid columnconfigure $top.buts 1 -weight 1 -uniform a
7600    grid $top.buts - -pady 10 -sticky ew
7601    focus $top.name
7602}
7603
7604proc mkbrgo {top} {
7605    global headids idheads
7606
7607    set name [$top.name get]
7608    set id [$top.sha1 get]
7609    if {$name eq {}} {
7610        error_popup [mc "Please specify a name for the new branch"]
7611        return
7612    }
7613    catch {destroy $top}
7614    nowbusy newbranch
7615    update
7616    if {[catch {
7617        exec git branch $name $id
7618    } err]} {
7619        notbusy newbranch
7620        error_popup $err
7621    } else {
7622        set headids($name) $id
7623        lappend idheads($id) $name
7624        addedhead $id $name
7625        notbusy newbranch
7626        redrawtags $id
7627        dispneartags 0
7628        run refill_reflist
7629    }
7630}
7631
7632proc cherrypick {} {
7633    global rowmenuid curview
7634    global mainhead mainheadid
7635
7636    set oldhead [exec git rev-parse HEAD]
7637    set dheads [descheads $rowmenuid]
7638    if {$dheads ne {} && [lsearch -exact $dheads $oldhead] >= 0} {
7639        set ok [confirm_popup [mc "Commit %s is already\
7640                included in branch %s -- really re-apply it?" \
7641                                   [string range $rowmenuid 0 7] $mainhead]]
7642        if {!$ok} return
7643    }
7644    nowbusy cherrypick [mc "Cherry-picking"]
7645    update
7646    # Unfortunately git-cherry-pick writes stuff to stderr even when
7647    # no error occurs, and exec takes that as an indication of error...
7648    if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} {
7649        notbusy cherrypick
7650        error_popup $err
7651        return
7652    }
7653    set newhead [exec git rev-parse HEAD]
7654    if {$newhead eq $oldhead} {
7655        notbusy cherrypick
7656        error_popup [mc "No changes committed"]
7657        return
7658    }
7659    addnewchild $newhead $oldhead
7660    if {[commitinview $oldhead $curview]} {
7661        insertrow $newhead $oldhead $curview
7662        if {$mainhead ne {}} {
7663            movehead $newhead $mainhead
7664            movedhead $newhead $mainhead
7665        }
7666        set mainheadid $newhead
7667        redrawtags $oldhead
7668        redrawtags $newhead
7669        selbyid $newhead
7670    }
7671    notbusy cherrypick
7672}
7673
7674proc resethead {} {
7675    global mainhead rowmenuid confirm_ok resettype
7676
7677    set confirm_ok 0
7678    set w ".confirmreset"
7679    toplevel $w
7680    wm transient $w .
7681    wm title $w [mc "Confirm reset"]
7682    message $w.m -text \
7683        [mc "Reset branch %s to %s?" $mainhead [string range $rowmenuid 0 7]] \
7684        -justify center -aspect 1000
7685    pack $w.m -side top -fill x -padx 20 -pady 20
7686    frame $w.f -relief sunken -border 2
7687    message $w.f.rt -text [mc "Reset type:"] -aspect 1000
7688    grid $w.f.rt -sticky w
7689    set resettype mixed
7690    radiobutton $w.f.soft -value soft -variable resettype -justify left \
7691        -text [mc "Soft: Leave working tree and index untouched"]
7692    grid $w.f.soft -sticky w
7693    radiobutton $w.f.mixed -value mixed -variable resettype -justify left \
7694        -text [mc "Mixed: Leave working tree untouched, reset index"]
7695    grid $w.f.mixed -sticky w
7696    radiobutton $w.f.hard -value hard -variable resettype -justify left \
7697        -text [mc "Hard: Reset working tree and index\n(discard ALL local changes)"]
7698    grid $w.f.hard -sticky w
7699    pack $w.f -side top -fill x
7700    button $w.ok -text [mc OK] -command "set confirm_ok 1; destroy $w"
7701    pack $w.ok -side left -fill x -padx 20 -pady 20
7702    button $w.cancel -text [mc Cancel] -command "destroy $w"
7703    pack $w.cancel -side right -fill x -padx 20 -pady 20
7704    bind $w <Visibility> "grab $w; focus $w"
7705    tkwait window $w
7706    if {!$confirm_ok} return
7707    if {[catch {set fd [open \
7708            [list | git reset --$resettype $rowmenuid 2>@1] r]} err]} {
7709        error_popup $err
7710    } else {
7711        dohidelocalchanges
7712        filerun $fd [list readresetstat $fd]
7713        nowbusy reset [mc "Resetting"]
7714        selbyid $rowmenuid
7715    }
7716}
7717
7718proc readresetstat {fd} {
7719    global mainhead mainheadid showlocalchanges rprogcoord
7720
7721    if {[gets $fd line] >= 0} {
7722        if {[regexp {([0-9]+)% \(([0-9]+)/([0-9]+)\)} $line match p m n]} {
7723            set rprogcoord [expr {1.0 * $m / $n}]
7724            adjustprogress
7725        }
7726        return 1
7727    }
7728    set rprogcoord 0
7729    adjustprogress
7730    notbusy reset
7731    if {[catch {close $fd} err]} {
7732        error_popup $err
7733    }
7734    set oldhead $mainheadid
7735    set newhead [exec git rev-parse HEAD]
7736    if {$newhead ne $oldhead} {
7737        movehead $newhead $mainhead
7738        movedhead $newhead $mainhead
7739        set mainheadid $newhead
7740        redrawtags $oldhead
7741        redrawtags $newhead
7742    }
7743    if {$showlocalchanges} {
7744        doshowlocalchanges
7745    }
7746    return 0
7747}
7748
7749# context menu for a head
7750proc headmenu {x y id head} {
7751    global headmenuid headmenuhead headctxmenu mainhead
7752
7753    stopfinding
7754    set headmenuid $id
7755    set headmenuhead $head
7756    set state normal
7757    if {$head eq $mainhead} {
7758        set state disabled
7759    }
7760    $headctxmenu entryconfigure 0 -state $state
7761    $headctxmenu entryconfigure 1 -state $state
7762    tk_popup $headctxmenu $x $y
7763}
7764
7765proc cobranch {} {
7766    global headmenuid headmenuhead headids
7767    global showlocalchanges mainheadid
7768
7769    # check the tree is clean first??
7770    nowbusy checkout [mc "Checking out"]
7771    update
7772    dohidelocalchanges
7773    if {[catch {
7774        set fd [open [list | git checkout $headmenuhead 2>@1] r]
7775    } err]} {
7776        notbusy checkout
7777        error_popup $err
7778        if {$showlocalchanges} {
7779            dodiffindex
7780        }
7781    } else {
7782        filerun $fd [list readcheckoutstat $fd $headmenuhead $headmenuid]
7783    }
7784}
7785
7786proc readcheckoutstat {fd newhead newheadid} {
7787    global mainhead mainheadid headids showlocalchanges progresscoords
7788
7789    if {[gets $fd line] >= 0} {
7790        if {[regexp {([0-9]+)% \(([0-9]+)/([0-9]+)\)} $line match p m n]} {
7791            set progresscoords [list 0 [expr {1.0 * $m / $n}]]
7792            adjustprogress
7793        }
7794        return 1
7795    }
7796    set progresscoords {0 0}
7797    adjustprogress
7798    notbusy checkout
7799    if {[catch {close $fd} err]} {
7800        error_popup $err
7801    }
7802    set oldmainid $mainheadid
7803    set mainhead $newhead
7804    set mainheadid $newheadid
7805    redrawtags $oldmainid
7806    redrawtags $newheadid
7807    selbyid $newheadid
7808    if {$showlocalchanges} {
7809        dodiffindex
7810    }
7811}
7812
7813proc rmbranch {} {
7814    global headmenuid headmenuhead mainhead
7815    global idheads
7816
7817    set head $headmenuhead
7818    set id $headmenuid
7819    # this check shouldn't be needed any more...
7820    if {$head eq $mainhead} {
7821        error_popup [mc "Cannot delete the currently checked-out branch"]
7822        return
7823    }
7824    set dheads [descheads $id]
7825    if {[llength $dheads] == 1 && $idheads($dheads) eq $head} {
7826        # the stuff on this branch isn't on any other branch
7827        if {![confirm_popup [mc "The commits on branch %s aren't on any other\
7828                        branch.\nReally delete branch %s?" $head $head]]} return
7829    }
7830    nowbusy rmbranch
7831    update
7832    if {[catch {exec git branch -D $head} err]} {
7833        notbusy rmbranch
7834        error_popup $err
7835        return
7836    }
7837    removehead $id $head
7838    removedhead $id $head
7839    redrawtags $id
7840    notbusy rmbranch
7841    dispneartags 0
7842    run refill_reflist
7843}
7844
7845# Display a list of tags and heads
7846proc showrefs {} {
7847    global showrefstop bgcolor fgcolor selectbgcolor
7848    global bglist fglist reflistfilter reflist maincursor
7849
7850    set top .showrefs
7851    set showrefstop $top
7852    if {[winfo exists $top]} {
7853        raise $top
7854        refill_reflist
7855        return
7856    }
7857    toplevel $top
7858    wm title $top [mc "Tags and heads: %s" [file tail [pwd]]]
7859    text $top.list -background $bgcolor -foreground $fgcolor \
7860        -selectbackground $selectbgcolor -font mainfont \
7861        -xscrollcommand "$top.xsb set" -yscrollcommand "$top.ysb set" \
7862        -width 30 -height 20 -cursor $maincursor \
7863        -spacing1 1 -spacing3 1 -state disabled
7864    $top.list tag configure highlight -background $selectbgcolor
7865    lappend bglist $top.list
7866    lappend fglist $top.list
7867    scrollbar $top.ysb -command "$top.list yview" -orient vertical
7868    scrollbar $top.xsb -command "$top.list xview" -orient horizontal
7869    grid $top.list $top.ysb -sticky nsew
7870    grid $top.xsb x -sticky ew
7871    frame $top.f
7872    label $top.f.l -text "[mc "Filter"]: "
7873    entry $top.f.e -width 20 -textvariable reflistfilter
7874    set reflistfilter "*"
7875    trace add variable reflistfilter write reflistfilter_change
7876    pack $top.f.e -side right -fill x -expand 1
7877    pack $top.f.l -side left
7878    grid $top.f - -sticky ew -pady 2
7879    button $top.close -command [list destroy $top] -text [mc "Close"]
7880    grid $top.close -
7881    grid columnconfigure $top 0 -weight 1
7882    grid rowconfigure $top 0 -weight 1
7883    bind $top.list <1> {break}
7884    bind $top.list <B1-Motion> {break}
7885    bind $top.list <ButtonRelease-1> {sel_reflist %W %x %y; break}
7886    set reflist {}
7887    refill_reflist
7888}
7889
7890proc sel_reflist {w x y} {
7891    global showrefstop reflist headids tagids otherrefids
7892
7893    if {![winfo exists $showrefstop]} return
7894    set l [lindex [split [$w index "@$x,$y"] "."] 0]
7895    set ref [lindex $reflist [expr {$l-1}]]
7896    set n [lindex $ref 0]
7897    switch -- [lindex $ref 1] {
7898        "H" {selbyid $headids($n)}
7899        "T" {selbyid $tagids($n)}
7900        "o" {selbyid $otherrefids($n)}
7901    }
7902    $showrefstop.list tag add highlight $l.0 "$l.0 lineend"
7903}
7904
7905proc unsel_reflist {} {
7906    global showrefstop
7907
7908    if {![info exists showrefstop] || ![winfo exists $showrefstop]} return
7909    $showrefstop.list tag remove highlight 0.0 end
7910}
7911
7912proc reflistfilter_change {n1 n2 op} {
7913    global reflistfilter
7914
7915    after cancel refill_reflist
7916    after 200 refill_reflist
7917}
7918
7919proc refill_reflist {} {
7920    global reflist reflistfilter showrefstop headids tagids otherrefids
7921    global curview commitinterest
7922
7923    if {![info exists showrefstop] || ![winfo exists $showrefstop]} return
7924    set refs {}
7925    foreach n [array names headids] {
7926        if {[string match $reflistfilter $n]} {
7927            if {[commitinview $headids($n) $curview]} {
7928                lappend refs [list $n H]
7929            } else {
7930                set commitinterest($headids($n)) {run refill_reflist}
7931            }
7932        }
7933    }
7934    foreach n [array names tagids] {
7935        if {[string match $reflistfilter $n]} {
7936            if {[commitinview $tagids($n) $curview]} {
7937                lappend refs [list $n T]
7938            } else {
7939                set commitinterest($tagids($n)) {run refill_reflist}
7940            }
7941        }
7942    }
7943    foreach n [array names otherrefids] {
7944        if {[string match $reflistfilter $n]} {
7945            if {[commitinview $otherrefids($n) $curview]} {
7946                lappend refs [list $n o]
7947            } else {
7948                set commitinterest($otherrefids($n)) {run refill_reflist}
7949            }
7950        }
7951    }
7952    set refs [lsort -index 0 $refs]
7953    if {$refs eq $reflist} return
7954
7955    # Update the contents of $showrefstop.list according to the
7956    # differences between $reflist (old) and $refs (new)
7957    $showrefstop.list conf -state normal
7958    $showrefstop.list insert end "\n"
7959    set i 0
7960    set j 0
7961    while {$i < [llength $reflist] || $j < [llength $refs]} {
7962        if {$i < [llength $reflist]} {
7963            if {$j < [llength $refs]} {
7964                set cmp [string compare [lindex $reflist $i 0] \
7965                             [lindex $refs $j 0]]
7966                if {$cmp == 0} {
7967                    set cmp [string compare [lindex $reflist $i 1] \
7968                                 [lindex $refs $j 1]]
7969                }
7970            } else {
7971                set cmp -1
7972            }
7973        } else {
7974            set cmp 1
7975        }
7976        switch -- $cmp {
7977            -1 {
7978                $showrefstop.list delete "[expr {$j+1}].0" "[expr {$j+2}].0"
7979                incr i
7980            }
7981            0 {
7982                incr i
7983                incr j
7984            }
7985            1 {
7986                set l [expr {$j + 1}]
7987                $showrefstop.list image create $l.0 -align baseline \
7988                    -image reficon-[lindex $refs $j 1] -padx 2
7989                $showrefstop.list insert $l.1 "[lindex $refs $j 0]\n"
7990                incr j
7991            }
7992        }
7993    }
7994    set reflist $refs
7995    # delete last newline
7996    $showrefstop.list delete end-2c end-1c
7997    $showrefstop.list conf -state disabled
7998}
7999
8000# Stuff for finding nearby tags
8001proc getallcommits {} {
8002    global allcommits nextarc seeds allccache allcwait cachedarcs allcupdate
8003    global idheads idtags idotherrefs allparents tagobjid
8004
8005    if {![info exists allcommits]} {
8006        set nextarc 0
8007        set allcommits 0
8008        set seeds {}
8009        set allcwait 0
8010        set cachedarcs 0
8011        set allccache [file join [gitdir] "gitk.cache"]
8012        if {![catch {
8013            set f [open $allccache r]
8014            set allcwait 1
8015            getcache $f
8016        }]} return
8017    }
8018
8019    if {$allcwait} {
8020        return
8021    }
8022    set cmd [list | git rev-list --parents]
8023    set allcupdate [expr {$seeds ne {}}]
8024    if {!$allcupdate} {
8025        set ids "--all"
8026    } else {
8027        set refs [concat [array names idheads] [array names idtags] \
8028                      [array names idotherrefs]]
8029        set ids {}
8030        set tagobjs {}
8031        foreach name [array names tagobjid] {
8032            lappend tagobjs $tagobjid($name)
8033        }
8034        foreach id [lsort -unique $refs] {
8035            if {![info exists allparents($id)] &&
8036                [lsearch -exact $tagobjs $id] < 0} {
8037                lappend ids $id
8038            }
8039        }
8040        if {$ids ne {}} {
8041            foreach id $seeds {
8042                lappend ids "^$id"
8043            }
8044        }
8045    }
8046    if {$ids ne {}} {
8047        set fd [open [concat $cmd $ids] r]
8048        fconfigure $fd -blocking 0
8049        incr allcommits
8050        nowbusy allcommits
8051        filerun $fd [list getallclines $fd]
8052    } else {
8053        dispneartags 0
8054    }
8055}
8056
8057# Since most commits have 1 parent and 1 child, we group strings of
8058# such commits into "arcs" joining branch/merge points (BMPs), which
8059# are commits that either don't have 1 parent or don't have 1 child.
8060#
8061# arcnos(id) - incoming arcs for BMP, arc we're on for other nodes
8062# arcout(id) - outgoing arcs for BMP
8063# arcids(a) - list of IDs on arc including end but not start
8064# arcstart(a) - BMP ID at start of arc
8065# arcend(a) - BMP ID at end of arc
8066# growing(a) - arc a is still growing
8067# arctags(a) - IDs out of arcids (excluding end) that have tags
8068# archeads(a) - IDs out of arcids (excluding end) that have heads
8069# The start of an arc is at the descendent end, so "incoming" means
8070# coming from descendents, and "outgoing" means going towards ancestors.
8071
8072proc getallclines {fd} {
8073    global allparents allchildren idtags idheads nextarc
8074    global arcnos arcids arctags arcout arcend arcstart archeads growing
8075    global seeds allcommits cachedarcs allcupdate
8076    
8077    set nid 0
8078    while {[incr nid] <= 1000 && [gets $fd line] >= 0} {
8079        set id [lindex $line 0]
8080        if {[info exists allparents($id)]} {
8081            # seen it already
8082            continue
8083        }
8084        set cachedarcs 0
8085        set olds [lrange $line 1 end]
8086        set allparents($id) $olds
8087        if {![info exists allchildren($id)]} {
8088            set allchildren($id) {}
8089            set arcnos($id) {}
8090            lappend seeds $id
8091        } else {
8092            set a $arcnos($id)
8093            if {[llength $olds] == 1 && [llength $a] == 1} {
8094                lappend arcids($a) $id
8095                if {[info exists idtags($id)]} {
8096                    lappend arctags($a) $id
8097                }
8098                if {[info exists idheads($id)]} {
8099                    lappend archeads($a) $id
8100                }
8101                if {[info exists allparents($olds)]} {
8102                    # seen parent already
8103                    if {![info exists arcout($olds)]} {
8104                        splitarc $olds
8105                    }
8106                    lappend arcids($a) $olds
8107                    set arcend($a) $olds
8108                    unset growing($a)
8109                }
8110                lappend allchildren($olds) $id
8111                lappend arcnos($olds) $a
8112                continue
8113            }
8114        }
8115        foreach a $arcnos($id) {
8116            lappend arcids($a) $id
8117            set arcend($a) $id
8118            unset growing($a)
8119        }
8120
8121        set ao {}
8122        foreach p $olds {
8123            lappend allchildren($p) $id
8124            set a [incr nextarc]
8125            set arcstart($a) $id
8126            set archeads($a) {}
8127            set arctags($a) {}
8128            set archeads($a) {}
8129            set arcids($a) {}
8130            lappend ao $a
8131            set growing($a) 1
8132            if {[info exists allparents($p)]} {
8133                # seen it already, may need to make a new branch
8134                if {![info exists arcout($p)]} {
8135                    splitarc $p
8136                }
8137                lappend arcids($a) $p
8138                set arcend($a) $p
8139                unset growing($a)
8140            }
8141            lappend arcnos($p) $a
8142        }
8143        set arcout($id) $ao
8144    }
8145    if {$nid > 0} {
8146        global cached_dheads cached_dtags cached_atags
8147        catch {unset cached_dheads}
8148        catch {unset cached_dtags}
8149        catch {unset cached_atags}
8150    }
8151    if {![eof $fd]} {
8152        return [expr {$nid >= 1000? 2: 1}]
8153    }
8154    set cacheok 1
8155    if {[catch {
8156        fconfigure $fd -blocking 1
8157        close $fd
8158    } err]} {
8159        # got an error reading the list of commits
8160        # if we were updating, try rereading the whole thing again
8161        if {$allcupdate} {
8162            incr allcommits -1
8163            dropcache $err
8164            return
8165        }
8166        error_popup "[mc "Error reading commit topology information;\
8167                branch and preceding/following tag information\
8168                will be incomplete."]\n($err)"
8169        set cacheok 0
8170    }
8171    if {[incr allcommits -1] == 0} {
8172        notbusy allcommits
8173        if {$cacheok} {
8174            run savecache
8175        }
8176    }
8177    dispneartags 0
8178    return 0
8179}
8180
8181proc recalcarc {a} {
8182    global arctags archeads arcids idtags idheads
8183
8184    set at {}
8185    set ah {}
8186    foreach id [lrange $arcids($a) 0 end-1] {
8187        if {[info exists idtags($id)]} {
8188            lappend at $id
8189        }
8190        if {[info exists idheads($id)]} {
8191            lappend ah $id
8192        }
8193    }
8194    set arctags($a) $at
8195    set archeads($a) $ah
8196}
8197
8198proc splitarc {p} {
8199    global arcnos arcids nextarc arctags archeads idtags idheads
8200    global arcstart arcend arcout allparents growing
8201
8202    set a $arcnos($p)
8203    if {[llength $a] != 1} {
8204        puts "oops splitarc called but [llength $a] arcs already"
8205        return
8206    }
8207    set a [lindex $a 0]
8208    set i [lsearch -exact $arcids($a) $p]
8209    if {$i < 0} {
8210        puts "oops splitarc $p not in arc $a"
8211        return
8212    }
8213    set na [incr nextarc]
8214    if {[info exists arcend($a)]} {
8215        set arcend($na) $arcend($a)
8216    } else {
8217        set l [lindex $allparents([lindex $arcids($a) end]) 0]
8218        set j [lsearch -exact $arcnos($l) $a]
8219        set arcnos($l) [lreplace $arcnos($l) $j $j $na]
8220    }
8221    set tail [lrange $arcids($a) [expr {$i+1}] end]
8222    set arcids($a) [lrange $arcids($a) 0 $i]
8223    set arcend($a) $p
8224    set arcstart($na) $p
8225    set arcout($p) $na
8226    set arcids($na) $tail
8227    if {[info exists growing($a)]} {
8228        set growing($na) 1
8229        unset growing($a)
8230    }
8231
8232    foreach id $tail {
8233        if {[llength $arcnos($id)] == 1} {
8234            set arcnos($id) $na
8235        } else {
8236            set j [lsearch -exact $arcnos($id) $a]
8237            set arcnos($id) [lreplace $arcnos($id) $j $j $na]
8238        }
8239    }
8240
8241    # reconstruct tags and heads lists
8242    if {$arctags($a) ne {} || $archeads($a) ne {}} {
8243        recalcarc $a
8244        recalcarc $na
8245    } else {
8246        set arctags($na) {}
8247        set archeads($na) {}
8248    }
8249}
8250
8251# Update things for a new commit added that is a child of one
8252# existing commit.  Used when cherry-picking.
8253proc addnewchild {id p} {
8254    global allparents allchildren idtags nextarc
8255    global arcnos arcids arctags arcout arcend arcstart archeads growing
8256    global seeds allcommits
8257
8258    if {![info exists allcommits] || ![info exists arcnos($p)]} return
8259    set allparents($id) [list $p]
8260    set allchildren($id) {}
8261    set arcnos($id) {}
8262    lappend seeds $id
8263    lappend allchildren($p) $id
8264    set a [incr nextarc]
8265    set arcstart($a) $id
8266    set archeads($a) {}
8267    set arctags($a) {}
8268    set arcids($a) [list $p]
8269    set arcend($a) $p
8270    if {![info exists arcout($p)]} {
8271        splitarc $p
8272    }
8273    lappend arcnos($p) $a
8274    set arcout($id) [list $a]
8275}
8276
8277# This implements a cache for the topology information.
8278# The cache saves, for each arc, the start and end of the arc,
8279# the ids on the arc, and the outgoing arcs from the end.
8280proc readcache {f} {
8281    global arcnos arcids arcout arcstart arcend arctags archeads nextarc
8282    global idtags idheads allparents cachedarcs possible_seeds seeds growing
8283    global allcwait
8284
8285    set a $nextarc
8286    set lim $cachedarcs
8287    if {$lim - $a > 500} {
8288        set lim [expr {$a + 500}]
8289    }
8290    if {[catch {
8291        if {$a == $lim} {
8292            # finish reading the cache and setting up arctags, etc.
8293            set line [gets $f]
8294            if {$line ne "1"} {error "bad final version"}
8295            close $f
8296            foreach id [array names idtags] {
8297                if {[info exists arcnos($id)] && [llength $arcnos($id)] == 1 &&
8298                    [llength $allparents($id)] == 1} {
8299                    set a [lindex $arcnos($id) 0]
8300                    if {$arctags($a) eq {}} {
8301                        recalcarc $a
8302                    }
8303                }
8304            }
8305            foreach id [array names idheads] {
8306                if {[info exists arcnos($id)] && [llength $arcnos($id)] == 1 &&
8307                    [llength $allparents($id)] == 1} {
8308                    set a [lindex $arcnos($id) 0]
8309                    if {$archeads($a) eq {}} {
8310                        recalcarc $a
8311                    }
8312                }
8313            }
8314            foreach id [lsort -unique $possible_seeds] {
8315                if {$arcnos($id) eq {}} {
8316                    lappend seeds $id
8317                }
8318            }
8319            set allcwait 0
8320        } else {
8321            while {[incr a] <= $lim} {
8322                set line [gets $f]
8323                if {[llength $line] != 3} {error "bad line"}
8324                set s [lindex $line 0]
8325                set arcstart($a) $s
8326                lappend arcout($s) $a
8327                if {![info exists arcnos($s)]} {
8328                    lappend possible_seeds $s
8329                    set arcnos($s) {}
8330                }
8331                set e [lindex $line 1]
8332                if {$e eq {}} {
8333                    set growing($a) 1
8334                } else {
8335                    set arcend($a) $e
8336                    if {![info exists arcout($e)]} {
8337                        set arcout($e) {}
8338                    }
8339                }
8340                set arcids($a) [lindex $line 2]
8341                foreach id $arcids($a) {
8342                    lappend allparents($s) $id
8343                    set s $id
8344                    lappend arcnos($id) $a
8345                }
8346                if {![info exists allparents($s)]} {
8347                    set allparents($s) {}
8348                }
8349                set arctags($a) {}
8350                set archeads($a) {}
8351            }
8352            set nextarc [expr {$a - 1}]
8353        }
8354    } err]} {
8355        dropcache $err
8356        return 0
8357    }
8358    if {!$allcwait} {
8359        getallcommits
8360    }
8361    return $allcwait
8362}
8363
8364proc getcache {f} {
8365    global nextarc cachedarcs possible_seeds
8366
8367    if {[catch {
8368        set line [gets $f]
8369        if {[llength $line] != 2 || [lindex $line 0] ne "1"} {error "bad version"}
8370        # make sure it's an integer
8371        set cachedarcs [expr {int([lindex $line 1])}]
8372        if {$cachedarcs < 0} {error "bad number of arcs"}
8373        set nextarc 0
8374        set possible_seeds {}
8375        run readcache $f
8376    } err]} {
8377        dropcache $err
8378    }
8379    return 0
8380}
8381
8382proc dropcache {err} {
8383    global allcwait nextarc cachedarcs seeds
8384
8385    #puts "dropping cache ($err)"
8386    foreach v {arcnos arcout arcids arcstart arcend growing \
8387                   arctags archeads allparents allchildren} {
8388        global $v
8389        catch {unset $v}
8390    }
8391    set allcwait 0
8392    set nextarc 0
8393    set cachedarcs 0
8394    set seeds {}
8395    getallcommits
8396}
8397
8398proc writecache {f} {
8399    global cachearc cachedarcs allccache
8400    global arcstart arcend arcnos arcids arcout
8401
8402    set a $cachearc
8403    set lim $cachedarcs
8404    if {$lim - $a > 1000} {
8405        set lim [expr {$a + 1000}]
8406    }
8407    if {[catch {
8408        while {[incr a] <= $lim} {
8409            if {[info exists arcend($a)]} {
8410                puts $f [list $arcstart($a) $arcend($a) $arcids($a)]
8411            } else {
8412                puts $f [list $arcstart($a) {} $arcids($a)]
8413            }
8414        }
8415    } err]} {
8416        catch {close $f}
8417        catch {file delete $allccache}
8418        #puts "writing cache failed ($err)"
8419        return 0
8420    }
8421    set cachearc [expr {$a - 1}]
8422    if {$a > $cachedarcs} {
8423        puts $f "1"
8424        close $f
8425        return 0
8426    }
8427    return 1
8428}
8429
8430proc savecache {} {
8431    global nextarc cachedarcs cachearc allccache
8432
8433    if {$nextarc == $cachedarcs} return
8434    set cachearc 0
8435    set cachedarcs $nextarc
8436    catch {
8437        set f [open $allccache w]
8438        puts $f [list 1 $cachedarcs]
8439        run writecache $f
8440    }
8441}
8442
8443# Returns 1 if a is an ancestor of b, -1 if b is an ancestor of a,
8444# or 0 if neither is true.
8445proc anc_or_desc {a b} {
8446    global arcout arcstart arcend arcnos cached_isanc
8447
8448    if {$arcnos($a) eq $arcnos($b)} {
8449        # Both are on the same arc(s); either both are the same BMP,
8450        # or if one is not a BMP, the other is also not a BMP or is
8451        # the BMP at end of the arc (and it only has 1 incoming arc).
8452        # Or both can be BMPs with no incoming arcs.
8453        if {$a eq $b || $arcnos($a) eq {}} {
8454            return 0
8455        }
8456        # assert {[llength $arcnos($a)] == 1}
8457        set arc [lindex $arcnos($a) 0]
8458        set i [lsearch -exact $arcids($arc) $a]
8459        set j [lsearch -exact $arcids($arc) $b]
8460        if {$i < 0 || $i > $j} {
8461            return 1
8462        } else {
8463            return -1
8464        }
8465    }
8466
8467    if {![info exists arcout($a)]} {
8468        set arc [lindex $arcnos($a) 0]
8469        if {[info exists arcend($arc)]} {
8470            set aend $arcend($arc)
8471        } else {
8472            set aend {}
8473        }
8474        set a $arcstart($arc)
8475    } else {
8476        set aend $a
8477    }
8478    if {![info exists arcout($b)]} {
8479        set arc [lindex $arcnos($b) 0]
8480        if {[info exists arcend($arc)]} {
8481            set bend $arcend($arc)
8482        } else {
8483            set bend {}
8484        }
8485        set b $arcstart($arc)
8486    } else {
8487        set bend $b
8488    }
8489    if {$a eq $bend} {
8490        return 1
8491    }
8492    if {$b eq $aend} {
8493        return -1
8494    }
8495    if {[info exists cached_isanc($a,$bend)]} {
8496        if {$cached_isanc($a,$bend)} {
8497            return 1
8498        }
8499    }
8500    if {[info exists cached_isanc($b,$aend)]} {
8501        if {$cached_isanc($b,$aend)} {
8502            return -1
8503        }
8504        if {[info exists cached_isanc($a,$bend)]} {
8505            return 0
8506        }
8507    }
8508
8509    set todo [list $a $b]
8510    set anc($a) a
8511    set anc($b) b
8512    for {set i 0} {$i < [llength $todo]} {incr i} {
8513        set x [lindex $todo $i]
8514        if {$anc($x) eq {}} {
8515            continue
8516        }
8517        foreach arc $arcnos($x) {
8518            set xd $arcstart($arc)
8519            if {$xd eq $bend} {
8520                set cached_isanc($a,$bend) 1
8521                set cached_isanc($b,$aend) 0
8522                return 1
8523            } elseif {$xd eq $aend} {
8524                set cached_isanc($b,$aend) 1
8525                set cached_isanc($a,$bend) 0
8526                return -1
8527            }
8528            if {![info exists anc($xd)]} {
8529                set anc($xd) $anc($x)
8530                lappend todo $xd
8531            } elseif {$anc($xd) ne $anc($x)} {
8532                set anc($xd) {}
8533            }
8534        }
8535    }
8536    set cached_isanc($a,$bend) 0
8537    set cached_isanc($b,$aend) 0
8538    return 0
8539}
8540
8541# This identifies whether $desc has an ancestor that is
8542# a growing tip of the graph and which is not an ancestor of $anc
8543# and returns 0 if so and 1 if not.
8544# If we subsequently discover a tag on such a growing tip, and that
8545# turns out to be a descendent of $anc (which it could, since we
8546# don't necessarily see children before parents), then $desc
8547# isn't a good choice to display as a descendent tag of
8548# $anc (since it is the descendent of another tag which is
8549# a descendent of $anc).  Similarly, $anc isn't a good choice to
8550# display as a ancestor tag of $desc.
8551#
8552proc is_certain {desc anc} {
8553    global arcnos arcout arcstart arcend growing problems
8554
8555    set certain {}
8556    if {[llength $arcnos($anc)] == 1} {
8557        # tags on the same arc are certain
8558        if {$arcnos($desc) eq $arcnos($anc)} {
8559            return 1
8560        }
8561        if {![info exists arcout($anc)]} {
8562            # if $anc is partway along an arc, use the start of the arc instead
8563            set a [lindex $arcnos($anc) 0]
8564            set anc $arcstart($a)
8565        }
8566    }
8567    if {[llength $arcnos($desc)] > 1 || [info exists arcout($desc)]} {
8568        set x $desc
8569    } else {
8570        set a [lindex $arcnos($desc) 0]
8571        set x $arcend($a)
8572    }
8573    if {$x == $anc} {
8574        return 1
8575    }
8576    set anclist [list $x]
8577    set dl($x) 1
8578    set nnh 1
8579    set ngrowanc 0
8580    for {set i 0} {$i < [llength $anclist] && ($nnh > 0 || $ngrowanc > 0)} {incr i} {
8581        set x [lindex $anclist $i]
8582        if {$dl($x)} {
8583            incr nnh -1
8584        }
8585        set done($x) 1
8586        foreach a $arcout($x) {
8587            if {[info exists growing($a)]} {
8588                if {![info exists growanc($x)] && $dl($x)} {
8589                    set growanc($x) 1
8590                    incr ngrowanc
8591                }
8592            } else {
8593                set y $arcend($a)
8594                if {[info exists dl($y)]} {
8595                    if {$dl($y)} {
8596                        if {!$dl($x)} {
8597                            set dl($y) 0
8598                            if {![info exists done($y)]} {
8599                                incr nnh -1
8600                            }
8601                            if {[info exists growanc($x)]} {
8602                                incr ngrowanc -1
8603                            }
8604                            set xl [list $y]
8605                            for {set k 0} {$k < [llength $xl]} {incr k} {
8606                                set z [lindex $xl $k]
8607                                foreach c $arcout($z) {
8608                                    if {[info exists arcend($c)]} {
8609                                        set v $arcend($c)
8610                                        if {[info exists dl($v)] && $dl($v)} {
8611                                            set dl($v) 0
8612                                            if {![info exists done($v)]} {
8613                                                incr nnh -1
8614                                            }
8615                                            if {[info exists growanc($v)]} {
8616                                                incr ngrowanc -1
8617                                            }
8618                                            lappend xl $v
8619                                        }
8620                                    }
8621                                }
8622                            }
8623                        }
8624                    }
8625                } elseif {$y eq $anc || !$dl($x)} {
8626                    set dl($y) 0
8627                    lappend anclist $y
8628                } else {
8629                    set dl($y) 1
8630                    lappend anclist $y
8631                    incr nnh
8632                }
8633            }
8634        }
8635    }
8636    foreach x [array names growanc] {
8637        if {$dl($x)} {
8638            return 0
8639        }
8640        return 0
8641    }
8642    return 1
8643}
8644
8645proc validate_arctags {a} {
8646    global arctags idtags
8647
8648    set i -1
8649    set na $arctags($a)
8650    foreach id $arctags($a) {
8651        incr i
8652        if {![info exists idtags($id)]} {
8653            set na [lreplace $na $i $i]
8654            incr i -1
8655        }
8656    }
8657    set arctags($a) $na
8658}
8659
8660proc validate_archeads {a} {
8661    global archeads idheads
8662
8663    set i -1
8664    set na $archeads($a)
8665    foreach id $archeads($a) {
8666        incr i
8667        if {![info exists idheads($id)]} {
8668            set na [lreplace $na $i $i]
8669            incr i -1
8670        }
8671    }
8672    set archeads($a) $na
8673}
8674
8675# Return the list of IDs that have tags that are descendents of id,
8676# ignoring IDs that are descendents of IDs already reported.
8677proc desctags {id} {
8678    global arcnos arcstart arcids arctags idtags allparents
8679    global growing cached_dtags
8680
8681    if {![info exists allparents($id)]} {
8682        return {}
8683    }
8684    set t1 [clock clicks -milliseconds]
8685    set argid $id
8686    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
8687        # part-way along an arc; check that arc first
8688        set a [lindex $arcnos($id) 0]
8689        if {$arctags($a) ne {}} {
8690            validate_arctags $a
8691            set i [lsearch -exact $arcids($a) $id]
8692            set tid {}
8693            foreach t $arctags($a) {
8694                set j [lsearch -exact $arcids($a) $t]
8695                if {$j >= $i} break
8696                set tid $t
8697            }
8698            if {$tid ne {}} {
8699                return $tid
8700            }
8701        }
8702        set id $arcstart($a)
8703        if {[info exists idtags($id)]} {
8704            return $id
8705        }
8706    }
8707    if {[info exists cached_dtags($id)]} {
8708        return $cached_dtags($id)
8709    }
8710
8711    set origid $id
8712    set todo [list $id]
8713    set queued($id) 1
8714    set nc 1
8715    for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
8716        set id [lindex $todo $i]
8717        set done($id) 1
8718        set ta [info exists hastaggedancestor($id)]
8719        if {!$ta} {
8720            incr nc -1
8721        }
8722        # ignore tags on starting node
8723        if {!$ta && $i > 0} {
8724            if {[info exists idtags($id)]} {
8725                set tagloc($id) $id
8726                set ta 1
8727            } elseif {[info exists cached_dtags($id)]} {
8728                set tagloc($id) $cached_dtags($id)
8729                set ta 1
8730            }
8731        }
8732        foreach a $arcnos($id) {
8733            set d $arcstart($a)
8734            if {!$ta && $arctags($a) ne {}} {
8735                validate_arctags $a
8736                if {$arctags($a) ne {}} {
8737                    lappend tagloc($id) [lindex $arctags($a) end]
8738                }
8739            }
8740            if {$ta || $arctags($a) ne {}} {
8741                set tomark [list $d]
8742                for {set j 0} {$j < [llength $tomark]} {incr j} {
8743                    set dd [lindex $tomark $j]
8744                    if {![info exists hastaggedancestor($dd)]} {
8745                        if {[info exists done($dd)]} {
8746                            foreach b $arcnos($dd) {
8747                                lappend tomark $arcstart($b)
8748                            }
8749                            if {[info exists tagloc($dd)]} {
8750                                unset tagloc($dd)
8751                            }
8752                        } elseif {[info exists queued($dd)]} {
8753                            incr nc -1
8754                        }
8755                        set hastaggedancestor($dd) 1
8756                    }
8757                }
8758            }
8759            if {![info exists queued($d)]} {
8760                lappend todo $d
8761                set queued($d) 1
8762                if {![info exists hastaggedancestor($d)]} {
8763                    incr nc
8764                }
8765            }
8766        }
8767    }
8768    set tags {}
8769    foreach id [array names tagloc] {
8770        if {![info exists hastaggedancestor($id)]} {
8771            foreach t $tagloc($id) {
8772                if {[lsearch -exact $tags $t] < 0} {
8773                    lappend tags $t
8774                }
8775            }
8776        }
8777    }
8778    set t2 [clock clicks -milliseconds]
8779    set loopix $i
8780
8781    # remove tags that are descendents of other tags
8782    for {set i 0} {$i < [llength $tags]} {incr i} {
8783        set a [lindex $tags $i]
8784        for {set j 0} {$j < $i} {incr j} {
8785            set b [lindex $tags $j]
8786            set r [anc_or_desc $a $b]
8787            if {$r == 1} {
8788                set tags [lreplace $tags $j $j]
8789                incr j -1
8790                incr i -1
8791            } elseif {$r == -1} {
8792                set tags [lreplace $tags $i $i]
8793                incr i -1
8794                break
8795            }
8796        }
8797    }
8798
8799    if {[array names growing] ne {}} {
8800        # graph isn't finished, need to check if any tag could get
8801        # eclipsed by another tag coming later.  Simply ignore any
8802        # tags that could later get eclipsed.
8803        set ctags {}
8804        foreach t $tags {
8805            if {[is_certain $t $origid]} {
8806                lappend ctags $t
8807            }
8808        }
8809        if {$tags eq $ctags} {
8810            set cached_dtags($origid) $tags
8811        } else {
8812            set tags $ctags
8813        }
8814    } else {
8815        set cached_dtags($origid) $tags
8816    }
8817    set t3 [clock clicks -milliseconds]
8818    if {0 && $t3 - $t1 >= 100} {
8819        puts "iterating descendents ($loopix/[llength $todo] nodes) took\
8820            [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
8821    }
8822    return $tags
8823}
8824
8825proc anctags {id} {
8826    global arcnos arcids arcout arcend arctags idtags allparents
8827    global growing cached_atags
8828
8829    if {![info exists allparents($id)]} {
8830        return {}
8831    }
8832    set t1 [clock clicks -milliseconds]
8833    set argid $id
8834    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
8835        # part-way along an arc; check that arc first
8836        set a [lindex $arcnos($id) 0]
8837        if {$arctags($a) ne {}} {
8838            validate_arctags $a
8839            set i [lsearch -exact $arcids($a) $id]
8840            foreach t $arctags($a) {
8841                set j [lsearch -exact $arcids($a) $t]
8842                if {$j > $i} {
8843                    return $t
8844                }
8845            }
8846        }
8847        if {![info exists arcend($a)]} {
8848            return {}
8849        }
8850        set id $arcend($a)
8851        if {[info exists idtags($id)]} {
8852            return $id
8853        }
8854    }
8855    if {[info exists cached_atags($id)]} {
8856        return $cached_atags($id)
8857    }
8858
8859    set origid $id
8860    set todo [list $id]
8861    set queued($id) 1
8862    set taglist {}
8863    set nc 1
8864    for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
8865        set id [lindex $todo $i]
8866        set done($id) 1
8867        set td [info exists hastaggeddescendent($id)]
8868        if {!$td} {
8869            incr nc -1
8870        }
8871        # ignore tags on starting node
8872        if {!$td && $i > 0} {
8873            if {[info exists idtags($id)]} {
8874                set tagloc($id) $id
8875                set td 1
8876            } elseif {[info exists cached_atags($id)]} {
8877                set tagloc($id) $cached_atags($id)
8878                set td 1
8879            }
8880        }
8881        foreach a $arcout($id) {
8882            if {!$td && $arctags($a) ne {}} {
8883                validate_arctags $a
8884                if {$arctags($a) ne {}} {
8885                    lappend tagloc($id) [lindex $arctags($a) 0]
8886                }
8887            }
8888            if {![info exists arcend($a)]} continue
8889            set d $arcend($a)
8890            if {$td || $arctags($a) ne {}} {
8891                set tomark [list $d]
8892                for {set j 0} {$j < [llength $tomark]} {incr j} {
8893                    set dd [lindex $tomark $j]
8894                    if {![info exists hastaggeddescendent($dd)]} {
8895                        if {[info exists done($dd)]} {
8896                            foreach b $arcout($dd) {
8897                                if {[info exists arcend($b)]} {
8898                                    lappend tomark $arcend($b)
8899                                }
8900                            }
8901                            if {[info exists tagloc($dd)]} {
8902                                unset tagloc($dd)
8903                            }
8904                        } elseif {[info exists queued($dd)]} {
8905                            incr nc -1
8906                        }
8907                        set hastaggeddescendent($dd) 1
8908                    }
8909                }
8910            }
8911            if {![info exists queued($d)]} {
8912                lappend todo $d
8913                set queued($d) 1
8914                if {![info exists hastaggeddescendent($d)]} {
8915                    incr nc
8916                }
8917            }
8918        }
8919    }
8920    set t2 [clock clicks -milliseconds]
8921    set loopix $i
8922    set tags {}
8923    foreach id [array names tagloc] {
8924        if {![info exists hastaggeddescendent($id)]} {
8925            foreach t $tagloc($id) {
8926                if {[lsearch -exact $tags $t] < 0} {
8927                    lappend tags $t
8928                }
8929            }
8930        }
8931    }
8932
8933    # remove tags that are ancestors of other tags
8934    for {set i 0} {$i < [llength $tags]} {incr i} {
8935        set a [lindex $tags $i]
8936        for {set j 0} {$j < $i} {incr j} {
8937            set b [lindex $tags $j]
8938            set r [anc_or_desc $a $b]
8939            if {$r == -1} {
8940                set tags [lreplace $tags $j $j]
8941                incr j -1
8942                incr i -1
8943            } elseif {$r == 1} {
8944                set tags [lreplace $tags $i $i]
8945                incr i -1
8946                break
8947            }
8948        }
8949    }
8950
8951    if {[array names growing] ne {}} {
8952        # graph isn't finished, need to check if any tag could get
8953        # eclipsed by another tag coming later.  Simply ignore any
8954        # tags that could later get eclipsed.
8955        set ctags {}
8956        foreach t $tags {
8957            if {[is_certain $origid $t]} {
8958                lappend ctags $t
8959            }
8960        }
8961        if {$tags eq $ctags} {
8962            set cached_atags($origid) $tags
8963        } else {
8964            set tags $ctags
8965        }
8966    } else {
8967        set cached_atags($origid) $tags
8968    }
8969    set t3 [clock clicks -milliseconds]
8970    if {0 && $t3 - $t1 >= 100} {
8971        puts "iterating ancestors ($loopix/[llength $todo] nodes) took\
8972            [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
8973    }
8974    return $tags
8975}
8976
8977# Return the list of IDs that have heads that are descendents of id,
8978# including id itself if it has a head.
8979proc descheads {id} {
8980    global arcnos arcstart arcids archeads idheads cached_dheads
8981    global allparents
8982
8983    if {![info exists allparents($id)]} {
8984        return {}
8985    }
8986    set aret {}
8987    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
8988        # part-way along an arc; check it first
8989        set a [lindex $arcnos($id) 0]
8990        if {$archeads($a) ne {}} {
8991            validate_archeads $a
8992            set i [lsearch -exact $arcids($a) $id]
8993            foreach t $archeads($a) {
8994                set j [lsearch -exact $arcids($a) $t]
8995                if {$j > $i} break
8996                lappend aret $t
8997            }
8998        }
8999        set id $arcstart($a)
9000    }
9001    set origid $id
9002    set todo [list $id]
9003    set seen($id) 1
9004    set ret {}
9005    for {set i 0} {$i < [llength $todo]} {incr i} {
9006        set id [lindex $todo $i]
9007        if {[info exists cached_dheads($id)]} {
9008            set ret [concat $ret $cached_dheads($id)]
9009        } else {
9010            if {[info exists idheads($id)]} {
9011                lappend ret $id
9012            }
9013            foreach a $arcnos($id) {
9014                if {$archeads($a) ne {}} {
9015                    validate_archeads $a
9016                    if {$archeads($a) ne {}} {
9017                        set ret [concat $ret $archeads($a)]
9018                    }
9019                }
9020                set d $arcstart($a)
9021                if {![info exists seen($d)]} {
9022                    lappend todo $d
9023                    set seen($d) 1
9024                }
9025            }
9026        }
9027    }
9028    set ret [lsort -unique $ret]
9029    set cached_dheads($origid) $ret
9030    return [concat $ret $aret]
9031}
9032
9033proc addedtag {id} {
9034    global arcnos arcout cached_dtags cached_atags
9035
9036    if {![info exists arcnos($id)]} return
9037    if {![info exists arcout($id)]} {
9038        recalcarc [lindex $arcnos($id) 0]
9039    }
9040    catch {unset cached_dtags}
9041    catch {unset cached_atags}
9042}
9043
9044proc addedhead {hid head} {
9045    global arcnos arcout cached_dheads
9046
9047    if {![info exists arcnos($hid)]} return
9048    if {![info exists arcout($hid)]} {
9049        recalcarc [lindex $arcnos($hid) 0]
9050    }
9051    catch {unset cached_dheads}
9052}
9053
9054proc removedhead {hid head} {
9055    global cached_dheads
9056
9057    catch {unset cached_dheads}
9058}
9059
9060proc movedhead {hid head} {
9061    global arcnos arcout cached_dheads
9062
9063    if {![info exists arcnos($hid)]} return
9064    if {![info exists arcout($hid)]} {
9065        recalcarc [lindex $arcnos($hid) 0]
9066    }
9067    catch {unset cached_dheads}
9068}
9069
9070proc changedrefs {} {
9071    global cached_dheads cached_dtags cached_atags
9072    global arctags archeads arcnos arcout idheads idtags
9073
9074    foreach id [concat [array names idheads] [array names idtags]] {
9075        if {[info exists arcnos($id)] && ![info exists arcout($id)]} {
9076            set a [lindex $arcnos($id) 0]
9077            if {![info exists donearc($a)]} {
9078                recalcarc $a
9079                set donearc($a) 1
9080            }
9081        }
9082    }
9083    catch {unset cached_dtags}
9084    catch {unset cached_atags}
9085    catch {unset cached_dheads}
9086}
9087
9088proc rereadrefs {} {
9089    global idtags idheads idotherrefs mainheadid
9090
9091    set refids [concat [array names idtags] \
9092                    [array names idheads] [array names idotherrefs]]
9093    foreach id $refids {
9094        if {![info exists ref($id)]} {
9095            set ref($id) [listrefs $id]
9096        }
9097    }
9098    set oldmainhead $mainheadid
9099    readrefs
9100    changedrefs
9101    set refids [lsort -unique [concat $refids [array names idtags] \
9102                        [array names idheads] [array names idotherrefs]]]
9103    foreach id $refids {
9104        set v [listrefs $id]
9105        if {![info exists ref($id)] || $ref($id) != $v} {
9106            redrawtags $id
9107        }
9108    }
9109    if {$oldmainhead ne $mainheadid} {
9110        redrawtags $oldmainhead
9111        redrawtags $mainheadid
9112    }
9113    run refill_reflist
9114}
9115
9116proc listrefs {id} {
9117    global idtags idheads idotherrefs
9118
9119    set x {}
9120    if {[info exists idtags($id)]} {
9121        set x $idtags($id)
9122    }
9123    set y {}
9124    if {[info exists idheads($id)]} {
9125        set y $idheads($id)
9126    }
9127    set z {}
9128    if {[info exists idotherrefs($id)]} {
9129        set z $idotherrefs($id)
9130    }
9131    return [list $x $y $z]
9132}
9133
9134proc showtag {tag isnew} {
9135    global ctext tagcontents tagids linknum tagobjid
9136
9137    if {$isnew} {
9138        addtohistory [list showtag $tag 0]
9139    }
9140    $ctext conf -state normal
9141    clear_ctext
9142    settabs 0
9143    set linknum 0
9144    if {![info exists tagcontents($tag)]} {
9145        catch {
9146            set tagcontents($tag) [exec git cat-file tag $tagobjid($tag)]
9147        }
9148    }
9149    if {[info exists tagcontents($tag)]} {
9150        set text $tagcontents($tag)
9151    } else {
9152        set text "[mc "Tag"]: $tag\n[mc "Id"]:  $tagids($tag)"
9153    }
9154    appendwithlinks $text {}
9155    $ctext conf -state disabled
9156    init_flist {}
9157}
9158
9159proc doquit {} {
9160    global stopped
9161    global gitktmpdir
9162
9163    set stopped 100
9164    savestuff .
9165    destroy .
9166
9167    if {[info exists gitktmpdir]} {
9168        catch {file delete -force $gitktmpdir}
9169    }
9170}
9171
9172proc mkfontdisp {font top which} {
9173    global fontattr fontpref $font
9174
9175    set fontpref($font) [set $font]
9176    button $top.${font}but -text $which -font optionfont \
9177        -command [list choosefont $font $which]
9178    label $top.$font -relief flat -font $font \
9179        -text $fontattr($font,family) -justify left
9180    grid x $top.${font}but $top.$font -sticky w
9181}
9182
9183proc choosefont {font which} {
9184    global fontparam fontlist fonttop fontattr
9185
9186    set fontparam(which) $which
9187    set fontparam(font) $font
9188    set fontparam(family) [font actual $font -family]
9189    set fontparam(size) $fontattr($font,size)
9190    set fontparam(weight) $fontattr($font,weight)
9191    set fontparam(slant) $fontattr($font,slant)
9192    set top .gitkfont
9193    set fonttop $top
9194    if {![winfo exists $top]} {
9195        font create sample
9196        eval font config sample [font actual $font]
9197        toplevel $top
9198        wm title $top [mc "Gitk font chooser"]
9199        label $top.l -textvariable fontparam(which)
9200        pack $top.l -side top
9201        set fontlist [lsort [font families]]
9202        frame $top.f
9203        listbox $top.f.fam -listvariable fontlist \
9204            -yscrollcommand [list $top.f.sb set]
9205        bind $top.f.fam <<ListboxSelect>> selfontfam
9206        scrollbar $top.f.sb -command [list $top.f.fam yview]
9207        pack $top.f.sb -side right -fill y
9208        pack $top.f.fam -side left -fill both -expand 1
9209        pack $top.f -side top -fill both -expand 1
9210        frame $top.g
9211        spinbox $top.g.size -from 4 -to 40 -width 4 \
9212            -textvariable fontparam(size) \
9213            -validatecommand {string is integer -strict %s}
9214        checkbutton $top.g.bold -padx 5 \
9215            -font {{Times New Roman} 12 bold} -text [mc "B"] -indicatoron 0 \
9216            -variable fontparam(weight) -onvalue bold -offvalue normal
9217        checkbutton $top.g.ital -padx 5 \
9218            -font {{Times New Roman} 12 italic} -text [mc "I"] -indicatoron 0  \
9219            -variable fontparam(slant) -onvalue italic -offvalue roman
9220        pack $top.g.size $top.g.bold $top.g.ital -side left
9221        pack $top.g -side top
9222        canvas $top.c -width 150 -height 50 -border 2 -relief sunk \
9223            -background white
9224        $top.c create text 100 25 -anchor center -text $which -font sample \
9225            -fill black -tags text
9226        bind $top.c <Configure> [list centertext $top.c]
9227        pack $top.c -side top -fill x
9228        frame $top.buts
9229        button $top.buts.ok -text [mc "OK"] -command fontok -default active
9230        button $top.buts.can -text [mc "Cancel"] -command fontcan -default normal
9231        grid $top.buts.ok $top.buts.can
9232        grid columnconfigure $top.buts 0 -weight 1 -uniform a
9233        grid columnconfigure $top.buts 1 -weight 1 -uniform a
9234        pack $top.buts -side bottom -fill x
9235        trace add variable fontparam write chg_fontparam
9236    } else {
9237        raise $top
9238        $top.c itemconf text -text $which
9239    }
9240    set i [lsearch -exact $fontlist $fontparam(family)]
9241    if {$i >= 0} {
9242        $top.f.fam selection set $i
9243        $top.f.fam see $i
9244    }
9245}
9246
9247proc centertext {w} {
9248    $w coords text [expr {[winfo width $w] / 2}] [expr {[winfo height $w] / 2}]
9249}
9250
9251proc fontok {} {
9252    global fontparam fontpref prefstop
9253
9254    set f $fontparam(font)
9255    set fontpref($f) [list $fontparam(family) $fontparam(size)]
9256    if {$fontparam(weight) eq "bold"} {
9257        lappend fontpref($f) "bold"
9258    }
9259    if {$fontparam(slant) eq "italic"} {
9260        lappend fontpref($f) "italic"
9261    }
9262    set w $prefstop.$f
9263    $w conf -text $fontparam(family) -font $fontpref($f)
9264        
9265    fontcan
9266}
9267
9268proc fontcan {} {
9269    global fonttop fontparam
9270
9271    if {[info exists fonttop]} {
9272        catch {destroy $fonttop}
9273        catch {font delete sample}
9274        unset fonttop
9275        unset fontparam
9276    }
9277}
9278
9279proc selfontfam {} {
9280    global fonttop fontparam
9281
9282    set i [$fonttop.f.fam curselection]
9283    if {$i ne {}} {
9284        set fontparam(family) [$fonttop.f.fam get $i]
9285    }
9286}
9287
9288proc chg_fontparam {v sub op} {
9289    global fontparam
9290
9291    font config sample -$sub $fontparam($sub)
9292}
9293
9294proc doprefs {} {
9295    global maxwidth maxgraphpct
9296    global oldprefs prefstop showneartags showlocalchanges
9297    global bgcolor fgcolor ctext diffcolors selectbgcolor
9298    global tabstop limitdiffs autoselect extdifftool
9299
9300    set top .gitkprefs
9301    set prefstop $top
9302    if {[winfo exists $top]} {
9303        raise $top
9304        return
9305    }
9306    foreach v {maxwidth maxgraphpct showneartags showlocalchanges \
9307                   limitdiffs tabstop} {
9308        set oldprefs($v) [set $v]
9309    }
9310    toplevel $top
9311    wm title $top [mc "Gitk preferences"]
9312    label $top.ldisp -text [mc "Commit list display options"]
9313    grid $top.ldisp - -sticky w -pady 10
9314    label $top.spacer -text " "
9315    label $top.maxwidthl -text [mc "Maximum graph width (lines)"] \
9316        -font optionfont
9317    spinbox $top.maxwidth -from 0 -to 100 -width 4 -textvariable maxwidth
9318    grid $top.spacer $top.maxwidthl $top.maxwidth -sticky w
9319    label $top.maxpctl -text [mc "Maximum graph width (% of pane)"] \
9320        -font optionfont
9321    spinbox $top.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct
9322    grid x $top.maxpctl $top.maxpct -sticky w
9323    frame $top.showlocal
9324    label $top.showlocal.l -text [mc "Show local changes"] -font optionfont
9325    checkbutton $top.showlocal.b -variable showlocalchanges
9326    pack $top.showlocal.b $top.showlocal.l -side left
9327    grid x $top.showlocal -sticky w
9328    frame $top.autoselect
9329    label $top.autoselect.l -text [mc "Auto-select SHA1"] -font optionfont
9330    checkbutton $top.autoselect.b -variable autoselect
9331    pack $top.autoselect.b $top.autoselect.l -side left
9332    grid x $top.autoselect -sticky w
9333
9334    label $top.ddisp -text [mc "Diff display options"]
9335    grid $top.ddisp - -sticky w -pady 10
9336    label $top.tabstopl -text [mc "Tab spacing"] -font optionfont
9337    spinbox $top.tabstop -from 1 -to 20 -width 4 -textvariable tabstop
9338    grid x $top.tabstopl $top.tabstop -sticky w
9339    frame $top.ntag
9340    label $top.ntag.l -text [mc "Display nearby tags"] -font optionfont
9341    checkbutton $top.ntag.b -variable showneartags
9342    pack $top.ntag.b $top.ntag.l -side left
9343    grid x $top.ntag -sticky w
9344    frame $top.ldiff
9345    label $top.ldiff.l -text [mc "Limit diffs to listed paths"] -font optionfont
9346    checkbutton $top.ldiff.b -variable limitdiffs
9347    pack $top.ldiff.b $top.ldiff.l -side left
9348    grid x $top.ldiff -sticky w
9349
9350    entry $top.extdifft -textvariable extdifftool
9351    frame $top.extdifff
9352    label $top.extdifff.l -text [mc "External diff tool" ] -font optionfont \
9353        -padx 10
9354    button $top.extdifff.b -text [mc "Choose..."] -font optionfont \
9355        -command choose_extdiff
9356    pack $top.extdifff.l $top.extdifff.b -side left
9357    grid x $top.extdifff $top.extdifft -sticky w
9358
9359    label $top.cdisp -text [mc "Colors: press to choose"]
9360    grid $top.cdisp - -sticky w -pady 10
9361    label $top.bg -padx 40 -relief sunk -background $bgcolor
9362    button $top.bgbut -text [mc "Background"] -font optionfont \
9363        -command [list choosecolor bgcolor {} $top.bg background setbg]
9364    grid x $top.bgbut $top.bg -sticky w
9365    label $top.fg -padx 40 -relief sunk -background $fgcolor
9366    button $top.fgbut -text [mc "Foreground"] -font optionfont \
9367        -command [list choosecolor fgcolor {} $top.fg foreground setfg]
9368    grid x $top.fgbut $top.fg -sticky w
9369    label $top.diffold -padx 40 -relief sunk -background [lindex $diffcolors 0]
9370    button $top.diffoldbut -text [mc "Diff: old lines"] -font optionfont \
9371        -command [list choosecolor diffcolors 0 $top.diffold "diff old lines" \
9372                      [list $ctext tag conf d0 -foreground]]
9373    grid x $top.diffoldbut $top.diffold -sticky w
9374    label $top.diffnew -padx 40 -relief sunk -background [lindex $diffcolors 1]
9375    button $top.diffnewbut -text [mc "Diff: new lines"] -font optionfont \
9376        -command [list choosecolor diffcolors 1 $top.diffnew "diff new lines" \
9377                      [list $ctext tag conf d1 -foreground]]
9378    grid x $top.diffnewbut $top.diffnew -sticky w
9379    label $top.hunksep -padx 40 -relief sunk -background [lindex $diffcolors 2]
9380    button $top.hunksepbut -text [mc "Diff: hunk header"] -font optionfont \
9381        -command [list choosecolor diffcolors 2 $top.hunksep \
9382                      "diff hunk header" \
9383                      [list $ctext tag conf hunksep -foreground]]
9384    grid x $top.hunksepbut $top.hunksep -sticky w
9385    label $top.selbgsep -padx 40 -relief sunk -background $selectbgcolor
9386    button $top.selbgbut -text [mc "Select bg"] -font optionfont \
9387        -command [list choosecolor selectbgcolor {} $top.selbgsep background setselbg]
9388    grid x $top.selbgbut $top.selbgsep -sticky w
9389
9390    label $top.cfont -text [mc "Fonts: press to choose"]
9391    grid $top.cfont - -sticky w -pady 10
9392    mkfontdisp mainfont $top [mc "Main font"]
9393    mkfontdisp textfont $top [mc "Diff display font"]
9394    mkfontdisp uifont $top [mc "User interface font"]
9395
9396    frame $top.buts
9397    button $top.buts.ok -text [mc "OK"] -command prefsok -default active
9398    button $top.buts.can -text [mc "Cancel"] -command prefscan -default normal
9399    grid $top.buts.ok $top.buts.can
9400    grid columnconfigure $top.buts 0 -weight 1 -uniform a
9401    grid columnconfigure $top.buts 1 -weight 1 -uniform a
9402    grid $top.buts - - -pady 10 -sticky ew
9403    bind $top <Visibility> "focus $top.buts.ok"
9404}
9405
9406proc choose_extdiff {} {
9407    global extdifftool
9408
9409    set prog [tk_getOpenFile -title "External diff tool" -multiple false]
9410    if {$prog ne {}} {
9411        set extdifftool $prog
9412    }
9413}
9414
9415proc choosecolor {v vi w x cmd} {
9416    global $v
9417
9418    set c [tk_chooseColor -initialcolor [lindex [set $v] $vi] \
9419               -title [mc "Gitk: choose color for %s" $x]]
9420    if {$c eq {}} return
9421    $w conf -background $c
9422    lset $v $vi $c
9423    eval $cmd $c
9424}
9425
9426proc setselbg {c} {
9427    global bglist cflist
9428    foreach w $bglist {
9429        $w configure -selectbackground $c
9430    }
9431    $cflist tag configure highlight \
9432        -background [$cflist cget -selectbackground]
9433    allcanvs itemconf secsel -fill $c
9434}
9435
9436proc setbg {c} {
9437    global bglist
9438
9439    foreach w $bglist {
9440        $w conf -background $c
9441    }
9442}
9443
9444proc setfg {c} {
9445    global fglist canv
9446
9447    foreach w $fglist {
9448        $w conf -foreground $c
9449    }
9450    allcanvs itemconf text -fill $c
9451    $canv itemconf circle -outline $c
9452}
9453
9454proc prefscan {} {
9455    global oldprefs prefstop
9456
9457    foreach v {maxwidth maxgraphpct showneartags showlocalchanges \
9458                   limitdiffs tabstop} {
9459        global $v
9460        set $v $oldprefs($v)
9461    }
9462    catch {destroy $prefstop}
9463    unset prefstop
9464    fontcan
9465}
9466
9467proc prefsok {} {
9468    global maxwidth maxgraphpct
9469    global oldprefs prefstop showneartags showlocalchanges
9470    global fontpref mainfont textfont uifont
9471    global limitdiffs treediffs
9472
9473    catch {destroy $prefstop}
9474    unset prefstop
9475    fontcan
9476    set fontchanged 0
9477    if {$mainfont ne $fontpref(mainfont)} {
9478        set mainfont $fontpref(mainfont)
9479        parsefont mainfont $mainfont
9480        eval font configure mainfont [fontflags mainfont]
9481        eval font configure mainfontbold [fontflags mainfont 1]
9482        setcoords
9483        set fontchanged 1
9484    }
9485    if {$textfont ne $fontpref(textfont)} {
9486        set textfont $fontpref(textfont)
9487        parsefont textfont $textfont
9488        eval font configure textfont [fontflags textfont]
9489        eval font configure textfontbold [fontflags textfont 1]
9490    }
9491    if {$uifont ne $fontpref(uifont)} {
9492        set uifont $fontpref(uifont)
9493        parsefont uifont $uifont
9494        eval font configure uifont [fontflags uifont]
9495    }
9496    settabs
9497    if {$showlocalchanges != $oldprefs(showlocalchanges)} {
9498        if {$showlocalchanges} {
9499            doshowlocalchanges
9500        } else {
9501            dohidelocalchanges
9502        }
9503    }
9504    if {$limitdiffs != $oldprefs(limitdiffs)} {
9505        # treediffs elements are limited by path
9506        catch {unset treediffs}
9507    }
9508    if {$fontchanged || $maxwidth != $oldprefs(maxwidth)
9509        || $maxgraphpct != $oldprefs(maxgraphpct)} {
9510        redisplay
9511    } elseif {$showneartags != $oldprefs(showneartags) ||
9512          $limitdiffs != $oldprefs(limitdiffs)} {
9513        reselectline
9514    }
9515}
9516
9517proc formatdate {d} {
9518    global datetimeformat
9519    if {$d ne {}} {
9520        set d [clock format $d -format $datetimeformat]
9521    }
9522    return $d
9523}
9524
9525# This list of encoding names and aliases is distilled from
9526# http://www.iana.org/assignments/character-sets.
9527# Not all of them are supported by Tcl.
9528set encoding_aliases {
9529    { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII
9530      ISO646-US US-ASCII us IBM367 cp367 csASCII }
9531    { ISO-10646-UTF-1 csISO10646UTF1 }
9532    { ISO_646.basic:1983 ref csISO646basic1983 }
9533    { INVARIANT csINVARIANT }
9534    { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion }
9535    { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom }
9536    { NATS-SEFI iso-ir-8-1 csNATSSEFI }
9537    { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD }
9538    { NATS-DANO iso-ir-9-1 csNATSDANO }
9539    { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD }
9540    { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish }
9541    { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames }
9542    { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 }
9543    { ISO-2022-KR csISO2022KR }
9544    { EUC-KR csEUCKR }
9545    { ISO-2022-JP csISO2022JP }
9546    { ISO-2022-JP-2 csISO2022JP2 }
9547    { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7
9548      csISO13JISC6220jp }
9549    { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro }
9550    { IT iso-ir-15 ISO646-IT csISO15Italian }
9551    { PT iso-ir-16 ISO646-PT csISO16Portuguese }
9552    { ES iso-ir-17 ISO646-ES csISO17Spanish }
9553    { greek7-old iso-ir-18 csISO18Greek7Old }
9554    { latin-greek iso-ir-19 csISO19LatinGreek }
9555    { DIN_66003 iso-ir-21 de ISO646-DE csISO21German }
9556    { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French }
9557    { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 }
9558    { ISO_5427 iso-ir-37 csISO5427Cyrillic }
9559    { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 }
9560    { BS_viewdata iso-ir-47 csISO47BSViewdata }
9561    { INIS iso-ir-49 csISO49INIS }
9562    { INIS-8 iso-ir-50 csISO50INIS8 }
9563    { INIS-cyrillic iso-ir-51 csISO51INISCyrillic }
9564    { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 }
9565    { ISO_5428:1980 iso-ir-55 csISO5428Greek }
9566    { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 }
9567    { GB_2312-80 iso-ir-58 chinese csISO58GB231280 }
9568    { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian
9569      csISO60Norwegian1 }
9570    { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 }
9571    { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French }
9572    { videotex-suppl iso-ir-70 csISO70VideotexSupp1 }
9573    { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 }
9574    { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 }
9575    { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian }
9576    { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 }
9577    { greek7 iso-ir-88 csISO88Greek7 }
9578    { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 }
9579    { iso-ir-90 csISO90 }
9580    { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a }
9581    { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b
9582      csISO92JISC62991984b }
9583    { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd }
9584    { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand }
9585    { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add
9586      csISO95JIS62291984handadd }
9587    { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana }
9588    { ISO_2033-1983 iso-ir-98 e13b csISO2033 }
9589    { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS }
9590    { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819
9591      CP819 csISOLatin1 }
9592    { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 }
9593    { T.61-7bit iso-ir-102 csISO102T617bit }
9594    { T.61-8bit T.61 iso-ir-103 csISO103T618bit }
9595    { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 }
9596    { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 }
9597    { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic }
9598    { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 }
9599    { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 }
9600    { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr }
9601    { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708
9602      arabic csISOLatinArabic }
9603    { ISO_8859-6-E csISO88596E ISO-8859-6-E }
9604    { ISO_8859-6-I csISO88596I ISO-8859-6-I }
9605    { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118
9606      greek greek8 csISOLatinGreek }
9607    { T.101-G2 iso-ir-128 csISO128T101G2 }
9608    { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew
9609      csISOLatinHebrew }
9610    { ISO_8859-8-E csISO88598E ISO-8859-8-E }
9611    { ISO_8859-8-I csISO88598I ISO-8859-8-I }
9612    { CSN_369103 iso-ir-139 csISO139CSN369103 }
9613    { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 }
9614    { ISO_6937-2-add iso-ir-142 csISOTextComm }
9615    { IEC_P27-1 iso-ir-143 csISO143IECP271 }
9616    { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic
9617      csISOLatinCyrillic }
9618    { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian }
9619    { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian }
9620    { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 }
9621    { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT }
9622    { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba }
9623    { ISO_6937-2-25 iso-ir-152 csISO6937Add }
9624    { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 }
9625    { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp }
9626    { ISO_10367-box iso-ir-155 csISO10367Box }
9627    { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 }
9628    { latin-lap lap iso-ir-158 csISO158Lap }
9629    { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 }
9630    { DS_2089 DS2089 ISO646-DK dk csISO646Danish }
9631    { us-dk csUSDK }
9632    { dk-us csDKUS }
9633    { JIS_X0201 X0201 csHalfWidthKatakana }
9634    { KSC5636 ISO646-KR csKSC5636 }
9635    { ISO-10646-UCS-2 csUnicode }
9636    { ISO-10646-UCS-4 csUCS4 }
9637    { DEC-MCS dec csDECMCS }
9638    { hp-roman8 roman8 r8 csHPRoman8 }
9639    { macintosh mac csMacintosh }
9640    { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl
9641      csIBM037 }
9642    { IBM038 EBCDIC-INT cp038 csIBM038 }
9643    { IBM273 CP273 csIBM273 }
9644    { IBM274 EBCDIC-BE CP274 csIBM274 }
9645    { IBM275 EBCDIC-BR cp275 csIBM275 }
9646    { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 }
9647    { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 }
9648    { IBM280 CP280 ebcdic-cp-it csIBM280 }
9649    { IBM281 EBCDIC-JP-E cp281 csIBM281 }
9650    { IBM284 CP284 ebcdic-cp-es csIBM284 }
9651    { IBM285 CP285 ebcdic-cp-gb csIBM285 }
9652    { IBM290 cp290 EBCDIC-JP-kana csIBM290 }
9653    { IBM297 cp297 ebcdic-cp-fr csIBM297 }
9654    { IBM420 cp420 ebcdic-cp-ar1 csIBM420 }
9655    { IBM423 cp423 ebcdic-cp-gr csIBM423 }
9656    { IBM424 cp424 ebcdic-cp-he csIBM424 }
9657    { IBM437 cp437 437 csPC8CodePage437 }
9658    { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 }
9659    { IBM775 cp775 csPC775Baltic }
9660    { IBM850 cp850 850 csPC850Multilingual }
9661    { IBM851 cp851 851 csIBM851 }
9662    { IBM852 cp852 852 csPCp852 }
9663    { IBM855 cp855 855 csIBM855 }
9664    { IBM857 cp857 857 csIBM857 }
9665    { IBM860 cp860 860 csIBM860 }
9666    { IBM861 cp861 861 cp-is csIBM861 }
9667    { IBM862 cp862 862 csPC862LatinHebrew }
9668    { IBM863 cp863 863 csIBM863 }
9669    { IBM864 cp864 csIBM864 }
9670    { IBM865 cp865 865 csIBM865 }
9671    { IBM866 cp866 866 csIBM866 }
9672    { IBM868 CP868 cp-ar csIBM868 }
9673    { IBM869 cp869 869 cp-gr csIBM869 }
9674    { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 }
9675    { IBM871 CP871 ebcdic-cp-is csIBM871 }
9676    { IBM880 cp880 EBCDIC-Cyrillic csIBM880 }
9677    { IBM891 cp891 csIBM891 }
9678    { IBM903 cp903 csIBM903 }
9679    { IBM904 cp904 904 csIBBM904 }
9680    { IBM905 CP905 ebcdic-cp-tr csIBM905 }
9681    { IBM918 CP918 ebcdic-cp-ar2 csIBM918 }
9682    { IBM1026 CP1026 csIBM1026 }
9683    { EBCDIC-AT-DE csIBMEBCDICATDE }
9684    { EBCDIC-AT-DE-A csEBCDICATDEA }
9685    { EBCDIC-CA-FR csEBCDICCAFR }
9686    { EBCDIC-DK-NO csEBCDICDKNO }
9687    { EBCDIC-DK-NO-A csEBCDICDKNOA }
9688    { EBCDIC-FI-SE csEBCDICFISE }
9689    { EBCDIC-FI-SE-A csEBCDICFISEA }
9690    { EBCDIC-FR csEBCDICFR }
9691    { EBCDIC-IT csEBCDICIT }
9692    { EBCDIC-PT csEBCDICPT }
9693    { EBCDIC-ES csEBCDICES }
9694    { EBCDIC-ES-A csEBCDICESA }
9695    { EBCDIC-ES-S csEBCDICESS }
9696    { EBCDIC-UK csEBCDICUK }
9697    { EBCDIC-US csEBCDICUS }
9698    { UNKNOWN-8BIT csUnknown8BiT }
9699    { MNEMONIC csMnemonic }
9700    { MNEM csMnem }
9701    { VISCII csVISCII }
9702    { VIQR csVIQR }
9703    { KOI8-R csKOI8R }
9704    { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro }
9705    { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro }
9706    { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro }
9707    { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro }
9708    { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro }
9709    { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro }
9710    { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro }
9711    { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro }
9712    { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro }
9713    { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro }
9714    { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro }
9715    { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro }
9716    { IBM1047 IBM-1047 }
9717    { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian }
9718    { Amiga-1251 Ami1251 Amiga1251 Ami-1251 }
9719    { UNICODE-1-1 csUnicode11 }
9720    { CESU-8 csCESU-8 }
9721    { BOCU-1 csBOCU-1 }
9722    { UNICODE-1-1-UTF-7 csUnicode11UTF7 }
9723    { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic
9724      l8 }
9725    { ISO-8859-15 ISO_8859-15 Latin-9 }
9726    { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 }
9727    { GBK CP936 MS936 windows-936 }
9728    { JIS_Encoding csJISEncoding }
9729    { Shift_JIS MS_Kanji csShiftJIS }
9730    { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese
9731      EUC-JP }
9732    { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese }
9733    { ISO-10646-UCS-Basic csUnicodeASCII }
9734    { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 }
9735    { ISO-Unicode-IBM-1261 csUnicodeIBM1261 }
9736    { ISO-Unicode-IBM-1268 csUnicodeIBM1268 }
9737    { ISO-Unicode-IBM-1276 csUnicodeIBM1276 }
9738    { ISO-Unicode-IBM-1264 csUnicodeIBM1264 }
9739    { ISO-Unicode-IBM-1265 csUnicodeIBM1265 }
9740    { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 }
9741    { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 }
9742    { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 }
9743    { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 }
9744    { Adobe-Standard-Encoding csAdobeStandardEncoding }
9745    { Ventura-US csVenturaUS }
9746    { Ventura-International csVenturaInternational }
9747    { PC8-Danish-Norwegian csPC8DanishNorwegian }
9748    { PC8-Turkish csPC8Turkish }
9749    { IBM-Symbols csIBMSymbols }
9750    { IBM-Thai csIBMThai }
9751    { HP-Legal csHPLegal }
9752    { HP-Pi-font csHPPiFont }
9753    { HP-Math8 csHPMath8 }
9754    { Adobe-Symbol-Encoding csHPPSMath }
9755    { HP-DeskTop csHPDesktop }
9756    { Ventura-Math csVenturaMath }
9757    { Microsoft-Publishing csMicrosoftPublishing }
9758    { Windows-31J csWindows31J }
9759    { GB2312 csGB2312 }
9760    { Big5 csBig5 }
9761}
9762
9763proc tcl_encoding {enc} {
9764    global encoding_aliases
9765    set names [encoding names]
9766    set lcnames [string tolower $names]
9767    set enc [string tolower $enc]
9768    set i [lsearch -exact $lcnames $enc]
9769    if {$i < 0} {
9770        # look for "isonnn" instead of "iso-nnn" or "iso_nnn"
9771        if {[regsub {^iso[-_]} $enc iso encx]} {
9772            set i [lsearch -exact $lcnames $encx]
9773        }
9774    }
9775    if {$i < 0} {
9776        foreach l $encoding_aliases {
9777            set ll [string tolower $l]
9778            if {[lsearch -exact $ll $enc] < 0} continue
9779            # look through the aliases for one that tcl knows about
9780            foreach e $ll {
9781                set i [lsearch -exact $lcnames $e]
9782                if {$i < 0} {
9783                    if {[regsub {^iso[-_]} $e iso ex]} {
9784                        set i [lsearch -exact $lcnames $ex]
9785                    }
9786                }
9787                if {$i >= 0} break
9788            }
9789            break
9790        }
9791    }
9792    if {$i >= 0} {
9793        return [lindex $names $i]
9794    }
9795    return {}
9796}
9797
9798# First check that Tcl/Tk is recent enough
9799if {[catch {package require Tk 8.4} err]} {
9800    show_error {} . [mc "Sorry, gitk cannot run with this version of Tcl/Tk.\n\
9801                     Gitk requires at least Tcl/Tk 8.4."]
9802    exit 1
9803}
9804
9805# defaults...
9806set wrcomcmd "git diff-tree --stdin -p --pretty"
9807
9808set gitencoding {}
9809catch {
9810    set gitencoding [exec git config --get i18n.commitencoding]
9811}
9812if {$gitencoding == ""} {
9813    set gitencoding "utf-8"
9814}
9815set tclencoding [tcl_encoding $gitencoding]
9816if {$tclencoding == {}} {
9817    puts stderr "Warning: encoding $gitencoding is not supported by Tcl/Tk"
9818}
9819
9820set mainfont {Helvetica 9}
9821set textfont {Courier 9}
9822set uifont {Helvetica 9 bold}
9823set tabstop 8
9824set findmergefiles 0
9825set maxgraphpct 50
9826set maxwidth 16
9827set revlistorder 0
9828set fastdate 0
9829set uparrowlen 5
9830set downarrowlen 5
9831set mingaplen 100
9832set cmitmode "patch"
9833set wrapcomment "none"
9834set showneartags 1
9835set maxrefs 20
9836set maxlinelen 200
9837set showlocalchanges 1
9838set limitdiffs 1
9839set datetimeformat "%Y-%m-%d %H:%M:%S"
9840set autoselect 1
9841
9842set extdifftool "meld"
9843
9844set colors {green red blue magenta darkgrey brown orange}
9845set bgcolor white
9846set fgcolor black
9847set diffcolors {red "#00a000" blue}
9848set diffcontext 3
9849set ignorespace 0
9850set selectbgcolor gray85
9851
9852set circlecolors {white blue gray blue blue}
9853
9854## For msgcat loading, first locate the installation location.
9855if { [info exists ::env(GITK_MSGSDIR)] } {
9856    ## Msgsdir was manually set in the environment.
9857    set gitk_msgsdir $::env(GITK_MSGSDIR)
9858} else {
9859    ## Let's guess the prefix from argv0.
9860    set gitk_prefix [file dirname [file dirname [file normalize $argv0]]]
9861    set gitk_libdir [file join $gitk_prefix share gitk lib]
9862    set gitk_msgsdir [file join $gitk_libdir msgs]
9863    unset gitk_prefix
9864}
9865
9866## Internationalization (i18n) through msgcat and gettext. See
9867## http://www.gnu.org/software/gettext/manual/html_node/Tcl.html
9868package require msgcat
9869namespace import ::msgcat::mc
9870## And eventually load the actual message catalog
9871::msgcat::mcload $gitk_msgsdir
9872
9873catch {source ~/.gitk}
9874
9875font create optionfont -family sans-serif -size -12
9876
9877parsefont mainfont $mainfont
9878eval font create mainfont [fontflags mainfont]
9879eval font create mainfontbold [fontflags mainfont 1]
9880
9881parsefont textfont $textfont
9882eval font create textfont [fontflags textfont]
9883eval font create textfontbold [fontflags textfont 1]
9884
9885parsefont uifont $uifont
9886eval font create uifont [fontflags uifont]
9887
9888setoptions
9889
9890# check that we can find a .git directory somewhere...
9891if {[catch {set gitdir [gitdir]}]} {
9892    show_error {} . [mc "Cannot find a git repository here."]
9893    exit 1
9894}
9895if {![file isdirectory $gitdir]} {
9896    show_error {} . [mc "Cannot find the git directory \"%s\"." $gitdir]
9897    exit 1
9898}
9899
9900set selecthead {}
9901set selectheadid {}
9902
9903set revtreeargs {}
9904set cmdline_files {}
9905set i 0
9906set revtreeargscmd {}
9907foreach arg $argv {
9908    switch -glob -- $arg {
9909        "" { }
9910        "--" {
9911            set cmdline_files [lrange $argv [expr {$i + 1}] end]
9912            break
9913        }
9914        "--select-commit=*" {
9915            set selecthead [string range $arg 16 end]
9916        }
9917        "--argscmd=*" {
9918            set revtreeargscmd [string range $arg 10 end]
9919        }
9920        default {
9921            lappend revtreeargs $arg
9922        }
9923    }
9924    incr i
9925}
9926
9927if {$selecthead eq "HEAD"} {
9928    set selecthead {}
9929}
9930
9931if {$i >= [llength $argv] && $revtreeargs ne {}} {
9932    # no -- on command line, but some arguments (other than --argscmd)
9933    if {[catch {
9934        set f [eval exec git rev-parse --no-revs --no-flags $revtreeargs]
9935        set cmdline_files [split $f "\n"]
9936        set n [llength $cmdline_files]
9937        set revtreeargs [lrange $revtreeargs 0 end-$n]
9938        # Unfortunately git rev-parse doesn't produce an error when
9939        # something is both a revision and a filename.  To be consistent
9940        # with git log and git rev-list, check revtreeargs for filenames.
9941        foreach arg $revtreeargs {
9942            if {[file exists $arg]} {
9943                show_error {} . [mc "Ambiguous argument '%s': both revision\
9944                                 and filename" $arg]
9945                exit 1
9946            }
9947        }
9948    } err]} {
9949        # unfortunately we get both stdout and stderr in $err,
9950        # so look for "fatal:".
9951        set i [string first "fatal:" $err]
9952        if {$i > 0} {
9953            set err [string range $err [expr {$i + 6}] end]
9954        }
9955        show_error {} . "[mc "Bad arguments to gitk:"]\n$err"
9956        exit 1
9957    }
9958}
9959
9960set nullid "0000000000000000000000000000000000000000"
9961set nullid2 "0000000000000000000000000000000000000001"
9962set nullfile "/dev/null"
9963
9964set have_tk85 [expr {[package vcompare $tk_version "8.5"] >= 0}]
9965
9966set runq {}
9967set history {}
9968set historyindex 0
9969set fh_serial 0
9970set nhl_names {}
9971set highlight_paths {}
9972set findpattern {}
9973set searchdirn -forwards
9974set boldrows {}
9975set boldnamerows {}
9976set diffelide {0 0}
9977set markingmatches 0
9978set linkentercount 0
9979set need_redisplay 0
9980set nrows_drawn 0
9981set firsttabstop 0
9982
9983set nextviewnum 1
9984set curview 0
9985set selectedview 0
9986set selectedhlview [mc "None"]
9987set highlight_related [mc "None"]
9988set highlight_files {}
9989set viewfiles(0) {}
9990set viewperm(0) 0
9991set viewargs(0) {}
9992set viewargscmd(0) {}
9993
9994set selectedline {}
9995set numcommits 0
9996set loginstance 0
9997set cmdlineok 0
9998set stopped 0
9999set stuffsaved 0
10000set patchnum 0
10001set lserial 0
10002set isworktree [expr {[exec git rev-parse --is-inside-work-tree] == "true"}]
10003setcoords
10004makewindow
10005# wait for the window to become visible
10006tkwait visibility .
10007wm title . "[file tail $argv0]: [file tail [pwd]]"
10008readrefs
10009
10010if {$cmdline_files ne {} || $revtreeargs ne {} || $revtreeargscmd ne {}} {
10011    # create a view for the files/dirs specified on the command line
10012    set curview 1
10013    set selectedview 1
10014    set nextviewnum 2
10015    set viewname(1) [mc "Command line"]
10016    set viewfiles(1) $cmdline_files
10017    set viewargs(1) $revtreeargs
10018    set viewargscmd(1) $revtreeargscmd
10019    set viewperm(1) 0
10020    set vdatemode(1) 0
10021    addviewmenu 1
10022    .bar.view entryconf [mc "Edit view..."] -state normal
10023    .bar.view entryconf [mc "Delete view"] -state normal
10024}
10025
10026if {[info exists permviews]} {
10027    foreach v $permviews {
10028        set n $nextviewnum
10029        incr nextviewnum
10030        set viewname($n) [lindex $v 0]
10031        set viewfiles($n) [lindex $v 1]
10032        set viewargs($n) [lindex $v 2]
10033        set viewargscmd($n) [lindex $v 3]
10034        set viewperm($n) 1
10035        addviewmenu $n
10036    }
10037}
10038getcommits {}