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