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