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