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