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