gitkon commit gitk: Add a 'rename' option to the branch context menu (5a046c5)
   1#!/bin/sh
   2# Tcl ignores the next line -*- tcl -*- \
   3exec wish "$0" -- "$@"
   4
   5# Copyright © 2005-2014 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
  10package require Tk
  11
  12proc hasworktree {} {
  13    return [expr {[exec git rev-parse --is-bare-repository] == "false" &&
  14                  [exec git rev-parse --is-inside-git-dir] == "false"}]
  15}
  16
  17proc reponame {} {
  18    global gitdir
  19    set n [file normalize $gitdir]
  20    if {[string match "*/.git" $n]} {
  21        set n [string range $n 0 end-5]
  22    }
  23    return [file tail $n]
  24}
  25
  26proc gitworktree {} {
  27    variable _gitworktree
  28    if {[info exists _gitworktree]} {
  29        return $_gitworktree
  30    }
  31    # v1.7.0 introduced --show-toplevel to return the canonical work-tree
  32    if {[catch {set _gitworktree [exec git rev-parse --show-toplevel]}]} {
  33        # try to set work tree from environment, core.worktree or use
  34        # cdup to obtain a relative path to the top of the worktree. If
  35        # run from the top, the ./ prefix ensures normalize expands pwd.
  36        if {[catch { set _gitworktree $env(GIT_WORK_TREE) }]} {
  37            catch {set _gitworktree [exec git config --get core.worktree]}
  38            if {$_gitworktree eq ""} {
  39                set _gitworktree [file normalize ./[exec git rev-parse --show-cdup]]
  40            }
  41        }
  42    }
  43    return $_gitworktree
  44}
  45
  46# A simple scheduler for compute-intensive stuff.
  47# The aim is to make sure that event handlers for GUI actions can
  48# run at least every 50-100 ms.  Unfortunately fileevent handlers are
  49# run before X event handlers, so reading from a fast source can
  50# make the GUI completely unresponsive.
  51proc run args {
  52    global isonrunq runq currunq
  53
  54    set script $args
  55    if {[info exists isonrunq($script)]} return
  56    if {$runq eq {} && ![info exists currunq]} {
  57        after idle dorunq
  58    }
  59    lappend runq [list {} $script]
  60    set isonrunq($script) 1
  61}
  62
  63proc filerun {fd script} {
  64    fileevent $fd readable [list filereadable $fd $script]
  65}
  66
  67proc filereadable {fd script} {
  68    global runq currunq
  69
  70    fileevent $fd readable {}
  71    if {$runq eq {} && ![info exists currunq]} {
  72        after idle dorunq
  73    }
  74    lappend runq [list $fd $script]
  75}
  76
  77proc nukefile {fd} {
  78    global runq
  79
  80    for {set i 0} {$i < [llength $runq]} {} {
  81        if {[lindex $runq $i 0] eq $fd} {
  82            set runq [lreplace $runq $i $i]
  83        } else {
  84            incr i
  85        }
  86    }
  87}
  88
  89proc dorunq {} {
  90    global isonrunq runq currunq
  91
  92    set tstart [clock clicks -milliseconds]
  93    set t0 $tstart
  94    while {[llength $runq] > 0} {
  95        set fd [lindex $runq 0 0]
  96        set script [lindex $runq 0 1]
  97        set currunq [lindex $runq 0]
  98        set runq [lrange $runq 1 end]
  99        set repeat [eval $script]
 100        unset currunq
 101        set t1 [clock clicks -milliseconds]
 102        set t [expr {$t1 - $t0}]
 103        if {$repeat ne {} && $repeat} {
 104            if {$fd eq {} || $repeat == 2} {
 105                # script returns 1 if it wants to be readded
 106                # file readers return 2 if they could do more straight away
 107                lappend runq [list $fd $script]
 108            } else {
 109                fileevent $fd readable [list filereadable $fd $script]
 110            }
 111        } elseif {$fd eq {}} {
 112            unset isonrunq($script)
 113        }
 114        set t0 $t1
 115        if {$t1 - $tstart >= 80} break
 116    }
 117    if {$runq ne {}} {
 118        after idle dorunq
 119    }
 120}
 121
 122proc reg_instance {fd} {
 123    global commfd leftover loginstance
 124
 125    set i [incr loginstance]
 126    set commfd($i) $fd
 127    set leftover($i) {}
 128    return $i
 129}
 130
 131proc unmerged_files {files} {
 132    global nr_unmerged
 133
 134    # find the list of unmerged files
 135    set mlist {}
 136    set nr_unmerged 0
 137    if {[catch {
 138        set fd [open "| git ls-files -u" r]
 139    } err]} {
 140        show_error {} . "[mc "Couldn't get list of unmerged files:"] $err"
 141        exit 1
 142    }
 143    while {[gets $fd line] >= 0} {
 144        set i [string first "\t" $line]
 145        if {$i < 0} continue
 146        set fname [string range $line [expr {$i+1}] end]
 147        if {[lsearch -exact $mlist $fname] >= 0} continue
 148        incr nr_unmerged
 149        if {$files eq {} || [path_filter $files $fname]} {
 150            lappend mlist $fname
 151        }
 152    }
 153    catch {close $fd}
 154    return $mlist
 155}
 156
 157proc parseviewargs {n arglist} {
 158    global vdatemode vmergeonly vflags vdflags vrevs vfiltered vorigargs env
 159    global vinlinediff
 160    global worddiff git_version
 161
 162    set vdatemode($n) 0
 163    set vmergeonly($n) 0
 164    set vinlinediff($n) 0
 165    set glflags {}
 166    set diffargs {}
 167    set nextisval 0
 168    set revargs {}
 169    set origargs $arglist
 170    set allknown 1
 171    set filtered 0
 172    set i -1
 173    foreach arg $arglist {
 174        incr i
 175        if {$nextisval} {
 176            lappend glflags $arg
 177            set nextisval 0
 178            continue
 179        }
 180        switch -glob -- $arg {
 181            "-d" -
 182            "--date-order" {
 183                set vdatemode($n) 1
 184                # remove from origargs in case we hit an unknown option
 185                set origargs [lreplace $origargs $i $i]
 186                incr i -1
 187            }
 188            "-[puabwcrRBMC]" -
 189            "--no-renames" - "--full-index" - "--binary" - "--abbrev=*" -
 190            "--find-copies-harder" - "-l*" - "--ext-diff" - "--no-ext-diff" -
 191            "--src-prefix=*" - "--dst-prefix=*" - "--no-prefix" -
 192            "-O*" - "--text" - "--full-diff" - "--ignore-space-at-eol" -
 193            "--ignore-space-change" - "-U*" - "--unified=*" {
 194                # These request or affect diff output, which we don't want.
 195                # Some could be used to set our defaults for diff display.
 196                lappend diffargs $arg
 197            }
 198            "--raw" - "--patch-with-raw" - "--patch-with-stat" -
 199            "--name-only" - "--name-status" - "--color" -
 200            "--log-size" - "--pretty=*" - "--decorate" - "--abbrev-commit" -
 201            "--cc" - "-z" - "--header" - "--parents" - "--boundary" -
 202            "--no-color" - "-g" - "--walk-reflogs" - "--no-walk" -
 203            "--timestamp" - "relative-date" - "--date=*" - "--stdin" -
 204            "--objects" - "--objects-edge" - "--reverse" {
 205                # These cause our parsing of git log's output to fail, or else
 206                # they're options we want to set ourselves, so ignore them.
 207            }
 208            "--color-words*" - "--word-diff=color" {
 209                # These trigger a word diff in the console interface,
 210                # so help the user by enabling our own support
 211                if {[package vcompare $git_version "1.7.2"] >= 0} {
 212                    set worddiff [mc "Color words"]
 213                }
 214            }
 215            "--word-diff*" {
 216                if {[package vcompare $git_version "1.7.2"] >= 0} {
 217                    set worddiff [mc "Markup words"]
 218                }
 219            }
 220            "--stat=*" - "--numstat" - "--shortstat" - "--summary" -
 221            "--check" - "--exit-code" - "--quiet" - "--topo-order" -
 222            "--full-history" - "--dense" - "--sparse" -
 223            "--follow" - "--left-right" - "--encoding=*" {
 224                # These are harmless, and some are even useful
 225                lappend glflags $arg
 226            }
 227            "--diff-filter=*" - "--no-merges" - "--unpacked" -
 228            "--max-count=*" - "--skip=*" - "--since=*" - "--after=*" -
 229            "--until=*" - "--before=*" - "--max-age=*" - "--min-age=*" -
 230            "--author=*" - "--committer=*" - "--grep=*" - "-[iE]" -
 231            "--remove-empty" - "--first-parent" - "--cherry-pick" -
 232            "-S*" - "-G*" - "--pickaxe-all" - "--pickaxe-regex" -
 233            "--simplify-by-decoration" {
 234                # These mean that we get a subset of the commits
 235                set filtered 1
 236                lappend glflags $arg
 237            }
 238            "-L*" {
 239                # Line-log with 'stuck' argument (unstuck form is
 240                # not supported)
 241                set filtered 1
 242                set vinlinediff($n) 1
 243                set allknown 0
 244                lappend glflags $arg
 245            }
 246            "-n" {
 247                # This appears to be the only one that has a value as a
 248                # separate word following it
 249                set filtered 1
 250                set nextisval 1
 251                lappend glflags $arg
 252            }
 253            "--not" - "--all" {
 254                lappend revargs $arg
 255            }
 256            "--merge" {
 257                set vmergeonly($n) 1
 258                # git rev-parse doesn't understand --merge
 259                lappend revargs --gitk-symmetric-diff-marker MERGE_HEAD...HEAD
 260            }
 261            "--no-replace-objects" {
 262                set env(GIT_NO_REPLACE_OBJECTS) "1"
 263            }
 264            "-*" {
 265                # Other flag arguments including -<n>
 266                if {[string is digit -strict [string range $arg 1 end]]} {
 267                    set filtered 1
 268                } else {
 269                    # a flag argument that we don't recognize;
 270                    # that means we can't optimize
 271                    set allknown 0
 272                }
 273                lappend glflags $arg
 274            }
 275            default {
 276                # Non-flag arguments specify commits or ranges of commits
 277                if {[string match "*...*" $arg]} {
 278                    lappend revargs --gitk-symmetric-diff-marker
 279                }
 280                lappend revargs $arg
 281            }
 282        }
 283    }
 284    set vdflags($n) $diffargs
 285    set vflags($n) $glflags
 286    set vrevs($n) $revargs
 287    set vfiltered($n) $filtered
 288    set vorigargs($n) $origargs
 289    return $allknown
 290}
 291
 292proc parseviewrevs {view revs} {
 293    global vposids vnegids
 294
 295    if {$revs eq {}} {
 296        set revs HEAD
 297    } elseif {[lsearch -exact $revs --all] >= 0} {
 298        lappend revs HEAD
 299    }
 300    if {[catch {set ids [eval exec git rev-parse $revs]} err]} {
 301        # we get stdout followed by stderr in $err
 302        # for an unknown rev, git rev-parse echoes it and then errors out
 303        set errlines [split $err "\n"]
 304        set badrev {}
 305        for {set l 0} {$l < [llength $errlines]} {incr l} {
 306            set line [lindex $errlines $l]
 307            if {!([string length $line] == 40 && [string is xdigit $line])} {
 308                if {[string match "fatal:*" $line]} {
 309                    if {[string match "fatal: ambiguous argument*" $line]
 310                        && $badrev ne {}} {
 311                        if {[llength $badrev] == 1} {
 312                            set err "unknown revision $badrev"
 313                        } else {
 314                            set err "unknown revisions: [join $badrev ", "]"
 315                        }
 316                    } else {
 317                        set err [join [lrange $errlines $l end] "\n"]
 318                    }
 319                    break
 320                }
 321                lappend badrev $line
 322            }
 323        }
 324        error_popup "[mc "Error parsing revisions:"] $err"
 325        return {}
 326    }
 327    set ret {}
 328    set pos {}
 329    set neg {}
 330    set sdm 0
 331    foreach id [split $ids "\n"] {
 332        if {$id eq "--gitk-symmetric-diff-marker"} {
 333            set sdm 4
 334        } elseif {[string match "^*" $id]} {
 335            if {$sdm != 1} {
 336                lappend ret $id
 337                if {$sdm == 3} {
 338                    set sdm 0
 339                }
 340            }
 341            lappend neg [string range $id 1 end]
 342        } else {
 343            if {$sdm != 2} {
 344                lappend ret $id
 345            } else {
 346                lset ret end $id...[lindex $ret end]
 347            }
 348            lappend pos $id
 349        }
 350        incr sdm -1
 351    }
 352    set vposids($view) $pos
 353    set vnegids($view) $neg
 354    return $ret
 355}
 356
 357# Start off a git log process and arrange to read its output
 358proc start_rev_list {view} {
 359    global startmsecs commitidx viewcomplete curview
 360    global tclencoding
 361    global viewargs viewargscmd viewfiles vfilelimit
 362    global showlocalchanges
 363    global viewactive viewinstances vmergeonly
 364    global mainheadid viewmainheadid viewmainheadid_orig
 365    global vcanopt vflags vrevs vorigargs
 366    global show_notes
 367
 368    set startmsecs [clock clicks -milliseconds]
 369    set commitidx($view) 0
 370    # these are set this way for the error exits
 371    set viewcomplete($view) 1
 372    set viewactive($view) 0
 373    varcinit $view
 374
 375    set args $viewargs($view)
 376    if {$viewargscmd($view) ne {}} {
 377        if {[catch {
 378            set str [exec sh -c $viewargscmd($view)]
 379        } err]} {
 380            error_popup "[mc "Error executing --argscmd command:"] $err"
 381            return 0
 382        }
 383        set args [concat $args [split $str "\n"]]
 384    }
 385    set vcanopt($view) [parseviewargs $view $args]
 386
 387    set files $viewfiles($view)
 388    if {$vmergeonly($view)} {
 389        set files [unmerged_files $files]
 390        if {$files eq {}} {
 391            global nr_unmerged
 392            if {$nr_unmerged == 0} {
 393                error_popup [mc "No files selected: --merge specified but\
 394                             no files are unmerged."]
 395            } else {
 396                error_popup [mc "No files selected: --merge specified but\
 397                             no unmerged files are within file limit."]
 398            }
 399            return 0
 400        }
 401    }
 402    set vfilelimit($view) $files
 403
 404    if {$vcanopt($view)} {
 405        set revs [parseviewrevs $view $vrevs($view)]
 406        if {$revs eq {}} {
 407            return 0
 408        }
 409        set args [concat $vflags($view) $revs]
 410    } else {
 411        set args $vorigargs($view)
 412    }
 413
 414    if {[catch {
 415        set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \
 416                        --parents --boundary $args "--" $files] r]
 417    } err]} {
 418        error_popup "[mc "Error executing git log:"] $err"
 419        return 0
 420    }
 421    set i [reg_instance $fd]
 422    set viewinstances($view) [list $i]
 423    set viewmainheadid($view) $mainheadid
 424    set viewmainheadid_orig($view) $mainheadid
 425    if {$files ne {} && $mainheadid ne {}} {
 426        get_viewmainhead $view
 427    }
 428    if {$showlocalchanges && $viewmainheadid($view) ne {}} {
 429        interestedin $viewmainheadid($view) dodiffindex
 430    }
 431    fconfigure $fd -blocking 0 -translation lf -eofchar {}
 432    if {$tclencoding != {}} {
 433        fconfigure $fd -encoding $tclencoding
 434    }
 435    filerun $fd [list getcommitlines $fd $i $view 0]
 436    nowbusy $view [mc "Reading"]
 437    set viewcomplete($view) 0
 438    set viewactive($view) 1
 439    return 1
 440}
 441
 442proc stop_instance {inst} {
 443    global commfd leftover
 444
 445    set fd $commfd($inst)
 446    catch {
 447        set pid [pid $fd]
 448
 449        if {$::tcl_platform(platform) eq {windows}} {
 450            exec taskkill /pid $pid
 451        } else {
 452            exec kill $pid
 453        }
 454    }
 455    catch {close $fd}
 456    nukefile $fd
 457    unset commfd($inst)
 458    unset leftover($inst)
 459}
 460
 461proc stop_backends {} {
 462    global commfd
 463
 464    foreach inst [array names commfd] {
 465        stop_instance $inst
 466    }
 467}
 468
 469proc stop_rev_list {view} {
 470    global viewinstances
 471
 472    foreach inst $viewinstances($view) {
 473        stop_instance $inst
 474    }
 475    set viewinstances($view) {}
 476}
 477
 478proc reset_pending_select {selid} {
 479    global pending_select mainheadid selectheadid
 480
 481    if {$selid ne {}} {
 482        set pending_select $selid
 483    } elseif {$selectheadid ne {}} {
 484        set pending_select $selectheadid
 485    } else {
 486        set pending_select $mainheadid
 487    }
 488}
 489
 490proc getcommits {selid} {
 491    global canv curview need_redisplay viewactive
 492
 493    initlayout
 494    if {[start_rev_list $curview]} {
 495        reset_pending_select $selid
 496        show_status [mc "Reading commits..."]
 497        set need_redisplay 1
 498    } else {
 499        show_status [mc "No commits selected"]
 500    }
 501}
 502
 503proc updatecommits {} {
 504    global curview vcanopt vorigargs vfilelimit viewinstances
 505    global viewactive viewcomplete tclencoding
 506    global startmsecs showneartags showlocalchanges
 507    global mainheadid viewmainheadid viewmainheadid_orig pending_select
 508    global hasworktree
 509    global varcid vposids vnegids vflags vrevs
 510    global show_notes
 511
 512    set hasworktree [hasworktree]
 513    rereadrefs
 514    set view $curview
 515    if {$mainheadid ne $viewmainheadid_orig($view)} {
 516        if {$showlocalchanges} {
 517            dohidelocalchanges
 518        }
 519        set viewmainheadid($view) $mainheadid
 520        set viewmainheadid_orig($view) $mainheadid
 521        if {$vfilelimit($view) ne {}} {
 522            get_viewmainhead $view
 523        }
 524    }
 525    if {$showlocalchanges} {
 526        doshowlocalchanges
 527    }
 528    if {$vcanopt($view)} {
 529        set oldpos $vposids($view)
 530        set oldneg $vnegids($view)
 531        set revs [parseviewrevs $view $vrevs($view)]
 532        if {$revs eq {}} {
 533            return
 534        }
 535        # note: getting the delta when negative refs change is hard,
 536        # and could require multiple git log invocations, so in that
 537        # case we ask git log for all the commits (not just the delta)
 538        if {$oldneg eq $vnegids($view)} {
 539            set newrevs {}
 540            set npos 0
 541            # take out positive refs that we asked for before or
 542            # that we have already seen
 543            foreach rev $revs {
 544                if {[string length $rev] == 40} {
 545                    if {[lsearch -exact $oldpos $rev] < 0
 546                        && ![info exists varcid($view,$rev)]} {
 547                        lappend newrevs $rev
 548                        incr npos
 549                    }
 550                } else {
 551                    lappend $newrevs $rev
 552                }
 553            }
 554            if {$npos == 0} return
 555            set revs $newrevs
 556            set vposids($view) [lsort -unique [concat $oldpos $vposids($view)]]
 557        }
 558        set args [concat $vflags($view) $revs --not $oldpos]
 559    } else {
 560        set args $vorigargs($view)
 561    }
 562    if {[catch {
 563        set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \
 564                        --parents --boundary $args "--" $vfilelimit($view)] r]
 565    } err]} {
 566        error_popup "[mc "Error executing git log:"] $err"
 567        return
 568    }
 569    if {$viewactive($view) == 0} {
 570        set startmsecs [clock clicks -milliseconds]
 571    }
 572    set i [reg_instance $fd]
 573    lappend viewinstances($view) $i
 574    fconfigure $fd -blocking 0 -translation lf -eofchar {}
 575    if {$tclencoding != {}} {
 576        fconfigure $fd -encoding $tclencoding
 577    }
 578    filerun $fd [list getcommitlines $fd $i $view 1]
 579    incr viewactive($view)
 580    set viewcomplete($view) 0
 581    reset_pending_select {}
 582    nowbusy $view [mc "Reading"]
 583    if {$showneartags} {
 584        getallcommits
 585    }
 586}
 587
 588proc reloadcommits {} {
 589    global curview viewcomplete selectedline currentid thickerline
 590    global showneartags treediffs commitinterest cached_commitrow
 591    global targetid
 592
 593    set selid {}
 594    if {$selectedline ne {}} {
 595        set selid $currentid
 596    }
 597
 598    if {!$viewcomplete($curview)} {
 599        stop_rev_list $curview
 600    }
 601    resetvarcs $curview
 602    set selectedline {}
 603    unset -nocomplain currentid
 604    unset -nocomplain thickerline
 605    unset -nocomplain treediffs
 606    readrefs
 607    changedrefs
 608    if {$showneartags} {
 609        getallcommits
 610    }
 611    clear_display
 612    unset -nocomplain commitinterest
 613    unset -nocomplain cached_commitrow
 614    unset -nocomplain targetid
 615    setcanvscroll
 616    getcommits $selid
 617    return 0
 618}
 619
 620# This makes a string representation of a positive integer which
 621# sorts as a string in numerical order
 622proc strrep {n} {
 623    if {$n < 16} {
 624        return [format "%x" $n]
 625    } elseif {$n < 256} {
 626        return [format "x%.2x" $n]
 627    } elseif {$n < 65536} {
 628        return [format "y%.4x" $n]
 629    }
 630    return [format "z%.8x" $n]
 631}
 632
 633# Procedures used in reordering commits from git log (without
 634# --topo-order) into the order for display.
 635
 636proc varcinit {view} {
 637    global varcstart vupptr vdownptr vleftptr vbackptr varctok varcrow
 638    global vtokmod varcmod vrowmod varcix vlastins
 639
 640    set varcstart($view) {{}}
 641    set vupptr($view) {0}
 642    set vdownptr($view) {0}
 643    set vleftptr($view) {0}
 644    set vbackptr($view) {0}
 645    set varctok($view) {{}}
 646    set varcrow($view) {{}}
 647    set vtokmod($view) {}
 648    set varcmod($view) 0
 649    set vrowmod($view) 0
 650    set varcix($view) {{}}
 651    set vlastins($view) {0}
 652}
 653
 654proc resetvarcs {view} {
 655    global varcid varccommits parents children vseedcount ordertok
 656    global vshortids
 657
 658    foreach vid [array names varcid $view,*] {
 659        unset varcid($vid)
 660        unset children($vid)
 661        unset parents($vid)
 662    }
 663    foreach vid [array names vshortids $view,*] {
 664        unset vshortids($vid)
 665    }
 666    # some commits might have children but haven't been seen yet
 667    foreach vid [array names children $view,*] {
 668        unset children($vid)
 669    }
 670    foreach va [array names varccommits $view,*] {
 671        unset varccommits($va)
 672    }
 673    foreach vd [array names vseedcount $view,*] {
 674        unset vseedcount($vd)
 675    }
 676    unset -nocomplain ordertok
 677}
 678
 679# returns a list of the commits with no children
 680proc seeds {v} {
 681    global vdownptr vleftptr varcstart
 682
 683    set ret {}
 684    set a [lindex $vdownptr($v) 0]
 685    while {$a != 0} {
 686        lappend ret [lindex $varcstart($v) $a]
 687        set a [lindex $vleftptr($v) $a]
 688    }
 689    return $ret
 690}
 691
 692proc newvarc {view id} {
 693    global varcid varctok parents children vdatemode
 694    global vupptr vdownptr vleftptr vbackptr varcrow varcix varcstart
 695    global commitdata commitinfo vseedcount varccommits vlastins
 696
 697    set a [llength $varctok($view)]
 698    set vid $view,$id
 699    if {[llength $children($vid)] == 0 || $vdatemode($view)} {
 700        if {![info exists commitinfo($id)]} {
 701            parsecommit $id $commitdata($id) 1
 702        }
 703        set cdate [lindex [lindex $commitinfo($id) 4] 0]
 704        if {![string is integer -strict $cdate]} {
 705            set cdate 0
 706        }
 707        if {![info exists vseedcount($view,$cdate)]} {
 708            set vseedcount($view,$cdate) -1
 709        }
 710        set c [incr vseedcount($view,$cdate)]
 711        set cdate [expr {$cdate ^ 0xffffffff}]
 712        set tok "s[strrep $cdate][strrep $c]"
 713    } else {
 714        set tok {}
 715    }
 716    set ka 0
 717    if {[llength $children($vid)] > 0} {
 718        set kid [lindex $children($vid) end]
 719        set k $varcid($view,$kid)
 720        if {[string compare [lindex $varctok($view) $k] $tok] > 0} {
 721            set ki $kid
 722            set ka $k
 723            set tok [lindex $varctok($view) $k]
 724        }
 725    }
 726    if {$ka != 0} {
 727        set i [lsearch -exact $parents($view,$ki) $id]
 728        set j [expr {[llength $parents($view,$ki)] - 1 - $i}]
 729        append tok [strrep $j]
 730    }
 731    set c [lindex $vlastins($view) $ka]
 732    if {$c == 0 || [string compare $tok [lindex $varctok($view) $c]] < 0} {
 733        set c $ka
 734        set b [lindex $vdownptr($view) $ka]
 735    } else {
 736        set b [lindex $vleftptr($view) $c]
 737    }
 738    while {$b != 0 && [string compare $tok [lindex $varctok($view) $b]] >= 0} {
 739        set c $b
 740        set b [lindex $vleftptr($view) $c]
 741    }
 742    if {$c == $ka} {
 743        lset vdownptr($view) $ka $a
 744        lappend vbackptr($view) 0
 745    } else {
 746        lset vleftptr($view) $c $a
 747        lappend vbackptr($view) $c
 748    }
 749    lset vlastins($view) $ka $a
 750    lappend vupptr($view) $ka
 751    lappend vleftptr($view) $b
 752    if {$b != 0} {
 753        lset vbackptr($view) $b $a
 754    }
 755    lappend varctok($view) $tok
 756    lappend varcstart($view) $id
 757    lappend vdownptr($view) 0
 758    lappend varcrow($view) {}
 759    lappend varcix($view) {}
 760    set varccommits($view,$a) {}
 761    lappend vlastins($view) 0
 762    return $a
 763}
 764
 765proc splitvarc {p v} {
 766    global varcid varcstart varccommits varctok vtokmod
 767    global vupptr vdownptr vleftptr vbackptr varcix varcrow vlastins
 768
 769    set oa $varcid($v,$p)
 770    set otok [lindex $varctok($v) $oa]
 771    set ac $varccommits($v,$oa)
 772    set i [lsearch -exact $varccommits($v,$oa) $p]
 773    if {$i <= 0} return
 774    set na [llength $varctok($v)]
 775    # "%" sorts before "0"...
 776    set tok "$otok%[strrep $i]"
 777    lappend varctok($v) $tok
 778    lappend varcrow($v) {}
 779    lappend varcix($v) {}
 780    set varccommits($v,$oa) [lrange $ac 0 [expr {$i - 1}]]
 781    set varccommits($v,$na) [lrange $ac $i end]
 782    lappend varcstart($v) $p
 783    foreach id $varccommits($v,$na) {
 784        set varcid($v,$id) $na
 785    }
 786    lappend vdownptr($v) [lindex $vdownptr($v) $oa]
 787    lappend vlastins($v) [lindex $vlastins($v) $oa]
 788    lset vdownptr($v) $oa $na
 789    lset vlastins($v) $oa 0
 790    lappend vupptr($v) $oa
 791    lappend vleftptr($v) 0
 792    lappend vbackptr($v) 0
 793    for {set b [lindex $vdownptr($v) $na]} {$b != 0} {set b [lindex $vleftptr($v) $b]} {
 794        lset vupptr($v) $b $na
 795    }
 796    if {[string compare $otok $vtokmod($v)] <= 0} {
 797        modify_arc $v $oa
 798    }
 799}
 800
 801proc renumbervarc {a v} {
 802    global parents children varctok varcstart varccommits
 803    global vupptr vdownptr vleftptr vbackptr vlastins varcid vtokmod vdatemode
 804
 805    set t1 [clock clicks -milliseconds]
 806    set todo {}
 807    set isrelated($a) 1
 808    set kidchanged($a) 1
 809    set ntot 0
 810    while {$a != 0} {
 811        if {[info exists isrelated($a)]} {
 812            lappend todo $a
 813            set id [lindex $varccommits($v,$a) end]
 814            foreach p $parents($v,$id) {
 815                if {[info exists varcid($v,$p)]} {
 816                    set isrelated($varcid($v,$p)) 1
 817                }
 818            }
 819        }
 820        incr ntot
 821        set b [lindex $vdownptr($v) $a]
 822        if {$b == 0} {
 823            while {$a != 0} {
 824                set b [lindex $vleftptr($v) $a]
 825                if {$b != 0} break
 826                set a [lindex $vupptr($v) $a]
 827            }
 828        }
 829        set a $b
 830    }
 831    foreach a $todo {
 832        if {![info exists kidchanged($a)]} continue
 833        set id [lindex $varcstart($v) $a]
 834        if {[llength $children($v,$id)] > 1} {
 835            set children($v,$id) [lsort -command [list vtokcmp $v] \
 836                                      $children($v,$id)]
 837        }
 838        set oldtok [lindex $varctok($v) $a]
 839        if {!$vdatemode($v)} {
 840            set tok {}
 841        } else {
 842            set tok $oldtok
 843        }
 844        set ka 0
 845        set kid [last_real_child $v,$id]
 846        if {$kid ne {}} {
 847            set k $varcid($v,$kid)
 848            if {[string compare [lindex $varctok($v) $k] $tok] > 0} {
 849                set ki $kid
 850                set ka $k
 851                set tok [lindex $varctok($v) $k]
 852            }
 853        }
 854        if {$ka != 0} {
 855            set i [lsearch -exact $parents($v,$ki) $id]
 856            set j [expr {[llength $parents($v,$ki)] - 1 - $i}]
 857            append tok [strrep $j]
 858        }
 859        if {$tok eq $oldtok} {
 860            continue
 861        }
 862        set id [lindex $varccommits($v,$a) end]
 863        foreach p $parents($v,$id) {
 864            if {[info exists varcid($v,$p)]} {
 865                set kidchanged($varcid($v,$p)) 1
 866            } else {
 867                set sortkids($p) 1
 868            }
 869        }
 870        lset varctok($v) $a $tok
 871        set b [lindex $vupptr($v) $a]
 872        if {$b != $ka} {
 873            if {[string compare [lindex $varctok($v) $ka] $vtokmod($v)] < 0} {
 874                modify_arc $v $ka
 875            }
 876            if {[string compare [lindex $varctok($v) $b] $vtokmod($v)] < 0} {
 877                modify_arc $v $b
 878            }
 879            set c [lindex $vbackptr($v) $a]
 880            set d [lindex $vleftptr($v) $a]
 881            if {$c == 0} {
 882                lset vdownptr($v) $b $d
 883            } else {
 884                lset vleftptr($v) $c $d
 885            }
 886            if {$d != 0} {
 887                lset vbackptr($v) $d $c
 888            }
 889            if {[lindex $vlastins($v) $b] == $a} {
 890                lset vlastins($v) $b $c
 891            }
 892            lset vupptr($v) $a $ka
 893            set c [lindex $vlastins($v) $ka]
 894            if {$c == 0 || \
 895                    [string compare $tok [lindex $varctok($v) $c]] < 0} {
 896                set c $ka
 897                set b [lindex $vdownptr($v) $ka]
 898            } else {
 899                set b [lindex $vleftptr($v) $c]
 900            }
 901            while {$b != 0 && \
 902                      [string compare $tok [lindex $varctok($v) $b]] >= 0} {
 903                set c $b
 904                set b [lindex $vleftptr($v) $c]
 905            }
 906            if {$c == $ka} {
 907                lset vdownptr($v) $ka $a
 908                lset vbackptr($v) $a 0
 909            } else {
 910                lset vleftptr($v) $c $a
 911                lset vbackptr($v) $a $c
 912            }
 913            lset vleftptr($v) $a $b
 914            if {$b != 0} {
 915                lset vbackptr($v) $b $a
 916            }
 917            lset vlastins($v) $ka $a
 918        }
 919    }
 920    foreach id [array names sortkids] {
 921        if {[llength $children($v,$id)] > 1} {
 922            set children($v,$id) [lsort -command [list vtokcmp $v] \
 923                                      $children($v,$id)]
 924        }
 925    }
 926    set t2 [clock clicks -milliseconds]
 927    #puts "renumbervarc did [llength $todo] of $ntot arcs in [expr {$t2-$t1}]ms"
 928}
 929
 930# Fix up the graph after we have found out that in view $v,
 931# $p (a commit that we have already seen) is actually the parent
 932# of the last commit in arc $a.
 933proc fix_reversal {p a v} {
 934    global varcid varcstart varctok vupptr
 935
 936    set pa $varcid($v,$p)
 937    if {$p ne [lindex $varcstart($v) $pa]} {
 938        splitvarc $p $v
 939        set pa $varcid($v,$p)
 940    }
 941    # seeds always need to be renumbered
 942    if {[lindex $vupptr($v) $pa] == 0 ||
 943        [string compare [lindex $varctok($v) $a] \
 944             [lindex $varctok($v) $pa]] > 0} {
 945        renumbervarc $pa $v
 946    }
 947}
 948
 949proc insertrow {id p v} {
 950    global cmitlisted children parents varcid varctok vtokmod
 951    global varccommits ordertok commitidx numcommits curview
 952    global targetid targetrow vshortids
 953
 954    readcommit $id
 955    set vid $v,$id
 956    set cmitlisted($vid) 1
 957    set children($vid) {}
 958    set parents($vid) [list $p]
 959    set a [newvarc $v $id]
 960    set varcid($vid) $a
 961    lappend vshortids($v,[string range $id 0 3]) $id
 962    if {[string compare [lindex $varctok($v) $a] $vtokmod($v)] < 0} {
 963        modify_arc $v $a
 964    }
 965    lappend varccommits($v,$a) $id
 966    set vp $v,$p
 967    if {[llength [lappend children($vp) $id]] > 1} {
 968        set children($vp) [lsort -command [list vtokcmp $v] $children($vp)]
 969        unset -nocomplain ordertok
 970    }
 971    fix_reversal $p $a $v
 972    incr commitidx($v)
 973    if {$v == $curview} {
 974        set numcommits $commitidx($v)
 975        setcanvscroll
 976        if {[info exists targetid]} {
 977            if {![comes_before $targetid $p]} {
 978                incr targetrow
 979            }
 980        }
 981    }
 982}
 983
 984proc insertfakerow {id p} {
 985    global varcid varccommits parents children cmitlisted
 986    global commitidx varctok vtokmod targetid targetrow curview numcommits
 987
 988    set v $curview
 989    set a $varcid($v,$p)
 990    set i [lsearch -exact $varccommits($v,$a) $p]
 991    if {$i < 0} {
 992        puts "oops: insertfakerow can't find [shortids $p] on arc $a"
 993        return
 994    }
 995    set children($v,$id) {}
 996    set parents($v,$id) [list $p]
 997    set varcid($v,$id) $a
 998    lappend children($v,$p) $id
 999    set cmitlisted($v,$id) 1
1000    set numcommits [incr commitidx($v)]
1001    # note we deliberately don't update varcstart($v) even if $i == 0
1002    set varccommits($v,$a) [linsert $varccommits($v,$a) $i $id]
1003    modify_arc $v $a $i
1004    if {[info exists targetid]} {
1005        if {![comes_before $targetid $p]} {
1006            incr targetrow
1007        }
1008    }
1009    setcanvscroll
1010    drawvisible
1011}
1012
1013proc removefakerow {id} {
1014    global varcid varccommits parents children commitidx
1015    global varctok vtokmod cmitlisted currentid selectedline
1016    global targetid curview numcommits
1017
1018    set v $curview
1019    if {[llength $parents($v,$id)] != 1} {
1020        puts "oops: removefakerow [shortids $id] has [llength $parents($v,$id)] parents"
1021        return
1022    }
1023    set p [lindex $parents($v,$id) 0]
1024    set a $varcid($v,$id)
1025    set i [lsearch -exact $varccommits($v,$a) $id]
1026    if {$i < 0} {
1027        puts "oops: removefakerow can't find [shortids $id] on arc $a"
1028        return
1029    }
1030    unset varcid($v,$id)
1031    set varccommits($v,$a) [lreplace $varccommits($v,$a) $i $i]
1032    unset parents($v,$id)
1033    unset children($v,$id)
1034    unset cmitlisted($v,$id)
1035    set numcommits [incr commitidx($v) -1]
1036    set j [lsearch -exact $children($v,$p) $id]
1037    if {$j >= 0} {
1038        set children($v,$p) [lreplace $children($v,$p) $j $j]
1039    }
1040    modify_arc $v $a $i
1041    if {[info exist currentid] && $id eq $currentid} {
1042        unset currentid
1043        set selectedline {}
1044    }
1045    if {[info exists targetid] && $targetid eq $id} {
1046        set targetid $p
1047    }
1048    setcanvscroll
1049    drawvisible
1050}
1051
1052proc real_children {vp} {
1053    global children nullid nullid2
1054
1055    set kids {}
1056    foreach id $children($vp) {
1057        if {$id ne $nullid && $id ne $nullid2} {
1058            lappend kids $id
1059        }
1060    }
1061    return $kids
1062}
1063
1064proc first_real_child {vp} {
1065    global children nullid nullid2
1066
1067    foreach id $children($vp) {
1068        if {$id ne $nullid && $id ne $nullid2} {
1069            return $id
1070        }
1071    }
1072    return {}
1073}
1074
1075proc last_real_child {vp} {
1076    global children nullid nullid2
1077
1078    set kids $children($vp)
1079    for {set i [llength $kids]} {[incr i -1] >= 0} {} {
1080        set id [lindex $kids $i]
1081        if {$id ne $nullid && $id ne $nullid2} {
1082            return $id
1083        }
1084    }
1085    return {}
1086}
1087
1088proc vtokcmp {v a b} {
1089    global varctok varcid
1090
1091    return [string compare [lindex $varctok($v) $varcid($v,$a)] \
1092                [lindex $varctok($v) $varcid($v,$b)]]
1093}
1094
1095# This assumes that if lim is not given, the caller has checked that
1096# arc a's token is less than $vtokmod($v)
1097proc modify_arc {v a {lim {}}} {
1098    global varctok vtokmod varcmod varcrow vupptr curview vrowmod varccommits
1099
1100    if {$lim ne {}} {
1101        set c [string compare [lindex $varctok($v) $a] $vtokmod($v)]
1102        if {$c > 0} return
1103        if {$c == 0} {
1104            set r [lindex $varcrow($v) $a]
1105            if {$r ne {} && $vrowmod($v) <= $r + $lim} return
1106        }
1107    }
1108    set vtokmod($v) [lindex $varctok($v) $a]
1109    set varcmod($v) $a
1110    if {$v == $curview} {
1111        while {$a != 0 && [lindex $varcrow($v) $a] eq {}} {
1112            set a [lindex $vupptr($v) $a]
1113            set lim {}
1114        }
1115        set r 0
1116        if {$a != 0} {
1117            if {$lim eq {}} {
1118                set lim [llength $varccommits($v,$a)]
1119            }
1120            set r [expr {[lindex $varcrow($v) $a] + $lim}]
1121        }
1122        set vrowmod($v) $r
1123        undolayout $r
1124    }
1125}
1126
1127proc update_arcrows {v} {
1128    global vtokmod varcmod vrowmod varcrow commitidx currentid selectedline
1129    global varcid vrownum varcorder varcix varccommits
1130    global vupptr vdownptr vleftptr varctok
1131    global displayorder parentlist curview cached_commitrow
1132
1133    if {$vrowmod($v) == $commitidx($v)} return
1134    if {$v == $curview} {
1135        if {[llength $displayorder] > $vrowmod($v)} {
1136            set displayorder [lrange $displayorder 0 [expr {$vrowmod($v) - 1}]]
1137            set parentlist [lrange $parentlist 0 [expr {$vrowmod($v) - 1}]]
1138        }
1139        unset -nocomplain cached_commitrow
1140    }
1141    set narctot [expr {[llength $varctok($v)] - 1}]
1142    set a $varcmod($v)
1143    while {$a != 0 && [lindex $varcix($v) $a] eq {}} {
1144        # go up the tree until we find something that has a row number,
1145        # or we get to a seed
1146        set a [lindex $vupptr($v) $a]
1147    }
1148    if {$a == 0} {
1149        set a [lindex $vdownptr($v) 0]
1150        if {$a == 0} return
1151        set vrownum($v) {0}
1152        set varcorder($v) [list $a]
1153        lset varcix($v) $a 0
1154        lset varcrow($v) $a 0
1155        set arcn 0
1156        set row 0
1157    } else {
1158        set arcn [lindex $varcix($v) $a]
1159        if {[llength $vrownum($v)] > $arcn + 1} {
1160            set vrownum($v) [lrange $vrownum($v) 0 $arcn]
1161            set varcorder($v) [lrange $varcorder($v) 0 $arcn]
1162        }
1163        set row [lindex $varcrow($v) $a]
1164    }
1165    while {1} {
1166        set p $a
1167        incr row [llength $varccommits($v,$a)]
1168        # go down if possible
1169        set b [lindex $vdownptr($v) $a]
1170        if {$b == 0} {
1171            # if not, go left, or go up until we can go left
1172            while {$a != 0} {
1173                set b [lindex $vleftptr($v) $a]
1174                if {$b != 0} break
1175                set a [lindex $vupptr($v) $a]
1176            }
1177            if {$a == 0} break
1178        }
1179        set a $b
1180        incr arcn
1181        lappend vrownum($v) $row
1182        lappend varcorder($v) $a
1183        lset varcix($v) $a $arcn
1184        lset varcrow($v) $a $row
1185    }
1186    set vtokmod($v) [lindex $varctok($v) $p]
1187    set varcmod($v) $p
1188    set vrowmod($v) $row
1189    if {[info exists currentid]} {
1190        set selectedline [rowofcommit $currentid]
1191    }
1192}
1193
1194# Test whether view $v contains commit $id
1195proc commitinview {id v} {
1196    global varcid
1197
1198    return [info exists varcid($v,$id)]
1199}
1200
1201# Return the row number for commit $id in the current view
1202proc rowofcommit {id} {
1203    global varcid varccommits varcrow curview cached_commitrow
1204    global varctok vtokmod
1205
1206    set v $curview
1207    if {![info exists varcid($v,$id)]} {
1208        puts "oops rowofcommit no arc for [shortids $id]"
1209        return {}
1210    }
1211    set a $varcid($v,$id)
1212    if {[string compare [lindex $varctok($v) $a] $vtokmod($v)] >= 0} {
1213        update_arcrows $v
1214    }
1215    if {[info exists cached_commitrow($id)]} {
1216        return $cached_commitrow($id)
1217    }
1218    set i [lsearch -exact $varccommits($v,$a) $id]
1219    if {$i < 0} {
1220        puts "oops didn't find commit [shortids $id] in arc $a"
1221        return {}
1222    }
1223    incr i [lindex $varcrow($v) $a]
1224    set cached_commitrow($id) $i
1225    return $i
1226}
1227
1228# Returns 1 if a is on an earlier row than b, otherwise 0
1229proc comes_before {a b} {
1230    global varcid varctok curview
1231
1232    set v $curview
1233    if {$a eq $b || ![info exists varcid($v,$a)] || \
1234            ![info exists varcid($v,$b)]} {
1235        return 0
1236    }
1237    if {$varcid($v,$a) != $varcid($v,$b)} {
1238        return [expr {[string compare [lindex $varctok($v) $varcid($v,$a)] \
1239                           [lindex $varctok($v) $varcid($v,$b)]] < 0}]
1240    }
1241    return [expr {[rowofcommit $a] < [rowofcommit $b]}]
1242}
1243
1244proc bsearch {l elt} {
1245    if {[llength $l] == 0 || $elt <= [lindex $l 0]} {
1246        return 0
1247    }
1248    set lo 0
1249    set hi [llength $l]
1250    while {$hi - $lo > 1} {
1251        set mid [expr {int(($lo + $hi) / 2)}]
1252        set t [lindex $l $mid]
1253        if {$elt < $t} {
1254            set hi $mid
1255        } elseif {$elt > $t} {
1256            set lo $mid
1257        } else {
1258            return $mid
1259        }
1260    }
1261    return $lo
1262}
1263
1264# Make sure rows $start..$end-1 are valid in displayorder and parentlist
1265proc make_disporder {start end} {
1266    global vrownum curview commitidx displayorder parentlist
1267    global varccommits varcorder parents vrowmod varcrow
1268    global d_valid_start d_valid_end
1269
1270    if {$end > $vrowmod($curview)} {
1271        update_arcrows $curview
1272    }
1273    set ai [bsearch $vrownum($curview) $start]
1274    set start [lindex $vrownum($curview) $ai]
1275    set narc [llength $vrownum($curview)]
1276    for {set r $start} {$ai < $narc && $r < $end} {incr ai} {
1277        set a [lindex $varcorder($curview) $ai]
1278        set l [llength $displayorder]
1279        set al [llength $varccommits($curview,$a)]
1280        if {$l < $r + $al} {
1281            if {$l < $r} {
1282                set pad [ntimes [expr {$r - $l}] {}]
1283                set displayorder [concat $displayorder $pad]
1284                set parentlist [concat $parentlist $pad]
1285            } elseif {$l > $r} {
1286                set displayorder [lrange $displayorder 0 [expr {$r - 1}]]
1287                set parentlist [lrange $parentlist 0 [expr {$r - 1}]]
1288            }
1289            foreach id $varccommits($curview,$a) {
1290                lappend displayorder $id
1291                lappend parentlist $parents($curview,$id)
1292            }
1293        } elseif {[lindex $displayorder [expr {$r + $al - 1}]] eq {}} {
1294            set i $r
1295            foreach id $varccommits($curview,$a) {
1296                lset displayorder $i $id
1297                lset parentlist $i $parents($curview,$id)
1298                incr i
1299            }
1300        }
1301        incr r $al
1302    }
1303}
1304
1305proc commitonrow {row} {
1306    global displayorder
1307
1308    set id [lindex $displayorder $row]
1309    if {$id eq {}} {
1310        make_disporder $row [expr {$row + 1}]
1311        set id [lindex $displayorder $row]
1312    }
1313    return $id
1314}
1315
1316proc closevarcs {v} {
1317    global varctok varccommits varcid parents children
1318    global cmitlisted commitidx vtokmod
1319
1320    set missing_parents 0
1321    set scripts {}
1322    set narcs [llength $varctok($v)]
1323    for {set a 1} {$a < $narcs} {incr a} {
1324        set id [lindex $varccommits($v,$a) end]
1325        foreach p $parents($v,$id) {
1326            if {[info exists varcid($v,$p)]} continue
1327            # add p as a new commit
1328            incr missing_parents
1329            set cmitlisted($v,$p) 0
1330            set parents($v,$p) {}
1331            if {[llength $children($v,$p)] == 1 &&
1332                [llength $parents($v,$id)] == 1} {
1333                set b $a
1334            } else {
1335                set b [newvarc $v $p]
1336            }
1337            set varcid($v,$p) $b
1338            if {[string compare [lindex $varctok($v) $b] $vtokmod($v)] < 0} {
1339                modify_arc $v $b
1340            }
1341            lappend varccommits($v,$b) $p
1342            incr commitidx($v)
1343            set scripts [check_interest $p $scripts]
1344        }
1345    }
1346    if {$missing_parents > 0} {
1347        foreach s $scripts {
1348            eval $s
1349        }
1350    }
1351}
1352
1353# Use $rwid as a substitute for $id, i.e. reparent $id's children to $rwid
1354# Assumes we already have an arc for $rwid.
1355proc rewrite_commit {v id rwid} {
1356    global children parents varcid varctok vtokmod varccommits
1357
1358    foreach ch $children($v,$id) {
1359        # make $rwid be $ch's parent in place of $id
1360        set i [lsearch -exact $parents($v,$ch) $id]
1361        if {$i < 0} {
1362            puts "oops rewrite_commit didn't find $id in parent list for $ch"
1363        }
1364        set parents($v,$ch) [lreplace $parents($v,$ch) $i $i $rwid]
1365        # add $ch to $rwid's children and sort the list if necessary
1366        if {[llength [lappend children($v,$rwid) $ch]] > 1} {
1367            set children($v,$rwid) [lsort -command [list vtokcmp $v] \
1368                                        $children($v,$rwid)]
1369        }
1370        # fix the graph after joining $id to $rwid
1371        set a $varcid($v,$ch)
1372        fix_reversal $rwid $a $v
1373        # parentlist is wrong for the last element of arc $a
1374        # even if displayorder is right, hence the 3rd arg here
1375        modify_arc $v $a [expr {[llength $varccommits($v,$a)] - 1}]
1376    }
1377}
1378
1379# Mechanism for registering a command to be executed when we come
1380# across a particular commit.  To handle the case when only the
1381# prefix of the commit is known, the commitinterest array is now
1382# indexed by the first 4 characters of the ID.  Each element is a
1383# list of id, cmd pairs.
1384proc interestedin {id cmd} {
1385    global commitinterest
1386
1387    lappend commitinterest([string range $id 0 3]) $id $cmd
1388}
1389
1390proc check_interest {id scripts} {
1391    global commitinterest
1392
1393    set prefix [string range $id 0 3]
1394    if {[info exists commitinterest($prefix)]} {
1395        set newlist {}
1396        foreach {i script} $commitinterest($prefix) {
1397            if {[string match "$i*" $id]} {
1398                lappend scripts [string map [list "%I" $id "%P" $i] $script]
1399            } else {
1400                lappend newlist $i $script
1401            }
1402        }
1403        if {$newlist ne {}} {
1404            set commitinterest($prefix) $newlist
1405        } else {
1406            unset commitinterest($prefix)
1407        }
1408    }
1409    return $scripts
1410}
1411
1412proc getcommitlines {fd inst view updating}  {
1413    global cmitlisted leftover
1414    global commitidx commitdata vdatemode
1415    global parents children curview hlview
1416    global idpending ordertok
1417    global varccommits varcid varctok vtokmod vfilelimit vshortids
1418
1419    set stuff [read $fd 500000]
1420    # git log doesn't terminate the last commit with a null...
1421    if {$stuff == {} && $leftover($inst) ne {} && [eof $fd]} {
1422        set stuff "\0"
1423    }
1424    if {$stuff == {}} {
1425        if {![eof $fd]} {
1426            return 1
1427        }
1428        global commfd viewcomplete viewactive viewname
1429        global viewinstances
1430        unset commfd($inst)
1431        set i [lsearch -exact $viewinstances($view) $inst]
1432        if {$i >= 0} {
1433            set viewinstances($view) [lreplace $viewinstances($view) $i $i]
1434        }
1435        # set it blocking so we wait for the process to terminate
1436        fconfigure $fd -blocking 1
1437        if {[catch {close $fd} err]} {
1438            set fv {}
1439            if {$view != $curview} {
1440                set fv " for the \"$viewname($view)\" view"
1441            }
1442            if {[string range $err 0 4] == "usage"} {
1443                set err "Gitk: error reading commits$fv:\
1444                        bad arguments to git log."
1445                if {$viewname($view) eq [mc "Command line"]} {
1446                    append err \
1447                        "  (Note: arguments to gitk are passed to git log\
1448                         to allow selection of commits to be displayed.)"
1449                }
1450            } else {
1451                set err "Error reading commits$fv: $err"
1452            }
1453            error_popup $err
1454        }
1455        if {[incr viewactive($view) -1] <= 0} {
1456            set viewcomplete($view) 1
1457            # Check if we have seen any ids listed as parents that haven't
1458            # appeared in the list
1459            closevarcs $view
1460            notbusy $view
1461        }
1462        if {$view == $curview} {
1463            run chewcommits
1464        }
1465        return 0
1466    }
1467    set start 0
1468    set gotsome 0
1469    set scripts {}
1470    while 1 {
1471        set i [string first "\0" $stuff $start]
1472        if {$i < 0} {
1473            append leftover($inst) [string range $stuff $start end]
1474            break
1475        }
1476        if {$start == 0} {
1477            set cmit $leftover($inst)
1478            append cmit [string range $stuff 0 [expr {$i - 1}]]
1479            set leftover($inst) {}
1480        } else {
1481            set cmit [string range $stuff $start [expr {$i - 1}]]
1482        }
1483        set start [expr {$i + 1}]
1484        set j [string first "\n" $cmit]
1485        set ok 0
1486        set listed 1
1487        if {$j >= 0 && [string match "commit *" $cmit]} {
1488            set ids [string range $cmit 7 [expr {$j - 1}]]
1489            if {[string match {[-^<>]*} $ids]} {
1490                switch -- [string index $ids 0] {
1491                    "-" {set listed 0}
1492                    "^" {set listed 2}
1493                    "<" {set listed 3}
1494                    ">" {set listed 4}
1495                }
1496                set ids [string range $ids 1 end]
1497            }
1498            set ok 1
1499            foreach id $ids {
1500                if {[string length $id] != 40} {
1501                    set ok 0
1502                    break
1503                }
1504            }
1505        }
1506        if {!$ok} {
1507            set shortcmit $cmit
1508            if {[string length $shortcmit] > 80} {
1509                set shortcmit "[string range $shortcmit 0 80]..."
1510            }
1511            error_popup "[mc "Can't parse git log output:"] {$shortcmit}"
1512            exit 1
1513        }
1514        set id [lindex $ids 0]
1515        set vid $view,$id
1516
1517        lappend vshortids($view,[string range $id 0 3]) $id
1518
1519        if {!$listed && $updating && ![info exists varcid($vid)] &&
1520            $vfilelimit($view) ne {}} {
1521            # git log doesn't rewrite parents for unlisted commits
1522            # when doing path limiting, so work around that here
1523            # by working out the rewritten parent with git rev-list
1524            # and if we already know about it, using the rewritten
1525            # parent as a substitute parent for $id's children.
1526            if {![catch {
1527                set rwid [exec git rev-list --first-parent --max-count=1 \
1528                              $id -- $vfilelimit($view)]
1529            }]} {
1530                if {$rwid ne {} && [info exists varcid($view,$rwid)]} {
1531                    # use $rwid in place of $id
1532                    rewrite_commit $view $id $rwid
1533                    continue
1534                }
1535            }
1536        }
1537
1538        set a 0
1539        if {[info exists varcid($vid)]} {
1540            if {$cmitlisted($vid) || !$listed} continue
1541            set a $varcid($vid)
1542        }
1543        if {$listed} {
1544            set olds [lrange $ids 1 end]
1545        } else {
1546            set olds {}
1547        }
1548        set commitdata($id) [string range $cmit [expr {$j + 1}] end]
1549        set cmitlisted($vid) $listed
1550        set parents($vid) $olds
1551        if {![info exists children($vid)]} {
1552            set children($vid) {}
1553        } elseif {$a == 0 && [llength $children($vid)] == 1} {
1554            set k [lindex $children($vid) 0]
1555            if {[llength $parents($view,$k)] == 1 &&
1556                (!$vdatemode($view) ||
1557                 $varcid($view,$k) == [llength $varctok($view)] - 1)} {
1558                set a $varcid($view,$k)
1559            }
1560        }
1561        if {$a == 0} {
1562            # new arc
1563            set a [newvarc $view $id]
1564        }
1565        if {[string compare [lindex $varctok($view) $a] $vtokmod($view)] < 0} {
1566            modify_arc $view $a
1567        }
1568        if {![info exists varcid($vid)]} {
1569            set varcid($vid) $a
1570            lappend varccommits($view,$a) $id
1571            incr commitidx($view)
1572        }
1573
1574        set i 0
1575        foreach p $olds {
1576            if {$i == 0 || [lsearch -exact $olds $p] >= $i} {
1577                set vp $view,$p
1578                if {[llength [lappend children($vp) $id]] > 1 &&
1579                    [vtokcmp $view [lindex $children($vp) end-1] $id] > 0} {
1580                    set children($vp) [lsort -command [list vtokcmp $view] \
1581                                           $children($vp)]
1582                    unset -nocomplain ordertok
1583                }
1584                if {[info exists varcid($view,$p)]} {
1585                    fix_reversal $p $a $view
1586                }
1587            }
1588            incr i
1589        }
1590
1591        set scripts [check_interest $id $scripts]
1592        set gotsome 1
1593    }
1594    if {$gotsome} {
1595        global numcommits hlview
1596
1597        if {$view == $curview} {
1598            set numcommits $commitidx($view)
1599            run chewcommits
1600        }
1601        if {[info exists hlview] && $view == $hlview} {
1602            # we never actually get here...
1603            run vhighlightmore
1604        }
1605        foreach s $scripts {
1606            eval $s
1607        }
1608    }
1609    return 2
1610}
1611
1612proc chewcommits {} {
1613    global curview hlview viewcomplete
1614    global pending_select
1615
1616    layoutmore
1617    if {$viewcomplete($curview)} {
1618        global commitidx varctok
1619        global numcommits startmsecs
1620
1621        if {[info exists pending_select]} {
1622            update
1623            reset_pending_select {}
1624
1625            if {[commitinview $pending_select $curview]} {
1626                selectline [rowofcommit $pending_select] 1
1627            } else {
1628                set row [first_real_row]
1629                selectline $row 1
1630            }
1631        }
1632        if {$commitidx($curview) > 0} {
1633            #set ms [expr {[clock clicks -milliseconds] - $startmsecs}]
1634            #puts "overall $ms ms for $numcommits commits"
1635            #puts "[llength $varctok($view)] arcs, $commitidx($view) commits"
1636        } else {
1637            show_status [mc "No commits selected"]
1638        }
1639        notbusy layout
1640    }
1641    return 0
1642}
1643
1644proc do_readcommit {id} {
1645    global tclencoding
1646
1647    # Invoke git-log to handle automatic encoding conversion
1648    set fd [open [concat | git log --no-color --pretty=raw -1 $id] r]
1649    # Read the results using i18n.logoutputencoding
1650    fconfigure $fd -translation lf -eofchar {}
1651    if {$tclencoding != {}} {
1652        fconfigure $fd -encoding $tclencoding
1653    }
1654    set contents [read $fd]
1655    close $fd
1656    # Remove the heading line
1657    regsub {^commit [0-9a-f]+\n} $contents {} contents
1658
1659    return $contents
1660}
1661
1662proc readcommit {id} {
1663    if {[catch {set contents [do_readcommit $id]}]} return
1664    parsecommit $id $contents 1
1665}
1666
1667proc parsecommit {id contents listed} {
1668    global commitinfo
1669
1670    set inhdr 1
1671    set comment {}
1672    set headline {}
1673    set auname {}
1674    set audate {}
1675    set comname {}
1676    set comdate {}
1677    set hdrend [string first "\n\n" $contents]
1678    if {$hdrend < 0} {
1679        # should never happen...
1680        set hdrend [string length $contents]
1681    }
1682    set header [string range $contents 0 [expr {$hdrend - 1}]]
1683    set comment [string range $contents [expr {$hdrend + 2}] end]
1684    foreach line [split $header "\n"] {
1685        set line [split $line " "]
1686        set tag [lindex $line 0]
1687        if {$tag == "author"} {
1688            set audate [lrange $line end-1 end]
1689            set auname [join [lrange $line 1 end-2] " "]
1690        } elseif {$tag == "committer"} {
1691            set comdate [lrange $line end-1 end]
1692            set comname [join [lrange $line 1 end-2] " "]
1693        }
1694    }
1695    set headline {}
1696    # take the first non-blank line of the comment as the headline
1697    set headline [string trimleft $comment]
1698    set i [string first "\n" $headline]
1699    if {$i >= 0} {
1700        set headline [string range $headline 0 $i]
1701    }
1702    set headline [string trimright $headline]
1703    set i [string first "\r" $headline]
1704    if {$i >= 0} {
1705        set headline [string trimright [string range $headline 0 $i]]
1706    }
1707    if {!$listed} {
1708        # git log indents the comment by 4 spaces;
1709        # if we got this via git cat-file, add the indentation
1710        set newcomment {}
1711        foreach line [split $comment "\n"] {
1712            append newcomment "    "
1713            append newcomment $line
1714            append newcomment "\n"
1715        }
1716        set comment $newcomment
1717    }
1718    set hasnote [string first "\nNotes:\n" $contents]
1719    set diff ""
1720    # If there is diff output shown in the git-log stream, split it
1721    # out.  But get rid of the empty line that always precedes the
1722    # diff.
1723    set i [string first "\n\ndiff" $comment]
1724    if {$i >= 0} {
1725        set diff [string range $comment $i+1 end]
1726        set comment [string range $comment 0 $i-1]
1727    }
1728    set commitinfo($id) [list $headline $auname $audate \
1729                             $comname $comdate $comment $hasnote $diff]
1730}
1731
1732proc getcommit {id} {
1733    global commitdata commitinfo
1734
1735    if {[info exists commitdata($id)]} {
1736        parsecommit $id $commitdata($id) 1
1737    } else {
1738        readcommit $id
1739        if {![info exists commitinfo($id)]} {
1740            set commitinfo($id) [list [mc "No commit information available"]]
1741        }
1742    }
1743    return 1
1744}
1745
1746# Expand an abbreviated commit ID to a list of full 40-char IDs that match
1747# and are present in the current view.
1748# This is fairly slow...
1749proc longid {prefix} {
1750    global varcid curview vshortids
1751
1752    set ids {}
1753    if {[string length $prefix] >= 4} {
1754        set vshortid $curview,[string range $prefix 0 3]
1755        if {[info exists vshortids($vshortid)]} {
1756            foreach id $vshortids($vshortid) {
1757                if {[string match "$prefix*" $id]} {
1758                    if {[lsearch -exact $ids $id] < 0} {
1759                        lappend ids $id
1760                        if {[llength $ids] >= 2} break
1761                    }
1762                }
1763            }
1764        }
1765    } else {
1766        foreach match [array names varcid "$curview,$prefix*"] {
1767            lappend ids [lindex [split $match ","] 1]
1768            if {[llength $ids] >= 2} break
1769        }
1770    }
1771    return $ids
1772}
1773
1774proc readrefs {} {
1775    global tagids idtags headids idheads tagobjid
1776    global otherrefids idotherrefs mainhead mainheadid
1777    global selecthead selectheadid
1778    global hideremotes
1779
1780    foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
1781        unset -nocomplain $v
1782    }
1783    set refd [open [list | git show-ref -d] r]
1784    while {[gets $refd line] >= 0} {
1785        if {[string index $line 40] ne " "} continue
1786        set id [string range $line 0 39]
1787        set ref [string range $line 41 end]
1788        if {![string match "refs/*" $ref]} continue
1789        set name [string range $ref 5 end]
1790        if {[string match "remotes/*" $name]} {
1791            if {![string match "*/HEAD" $name] && !$hideremotes} {
1792                set headids($name) $id
1793                lappend idheads($id) $name
1794            }
1795        } elseif {[string match "heads/*" $name]} {
1796            set name [string range $name 6 end]
1797            set headids($name) $id
1798            lappend idheads($id) $name
1799        } elseif {[string match "tags/*" $name]} {
1800            # this lets refs/tags/foo^{} overwrite refs/tags/foo,
1801            # which is what we want since the former is the commit ID
1802            set name [string range $name 5 end]
1803            if {[string match "*^{}" $name]} {
1804                set name [string range $name 0 end-3]
1805            } else {
1806                set tagobjid($name) $id
1807            }
1808            set tagids($name) $id
1809            lappend idtags($id) $name
1810        } else {
1811            set otherrefids($name) $id
1812            lappend idotherrefs($id) $name
1813        }
1814    }
1815    catch {close $refd}
1816    set mainhead {}
1817    set mainheadid {}
1818    catch {
1819        set mainheadid [exec git rev-parse HEAD]
1820        set thehead [exec git symbolic-ref HEAD]
1821        if {[string match "refs/heads/*" $thehead]} {
1822            set mainhead [string range $thehead 11 end]
1823        }
1824    }
1825    set selectheadid {}
1826    if {$selecthead ne {}} {
1827        catch {
1828            set selectheadid [exec git rev-parse --verify $selecthead]
1829        }
1830    }
1831}
1832
1833# skip over fake commits
1834proc first_real_row {} {
1835    global nullid nullid2 numcommits
1836
1837    for {set row 0} {$row < $numcommits} {incr row} {
1838        set id [commitonrow $row]
1839        if {$id ne $nullid && $id ne $nullid2} {
1840            break
1841        }
1842    }
1843    return $row
1844}
1845
1846# update things for a head moved to a child of its previous location
1847proc movehead {id name} {
1848    global headids idheads
1849
1850    removehead $headids($name) $name
1851    set headids($name) $id
1852    lappend idheads($id) $name
1853}
1854
1855# update things when a head has been removed
1856proc removehead {id name} {
1857    global headids idheads
1858
1859    if {$idheads($id) eq $name} {
1860        unset idheads($id)
1861    } else {
1862        set i [lsearch -exact $idheads($id) $name]
1863        if {$i >= 0} {
1864            set idheads($id) [lreplace $idheads($id) $i $i]
1865        }
1866    }
1867    unset headids($name)
1868}
1869
1870proc ttk_toplevel {w args} {
1871    global use_ttk
1872    eval [linsert $args 0 ::toplevel $w]
1873    if {$use_ttk} {
1874        place [ttk::frame $w._toplevel_background] -x 0 -y 0 -relwidth 1 -relheight 1
1875    }
1876    return $w
1877}
1878
1879proc make_transient {window origin} {
1880    global have_tk85
1881
1882    # In MacOS Tk 8.4 transient appears to work by setting
1883    # overrideredirect, which is utterly useless, since the
1884    # windows get no border, and are not even kept above
1885    # the parent.
1886    if {!$have_tk85 && [tk windowingsystem] eq {aqua}} return
1887
1888    wm transient $window $origin
1889
1890    # Windows fails to place transient windows normally, so
1891    # schedule a callback to center them on the parent.
1892    if {[tk windowingsystem] eq {win32}} {
1893        after idle [list tk::PlaceWindow $window widget $origin]
1894    }
1895}
1896
1897proc show_error {w top msg} {
1898    global NS
1899    if {![info exists NS]} {set NS ""}
1900    if {[wm state $top] eq "withdrawn"} { wm deiconify $top }
1901    message $w.m -text $msg -justify center -aspect 400
1902    pack $w.m -side top -fill x -padx 20 -pady 20
1903    ${NS}::button $w.ok -default active -text [mc OK] -command "destroy $top"
1904    pack $w.ok -side bottom -fill x
1905    bind $top <Visibility> "grab $top; focus $top"
1906    bind $top <Key-Return> "destroy $top"
1907    bind $top <Key-space>  "destroy $top"
1908    bind $top <Key-Escape> "destroy $top"
1909    tkwait window $top
1910}
1911
1912proc error_popup {msg {owner .}} {
1913    if {[tk windowingsystem] eq "win32"} {
1914        tk_messageBox -icon error -type ok -title [wm title .] \
1915            -parent $owner -message $msg
1916    } else {
1917        set w .error
1918        ttk_toplevel $w
1919        make_transient $w $owner
1920        show_error $w $w $msg
1921    }
1922}
1923
1924proc confirm_popup {msg {owner .}} {
1925    global confirm_ok NS
1926    set confirm_ok 0
1927    set w .confirm
1928    ttk_toplevel $w
1929    make_transient $w $owner
1930    message $w.m -text $msg -justify center -aspect 400
1931    pack $w.m -side top -fill x -padx 20 -pady 20
1932    ${NS}::button $w.ok -text [mc OK] -command "set confirm_ok 1; destroy $w"
1933    pack $w.ok -side left -fill x
1934    ${NS}::button $w.cancel -text [mc Cancel] -command "destroy $w"
1935    pack $w.cancel -side right -fill x
1936    bind $w <Visibility> "grab $w; focus $w"
1937    bind $w <Key-Return> "set confirm_ok 1; destroy $w"
1938    bind $w <Key-space>  "set confirm_ok 1; destroy $w"
1939    bind $w <Key-Escape> "destroy $w"
1940    tk::PlaceWindow $w widget $owner
1941    tkwait window $w
1942    return $confirm_ok
1943}
1944
1945proc setoptions {} {
1946    global use_ttk
1947
1948    if {[tk windowingsystem] ne "win32"} {
1949        option add *Panedwindow.showHandle 1 startupFile
1950        option add *Panedwindow.sashRelief raised startupFile
1951        if {[tk windowingsystem] ne "aqua"} {
1952            option add *Menu.font uifont startupFile
1953        }
1954    } else {
1955        option add *Menu.TearOff 0 startupFile
1956    }
1957    option add *Button.font uifont startupFile
1958    option add *Checkbutton.font uifont startupFile
1959    option add *Radiobutton.font uifont startupFile
1960    option add *Menubutton.font uifont startupFile
1961    option add *Label.font uifont startupFile
1962    option add *Message.font uifont startupFile
1963    option add *Entry.font textfont startupFile
1964    option add *Text.font textfont startupFile
1965    option add *Labelframe.font uifont startupFile
1966    option add *Spinbox.font textfont startupFile
1967    option add *Listbox.font mainfont startupFile
1968}
1969
1970proc setttkstyle {} {
1971    eval font configure TkDefaultFont [fontflags mainfont]
1972    eval font configure TkTextFont [fontflags textfont]
1973    eval font configure TkHeadingFont [fontflags mainfont]
1974    eval font configure TkCaptionFont [fontflags mainfont] -weight bold
1975    eval font configure TkTooltipFont [fontflags uifont]
1976    eval font configure TkFixedFont   [fontflags textfont]
1977    eval font configure TkIconFont    [fontflags uifont]
1978    eval font configure TkMenuFont    [fontflags uifont]
1979    eval font configure TkSmallCaptionFont [fontflags uifont]
1980}
1981
1982# Make a menu and submenus.
1983# m is the window name for the menu, items is the list of menu items to add.
1984# Each item is a list {mc label type description options...}
1985# mc is ignored; it's so we can put mc there to alert xgettext
1986# label is the string that appears in the menu
1987# type is cascade, command or radiobutton (should add checkbutton)
1988# description depends on type; it's the sublist for cascade, the
1989# command to invoke for command, or {variable value} for radiobutton
1990proc makemenu {m items} {
1991    menu $m
1992    if {[tk windowingsystem] eq {aqua}} {
1993        set Meta1 Cmd
1994    } else {
1995        set Meta1 Ctrl
1996    }
1997    foreach i $items {
1998        set name [mc [lindex $i 1]]
1999        set type [lindex $i 2]
2000        set thing [lindex $i 3]
2001        set params [list $type]
2002        if {$name ne {}} {
2003            set u [string first "&" [string map {&& x} $name]]
2004            lappend params -label [string map {&& & & {}} $name]
2005            if {$u >= 0} {
2006                lappend params -underline $u
2007            }
2008        }
2009        switch -- $type {
2010            "cascade" {
2011                set submenu [string tolower [string map {& ""} [lindex $i 1]]]
2012                lappend params -menu $m.$submenu
2013            }
2014            "command" {
2015                lappend params -command $thing
2016            }
2017            "radiobutton" {
2018                lappend params -variable [lindex $thing 0] \
2019                    -value [lindex $thing 1]
2020            }
2021        }
2022        set tail [lrange $i 4 end]
2023        regsub -all {\yMeta1\y} $tail $Meta1 tail
2024        eval $m add $params $tail
2025        if {$type eq "cascade"} {
2026            makemenu $m.$submenu $thing
2027        }
2028    }
2029}
2030
2031# translate string and remove ampersands
2032proc mca {str} {
2033    return [string map {&& & & {}} [mc $str]]
2034}
2035
2036proc cleardropsel {w} {
2037    $w selection clear
2038}
2039proc makedroplist {w varname args} {
2040    global use_ttk
2041    if {$use_ttk} {
2042        set width 0
2043        foreach label $args {
2044            set cx [string length $label]
2045            if {$cx > $width} {set width $cx}
2046        }
2047        set gm [ttk::combobox $w -width $width -state readonly\
2048                    -textvariable $varname -values $args \
2049                    -exportselection false]
2050        bind $gm <<ComboboxSelected>> [list $gm selection clear]
2051    } else {
2052        set gm [eval [linsert $args 0 tk_optionMenu $w $varname]]
2053    }
2054    return $gm
2055}
2056
2057proc makewindow {} {
2058    global canv canv2 canv3 linespc charspc ctext cflist cscroll
2059    global tabstop
2060    global findtype findtypemenu findloc findstring fstring geometry
2061    global entries sha1entry sha1string sha1but
2062    global diffcontextstring diffcontext
2063    global ignorespace
2064    global maincursor textcursor curtextcursor
2065    global rowctxmenu fakerowmenu mergemax wrapcomment
2066    global highlight_files gdttype
2067    global searchstring sstring
2068    global bgcolor fgcolor bglist fglist diffcolors selectbgcolor
2069    global uifgcolor uifgdisabledcolor
2070    global filesepbgcolor filesepfgcolor
2071    global mergecolors foundbgcolor currentsearchhitbgcolor
2072    global headctxmenu progresscanv progressitem progresscoords statusw
2073    global fprogitem fprogcoord lastprogupdate progupdatepending
2074    global rprogitem rprogcoord rownumsel numcommits
2075    global have_tk85 use_ttk NS
2076    global git_version
2077    global worddiff
2078
2079    # The "mc" arguments here are purely so that xgettext
2080    # sees the following string as needing to be translated
2081    set file {
2082        mc "&File" cascade {
2083            {mc "&Update" command updatecommits -accelerator F5}
2084            {mc "&Reload" command reloadcommits -accelerator Shift-F5}
2085            {mc "Reread re&ferences" command rereadrefs}
2086            {mc "&List references" command showrefs -accelerator F2}
2087            {xx "" separator}
2088            {mc "Start git &gui" command {exec git gui &}}
2089            {xx "" separator}
2090            {mc "&Quit" command doquit -accelerator Meta1-Q}
2091        }}
2092    set edit {
2093        mc "&Edit" cascade {
2094            {mc "&Preferences" command doprefs}
2095        }}
2096    set view {
2097        mc "&View" cascade {
2098            {mc "&New view..." command {newview 0} -accelerator Shift-F4}
2099            {mc "&Edit view..." command editview -state disabled -accelerator F4}
2100            {mc "&Delete view" command delview -state disabled}
2101            {xx "" separator}
2102            {mc "&All files" radiobutton {selectedview 0} -command {showview 0}}
2103        }}
2104    if {[tk windowingsystem] ne "aqua"} {
2105        set help {
2106        mc "&Help" cascade {
2107            {mc "&About gitk" command about}
2108            {mc "&Key bindings" command keys}
2109        }}
2110        set bar [list $file $edit $view $help]
2111    } else {
2112        proc ::tk::mac::ShowPreferences {} {doprefs}
2113        proc ::tk::mac::Quit {} {doquit}
2114        lset file end [lreplace [lindex $file end] end-1 end]
2115        set apple {
2116        xx "&Apple" cascade {
2117            {mc "&About gitk" command about}
2118            {xx "" separator}
2119        }}
2120        set help {
2121        mc "&Help" cascade {
2122            {mc "&Key bindings" command keys}
2123        }}
2124        set bar [list $apple $file $view $help]
2125    }
2126    makemenu .bar $bar
2127    . configure -menu .bar
2128
2129    if {$use_ttk} {
2130        # cover the non-themed toplevel with a themed frame.
2131        place [ttk::frame ._main_background] -x 0 -y 0 -relwidth 1 -relheight 1
2132    }
2133
2134    # the gui has upper and lower half, parts of a paned window.
2135    ${NS}::panedwindow .ctop -orient vertical
2136
2137    # possibly use assumed geometry
2138    if {![info exists geometry(pwsash0)]} {
2139        set geometry(topheight) [expr {15 * $linespc}]
2140        set geometry(topwidth) [expr {80 * $charspc}]
2141        set geometry(botheight) [expr {15 * $linespc}]
2142        set geometry(botwidth) [expr {50 * $charspc}]
2143        set geometry(pwsash0) [list [expr {40 * $charspc}] 2]
2144        set geometry(pwsash1) [list [expr {60 * $charspc}] 2]
2145    }
2146
2147    # the upper half will have a paned window, a scroll bar to the right, and some stuff below
2148    ${NS}::frame .tf -height $geometry(topheight) -width $geometry(topwidth)
2149    ${NS}::frame .tf.histframe
2150    ${NS}::panedwindow .tf.histframe.pwclist -orient horizontal
2151    if {!$use_ttk} {
2152        .tf.histframe.pwclist configure -sashpad 0 -handlesize 4
2153    }
2154
2155    # create three canvases
2156    set cscroll .tf.histframe.csb
2157    set canv .tf.histframe.pwclist.canv
2158    canvas $canv \
2159        -selectbackground $selectbgcolor \
2160        -background $bgcolor -bd 0 \
2161        -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll"
2162    .tf.histframe.pwclist add $canv
2163    set canv2 .tf.histframe.pwclist.canv2
2164    canvas $canv2 \
2165        -selectbackground $selectbgcolor \
2166        -background $bgcolor -bd 0 -yscrollincr $linespc
2167    .tf.histframe.pwclist add $canv2
2168    set canv3 .tf.histframe.pwclist.canv3
2169    canvas $canv3 \
2170        -selectbackground $selectbgcolor \
2171        -background $bgcolor -bd 0 -yscrollincr $linespc
2172    .tf.histframe.pwclist add $canv3
2173    if {$use_ttk} {
2174        bind .tf.histframe.pwclist <Map> {
2175            bind %W <Map> {}
2176            .tf.histframe.pwclist sashpos 1 [lindex $::geometry(pwsash1) 0]
2177            .tf.histframe.pwclist sashpos 0 [lindex $::geometry(pwsash0) 0]
2178        }
2179    } else {
2180        eval .tf.histframe.pwclist sash place 0 $geometry(pwsash0)
2181        eval .tf.histframe.pwclist sash place 1 $geometry(pwsash1)
2182    }
2183
2184    # a scroll bar to rule them
2185    ${NS}::scrollbar $cscroll -command {allcanvs yview}
2186    if {!$use_ttk} {$cscroll configure -highlightthickness 0}
2187    pack $cscroll -side right -fill y
2188    bind .tf.histframe.pwclist <Configure> {resizeclistpanes %W %w}
2189    lappend bglist $canv $canv2 $canv3
2190    pack .tf.histframe.pwclist -fill both -expand 1 -side left
2191
2192    # we have two button bars at bottom of top frame. Bar 1
2193    ${NS}::frame .tf.bar
2194    ${NS}::frame .tf.lbar -height 15
2195
2196    set sha1entry .tf.bar.sha1
2197    set entries $sha1entry
2198    set sha1but .tf.bar.sha1label
2199    button $sha1but -text "[mc "SHA1 ID:"] " -state disabled -relief flat \
2200        -command gotocommit -width 8
2201    $sha1but conf -disabledforeground [$sha1but cget -foreground]
2202    pack .tf.bar.sha1label -side left
2203    ${NS}::entry $sha1entry -width 40 -font textfont -textvariable sha1string
2204    trace add variable sha1string write sha1change
2205    pack $sha1entry -side left -pady 2
2206
2207    set bm_left_data {
2208        #define left_width 16
2209        #define left_height 16
2210        static unsigned char left_bits[] = {
2211        0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
2212        0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
2213        0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
2214    }
2215    set bm_right_data {
2216        #define right_width 16
2217        #define right_height 16
2218        static unsigned char right_bits[] = {
2219        0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
2220        0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
2221        0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
2222    }
2223    image create bitmap bm-left -data $bm_left_data -foreground $uifgcolor
2224    image create bitmap bm-left-gray -data $bm_left_data -foreground $uifgdisabledcolor
2225    image create bitmap bm-right -data $bm_right_data -foreground $uifgcolor
2226    image create bitmap bm-right-gray -data $bm_right_data -foreground $uifgdisabledcolor
2227
2228    ${NS}::button .tf.bar.leftbut -command goback -state disabled -width 26
2229    if {$use_ttk} {
2230        .tf.bar.leftbut configure -image [list bm-left disabled bm-left-gray]
2231    } else {
2232        .tf.bar.leftbut configure -image bm-left
2233    }
2234    pack .tf.bar.leftbut -side left -fill y
2235    ${NS}::button .tf.bar.rightbut -command goforw -state disabled -width 26
2236    if {$use_ttk} {
2237        .tf.bar.rightbut configure -image [list bm-right disabled bm-right-gray]
2238    } else {
2239        .tf.bar.rightbut configure -image bm-right
2240    }
2241    pack .tf.bar.rightbut -side left -fill y
2242
2243    ${NS}::label .tf.bar.rowlabel -text [mc "Row"]
2244    set rownumsel {}
2245    ${NS}::label .tf.bar.rownum -width 7 -textvariable rownumsel \
2246        -relief sunken -anchor e
2247    ${NS}::label .tf.bar.rowlabel2 -text "/"
2248    ${NS}::label .tf.bar.numcommits -width 7 -textvariable numcommits \
2249        -relief sunken -anchor e
2250    pack .tf.bar.rowlabel .tf.bar.rownum .tf.bar.rowlabel2 .tf.bar.numcommits \
2251        -side left
2252    if {!$use_ttk} {
2253        foreach w {rownum numcommits} {.tf.bar.$w configure -font textfont}
2254    }
2255    global selectedline
2256    trace add variable selectedline write selectedline_change
2257
2258    # Status label and progress bar
2259    set statusw .tf.bar.status
2260    ${NS}::label $statusw -width 15 -relief sunken
2261    pack $statusw -side left -padx 5
2262    if {$use_ttk} {
2263        set progresscanv [ttk::progressbar .tf.bar.progress]
2264    } else {
2265        set h [expr {[font metrics uifont -linespace] + 2}]
2266        set progresscanv .tf.bar.progress
2267        canvas $progresscanv -relief sunken -height $h -borderwidth 2
2268        set progressitem [$progresscanv create rect -1 0 0 $h -fill lime]
2269        set fprogitem [$progresscanv create rect -1 0 0 $h -fill yellow]
2270        set rprogitem [$progresscanv create rect -1 0 0 $h -fill red]
2271    }
2272    pack $progresscanv -side right -expand 1 -fill x -padx {0 2}
2273    set progresscoords {0 0}
2274    set fprogcoord 0
2275    set rprogcoord 0
2276    bind $progresscanv <Configure> adjustprogress
2277    set lastprogupdate [clock clicks -milliseconds]
2278    set progupdatepending 0
2279
2280    # build up the bottom bar of upper window
2281    ${NS}::label .tf.lbar.flabel -text "[mc "Find"] "
2282
2283    set bm_down_data {
2284        #define down_width 16
2285        #define down_height 16
2286        static unsigned char down_bits[] = {
2287        0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01,
2288        0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01,
2289        0x87, 0xe1, 0x8e, 0x71, 0x9c, 0x39, 0xb8, 0x1d,
2290        0xf0, 0x0f, 0xe0, 0x07, 0xc0, 0x03, 0x80, 0x01};
2291    }
2292    image create bitmap bm-down -data $bm_down_data -foreground $uifgcolor
2293    ${NS}::button .tf.lbar.fnext -width 26 -command {dofind 1 1}
2294    .tf.lbar.fnext configure -image bm-down
2295
2296    set bm_up_data {
2297        #define up_width 16
2298        #define up_height 16
2299        static unsigned char up_bits[] = {
2300        0x80, 0x01, 0xc0, 0x03, 0xe0, 0x07, 0xf0, 0x0f,
2301        0xb8, 0x1d, 0x9c, 0x39, 0x8e, 0x71, 0x87, 0xe1,
2302        0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01,
2303        0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01};
2304    }
2305    image create bitmap bm-up -data $bm_up_data -foreground $uifgcolor
2306    ${NS}::button .tf.lbar.fprev -width 26 -command {dofind -1 1}
2307    .tf.lbar.fprev configure -image bm-up
2308
2309    ${NS}::label .tf.lbar.flab2 -text " [mc "commit"] "
2310
2311    pack .tf.lbar.flabel .tf.lbar.fnext .tf.lbar.fprev .tf.lbar.flab2 \
2312        -side left -fill y
2313    set gdttype [mc "containing:"]
2314    set gm [makedroplist .tf.lbar.gdttype gdttype \
2315                [mc "containing:"] \
2316                [mc "touching paths:"] \
2317                [mc "adding/removing string:"] \
2318                [mc "changing lines matching:"]]
2319    trace add variable gdttype write gdttype_change
2320    pack .tf.lbar.gdttype -side left -fill y
2321
2322    set findstring {}
2323    set fstring .tf.lbar.findstring
2324    lappend entries $fstring
2325    ${NS}::entry $fstring -width 30 -textvariable findstring
2326    trace add variable findstring write find_change
2327    set findtype [mc "Exact"]
2328    set findtypemenu [makedroplist .tf.lbar.findtype \
2329                          findtype [mc "Exact"] [mc "IgnCase"] [mc "Regexp"]]
2330    trace add variable findtype write findcom_change
2331    set findloc [mc "All fields"]
2332    makedroplist .tf.lbar.findloc findloc [mc "All fields"] [mc "Headline"] \
2333        [mc "Comments"] [mc "Author"] [mc "Committer"]
2334    trace add variable findloc write find_change
2335    pack .tf.lbar.findloc -side right
2336    pack .tf.lbar.findtype -side right
2337    pack $fstring -side left -expand 1 -fill x
2338
2339    # Finish putting the upper half of the viewer together
2340    pack .tf.lbar -in .tf -side bottom -fill x
2341    pack .tf.bar -in .tf -side bottom -fill x
2342    pack .tf.histframe -fill both -side top -expand 1
2343    .ctop add .tf
2344    if {!$use_ttk} {
2345        .ctop paneconfigure .tf -height $geometry(topheight)
2346        .ctop paneconfigure .tf -width $geometry(topwidth)
2347    }
2348
2349    # now build up the bottom
2350    ${NS}::panedwindow .pwbottom -orient horizontal
2351
2352    # lower left, a text box over search bar, scroll bar to the right
2353    # if we know window height, then that will set the lower text height, otherwise
2354    # we set lower text height which will drive window height
2355    if {[info exists geometry(main)]} {
2356        ${NS}::frame .bleft -width $geometry(botwidth)
2357    } else {
2358        ${NS}::frame .bleft -width $geometry(botwidth) -height $geometry(botheight)
2359    }
2360    ${NS}::frame .bleft.top
2361    ${NS}::frame .bleft.mid
2362    ${NS}::frame .bleft.bottom
2363
2364    # gap between sub-widgets
2365    set wgap [font measure uifont "i"]
2366
2367    ${NS}::button .bleft.top.search -text [mc "Search"] -command dosearch
2368    pack .bleft.top.search -side left -padx 5
2369    set sstring .bleft.top.sstring
2370    set searchstring ""
2371    ${NS}::entry $sstring -width 20 -textvariable searchstring
2372    lappend entries $sstring
2373    trace add variable searchstring write incrsearch
2374    pack $sstring -side left -expand 1 -fill x
2375    ${NS}::radiobutton .bleft.mid.diff -text [mc "Diff"] \
2376        -command changediffdisp -variable diffelide -value {0 0}
2377    ${NS}::radiobutton .bleft.mid.old -text [mc "Old version"] \
2378        -command changediffdisp -variable diffelide -value {0 1}
2379    ${NS}::radiobutton .bleft.mid.new -text [mc "New version"] \
2380        -command changediffdisp -variable diffelide -value {1 0}
2381
2382    ${NS}::label .bleft.mid.labeldiffcontext -text "      [mc "Lines of context"]: "
2383    pack .bleft.mid.diff .bleft.mid.old .bleft.mid.new -side left -ipadx $wgap
2384    spinbox .bleft.mid.diffcontext -width 5 \
2385        -from 0 -increment 1 -to 10000000 \
2386        -validate all -validatecommand "diffcontextvalidate %P" \
2387        -textvariable diffcontextstring
2388    .bleft.mid.diffcontext set $diffcontext
2389    trace add variable diffcontextstring write diffcontextchange
2390    lappend entries .bleft.mid.diffcontext
2391    pack .bleft.mid.labeldiffcontext .bleft.mid.diffcontext -side left -ipadx $wgap
2392    ${NS}::checkbutton .bleft.mid.ignspace -text [mc "Ignore space change"] \
2393        -command changeignorespace -variable ignorespace
2394    pack .bleft.mid.ignspace -side left -padx 5
2395
2396    set worddiff [mc "Line diff"]
2397    if {[package vcompare $git_version "1.7.2"] >= 0} {
2398        makedroplist .bleft.mid.worddiff worddiff [mc "Line diff"] \
2399            [mc "Markup words"] [mc "Color words"]
2400        trace add variable worddiff write changeworddiff
2401        pack .bleft.mid.worddiff -side left -padx 5
2402    }
2403
2404    set ctext .bleft.bottom.ctext
2405    text $ctext -background $bgcolor -foreground $fgcolor \
2406        -state disabled -font textfont \
2407        -yscrollcommand scrolltext -wrap none \
2408        -xscrollcommand ".bleft.bottom.sbhorizontal set"
2409    if {$have_tk85} {
2410        $ctext conf -tabstyle wordprocessor
2411    }
2412    ${NS}::scrollbar .bleft.bottom.sb -command "$ctext yview"
2413    ${NS}::scrollbar .bleft.bottom.sbhorizontal -command "$ctext xview" -orient h
2414    pack .bleft.top -side top -fill x
2415    pack .bleft.mid -side top -fill x
2416    grid $ctext .bleft.bottom.sb -sticky nsew
2417    grid .bleft.bottom.sbhorizontal -sticky ew
2418    grid columnconfigure .bleft.bottom 0 -weight 1
2419    grid rowconfigure .bleft.bottom 0 -weight 1
2420    grid rowconfigure .bleft.bottom 1 -weight 0
2421    pack .bleft.bottom -side top -fill both -expand 1
2422    lappend bglist $ctext
2423    lappend fglist $ctext
2424
2425    $ctext tag conf comment -wrap $wrapcomment
2426    $ctext tag conf filesep -font textfontbold -fore $filesepfgcolor -back $filesepbgcolor
2427    $ctext tag conf hunksep -fore [lindex $diffcolors 2]
2428    $ctext tag conf d0 -fore [lindex $diffcolors 0]
2429    $ctext tag conf dresult -fore [lindex $diffcolors 1]
2430    $ctext tag conf m0 -fore [lindex $mergecolors 0]
2431    $ctext tag conf m1 -fore [lindex $mergecolors 1]
2432    $ctext tag conf m2 -fore [lindex $mergecolors 2]
2433    $ctext tag conf m3 -fore [lindex $mergecolors 3]
2434    $ctext tag conf m4 -fore [lindex $mergecolors 4]
2435    $ctext tag conf m5 -fore [lindex $mergecolors 5]
2436    $ctext tag conf m6 -fore [lindex $mergecolors 6]
2437    $ctext tag conf m7 -fore [lindex $mergecolors 7]
2438    $ctext tag conf m8 -fore [lindex $mergecolors 8]
2439    $ctext tag conf m9 -fore [lindex $mergecolors 9]
2440    $ctext tag conf m10 -fore [lindex $mergecolors 10]
2441    $ctext tag conf m11 -fore [lindex $mergecolors 11]
2442    $ctext tag conf m12 -fore [lindex $mergecolors 12]
2443    $ctext tag conf m13 -fore [lindex $mergecolors 13]
2444    $ctext tag conf m14 -fore [lindex $mergecolors 14]
2445    $ctext tag conf m15 -fore [lindex $mergecolors 15]
2446    $ctext tag conf mmax -fore darkgrey
2447    set mergemax 16
2448    $ctext tag conf mresult -font textfontbold
2449    $ctext tag conf msep -font textfontbold
2450    $ctext tag conf found -back $foundbgcolor
2451    $ctext tag conf currentsearchhit -back $currentsearchhitbgcolor
2452    $ctext tag conf wwrap -wrap word -lmargin2 1c
2453    $ctext tag conf bold -font textfontbold
2454
2455    .pwbottom add .bleft
2456    if {!$use_ttk} {
2457        .pwbottom paneconfigure .bleft -width $geometry(botwidth)
2458    }
2459
2460    # lower right
2461    ${NS}::frame .bright
2462    ${NS}::frame .bright.mode
2463    ${NS}::radiobutton .bright.mode.patch -text [mc "Patch"] \
2464        -command reselectline -variable cmitmode -value "patch"
2465    ${NS}::radiobutton .bright.mode.tree -text [mc "Tree"] \
2466        -command reselectline -variable cmitmode -value "tree"
2467    grid .bright.mode.patch .bright.mode.tree -sticky ew
2468    pack .bright.mode -side top -fill x
2469    set cflist .bright.cfiles
2470    set indent [font measure mainfont "nn"]
2471    text $cflist \
2472        -selectbackground $selectbgcolor \
2473        -background $bgcolor -foreground $fgcolor \
2474        -font mainfont \
2475        -tabs [list $indent [expr {2 * $indent}]] \
2476        -yscrollcommand ".bright.sb set" \
2477        -cursor [. cget -cursor] \
2478        -spacing1 1 -spacing3 1
2479    lappend bglist $cflist
2480    lappend fglist $cflist
2481    ${NS}::scrollbar .bright.sb -command "$cflist yview"
2482    pack .bright.sb -side right -fill y
2483    pack $cflist -side left -fill both -expand 1
2484    $cflist tag configure highlight \
2485        -background [$cflist cget -selectbackground]
2486    $cflist tag configure bold -font mainfontbold
2487
2488    .pwbottom add .bright
2489    .ctop add .pwbottom
2490
2491    # restore window width & height if known
2492    if {[info exists geometry(main)]} {
2493        if {[scan $geometry(main) "%dx%d" w h] >= 2} {
2494            if {$w > [winfo screenwidth .]} {
2495                set w [winfo screenwidth .]
2496            }
2497            if {$h > [winfo screenheight .]} {
2498                set h [winfo screenheight .]
2499            }
2500            wm geometry . "${w}x$h"
2501        }
2502    }
2503
2504    if {[info exists geometry(state)] && $geometry(state) eq "zoomed"} {
2505        wm state . $geometry(state)
2506    }
2507
2508    if {[tk windowingsystem] eq {aqua}} {
2509        set M1B M1
2510        set ::BM "3"
2511    } else {
2512        set M1B Control
2513        set ::BM "2"
2514    }
2515
2516    if {$use_ttk} {
2517        bind .ctop <Map> {
2518            bind %W <Map> {}
2519            %W sashpos 0 $::geometry(topheight)
2520        }
2521        bind .pwbottom <Map> {
2522            bind %W <Map> {}
2523            %W sashpos 0 $::geometry(botwidth)
2524        }
2525    }
2526
2527    bind .pwbottom <Configure> {resizecdetpanes %W %w}
2528    pack .ctop -fill both -expand 1
2529    bindall <1> {selcanvline %W %x %y}
2530    #bindall <B1-Motion> {selcanvline %W %x %y}
2531    if {[tk windowingsystem] == "win32"} {
2532        bind . <MouseWheel> { windows_mousewheel_redirector %W %X %Y %D }
2533        bind $ctext <MouseWheel> { windows_mousewheel_redirector %W %X %Y %D ; break }
2534    } else {
2535        bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
2536        bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
2537        bind $ctext <Button> {
2538            if {"%b" eq 6} {
2539                $ctext xview scroll -5 units
2540            } elseif {"%b" eq 7} {
2541                $ctext xview scroll 5 units
2542            }
2543        }
2544        if {[tk windowingsystem] eq "aqua"} {
2545            bindall <MouseWheel> {
2546                set delta [expr {- (%D)}]
2547                allcanvs yview scroll $delta units
2548            }
2549            bindall <Shift-MouseWheel> {
2550                set delta [expr {- (%D)}]
2551                $canv xview scroll $delta units
2552            }
2553        }
2554    }
2555    bindall <$::BM> "canvscan mark %W %x %y"
2556    bindall <B$::BM-Motion> "canvscan dragto %W %x %y"
2557    bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
2558    bind . <$M1B-Key-w> doquit
2559    bindkey <Home> selfirstline
2560    bindkey <End> sellastline
2561    bind . <Key-Up> "selnextline -1"
2562    bind . <Key-Down> "selnextline 1"
2563    bind . <Shift-Key-Up> "dofind -1 0"
2564    bind . <Shift-Key-Down> "dofind 1 0"
2565    bindkey <Key-Right> "goforw"
2566    bindkey <Key-Left> "goback"
2567    bind . <Key-Prior> "selnextpage -1"
2568    bind . <Key-Next> "selnextpage 1"
2569    bind . <$M1B-Home> "allcanvs yview moveto 0.0"
2570    bind . <$M1B-End> "allcanvs yview moveto 1.0"
2571    bind . <$M1B-Key-Up> "allcanvs yview scroll -1 units"
2572    bind . <$M1B-Key-Down> "allcanvs yview scroll 1 units"
2573    bind . <$M1B-Key-Prior> "allcanvs yview scroll -1 pages"
2574    bind . <$M1B-Key-Next> "allcanvs yview scroll 1 pages"
2575    bindkey <Key-Delete> "$ctext yview scroll -1 pages"
2576    bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
2577    bindkey <Key-space> "$ctext yview scroll 1 pages"
2578    bindkey p "selnextline -1"
2579    bindkey n "selnextline 1"
2580    bindkey z "goback"
2581    bindkey x "goforw"
2582    bindkey k "selnextline -1"
2583    bindkey j "selnextline 1"
2584    bindkey h "goback"
2585    bindkey l "goforw"
2586    bindkey b prevfile
2587    bindkey d "$ctext yview scroll 18 units"
2588    bindkey u "$ctext yview scroll -18 units"
2589    bindkey g {$sha1entry delete 0 end; focus $sha1entry}
2590    bindkey / {focus $fstring}
2591    bindkey <Key-KP_Divide> {focus $fstring}
2592    bindkey <Key-Return> {dofind 1 1}
2593    bindkey ? {dofind -1 1}
2594    bindkey f nextfile
2595    bind . <F5> updatecommits
2596    bindmodfunctionkey Shift 5 reloadcommits
2597    bind . <F2> showrefs
2598    bindmodfunctionkey Shift 4 {newview 0}
2599    bind . <F4> edit_or_newview
2600    bind . <$M1B-q> doquit
2601    bind . <$M1B-f> {dofind 1 1}
2602    bind . <$M1B-g> {dofind 1 0}
2603    bind . <$M1B-r> dosearchback
2604    bind . <$M1B-s> dosearch
2605    bind . <$M1B-equal> {incrfont 1}
2606    bind . <$M1B-plus> {incrfont 1}
2607    bind . <$M1B-KP_Add> {incrfont 1}
2608    bind . <$M1B-minus> {incrfont -1}
2609    bind . <$M1B-KP_Subtract> {incrfont -1}
2610    wm protocol . WM_DELETE_WINDOW doquit
2611    bind . <Destroy> {stop_backends}
2612    bind . <Button-1> "click %W"
2613    bind $fstring <Key-Return> {dofind 1 1}
2614    bind $sha1entry <Key-Return> {gotocommit; break}
2615    bind $sha1entry <<PasteSelection>> clearsha1
2616    bind $sha1entry <<Paste>> clearsha1
2617    bind $cflist <1> {sel_flist %W %x %y; break}
2618    bind $cflist <B1-Motion> {sel_flist %W %x %y; break}
2619    bind $cflist <ButtonRelease-1> {treeclick %W %x %y}
2620    global ctxbut
2621    bind $cflist $ctxbut {pop_flist_menu %W %X %Y %x %y}
2622    bind $ctext $ctxbut {pop_diff_menu %W %X %Y %x %y}
2623    bind $ctext <Button-1> {focus %W}
2624    bind $ctext <<Selection>> rehighlight_search_results
2625    for {set i 1} {$i < 10} {incr i} {
2626        bind . <$M1B-Key-$i> [list go_to_parent $i]
2627    }
2628
2629    set maincursor [. cget -cursor]
2630    set textcursor [$ctext cget -cursor]
2631    set curtextcursor $textcursor
2632
2633    set rowctxmenu .rowctxmenu
2634    makemenu $rowctxmenu {
2635        {mc "Diff this -> selected" command {diffvssel 0}}
2636        {mc "Diff selected -> this" command {diffvssel 1}}
2637        {mc "Make patch" command mkpatch}
2638        {mc "Create tag" command mktag}
2639        {mc "Copy commit summary" command copysummary}
2640        {mc "Write commit to file" command writecommit}
2641        {mc "Create new branch" command mkbranch}
2642        {mc "Cherry-pick this commit" command cherrypick}
2643        {mc "Reset HEAD branch to here" command resethead}
2644        {mc "Mark this commit" command markhere}
2645        {mc "Return to mark" command gotomark}
2646        {mc "Find descendant of this and mark" command find_common_desc}
2647        {mc "Compare with marked commit" command compare_commits}
2648        {mc "Diff this -> marked commit" command {diffvsmark 0}}
2649        {mc "Diff marked commit -> this" command {diffvsmark 1}}
2650        {mc "Revert this commit" command revert}
2651    }
2652    $rowctxmenu configure -tearoff 0
2653
2654    set fakerowmenu .fakerowmenu
2655    makemenu $fakerowmenu {
2656        {mc "Diff this -> selected" command {diffvssel 0}}
2657        {mc "Diff selected -> this" command {diffvssel 1}}
2658        {mc "Make patch" command mkpatch}
2659        {mc "Diff this -> marked commit" command {diffvsmark 0}}
2660        {mc "Diff marked commit -> this" command {diffvsmark 1}}
2661    }
2662    $fakerowmenu configure -tearoff 0
2663
2664    set headctxmenu .headctxmenu
2665    makemenu $headctxmenu {
2666        {mc "Check out this branch" command cobranch}
2667        {mc "Rename this branch" command mvbranch}
2668        {mc "Remove this branch" command rmbranch}
2669        {mc "Copy branch name" command {clipboard clear; clipboard append $headmenuhead}}
2670    }
2671    $headctxmenu configure -tearoff 0
2672
2673    global flist_menu
2674    set flist_menu .flistctxmenu
2675    makemenu $flist_menu {
2676        {mc "Highlight this too" command {flist_hl 0}}
2677        {mc "Highlight this only" command {flist_hl 1}}
2678        {mc "External diff" command {external_diff}}
2679        {mc "Blame parent commit" command {external_blame 1}}
2680        {mc "Copy path" command {clipboard clear; clipboard append $flist_menu_file}}
2681    }
2682    $flist_menu configure -tearoff 0
2683
2684    global diff_menu
2685    set diff_menu .diffctxmenu
2686    makemenu $diff_menu {
2687        {mc "Show origin of this line" command show_line_source}
2688        {mc "Run git gui blame on this line" command {external_blame_diff}}
2689    }
2690    $diff_menu configure -tearoff 0
2691}
2692
2693# Windows sends all mouse wheel events to the current focused window, not
2694# the one where the mouse hovers, so bind those events here and redirect
2695# to the correct window
2696proc windows_mousewheel_redirector {W X Y D} {
2697    global canv canv2 canv3
2698    set w [winfo containing -displayof $W $X $Y]
2699    if {$w ne ""} {
2700        set u [expr {$D < 0 ? 5 : -5}]
2701        if {$w == $canv || $w == $canv2 || $w == $canv3} {
2702            allcanvs yview scroll $u units
2703        } else {
2704            catch {
2705                $w yview scroll $u units
2706            }
2707        }
2708    }
2709}
2710
2711# Update row number label when selectedline changes
2712proc selectedline_change {n1 n2 op} {
2713    global selectedline rownumsel
2714
2715    if {$selectedline eq {}} {
2716        set rownumsel {}
2717    } else {
2718        set rownumsel [expr {$selectedline + 1}]
2719    }
2720}
2721
2722# mouse-2 makes all windows scan vertically, but only the one
2723# the cursor is in scans horizontally
2724proc canvscan {op w x y} {
2725    global canv canv2 canv3
2726    foreach c [list $canv $canv2 $canv3] {
2727        if {$c == $w} {
2728            $c scan $op $x $y
2729        } else {
2730            $c scan $op 0 $y
2731        }
2732    }
2733}
2734
2735proc scrollcanv {cscroll f0 f1} {
2736    $cscroll set $f0 $f1
2737    drawvisible
2738    flushhighlights
2739}
2740
2741# when we make a key binding for the toplevel, make sure
2742# it doesn't get triggered when that key is pressed in the
2743# find string entry widget.
2744proc bindkey {ev script} {
2745    global entries
2746    bind . $ev $script
2747    set escript [bind Entry $ev]
2748    if {$escript == {}} {
2749        set escript [bind Entry <Key>]
2750    }
2751    foreach e $entries {
2752        bind $e $ev "$escript; break"
2753    }
2754}
2755
2756proc bindmodfunctionkey {mod n script} {
2757    bind . <$mod-F$n> $script
2758    catch { bind . <$mod-XF86_Switch_VT_$n> $script }
2759}
2760
2761# set the focus back to the toplevel for any click outside
2762# the entry widgets
2763proc click {w} {
2764    global ctext entries
2765    foreach e [concat $entries $ctext] {
2766        if {$w == $e} return
2767    }
2768    focus .
2769}
2770
2771# Adjust the progress bar for a change in requested extent or canvas size
2772proc adjustprogress {} {
2773    global progresscanv progressitem progresscoords
2774    global fprogitem fprogcoord lastprogupdate progupdatepending
2775    global rprogitem rprogcoord use_ttk
2776
2777    if {$use_ttk} {
2778        $progresscanv configure -value [expr {int($fprogcoord * 100)}]
2779        return
2780    }
2781
2782    set w [expr {[winfo width $progresscanv] - 4}]
2783    set x0 [expr {$w * [lindex $progresscoords 0]}]
2784    set x1 [expr {$w * [lindex $progresscoords 1]}]
2785    set h [winfo height $progresscanv]
2786    $progresscanv coords $progressitem $x0 0 $x1 $h
2787    $progresscanv coords $fprogitem 0 0 [expr {$w * $fprogcoord}] $h
2788    $progresscanv coords $rprogitem 0 0 [expr {$w * $rprogcoord}] $h
2789    set now [clock clicks -milliseconds]
2790    if {$now >= $lastprogupdate + 100} {
2791        set progupdatepending 0
2792        update
2793    } elseif {!$progupdatepending} {
2794        set progupdatepending 1
2795        after [expr {$lastprogupdate + 100 - $now}] doprogupdate
2796    }
2797}
2798
2799proc doprogupdate {} {
2800    global lastprogupdate progupdatepending
2801
2802    if {$progupdatepending} {
2803        set progupdatepending 0
2804        set lastprogupdate [clock clicks -milliseconds]
2805        update
2806    }
2807}
2808
2809proc config_check_tmp_exists {tries_left} {
2810    global config_file_tmp
2811
2812    if {[file exists $config_file_tmp]} {
2813        incr tries_left -1
2814        if {$tries_left > 0} {
2815            after 100 [list config_check_tmp_exists $tries_left]
2816        } else {
2817            error_popup "There appears to be a stale $config_file_tmp\
2818 file, which will prevent gitk from saving its configuration on exit.\
2819 Please remove it if it is not being used by any existing gitk process."
2820        }
2821    }
2822}
2823
2824proc config_init_trace {name} {
2825    global config_variable_changed config_variable_original
2826
2827    upvar #0 $name var
2828    set config_variable_changed($name) 0
2829    set config_variable_original($name) $var
2830}
2831
2832proc config_variable_change_cb {name name2 op} {
2833    global config_variable_changed config_variable_original
2834
2835    upvar #0 $name var
2836    if {$op eq "write" &&
2837        (![info exists config_variable_original($name)] ||
2838         $config_variable_original($name) ne $var)} {
2839        set config_variable_changed($name) 1
2840    }
2841}
2842
2843proc savestuff {w} {
2844    global stuffsaved
2845    global config_file config_file_tmp
2846    global config_variables config_variable_changed
2847    global viewchanged
2848
2849    upvar #0 viewname current_viewname
2850    upvar #0 viewfiles current_viewfiles
2851    upvar #0 viewargs current_viewargs
2852    upvar #0 viewargscmd current_viewargscmd
2853    upvar #0 viewperm current_viewperm
2854    upvar #0 nextviewnum current_nextviewnum
2855    upvar #0 use_ttk current_use_ttk
2856
2857    if {$stuffsaved} return
2858    if {![winfo viewable .]} return
2859    set remove_tmp 0
2860    if {[catch {
2861        set try_count 0
2862        while {[catch {set f [open $config_file_tmp {WRONLY CREAT EXCL}]}]} {
2863            if {[incr try_count] > 50} {
2864                error "Unable to write config file: $config_file_tmp exists"
2865            }
2866            after 100
2867        }
2868        set remove_tmp 1
2869        if {$::tcl_platform(platform) eq {windows}} {
2870            file attributes $config_file_tmp -hidden true
2871        }
2872        if {[file exists $config_file]} {
2873            source $config_file
2874        }
2875        foreach var_name $config_variables {
2876            upvar #0 $var_name var
2877            upvar 0 $var_name old_var
2878            if {!$config_variable_changed($var_name) && [info exists old_var]} {
2879                puts $f [list set $var_name $old_var]
2880            } else {
2881                puts $f [list set $var_name $var]
2882            }
2883        }
2884
2885        puts $f "set geometry(main) [wm geometry .]"
2886        puts $f "set geometry(state) [wm state .]"
2887        puts $f "set geometry(topwidth) [winfo width .tf]"
2888        puts $f "set geometry(topheight) [winfo height .tf]"
2889        if {$current_use_ttk} {
2890            puts $f "set geometry(pwsash0) \"[.tf.histframe.pwclist sashpos 0] 1\""
2891            puts $f "set geometry(pwsash1) \"[.tf.histframe.pwclist sashpos 1] 1\""
2892        } else {
2893            puts $f "set geometry(pwsash0) \"[.tf.histframe.pwclist sash coord 0]\""
2894            puts $f "set geometry(pwsash1) \"[.tf.histframe.pwclist sash coord 1]\""
2895        }
2896        puts $f "set geometry(botwidth) [winfo width .bleft]"
2897        puts $f "set geometry(botheight) [winfo height .bleft]"
2898
2899        array set view_save {}
2900        array set views {}
2901        if {![info exists permviews]} { set permviews {} }
2902        foreach view $permviews {
2903            set view_save([lindex $view 0]) 1
2904            set views([lindex $view 0]) $view
2905        }
2906        puts -nonewline $f "set permviews {"
2907        for {set v 1} {$v < $current_nextviewnum} {incr v} {
2908            if {$viewchanged($v)} {
2909                if {$current_viewperm($v)} {
2910                    set views($current_viewname($v)) [list $current_viewname($v) $current_viewfiles($v) $current_viewargs($v) $current_viewargscmd($v)]
2911                } else {
2912                    set view_save($current_viewname($v)) 0
2913                }
2914            }
2915        }
2916        # write old and updated view to their places and append remaining to the end
2917        foreach view $permviews {
2918            set view_name [lindex $view 0]
2919            if {$view_save($view_name)} {
2920                puts $f "{$views($view_name)}"
2921            }
2922            unset views($view_name)
2923        }
2924        foreach view_name [array names views] {
2925            puts $f "{$views($view_name)}"
2926        }
2927        puts $f "}"
2928        close $f
2929        file rename -force $config_file_tmp $config_file
2930        set remove_tmp 0
2931    } err]} {
2932        puts "Error saving config: $err"
2933    }
2934    if {$remove_tmp} {
2935        file delete -force $config_file_tmp
2936    }
2937    set stuffsaved 1
2938}
2939
2940proc resizeclistpanes {win w} {
2941    global oldwidth use_ttk
2942    if {[info exists oldwidth($win)]} {
2943        if {$use_ttk} {
2944            set s0 [$win sashpos 0]
2945            set s1 [$win sashpos 1]
2946        } else {
2947            set s0 [$win sash coord 0]
2948            set s1 [$win sash coord 1]
2949        }
2950        if {$w < 60} {
2951            set sash0 [expr {int($w/2 - 2)}]
2952            set sash1 [expr {int($w*5/6 - 2)}]
2953        } else {
2954            set factor [expr {1.0 * $w / $oldwidth($win)}]
2955            set sash0 [expr {int($factor * [lindex $s0 0])}]
2956            set sash1 [expr {int($factor * [lindex $s1 0])}]
2957            if {$sash0 < 30} {
2958                set sash0 30
2959            }
2960            if {$sash1 < $sash0 + 20} {
2961                set sash1 [expr {$sash0 + 20}]
2962            }
2963            if {$sash1 > $w - 10} {
2964                set sash1 [expr {$w - 10}]
2965                if {$sash0 > $sash1 - 20} {
2966                    set sash0 [expr {$sash1 - 20}]
2967                }
2968            }
2969        }
2970        if {$use_ttk} {
2971            $win sashpos 0 $sash0
2972            $win sashpos 1 $sash1
2973        } else {
2974            $win sash place 0 $sash0 [lindex $s0 1]
2975            $win sash place 1 $sash1 [lindex $s1 1]
2976        }
2977    }
2978    set oldwidth($win) $w
2979}
2980
2981proc resizecdetpanes {win w} {
2982    global oldwidth use_ttk
2983    if {[info exists oldwidth($win)]} {
2984        if {$use_ttk} {
2985            set s0 [$win sashpos 0]
2986        } else {
2987            set s0 [$win sash coord 0]
2988        }
2989        if {$w < 60} {
2990            set sash0 [expr {int($w*3/4 - 2)}]
2991        } else {
2992            set factor [expr {1.0 * $w / $oldwidth($win)}]
2993            set sash0 [expr {int($factor * [lindex $s0 0])}]
2994            if {$sash0 < 45} {
2995                set sash0 45
2996            }
2997            if {$sash0 > $w - 15} {
2998                set sash0 [expr {$w - 15}]
2999            }
3000        }
3001        if {$use_ttk} {
3002            $win sashpos 0 $sash0
3003        } else {
3004            $win sash place 0 $sash0 [lindex $s0 1]
3005        }
3006    }
3007    set oldwidth($win) $w
3008}
3009
3010proc allcanvs args {
3011    global canv canv2 canv3
3012    eval $canv $args
3013    eval $canv2 $args
3014    eval $canv3 $args
3015}
3016
3017proc bindall {event action} {
3018    global canv canv2 canv3
3019    bind $canv $event $action
3020    bind $canv2 $event $action
3021    bind $canv3 $event $action
3022}
3023
3024proc about {} {
3025    global bgcolor NS
3026    set w .about
3027    if {[winfo exists $w]} {
3028        raise $w
3029        return
3030    }
3031    ttk_toplevel $w
3032    wm title $w [mc "About gitk"]
3033    make_transient $w .
3034    message $w.m -text [mc "
3035Gitk - a commit viewer for git
3036
3037Copyright \u00a9 2005-2014 Paul Mackerras
3038
3039Use and redistribute under the terms of the GNU General Public License"] \
3040            -justify center -aspect 400 -border 2 -bg $bgcolor -relief groove
3041    pack $w.m -side top -fill x -padx 2 -pady 2
3042    ${NS}::button $w.ok -text [mc "Close"] -command "destroy $w" -default active
3043    pack $w.ok -side bottom
3044    bind $w <Visibility> "focus $w.ok"
3045    bind $w <Key-Escape> "destroy $w"
3046    bind $w <Key-Return> "destroy $w"
3047    tk::PlaceWindow $w widget .
3048}
3049
3050proc keys {} {
3051    global bgcolor NS
3052    set w .keys
3053    if {[winfo exists $w]} {
3054        raise $w
3055        return
3056    }
3057    if {[tk windowingsystem] eq {aqua}} {
3058        set M1T Cmd
3059    } else {
3060        set M1T Ctrl
3061    }
3062    ttk_toplevel $w
3063    wm title $w [mc "Gitk key bindings"]
3064    make_transient $w .
3065    message $w.m -text "
3066[mc "Gitk key bindings:"]
3067
3068[mc "<%s-Q>             Quit" $M1T]
3069[mc "<%s-W>             Close window" $M1T]
3070[mc "<Home>             Move to first commit"]
3071[mc "<End>              Move to last commit"]
3072[mc "<Up>, p, k Move up one commit"]
3073[mc "<Down>, n, j       Move down one commit"]
3074[mc "<Left>, z, h       Go back in history list"]
3075[mc "<Right>, x, l      Go forward in history list"]
3076[mc "<%s-n>     Go to n-th parent of current commit in history list" $M1T]
3077[mc "<PageUp>   Move up one page in commit list"]
3078[mc "<PageDown> Move down one page in commit list"]
3079[mc "<%s-Home>  Scroll to top of commit list" $M1T]
3080[mc "<%s-End>   Scroll to bottom of commit list" $M1T]
3081[mc "<%s-Up>    Scroll commit list up one line" $M1T]
3082[mc "<%s-Down>  Scroll commit list down one line" $M1T]
3083[mc "<%s-PageUp>        Scroll commit list up one page" $M1T]
3084[mc "<%s-PageDown>      Scroll commit list down one page" $M1T]
3085[mc "<Shift-Up> Find backwards (upwards, later commits)"]
3086[mc "<Shift-Down>       Find forwards (downwards, earlier commits)"]
3087[mc "<Delete>, b        Scroll diff view up one page"]
3088[mc "<Backspace>        Scroll diff view up one page"]
3089[mc "<Space>            Scroll diff view down one page"]
3090[mc "u          Scroll diff view up 18 lines"]
3091[mc "d          Scroll diff view down 18 lines"]
3092[mc "<%s-F>             Find" $M1T]
3093[mc "<%s-G>             Move to next find hit" $M1T]
3094[mc "<Return>   Move to next find hit"]
3095[mc "g          Go to commit"]
3096[mc "/          Focus the search box"]
3097[mc "?          Move to previous find hit"]
3098[mc "f          Scroll diff view to next file"]
3099[mc "<%s-S>             Search for next hit in diff view" $M1T]
3100[mc "<%s-R>             Search for previous hit in diff view" $M1T]
3101[mc "<%s-KP+>   Increase font size" $M1T]
3102[mc "<%s-plus>  Increase font size" $M1T]
3103[mc "<%s-KP->   Decrease font size" $M1T]
3104[mc "<%s-minus> Decrease font size" $M1T]
3105[mc "<F5>               Update"]
3106" \
3107            -justify left -bg $bgcolor -border 2 -relief groove
3108    pack $w.m -side top -fill both -padx 2 -pady 2
3109    ${NS}::button $w.ok -text [mc "Close"] -command "destroy $w" -default active
3110    bind $w <Key-Escape> [list destroy $w]
3111    pack $w.ok -side bottom
3112    bind $w <Visibility> "focus $w.ok"
3113    bind $w <Key-Escape> "destroy $w"
3114    bind $w <Key-Return> "destroy $w"
3115}
3116
3117# Procedures for manipulating the file list window at the
3118# bottom right of the overall window.
3119
3120proc treeview {w l openlevs} {
3121    global treecontents treediropen treeheight treeparent treeindex
3122
3123    set ix 0
3124    set treeindex() 0
3125    set lev 0
3126    set prefix {}
3127    set prefixend -1
3128    set prefendstack {}
3129    set htstack {}
3130    set ht 0
3131    set treecontents() {}
3132    $w conf -state normal
3133    foreach f $l {
3134        while {[string range $f 0 $prefixend] ne $prefix} {
3135            if {$lev <= $openlevs} {
3136                $w mark set e:$treeindex($prefix) "end -1c"
3137                $w mark gravity e:$treeindex($prefix) left
3138            }
3139            set treeheight($prefix) $ht
3140            incr ht [lindex $htstack end]
3141            set htstack [lreplace $htstack end end]
3142            set prefixend [lindex $prefendstack end]
3143            set prefendstack [lreplace $prefendstack end end]
3144            set prefix [string range $prefix 0 $prefixend]
3145            incr lev -1
3146        }
3147        set tail [string range $f [expr {$prefixend+1}] end]
3148        while {[set slash [string first "/" $tail]] >= 0} {
3149            lappend htstack $ht
3150            set ht 0
3151            lappend prefendstack $prefixend
3152            incr prefixend [expr {$slash + 1}]
3153            set d [string range $tail 0 $slash]
3154            lappend treecontents($prefix) $d
3155            set oldprefix $prefix
3156            append prefix $d
3157            set treecontents($prefix) {}
3158            set treeindex($prefix) [incr ix]
3159            set treeparent($prefix) $oldprefix
3160            set tail [string range $tail [expr {$slash+1}] end]
3161            if {$lev <= $openlevs} {
3162                set ht 1
3163                set treediropen($prefix) [expr {$lev < $openlevs}]
3164                set bm [expr {$lev == $openlevs? "tri-rt": "tri-dn"}]
3165                $w mark set d:$ix "end -1c"
3166                $w mark gravity d:$ix left
3167                set str "\n"
3168                for {set i 0} {$i < $lev} {incr i} {append str "\t"}
3169                $w insert end $str
3170                $w image create end -align center -image $bm -padx 1 \
3171                    -name a:$ix
3172                $w insert end $d [highlight_tag $prefix]
3173                $w mark set s:$ix "end -1c"
3174                $w mark gravity s:$ix left
3175            }
3176            incr lev
3177        }
3178        if {$tail ne {}} {
3179            if {$lev <= $openlevs} {
3180                incr ht
3181                set str "\n"
3182                for {set i 0} {$i < $lev} {incr i} {append str "\t"}
3183                $w insert end $str
3184                $w insert end $tail [highlight_tag $f]
3185            }
3186            lappend treecontents($prefix) $tail
3187        }
3188    }
3189    while {$htstack ne {}} {
3190        set treeheight($prefix) $ht
3191        incr ht [lindex $htstack end]
3192        set htstack [lreplace $htstack end end]
3193        set prefixend [lindex $prefendstack end]
3194        set prefendstack [lreplace $prefendstack end end]
3195        set prefix [string range $prefix 0 $prefixend]
3196    }
3197    $w conf -state disabled
3198}
3199
3200proc linetoelt {l} {
3201    global treeheight treecontents
3202
3203    set y 2
3204    set prefix {}
3205    while {1} {
3206        foreach e $treecontents($prefix) {
3207            if {$y == $l} {
3208                return "$prefix$e"
3209            }
3210            set n 1
3211            if {[string index $e end] eq "/"} {
3212                set n $treeheight($prefix$e)
3213                if {$y + $n > $l} {
3214                    append prefix $e
3215                    incr y
3216                    break
3217                }
3218            }
3219            incr y $n
3220        }
3221    }
3222}
3223
3224proc highlight_tree {y prefix} {
3225    global treeheight treecontents cflist
3226
3227    foreach e $treecontents($prefix) {
3228        set path $prefix$e
3229        if {[highlight_tag $path] ne {}} {
3230            $cflist tag add bold $y.0 "$y.0 lineend"
3231        }
3232        incr y
3233        if {[string index $e end] eq "/" && $treeheight($path) > 1} {
3234            set y [highlight_tree $y $path]
3235        }
3236    }
3237    return $y
3238}
3239
3240proc treeclosedir {w dir} {
3241    global treediropen treeheight treeparent treeindex
3242
3243    set ix $treeindex($dir)
3244    $w conf -state normal
3245    $w delete s:$ix e:$ix
3246    set treediropen($dir) 0
3247    $w image configure a:$ix -image tri-rt
3248    $w conf -state disabled
3249    set n [expr {1 - $treeheight($dir)}]
3250    while {$dir ne {}} {
3251        incr treeheight($dir) $n
3252        set dir $treeparent($dir)
3253    }
3254}
3255
3256proc treeopendir {w dir} {
3257    global treediropen treeheight treeparent treecontents treeindex
3258
3259    set ix $treeindex($dir)
3260    $w conf -state normal
3261    $w image configure a:$ix -image tri-dn
3262    $w mark set e:$ix s:$ix
3263    $w mark gravity e:$ix right
3264    set lev 0
3265    set str "\n"
3266    set n [llength $treecontents($dir)]
3267    for {set x $dir} {$x ne {}} {set x $treeparent($x)} {
3268        incr lev
3269        append str "\t"
3270        incr treeheight($x) $n
3271    }
3272    foreach e $treecontents($dir) {
3273        set de $dir$e
3274        if {[string index $e end] eq "/"} {
3275            set iy $treeindex($de)
3276            $w mark set d:$iy e:$ix
3277            $w mark gravity d:$iy left
3278            $w insert e:$ix $str
3279            set treediropen($de) 0
3280            $w image create e:$ix -align center -image tri-rt -padx 1 \
3281                -name a:$iy
3282            $w insert e:$ix $e [highlight_tag $de]
3283            $w mark set s:$iy e:$ix
3284            $w mark gravity s:$iy left
3285            set treeheight($de) 1
3286        } else {
3287            $w insert e:$ix $str
3288            $w insert e:$ix $e [highlight_tag $de]
3289        }
3290    }
3291    $w mark gravity e:$ix right
3292    $w conf -state disabled
3293    set treediropen($dir) 1
3294    set top [lindex [split [$w index @0,0] .] 0]
3295    set ht [$w cget -height]
3296    set l [lindex [split [$w index s:$ix] .] 0]
3297    if {$l < $top} {
3298        $w yview $l.0
3299    } elseif {$l + $n + 1 > $top + $ht} {
3300        set top [expr {$l + $n + 2 - $ht}]
3301        if {$l < $top} {
3302            set top $l
3303        }
3304        $w yview $top.0
3305    }
3306}
3307
3308proc treeclick {w x y} {
3309    global treediropen cmitmode ctext cflist cflist_top
3310
3311    if {$cmitmode ne "tree"} return
3312    if {![info exists cflist_top]} return
3313    set l [lindex [split [$w index "@$x,$y"] "."] 0]
3314    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
3315    $cflist tag add highlight $l.0 "$l.0 lineend"
3316    set cflist_top $l
3317    if {$l == 1} {
3318        $ctext yview 1.0
3319        return
3320    }
3321    set e [linetoelt $l]
3322    if {[string index $e end] ne "/"} {
3323        showfile $e
3324    } elseif {$treediropen($e)} {
3325        treeclosedir $w $e
3326    } else {
3327        treeopendir $w $e
3328    }
3329}
3330
3331proc setfilelist {id} {
3332    global treefilelist cflist jump_to_here
3333
3334    treeview $cflist $treefilelist($id) 0
3335    if {$jump_to_here ne {}} {
3336        set f [lindex $jump_to_here 0]
3337        if {[lsearch -exact $treefilelist($id) $f] >= 0} {
3338            showfile $f
3339        }
3340    }
3341}
3342
3343image create bitmap tri-rt -background black -foreground blue -data {
3344    #define tri-rt_width 13
3345    #define tri-rt_height 13
3346    static unsigned char tri-rt_bits[] = {
3347       0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x70, 0x00, 0xf0, 0x00,
3348       0xf0, 0x01, 0xf0, 0x00, 0x70, 0x00, 0x30, 0x00, 0x10, 0x00, 0x00, 0x00,
3349       0x00, 0x00};
3350} -maskdata {
3351    #define tri-rt-mask_width 13
3352    #define tri-rt-mask_height 13
3353    static unsigned char tri-rt-mask_bits[] = {
3354       0x08, 0x00, 0x18, 0x00, 0x38, 0x00, 0x78, 0x00, 0xf8, 0x00, 0xf8, 0x01,
3355       0xf8, 0x03, 0xf8, 0x01, 0xf8, 0x00, 0x78, 0x00, 0x38, 0x00, 0x18, 0x00,
3356       0x08, 0x00};
3357}
3358image create bitmap tri-dn -background black -foreground blue -data {
3359    #define tri-dn_width 13
3360    #define tri-dn_height 13
3361    static unsigned char tri-dn_bits[] = {
3362       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x07, 0xf8, 0x03,
3363       0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
3364       0x00, 0x00};
3365} -maskdata {
3366    #define tri-dn-mask_width 13
3367    #define tri-dn-mask_height 13
3368    static unsigned char tri-dn-mask_bits[] = {
3369       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x1f, 0xfe, 0x0f, 0xfc, 0x07,
3370       0xf8, 0x03, 0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
3371       0x00, 0x00};
3372}
3373
3374image create bitmap reficon-T -background black -foreground yellow -data {
3375    #define tagicon_width 13
3376    #define tagicon_height 9
3377    static unsigned char tagicon_bits[] = {
3378       0x00, 0x00, 0x00, 0x00, 0xf0, 0x07, 0xf8, 0x07,
3379       0xfc, 0x07, 0xf8, 0x07, 0xf0, 0x07, 0x00, 0x00, 0x00, 0x00};
3380} -maskdata {
3381    #define tagicon-mask_width 13
3382    #define tagicon-mask_height 9
3383    static unsigned char tagicon-mask_bits[] = {
3384       0x00, 0x00, 0xf0, 0x0f, 0xf8, 0x0f, 0xfc, 0x0f,
3385       0xfe, 0x0f, 0xfc, 0x0f, 0xf8, 0x0f, 0xf0, 0x0f, 0x00, 0x00};
3386}
3387set rectdata {
3388    #define headicon_width 13
3389    #define headicon_height 9
3390    static unsigned char headicon_bits[] = {
3391       0x00, 0x00, 0x00, 0x00, 0xf8, 0x07, 0xf8, 0x07,
3392       0xf8, 0x07, 0xf8, 0x07, 0xf8, 0x07, 0x00, 0x00, 0x00, 0x00};
3393}
3394set rectmask {
3395    #define headicon-mask_width 13
3396    #define headicon-mask_height 9
3397    static unsigned char headicon-mask_bits[] = {
3398       0x00, 0x00, 0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f,
3399       0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f, 0xfc, 0x0f, 0x00, 0x00};
3400}
3401image create bitmap reficon-H -background black -foreground lime \
3402    -data $rectdata -maskdata $rectmask
3403image create bitmap reficon-o -background black -foreground "#ddddff" \
3404    -data $rectdata -maskdata $rectmask
3405
3406proc init_flist {first} {
3407    global cflist cflist_top difffilestart
3408
3409    $cflist conf -state normal
3410    $cflist delete 0.0 end
3411    if {$first ne {}} {
3412        $cflist insert end $first
3413        set cflist_top 1
3414        $cflist tag add highlight 1.0 "1.0 lineend"
3415    } else {
3416        unset -nocomplain cflist_top
3417    }
3418    $cflist conf -state disabled
3419    set difffilestart {}
3420}
3421
3422proc highlight_tag {f} {
3423    global highlight_paths
3424
3425    foreach p $highlight_paths {
3426        if {[string match $p $f]} {
3427            return "bold"
3428        }
3429    }
3430    return {}
3431}
3432
3433proc highlight_filelist {} {
3434    global cmitmode cflist
3435
3436    $cflist conf -state normal
3437    if {$cmitmode ne "tree"} {
3438        set end [lindex [split [$cflist index end] .] 0]
3439        for {set l 2} {$l < $end} {incr l} {
3440            set line [$cflist get $l.0 "$l.0 lineend"]
3441            if {[highlight_tag $line] ne {}} {
3442                $cflist tag add bold $l.0 "$l.0 lineend"
3443            }
3444        }
3445    } else {
3446        highlight_tree 2 {}
3447    }
3448    $cflist conf -state disabled
3449}
3450
3451proc unhighlight_filelist {} {
3452    global cflist
3453
3454    $cflist conf -state normal
3455    $cflist tag remove bold 1.0 end
3456    $cflist conf -state disabled
3457}
3458
3459proc add_flist {fl} {
3460    global cflist
3461
3462    $cflist conf -state normal
3463    foreach f $fl {
3464        $cflist insert end "\n"
3465        $cflist insert end $f [highlight_tag $f]
3466    }
3467    $cflist conf -state disabled
3468}
3469
3470proc sel_flist {w x y} {
3471    global ctext difffilestart cflist cflist_top cmitmode
3472
3473    if {$cmitmode eq "tree"} return
3474    if {![info exists cflist_top]} return
3475    set l [lindex [split [$w index "@$x,$y"] "."] 0]
3476    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
3477    $cflist tag add highlight $l.0 "$l.0 lineend"
3478    set cflist_top $l
3479    if {$l == 1} {
3480        $ctext yview 1.0
3481    } else {
3482        catch {$ctext yview [lindex $difffilestart [expr {$l - 2}]]}
3483    }
3484    suppress_highlighting_file_for_current_scrollpos
3485}
3486
3487proc pop_flist_menu {w X Y x y} {
3488    global ctext cflist cmitmode flist_menu flist_menu_file
3489    global treediffs diffids
3490
3491    stopfinding
3492    set l [lindex [split [$w index "@$x,$y"] "."] 0]
3493    if {$l <= 1} return
3494    if {$cmitmode eq "tree"} {
3495        set e [linetoelt $l]
3496        if {[string index $e end] eq "/"} return
3497    } else {
3498        set e [lindex $treediffs($diffids) [expr {$l-2}]]
3499    }
3500    set flist_menu_file $e
3501    set xdiffstate "normal"
3502    if {$cmitmode eq "tree"} {
3503        set xdiffstate "disabled"
3504    }
3505    # Disable "External diff" item in tree mode
3506    $flist_menu entryconf 2 -state $xdiffstate
3507    tk_popup $flist_menu $X $Y
3508}
3509
3510proc find_ctext_fileinfo {line} {
3511    global ctext_file_names ctext_file_lines
3512
3513    set ok [bsearch $ctext_file_lines $line]
3514    set tline [lindex $ctext_file_lines $ok]
3515
3516    if {$ok >= [llength $ctext_file_lines] || $line < $tline} {
3517        return {}
3518    } else {
3519        return [list [lindex $ctext_file_names $ok] $tline]
3520    }
3521}
3522
3523proc pop_diff_menu {w X Y x y} {
3524    global ctext diff_menu flist_menu_file
3525    global diff_menu_txtpos diff_menu_line
3526    global diff_menu_filebase
3527
3528    set diff_menu_txtpos [split [$w index "@$x,$y"] "."]
3529    set diff_menu_line [lindex $diff_menu_txtpos 0]
3530    # don't pop up the menu on hunk-separator or file-separator lines
3531    if {[lsearch -glob [$ctext tag names $diff_menu_line.0] "*sep"] >= 0} {
3532        return
3533    }
3534    stopfinding
3535    set f [find_ctext_fileinfo $diff_menu_line]
3536    if {$f eq {}} return
3537    set flist_menu_file [lindex $f 0]
3538    set diff_menu_filebase [lindex $f 1]
3539    tk_popup $diff_menu $X $Y
3540}
3541
3542proc flist_hl {only} {
3543    global flist_menu_file findstring gdttype
3544
3545    set x [shellquote $flist_menu_file]
3546    if {$only || $findstring eq {} || $gdttype ne [mc "touching paths:"]} {
3547        set findstring $x
3548    } else {
3549        append findstring " " $x
3550    }
3551    set gdttype [mc "touching paths:"]
3552}
3553
3554proc gitknewtmpdir {} {
3555    global diffnum gitktmpdir gitdir env
3556
3557    if {![info exists gitktmpdir]} {
3558        if {[info exists env(GITK_TMPDIR)]} {
3559            set tmpdir $env(GITK_TMPDIR)
3560        } elseif {[info exists env(TMPDIR)]} {
3561            set tmpdir $env(TMPDIR)
3562        } else {
3563            set tmpdir $gitdir
3564        }
3565        set gitktmpformat [file join $tmpdir ".gitk-tmp.XXXXXX"]
3566        if {[catch {set gitktmpdir [exec mktemp -d $gitktmpformat]}]} {
3567            set gitktmpdir [file join $gitdir [format ".gitk-tmp.%s" [pid]]]
3568        }
3569        if {[catch {file mkdir $gitktmpdir} err]} {
3570            error_popup "[mc "Error creating temporary directory %s:" $gitktmpdir] $err"
3571            unset gitktmpdir
3572            return {}
3573        }
3574        set diffnum 0
3575    }
3576    incr diffnum
3577    set diffdir [file join $gitktmpdir $diffnum]
3578    if {[catch {file mkdir $diffdir} err]} {
3579        error_popup "[mc "Error creating temporary directory %s:" $diffdir] $err"
3580        return {}
3581    }
3582    return $diffdir
3583}
3584
3585proc save_file_from_commit {filename output what} {
3586    global nullfile
3587
3588    if {[catch {exec git show $filename -- > $output} err]} {
3589        if {[string match "fatal: bad revision *" $err]} {
3590            return $nullfile
3591        }
3592        error_popup "[mc "Error getting \"%s\" from %s:" $filename $what] $err"
3593        return {}
3594    }
3595    return $output
3596}
3597
3598proc external_diff_get_one_file {diffid filename diffdir} {
3599    global nullid nullid2 nullfile
3600    global worktree
3601
3602    if {$diffid == $nullid} {
3603        set difffile [file join $worktree $filename]
3604        if {[file exists $difffile]} {
3605            return $difffile
3606        }
3607        return $nullfile
3608    }
3609    if {$diffid == $nullid2} {
3610        set difffile [file join $diffdir "\[index\] [file tail $filename]"]
3611        return [save_file_from_commit :$filename $difffile index]
3612    }
3613    set difffile [file join $diffdir "\[$diffid\] [file tail $filename]"]
3614    return [save_file_from_commit $diffid:$filename $difffile \
3615               "revision $diffid"]
3616}
3617
3618proc external_diff {} {
3619    global nullid nullid2
3620    global flist_menu_file
3621    global diffids
3622    global extdifftool
3623
3624    if {[llength $diffids] == 1} {
3625        # no reference commit given
3626        set diffidto [lindex $diffids 0]
3627        if {$diffidto eq $nullid} {
3628            # diffing working copy with index
3629            set diffidfrom $nullid2
3630        } elseif {$diffidto eq $nullid2} {
3631            # diffing index with HEAD
3632            set diffidfrom "HEAD"
3633        } else {
3634            # use first parent commit
3635            global parentlist selectedline
3636            set diffidfrom [lindex $parentlist $selectedline 0]
3637        }
3638    } else {
3639        set diffidfrom [lindex $diffids 0]
3640        set diffidto [lindex $diffids 1]
3641    }
3642
3643    # make sure that several diffs wont collide
3644    set diffdir [gitknewtmpdir]
3645    if {$diffdir eq {}} return
3646
3647    # gather files to diff
3648    set difffromfile [external_diff_get_one_file $diffidfrom $flist_menu_file $diffdir]
3649    set difftofile [external_diff_get_one_file $diffidto $flist_menu_file $diffdir]
3650
3651    if {$difffromfile ne {} && $difftofile ne {}} {
3652        set cmd [list [shellsplit $extdifftool] $difffromfile $difftofile]
3653        if {[catch {set fl [open |$cmd r]} err]} {
3654            file delete -force $diffdir
3655            error_popup "$extdifftool: [mc "command failed:"] $err"
3656        } else {
3657            fconfigure $fl -blocking 0
3658            filerun $fl [list delete_at_eof $fl $diffdir]
3659        }
3660    }
3661}
3662
3663proc find_hunk_blamespec {base line} {
3664    global ctext
3665
3666    # Find and parse the hunk header
3667    set s_lix [$ctext search -backwards -regexp ^@@ "$line.0 lineend" $base.0]
3668    if {$s_lix eq {}} return
3669
3670    set s_line [$ctext get $s_lix "$s_lix + 1 lines"]
3671    if {![regexp {^@@@*(( -\d+(,\d+)?)+) \+(\d+)(,\d+)? @@} $s_line \
3672            s_line old_specs osz osz1 new_line nsz]} {
3673        return
3674    }
3675
3676    # base lines for the parents
3677    set base_lines [list $new_line]
3678    foreach old_spec [lrange [split $old_specs " "] 1 end] {
3679        if {![regexp -- {-(\d+)(,\d+)?} $old_spec \
3680                old_spec old_line osz]} {
3681            return
3682        }
3683        lappend base_lines $old_line
3684    }
3685
3686    # Now scan the lines to determine offset within the hunk
3687    set max_parent [expr {[llength $base_lines]-2}]
3688    set dline 0
3689    set s_lno [lindex [split $s_lix "."] 0]
3690
3691    # Determine if the line is removed
3692    set chunk [$ctext get $line.0 "$line.1 + $max_parent chars"]
3693    if {[string match {[-+ ]*} $chunk]} {
3694        set removed_idx [string first "-" $chunk]
3695        # Choose a parent index
3696        if {$removed_idx >= 0} {
3697            set parent $removed_idx
3698        } else {
3699            set unchanged_idx [string first " " $chunk]
3700            if {$unchanged_idx >= 0} {
3701                set parent $unchanged_idx
3702            } else {
3703                # blame the current commit
3704                set parent -1
3705            }
3706        }
3707        # then count other lines that belong to it
3708        for {set i $line} {[incr i -1] > $s_lno} {} {
3709            set chunk [$ctext get $i.0 "$i.1 + $max_parent chars"]
3710            # Determine if the line is removed
3711            set removed_idx [string first "-" $chunk]
3712            if {$parent >= 0} {
3713                set code [string index $chunk $parent]
3714                if {$code eq "-" || ($removed_idx < 0 && $code ne "+")} {
3715                    incr dline
3716                }
3717            } else {
3718                if {$removed_idx < 0} {
3719                    incr dline
3720                }
3721            }
3722        }
3723        incr parent
3724    } else {
3725        set parent 0
3726    }
3727
3728    incr dline [lindex $base_lines $parent]
3729    return [list $parent $dline]
3730}
3731
3732proc external_blame_diff {} {
3733    global currentid cmitmode
3734    global diff_menu_txtpos diff_menu_line
3735    global diff_menu_filebase flist_menu_file
3736
3737    if {$cmitmode eq "tree"} {
3738        set parent_idx 0
3739        set line [expr {$diff_menu_line - $diff_menu_filebase}]
3740    } else {
3741        set hinfo [find_hunk_blamespec $diff_menu_filebase $diff_menu_line]
3742        if {$hinfo ne {}} {
3743            set parent_idx [lindex $hinfo 0]
3744            set line [lindex $hinfo 1]
3745        } else {
3746            set parent_idx 0
3747            set line 0
3748        }
3749    }
3750
3751    external_blame $parent_idx $line
3752}
3753
3754# Find the SHA1 ID of the blob for file $fname in the index
3755# at stage 0 or 2
3756proc index_sha1 {fname} {
3757    set f [open [list | git ls-files -s $fname] r]
3758    while {[gets $f line] >= 0} {
3759        set info [lindex [split $line "\t"] 0]
3760        set stage [lindex $info 2]
3761        if {$stage eq "0" || $stage eq "2"} {
3762            close $f
3763            return [lindex $info 1]
3764        }
3765    }
3766    close $f
3767    return {}
3768}
3769
3770# Turn an absolute path into one relative to the current directory
3771proc make_relative {f} {
3772    if {[file pathtype $f] eq "relative"} {
3773        return $f
3774    }
3775    set elts [file split $f]
3776    set here [file split [pwd]]
3777    set ei 0
3778    set hi 0
3779    set res {}
3780    foreach d $here {
3781        if {$ei < $hi || $ei >= [llength $elts] || [lindex $elts $ei] ne $d} {
3782            lappend res ".."
3783        } else {
3784            incr ei
3785        }
3786        incr hi
3787    }
3788    set elts [concat $res [lrange $elts $ei end]]
3789    return [eval file join $elts]
3790}
3791
3792proc external_blame {parent_idx {line {}}} {
3793    global flist_menu_file cdup
3794    global nullid nullid2
3795    global parentlist selectedline currentid
3796
3797    if {$parent_idx > 0} {
3798        set base_commit [lindex $parentlist $selectedline [expr {$parent_idx-1}]]
3799    } else {
3800        set base_commit $currentid
3801    }
3802
3803    if {$base_commit eq {} || $base_commit eq $nullid || $base_commit eq $nullid2} {
3804        error_popup [mc "No such commit"]
3805        return
3806    }
3807
3808    set cmdline [list git gui blame]
3809    if {$line ne {} && $line > 1} {
3810        lappend cmdline "--line=$line"
3811    }
3812    set f [file join $cdup $flist_menu_file]
3813    # Unfortunately it seems git gui blame doesn't like
3814    # being given an absolute path...
3815    set f [make_relative $f]
3816    lappend cmdline $base_commit $f
3817    if {[catch {eval exec $cmdline &} err]} {
3818        error_popup "[mc "git gui blame: command failed:"] $err"
3819    }
3820}
3821
3822proc show_line_source {} {
3823    global cmitmode currentid parents curview blamestuff blameinst
3824    global diff_menu_line diff_menu_filebase flist_menu_file
3825    global nullid nullid2 gitdir cdup
3826
3827    set from_index {}
3828    if {$cmitmode eq "tree"} {
3829        set id $currentid
3830        set line [expr {$diff_menu_line - $diff_menu_filebase}]
3831    } else {
3832        set h [find_hunk_blamespec $diff_menu_filebase $diff_menu_line]
3833        if {$h eq {}} return
3834        set pi [lindex $h 0]
3835        if {$pi == 0} {
3836            mark_ctext_line $diff_menu_line
3837            return
3838        }
3839        incr pi -1
3840        if {$currentid eq $nullid} {
3841            if {$pi > 0} {
3842                # must be a merge in progress...
3843                if {[catch {
3844                    # get the last line from .git/MERGE_HEAD
3845                    set f [open [file join $gitdir MERGE_HEAD] r]
3846                    set id [lindex [split [read $f] "\n"] end-1]
3847                    close $f
3848                } err]} {
3849                    error_popup [mc "Couldn't read merge head: %s" $err]
3850                    return
3851                }
3852            } elseif {$parents($curview,$currentid) eq $nullid2} {
3853                # need to do the blame from the index
3854                if {[catch {
3855                    set from_index [index_sha1 $flist_menu_file]
3856                } err]} {
3857                    error_popup [mc "Error reading index: %s" $err]
3858                    return
3859                }
3860            } else {
3861                set id $parents($curview,$currentid)
3862            }
3863        } else {
3864            set id [lindex $parents($curview,$currentid) $pi]
3865        }
3866        set line [lindex $h 1]
3867    }
3868    set blameargs {}
3869    if {$from_index ne {}} {
3870        lappend blameargs | git cat-file blob $from_index
3871    }
3872    lappend blameargs | git blame -p -L$line,+1
3873    if {$from_index ne {}} {
3874        lappend blameargs --contents -
3875    } else {
3876        lappend blameargs $id
3877    }
3878    lappend blameargs -- [file join $cdup $flist_menu_file]
3879    if {[catch {
3880        set f [open $blameargs r]
3881    } err]} {
3882        error_popup [mc "Couldn't start git blame: %s" $err]
3883        return
3884    }
3885    nowbusy blaming [mc "Searching"]
3886    fconfigure $f -blocking 0
3887    set i [reg_instance $f]
3888    set blamestuff($i) {}
3889    set blameinst $i
3890    filerun $f [list read_line_source $f $i]
3891}
3892
3893proc stopblaming {} {
3894    global blameinst
3895
3896    if {[info exists blameinst]} {
3897        stop_instance $blameinst
3898        unset blameinst
3899        notbusy blaming
3900    }
3901}
3902
3903proc read_line_source {fd inst} {
3904    global blamestuff curview commfd blameinst nullid nullid2
3905
3906    while {[gets $fd line] >= 0} {
3907        lappend blamestuff($inst) $line
3908    }
3909    if {![eof $fd]} {
3910        return 1
3911    }
3912    unset commfd($inst)
3913    unset blameinst
3914    notbusy blaming
3915    fconfigure $fd -blocking 1
3916    if {[catch {close $fd} err]} {
3917        error_popup [mc "Error running git blame: %s" $err]
3918        return 0
3919    }
3920
3921    set fname {}
3922    set line [split [lindex $blamestuff($inst) 0] " "]
3923    set id [lindex $line 0]
3924    set lnum [lindex $line 1]
3925    if {[string length $id] == 40 && [string is xdigit $id] &&
3926        [string is digit -strict $lnum]} {
3927        # look for "filename" line
3928        foreach l $blamestuff($inst) {
3929            if {[string match "filename *" $l]} {
3930                set fname [string range $l 9 end]
3931                break
3932            }
3933        }
3934    }
3935    if {$fname ne {}} {
3936        # all looks good, select it
3937        if {$id eq $nullid} {
3938            # blame uses all-zeroes to mean not committed,
3939            # which would mean a change in the index
3940            set id $nullid2
3941        }
3942        if {[commitinview $id $curview]} {
3943            selectline [rowofcommit $id] 1 [list $fname $lnum] 1
3944        } else {
3945            error_popup [mc "That line comes from commit %s, \
3946                             which is not in this view" [shortids $id]]
3947        }
3948    } else {
3949        puts "oops couldn't parse git blame output"
3950    }
3951    return 0
3952}
3953
3954# delete $dir when we see eof on $f (presumably because the child has exited)
3955proc delete_at_eof {f dir} {
3956    while {[gets $f line] >= 0} {}
3957    if {[eof $f]} {
3958        if {[catch {close $f} err]} {
3959            error_popup "[mc "External diff viewer failed:"] $err"
3960        }
3961        file delete -force $dir
3962        return 0
3963    }
3964    return 1
3965}
3966
3967# Functions for adding and removing shell-type quoting
3968
3969proc shellquote {str} {
3970    if {![string match "*\['\"\\ \t]*" $str]} {
3971        return $str
3972    }
3973    if {![string match "*\['\"\\]*" $str]} {
3974        return "\"$str\""
3975    }
3976    if {![string match "*'*" $str]} {
3977        return "'$str'"
3978    }
3979    return "\"[string map {\" \\\" \\ \\\\} $str]\""
3980}
3981
3982proc shellarglist {l} {
3983    set str {}
3984    foreach a $l {
3985        if {$str ne {}} {
3986            append str " "
3987        }
3988        append str [shellquote $a]
3989    }
3990    return $str
3991}
3992
3993proc shelldequote {str} {
3994    set ret {}
3995    set used -1
3996    while {1} {
3997        incr used
3998        if {![regexp -start $used -indices "\['\"\\\\ \t]" $str first]} {
3999            append ret [string range $str $used end]
4000            set used [string length $str]
4001            break
4002        }
4003        set first [lindex $first 0]
4004        set ch [string index $str $first]
4005        if {$first > $used} {
4006            append ret [string range $str $used [expr {$first - 1}]]
4007            set used $first
4008        }
4009        if {$ch eq " " || $ch eq "\t"} break
4010        incr used
4011        if {$ch eq "'"} {
4012            set first [string first "'" $str $used]
4013            if {$first < 0} {
4014                error "unmatched single-quote"
4015            }
4016            append ret [string range $str $used [expr {$first - 1}]]
4017            set used $first
4018            continue
4019        }
4020        if {$ch eq "\\"} {
4021            if {$used >= [string length $str]} {
4022                error "trailing backslash"
4023            }
4024            append ret [string index $str $used]
4025            continue
4026        }
4027        # here ch == "\""
4028        while {1} {
4029            if {![regexp -start $used -indices "\[\"\\\\]" $str first]} {
4030                error "unmatched double-quote"
4031            }
4032            set first [lindex $first 0]
4033            set ch [string index $str $first]
4034            if {$first > $used} {
4035                append ret [string range $str $used [expr {$first - 1}]]
4036                set used $first
4037            }
4038            if {$ch eq "\""} break
4039            incr used
4040            append ret [string index $str $used]
4041            incr used
4042        }
4043    }
4044    return [list $used $ret]
4045}
4046
4047proc shellsplit {str} {
4048    set l {}
4049    while {1} {
4050        set str [string trimleft $str]
4051        if {$str eq {}} break
4052        set dq [shelldequote $str]
4053        set n [lindex $dq 0]
4054        set word [lindex $dq 1]
4055        set str [string range $str $n end]
4056        lappend l $word
4057    }
4058    return $l
4059}
4060
4061proc set_window_title {} {
4062    global appname curview viewname vrevs
4063    set rev [mc "All files"]
4064    if {$curview ne 0} {
4065        if {$viewname($curview) eq [mc "Command line"]} {
4066            set rev [string map {"--gitk-symmetric-diff-marker" "--merge"} $vrevs($curview)]
4067        } else {
4068            set rev $viewname($curview)
4069        }
4070    }
4071    wm title . "[reponame]: $rev - $appname"
4072}
4073
4074# Code to implement multiple views
4075
4076proc newview {ishighlight} {
4077    global nextviewnum newviewname newishighlight
4078    global revtreeargs viewargscmd newviewopts curview
4079
4080    set newishighlight $ishighlight
4081    set top .gitkview
4082    if {[winfo exists $top]} {
4083        raise $top
4084        return
4085    }
4086    decode_view_opts $nextviewnum $revtreeargs
4087    set newviewname($nextviewnum) "[mc "View"] $nextviewnum"
4088    set newviewopts($nextviewnum,perm) 0
4089    set newviewopts($nextviewnum,cmd)  $viewargscmd($curview)
4090    vieweditor $top $nextviewnum [mc "Gitk view definition"]
4091}
4092
4093set known_view_options {
4094    {perm      b    .  {}               {mc "Remember this view"}}
4095    {reflabel  l    +  {}               {mc "References (space separated list):"}}
4096    {refs      t15  .. {}               {mc "Branches & tags:"}}
4097    {allrefs   b    *. "--all"          {mc "All refs"}}
4098    {branches  b    .  "--branches"     {mc "All (local) branches"}}
4099    {tags      b    .  "--tags"         {mc "All tags"}}
4100    {remotes   b    .  "--remotes"      {mc "All remote-tracking branches"}}
4101    {commitlbl l    +  {}               {mc "Commit Info (regular expressions):"}}
4102    {author    t15  .. "--author=*"     {mc "Author:"}}
4103    {committer t15  .  "--committer=*"  {mc "Committer:"}}
4104    {loginfo   t15  .. "--grep=*"       {mc "Commit Message:"}}
4105    {allmatch  b    .. "--all-match"    {mc "Matches all Commit Info criteria"}}
4106    {igrep     b    .. "--invert-grep"  {mc "Matches no Commit Info criteria"}}
4107    {changes_l l    +  {}               {mc "Changes to Files:"}}
4108    {pickaxe_s r0   .  {}               {mc "Fixed String"}}
4109    {pickaxe_t r1   .  "--pickaxe-regex"  {mc "Regular Expression"}}
4110    {pickaxe   t15  .. "-S*"            {mc "Search string:"}}
4111    {datelabel l    +  {}               {mc "Commit Dates (\"2 weeks ago\", \"2009-03-17 15:27:38\", \"March 17, 2009 15:27:38\"):"}}
4112    {since     t15  ..  {"--since=*" "--after=*"}  {mc "Since:"}}
4113    {until     t15  .   {"--until=*" "--before=*"} {mc "Until:"}}
4114    {limit_lbl l    +  {}               {mc "Limit and/or skip a number of revisions (positive integer):"}}
4115    {limit     t10  *. "--max-count=*"  {mc "Number to show:"}}
4116    {skip      t10  .  "--skip=*"       {mc "Number to skip:"}}
4117    {misc_lbl  l    +  {}               {mc "Miscellaneous options:"}}
4118    {dorder    b    *. {"--date-order" "-d"}      {mc "Strictly sort by date"}}
4119    {lright    b    .  "--left-right"   {mc "Mark branch sides"}}
4120    {first     b    .  "--first-parent" {mc "Limit to first parent"}}
4121    {smplhst   b    .  "--simplify-by-decoration"   {mc "Simple history"}}
4122    {args      t50  *. {}               {mc "Additional arguments to git log:"}}
4123    {allpaths  path +  {}               {mc "Enter files and directories to include, one per line:"}}
4124    {cmd       t50= +  {}               {mc "Command to generate more commits to include:"}}
4125    }
4126
4127# Convert $newviewopts($n, ...) into args for git log.
4128proc encode_view_opts {n} {
4129    global known_view_options newviewopts
4130
4131    set rargs [list]
4132    foreach opt $known_view_options {
4133        set patterns [lindex $opt 3]
4134        if {$patterns eq {}} continue
4135        set pattern [lindex $patterns 0]
4136
4137        if {[lindex $opt 1] eq "b"} {
4138            set val $newviewopts($n,[lindex $opt 0])
4139            if {$val} {
4140                lappend rargs $pattern
4141            }
4142        } elseif {[regexp {^r(\d+)$} [lindex $opt 1] type value]} {
4143            regexp {^(.*_)} [lindex $opt 0] uselessvar button_id
4144            set val $newviewopts($n,$button_id)
4145            if {$val eq $value} {
4146                lappend rargs $pattern
4147            }
4148        } else {
4149            set val $newviewopts($n,[lindex $opt 0])
4150            set val [string trim $val]
4151            if {$val ne {}} {
4152                set pfix [string range $pattern 0 end-1]
4153                lappend rargs $pfix$val
4154            }
4155        }
4156    }
4157    set rargs [concat $rargs [shellsplit $newviewopts($n,refs)]]
4158    return [concat $rargs [shellsplit $newviewopts($n,args)]]
4159}
4160
4161# Fill $newviewopts($n, ...) based on args for git log.
4162proc decode_view_opts {n view_args} {
4163    global known_view_options newviewopts
4164
4165    foreach opt $known_view_options {
4166        set id [lindex $opt 0]
4167        if {[lindex $opt 1] eq "b"} {
4168            # Checkboxes
4169            set val 0
4170        } elseif {[regexp {^r(\d+)$} [lindex $opt 1]]} {
4171            # Radiobuttons
4172            regexp {^(.*_)} $id uselessvar id
4173            set val 0
4174        } else {
4175            # Text fields
4176            set val {}
4177        }
4178        set newviewopts($n,$id) $val
4179    }
4180    set oargs [list]
4181    set refargs [list]
4182    foreach arg $view_args {
4183        if {[regexp -- {^-([0-9]+)$} $arg arg cnt]
4184            && ![info exists found(limit)]} {
4185            set newviewopts($n,limit) $cnt
4186            set found(limit) 1
4187            continue
4188        }
4189        catch { unset val }
4190        foreach opt $known_view_options {
4191            set id [lindex $opt 0]
4192            if {[info exists found($id)]} continue
4193            foreach pattern [lindex $opt 3] {
4194                if {![string match $pattern $arg]} continue
4195                if {[lindex $opt 1] eq "b"} {
4196                    # Check buttons
4197                    set val 1
4198                } elseif {[regexp {^r(\d+)$} [lindex $opt 1] match num]} {
4199                    # Radio buttons
4200                    regexp {^(.*_)} $id uselessvar id
4201                    set val $num
4202                } else {
4203                    # Text input fields
4204                    set size [string length $pattern]
4205                    set val [string range $arg [expr {$size-1}] end]
4206                }
4207                set newviewopts($n,$id) $val
4208                set found($id) 1
4209                break
4210            }
4211            if {[info exists val]} break
4212        }
4213        if {[info exists val]} continue
4214        if {[regexp {^-} $arg]} {
4215            lappend oargs $arg
4216        } else {
4217            lappend refargs $arg
4218        }
4219    }
4220    set newviewopts($n,refs) [shellarglist $refargs]
4221    set newviewopts($n,args) [shellarglist $oargs]
4222}
4223
4224proc edit_or_newview {} {
4225    global curview
4226
4227    if {$curview > 0} {
4228        editview
4229    } else {
4230        newview 0
4231    }
4232}
4233
4234proc editview {} {
4235    global curview
4236    global viewname viewperm newviewname newviewopts
4237    global viewargs viewargscmd
4238
4239    set top .gitkvedit-$curview
4240    if {[winfo exists $top]} {
4241        raise $top
4242        return
4243    }
4244    decode_view_opts $curview $viewargs($curview)
4245    set newviewname($curview)      $viewname($curview)
4246    set newviewopts($curview,perm) $viewperm($curview)
4247    set newviewopts($curview,cmd)  $viewargscmd($curview)
4248    vieweditor $top $curview "[mc "Gitk: edit view"] $viewname($curview)"
4249}
4250
4251proc vieweditor {top n title} {
4252    global newviewname newviewopts viewfiles bgcolor
4253    global known_view_options NS
4254
4255    ttk_toplevel $top
4256    wm title $top [concat $title [mc "-- criteria for selecting revisions"]]
4257    make_transient $top .
4258
4259    # View name
4260    ${NS}::frame $top.nfr
4261    ${NS}::label $top.nl -text [mc "View Name"]
4262    ${NS}::entry $top.name -width 20 -textvariable newviewname($n)
4263    pack $top.nfr -in $top -fill x -pady 5 -padx 3
4264    pack $top.nl -in $top.nfr -side left -padx {0 5}
4265    pack $top.name -in $top.nfr -side left -padx {0 25}
4266
4267    # View options
4268    set cframe $top.nfr
4269    set cexpand 0
4270    set cnt 0
4271    foreach opt $known_view_options {
4272        set id [lindex $opt 0]
4273        set type [lindex $opt 1]
4274        set flags [lindex $opt 2]
4275        set title [eval [lindex $opt 4]]
4276        set lxpad 0
4277
4278        if {$flags eq "+" || $flags eq "*"} {
4279            set cframe $top.fr$cnt
4280            incr cnt
4281            ${NS}::frame $cframe
4282            pack $cframe -in $top -fill x -pady 3 -padx 3
4283            set cexpand [expr {$flags eq "*"}]
4284        } elseif {$flags eq ".." || $flags eq "*."} {
4285            set cframe $top.fr$cnt
4286            incr cnt
4287            ${NS}::frame $cframe
4288            pack $cframe -in $top -fill x -pady 3 -padx [list 15 3]
4289            set cexpand [expr {$flags eq "*."}]
4290        } else {
4291            set lxpad 5
4292        }
4293
4294        if {$type eq "l"} {
4295            ${NS}::label $cframe.l_$id -text $title
4296            pack $cframe.l_$id -in $cframe -side left -pady [list 3 0] -anchor w
4297        } elseif {$type eq "b"} {
4298            ${NS}::checkbutton $cframe.c_$id -text $title -variable newviewopts($n,$id)
4299            pack $cframe.c_$id -in $cframe -side left \
4300                -padx [list $lxpad 0] -expand $cexpand -anchor w
4301        } elseif {[regexp {^r(\d+)$} $type type sz]} {
4302            regexp {^(.*_)} $id uselessvar button_id
4303            ${NS}::radiobutton $cframe.c_$id -text $title -variable newviewopts($n,$button_id) -value $sz
4304            pack $cframe.c_$id -in $cframe -side left \
4305                -padx [list $lxpad 0] -expand $cexpand -anchor w
4306        } elseif {[regexp {^t(\d+)$} $type type sz]} {
4307            ${NS}::label $cframe.l_$id -text $title
4308            ${NS}::entry $cframe.e_$id -width $sz -background $bgcolor \
4309                -textvariable newviewopts($n,$id)
4310            pack $cframe.l_$id -in $cframe -side left -padx [list $lxpad 0]
4311            pack $cframe.e_$id -in $cframe -side left -expand 1 -fill x
4312        } elseif {[regexp {^t(\d+)=$} $type type sz]} {
4313            ${NS}::label $cframe.l_$id -text $title
4314            ${NS}::entry $cframe.e_$id -width $sz -background $bgcolor \
4315                -textvariable newviewopts($n,$id)
4316            pack $cframe.l_$id -in $cframe -side top -pady [list 3 0] -anchor w
4317            pack $cframe.e_$id -in $cframe -side top -fill x
4318        } elseif {$type eq "path"} {
4319            ${NS}::label $top.l -text $title
4320            pack $top.l -in $top -side top -pady [list 3 0] -anchor w -padx 3
4321            text $top.t -width 40 -height 5 -background $bgcolor
4322            if {[info exists viewfiles($n)]} {
4323                foreach f $viewfiles($n) {
4324                    $top.t insert end $f
4325                    $top.t insert end "\n"
4326                }
4327                $top.t delete {end - 1c} end
4328                $top.t mark set insert 0.0
4329            }
4330            pack $top.t -in $top -side top -pady [list 0 5] -fill both -expand 1 -padx 3
4331        }
4332    }
4333
4334    ${NS}::frame $top.buts
4335    ${NS}::button $top.buts.ok -text [mc "OK"] -command [list newviewok $top $n]
4336    ${NS}::button $top.buts.apply -text [mc "Apply (F5)"] -command [list newviewok $top $n 1]
4337    ${NS}::button $top.buts.can -text [mc "Cancel"] -command [list destroy $top]
4338    bind $top <Control-Return> [list newviewok $top $n]
4339    bind $top <F5> [list newviewok $top $n 1]
4340    bind $top <Escape> [list destroy $top]
4341    grid $top.buts.ok $top.buts.apply $top.buts.can
4342    grid columnconfigure $top.buts 0 -weight 1 -uniform a
4343    grid columnconfigure $top.buts 1 -weight 1 -uniform a
4344    grid columnconfigure $top.buts 2 -weight 1 -uniform a
4345    pack $top.buts -in $top -side top -fill x
4346    focus $top.t
4347}
4348
4349proc doviewmenu {m first cmd op argv} {
4350    set nmenu [$m index end]
4351    for {set i $first} {$i <= $nmenu} {incr i} {
4352        if {[$m entrycget $i -command] eq $cmd} {
4353            eval $m $op $i $argv
4354            break
4355        }
4356    }
4357}
4358
4359proc allviewmenus {n op args} {
4360    # global viewhlmenu
4361
4362    doviewmenu .bar.view 5 [list showview $n] $op $args
4363    # doviewmenu $viewhlmenu 1 [list addvhighlight $n] $op $args
4364}
4365
4366proc newviewok {top n {apply 0}} {
4367    global nextviewnum newviewperm newviewname newishighlight
4368    global viewname viewfiles viewperm viewchanged selectedview curview
4369    global viewargs viewargscmd newviewopts viewhlmenu
4370
4371    if {[catch {
4372        set newargs [encode_view_opts $n]
4373    } err]} {
4374        error_popup "[mc "Error in commit selection arguments:"] $err" $top
4375        return
4376    }
4377    set files {}
4378    foreach f [split [$top.t get 0.0 end] "\n"] {
4379        set ft [string trim $f]
4380        if {$ft ne {}} {
4381            lappend files $ft
4382        }
4383    }
4384    if {![info exists viewfiles($n)]} {
4385        # creating a new view
4386        incr nextviewnum
4387        set viewname($n) $newviewname($n)
4388        set viewperm($n) $newviewopts($n,perm)
4389        set viewchanged($n) 1
4390        set viewfiles($n) $files
4391        set viewargs($n) $newargs
4392        set viewargscmd($n) $newviewopts($n,cmd)
4393        addviewmenu $n
4394        if {!$newishighlight} {
4395            run showview $n
4396        } else {
4397            run addvhighlight $n
4398        }
4399    } else {
4400        # editing an existing view
4401        set viewperm($n) $newviewopts($n,perm)
4402        set viewchanged($n) 1
4403        if {$newviewname($n) ne $viewname($n)} {
4404            set viewname($n) $newviewname($n)
4405            doviewmenu .bar.view 5 [list showview $n] \
4406                entryconf [list -label $viewname($n)]
4407            # doviewmenu $viewhlmenu 1 [list addvhighlight $n] \
4408                # entryconf [list -label $viewname($n) -value $viewname($n)]
4409        }
4410        if {$files ne $viewfiles($n) || $newargs ne $viewargs($n) || \
4411                $newviewopts($n,cmd) ne $viewargscmd($n)} {
4412            set viewfiles($n) $files
4413            set viewargs($n) $newargs
4414            set viewargscmd($n) $newviewopts($n,cmd)
4415            if {$curview == $n} {
4416                run reloadcommits
4417            }
4418        }
4419    }
4420    if {$apply} return
4421    catch {destroy $top}
4422}
4423
4424proc delview {} {
4425    global curview viewperm hlview selectedhlview viewchanged
4426
4427    if {$curview == 0} return
4428    if {[info exists hlview] && $hlview == $curview} {
4429        set selectedhlview [mc "None"]
4430        unset hlview
4431    }
4432    allviewmenus $curview delete
4433    set viewperm($curview) 0
4434    set viewchanged($curview) 1
4435    showview 0
4436}
4437
4438proc addviewmenu {n} {
4439    global viewname viewhlmenu
4440
4441    .bar.view add radiobutton -label $viewname($n) \
4442        -command [list showview $n] -variable selectedview -value $n
4443    #$viewhlmenu add radiobutton -label $viewname($n) \
4444    #   -command [list addvhighlight $n] -variable selectedhlview
4445}
4446
4447proc showview {n} {
4448    global curview cached_commitrow ordertok
4449    global displayorder parentlist rowidlist rowisopt rowfinal
4450    global colormap rowtextx nextcolor canvxmax
4451    global numcommits viewcomplete
4452    global selectedline currentid canv canvy0
4453    global treediffs
4454    global pending_select mainheadid
4455    global commitidx
4456    global selectedview
4457    global hlview selectedhlview commitinterest
4458
4459    if {$n == $curview} return
4460    set selid {}
4461    set ymax [lindex [$canv cget -scrollregion] 3]
4462    set span [$canv yview]
4463    set ytop [expr {[lindex $span 0] * $ymax}]
4464    set ybot [expr {[lindex $span 1] * $ymax}]
4465    set yscreen [expr {($ybot - $ytop) / 2}]
4466    if {$selectedline ne {}} {
4467        set selid $currentid
4468        set y [yc $selectedline]
4469        if {$ytop < $y && $y < $ybot} {
4470            set yscreen [expr {$y - $ytop}]
4471        }
4472    } elseif {[info exists pending_select]} {
4473        set selid $pending_select
4474        unset pending_select
4475    }
4476    unselectline
4477    normalline
4478    unset -nocomplain treediffs
4479    clear_display
4480    if {[info exists hlview] && $hlview == $n} {
4481        unset hlview
4482        set selectedhlview [mc "None"]
4483    }
4484    unset -nocomplain commitinterest
4485    unset -nocomplain cached_commitrow
4486    unset -nocomplain ordertok
4487
4488    set curview $n
4489    set selectedview $n
4490    .bar.view entryconf [mca "&Edit view..."] -state [expr {$n == 0? "disabled": "normal"}]
4491    .bar.view entryconf [mca "&Delete view"] -state [expr {$n == 0? "disabled": "normal"}]
4492
4493    run refill_reflist
4494    if {![info exists viewcomplete($n)]} {
4495        getcommits $selid
4496        return
4497    }
4498
4499    set displayorder {}
4500    set parentlist {}
4501    set rowidlist {}
4502    set rowisopt {}
4503    set rowfinal {}
4504    set numcommits $commitidx($n)
4505
4506    unset -nocomplain colormap
4507    unset -nocomplain rowtextx
4508    set nextcolor 0
4509    set canvxmax [$canv cget -width]
4510    set curview $n
4511    set row 0
4512    setcanvscroll
4513    set yf 0
4514    set row {}
4515    if {$selid ne {} && [commitinview $selid $n]} {
4516        set row [rowofcommit $selid]
4517        # try to get the selected row in the same position on the screen
4518        set ymax [lindex [$canv cget -scrollregion] 3]
4519        set ytop [expr {[yc $row] - $yscreen}]
4520        if {$ytop < 0} {
4521            set ytop 0
4522        }
4523        set yf [expr {$ytop * 1.0 / $ymax}]
4524    }
4525    allcanvs yview moveto $yf
4526    drawvisible
4527    if {$row ne {}} {
4528        selectline $row 0
4529    } elseif {!$viewcomplete($n)} {
4530        reset_pending_select $selid
4531    } else {
4532        reset_pending_select {}
4533
4534        if {[commitinview $pending_select $curview]} {
4535            selectline [rowofcommit $pending_select] 1
4536        } else {
4537            set row [first_real_row]
4538            if {$row < $numcommits} {
4539                selectline $row 0
4540            }
4541        }
4542    }
4543    if {!$viewcomplete($n)} {
4544        if {$numcommits == 0} {
4545            show_status [mc "Reading commits..."]
4546        }
4547    } elseif {$numcommits == 0} {
4548        show_status [mc "No commits selected"]
4549    }
4550    set_window_title
4551}
4552
4553# Stuff relating to the highlighting facility
4554
4555proc ishighlighted {id} {
4556    global vhighlights fhighlights nhighlights rhighlights
4557
4558    if {[info exists nhighlights($id)] && $nhighlights($id) > 0} {
4559        return $nhighlights($id)
4560    }
4561    if {[info exists vhighlights($id)] && $vhighlights($id) > 0} {
4562        return $vhighlights($id)
4563    }
4564    if {[info exists fhighlights($id)] && $fhighlights($id) > 0} {
4565        return $fhighlights($id)
4566    }
4567    if {[info exists rhighlights($id)] && $rhighlights($id) > 0} {
4568        return $rhighlights($id)
4569    }
4570    return 0
4571}
4572
4573proc bolden {id font} {
4574    global canv linehtag currentid boldids need_redisplay markedid
4575
4576    # need_redisplay = 1 means the display is stale and about to be redrawn
4577    if {$need_redisplay} return
4578    lappend boldids $id
4579    $canv itemconf $linehtag($id) -font $font
4580    if {[info exists currentid] && $id eq $currentid} {
4581        $canv delete secsel
4582        set t [eval $canv create rect [$canv bbox $linehtag($id)] \
4583                   -outline {{}} -tags secsel \
4584                   -fill [$canv cget -selectbackground]]
4585        $canv lower $t
4586    }
4587    if {[info exists markedid] && $id eq $markedid} {
4588        make_idmark $id
4589    }
4590}
4591
4592proc bolden_name {id font} {
4593    global canv2 linentag currentid boldnameids need_redisplay
4594
4595    if {$need_redisplay} return
4596    lappend boldnameids $id
4597    $canv2 itemconf $linentag($id) -font $font
4598    if {[info exists currentid] && $id eq $currentid} {
4599        $canv2 delete secsel
4600        set t [eval $canv2 create rect [$canv2 bbox $linentag($id)] \
4601                   -outline {{}} -tags secsel \
4602                   -fill [$canv2 cget -selectbackground]]
4603        $canv2 lower $t
4604    }
4605}
4606
4607proc unbolden {} {
4608    global boldids
4609
4610    set stillbold {}
4611    foreach id $boldids {
4612        if {![ishighlighted $id]} {
4613            bolden $id mainfont
4614        } else {
4615            lappend stillbold $id
4616        }
4617    }
4618    set boldids $stillbold
4619}
4620
4621proc addvhighlight {n} {
4622    global hlview viewcomplete curview vhl_done commitidx
4623
4624    if {[info exists hlview]} {
4625        delvhighlight
4626    }
4627    set hlview $n
4628    if {$n != $curview && ![info exists viewcomplete($n)]} {
4629        start_rev_list $n
4630    }
4631    set vhl_done $commitidx($hlview)
4632    if {$vhl_done > 0} {
4633        drawvisible
4634    }
4635}
4636
4637proc delvhighlight {} {
4638    global hlview vhighlights
4639
4640    if {![info exists hlview]} return
4641    unset hlview
4642    unset -nocomplain vhighlights
4643    unbolden
4644}
4645
4646proc vhighlightmore {} {
4647    global hlview vhl_done commitidx vhighlights curview
4648
4649    set max $commitidx($hlview)
4650    set vr [visiblerows]
4651    set r0 [lindex $vr 0]
4652    set r1 [lindex $vr 1]
4653    for {set i $vhl_done} {$i < $max} {incr i} {
4654        set id [commitonrow $i $hlview]
4655        if {[commitinview $id $curview]} {
4656            set row [rowofcommit $id]
4657            if {$r0 <= $row && $row <= $r1} {
4658                if {![highlighted $row]} {
4659                    bolden $id mainfontbold
4660                }
4661                set vhighlights($id) 1
4662            }
4663        }
4664    }
4665    set vhl_done $max
4666    return 0
4667}
4668
4669proc askvhighlight {row id} {
4670    global hlview vhighlights iddrawn
4671
4672    if {[commitinview $id $hlview]} {
4673        if {[info exists iddrawn($id)] && ![ishighlighted $id]} {
4674            bolden $id mainfontbold
4675        }
4676        set vhighlights($id) 1
4677    } else {
4678        set vhighlights($id) 0
4679    }
4680}
4681
4682proc hfiles_change {} {
4683    global highlight_files filehighlight fhighlights fh_serial
4684    global highlight_paths
4685
4686    if {[info exists filehighlight]} {
4687        # delete previous highlights
4688        catch {close $filehighlight}
4689        unset filehighlight
4690        unset -nocomplain fhighlights
4691        unbolden
4692        unhighlight_filelist
4693    }
4694    set highlight_paths {}
4695    after cancel do_file_hl $fh_serial
4696    incr fh_serial
4697    if {$highlight_files ne {}} {
4698        after 300 do_file_hl $fh_serial
4699    }
4700}
4701
4702proc gdttype_change {name ix op} {
4703    global gdttype highlight_files findstring findpattern
4704
4705    stopfinding
4706    if {$findstring ne {}} {
4707        if {$gdttype eq [mc "containing:"]} {
4708            if {$highlight_files ne {}} {
4709                set highlight_files {}
4710                hfiles_change
4711            }
4712            findcom_change
4713        } else {
4714            if {$findpattern ne {}} {
4715                set findpattern {}
4716                findcom_change
4717            }
4718            set highlight_files $findstring
4719            hfiles_change
4720        }
4721        drawvisible
4722    }
4723    # enable/disable findtype/findloc menus too
4724}
4725
4726proc find_change {name ix op} {
4727    global gdttype findstring highlight_files
4728
4729    stopfinding
4730    if {$gdttype eq [mc "containing:"]} {
4731        findcom_change
4732    } else {
4733        if {$highlight_files ne $findstring} {
4734            set highlight_files $findstring
4735            hfiles_change
4736        }
4737    }
4738    drawvisible
4739}
4740
4741proc findcom_change args {
4742    global nhighlights boldnameids
4743    global findpattern findtype findstring gdttype
4744
4745    stopfinding
4746    # delete previous highlights, if any
4747    foreach id $boldnameids {
4748        bolden_name $id mainfont
4749    }
4750    set boldnameids {}
4751    unset -nocomplain nhighlights
4752    unbolden
4753    unmarkmatches
4754    if {$gdttype ne [mc "containing:"] || $findstring eq {}} {
4755        set findpattern {}
4756    } elseif {$findtype eq [mc "Regexp"]} {
4757        set findpattern $findstring
4758    } else {
4759        set e [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} \
4760                   $findstring]
4761        set findpattern "*$e*"
4762    }
4763}
4764
4765proc makepatterns {l} {
4766    set ret {}
4767    foreach e $l {
4768        set ee [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} $e]
4769        if {[string index $ee end] eq "/"} {
4770            lappend ret "$ee*"
4771        } else {
4772            lappend ret $ee
4773            lappend ret "$ee/*"
4774        }
4775    }
4776    return $ret
4777}
4778
4779proc do_file_hl {serial} {
4780    global highlight_files filehighlight highlight_paths gdttype fhl_list
4781    global cdup findtype
4782
4783    if {$gdttype eq [mc "touching paths:"]} {
4784        # If "exact" match then convert backslashes to forward slashes.
4785        # Most useful to support Windows-flavoured file paths.
4786        if {$findtype eq [mc "Exact"]} {
4787            set highlight_files [string map {"\\" "/"} $highlight_files]
4788        }
4789        if {[catch {set paths [shellsplit $highlight_files]}]} return
4790        set highlight_paths [makepatterns $paths]
4791        highlight_filelist
4792        set relative_paths {}
4793        foreach path $paths {
4794            lappend relative_paths [file join $cdup $path]
4795        }
4796        set gdtargs [concat -- $relative_paths]
4797    } elseif {$gdttype eq [mc "adding/removing string:"]} {
4798        set gdtargs [list "-S$highlight_files"]
4799    } elseif {$gdttype eq [mc "changing lines matching:"]} {
4800        set gdtargs [list "-G$highlight_files"]
4801    } else {
4802        # must be "containing:", i.e. we're searching commit info
4803        return
4804    }
4805    set cmd [concat | git diff-tree -r -s --stdin $gdtargs]
4806    set filehighlight [open $cmd r+]
4807    fconfigure $filehighlight -blocking 0
4808    filerun $filehighlight readfhighlight
4809    set fhl_list {}
4810    drawvisible
4811    flushhighlights
4812}
4813
4814proc flushhighlights {} {
4815    global filehighlight fhl_list
4816
4817    if {[info exists filehighlight]} {
4818        lappend fhl_list {}
4819        puts $filehighlight ""
4820        flush $filehighlight
4821    }
4822}
4823
4824proc askfilehighlight {row id} {
4825    global filehighlight fhighlights fhl_list
4826
4827    lappend fhl_list $id
4828    set fhighlights($id) -1
4829    puts $filehighlight $id
4830}
4831
4832proc readfhighlight {} {
4833    global filehighlight fhighlights curview iddrawn
4834    global fhl_list find_dirn
4835
4836    if {![info exists filehighlight]} {
4837        return 0
4838    }
4839    set nr 0
4840    while {[incr nr] <= 100 && [gets $filehighlight line] >= 0} {
4841        set line [string trim $line]
4842        set i [lsearch -exact $fhl_list $line]
4843        if {$i < 0} continue
4844        for {set j 0} {$j < $i} {incr j} {
4845            set id [lindex $fhl_list $j]
4846            set fhighlights($id) 0
4847        }
4848        set fhl_list [lrange $fhl_list [expr {$i+1}] end]
4849        if {$line eq {}} continue
4850        if {![commitinview $line $curview]} continue
4851        if {[info exists iddrawn($line)] && ![ishighlighted $line]} {
4852            bolden $line mainfontbold
4853        }
4854        set fhighlights($line) 1
4855    }
4856    if {[eof $filehighlight]} {
4857        # strange...
4858        puts "oops, git diff-tree died"
4859        catch {close $filehighlight}
4860        unset filehighlight
4861        return 0
4862    }
4863    if {[info exists find_dirn]} {
4864        run findmore
4865    }
4866    return 1
4867}
4868
4869proc doesmatch {f} {
4870    global findtype findpattern
4871
4872    if {$findtype eq [mc "Regexp"]} {
4873        return [regexp $findpattern $f]
4874    } elseif {$findtype eq [mc "IgnCase"]} {
4875        return [string match -nocase $findpattern $f]
4876    } else {
4877        return [string match $findpattern $f]
4878    }
4879}
4880
4881proc askfindhighlight {row id} {
4882    global nhighlights commitinfo iddrawn
4883    global findloc
4884    global markingmatches
4885
4886    if {![info exists commitinfo($id)]} {
4887        getcommit $id
4888    }
4889    set info $commitinfo($id)
4890    set isbold 0
4891    set fldtypes [list [mc Headline] [mc Author] "" [mc Committer] "" [mc Comments]]
4892    foreach f $info ty $fldtypes {
4893        if {$ty eq ""} continue
4894        if {($findloc eq [mc "All fields"] || $findloc eq $ty) &&
4895            [doesmatch $f]} {
4896            if {$ty eq [mc "Author"]} {
4897                set isbold 2
4898                break
4899            }
4900            set isbold 1
4901        }
4902    }
4903    if {$isbold && [info exists iddrawn($id)]} {
4904        if {![ishighlighted $id]} {
4905            bolden $id mainfontbold
4906            if {$isbold > 1} {
4907                bolden_name $id mainfontbold
4908            }
4909        }
4910        if {$markingmatches} {
4911            markrowmatches $row $id
4912        }
4913    }
4914    set nhighlights($id) $isbold
4915}
4916
4917proc markrowmatches {row id} {
4918    global canv canv2 linehtag linentag commitinfo findloc
4919
4920    set headline [lindex $commitinfo($id) 0]
4921    set author [lindex $commitinfo($id) 1]
4922    $canv delete match$row
4923    $canv2 delete match$row
4924    if {$findloc eq [mc "All fields"] || $findloc eq [mc "Headline"]} {
4925        set m [findmatches $headline]
4926        if {$m ne {}} {
4927            markmatches $canv $row $headline $linehtag($id) $m \
4928                [$canv itemcget $linehtag($id) -font] $row
4929        }
4930    }
4931    if {$findloc eq [mc "All fields"] || $findloc eq [mc "Author"]} {
4932        set m [findmatches $author]
4933        if {$m ne {}} {
4934            markmatches $canv2 $row $author $linentag($id) $m \
4935                [$canv2 itemcget $linentag($id) -font] $row
4936        }
4937    }
4938}
4939
4940proc vrel_change {name ix op} {
4941    global highlight_related
4942
4943    rhighlight_none
4944    if {$highlight_related ne [mc "None"]} {
4945        run drawvisible
4946    }
4947}
4948
4949# prepare for testing whether commits are descendents or ancestors of a
4950proc rhighlight_sel {a} {
4951    global descendent desc_todo ancestor anc_todo
4952    global highlight_related
4953
4954    unset -nocomplain descendent
4955    set desc_todo [list $a]
4956    unset -nocomplain ancestor
4957    set anc_todo [list $a]
4958    if {$highlight_related ne [mc "None"]} {
4959        rhighlight_none
4960        run drawvisible
4961    }
4962}
4963
4964proc rhighlight_none {} {
4965    global rhighlights
4966
4967    unset -nocomplain rhighlights
4968    unbolden
4969}
4970
4971proc is_descendent {a} {
4972    global curview children descendent desc_todo
4973
4974    set v $curview
4975    set la [rowofcommit $a]
4976    set todo $desc_todo
4977    set leftover {}
4978    set done 0
4979    for {set i 0} {$i < [llength $todo]} {incr i} {
4980        set do [lindex $todo $i]
4981        if {[rowofcommit $do] < $la} {
4982            lappend leftover $do
4983            continue
4984        }
4985        foreach nk $children($v,$do) {
4986            if {![info exists descendent($nk)]} {
4987                set descendent($nk) 1
4988                lappend todo $nk
4989                if {$nk eq $a} {
4990                    set done 1
4991                }
4992            }
4993        }
4994        if {$done} {
4995            set desc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
4996            return
4997        }
4998    }
4999    set descendent($a) 0
5000    set desc_todo $leftover
5001}
5002
5003proc is_ancestor {a} {
5004    global curview parents ancestor anc_todo
5005
5006    set v $curview
5007    set la [rowofcommit $a]
5008    set todo $anc_todo
5009    set leftover {}
5010    set done 0
5011    for {set i 0} {$i < [llength $todo]} {incr i} {
5012        set do [lindex $todo $i]
5013        if {![commitinview $do $v] || [rowofcommit $do] > $la} {
5014            lappend leftover $do
5015            continue
5016        }
5017        foreach np $parents($v,$do) {
5018            if {![info exists ancestor($np)]} {
5019                set ancestor($np) 1
5020                lappend todo $np
5021                if {$np eq $a} {
5022                    set done 1
5023                }
5024            }
5025        }
5026        if {$done} {
5027            set anc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
5028            return
5029        }
5030    }
5031    set ancestor($a) 0
5032    set anc_todo $leftover
5033}
5034
5035proc askrelhighlight {row id} {
5036    global descendent highlight_related iddrawn rhighlights
5037    global selectedline ancestor
5038
5039    if {$selectedline eq {}} return
5040    set isbold 0
5041    if {$highlight_related eq [mc "Descendant"] ||
5042        $highlight_related eq [mc "Not descendant"]} {
5043        if {![info exists descendent($id)]} {
5044            is_descendent $id
5045        }
5046        if {$descendent($id) == ($highlight_related eq [mc "Descendant"])} {
5047            set isbold 1
5048        }
5049    } elseif {$highlight_related eq [mc "Ancestor"] ||
5050              $highlight_related eq [mc "Not ancestor"]} {
5051        if {![info exists ancestor($id)]} {
5052            is_ancestor $id
5053        }
5054        if {$ancestor($id) == ($highlight_related eq [mc "Ancestor"])} {
5055            set isbold 1
5056        }
5057    }
5058    if {[info exists iddrawn($id)]} {
5059        if {$isbold && ![ishighlighted $id]} {
5060            bolden $id mainfontbold
5061        }
5062    }
5063    set rhighlights($id) $isbold
5064}
5065
5066# Graph layout functions
5067
5068proc shortids {ids} {
5069    set res {}
5070    foreach id $ids {
5071        if {[llength $id] > 1} {
5072            lappend res [shortids $id]
5073        } elseif {[regexp {^[0-9a-f]{40}$} $id]} {
5074            lappend res [string range $id 0 7]
5075        } else {
5076            lappend res $id
5077        }
5078    }
5079    return $res
5080}
5081
5082proc ntimes {n o} {
5083    set ret {}
5084    set o [list $o]
5085    for {set mask 1} {$mask <= $n} {incr mask $mask} {
5086        if {($n & $mask) != 0} {
5087            set ret [concat $ret $o]
5088        }
5089        set o [concat $o $o]
5090    }
5091    return $ret
5092}
5093
5094proc ordertoken {id} {
5095    global ordertok curview varcid varcstart varctok curview parents children
5096    global nullid nullid2
5097
5098    if {[info exists ordertok($id)]} {
5099        return $ordertok($id)
5100    }
5101    set origid $id
5102    set todo {}
5103    while {1} {
5104        if {[info exists varcid($curview,$id)]} {
5105            set a $varcid($curview,$id)
5106            set p [lindex $varcstart($curview) $a]
5107        } else {
5108            set p [lindex $children($curview,$id) 0]
5109        }
5110        if {[info exists ordertok($p)]} {
5111            set tok $ordertok($p)
5112            break
5113        }
5114        set id [first_real_child $curview,$p]
5115        if {$id eq {}} {
5116            # it's a root
5117            set tok [lindex $varctok($curview) $varcid($curview,$p)]
5118            break
5119        }
5120        if {[llength $parents($curview,$id)] == 1} {
5121            lappend todo [list $p {}]
5122        } else {
5123            set j [lsearch -exact $parents($curview,$id) $p]
5124            if {$j < 0} {
5125                puts "oops didn't find [shortids $p] in parents of [shortids $id]"
5126            }
5127            lappend todo [list $p [strrep $j]]
5128        }
5129    }
5130    for {set i [llength $todo]} {[incr i -1] >= 0} {} {
5131        set p [lindex $todo $i 0]
5132        append tok [lindex $todo $i 1]
5133        set ordertok($p) $tok
5134    }
5135    set ordertok($origid) $tok
5136    return $tok
5137}
5138
5139# Work out where id should go in idlist so that order-token
5140# values increase from left to right
5141proc idcol {idlist id {i 0}} {
5142    set t [ordertoken $id]
5143    if {$i < 0} {
5144        set i 0
5145    }
5146    if {$i >= [llength $idlist] || $t < [ordertoken [lindex $idlist $i]]} {
5147        if {$i > [llength $idlist]} {
5148            set i [llength $idlist]
5149        }
5150        while {[incr i -1] >= 0 && $t < [ordertoken [lindex $idlist $i]]} {}
5151        incr i
5152    } else {
5153        if {$t > [ordertoken [lindex $idlist $i]]} {
5154            while {[incr i] < [llength $idlist] &&
5155                   $t >= [ordertoken [lindex $idlist $i]]} {}
5156        }
5157    }
5158    return $i
5159}
5160
5161proc initlayout {} {
5162    global rowidlist rowisopt rowfinal displayorder parentlist
5163    global numcommits canvxmax canv
5164    global nextcolor
5165    global colormap rowtextx
5166
5167    set numcommits 0
5168    set displayorder {}
5169    set parentlist {}
5170    set nextcolor 0
5171    set rowidlist {}
5172    set rowisopt {}
5173    set rowfinal {}
5174    set canvxmax [$canv cget -width]
5175    unset -nocomplain colormap
5176    unset -nocomplain rowtextx
5177    setcanvscroll
5178}
5179
5180proc setcanvscroll {} {
5181    global canv canv2 canv3 numcommits linespc canvxmax canvy0
5182    global lastscrollset lastscrollrows
5183
5184    set ymax [expr {$canvy0 + ($numcommits - 0.5) * $linespc + 2}]
5185    $canv conf -scrollregion [list 0 0 $canvxmax $ymax]
5186    $canv2 conf -scrollregion [list 0 0 0 $ymax]
5187    $canv3 conf -scrollregion [list 0 0 0 $ymax]
5188    set lastscrollset [clock clicks -milliseconds]
5189    set lastscrollrows $numcommits
5190}
5191
5192proc visiblerows {} {
5193    global canv numcommits linespc
5194
5195    set ymax [lindex [$canv cget -scrollregion] 3]
5196    if {$ymax eq {} || $ymax == 0} return
5197    set f [$canv yview]
5198    set y0 [expr {int([lindex $f 0] * $ymax)}]
5199    set r0 [expr {int(($y0 - 3) / $linespc) - 1}]
5200    if {$r0 < 0} {
5201        set r0 0
5202    }
5203    set y1 [expr {int([lindex $f 1] * $ymax)}]
5204    set r1 [expr {int(($y1 - 3) / $linespc) + 1}]
5205    if {$r1 >= $numcommits} {
5206        set r1 [expr {$numcommits - 1}]
5207    }
5208    return [list $r0 $r1]
5209}
5210
5211proc layoutmore {} {
5212    global commitidx viewcomplete curview
5213    global numcommits pending_select curview
5214    global lastscrollset lastscrollrows
5215
5216    if {$lastscrollrows < 100 || $viewcomplete($curview) ||
5217        [clock clicks -milliseconds] - $lastscrollset > 500} {
5218        setcanvscroll
5219    }
5220    if {[info exists pending_select] &&
5221        [commitinview $pending_select $curview]} {
5222        update
5223        selectline [rowofcommit $pending_select] 1
5224    }
5225    drawvisible
5226}
5227
5228# With path limiting, we mightn't get the actual HEAD commit,
5229# so ask git rev-list what is the first ancestor of HEAD that
5230# touches a file in the path limit.
5231proc get_viewmainhead {view} {
5232    global viewmainheadid vfilelimit viewinstances mainheadid
5233
5234    catch {
5235        set rfd [open [concat | git rev-list -1 $mainheadid \
5236                           -- $vfilelimit($view)] r]
5237        set j [reg_instance $rfd]
5238        lappend viewinstances($view) $j
5239        fconfigure $rfd -blocking 0
5240        filerun $rfd [list getviewhead $rfd $j $view]
5241        set viewmainheadid($curview) {}
5242    }
5243}
5244
5245# git rev-list should give us just 1 line to use as viewmainheadid($view)
5246proc getviewhead {fd inst view} {
5247    global viewmainheadid commfd curview viewinstances showlocalchanges
5248
5249    set id {}
5250    if {[gets $fd line] < 0} {
5251        if {![eof $fd]} {
5252            return 1
5253        }
5254    } elseif {[string length $line] == 40 && [string is xdigit $line]} {
5255        set id $line
5256    }
5257    set viewmainheadid($view) $id
5258    close $fd
5259    unset commfd($inst)
5260    set i [lsearch -exact $viewinstances($view) $inst]
5261    if {$i >= 0} {
5262        set viewinstances($view) [lreplace $viewinstances($view) $i $i]
5263    }
5264    if {$showlocalchanges && $id ne {} && $view == $curview} {
5265        doshowlocalchanges
5266    }
5267    return 0
5268}
5269
5270proc doshowlocalchanges {} {
5271    global curview viewmainheadid
5272
5273    if {$viewmainheadid($curview) eq {}} return
5274    if {[commitinview $viewmainheadid($curview) $curview]} {
5275        dodiffindex
5276    } else {
5277        interestedin $viewmainheadid($curview) dodiffindex
5278    }
5279}
5280
5281proc dohidelocalchanges {} {
5282    global nullid nullid2 lserial curview
5283
5284    if {[commitinview $nullid $curview]} {
5285        removefakerow $nullid
5286    }
5287    if {[commitinview $nullid2 $curview]} {
5288        removefakerow $nullid2
5289    }
5290    incr lserial
5291}
5292
5293# spawn off a process to do git diff-index --cached HEAD
5294proc dodiffindex {} {
5295    global lserial showlocalchanges vfilelimit curview
5296    global hasworktree git_version
5297
5298    if {!$showlocalchanges || !$hasworktree} return
5299    incr lserial
5300    if {[package vcompare $git_version "1.7.2"] >= 0} {
5301        set cmd "|git diff-index --cached --ignore-submodules=dirty HEAD"
5302    } else {
5303        set cmd "|git diff-index --cached HEAD"
5304    }
5305    if {$vfilelimit($curview) ne {}} {
5306        set cmd [concat $cmd -- $vfilelimit($curview)]
5307    }
5308    set fd [open $cmd r]
5309    fconfigure $fd -blocking 0
5310    set i [reg_instance $fd]
5311    filerun $fd [list readdiffindex $fd $lserial $i]
5312}
5313
5314proc readdiffindex {fd serial inst} {
5315    global viewmainheadid nullid nullid2 curview commitinfo commitdata lserial
5316    global vfilelimit
5317
5318    set isdiff 1
5319    if {[gets $fd line] < 0} {
5320        if {![eof $fd]} {
5321            return 1
5322        }
5323        set isdiff 0
5324    }
5325    # we only need to see one line and we don't really care what it says...
5326    stop_instance $inst
5327
5328    if {$serial != $lserial} {
5329        return 0
5330    }
5331
5332    # now see if there are any local changes not checked in to the index
5333    set cmd "|git diff-files"
5334    if {$vfilelimit($curview) ne {}} {
5335        set cmd [concat $cmd -- $vfilelimit($curview)]
5336    }
5337    set fd [open $cmd r]
5338    fconfigure $fd -blocking 0
5339    set i [reg_instance $fd]
5340    filerun $fd [list readdifffiles $fd $serial $i]
5341
5342    if {$isdiff && ![commitinview $nullid2 $curview]} {
5343        # add the line for the changes in the index to the graph
5344        set hl [mc "Local changes checked in to index but not committed"]
5345        set commitinfo($nullid2) [list  $hl {} {} {} {} "    $hl\n"]
5346        set commitdata($nullid2) "\n    $hl\n"
5347        if {[commitinview $nullid $curview]} {
5348            removefakerow $nullid
5349        }
5350        insertfakerow $nullid2 $viewmainheadid($curview)
5351    } elseif {!$isdiff && [commitinview $nullid2 $curview]} {
5352        if {[commitinview $nullid $curview]} {
5353            removefakerow $nullid
5354        }
5355        removefakerow $nullid2
5356    }
5357    return 0
5358}
5359
5360proc readdifffiles {fd serial inst} {
5361    global viewmainheadid nullid nullid2 curview
5362    global commitinfo commitdata lserial
5363
5364    set isdiff 1
5365    if {[gets $fd line] < 0} {
5366        if {![eof $fd]} {
5367            return 1
5368        }
5369        set isdiff 0
5370    }
5371    # we only need to see one line and we don't really care what it says...
5372    stop_instance $inst
5373
5374    if {$serial != $lserial} {
5375        return 0
5376    }
5377
5378    if {$isdiff && ![commitinview $nullid $curview]} {
5379        # add the line for the local diff to the graph
5380        set hl [mc "Local uncommitted changes, not checked in to index"]
5381        set commitinfo($nullid) [list  $hl {} {} {} {} "    $hl\n"]
5382        set commitdata($nullid) "\n    $hl\n"
5383        if {[commitinview $nullid2 $curview]} {
5384            set p $nullid2
5385        } else {
5386            set p $viewmainheadid($curview)
5387        }
5388        insertfakerow $nullid $p
5389    } elseif {!$isdiff && [commitinview $nullid $curview]} {
5390        removefakerow $nullid
5391    }
5392    return 0
5393}
5394
5395proc nextuse {id row} {
5396    global curview children
5397
5398    if {[info exists children($curview,$id)]} {
5399        foreach kid $children($curview,$id) {
5400            if {![commitinview $kid $curview]} {
5401                return -1
5402            }
5403            if {[rowofcommit $kid] > $row} {
5404                return [rowofcommit $kid]
5405            }
5406        }
5407    }
5408    if {[commitinview $id $curview]} {
5409        return [rowofcommit $id]
5410    }
5411    return -1
5412}
5413
5414proc prevuse {id row} {
5415    global curview children
5416
5417    set ret -1
5418    if {[info exists children($curview,$id)]} {
5419        foreach kid $children($curview,$id) {
5420            if {![commitinview $kid $curview]} break
5421            if {[rowofcommit $kid] < $row} {
5422                set ret [rowofcommit $kid]
5423            }
5424        }
5425    }
5426    return $ret
5427}
5428
5429proc make_idlist {row} {
5430    global displayorder parentlist uparrowlen downarrowlen mingaplen
5431    global commitidx curview children
5432
5433    set r [expr {$row - $mingaplen - $downarrowlen - 1}]
5434    if {$r < 0} {
5435        set r 0
5436    }
5437    set ra [expr {$row - $downarrowlen}]
5438    if {$ra < 0} {
5439        set ra 0
5440    }
5441    set rb [expr {$row + $uparrowlen}]
5442    if {$rb > $commitidx($curview)} {
5443        set rb $commitidx($curview)
5444    }
5445    make_disporder $r [expr {$rb + 1}]
5446    set ids {}
5447    for {} {$r < $ra} {incr r} {
5448        set nextid [lindex $displayorder [expr {$r + 1}]]
5449        foreach p [lindex $parentlist $r] {
5450            if {$p eq $nextid} continue
5451            set rn [nextuse $p $r]
5452            if {$rn >= $row &&
5453                $rn <= $r + $downarrowlen + $mingaplen + $uparrowlen} {
5454                lappend ids [list [ordertoken $p] $p]
5455            }
5456        }
5457    }
5458    for {} {$r < $row} {incr r} {
5459        set nextid [lindex $displayorder [expr {$r + 1}]]
5460        foreach p [lindex $parentlist $r] {
5461            if {$p eq $nextid} continue
5462            set rn [nextuse $p $r]
5463            if {$rn < 0 || $rn >= $row} {
5464                lappend ids [list [ordertoken $p] $p]
5465            }
5466        }
5467    }
5468    set id [lindex $displayorder $row]
5469    lappend ids [list [ordertoken $id] $id]
5470    while {$r < $rb} {
5471        foreach p [lindex $parentlist $r] {
5472            set firstkid [lindex $children($curview,$p) 0]
5473            if {[rowofcommit $firstkid] < $row} {
5474                lappend ids [list [ordertoken $p] $p]
5475            }
5476        }
5477        incr r
5478        set id [lindex $displayorder $r]
5479        if {$id ne {}} {
5480            set firstkid [lindex $children($curview,$id) 0]
5481            if {$firstkid ne {} && [rowofcommit $firstkid] < $row} {
5482                lappend ids [list [ordertoken $id] $id]
5483            }
5484        }
5485    }
5486    set idlist {}
5487    foreach idx [lsort -unique $ids] {
5488        lappend idlist [lindex $idx 1]
5489    }
5490    return $idlist
5491}
5492
5493proc rowsequal {a b} {
5494    while {[set i [lsearch -exact $a {}]] >= 0} {
5495        set a [lreplace $a $i $i]
5496    }
5497    while {[set i [lsearch -exact $b {}]] >= 0} {
5498        set b [lreplace $b $i $i]
5499    }
5500    return [expr {$a eq $b}]
5501}
5502
5503proc makeupline {id row rend col} {
5504    global rowidlist uparrowlen downarrowlen mingaplen
5505
5506    for {set r $rend} {1} {set r $rstart} {
5507        set rstart [prevuse $id $r]
5508        if {$rstart < 0} return
5509        if {$rstart < $row} break
5510    }
5511    if {$rstart + $uparrowlen + $mingaplen + $downarrowlen < $rend} {
5512        set rstart [expr {$rend - $uparrowlen - 1}]
5513    }
5514    for {set r $rstart} {[incr r] <= $row} {} {
5515        set idlist [lindex $rowidlist $r]
5516        if {$idlist ne {} && [lsearch -exact $idlist $id] < 0} {
5517            set col [idcol $idlist $id $col]
5518            lset rowidlist $r [linsert $idlist $col $id]
5519            changedrow $r
5520        }
5521    }
5522}
5523
5524proc layoutrows {row endrow} {
5525    global rowidlist rowisopt rowfinal displayorder
5526    global uparrowlen downarrowlen maxwidth mingaplen
5527    global children parentlist
5528    global commitidx viewcomplete curview
5529
5530    make_disporder [expr {$row - 1}] [expr {$endrow + $uparrowlen}]
5531    set idlist {}
5532    if {$row > 0} {
5533        set rm1 [expr {$row - 1}]
5534        foreach id [lindex $rowidlist $rm1] {
5535            if {$id ne {}} {
5536                lappend idlist $id
5537            }
5538        }
5539        set final [lindex $rowfinal $rm1]
5540    }
5541    for {} {$row < $endrow} {incr row} {
5542        set rm1 [expr {$row - 1}]
5543        if {$rm1 < 0 || $idlist eq {}} {
5544            set idlist [make_idlist $row]
5545            set final 1
5546        } else {
5547            set id [lindex $displayorder $rm1]
5548            set col [lsearch -exact $idlist $id]
5549            set idlist [lreplace $idlist $col $col]
5550            foreach p [lindex $parentlist $rm1] {
5551                if {[lsearch -exact $idlist $p] < 0} {
5552                    set col [idcol $idlist $p $col]
5553                    set idlist [linsert $idlist $col $p]
5554                    # if not the first child, we have to insert a line going up
5555                    if {$id ne [lindex $children($curview,$p) 0]} {
5556                        makeupline $p $rm1 $row $col
5557                    }
5558                }
5559            }
5560            set id [lindex $displayorder $row]
5561            if {$row > $downarrowlen} {
5562                set termrow [expr {$row - $downarrowlen - 1}]
5563                foreach p [lindex $parentlist $termrow] {
5564                    set i [lsearch -exact $idlist $p]
5565                    if {$i < 0} continue
5566                    set nr [nextuse $p $termrow]
5567                    if {$nr < 0 || $nr >= $row + $mingaplen + $uparrowlen} {
5568                        set idlist [lreplace $idlist $i $i]
5569                    }
5570                }
5571            }
5572            set col [lsearch -exact $idlist $id]
5573            if {$col < 0} {
5574                set col [idcol $idlist $id]
5575                set idlist [linsert $idlist $col $id]
5576                if {$children($curview,$id) ne {}} {
5577                    makeupline $id $rm1 $row $col
5578                }
5579            }
5580            set r [expr {$row + $uparrowlen - 1}]
5581            if {$r < $commitidx($curview)} {
5582                set x $col
5583                foreach p [lindex $parentlist $r] {
5584                    if {[lsearch -exact $idlist $p] >= 0} continue
5585                    set fk [lindex $children($curview,$p) 0]
5586                    if {[rowofcommit $fk] < $row} {
5587                        set x [idcol $idlist $p $x]
5588                        set idlist [linsert $idlist $x $p]
5589                    }
5590                }
5591                if {[incr r] < $commitidx($curview)} {
5592                    set p [lindex $displayorder $r]
5593                    if {[lsearch -exact $idlist $p] < 0} {
5594                        set fk [lindex $children($curview,$p) 0]
5595                        if {$fk ne {} && [rowofcommit $fk] < $row} {
5596                            set x [idcol $idlist $p $x]
5597                            set idlist [linsert $idlist $x $p]
5598                        }
5599                    }
5600                }
5601            }
5602        }
5603        if {$final && !$viewcomplete($curview) &&
5604            $row + $uparrowlen + $mingaplen + $downarrowlen
5605                >= $commitidx($curview)} {
5606            set final 0
5607        }
5608        set l [llength $rowidlist]
5609        if {$row == $l} {
5610            lappend rowidlist $idlist
5611            lappend rowisopt 0
5612            lappend rowfinal $final
5613        } elseif {$row < $l} {
5614            if {![rowsequal $idlist [lindex $rowidlist $row]]} {
5615                lset rowidlist $row $idlist
5616                changedrow $row
5617            }
5618            lset rowfinal $row $final
5619        } else {
5620            set pad [ntimes [expr {$row - $l}] {}]
5621            set rowidlist [concat $rowidlist $pad]
5622            lappend rowidlist $idlist
5623            set rowfinal [concat $rowfinal $pad]
5624            lappend rowfinal $final
5625            set rowisopt [concat $rowisopt [ntimes [expr {$row - $l + 1}] 0]]
5626        }
5627    }
5628    return $row
5629}
5630
5631proc changedrow {row} {
5632    global displayorder iddrawn rowisopt need_redisplay
5633
5634    set l [llength $rowisopt]
5635    if {$row < $l} {
5636        lset rowisopt $row 0
5637        if {$row + 1 < $l} {
5638            lset rowisopt [expr {$row + 1}] 0
5639            if {$row + 2 < $l} {
5640                lset rowisopt [expr {$row + 2}] 0
5641            }
5642        }
5643    }
5644    set id [lindex $displayorder $row]
5645    if {[info exists iddrawn($id)]} {
5646        set need_redisplay 1
5647    }
5648}
5649
5650proc insert_pad {row col npad} {
5651    global rowidlist
5652
5653    set pad [ntimes $npad {}]
5654    set idlist [lindex $rowidlist $row]
5655    set bef [lrange $idlist 0 [expr {$col - 1}]]
5656    set aft [lrange $idlist $col end]
5657    set i [lsearch -exact $aft {}]
5658    if {$i > 0} {
5659        set aft [lreplace $aft $i $i]
5660    }
5661    lset rowidlist $row [concat $bef $pad $aft]
5662    changedrow $row
5663}
5664
5665proc optimize_rows {row col endrow} {
5666    global rowidlist rowisopt displayorder curview children
5667
5668    if {$row < 1} {
5669        set row 1
5670    }
5671    for {} {$row < $endrow} {incr row; set col 0} {
5672        if {[lindex $rowisopt $row]} continue
5673        set haspad 0
5674        set y0 [expr {$row - 1}]
5675        set ym [expr {$row - 2}]
5676        set idlist [lindex $rowidlist $row]
5677        set previdlist [lindex $rowidlist $y0]
5678        if {$idlist eq {} || $previdlist eq {}} continue
5679        if {$ym >= 0} {
5680            set pprevidlist [lindex $rowidlist $ym]
5681            if {$pprevidlist eq {}} continue
5682        } else {
5683            set pprevidlist {}
5684        }
5685        set x0 -1
5686        set xm -1
5687        for {} {$col < [llength $idlist]} {incr col} {
5688            set id [lindex $idlist $col]
5689            if {[lindex $previdlist $col] eq $id} continue
5690            if {$id eq {}} {
5691                set haspad 1
5692                continue
5693            }
5694            set x0 [lsearch -exact $previdlist $id]
5695            if {$x0 < 0} continue
5696            set z [expr {$x0 - $col}]
5697            set isarrow 0
5698            set z0 {}
5699            if {$ym >= 0} {
5700                set xm [lsearch -exact $pprevidlist $id]
5701                if {$xm >= 0} {
5702                    set z0 [expr {$xm - $x0}]
5703                }
5704            }
5705            if {$z0 eq {}} {
5706                # if row y0 is the first child of $id then it's not an arrow
5707                if {[lindex $children($curview,$id) 0] ne
5708                    [lindex $displayorder $y0]} {
5709                    set isarrow 1
5710                }
5711            }
5712            if {!$isarrow && $id ne [lindex $displayorder $row] &&
5713                [lsearch -exact [lindex $rowidlist [expr {$row+1}]] $id] < 0} {
5714                set isarrow 1
5715            }
5716            # Looking at lines from this row to the previous row,
5717            # make them go straight up if they end in an arrow on
5718            # the previous row; otherwise make them go straight up
5719            # or at 45 degrees.
5720            if {$z < -1 || ($z < 0 && $isarrow)} {
5721                # Line currently goes left too much;
5722                # insert pads in the previous row, then optimize it
5723                set npad [expr {-1 - $z + $isarrow}]
5724                insert_pad $y0 $x0 $npad
5725                if {$y0 > 0} {
5726                    optimize_rows $y0 $x0 $row
5727                }
5728                set previdlist [lindex $rowidlist $y0]
5729                set x0 [lsearch -exact $previdlist $id]
5730                set z [expr {$x0 - $col}]
5731                if {$z0 ne {}} {
5732                    set pprevidlist [lindex $rowidlist $ym]
5733                    set xm [lsearch -exact $pprevidlist $id]
5734                    set z0 [expr {$xm - $x0}]
5735                }
5736            } elseif {$z > 1 || ($z > 0 && $isarrow)} {
5737                # Line currently goes right too much;
5738                # insert pads in this line
5739                set npad [expr {$z - 1 + $isarrow}]
5740                insert_pad $row $col $npad
5741                set idlist [lindex $rowidlist $row]
5742                incr col $npad
5743                set z [expr {$x0 - $col}]
5744                set haspad 1
5745            }
5746            if {$z0 eq {} && !$isarrow && $ym >= 0} {
5747                # this line links to its first child on row $row-2
5748                set id [lindex $displayorder $ym]
5749                set xc [lsearch -exact $pprevidlist $id]
5750                if {$xc >= 0} {
5751                    set z0 [expr {$xc - $x0}]
5752                }
5753            }
5754            # avoid lines jigging left then immediately right
5755            if {$z0 ne {} && $z < 0 && $z0 > 0} {
5756                insert_pad $y0 $x0 1
5757                incr x0
5758                optimize_rows $y0 $x0 $row
5759                set previdlist [lindex $rowidlist $y0]
5760            }
5761        }
5762        if {!$haspad} {
5763            # Find the first column that doesn't have a line going right
5764            for {set col [llength $idlist]} {[incr col -1] >= 0} {} {
5765                set id [lindex $idlist $col]
5766                if {$id eq {}} break
5767                set x0 [lsearch -exact $previdlist $id]
5768                if {$x0 < 0} {
5769                    # check if this is the link to the first child
5770                    set kid [lindex $displayorder $y0]
5771                    if {[lindex $children($curview,$id) 0] eq $kid} {
5772                        # it is, work out offset to child
5773                        set x0 [lsearch -exact $previdlist $kid]
5774                    }
5775                }
5776                if {$x0 <= $col} break
5777            }
5778            # Insert a pad at that column as long as it has a line and
5779            # isn't the last column
5780            if {$x0 >= 0 && [incr col] < [llength $idlist]} {
5781                set idlist [linsert $idlist $col {}]
5782                lset rowidlist $row $idlist
5783                changedrow $row
5784            }
5785        }
5786    }
5787}
5788
5789proc xc {row col} {
5790    global canvx0 linespc
5791    return [expr {$canvx0 + $col * $linespc}]
5792}
5793
5794proc yc {row} {
5795    global canvy0 linespc
5796    return [expr {$canvy0 + $row * $linespc}]
5797}
5798
5799proc linewidth {id} {
5800    global thickerline lthickness
5801
5802    set wid $lthickness
5803    if {[info exists thickerline] && $id eq $thickerline} {
5804        set wid [expr {2 * $lthickness}]
5805    }
5806    return $wid
5807}
5808
5809proc rowranges {id} {
5810    global curview children uparrowlen downarrowlen
5811    global rowidlist
5812
5813    set kids $children($curview,$id)
5814    if {$kids eq {}} {
5815        return {}
5816    }
5817    set ret {}
5818    lappend kids $id
5819    foreach child $kids {
5820        if {![commitinview $child $curview]} break
5821        set row [rowofcommit $child]
5822        if {![info exists prev]} {
5823            lappend ret [expr {$row + 1}]
5824        } else {
5825            if {$row <= $prevrow} {
5826                puts "oops children of [shortids $id] out of order [shortids $child] $row <= [shortids $prev] $prevrow"
5827            }
5828            # see if the line extends the whole way from prevrow to row
5829            if {$row > $prevrow + $uparrowlen + $downarrowlen &&
5830                [lsearch -exact [lindex $rowidlist \
5831                            [expr {int(($row + $prevrow) / 2)}]] $id] < 0} {
5832                # it doesn't, see where it ends
5833                set r [expr {$prevrow + $downarrowlen}]
5834                if {[lsearch -exact [lindex $rowidlist $r] $id] < 0} {
5835                    while {[incr r -1] > $prevrow &&
5836                           [lsearch -exact [lindex $rowidlist $r] $id] < 0} {}
5837                } else {
5838                    while {[incr r] <= $row &&
5839                           [lsearch -exact [lindex $rowidlist $r] $id] >= 0} {}
5840                    incr r -1
5841                }
5842                lappend ret $r
5843                # see where it starts up again
5844                set r [expr {$row - $uparrowlen}]
5845                if {[lsearch -exact [lindex $rowidlist $r] $id] < 0} {
5846                    while {[incr r] < $row &&
5847                           [lsearch -exact [lindex $rowidlist $r] $id] < 0} {}
5848                } else {
5849                    while {[incr r -1] >= $prevrow &&
5850                           [lsearch -exact [lindex $rowidlist $r] $id] >= 0} {}
5851                    incr r
5852                }
5853                lappend ret $r
5854            }
5855        }
5856        if {$child eq $id} {
5857            lappend ret $row
5858        }
5859        set prev $child
5860        set prevrow $row
5861    }
5862    return $ret
5863}
5864
5865proc drawlineseg {id row endrow arrowlow} {
5866    global rowidlist displayorder iddrawn linesegs
5867    global canv colormap linespc curview maxlinelen parentlist
5868
5869    set cols [list [lsearch -exact [lindex $rowidlist $row] $id]]
5870    set le [expr {$row + 1}]
5871    set arrowhigh 1
5872    while {1} {
5873        set c [lsearch -exact [lindex $rowidlist $le] $id]
5874        if {$c < 0} {
5875            incr le -1
5876            break
5877        }
5878        lappend cols $c
5879        set x [lindex $displayorder $le]
5880        if {$x eq $id} {
5881            set arrowhigh 0
5882            break
5883        }
5884        if {[info exists iddrawn($x)] || $le == $endrow} {
5885            set c [lsearch -exact [lindex $rowidlist [expr {$le+1}]] $id]
5886            if {$c >= 0} {
5887                lappend cols $c
5888                set arrowhigh 0
5889            }
5890            break
5891        }
5892        incr le
5893    }
5894    if {$le <= $row} {
5895        return $row
5896    }
5897
5898    set lines {}
5899    set i 0
5900    set joinhigh 0
5901    if {[info exists linesegs($id)]} {
5902        set lines $linesegs($id)
5903        foreach li $lines {
5904            set r0 [lindex $li 0]
5905            if {$r0 > $row} {
5906                if {$r0 == $le && [lindex $li 1] - $row <= $maxlinelen} {
5907                    set joinhigh 1
5908                }
5909                break
5910            }
5911            incr i
5912        }
5913    }
5914    set joinlow 0
5915    if {$i > 0} {
5916        set li [lindex $lines [expr {$i-1}]]
5917        set r1 [lindex $li 1]
5918        if {$r1 == $row && $le - [lindex $li 0] <= $maxlinelen} {
5919            set joinlow 1
5920        }
5921    }
5922
5923    set x [lindex $cols [expr {$le - $row}]]
5924    set xp [lindex $cols [expr {$le - 1 - $row}]]
5925    set dir [expr {$xp - $x}]
5926    if {$joinhigh} {
5927        set ith [lindex $lines $i 2]
5928        set coords [$canv coords $ith]
5929        set ah [$canv itemcget $ith -arrow]
5930        set arrowhigh [expr {$ah eq "first" || $ah eq "both"}]
5931        set x2 [lindex $cols [expr {$le + 1 - $row}]]
5932        if {$x2 ne {} && $x - $x2 == $dir} {
5933            set coords [lrange $coords 0 end-2]
5934        }
5935    } else {
5936        set coords [list [xc $le $x] [yc $le]]
5937    }
5938    if {$joinlow} {
5939        set itl [lindex $lines [expr {$i-1}] 2]
5940        set al [$canv itemcget $itl -arrow]
5941        set arrowlow [expr {$al eq "last" || $al eq "both"}]
5942    } elseif {$arrowlow} {
5943        if {[lsearch -exact [lindex $rowidlist [expr {$row-1}]] $id] >= 0 ||
5944            [lsearch -exact [lindex $parentlist [expr {$row-1}]] $id] >= 0} {
5945            set arrowlow 0
5946        }
5947    }
5948    set arrow [lindex {none first last both} [expr {$arrowhigh + 2*$arrowlow}]]
5949    for {set y $le} {[incr y -1] > $row} {} {
5950        set x $xp
5951        set xp [lindex $cols [expr {$y - 1 - $row}]]
5952        set ndir [expr {$xp - $x}]
5953        if {$dir != $ndir || $xp < 0} {
5954            lappend coords [xc $y $x] [yc $y]
5955        }
5956        set dir $ndir
5957    }
5958    if {!$joinlow} {
5959        if {$xp < 0} {
5960            # join parent line to first child
5961            set ch [lindex $displayorder $row]
5962            set xc [lsearch -exact [lindex $rowidlist $row] $ch]
5963            if {$xc < 0} {
5964                puts "oops: drawlineseg: child $ch not on row $row"
5965            } elseif {$xc != $x} {
5966                if {($arrowhigh && $le == $row + 1) || $dir == 0} {
5967                    set d [expr {int(0.5 * $linespc)}]
5968                    set x1 [xc $row $x]
5969                    if {$xc < $x} {
5970                        set x2 [expr {$x1 - $d}]
5971                    } else {
5972                        set x2 [expr {$x1 + $d}]
5973                    }
5974                    set y2 [yc $row]
5975                    set y1 [expr {$y2 + $d}]
5976                    lappend coords $x1 $y1 $x2 $y2
5977                } elseif {$xc < $x - 1} {
5978                    lappend coords [xc $row [expr {$x-1}]] [yc $row]
5979                } elseif {$xc > $x + 1} {
5980                    lappend coords [xc $row [expr {$x+1}]] [yc $row]
5981                }
5982                set x $xc
5983            }
5984            lappend coords [xc $row $x] [yc $row]
5985        } else {
5986            set xn [xc $row $xp]
5987            set yn [yc $row]
5988            lappend coords $xn $yn
5989        }
5990        if {!$joinhigh} {
5991            assigncolor $id
5992            set t [$canv create line $coords -width [linewidth $id] \
5993                       -fill $colormap($id) -tags lines.$id -arrow $arrow]
5994            $canv lower $t
5995            bindline $t $id
5996            set lines [linsert $lines $i [list $row $le $t]]
5997        } else {
5998            $canv coords $ith $coords
5999            if {$arrow ne $ah} {
6000                $canv itemconf $ith -arrow $arrow
6001            }
6002            lset lines $i 0 $row
6003        }
6004    } else {
6005        set xo [lsearch -exact [lindex $rowidlist [expr {$row - 1}]] $id]
6006        set ndir [expr {$xo - $xp}]
6007        set clow [$canv coords $itl]
6008        if {$dir == $ndir} {
6009            set clow [lrange $clow 2 end]
6010        }
6011        set coords [concat $coords $clow]
6012        if {!$joinhigh} {
6013            lset lines [expr {$i-1}] 1 $le
6014        } else {
6015            # coalesce two pieces
6016            $canv delete $ith
6017            set b [lindex $lines [expr {$i-1}] 0]
6018            set e [lindex $lines $i 1]
6019            set lines [lreplace $lines [expr {$i-1}] $i [list $b $e $itl]]
6020        }
6021        $canv coords $itl $coords
6022        if {$arrow ne $al} {
6023            $canv itemconf $itl -arrow $arrow
6024        }
6025    }
6026
6027    set linesegs($id) $lines
6028    return $le
6029}
6030
6031proc drawparentlinks {id row} {
6032    global rowidlist canv colormap curview parentlist
6033    global idpos linespc
6034
6035    set rowids [lindex $rowidlist $row]
6036    set col [lsearch -exact $rowids $id]
6037    if {$col < 0} return
6038    set olds [lindex $parentlist $row]
6039    set row2 [expr {$row + 1}]
6040    set x [xc $row $col]
6041    set y [yc $row]
6042    set y2 [yc $row2]
6043    set d [expr {int(0.5 * $linespc)}]
6044    set ymid [expr {$y + $d}]
6045    set ids [lindex $rowidlist $row2]
6046    # rmx = right-most X coord used
6047    set rmx 0
6048    foreach p $olds {
6049        set i [lsearch -exact $ids $p]
6050        if {$i < 0} {
6051            puts "oops, parent $p of $id not in list"
6052            continue
6053        }
6054        set x2 [xc $row2 $i]
6055        if {$x2 > $rmx} {
6056            set rmx $x2
6057        }
6058        set j [lsearch -exact $rowids $p]
6059        if {$j < 0} {
6060            # drawlineseg will do this one for us
6061            continue
6062        }
6063        assigncolor $p
6064        # should handle duplicated parents here...
6065        set coords [list $x $y]
6066        if {$i != $col} {
6067            # if attaching to a vertical segment, draw a smaller
6068            # slant for visual distinctness
6069            if {$i == $j} {
6070                if {$i < $col} {
6071                    lappend coords [expr {$x2 + $d}] $y $x2 $ymid
6072                } else {
6073                    lappend coords [expr {$x2 - $d}] $y $x2 $ymid
6074                }
6075            } elseif {$i < $col && $i < $j} {
6076                # segment slants towards us already
6077                lappend coords [xc $row $j] $y
6078            } else {
6079                if {$i < $col - 1} {
6080                    lappend coords [expr {$x2 + $linespc}] $y
6081                } elseif {$i > $col + 1} {
6082                    lappend coords [expr {$x2 - $linespc}] $y
6083                }
6084                lappend coords $x2 $y2
6085            }
6086        } else {
6087            lappend coords $x2 $y2
6088        }
6089        set t [$canv create line $coords -width [linewidth $p] \
6090                   -fill $colormap($p) -tags lines.$p]
6091        $canv lower $t
6092        bindline $t $p
6093    }
6094    if {$rmx > [lindex $idpos($id) 1]} {
6095        lset idpos($id) 1 $rmx
6096        redrawtags $id
6097    }
6098}
6099
6100proc drawlines {id} {
6101    global canv
6102
6103    $canv itemconf lines.$id -width [linewidth $id]
6104}
6105
6106proc drawcmittext {id row col} {
6107    global linespc canv canv2 canv3 fgcolor curview
6108    global cmitlisted commitinfo rowidlist parentlist
6109    global rowtextx idpos idtags idheads idotherrefs
6110    global linehtag linentag linedtag selectedline
6111    global canvxmax boldids boldnameids fgcolor markedid
6112    global mainheadid nullid nullid2 circleitem circlecolors ctxbut
6113    global mainheadcirclecolor workingfilescirclecolor indexcirclecolor
6114    global circleoutlinecolor
6115
6116    # listed is 0 for boundary, 1 for normal, 2 for negative, 3 for left, 4 for right
6117    set listed $cmitlisted($curview,$id)
6118    if {$id eq $nullid} {
6119        set ofill $workingfilescirclecolor
6120    } elseif {$id eq $nullid2} {
6121        set ofill $indexcirclecolor
6122    } elseif {$id eq $mainheadid} {
6123        set ofill $mainheadcirclecolor
6124    } else {
6125        set ofill [lindex $circlecolors $listed]
6126    }
6127    set x [xc $row $col]
6128    set y [yc $row]
6129    set orad [expr {$linespc / 3}]
6130    if {$listed <= 2} {
6131        set t [$canv create oval [expr {$x - $orad}] [expr {$y - $orad}] \
6132                   [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
6133                   -fill $ofill -outline $circleoutlinecolor -width 1 -tags circle]
6134    } elseif {$listed == 3} {
6135        # triangle pointing left for left-side commits
6136        set t [$canv create polygon \
6137                   [expr {$x - $orad}] $y \
6138                   [expr {$x + $orad - 1}] [expr {$y - $orad}] \
6139                   [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
6140                   -fill $ofill -outline $circleoutlinecolor -width 1 -tags circle]
6141    } else {
6142        # triangle pointing right for right-side commits
6143        set t [$canv create polygon \
6144                   [expr {$x + $orad - 1}] $y \
6145                   [expr {$x - $orad}] [expr {$y - $orad}] \
6146                   [expr {$x - $orad}] [expr {$y + $orad - 1}] \
6147                   -fill $ofill -outline $circleoutlinecolor -width 1 -tags circle]
6148    }
6149    set circleitem($row) $t
6150    $canv raise $t
6151    $canv bind $t <1> {selcanvline {} %x %y}
6152    set rmx [llength [lindex $rowidlist $row]]
6153    set olds [lindex $parentlist $row]
6154    if {$olds ne {}} {
6155        set nextids [lindex $rowidlist [expr {$row + 1}]]
6156        foreach p $olds {
6157            set i [lsearch -exact $nextids $p]
6158            if {$i > $rmx} {
6159                set rmx $i
6160            }
6161        }
6162    }
6163    set xt [xc $row $rmx]
6164    set rowtextx($row) $xt
6165    set idpos($id) [list $x $xt $y]
6166    if {[info exists idtags($id)] || [info exists idheads($id)]
6167        || [info exists idotherrefs($id)]} {
6168        set xt [drawtags $id $x $xt $y]
6169    }
6170    if {[lindex $commitinfo($id) 6] > 0} {
6171        set xt [drawnotesign $xt $y]
6172    }
6173    set headline [lindex $commitinfo($id) 0]
6174    set name [lindex $commitinfo($id) 1]
6175    set date [lindex $commitinfo($id) 2]
6176    set date [formatdate $date]
6177    set font mainfont
6178    set nfont mainfont
6179    set isbold [ishighlighted $id]
6180    if {$isbold > 0} {
6181        lappend boldids $id
6182        set font mainfontbold
6183        if {$isbold > 1} {
6184            lappend boldnameids $id
6185            set nfont mainfontbold
6186        }
6187    }
6188    set linehtag($id) [$canv create text $xt $y -anchor w -fill $fgcolor \
6189                           -text $headline -font $font -tags text]
6190    $canv bind $linehtag($id) $ctxbut "rowmenu %X %Y $id"
6191    set linentag($id) [$canv2 create text 3 $y -anchor w -fill $fgcolor \
6192                           -text $name -font $nfont -tags text]
6193    set linedtag($id) [$canv3 create text 3 $y -anchor w -fill $fgcolor \
6194                           -text $date -font mainfont -tags text]
6195    if {$selectedline == $row} {
6196        make_secsel $id
6197    }
6198    if {[info exists markedid] && $markedid eq $id} {
6199        make_idmark $id
6200    }
6201    set xr [expr {$xt + [font measure $font $headline]}]
6202    if {$xr > $canvxmax} {
6203        set canvxmax $xr
6204        setcanvscroll
6205    }
6206}
6207
6208proc drawcmitrow {row} {
6209    global displayorder rowidlist nrows_drawn
6210    global iddrawn markingmatches
6211    global commitinfo numcommits
6212    global filehighlight fhighlights findpattern nhighlights
6213    global hlview vhighlights
6214    global highlight_related rhighlights
6215
6216    if {$row >= $numcommits} return
6217
6218    set id [lindex $displayorder $row]
6219    if {[info exists hlview] && ![info exists vhighlights($id)]} {
6220        askvhighlight $row $id
6221    }
6222    if {[info exists filehighlight] && ![info exists fhighlights($id)]} {
6223        askfilehighlight $row $id
6224    }
6225    if {$findpattern ne {} && ![info exists nhighlights($id)]} {
6226        askfindhighlight $row $id
6227    }
6228    if {$highlight_related ne [mc "None"] && ![info exists rhighlights($id)]} {
6229        askrelhighlight $row $id
6230    }
6231    if {![info exists iddrawn($id)]} {
6232        set col [lsearch -exact [lindex $rowidlist $row] $id]
6233        if {$col < 0} {
6234            puts "oops, row $row id $id not in list"
6235            return
6236        }
6237        if {![info exists commitinfo($id)]} {
6238            getcommit $id
6239        }
6240        assigncolor $id
6241        drawcmittext $id $row $col
6242        set iddrawn($id) 1
6243        incr nrows_drawn
6244    }
6245    if {$markingmatches} {
6246        markrowmatches $row $id
6247    }
6248}
6249
6250proc drawcommits {row {endrow {}}} {
6251    global numcommits iddrawn displayorder curview need_redisplay
6252    global parentlist rowidlist rowfinal uparrowlen downarrowlen nrows_drawn
6253
6254    if {$row < 0} {
6255        set row 0
6256    }
6257    if {$endrow eq {}} {
6258        set endrow $row
6259    }
6260    if {$endrow >= $numcommits} {
6261        set endrow [expr {$numcommits - 1}]
6262    }
6263
6264    set rl1 [expr {$row - $downarrowlen - 3}]
6265    if {$rl1 < 0} {
6266        set rl1 0
6267    }
6268    set ro1 [expr {$row - 3}]
6269    if {$ro1 < 0} {
6270        set ro1 0
6271    }
6272    set r2 [expr {$endrow + $uparrowlen + 3}]
6273    if {$r2 > $numcommits} {
6274        set r2 $numcommits
6275    }
6276    for {set r $rl1} {$r < $r2} {incr r} {
6277        if {[lindex $rowidlist $r] ne {} && [lindex $rowfinal $r]} {
6278            if {$rl1 < $r} {
6279                layoutrows $rl1 $r
6280            }
6281            set rl1 [expr {$r + 1}]
6282        }
6283    }
6284    if {$rl1 < $r} {
6285        layoutrows $rl1 $r
6286    }
6287    optimize_rows $ro1 0 $r2
6288    if {$need_redisplay || $nrows_drawn > 2000} {
6289        clear_display
6290    }
6291
6292    # make the lines join to already-drawn rows either side
6293    set r [expr {$row - 1}]
6294    if {$r < 0 || ![info exists iddrawn([lindex $displayorder $r])]} {
6295        set r $row
6296    }
6297    set er [expr {$endrow + 1}]
6298    if {$er >= $numcommits ||
6299        ![info exists iddrawn([lindex $displayorder $er])]} {
6300        set er $endrow
6301    }
6302    for {} {$r <= $er} {incr r} {
6303        set id [lindex $displayorder $r]
6304        set wasdrawn [info exists iddrawn($id)]
6305        drawcmitrow $r
6306        if {$r == $er} break
6307        set nextid [lindex $displayorder [expr {$r + 1}]]
6308        if {$wasdrawn && [info exists iddrawn($nextid)]} continue
6309        drawparentlinks $id $r
6310
6311        set rowids [lindex $rowidlist $r]
6312        foreach lid $rowids {
6313            if {$lid eq {}} continue
6314            if {[info exists lineend($lid)] && $lineend($lid) > $r} continue
6315            if {$lid eq $id} {
6316                # see if this is the first child of any of its parents
6317                foreach p [lindex $parentlist $r] {
6318                    if {[lsearch -exact $rowids $p] < 0} {
6319                        # make this line extend up to the child
6320                        set lineend($p) [drawlineseg $p $r $er 0]
6321                    }
6322                }
6323            } else {
6324                set lineend($lid) [drawlineseg $lid $r $er 1]
6325            }
6326        }
6327    }
6328}
6329
6330proc undolayout {row} {
6331    global uparrowlen mingaplen downarrowlen
6332    global rowidlist rowisopt rowfinal need_redisplay
6333
6334    set r [expr {$row - ($uparrowlen + $mingaplen + $downarrowlen)}]
6335    if {$r < 0} {
6336        set r 0
6337    }
6338    if {[llength $rowidlist] > $r} {
6339        incr r -1
6340        set rowidlist [lrange $rowidlist 0 $r]
6341        set rowfinal [lrange $rowfinal 0 $r]
6342        set rowisopt [lrange $rowisopt 0 $r]
6343        set need_redisplay 1
6344        run drawvisible
6345    }
6346}
6347
6348proc drawvisible {} {
6349    global canv linespc curview vrowmod selectedline targetrow targetid
6350    global need_redisplay cscroll numcommits
6351
6352    set fs [$canv yview]
6353    set ymax [lindex [$canv cget -scrollregion] 3]
6354    if {$ymax eq {} || $ymax == 0 || $numcommits == 0} return
6355    set f0 [lindex $fs 0]
6356    set f1 [lindex $fs 1]
6357    set y0 [expr {int($f0 * $ymax)}]
6358    set y1 [expr {int($f1 * $ymax)}]
6359
6360    if {[info exists targetid]} {
6361        if {[commitinview $targetid $curview]} {
6362            set r [rowofcommit $targetid]
6363            if {$r != $targetrow} {
6364                # Fix up the scrollregion and change the scrolling position
6365                # now that our target row has moved.
6366                set diff [expr {($r - $targetrow) * $linespc}]
6367                set targetrow $r
6368                setcanvscroll
6369                set ymax [lindex [$canv cget -scrollregion] 3]
6370                incr y0 $diff
6371                incr y1 $diff
6372                set f0 [expr {$y0 / $ymax}]
6373                set f1 [expr {$y1 / $ymax}]
6374                allcanvs yview moveto $f0
6375                $cscroll set $f0 $f1
6376                set need_redisplay 1
6377            }
6378        } else {
6379            unset targetid
6380        }
6381    }
6382
6383    set row [expr {int(($y0 - 3) / $linespc) - 1}]
6384    set endrow [expr {int(($y1 - 3) / $linespc) + 1}]
6385    if {$endrow >= $vrowmod($curview)} {
6386        update_arcrows $curview
6387    }
6388    if {$selectedline ne {} &&
6389        $row <= $selectedline && $selectedline <= $endrow} {
6390        set targetrow $selectedline
6391    } elseif {[info exists targetid]} {
6392        set targetrow [expr {int(($row + $endrow) / 2)}]
6393    }
6394    if {[info exists targetrow]} {
6395        if {$targetrow >= $numcommits} {
6396            set targetrow [expr {$numcommits - 1}]
6397        }
6398        set targetid [commitonrow $targetrow]
6399    }
6400    drawcommits $row $endrow
6401}
6402
6403proc clear_display {} {
6404    global iddrawn linesegs need_redisplay nrows_drawn
6405    global vhighlights fhighlights nhighlights rhighlights
6406    global linehtag linentag linedtag boldids boldnameids
6407
6408    allcanvs delete all
6409    unset -nocomplain iddrawn
6410    unset -nocomplain linesegs
6411    unset -nocomplain linehtag
6412    unset -nocomplain linentag
6413    unset -nocomplain linedtag
6414    set boldids {}
6415    set boldnameids {}
6416    unset -nocomplain vhighlights
6417    unset -nocomplain fhighlights
6418    unset -nocomplain nhighlights
6419    unset -nocomplain rhighlights
6420    set need_redisplay 0
6421    set nrows_drawn 0
6422}
6423
6424proc findcrossings {id} {
6425    global rowidlist parentlist numcommits displayorder
6426
6427    set cross {}
6428    set ccross {}
6429    foreach {s e} [rowranges $id] {
6430        if {$e >= $numcommits} {
6431            set e [expr {$numcommits - 1}]
6432        }
6433        if {$e <= $s} continue
6434        for {set row $e} {[incr row -1] >= $s} {} {
6435            set x [lsearch -exact [lindex $rowidlist $row] $id]
6436            if {$x < 0} break
6437            set olds [lindex $parentlist $row]
6438            set kid [lindex $displayorder $row]
6439            set kidx [lsearch -exact [lindex $rowidlist $row] $kid]
6440            if {$kidx < 0} continue
6441            set nextrow [lindex $rowidlist [expr {$row + 1}]]
6442            foreach p $olds {
6443                set px [lsearch -exact $nextrow $p]
6444                if {$px < 0} continue
6445                if {($kidx < $x && $x < $px) || ($px < $x && $x < $kidx)} {
6446                    if {[lsearch -exact $ccross $p] >= 0} continue
6447                    if {$x == $px + ($kidx < $px? -1: 1)} {
6448                        lappend ccross $p
6449                    } elseif {[lsearch -exact $cross $p] < 0} {
6450                        lappend cross $p
6451                    }
6452                }
6453            }
6454        }
6455    }
6456    return [concat $ccross {{}} $cross]
6457}
6458
6459proc assigncolor {id} {
6460    global colormap colors nextcolor
6461    global parents children children curview
6462
6463    if {[info exists colormap($id)]} return
6464    set ncolors [llength $colors]
6465    if {[info exists children($curview,$id)]} {
6466        set kids $children($curview,$id)
6467    } else {
6468        set kids {}
6469    }
6470    if {[llength $kids] == 1} {
6471        set child [lindex $kids 0]
6472        if {[info exists colormap($child)]
6473            && [llength $parents($curview,$child)] == 1} {
6474            set colormap($id) $colormap($child)
6475            return
6476        }
6477    }
6478    set badcolors {}
6479    set origbad {}
6480    foreach x [findcrossings $id] {
6481        if {$x eq {}} {
6482            # delimiter between corner crossings and other crossings
6483            if {[llength $badcolors] >= $ncolors - 1} break
6484            set origbad $badcolors
6485        }
6486        if {[info exists colormap($x)]
6487            && [lsearch -exact $badcolors $colormap($x)] < 0} {
6488            lappend badcolors $colormap($x)
6489        }
6490    }
6491    if {[llength $badcolors] >= $ncolors} {
6492        set badcolors $origbad
6493    }
6494    set origbad $badcolors
6495    if {[llength $badcolors] < $ncolors - 1} {
6496        foreach child $kids {
6497            if {[info exists colormap($child)]
6498                && [lsearch -exact $badcolors $colormap($child)] < 0} {
6499                lappend badcolors $colormap($child)
6500            }
6501            foreach p $parents($curview,$child) {
6502                if {[info exists colormap($p)]
6503                    && [lsearch -exact $badcolors $colormap($p)] < 0} {
6504                    lappend badcolors $colormap($p)
6505                }
6506            }
6507        }
6508        if {[llength $badcolors] >= $ncolors} {
6509            set badcolors $origbad
6510        }
6511    }
6512    for {set i 0} {$i <= $ncolors} {incr i} {
6513        set c [lindex $colors $nextcolor]
6514        if {[incr nextcolor] >= $ncolors} {
6515            set nextcolor 0
6516        }
6517        if {[lsearch -exact $badcolors $c]} break
6518    }
6519    set colormap($id) $c
6520}
6521
6522proc bindline {t id} {
6523    global canv
6524
6525    $canv bind $t <Enter> "lineenter %x %y $id"
6526    $canv bind $t <Motion> "linemotion %x %y $id"
6527    $canv bind $t <Leave> "lineleave $id"
6528    $canv bind $t <Button-1> "lineclick %x %y $id 1"
6529}
6530
6531proc graph_pane_width {} {
6532    global use_ttk
6533
6534    if {$use_ttk} {
6535        set g [.tf.histframe.pwclist sashpos 0]
6536    } else {
6537        set g [.tf.histframe.pwclist sash coord 0]
6538    }
6539    return [lindex $g 0]
6540}
6541
6542proc totalwidth {l font extra} {
6543    set tot 0
6544    foreach str $l {
6545        set tot [expr {$tot + [font measure $font $str] + $extra}]
6546    }
6547    return $tot
6548}
6549
6550proc drawtags {id x xt y1} {
6551    global idtags idheads idotherrefs mainhead
6552    global linespc lthickness
6553    global canv rowtextx curview fgcolor bgcolor ctxbut
6554    global headbgcolor headfgcolor headoutlinecolor remotebgcolor
6555    global tagbgcolor tagfgcolor tagoutlinecolor
6556    global reflinecolor
6557
6558    set marks {}
6559    set ntags 0
6560    set nheads 0
6561    set singletag 0
6562    set maxtags 3
6563    set maxtagpct 25
6564    set maxwidth [expr {[graph_pane_width] * $maxtagpct / 100}]
6565    set delta [expr {int(0.5 * ($linespc - $lthickness))}]
6566    set extra [expr {$delta + $lthickness + $linespc}]
6567
6568    if {[info exists idtags($id)]} {
6569        set marks $idtags($id)
6570        set ntags [llength $marks]
6571        if {$ntags > $maxtags ||
6572            [totalwidth $marks mainfont $extra] > $maxwidth} {
6573            # show just a single "n tags..." tag
6574            set singletag 1
6575            if {$ntags == 1} {
6576                set marks [list "tag..."]
6577            } else {
6578                set marks [list [format "%d tags..." $ntags]]
6579            }
6580            set ntags 1
6581        }
6582    }
6583    if {[info exists idheads($id)]} {
6584        set marks [concat $marks $idheads($id)]
6585        set nheads [llength $idheads($id)]
6586    }
6587    if {[info exists idotherrefs($id)]} {
6588        set marks [concat $marks $idotherrefs($id)]
6589    }
6590    if {$marks eq {}} {
6591        return $xt
6592    }
6593
6594    set yt [expr {$y1 - 0.5 * $linespc}]
6595    set yb [expr {$yt + $linespc - 1}]
6596    set xvals {}
6597    set wvals {}
6598    set i -1
6599    foreach tag $marks {
6600        incr i
6601        if {$i >= $ntags && $i < $ntags + $nheads && $tag eq $mainhead} {
6602            set wid [font measure mainfontbold $tag]
6603        } else {
6604            set wid [font measure mainfont $tag]
6605        }
6606        lappend xvals $xt
6607        lappend wvals $wid
6608        set xt [expr {$xt + $wid + $extra}]
6609    }
6610    set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
6611               -width $lthickness -fill $reflinecolor -tags tag.$id]
6612    $canv lower $t
6613    foreach tag $marks x $xvals wid $wvals {
6614        set tag_quoted [string map {% %%} $tag]
6615        set xl [expr {$x + $delta}]
6616        set xr [expr {$x + $delta + $wid + $lthickness}]
6617        set font mainfont
6618        if {[incr ntags -1] >= 0} {
6619            # draw a tag
6620            set t [$canv create polygon $x [expr {$yt + $delta}] $xl $yt \
6621                       $xr $yt $xr $yb $xl $yb $x [expr {$yb - $delta}] \
6622                       -width 1 -outline $tagoutlinecolor -fill $tagbgcolor \
6623                       -tags tag.$id]
6624            if {$singletag} {
6625                set tagclick [list showtags $id 1]
6626            } else {
6627                set tagclick [list showtag $tag_quoted 1]
6628            }
6629            $canv bind $t <1> $tagclick
6630            set rowtextx([rowofcommit $id]) [expr {$xr + $linespc}]
6631        } else {
6632            # draw a head or other ref
6633            if {[incr nheads -1] >= 0} {
6634                set col $headbgcolor
6635                if {$tag eq $mainhead} {
6636                    set font mainfontbold
6637                }
6638            } else {
6639                set col "#ddddff"
6640            }
6641            set xl [expr {$xl - $delta/2}]
6642            $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
6643                -width 1 -outline black -fill $col -tags tag.$id
6644            if {[regexp {^(remotes/.*/|remotes/)} $tag match remoteprefix]} {
6645                set rwid [font measure mainfont $remoteprefix]
6646                set xi [expr {$x + 1}]
6647                set yti [expr {$yt + 1}]
6648                set xri [expr {$x + $rwid}]
6649                $canv create polygon $xi $yti $xri $yti $xri $yb $xi $yb \
6650                        -width 0 -fill $remotebgcolor -tags tag.$id
6651            }
6652        }
6653        set t [$canv create text $xl $y1 -anchor w -text $tag -fill $headfgcolor \
6654                   -font $font -tags [list tag.$id text]]
6655        if {$ntags >= 0} {
6656            $canv bind $t <1> $tagclick
6657        } elseif {$nheads >= 0} {
6658            $canv bind $t $ctxbut [list headmenu %X %Y $id $tag_quoted]
6659        }
6660    }
6661    return $xt
6662}
6663
6664proc drawnotesign {xt y} {
6665    global linespc canv fgcolor
6666
6667    set orad [expr {$linespc / 3}]
6668    set t [$canv create rectangle [expr {$xt - $orad}] [expr {$y - $orad}] \
6669               [expr {$xt + $orad - 1}] [expr {$y + $orad - 1}] \
6670               -fill yellow -outline $fgcolor -width 1 -tags circle]
6671    set xt [expr {$xt + $orad * 3}]
6672    return $xt
6673}
6674
6675proc xcoord {i level ln} {
6676    global canvx0 xspc1 xspc2
6677
6678    set x [expr {$canvx0 + $i * $xspc1($ln)}]
6679    if {$i > 0 && $i == $level} {
6680        set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
6681    } elseif {$i > $level} {
6682        set x [expr {$x + $xspc2 - $xspc1($ln)}]
6683    }
6684    return $x
6685}
6686
6687proc show_status {msg} {
6688    global canv fgcolor
6689
6690    clear_display
6691    set_window_title
6692    $canv create text 3 3 -anchor nw -text $msg -font mainfont \
6693        -tags text -fill $fgcolor
6694}
6695
6696# Don't change the text pane cursor if it is currently the hand cursor,
6697# showing that we are over a sha1 ID link.
6698proc settextcursor {c} {
6699    global ctext curtextcursor
6700
6701    if {[$ctext cget -cursor] == $curtextcursor} {
6702        $ctext config -cursor $c
6703    }
6704    set curtextcursor $c
6705}
6706
6707proc nowbusy {what {name {}}} {
6708    global isbusy busyname statusw
6709
6710    if {[array names isbusy] eq {}} {
6711        . config -cursor watch
6712        settextcursor watch
6713    }
6714    set isbusy($what) 1
6715    set busyname($what) $name
6716    if {$name ne {}} {
6717        $statusw conf -text $name
6718    }
6719}
6720
6721proc notbusy {what} {
6722    global isbusy maincursor textcursor busyname statusw
6723
6724    catch {
6725        unset isbusy($what)
6726        if {$busyname($what) ne {} &&
6727            [$statusw cget -text] eq $busyname($what)} {
6728            $statusw conf -text {}
6729        }
6730    }
6731    if {[array names isbusy] eq {}} {
6732        . config -cursor $maincursor
6733        settextcursor $textcursor
6734    }
6735}
6736
6737proc findmatches {f} {
6738    global findtype findstring
6739    if {$findtype == [mc "Regexp"]} {
6740        set matches [regexp -indices -all -inline $findstring $f]
6741    } else {
6742        set fs $findstring
6743        if {$findtype == [mc "IgnCase"]} {
6744            set f [string tolower $f]
6745            set fs [string tolower $fs]
6746        }
6747        set matches {}
6748        set i 0
6749        set l [string length $fs]
6750        while {[set j [string first $fs $f $i]] >= 0} {
6751            lappend matches [list $j [expr {$j+$l-1}]]
6752            set i [expr {$j + $l}]
6753        }
6754    }
6755    return $matches
6756}
6757
6758proc dofind {{dirn 1} {wrap 1}} {
6759    global findstring findstartline findcurline selectedline numcommits
6760    global gdttype filehighlight fh_serial find_dirn findallowwrap
6761
6762    if {[info exists find_dirn]} {
6763        if {$find_dirn == $dirn} return
6764        stopfinding
6765    }
6766    focus .
6767    if {$findstring eq {} || $numcommits == 0} return
6768    if {$selectedline eq {}} {
6769        set findstartline [lindex [visiblerows] [expr {$dirn < 0}]]
6770    } else {
6771        set findstartline $selectedline
6772    }
6773    set findcurline $findstartline
6774    nowbusy finding [mc "Searching"]
6775    if {$gdttype ne [mc "containing:"] && ![info exists filehighlight]} {
6776        after cancel do_file_hl $fh_serial
6777        do_file_hl $fh_serial
6778    }
6779    set find_dirn $dirn
6780    set findallowwrap $wrap
6781    run findmore
6782}
6783
6784proc stopfinding {} {
6785    global find_dirn findcurline fprogcoord
6786
6787    if {[info exists find_dirn]} {
6788        unset find_dirn
6789        unset findcurline
6790        notbusy finding
6791        set fprogcoord 0
6792        adjustprogress
6793    }
6794    stopblaming
6795}
6796
6797proc findmore {} {
6798    global commitdata commitinfo numcommits findpattern findloc
6799    global findstartline findcurline findallowwrap
6800    global find_dirn gdttype fhighlights fprogcoord
6801    global curview varcorder vrownum varccommits vrowmod
6802
6803    if {![info exists find_dirn]} {
6804        return 0
6805    }
6806    set fldtypes [list [mc "Headline"] [mc "Author"] "" [mc "Committer"] "" [mc "Comments"]]
6807    set l $findcurline
6808    set moretodo 0
6809    if {$find_dirn > 0} {
6810        incr l
6811        if {$l >= $numcommits} {
6812            set l 0
6813        }
6814        if {$l <= $findstartline} {
6815            set lim [expr {$findstartline + 1}]
6816        } else {
6817            set lim $numcommits
6818            set moretodo $findallowwrap
6819        }
6820    } else {
6821        if {$l == 0} {
6822            set l $numcommits
6823        }
6824        incr l -1
6825        if {$l >= $findstartline} {
6826            set lim [expr {$findstartline - 1}]
6827        } else {
6828            set lim -1
6829            set moretodo $findallowwrap
6830        }
6831    }
6832    set n [expr {($lim - $l) * $find_dirn}]
6833    if {$n > 500} {
6834        set n 500
6835        set moretodo 1
6836    }
6837    if {$l + ($find_dirn > 0? $n: 1) > $vrowmod($curview)} {
6838        update_arcrows $curview
6839    }
6840    set found 0
6841    set domore 1
6842    set ai [bsearch $vrownum($curview) $l]
6843    set a [lindex $varcorder($curview) $ai]
6844    set arow [lindex $vrownum($curview) $ai]
6845    set ids [lindex $varccommits($curview,$a)]
6846    set arowend [expr {$arow + [llength $ids]}]
6847    if {$gdttype eq [mc "containing:"]} {
6848        for {} {$n > 0} {incr n -1; incr l $find_dirn} {
6849            if {$l < $arow || $l >= $arowend} {
6850                incr ai $find_dirn
6851                set a [lindex $varcorder($curview) $ai]
6852                set arow [lindex $vrownum($curview) $ai]
6853                set ids [lindex $varccommits($curview,$a)]
6854                set arowend [expr {$arow + [llength $ids]}]
6855            }
6856            set id [lindex $ids [expr {$l - $arow}]]
6857            # shouldn't happen unless git log doesn't give all the commits...
6858            if {![info exists commitdata($id)] ||
6859                ![doesmatch $commitdata($id)]} {
6860                continue
6861            }
6862            if {![info exists commitinfo($id)]} {
6863                getcommit $id
6864            }
6865            set info $commitinfo($id)
6866            foreach f $info ty $fldtypes {
6867                if {$ty eq ""} continue
6868                if {($findloc eq [mc "All fields"] || $findloc eq $ty) &&
6869                    [doesmatch $f]} {
6870                    set found 1
6871                    break
6872                }
6873            }
6874            if {$found} break
6875        }
6876    } else {
6877        for {} {$n > 0} {incr n -1; incr l $find_dirn} {
6878            if {$l < $arow || $l >= $arowend} {
6879                incr ai $find_dirn
6880                set a [lindex $varcorder($curview) $ai]
6881                set arow [lindex $vrownum($curview) $ai]
6882                set ids [lindex $varccommits($curview,$a)]
6883                set arowend [expr {$arow + [llength $ids]}]
6884            }
6885            set id [lindex $ids [expr {$l - $arow}]]
6886            if {![info exists fhighlights($id)]} {
6887                # this sets fhighlights($id) to -1
6888                askfilehighlight $l $id
6889            }
6890            if {$fhighlights($id) > 0} {
6891                set found $domore
6892                break
6893            }
6894            if {$fhighlights($id) < 0} {
6895                if {$domore} {
6896                    set domore 0
6897                    set findcurline [expr {$l - $find_dirn}]
6898                }
6899            }
6900        }
6901    }
6902    if {$found || ($domore && !$moretodo)} {
6903        unset findcurline
6904        unset find_dirn
6905        notbusy finding
6906        set fprogcoord 0
6907        adjustprogress
6908        if {$found} {
6909            findselectline $l
6910        } else {
6911            bell
6912        }
6913        return 0
6914    }
6915    if {!$domore} {
6916        flushhighlights
6917    } else {
6918        set findcurline [expr {$l - $find_dirn}]
6919    }
6920    set n [expr {($findcurline - $findstartline) * $find_dirn - 1}]
6921    if {$n < 0} {
6922        incr n $numcommits
6923    }
6924    set fprogcoord [expr {$n * 1.0 / $numcommits}]
6925    adjustprogress
6926    return $domore
6927}
6928
6929proc findselectline {l} {
6930    global findloc commentend ctext findcurline markingmatches gdttype
6931
6932    set markingmatches [expr {$gdttype eq [mc "containing:"]}]
6933    set findcurline $l
6934    selectline $l 1
6935    if {$markingmatches &&
6936        ($findloc eq [mc "All fields"] || $findloc eq [mc "Comments"])} {
6937        # highlight the matches in the comments
6938        set f [$ctext get 1.0 $commentend]
6939        set matches [findmatches $f]
6940        foreach match $matches {
6941            set start [lindex $match 0]
6942            set end [expr {[lindex $match 1] + 1}]
6943            $ctext tag add found "1.0 + $start c" "1.0 + $end c"
6944        }
6945    }
6946    drawvisible
6947}
6948
6949# mark the bits of a headline or author that match a find string
6950proc markmatches {canv l str tag matches font row} {
6951    global selectedline
6952
6953    set bbox [$canv bbox $tag]
6954    set x0 [lindex $bbox 0]
6955    set y0 [lindex $bbox 1]
6956    set y1 [lindex $bbox 3]
6957    foreach match $matches {
6958        set start [lindex $match 0]
6959        set end [lindex $match 1]
6960        if {$start > $end} continue
6961        set xoff [font measure $font [string range $str 0 [expr {$start-1}]]]
6962        set xlen [font measure $font [string range $str 0 [expr {$end}]]]
6963        set t [$canv create rect [expr {$x0+$xoff}] $y0 \
6964                   [expr {$x0+$xlen+2}] $y1 \
6965                   -outline {} -tags [list match$l matches] -fill yellow]
6966        $canv lower $t
6967        if {$row == $selectedline} {
6968            $canv raise $t secsel
6969        }
6970    }
6971}
6972
6973proc unmarkmatches {} {
6974    global markingmatches
6975
6976    allcanvs delete matches
6977    set markingmatches 0
6978    stopfinding
6979}
6980
6981proc selcanvline {w x y} {
6982    global canv canvy0 ctext linespc
6983    global rowtextx
6984    set ymax [lindex [$canv cget -scrollregion] 3]
6985    if {$ymax == {}} return
6986    set yfrac [lindex [$canv yview] 0]
6987    set y [expr {$y + $yfrac * $ymax}]
6988    set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
6989    if {$l < 0} {
6990        set l 0
6991    }
6992    if {$w eq $canv} {
6993        set xmax [lindex [$canv cget -scrollregion] 2]
6994        set xleft [expr {[lindex [$canv xview] 0] * $xmax}]
6995        if {![info exists rowtextx($l)] || $xleft + $x < $rowtextx($l)} return
6996    }
6997    unmarkmatches
6998    selectline $l 1
6999}
7000
7001proc commit_descriptor {p} {
7002    global commitinfo
7003    if {![info exists commitinfo($p)]} {
7004        getcommit $p
7005    }
7006    set l "..."
7007    if {[llength $commitinfo($p)] > 1} {
7008        set l [lindex $commitinfo($p) 0]
7009    }
7010    return "$p ($l)\n"
7011}
7012
7013# append some text to the ctext widget, and make any SHA1 ID
7014# that we know about be a clickable link.
7015proc appendwithlinks {text tags} {
7016    global ctext linknum curview
7017
7018    set start [$ctext index "end - 1c"]
7019    $ctext insert end $text $tags
7020    set links [regexp -indices -all -inline {(?:\m|-g)[0-9a-f]{6,40}\M} $text]
7021    foreach l $links {
7022        set s [lindex $l 0]
7023        set e [lindex $l 1]
7024        set linkid [string range $text $s $e]
7025        incr e
7026        $ctext tag delete link$linknum
7027        $ctext tag add link$linknum "$start + $s c" "$start + $e c"
7028        setlink $linkid link$linknum
7029        incr linknum
7030    }
7031}
7032
7033proc setlink {id lk} {
7034    global curview ctext pendinglinks
7035    global linkfgcolor
7036
7037    if {[string range $id 0 1] eq "-g"} {
7038      set id [string range $id 2 end]
7039    }
7040
7041    set known 0
7042    if {[string length $id] < 40} {
7043        set matches [longid $id]
7044        if {[llength $matches] > 0} {
7045            if {[llength $matches] > 1} return
7046            set known 1
7047            set id [lindex $matches 0]
7048        }
7049    } else {
7050        set known [commitinview $id $curview]
7051    }
7052    if {$known} {
7053        $ctext tag conf $lk -foreground $linkfgcolor -underline 1
7054        $ctext tag bind $lk <1> [list selbyid $id]
7055        $ctext tag bind $lk <Enter> {linkcursor %W 1}
7056        $ctext tag bind $lk <Leave> {linkcursor %W -1}
7057    } else {
7058        lappend pendinglinks($id) $lk
7059        interestedin $id {makelink %P}
7060    }
7061}
7062
7063proc appendshortlink {id {pre {}} {post {}}} {
7064    global ctext linknum
7065
7066    $ctext insert end $pre
7067    $ctext tag delete link$linknum
7068    $ctext insert end [string range $id 0 7] link$linknum
7069    $ctext insert end $post
7070    setlink $id link$linknum
7071    incr linknum
7072}
7073
7074proc makelink {id} {
7075    global pendinglinks
7076
7077    if {![info exists pendinglinks($id)]} return
7078    foreach lk $pendinglinks($id) {
7079        setlink $id $lk
7080    }
7081    unset pendinglinks($id)
7082}
7083
7084proc linkcursor {w inc} {
7085    global linkentercount curtextcursor
7086
7087    if {[incr linkentercount $inc] > 0} {
7088        $w configure -cursor hand2
7089    } else {
7090        $w configure -cursor $curtextcursor
7091        if {$linkentercount < 0} {
7092            set linkentercount 0
7093        }
7094    }
7095}
7096
7097proc viewnextline {dir} {
7098    global canv linespc
7099
7100    $canv delete hover
7101    set ymax [lindex [$canv cget -scrollregion] 3]
7102    set wnow [$canv yview]
7103    set wtop [expr {[lindex $wnow 0] * $ymax}]
7104    set newtop [expr {$wtop + $dir * $linespc}]
7105    if {$newtop < 0} {
7106        set newtop 0
7107    } elseif {$newtop > $ymax} {
7108        set newtop $ymax
7109    }
7110    allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
7111}
7112
7113# add a list of tag or branch names at position pos
7114# returns the number of names inserted
7115proc appendrefs {pos ids var} {
7116    global ctext linknum curview $var maxrefs visiblerefs mainheadid
7117
7118    if {[catch {$ctext index $pos}]} {
7119        return 0
7120    }
7121    $ctext conf -state normal
7122    $ctext delete $pos "$pos lineend"
7123    set tags {}
7124    foreach id $ids {
7125        foreach tag [set $var\($id\)] {
7126            lappend tags [list $tag $id]
7127        }
7128    }
7129
7130    set sep {}
7131    set tags [lsort -index 0 -decreasing $tags]
7132    set nutags 0
7133
7134    if {[llength $tags] > $maxrefs} {
7135        # If we are displaying heads, and there are too many,
7136        # see if there are some important heads to display.
7137        # Currently that are the current head and heads listed in $visiblerefs option
7138        set itags {}
7139        if {$var eq "idheads"} {
7140            set utags {}
7141            foreach ti $tags {
7142                set hname [lindex $ti 0]
7143                set id [lindex $ti 1]
7144                if {([lsearch -exact $visiblerefs $hname] != -1 || $id eq $mainheadid) &&
7145                    [llength $itags] < $maxrefs} {
7146                    lappend itags $ti
7147                } else {
7148                    lappend utags $ti
7149                }
7150            }
7151            set tags $utags
7152        }
7153        if {$itags ne {}} {
7154            set str [mc "and many more"]
7155            set sep " "
7156        } else {
7157            set str [mc "many"]
7158        }
7159        $ctext insert $pos "$str ([llength $tags])"
7160        set nutags [llength $tags]
7161        set tags $itags
7162    }
7163
7164    foreach ti $tags {
7165        set id [lindex $ti 1]
7166        set lk link$linknum
7167        incr linknum
7168        $ctext tag delete $lk
7169        $ctext insert $pos $sep
7170        $ctext insert $pos [lindex $ti 0] $lk
7171        setlink $id $lk
7172        set sep ", "
7173    }
7174    $ctext tag add wwrap "$pos linestart" "$pos lineend"
7175    $ctext conf -state disabled
7176    return [expr {[llength $tags] + $nutags}]
7177}
7178
7179# called when we have finished computing the nearby tags
7180proc dispneartags {delay} {
7181    global selectedline currentid showneartags tagphase
7182
7183    if {$selectedline eq {} || !$showneartags} return
7184    after cancel dispnexttag
7185    if {$delay} {
7186        after 200 dispnexttag
7187        set tagphase -1
7188    } else {
7189        after idle dispnexttag
7190        set tagphase 0
7191    }
7192}
7193
7194proc dispnexttag {} {
7195    global selectedline currentid showneartags tagphase ctext
7196
7197    if {$selectedline eq {} || !$showneartags} return
7198    switch -- $tagphase {
7199        0 {
7200            set dtags [desctags $currentid]
7201            if {$dtags ne {}} {
7202                appendrefs precedes $dtags idtags
7203            }
7204        }
7205        1 {
7206            set atags [anctags $currentid]
7207            if {$atags ne {}} {
7208                appendrefs follows $atags idtags
7209            }
7210        }
7211        2 {
7212            set dheads [descheads $currentid]
7213            if {$dheads ne {}} {
7214                if {[appendrefs branch $dheads idheads] > 1
7215                    && [$ctext get "branch -3c"] eq "h"} {
7216                    # turn "Branch" into "Branches"
7217                    $ctext conf -state normal
7218                    $ctext insert "branch -2c" "es"
7219                    $ctext conf -state disabled
7220                }
7221            }
7222        }
7223    }
7224    if {[incr tagphase] <= 2} {
7225        after idle dispnexttag
7226    }
7227}
7228
7229proc make_secsel {id} {
7230    global linehtag linentag linedtag canv canv2 canv3
7231
7232    if {![info exists linehtag($id)]} return
7233    $canv delete secsel
7234    set t [eval $canv create rect [$canv bbox $linehtag($id)] -outline {{}} \
7235               -tags secsel -fill [$canv cget -selectbackground]]
7236    $canv lower $t
7237    $canv2 delete secsel
7238    set t [eval $canv2 create rect [$canv2 bbox $linentag($id)] -outline {{}} \
7239               -tags secsel -fill [$canv2 cget -selectbackground]]
7240    $canv2 lower $t
7241    $canv3 delete secsel
7242    set t [eval $canv3 create rect [$canv3 bbox $linedtag($id)] -outline {{}} \
7243               -tags secsel -fill [$canv3 cget -selectbackground]]
7244    $canv3 lower $t
7245}
7246
7247proc make_idmark {id} {
7248    global linehtag canv fgcolor
7249
7250    if {![info exists linehtag($id)]} return
7251    $canv delete markid
7252    set t [eval $canv create rect [$canv bbox $linehtag($id)] \
7253               -tags markid -outline $fgcolor]
7254    $canv raise $t
7255}
7256
7257proc selectline {l isnew {desired_loc {}} {switch_to_patch 0}} {
7258    global canv ctext commitinfo selectedline
7259    global canvy0 linespc parents children curview
7260    global currentid sha1entry
7261    global commentend idtags linknum
7262    global mergemax numcommits pending_select
7263    global cmitmode showneartags allcommits
7264    global targetrow targetid lastscrollrows
7265    global autoselect autosellen jump_to_here
7266    global vinlinediff
7267
7268    unset -nocomplain pending_select
7269    $canv delete hover
7270    normalline
7271    unsel_reflist
7272    stopfinding
7273    if {$l < 0 || $l >= $numcommits} return
7274    set id [commitonrow $l]
7275    set targetid $id
7276    set targetrow $l
7277    set selectedline $l
7278    set currentid $id
7279    if {$lastscrollrows < $numcommits} {
7280        setcanvscroll
7281    }
7282
7283    if {$cmitmode ne "patch" && $switch_to_patch} {
7284        set cmitmode "patch"
7285    }
7286
7287    set y [expr {$canvy0 + $l * $linespc}]
7288    set ymax [lindex [$canv cget -scrollregion] 3]
7289    set ytop [expr {$y - $linespc - 1}]
7290    set ybot [expr {$y + $linespc + 1}]
7291    set wnow [$canv yview]
7292    set wtop [expr {[lindex $wnow 0] * $ymax}]
7293    set wbot [expr {[lindex $wnow 1] * $ymax}]
7294    set wh [expr {$wbot - $wtop}]
7295    set newtop $wtop
7296    if {$ytop < $wtop} {
7297        if {$ybot < $wtop} {
7298            set newtop [expr {$y - $wh / 2.0}]
7299        } else {
7300            set newtop $ytop
7301            if {$newtop > $wtop - $linespc} {
7302                set newtop [expr {$wtop - $linespc}]
7303            }
7304        }
7305    } elseif {$ybot > $wbot} {
7306        if {$ytop > $wbot} {
7307            set newtop [expr {$y - $wh / 2.0}]
7308        } else {
7309            set newtop [expr {$ybot - $wh}]
7310            if {$newtop < $wtop + $linespc} {
7311                set newtop [expr {$wtop + $linespc}]
7312            }
7313        }
7314    }
7315    if {$newtop != $wtop} {
7316        if {$newtop < 0} {
7317            set newtop 0
7318        }
7319        allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
7320        drawvisible
7321    }
7322
7323    make_secsel $id
7324
7325    if {$isnew} {
7326        addtohistory [list selbyid $id 0] savecmitpos
7327    }
7328
7329    $sha1entry delete 0 end
7330    $sha1entry insert 0 $id
7331    if {$autoselect} {
7332        $sha1entry selection range 0 $autosellen
7333    }
7334    rhighlight_sel $id
7335
7336    $ctext conf -state normal
7337    clear_ctext
7338    set linknum 0
7339    if {![info exists commitinfo($id)]} {
7340        getcommit $id
7341    }
7342    set info $commitinfo($id)
7343    set date [formatdate [lindex $info 2]]
7344    $ctext insert end "[mc "Author"]: [lindex $info 1]  $date\n"
7345    set date [formatdate [lindex $info 4]]
7346    $ctext insert end "[mc "Committer"]: [lindex $info 3]  $date\n"
7347    if {[info exists idtags($id)]} {
7348        $ctext insert end [mc "Tags:"]
7349        foreach tag $idtags($id) {
7350            $ctext insert end " $tag"
7351        }
7352        $ctext insert end "\n"
7353    }
7354
7355    set headers {}
7356    set olds $parents($curview,$id)
7357    if {[llength $olds] > 1} {
7358        set np 0
7359        foreach p $olds {
7360            if {$np >= $mergemax} {
7361                set tag mmax
7362            } else {
7363                set tag m$np
7364            }
7365            $ctext insert end "[mc "Parent"]: " $tag
7366            appendwithlinks [commit_descriptor $p] {}
7367            incr np
7368        }
7369    } else {
7370        foreach p $olds {
7371            append headers "[mc "Parent"]: [commit_descriptor $p]"
7372        }
7373    }
7374
7375    foreach c $children($curview,$id) {
7376        append headers "[mc "Child"]:  [commit_descriptor $c]"
7377    }
7378
7379    # make anything that looks like a SHA1 ID be a clickable link
7380    appendwithlinks $headers {}
7381    if {$showneartags} {
7382        if {![info exists allcommits]} {
7383            getallcommits
7384        }
7385        $ctext insert end "[mc "Branch"]: "
7386        $ctext mark set branch "end -1c"
7387        $ctext mark gravity branch left
7388        $ctext insert end "\n[mc "Follows"]: "
7389        $ctext mark set follows "end -1c"
7390        $ctext mark gravity follows left
7391        $ctext insert end "\n[mc "Precedes"]: "
7392        $ctext mark set precedes "end -1c"
7393        $ctext mark gravity precedes left
7394        $ctext insert end "\n"
7395        dispneartags 1
7396    }
7397    $ctext insert end "\n"
7398    set comment [lindex $info 5]
7399    if {[string first "\r" $comment] >= 0} {
7400        set comment [string map {"\r" "\n    "} $comment]
7401    }
7402    appendwithlinks $comment {comment}
7403
7404    $ctext tag remove found 1.0 end
7405    $ctext conf -state disabled
7406    set commentend [$ctext index "end - 1c"]
7407
7408    set jump_to_here $desired_loc
7409    init_flist [mc "Comments"]
7410    if {$cmitmode eq "tree"} {
7411        gettree $id
7412    } elseif {$vinlinediff($curview) == 1} {
7413        showinlinediff $id
7414    } elseif {[llength $olds] <= 1} {
7415        startdiff $id
7416    } else {
7417        mergediff $id
7418    }
7419}
7420
7421proc selfirstline {} {
7422    unmarkmatches
7423    selectline 0 1
7424}
7425
7426proc sellastline {} {
7427    global numcommits
7428    unmarkmatches
7429    set l [expr {$numcommits - 1}]
7430    selectline $l 1
7431}
7432
7433proc selnextline {dir} {
7434    global selectedline
7435    focus .
7436    if {$selectedline eq {}} return
7437    set l [expr {$selectedline + $dir}]
7438    unmarkmatches
7439    selectline $l 1
7440}
7441
7442proc selnextpage {dir} {
7443    global canv linespc selectedline numcommits
7444
7445    set lpp [expr {([winfo height $canv] - 2) / $linespc}]
7446    if {$lpp < 1} {
7447        set lpp 1
7448    }
7449    allcanvs yview scroll [expr {$dir * $lpp}] units
7450    drawvisible
7451    if {$selectedline eq {}} return
7452    set l [expr {$selectedline + $dir * $lpp}]
7453    if {$l < 0} {
7454        set l 0
7455    } elseif {$l >= $numcommits} {
7456        set l [expr $numcommits - 1]
7457    }
7458    unmarkmatches
7459    selectline $l 1
7460}
7461
7462proc unselectline {} {
7463    global selectedline currentid
7464
7465    set selectedline {}
7466    unset -nocomplain currentid
7467    allcanvs delete secsel
7468    rhighlight_none
7469}
7470
7471proc reselectline {} {
7472    global selectedline
7473
7474    if {$selectedline ne {}} {
7475        selectline $selectedline 0
7476    }
7477}
7478
7479proc addtohistory {cmd {saveproc {}}} {
7480    global history historyindex curview
7481
7482    unset_posvars
7483    save_position
7484    set elt [list $curview $cmd $saveproc {}]
7485    if {$historyindex > 0
7486        && [lindex $history [expr {$historyindex - 1}]] == $elt} {
7487        return
7488    }
7489
7490    if {$historyindex < [llength $history]} {
7491        set history [lreplace $history $historyindex end $elt]
7492    } else {
7493        lappend history $elt
7494    }
7495    incr historyindex
7496    if {$historyindex > 1} {
7497        .tf.bar.leftbut conf -state normal
7498    } else {
7499        .tf.bar.leftbut conf -state disabled
7500    }
7501    .tf.bar.rightbut conf -state disabled
7502}
7503
7504# save the scrolling position of the diff display pane
7505proc save_position {} {
7506    global historyindex history
7507
7508    if {$historyindex < 1} return
7509    set hi [expr {$historyindex - 1}]
7510    set fn [lindex $history $hi 2]
7511    if {$fn ne {}} {
7512        lset history $hi 3 [eval $fn]
7513    }
7514}
7515
7516proc unset_posvars {} {
7517    global last_posvars
7518
7519    if {[info exists last_posvars]} {
7520        foreach {var val} $last_posvars {
7521            global $var
7522            unset -nocomplain $var
7523        }
7524        unset last_posvars
7525    }
7526}
7527
7528proc godo {elt} {
7529    global curview last_posvars
7530
7531    set view [lindex $elt 0]
7532    set cmd [lindex $elt 1]
7533    set pv [lindex $elt 3]
7534    if {$curview != $view} {
7535        showview $view
7536    }
7537    unset_posvars
7538    foreach {var val} $pv {
7539        global $var
7540        set $var $val
7541    }
7542    set last_posvars $pv
7543    eval $cmd
7544}
7545
7546proc goback {} {
7547    global history historyindex
7548    focus .
7549
7550    if {$historyindex > 1} {
7551        save_position
7552        incr historyindex -1
7553        godo [lindex $history [expr {$historyindex - 1}]]
7554        .tf.bar.rightbut conf -state normal
7555    }
7556    if {$historyindex <= 1} {
7557        .tf.bar.leftbut conf -state disabled
7558    }
7559}
7560
7561proc goforw {} {
7562    global history historyindex
7563    focus .
7564
7565    if {$historyindex < [llength $history]} {
7566        save_position
7567        set cmd [lindex $history $historyindex]
7568        incr historyindex
7569        godo $cmd
7570        .tf.bar.leftbut conf -state normal
7571    }
7572    if {$historyindex >= [llength $history]} {
7573        .tf.bar.rightbut conf -state disabled
7574    }
7575}
7576
7577proc go_to_parent {i} {
7578    global parents curview targetid
7579    set ps $parents($curview,$targetid)
7580    if {[llength $ps] >= $i} {
7581        selbyid [lindex $ps [expr $i - 1]]
7582    }
7583}
7584
7585proc gettree {id} {
7586    global treefilelist treeidlist diffids diffmergeid treepending
7587    global nullid nullid2
7588
7589    set diffids $id
7590    unset -nocomplain diffmergeid
7591    if {![info exists treefilelist($id)]} {
7592        if {![info exists treepending]} {
7593            if {$id eq $nullid} {
7594                set cmd [list | git ls-files]
7595            } elseif {$id eq $nullid2} {
7596                set cmd [list | git ls-files --stage -t]
7597            } else {
7598                set cmd [list | git ls-tree -r $id]
7599            }
7600            if {[catch {set gtf [open $cmd r]}]} {
7601                return
7602            }
7603            set treepending $id
7604            set treefilelist($id) {}
7605            set treeidlist($id) {}
7606            fconfigure $gtf -blocking 0 -encoding binary
7607            filerun $gtf [list gettreeline $gtf $id]
7608        }
7609    } else {
7610        setfilelist $id
7611    }
7612}
7613
7614proc gettreeline {gtf id} {
7615    global treefilelist treeidlist treepending cmitmode diffids nullid nullid2
7616
7617    set nl 0
7618    while {[incr nl] <= 1000 && [gets $gtf line] >= 0} {
7619        if {$diffids eq $nullid} {
7620            set fname $line
7621        } else {
7622            set i [string first "\t" $line]
7623            if {$i < 0} continue
7624            set fname [string range $line [expr {$i+1}] end]
7625            set line [string range $line 0 [expr {$i-1}]]
7626            if {$diffids ne $nullid2 && [lindex $line 1] ne "blob"} continue
7627            set sha1 [lindex $line 2]
7628            lappend treeidlist($id) $sha1
7629        }
7630        if {[string index $fname 0] eq "\""} {
7631            set fname [lindex $fname 0]
7632        }
7633        set fname [encoding convertfrom $fname]
7634        lappend treefilelist($id) $fname
7635    }
7636    if {![eof $gtf]} {
7637        return [expr {$nl >= 1000? 2: 1}]
7638    }
7639    close $gtf
7640    unset treepending
7641    if {$cmitmode ne "tree"} {
7642        if {![info exists diffmergeid]} {
7643            gettreediffs $diffids
7644        }
7645    } elseif {$id ne $diffids} {
7646        gettree $diffids
7647    } else {
7648        setfilelist $id
7649    }
7650    return 0
7651}
7652
7653proc showfile {f} {
7654    global treefilelist treeidlist diffids nullid nullid2
7655    global ctext_file_names ctext_file_lines
7656    global ctext commentend
7657
7658    set i [lsearch -exact $treefilelist($diffids) $f]
7659    if {$i < 0} {
7660        puts "oops, $f not in list for id $diffids"
7661        return
7662    }
7663    if {$diffids eq $nullid} {
7664        if {[catch {set bf [open $f r]} err]} {
7665            puts "oops, can't read $f: $err"
7666            return
7667        }
7668    } else {
7669        set blob [lindex $treeidlist($diffids) $i]
7670        if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} {
7671            puts "oops, error reading blob $blob: $err"
7672            return
7673        }
7674    }
7675    fconfigure $bf -blocking 0 -encoding [get_path_encoding $f]
7676    filerun $bf [list getblobline $bf $diffids]
7677    $ctext config -state normal
7678    clear_ctext $commentend
7679    lappend ctext_file_names $f
7680    lappend ctext_file_lines [lindex [split $commentend "."] 0]
7681    $ctext insert end "\n"
7682    $ctext insert end "$f\n" filesep
7683    $ctext config -state disabled
7684    $ctext yview $commentend
7685    settabs 0
7686}
7687
7688proc getblobline {bf id} {
7689    global diffids cmitmode ctext
7690
7691    if {$id ne $diffids || $cmitmode ne "tree"} {
7692        catch {close $bf}
7693        return 0
7694    }
7695    $ctext config -state normal
7696    set nl 0
7697    while {[incr nl] <= 1000 && [gets $bf line] >= 0} {
7698        $ctext insert end "$line\n"
7699    }
7700    if {[eof $bf]} {
7701        global jump_to_here ctext_file_names commentend
7702
7703        # delete last newline
7704        $ctext delete "end - 2c" "end - 1c"
7705        close $bf
7706        if {$jump_to_here ne {} &&
7707            [lindex $jump_to_here 0] eq [lindex $ctext_file_names 0]} {
7708            set lnum [expr {[lindex $jump_to_here 1] +
7709                            [lindex [split $commentend .] 0]}]
7710            mark_ctext_line $lnum
7711        }
7712        $ctext config -state disabled
7713        return 0
7714    }
7715    $ctext config -state disabled
7716    return [expr {$nl >= 1000? 2: 1}]
7717}
7718
7719proc mark_ctext_line {lnum} {
7720    global ctext markbgcolor
7721
7722    $ctext tag delete omark
7723    $ctext tag add omark $lnum.0 "$lnum.0 + 1 line"
7724    $ctext tag conf omark -background $markbgcolor
7725    $ctext see $lnum.0
7726}
7727
7728proc mergediff {id} {
7729    global diffmergeid
7730    global diffids treediffs
7731    global parents curview
7732
7733    set diffmergeid $id
7734    set diffids $id
7735    set treediffs($id) {}
7736    set np [llength $parents($curview,$id)]
7737    settabs $np
7738    getblobdiffs $id
7739}
7740
7741proc startdiff {ids} {
7742    global treediffs diffids treepending diffmergeid nullid nullid2
7743
7744    settabs 1
7745    set diffids $ids
7746    unset -nocomplain diffmergeid
7747    if {![info exists treediffs($ids)] ||
7748        [lsearch -exact $ids $nullid] >= 0 ||
7749        [lsearch -exact $ids $nullid2] >= 0} {
7750        if {![info exists treepending]} {
7751            gettreediffs $ids
7752        }
7753    } else {
7754        addtocflist $ids
7755    }
7756}
7757
7758proc showinlinediff {ids} {
7759    global commitinfo commitdata ctext
7760    global treediffs
7761
7762    set info $commitinfo($ids)
7763    set diff [lindex $info 7]
7764    set difflines [split $diff "\n"]
7765
7766    initblobdiffvars
7767    set treediff {}
7768
7769    set inhdr 0
7770    foreach line $difflines {
7771        if {![string compare -length 5 "diff " $line]} {
7772            set inhdr 1
7773        } elseif {$inhdr && ![string compare -length 4 "+++ " $line]} {
7774            # offset also accounts for the b/ prefix
7775            lappend treediff [string range $line 6 end]
7776            set inhdr 0
7777        }
7778    }
7779
7780    set treediffs($ids) $treediff
7781    add_flist $treediff
7782
7783    $ctext conf -state normal
7784    foreach line $difflines {
7785        parseblobdiffline $ids $line
7786    }
7787    maybe_scroll_ctext 1
7788    $ctext conf -state disabled
7789}
7790
7791# If the filename (name) is under any of the passed filter paths
7792# then return true to include the file in the listing.
7793proc path_filter {filter name} {
7794    set worktree [gitworktree]
7795    foreach p $filter {
7796        set fq_p [file normalize $p]
7797        set fq_n [file normalize [file join $worktree $name]]
7798        if {[string match [file normalize $fq_p]* $fq_n]} {
7799            return 1
7800        }
7801    }
7802    return 0
7803}
7804
7805proc addtocflist {ids} {
7806    global treediffs
7807
7808    add_flist $treediffs($ids)
7809    getblobdiffs $ids
7810}
7811
7812proc diffcmd {ids flags} {
7813    global log_showroot nullid nullid2 git_version
7814
7815    set i [lsearch -exact $ids $nullid]
7816    set j [lsearch -exact $ids $nullid2]
7817    if {$i >= 0} {
7818        if {[llength $ids] > 1 && $j < 0} {
7819            # comparing working directory with some specific revision
7820            set cmd [concat | git diff-index $flags]
7821            if {$i == 0} {
7822                lappend cmd -R [lindex $ids 1]
7823            } else {
7824                lappend cmd [lindex $ids 0]
7825            }
7826        } else {
7827            # comparing working directory with index
7828            set cmd [concat | git diff-files $flags]
7829            if {$j == 1} {
7830                lappend cmd -R
7831            }
7832        }
7833    } elseif {$j >= 0} {
7834        if {[package vcompare $git_version "1.7.2"] >= 0} {
7835            set flags "$flags --ignore-submodules=dirty"
7836        }
7837        set cmd [concat | git diff-index --cached $flags]
7838        if {[llength $ids] > 1} {
7839            # comparing index with specific revision
7840            if {$j == 0} {
7841                lappend cmd -R [lindex $ids 1]
7842            } else {
7843                lappend cmd [lindex $ids 0]
7844            }
7845        } else {
7846            # comparing index with HEAD
7847            lappend cmd HEAD
7848        }
7849    } else {
7850        if {$log_showroot} {
7851            lappend flags --root
7852        }
7853        set cmd [concat | git diff-tree -r $flags $ids]
7854    }
7855    return $cmd
7856}
7857
7858proc gettreediffs {ids} {
7859    global treediff treepending limitdiffs vfilelimit curview
7860
7861    set cmd [diffcmd $ids {--no-commit-id}]
7862    if {$limitdiffs && $vfilelimit($curview) ne {}} {
7863            set cmd [concat $cmd -- $vfilelimit($curview)]
7864    }
7865    if {[catch {set gdtf [open $cmd r]}]} return
7866
7867    set treepending $ids
7868    set treediff {}
7869    fconfigure $gdtf -blocking 0 -encoding binary
7870    filerun $gdtf [list gettreediffline $gdtf $ids]
7871}
7872
7873proc gettreediffline {gdtf ids} {
7874    global treediff treediffs treepending diffids diffmergeid
7875    global cmitmode vfilelimit curview limitdiffs perfile_attrs
7876
7877    set nr 0
7878    set sublist {}
7879    set max 1000
7880    if {$perfile_attrs} {
7881        # cache_gitattr is slow, and even slower on win32 where we
7882        # have to invoke it for only about 30 paths at a time
7883        set max 500
7884        if {[tk windowingsystem] == "win32"} {
7885            set max 120
7886        }
7887    }
7888    while {[incr nr] <= $max && [gets $gdtf line] >= 0} {
7889        set i [string first "\t" $line]
7890        if {$i >= 0} {
7891            set file [string range $line [expr {$i+1}] end]
7892            if {[string index $file 0] eq "\""} {
7893                set file [lindex $file 0]
7894            }
7895            set file [encoding convertfrom $file]
7896            if {$file ne [lindex $treediff end]} {
7897                lappend treediff $file
7898                lappend sublist $file
7899            }
7900        }
7901    }
7902    if {$perfile_attrs} {
7903        cache_gitattr encoding $sublist
7904    }
7905    if {![eof $gdtf]} {
7906        return [expr {$nr >= $max? 2: 1}]
7907    }
7908    close $gdtf
7909    set treediffs($ids) $treediff
7910    unset treepending
7911    if {$cmitmode eq "tree" && [llength $diffids] == 1} {
7912        gettree $diffids
7913    } elseif {$ids != $diffids} {
7914        if {![info exists diffmergeid]} {
7915            gettreediffs $diffids
7916        }
7917    } else {
7918        addtocflist $ids
7919    }
7920    return 0
7921}
7922
7923# empty string or positive integer
7924proc diffcontextvalidate {v} {
7925    return [regexp {^(|[1-9][0-9]*)$} $v]
7926}
7927
7928proc diffcontextchange {n1 n2 op} {
7929    global diffcontextstring diffcontext
7930
7931    if {[string is integer -strict $diffcontextstring]} {
7932        if {$diffcontextstring >= 0} {
7933            set diffcontext $diffcontextstring
7934            reselectline
7935        }
7936    }
7937}
7938
7939proc changeignorespace {} {
7940    reselectline
7941}
7942
7943proc changeworddiff {name ix op} {
7944    reselectline
7945}
7946
7947proc initblobdiffvars {} {
7948    global diffencoding targetline diffnparents
7949    global diffinhdr currdiffsubmod diffseehere
7950    set targetline {}
7951    set diffnparents 0
7952    set diffinhdr 0
7953    set diffencoding [get_path_encoding {}]
7954    set currdiffsubmod ""
7955    set diffseehere -1
7956}
7957
7958proc getblobdiffs {ids} {
7959    global blobdifffd diffids env
7960    global treediffs
7961    global diffcontext
7962    global ignorespace
7963    global worddiff
7964    global limitdiffs vfilelimit curview
7965    global git_version
7966
7967    set textconv {}
7968    if {[package vcompare $git_version "1.6.1"] >= 0} {
7969        set textconv "--textconv"
7970    }
7971    set submodule {}
7972    if {[package vcompare $git_version "1.6.6"] >= 0} {
7973        set submodule "--submodule"
7974    }
7975    set cmd [diffcmd $ids "-p $textconv $submodule  -C --cc --no-commit-id -U$diffcontext"]
7976    if {$ignorespace} {
7977        append cmd " -w"
7978    }
7979    if {$worddiff ne [mc "Line diff"]} {
7980        append cmd " --word-diff=porcelain"
7981    }
7982    if {$limitdiffs && $vfilelimit($curview) ne {}} {
7983        set cmd [concat $cmd -- $vfilelimit($curview)]
7984    }
7985    if {[catch {set bdf [open $cmd r]} err]} {
7986        error_popup [mc "Error getting diffs: %s" $err]
7987        return
7988    }
7989    fconfigure $bdf -blocking 0 -encoding binary -eofchar {}
7990    set blobdifffd($ids) $bdf
7991    initblobdiffvars
7992    filerun $bdf [list getblobdiffline $bdf $diffids]
7993}
7994
7995proc savecmitpos {} {
7996    global ctext cmitmode
7997
7998    if {$cmitmode eq "tree"} {
7999        return {}
8000    }
8001    return [list target_scrollpos [$ctext index @0,0]]
8002}
8003
8004proc savectextpos {} {
8005    global ctext
8006
8007    return [list target_scrollpos [$ctext index @0,0]]
8008}
8009
8010proc maybe_scroll_ctext {ateof} {
8011    global ctext target_scrollpos
8012
8013    if {![info exists target_scrollpos]} return
8014    if {!$ateof} {
8015        set nlines [expr {[winfo height $ctext]
8016                          / [font metrics textfont -linespace]}]
8017        if {[$ctext compare "$target_scrollpos + $nlines lines" <= end]} return
8018    }
8019    $ctext yview $target_scrollpos
8020    unset target_scrollpos
8021}
8022
8023proc setinlist {var i val} {
8024    global $var
8025
8026    while {[llength [set $var]] < $i} {
8027        lappend $var {}
8028    }
8029    if {[llength [set $var]] == $i} {
8030        lappend $var $val
8031    } else {
8032        lset $var $i $val
8033    }
8034}
8035
8036proc makediffhdr {fname ids} {
8037    global ctext curdiffstart treediffs diffencoding
8038    global ctext_file_names jump_to_here targetline diffline
8039
8040    set fname [encoding convertfrom $fname]
8041    set diffencoding [get_path_encoding $fname]
8042    set i [lsearch -exact $treediffs($ids) $fname]
8043    if {$i >= 0} {
8044        setinlist difffilestart $i $curdiffstart
8045    }
8046    lset ctext_file_names end $fname
8047    set l [expr {(78 - [string length $fname]) / 2}]
8048    set pad [string range "----------------------------------------" 1 $l]
8049    $ctext insert $curdiffstart "$pad $fname $pad" filesep
8050    set targetline {}
8051    if {$jump_to_here ne {} && [lindex $jump_to_here 0] eq $fname} {
8052        set targetline [lindex $jump_to_here 1]
8053    }
8054    set diffline 0
8055}
8056
8057proc blobdiffmaybeseehere {ateof} {
8058    global diffseehere
8059    if {$diffseehere >= 0} {
8060        mark_ctext_line [lindex [split $diffseehere .] 0]
8061    }
8062    maybe_scroll_ctext $ateof
8063}
8064
8065proc getblobdiffline {bdf ids} {
8066    global diffids blobdifffd
8067    global ctext
8068
8069    set nr 0
8070    $ctext conf -state normal
8071    while {[incr nr] <= 1000 && [gets $bdf line] >= 0} {
8072        if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
8073            catch {close $bdf}
8074            return 0
8075        }
8076        parseblobdiffline $ids $line
8077    }
8078    $ctext conf -state disabled
8079    blobdiffmaybeseehere [eof $bdf]
8080    if {[eof $bdf]} {
8081        catch {close $bdf}
8082        return 0
8083    }
8084    return [expr {$nr >= 1000? 2: 1}]
8085}
8086
8087proc parseblobdiffline {ids line} {
8088    global ctext curdiffstart
8089    global diffnexthead diffnextnote difffilestart
8090    global ctext_file_names ctext_file_lines
8091    global diffinhdr treediffs mergemax diffnparents
8092    global diffencoding jump_to_here targetline diffline currdiffsubmod
8093    global worddiff diffseehere
8094
8095    if {![string compare -length 5 "diff " $line]} {
8096        if {![regexp {^diff (--cc|--git) } $line m type]} {
8097            set line [encoding convertfrom $line]
8098            $ctext insert end "$line\n" hunksep
8099            continue
8100        }
8101        # start of a new file
8102        set diffinhdr 1
8103        $ctext insert end "\n"
8104        set curdiffstart [$ctext index "end - 1c"]
8105        lappend ctext_file_names ""
8106        lappend ctext_file_lines [lindex [split $curdiffstart "."] 0]
8107        $ctext insert end "\n" filesep
8108
8109        if {$type eq "--cc"} {
8110            # start of a new file in a merge diff
8111            set fname [string range $line 10 end]
8112            if {[lsearch -exact $treediffs($ids) $fname] < 0} {
8113                lappend treediffs($ids) $fname
8114                add_flist [list $fname]
8115            }
8116
8117        } else {
8118            set line [string range $line 11 end]
8119            # If the name hasn't changed the length will be odd,
8120            # the middle char will be a space, and the two bits either
8121            # side will be a/name and b/name, or "a/name" and "b/name".
8122            # If the name has changed we'll get "rename from" and
8123            # "rename to" or "copy from" and "copy to" lines following
8124            # this, and we'll use them to get the filenames.
8125            # This complexity is necessary because spaces in the
8126            # filename(s) don't get escaped.
8127            set l [string length $line]
8128            set i [expr {$l / 2}]
8129            if {!(($l & 1) && [string index $line $i] eq " " &&
8130                  [string range $line 2 [expr {$i - 1}]] eq \
8131                      [string range $line [expr {$i + 3}] end])} {
8132                return
8133            }
8134            # unescape if quoted and chop off the a/ from the front
8135            if {[string index $line 0] eq "\""} {
8136                set fname [string range [lindex $line 0] 2 end]
8137            } else {
8138                set fname [string range $line 2 [expr {$i - 1}]]
8139            }
8140        }
8141        makediffhdr $fname $ids
8142
8143    } elseif {![string compare -length 16 "* Unmerged path " $line]} {
8144        set fname [encoding convertfrom [string range $line 16 end]]
8145        $ctext insert end "\n"
8146        set curdiffstart [$ctext index "end - 1c"]
8147        lappend ctext_file_names $fname
8148        lappend ctext_file_lines [lindex [split $curdiffstart "."] 0]
8149        $ctext insert end "$line\n" filesep
8150        set i [lsearch -exact $treediffs($ids) $fname]
8151        if {$i >= 0} {
8152            setinlist difffilestart $i $curdiffstart
8153        }
8154
8155    } elseif {![string compare -length 2 "@@" $line]} {
8156        regexp {^@@+} $line ats
8157        set line [encoding convertfrom $diffencoding $line]
8158        $ctext insert end "$line\n" hunksep
8159        if {[regexp { \+(\d+),\d+ @@} $line m nl]} {
8160            set diffline $nl
8161        }
8162        set diffnparents [expr {[string length $ats] - 1}]
8163        set diffinhdr 0
8164
8165    } elseif {![string compare -length 10 "Submodule " $line]} {
8166        # start of a new submodule
8167        if {[regexp -indices "\[0-9a-f\]+\\.\\." $line nameend]} {
8168            set fname [string range $line 10 [expr [lindex $nameend 0] - 2]]
8169        } else {
8170            set fname [string range $line 10 [expr [string first "contains " $line] - 2]]
8171        }
8172        if {$currdiffsubmod != $fname} {
8173            $ctext insert end "\n";     # Add newline after commit message
8174        }
8175        set curdiffstart [$ctext index "end - 1c"]
8176        lappend ctext_file_names ""
8177        if {$currdiffsubmod != $fname} {
8178            lappend ctext_file_lines $fname
8179            makediffhdr $fname $ids
8180            set currdiffsubmod $fname
8181            $ctext insert end "\n$line\n" filesep
8182        } else {
8183            $ctext insert end "$line\n" filesep
8184        }
8185    } elseif {![string compare -length 3 "  >" $line]} {
8186        set $currdiffsubmod ""
8187        set line [encoding convertfrom $diffencoding $line]
8188        $ctext insert end "$line\n" dresult
8189    } elseif {![string compare -length 3 "  <" $line]} {
8190        set $currdiffsubmod ""
8191        set line [encoding convertfrom $diffencoding $line]
8192        $ctext insert end "$line\n" d0
8193    } elseif {$diffinhdr} {
8194        if {![string compare -length 12 "rename from " $line]} {
8195            set fname [string range $line [expr 6 + [string first " from " $line] ] end]
8196            if {[string index $fname 0] eq "\""} {
8197                set fname [lindex $fname 0]
8198            }
8199            set fname [encoding convertfrom $fname]
8200            set i [lsearch -exact $treediffs($ids) $fname]
8201            if {$i >= 0} {
8202                setinlist difffilestart $i $curdiffstart
8203            }
8204        } elseif {![string compare -length 10 $line "rename to "] ||
8205                  ![string compare -length 8 $line "copy to "]} {
8206            set fname [string range $line [expr 4 + [string first " to " $line] ] end]
8207            if {[string index $fname 0] eq "\""} {
8208                set fname [lindex $fname 0]
8209            }
8210            makediffhdr $fname $ids
8211        } elseif {[string compare -length 3 $line "---"] == 0} {
8212            # do nothing
8213            return
8214        } elseif {[string compare -length 3 $line "+++"] == 0} {
8215            set diffinhdr 0
8216            return
8217        }
8218        $ctext insert end "$line\n" filesep
8219
8220    } else {
8221        set line [string map {\x1A ^Z} \
8222                      [encoding convertfrom $diffencoding $line]]
8223        # parse the prefix - one ' ', '-' or '+' for each parent
8224        set prefix [string range $line 0 [expr {$diffnparents - 1}]]
8225        set tag [expr {$diffnparents > 1? "m": "d"}]
8226        set dowords [expr {$worddiff ne [mc "Line diff"] && $diffnparents == 1}]
8227        set words_pre_markup ""
8228        set words_post_markup ""
8229        if {[string trim $prefix " -+"] eq {}} {
8230            # prefix only has " ", "-" and "+" in it: normal diff line
8231            set num [string first "-" $prefix]
8232            if {$dowords} {
8233                set line [string range $line 1 end]
8234            }
8235            if {$num >= 0} {
8236                # removed line, first parent with line is $num
8237                if {$num >= $mergemax} {
8238                    set num "max"
8239                }
8240                if {$dowords && $worddiff eq [mc "Markup words"]} {
8241                    $ctext insert end "\[-$line-\]" $tag$num
8242                } else {
8243                    $ctext insert end "$line" $tag$num
8244                }
8245                if {!$dowords} {
8246                    $ctext insert end "\n" $tag$num
8247                }
8248            } else {
8249                set tags {}
8250                if {[string first "+" $prefix] >= 0} {
8251                    # added line
8252                    lappend tags ${tag}result
8253                    if {$diffnparents > 1} {
8254                        set num [string first " " $prefix]
8255                        if {$num >= 0} {
8256                            if {$num >= $mergemax} {
8257                                set num "max"
8258                            }
8259                            lappend tags m$num
8260                        }
8261                    }
8262                    set words_pre_markup "{+"
8263                    set words_post_markup "+}"
8264                }
8265                if {$targetline ne {}} {
8266                    if {$diffline == $targetline} {
8267                        set diffseehere [$ctext index "end - 1 chars"]
8268                        set targetline {}
8269                    } else {
8270                        incr diffline
8271                    }
8272                }
8273                if {$dowords && $worddiff eq [mc "Markup words"]} {
8274                    $ctext insert end "$words_pre_markup$line$words_post_markup" $tags
8275                } else {
8276                    $ctext insert end "$line" $tags
8277                }
8278                if {!$dowords} {
8279                    $ctext insert end "\n" $tags
8280                }
8281            }
8282        } elseif {$dowords && $prefix eq "~"} {
8283            $ctext insert end "\n" {}
8284        } else {
8285            # "\ No newline at end of file",
8286            # or something else we don't recognize
8287            $ctext insert end "$line\n" hunksep
8288        }
8289    }
8290}
8291
8292proc changediffdisp {} {
8293    global ctext diffelide
8294
8295    $ctext tag conf d0 -elide [lindex $diffelide 0]
8296    $ctext tag conf dresult -elide [lindex $diffelide 1]
8297}
8298
8299proc highlightfile {cline} {
8300    global cflist cflist_top
8301
8302    if {![info exists cflist_top]} return
8303
8304    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
8305    $cflist tag add highlight $cline.0 "$cline.0 lineend"
8306    $cflist see $cline.0
8307    set cflist_top $cline
8308}
8309
8310proc highlightfile_for_scrollpos {topidx} {
8311    global cmitmode difffilestart
8312
8313    if {$cmitmode eq "tree"} return
8314    if {![info exists difffilestart]} return
8315
8316    set top [lindex [split $topidx .] 0]
8317    if {$difffilestart eq {} || $top < [lindex $difffilestart 0]} {
8318        highlightfile 0
8319    } else {
8320        highlightfile [expr {[bsearch $difffilestart $top] + 2}]
8321    }
8322}
8323
8324proc prevfile {} {
8325    global difffilestart ctext cmitmode
8326
8327    if {$cmitmode eq "tree"} return
8328    set prev 0.0
8329    set here [$ctext index @0,0]
8330    foreach loc $difffilestart {
8331        if {[$ctext compare $loc >= $here]} {
8332            $ctext yview $prev
8333            return
8334        }
8335        set prev $loc
8336    }
8337    $ctext yview $prev
8338}
8339
8340proc nextfile {} {
8341    global difffilestart ctext cmitmode
8342
8343    if {$cmitmode eq "tree"} return
8344    set here [$ctext index @0,0]
8345    foreach loc $difffilestart {
8346        if {[$ctext compare $loc > $here]} {
8347            $ctext yview $loc
8348            return
8349        }
8350    }
8351}
8352
8353proc clear_ctext {{first 1.0}} {
8354    global ctext smarktop smarkbot
8355    global ctext_file_names ctext_file_lines
8356    global pendinglinks
8357
8358    set l [lindex [split $first .] 0]
8359    if {![info exists smarktop] || [$ctext compare $first < $smarktop.0]} {
8360        set smarktop $l
8361    }
8362    if {![info exists smarkbot] || [$ctext compare $first < $smarkbot.0]} {
8363        set smarkbot $l
8364    }
8365    $ctext delete $first end
8366    if {$first eq "1.0"} {
8367        unset -nocomplain pendinglinks
8368    }
8369    set ctext_file_names {}
8370    set ctext_file_lines {}
8371}
8372
8373proc settabs {{firstab {}}} {
8374    global firsttabstop tabstop ctext have_tk85
8375
8376    if {$firstab ne {} && $have_tk85} {
8377        set firsttabstop $firstab
8378    }
8379    set w [font measure textfont "0"]
8380    if {$firsttabstop != 0} {
8381        $ctext conf -tabs [list [expr {($firsttabstop + $tabstop) * $w}] \
8382                               [expr {($firsttabstop + 2 * $tabstop) * $w}]]
8383    } elseif {$have_tk85 || $tabstop != 8} {
8384        $ctext conf -tabs [expr {$tabstop * $w}]
8385    } else {
8386        $ctext conf -tabs {}
8387    }
8388}
8389
8390proc incrsearch {name ix op} {
8391    global ctext searchstring searchdirn
8392
8393    if {[catch {$ctext index anchor}]} {
8394        # no anchor set, use start of selection, or of visible area
8395        set sel [$ctext tag ranges sel]
8396        if {$sel ne {}} {
8397            $ctext mark set anchor [lindex $sel 0]
8398        } elseif {$searchdirn eq "-forwards"} {
8399            $ctext mark set anchor @0,0
8400        } else {
8401            $ctext mark set anchor @0,[winfo height $ctext]
8402        }
8403    }
8404    if {$searchstring ne {}} {
8405        set here [$ctext search -count mlen $searchdirn -- $searchstring anchor]
8406        if {$here ne {}} {
8407            $ctext see $here
8408            set mend "$here + $mlen c"
8409            $ctext tag remove sel 1.0 end
8410            $ctext tag add sel $here $mend
8411            suppress_highlighting_file_for_current_scrollpos
8412            highlightfile_for_scrollpos $here
8413        }
8414    }
8415    rehighlight_search_results
8416}
8417
8418proc dosearch {} {
8419    global sstring ctext searchstring searchdirn
8420
8421    focus $sstring
8422    $sstring icursor end
8423    set searchdirn -forwards
8424    if {$searchstring ne {}} {
8425        set sel [$ctext tag ranges sel]
8426        if {$sel ne {}} {
8427            set start "[lindex $sel 0] + 1c"
8428        } elseif {[catch {set start [$ctext index anchor]}]} {
8429            set start "@0,0"
8430        }
8431        set match [$ctext search -count mlen -- $searchstring $start]
8432        $ctext tag remove sel 1.0 end
8433        if {$match eq {}} {
8434            bell
8435            return
8436        }
8437        $ctext see $match
8438        suppress_highlighting_file_for_current_scrollpos
8439        highlightfile_for_scrollpos $match
8440        set mend "$match + $mlen c"
8441        $ctext tag add sel $match $mend
8442        $ctext mark unset anchor
8443        rehighlight_search_results
8444    }
8445}
8446
8447proc dosearchback {} {
8448    global sstring ctext searchstring searchdirn
8449
8450    focus $sstring
8451    $sstring icursor end
8452    set searchdirn -backwards
8453    if {$searchstring ne {}} {
8454        set sel [$ctext tag ranges sel]
8455        if {$sel ne {}} {
8456            set start [lindex $sel 0]
8457        } elseif {[catch {set start [$ctext index anchor]}]} {
8458            set start @0,[winfo height $ctext]
8459        }
8460        set match [$ctext search -backwards -count ml -- $searchstring $start]
8461        $ctext tag remove sel 1.0 end
8462        if {$match eq {}} {
8463            bell
8464            return
8465        }
8466        $ctext see $match
8467        suppress_highlighting_file_for_current_scrollpos
8468        highlightfile_for_scrollpos $match
8469        set mend "$match + $ml c"
8470        $ctext tag add sel $match $mend
8471        $ctext mark unset anchor
8472        rehighlight_search_results
8473    }
8474}
8475
8476proc rehighlight_search_results {} {
8477    global ctext searchstring
8478
8479    $ctext tag remove found 1.0 end
8480    $ctext tag remove currentsearchhit 1.0 end
8481
8482    if {$searchstring ne {}} {
8483        searchmarkvisible 1
8484    }
8485}
8486
8487proc searchmark {first last} {
8488    global ctext searchstring
8489
8490    set sel [$ctext tag ranges sel]
8491
8492    set mend $first.0
8493    while {1} {
8494        set match [$ctext search -count mlen -- $searchstring $mend $last.end]
8495        if {$match eq {}} break
8496        set mend "$match + $mlen c"
8497        if {$sel ne {} && [$ctext compare $match == [lindex $sel 0]]} {
8498            $ctext tag add currentsearchhit $match $mend
8499        } else {
8500            $ctext tag add found $match $mend
8501        }
8502    }
8503}
8504
8505proc searchmarkvisible {doall} {
8506    global ctext smarktop smarkbot
8507
8508    set topline [lindex [split [$ctext index @0,0] .] 0]
8509    set botline [lindex [split [$ctext index @0,[winfo height $ctext]] .] 0]
8510    if {$doall || $botline < $smarktop || $topline > $smarkbot} {
8511        # no overlap with previous
8512        searchmark $topline $botline
8513        set smarktop $topline
8514        set smarkbot $botline
8515    } else {
8516        if {$topline < $smarktop} {
8517            searchmark $topline [expr {$smarktop-1}]
8518            set smarktop $topline
8519        }
8520        if {$botline > $smarkbot} {
8521            searchmark [expr {$smarkbot+1}] $botline
8522            set smarkbot $botline
8523        }
8524    }
8525}
8526
8527proc suppress_highlighting_file_for_current_scrollpos {} {
8528    global ctext suppress_highlighting_file_for_this_scrollpos
8529
8530    set suppress_highlighting_file_for_this_scrollpos [$ctext index @0,0]
8531}
8532
8533proc scrolltext {f0 f1} {
8534    global searchstring cmitmode ctext
8535    global suppress_highlighting_file_for_this_scrollpos
8536
8537    set topidx [$ctext index @0,0]
8538    if {![info exists suppress_highlighting_file_for_this_scrollpos]
8539        || $topidx ne $suppress_highlighting_file_for_this_scrollpos} {
8540        highlightfile_for_scrollpos $topidx
8541    }
8542
8543    unset -nocomplain suppress_highlighting_file_for_this_scrollpos
8544
8545    .bleft.bottom.sb set $f0 $f1
8546    if {$searchstring ne {}} {
8547        searchmarkvisible 0
8548    }
8549}
8550
8551proc setcoords {} {
8552    global linespc charspc canvx0 canvy0
8553    global xspc1 xspc2 lthickness
8554
8555    set linespc [font metrics mainfont -linespace]
8556    set charspc [font measure mainfont "m"]
8557    set canvy0 [expr {int(3 + 0.5 * $linespc)}]
8558    set canvx0 [expr {int(3 + 0.5 * $linespc)}]
8559    set lthickness [expr {int($linespc / 9) + 1}]
8560    set xspc1(0) $linespc
8561    set xspc2 $linespc
8562}
8563
8564proc redisplay {} {
8565    global canv
8566    global selectedline
8567
8568    set ymax [lindex [$canv cget -scrollregion] 3]
8569    if {$ymax eq {} || $ymax == 0} return
8570    set span [$canv yview]
8571    clear_display
8572    setcanvscroll
8573    allcanvs yview moveto [lindex $span 0]
8574    drawvisible
8575    if {$selectedline ne {}} {
8576        selectline $selectedline 0
8577        allcanvs yview moveto [lindex $span 0]
8578    }
8579}
8580
8581proc parsefont {f n} {
8582    global fontattr
8583
8584    set fontattr($f,family) [lindex $n 0]
8585    set s [lindex $n 1]
8586    if {$s eq {} || $s == 0} {
8587        set s 10
8588    } elseif {$s < 0} {
8589        set s [expr {int(-$s / [winfo fpixels . 1p] + 0.5)}]
8590    }
8591    set fontattr($f,size) $s
8592    set fontattr($f,weight) normal
8593    set fontattr($f,slant) roman
8594    foreach style [lrange $n 2 end] {
8595        switch -- $style {
8596            "normal" -
8597            "bold"   {set fontattr($f,weight) $style}
8598            "roman" -
8599            "italic" {set fontattr($f,slant) $style}
8600        }
8601    }
8602}
8603
8604proc fontflags {f {isbold 0}} {
8605    global fontattr
8606
8607    return [list -family $fontattr($f,family) -size $fontattr($f,size) \
8608                -weight [expr {$isbold? "bold": $fontattr($f,weight)}] \
8609                -slant $fontattr($f,slant)]
8610}
8611
8612proc fontname {f} {
8613    global fontattr
8614
8615    set n [list $fontattr($f,family) $fontattr($f,size)]
8616    if {$fontattr($f,weight) eq "bold"} {
8617        lappend n "bold"
8618    }
8619    if {$fontattr($f,slant) eq "italic"} {
8620        lappend n "italic"
8621    }
8622    return $n
8623}
8624
8625proc incrfont {inc} {
8626    global mainfont textfont ctext canv cflist showrefstop
8627    global stopped entries fontattr
8628
8629    unmarkmatches
8630    set s $fontattr(mainfont,size)
8631    incr s $inc
8632    if {$s < 1} {
8633        set s 1
8634    }
8635    set fontattr(mainfont,size) $s
8636    font config mainfont -size $s
8637    font config mainfontbold -size $s
8638    set mainfont [fontname mainfont]
8639    set s $fontattr(textfont,size)
8640    incr s $inc
8641    if {$s < 1} {
8642        set s 1
8643    }
8644    set fontattr(textfont,size) $s
8645    font config textfont -size $s
8646    font config textfontbold -size $s
8647    set textfont [fontname textfont]
8648    setcoords
8649    settabs
8650    redisplay
8651}
8652
8653proc clearsha1 {} {
8654    global sha1entry sha1string
8655    if {[string length $sha1string] == 40} {
8656        $sha1entry delete 0 end
8657    }
8658}
8659
8660proc sha1change {n1 n2 op} {
8661    global sha1string currentid sha1but
8662    if {$sha1string == {}
8663        || ([info exists currentid] && $sha1string == $currentid)} {
8664        set state disabled
8665    } else {
8666        set state normal
8667    }
8668    if {[$sha1but cget -state] == $state} return
8669    if {$state == "normal"} {
8670        $sha1but conf -state normal -relief raised -text "[mc "Goto:"] "
8671    } else {
8672        $sha1but conf -state disabled -relief flat -text "[mc "SHA1 ID:"] "
8673    }
8674}
8675
8676proc gotocommit {} {
8677    global sha1string tagids headids curview varcid
8678
8679    if {$sha1string == {}
8680        || ([info exists currentid] && $sha1string == $currentid)} return
8681    if {[info exists tagids($sha1string)]} {
8682        set id $tagids($sha1string)
8683    } elseif {[info exists headids($sha1string)]} {
8684        set id $headids($sha1string)
8685    } else {
8686        set id [string tolower $sha1string]
8687        if {[regexp {^[0-9a-f]{4,39}$} $id]} {
8688            set matches [longid $id]
8689            if {$matches ne {}} {
8690                if {[llength $matches] > 1} {
8691                    error_popup [mc "Short SHA1 id %s is ambiguous" $id]
8692                    return
8693                }
8694                set id [lindex $matches 0]
8695            }
8696        } else {
8697            if {[catch {set id [exec git rev-parse --verify $sha1string]}]} {
8698                error_popup [mc "Revision %s is not known" $sha1string]
8699                return
8700            }
8701        }
8702    }
8703    if {[commitinview $id $curview]} {
8704        selectline [rowofcommit $id] 1
8705        return
8706    }
8707    if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
8708        set msg [mc "SHA1 id %s is not known" $sha1string]
8709    } else {
8710        set msg [mc "Revision %s is not in the current view" $sha1string]
8711    }
8712    error_popup $msg
8713}
8714
8715proc lineenter {x y id} {
8716    global hoverx hovery hoverid hovertimer
8717    global commitinfo canv
8718
8719    if {![info exists commitinfo($id)] && ![getcommit $id]} return
8720    set hoverx $x
8721    set hovery $y
8722    set hoverid $id
8723    if {[info exists hovertimer]} {
8724        after cancel $hovertimer
8725    }
8726    set hovertimer [after 500 linehover]
8727    $canv delete hover
8728}
8729
8730proc linemotion {x y id} {
8731    global hoverx hovery hoverid hovertimer
8732
8733    if {[info exists hoverid] && $id == $hoverid} {
8734        set hoverx $x
8735        set hovery $y
8736        if {[info exists hovertimer]} {
8737            after cancel $hovertimer
8738        }
8739        set hovertimer [after 500 linehover]
8740    }
8741}
8742
8743proc lineleave {id} {
8744    global hoverid hovertimer canv
8745
8746    if {[info exists hoverid] && $id == $hoverid} {
8747        $canv delete hover
8748        if {[info exists hovertimer]} {
8749            after cancel $hovertimer
8750            unset hovertimer
8751        }
8752        unset hoverid
8753    }
8754}
8755
8756proc linehover {} {
8757    global hoverx hovery hoverid hovertimer
8758    global canv linespc lthickness
8759    global linehoverbgcolor linehoverfgcolor linehoveroutlinecolor
8760
8761    global commitinfo
8762
8763    set text [lindex $commitinfo($hoverid) 0]
8764    set ymax [lindex [$canv cget -scrollregion] 3]
8765    if {$ymax == {}} return
8766    set yfrac [lindex [$canv yview] 0]
8767    set x [expr {$hoverx + 2 * $linespc}]
8768    set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
8769    set x0 [expr {$x - 2 * $lthickness}]
8770    set y0 [expr {$y - 2 * $lthickness}]
8771    set x1 [expr {$x + [font measure mainfont $text] + 2 * $lthickness}]
8772    set y1 [expr {$y + $linespc + 2 * $lthickness}]
8773    set t [$canv create rectangle $x0 $y0 $x1 $y1 \
8774               -fill $linehoverbgcolor -outline $linehoveroutlinecolor \
8775               -width 1 -tags hover]
8776    $canv raise $t
8777    set t [$canv create text $x $y -anchor nw -text $text -tags hover \
8778               -font mainfont -fill $linehoverfgcolor]
8779    $canv raise $t
8780}
8781
8782proc clickisonarrow {id y} {
8783    global lthickness
8784
8785    set ranges [rowranges $id]
8786    set thresh [expr {2 * $lthickness + 6}]
8787    set n [expr {[llength $ranges] - 1}]
8788    for {set i 1} {$i < $n} {incr i} {
8789        set row [lindex $ranges $i]
8790        if {abs([yc $row] - $y) < $thresh} {
8791            return $i
8792        }
8793    }
8794    return {}
8795}
8796
8797proc arrowjump {id n y} {
8798    global canv
8799
8800    # 1 <-> 2, 3 <-> 4, etc...
8801    set n [expr {(($n - 1) ^ 1) + 1}]
8802    set row [lindex [rowranges $id] $n]
8803    set yt [yc $row]
8804    set ymax [lindex [$canv cget -scrollregion] 3]
8805    if {$ymax eq {} || $ymax <= 0} return
8806    set view [$canv yview]
8807    set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
8808    set yfrac [expr {$yt / $ymax - $yspan / 2}]
8809    if {$yfrac < 0} {
8810        set yfrac 0
8811    }
8812    allcanvs yview moveto $yfrac
8813}
8814
8815proc lineclick {x y id isnew} {
8816    global ctext commitinfo children canv thickerline curview
8817
8818    if {![info exists commitinfo($id)] && ![getcommit $id]} return
8819    unmarkmatches
8820    unselectline
8821    normalline
8822    $canv delete hover
8823    # draw this line thicker than normal
8824    set thickerline $id
8825    drawlines $id
8826    if {$isnew} {
8827        set ymax [lindex [$canv cget -scrollregion] 3]
8828        if {$ymax eq {}} return
8829        set yfrac [lindex [$canv yview] 0]
8830        set y [expr {$y + $yfrac * $ymax}]
8831    }
8832    set dirn [clickisonarrow $id $y]
8833    if {$dirn ne {}} {
8834        arrowjump $id $dirn $y
8835        return
8836    }
8837
8838    if {$isnew} {
8839        addtohistory [list lineclick $x $y $id 0] savectextpos
8840    }
8841    # fill the details pane with info about this line
8842    $ctext conf -state normal
8843    clear_ctext
8844    settabs 0
8845    $ctext insert end "[mc "Parent"]:\t"
8846    $ctext insert end $id link0
8847    setlink $id link0
8848    set info $commitinfo($id)
8849    $ctext insert end "\n\t[lindex $info 0]\n"
8850    $ctext insert end "\t[mc "Author"]:\t[lindex $info 1]\n"
8851    set date [formatdate [lindex $info 2]]
8852    $ctext insert end "\t[mc "Date"]:\t$date\n"
8853    set kids $children($curview,$id)
8854    if {$kids ne {}} {
8855        $ctext insert end "\n[mc "Children"]:"
8856        set i 0
8857        foreach child $kids {
8858            incr i
8859            if {![info exists commitinfo($child)] && ![getcommit $child]} continue
8860            set info $commitinfo($child)
8861            $ctext insert end "\n\t"
8862            $ctext insert end $child link$i
8863            setlink $child link$i
8864            $ctext insert end "\n\t[lindex $info 0]"
8865            $ctext insert end "\n\t[mc "Author"]:\t[lindex $info 1]"
8866            set date [formatdate [lindex $info 2]]
8867            $ctext insert end "\n\t[mc "Date"]:\t$date\n"
8868        }
8869    }
8870    maybe_scroll_ctext 1
8871    $ctext conf -state disabled
8872    init_flist {}
8873}
8874
8875proc normalline {} {
8876    global thickerline
8877    if {[info exists thickerline]} {
8878        set id $thickerline
8879        unset thickerline
8880        drawlines $id
8881    }
8882}
8883
8884proc selbyid {id {isnew 1}} {
8885    global curview
8886    if {[commitinview $id $curview]} {
8887        selectline [rowofcommit $id] $isnew
8888    }
8889}
8890
8891proc mstime {} {
8892    global startmstime
8893    if {![info exists startmstime]} {
8894        set startmstime [clock clicks -milliseconds]
8895    }
8896    return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
8897}
8898
8899proc rowmenu {x y id} {
8900    global rowctxmenu selectedline rowmenuid curview
8901    global nullid nullid2 fakerowmenu mainhead markedid
8902
8903    stopfinding
8904    set rowmenuid $id
8905    if {$selectedline eq {} || [rowofcommit $id] eq $selectedline} {
8906        set state disabled
8907    } else {
8908        set state normal
8909    }
8910    if {[info exists markedid] && $markedid ne $id} {
8911        set mstate normal
8912    } else {
8913        set mstate disabled
8914    }
8915    if {$id ne $nullid && $id ne $nullid2} {
8916        set menu $rowctxmenu
8917        if {$mainhead ne {}} {
8918            $menu entryconfigure 8 -label [mc "Reset %s branch to here" $mainhead] -state normal
8919        } else {
8920            $menu entryconfigure 8 -label [mc "Detached head: can't reset" $mainhead] -state disabled
8921        }
8922        $menu entryconfigure 10 -state $mstate
8923        $menu entryconfigure 11 -state $mstate
8924        $menu entryconfigure 12 -state $mstate
8925    } else {
8926        set menu $fakerowmenu
8927    }
8928    $menu entryconfigure [mca "Diff this -> selected"] -state $state
8929    $menu entryconfigure [mca "Diff selected -> this"] -state $state
8930    $menu entryconfigure [mca "Make patch"] -state $state
8931    $menu entryconfigure [mca "Diff this -> marked commit"] -state $mstate
8932    $menu entryconfigure [mca "Diff marked commit -> this"] -state $mstate
8933    tk_popup $menu $x $y
8934}
8935
8936proc markhere {} {
8937    global rowmenuid markedid canv
8938
8939    set markedid $rowmenuid
8940    make_idmark $markedid
8941}
8942
8943proc gotomark {} {
8944    global markedid
8945
8946    if {[info exists markedid]} {
8947        selbyid $markedid
8948    }
8949}
8950
8951proc replace_by_kids {l r} {
8952    global curview children
8953
8954    set id [commitonrow $r]
8955    set l [lreplace $l 0 0]
8956    foreach kid $children($curview,$id) {
8957        lappend l [rowofcommit $kid]
8958    }
8959    return [lsort -integer -decreasing -unique $l]
8960}
8961
8962proc find_common_desc {} {
8963    global markedid rowmenuid curview children
8964
8965    if {![info exists markedid]} return
8966    if {![commitinview $markedid $curview] ||
8967        ![commitinview $rowmenuid $curview]} return
8968    #set t1 [clock clicks -milliseconds]
8969    set l1 [list [rowofcommit $markedid]]
8970    set l2 [list [rowofcommit $rowmenuid]]
8971    while 1 {
8972        set r1 [lindex $l1 0]
8973        set r2 [lindex $l2 0]
8974        if {$r1 eq {} || $r2 eq {}} break
8975        if {$r1 == $r2} {
8976            selectline $r1 1
8977            break
8978        }
8979        if {$r1 > $r2} {
8980            set l1 [replace_by_kids $l1 $r1]
8981        } else {
8982            set l2 [replace_by_kids $l2 $r2]
8983        }
8984    }
8985    #set t2 [clock clicks -milliseconds]
8986    #puts "took [expr {$t2-$t1}]ms"
8987}
8988
8989proc compare_commits {} {
8990    global markedid rowmenuid curview children
8991
8992    if {![info exists markedid]} return
8993    if {![commitinview $markedid $curview]} return
8994    addtohistory [list do_cmp_commits $markedid $rowmenuid]
8995    do_cmp_commits $markedid $rowmenuid
8996}
8997
8998proc getpatchid {id} {
8999    global patchids
9000
9001    if {![info exists patchids($id)]} {
9002        set cmd [diffcmd [list $id] {-p --root}]
9003        # trim off the initial "|"
9004        set cmd [lrange $cmd 1 end]
9005        if {[catch {
9006            set x [eval exec $cmd | git patch-id]
9007            set patchids($id) [lindex $x 0]
9008        }]} {
9009            set patchids($id) "error"
9010        }
9011    }
9012    return $patchids($id)
9013}
9014
9015proc do_cmp_commits {a b} {
9016    global ctext curview parents children patchids commitinfo
9017
9018    $ctext conf -state normal
9019    clear_ctext
9020    init_flist {}
9021    for {set i 0} {$i < 100} {incr i} {
9022        set skipa 0
9023        set skipb 0
9024        if {[llength $parents($curview,$a)] > 1} {
9025            appendshortlink $a [mc "Skipping merge commit "] "\n"
9026            set skipa 1
9027        } else {
9028            set patcha [getpatchid $a]
9029        }
9030        if {[llength $parents($curview,$b)] > 1} {
9031            appendshortlink $b [mc "Skipping merge commit "] "\n"
9032            set skipb 1
9033        } else {
9034            set patchb [getpatchid $b]
9035        }
9036        if {!$skipa && !$skipb} {
9037            set heada [lindex $commitinfo($a) 0]
9038            set headb [lindex $commitinfo($b) 0]
9039            if {$patcha eq "error"} {
9040                appendshortlink $a [mc "Error getting patch ID for "] \
9041                    [mc " - stopping\n"]
9042                break
9043            }
9044            if {$patchb eq "error"} {
9045                appendshortlink $b [mc "Error getting patch ID for "] \
9046                    [mc " - stopping\n"]
9047                break
9048            }
9049            if {$patcha eq $patchb} {
9050                if {$heada eq $headb} {
9051                    appendshortlink $a [mc "Commit "]
9052                    appendshortlink $b " == " "  $heada\n"
9053                } else {
9054                    appendshortlink $a [mc "Commit "] "  $heada\n"
9055                    appendshortlink $b [mc " is the same patch as\n       "] \
9056                        "  $headb\n"
9057                }
9058                set skipa 1
9059                set skipb 1
9060            } else {
9061                $ctext insert end "\n"
9062                appendshortlink $a [mc "Commit "] "  $heada\n"
9063                appendshortlink $b [mc " differs from\n       "] \
9064                    "  $headb\n"
9065                $ctext insert end [mc "Diff of commits:\n\n"]
9066                $ctext conf -state disabled
9067                update
9068                diffcommits $a $b
9069                return
9070            }
9071        }
9072        if {$skipa} {
9073            set kids [real_children $curview,$a]
9074            if {[llength $kids] != 1} {
9075                $ctext insert end "\n"
9076                appendshortlink $a [mc "Commit "] \
9077                    [mc " has %s children - stopping\n" [llength $kids]]
9078                break
9079            }
9080            set a [lindex $kids 0]
9081        }
9082        if {$skipb} {
9083            set kids [real_children $curview,$b]
9084            if {[llength $kids] != 1} {
9085                appendshortlink $b [mc "Commit "] \
9086                    [mc " has %s children - stopping\n" [llength $kids]]
9087                break
9088            }
9089            set b [lindex $kids 0]
9090        }
9091    }
9092    $ctext conf -state disabled
9093}
9094
9095proc diffcommits {a b} {
9096    global diffcontext diffids blobdifffd diffinhdr currdiffsubmod
9097
9098    set tmpdir [gitknewtmpdir]
9099    set fna [file join $tmpdir "commit-[string range $a 0 7]"]
9100    set fnb [file join $tmpdir "commit-[string range $b 0 7]"]
9101    if {[catch {
9102        exec git diff-tree -p --pretty $a >$fna
9103        exec git diff-tree -p --pretty $b >$fnb
9104    } err]} {
9105        error_popup [mc "Error writing commit to file: %s" $err]
9106        return
9107    }
9108    if {[catch {
9109        set fd [open "| diff -U$diffcontext $fna $fnb" r]
9110    } err]} {
9111        error_popup [mc "Error diffing commits: %s" $err]
9112        return
9113    }
9114    set diffids [list commits $a $b]
9115    set blobdifffd($diffids) $fd
9116    set diffinhdr 0
9117    set currdiffsubmod ""
9118    filerun $fd [list getblobdiffline $fd $diffids]
9119}
9120
9121proc diffvssel {dirn} {
9122    global rowmenuid selectedline
9123
9124    if {$selectedline eq {}} return
9125    if {$dirn} {
9126        set oldid [commitonrow $selectedline]
9127        set newid $rowmenuid
9128    } else {
9129        set oldid $rowmenuid
9130        set newid [commitonrow $selectedline]
9131    }
9132    addtohistory [list doseldiff $oldid $newid] savectextpos
9133    doseldiff $oldid $newid
9134}
9135
9136proc diffvsmark {dirn} {
9137    global rowmenuid markedid
9138
9139    if {![info exists markedid]} return
9140    if {$dirn} {
9141        set oldid $markedid
9142        set newid $rowmenuid
9143    } else {
9144        set oldid $rowmenuid
9145        set newid $markedid
9146    }
9147    addtohistory [list doseldiff $oldid $newid] savectextpos
9148    doseldiff $oldid $newid
9149}
9150
9151proc doseldiff {oldid newid} {
9152    global ctext
9153    global commitinfo
9154
9155    $ctext conf -state normal
9156    clear_ctext
9157    init_flist [mc "Top"]
9158    $ctext insert end "[mc "From"] "
9159    $ctext insert end $oldid link0
9160    setlink $oldid link0
9161    $ctext insert end "\n     "
9162    $ctext insert end [lindex $commitinfo($oldid) 0]
9163    $ctext insert end "\n\n[mc "To"]   "
9164    $ctext insert end $newid link1
9165    setlink $newid link1
9166    $ctext insert end "\n     "
9167    $ctext insert end [lindex $commitinfo($newid) 0]
9168    $ctext insert end "\n"
9169    $ctext conf -state disabled
9170    $ctext tag remove found 1.0 end
9171    startdiff [list $oldid $newid]
9172}
9173
9174proc mkpatch {} {
9175    global rowmenuid currentid commitinfo patchtop patchnum NS
9176
9177    if {![info exists currentid]} return
9178    set oldid $currentid
9179    set oldhead [lindex $commitinfo($oldid) 0]
9180    set newid $rowmenuid
9181    set newhead [lindex $commitinfo($newid) 0]
9182    set top .patch
9183    set patchtop $top
9184    catch {destroy $top}
9185    ttk_toplevel $top
9186    make_transient $top .
9187    ${NS}::label $top.title -text [mc "Generate patch"]
9188    grid $top.title - -pady 10
9189    ${NS}::label $top.from -text [mc "From:"]
9190    ${NS}::entry $top.fromsha1 -width 40
9191    $top.fromsha1 insert 0 $oldid
9192    $top.fromsha1 conf -state readonly
9193    grid $top.from $top.fromsha1 -sticky w
9194    ${NS}::entry $top.fromhead -width 60
9195    $top.fromhead insert 0 $oldhead
9196    $top.fromhead conf -state readonly
9197    grid x $top.fromhead -sticky w
9198    ${NS}::label $top.to -text [mc "To:"]
9199    ${NS}::entry $top.tosha1 -width 40
9200    $top.tosha1 insert 0 $newid
9201    $top.tosha1 conf -state readonly
9202    grid $top.to $top.tosha1 -sticky w
9203    ${NS}::entry $top.tohead -width 60
9204    $top.tohead insert 0 $newhead
9205    $top.tohead conf -state readonly
9206    grid x $top.tohead -sticky w
9207    ${NS}::button $top.rev -text [mc "Reverse"] -command mkpatchrev
9208    grid $top.rev x -pady 10 -padx 5
9209    ${NS}::label $top.flab -text [mc "Output file:"]
9210    ${NS}::entry $top.fname -width 60
9211    $top.fname insert 0 [file normalize "patch$patchnum.patch"]
9212    incr patchnum
9213    grid $top.flab $top.fname -sticky w
9214    ${NS}::frame $top.buts
9215    ${NS}::button $top.buts.gen -text [mc "Generate"] -command mkpatchgo
9216    ${NS}::button $top.buts.can -text [mc "Cancel"] -command mkpatchcan
9217    bind $top <Key-Return> mkpatchgo
9218    bind $top <Key-Escape> mkpatchcan
9219    grid $top.buts.gen $top.buts.can
9220    grid columnconfigure $top.buts 0 -weight 1 -uniform a
9221    grid columnconfigure $top.buts 1 -weight 1 -uniform a
9222    grid $top.buts - -pady 10 -sticky ew
9223    focus $top.fname
9224}
9225
9226proc mkpatchrev {} {
9227    global patchtop
9228
9229    set oldid [$patchtop.fromsha1 get]
9230    set oldhead [$patchtop.fromhead get]
9231    set newid [$patchtop.tosha1 get]
9232    set newhead [$patchtop.tohead get]
9233    foreach e [list fromsha1 fromhead tosha1 tohead] \
9234            v [list $newid $newhead $oldid $oldhead] {
9235        $patchtop.$e conf -state normal
9236        $patchtop.$e delete 0 end
9237        $patchtop.$e insert 0 $v
9238        $patchtop.$e conf -state readonly
9239    }
9240}
9241
9242proc mkpatchgo {} {
9243    global patchtop nullid nullid2
9244
9245    set oldid [$patchtop.fromsha1 get]
9246    set newid [$patchtop.tosha1 get]
9247    set fname [$patchtop.fname get]
9248    set cmd [diffcmd [list $oldid $newid] -p]
9249    # trim off the initial "|"
9250    set cmd [lrange $cmd 1 end]
9251    lappend cmd >$fname &
9252    if {[catch {eval exec $cmd} err]} {
9253        error_popup "[mc "Error creating patch:"] $err" $patchtop
9254    }
9255    catch {destroy $patchtop}
9256    unset patchtop
9257}
9258
9259proc mkpatchcan {} {
9260    global patchtop
9261
9262    catch {destroy $patchtop}
9263    unset patchtop
9264}
9265
9266proc mktag {} {
9267    global rowmenuid mktagtop commitinfo NS
9268
9269    set top .maketag
9270    set mktagtop $top
9271    catch {destroy $top}
9272    ttk_toplevel $top
9273    make_transient $top .
9274    ${NS}::label $top.title -text [mc "Create tag"]
9275    grid $top.title - -pady 10
9276    ${NS}::label $top.id -text [mc "ID:"]
9277    ${NS}::entry $top.sha1 -width 40
9278    $top.sha1 insert 0 $rowmenuid
9279    $top.sha1 conf -state readonly
9280    grid $top.id $top.sha1 -sticky w
9281    ${NS}::entry $top.head -width 60
9282    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
9283    $top.head conf -state readonly
9284    grid x $top.head -sticky w
9285    ${NS}::label $top.tlab -text [mc "Tag name:"]
9286    ${NS}::entry $top.tag -width 60
9287    grid $top.tlab $top.tag -sticky w
9288    ${NS}::label $top.op -text [mc "Tag message is optional"]
9289    grid $top.op -columnspan 2 -sticky we
9290    ${NS}::label $top.mlab -text [mc "Tag message:"]
9291    ${NS}::entry $top.msg -width 60
9292    grid $top.mlab $top.msg -sticky w
9293    ${NS}::frame $top.buts
9294    ${NS}::button $top.buts.gen -text [mc "Create"] -command mktaggo
9295    ${NS}::button $top.buts.can -text [mc "Cancel"] -command mktagcan
9296    bind $top <Key-Return> mktaggo
9297    bind $top <Key-Escape> mktagcan
9298    grid $top.buts.gen $top.buts.can
9299    grid columnconfigure $top.buts 0 -weight 1 -uniform a
9300    grid columnconfigure $top.buts 1 -weight 1 -uniform a
9301    grid $top.buts - -pady 10 -sticky ew
9302    focus $top.tag
9303}
9304
9305proc domktag {} {
9306    global mktagtop env tagids idtags
9307
9308    set id [$mktagtop.sha1 get]
9309    set tag [$mktagtop.tag get]
9310    set msg [$mktagtop.msg get]
9311    if {$tag == {}} {
9312        error_popup [mc "No tag name specified"] $mktagtop
9313        return 0
9314    }
9315    if {[info exists tagids($tag)]} {
9316        error_popup [mc "Tag \"%s\" already exists" $tag] $mktagtop
9317        return 0
9318    }
9319    if {[catch {
9320        if {$msg != {}} {
9321            exec git tag -a -m $msg $tag $id
9322        } else {
9323            exec git tag $tag $id
9324        }
9325    } err]} {
9326        error_popup "[mc "Error creating tag:"] $err" $mktagtop
9327        return 0
9328    }
9329
9330    set tagids($tag) $id
9331    lappend idtags($id) $tag
9332    redrawtags $id
9333    addedtag $id
9334    dispneartags 0
9335    run refill_reflist
9336    return 1
9337}
9338
9339proc redrawtags {id} {
9340    global canv linehtag idpos currentid curview cmitlisted markedid
9341    global canvxmax iddrawn circleitem mainheadid circlecolors
9342    global mainheadcirclecolor
9343
9344    if {![commitinview $id $curview]} return
9345    if {![info exists iddrawn($id)]} return
9346    set row [rowofcommit $id]
9347    if {$id eq $mainheadid} {
9348        set ofill $mainheadcirclecolor
9349    } else {
9350        set ofill [lindex $circlecolors $cmitlisted($curview,$id)]
9351    }
9352    $canv itemconf $circleitem($row) -fill $ofill
9353    $canv delete tag.$id
9354    set xt [eval drawtags $id $idpos($id)]
9355    $canv coords $linehtag($id) $xt [lindex $idpos($id) 2]
9356    set text [$canv itemcget $linehtag($id) -text]
9357    set font [$canv itemcget $linehtag($id) -font]
9358    set xr [expr {$xt + [font measure $font $text]}]
9359    if {$xr > $canvxmax} {
9360        set canvxmax $xr
9361        setcanvscroll
9362    }
9363    if {[info exists currentid] && $currentid == $id} {
9364        make_secsel $id
9365    }
9366    if {[info exists markedid] && $markedid eq $id} {
9367        make_idmark $id
9368    }
9369}
9370
9371proc mktagcan {} {
9372    global mktagtop
9373
9374    catch {destroy $mktagtop}
9375    unset mktagtop
9376}
9377
9378proc mktaggo {} {
9379    if {![domktag]} return
9380    mktagcan
9381}
9382
9383proc copysummary {} {
9384    global rowmenuid autosellen
9385
9386    set format "%h (\"%s\", %ad)"
9387    set cmd [list git show -s --pretty=format:$format --date=short]
9388    if {$autosellen < 40} {
9389        lappend cmd --abbrev=$autosellen
9390    }
9391    set summary [eval exec $cmd $rowmenuid]
9392
9393    clipboard clear
9394    clipboard append $summary
9395}
9396
9397proc writecommit {} {
9398    global rowmenuid wrcomtop commitinfo wrcomcmd NS
9399
9400    set top .writecommit
9401    set wrcomtop $top
9402    catch {destroy $top}
9403    ttk_toplevel $top
9404    make_transient $top .
9405    ${NS}::label $top.title -text [mc "Write commit to file"]
9406    grid $top.title - -pady 10
9407    ${NS}::label $top.id -text [mc "ID:"]
9408    ${NS}::entry $top.sha1 -width 40
9409    $top.sha1 insert 0 $rowmenuid
9410    $top.sha1 conf -state readonly
9411    grid $top.id $top.sha1 -sticky w
9412    ${NS}::entry $top.head -width 60
9413    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
9414    $top.head conf -state readonly
9415    grid x $top.head -sticky w
9416    ${NS}::label $top.clab -text [mc "Command:"]
9417    ${NS}::entry $top.cmd -width 60 -textvariable wrcomcmd
9418    grid $top.clab $top.cmd -sticky w -pady 10
9419    ${NS}::label $top.flab -text [mc "Output file:"]
9420    ${NS}::entry $top.fname -width 60
9421    $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
9422    grid $top.flab $top.fname -sticky w
9423    ${NS}::frame $top.buts
9424    ${NS}::button $top.buts.gen -text [mc "Write"] -command wrcomgo
9425    ${NS}::button $top.buts.can -text [mc "Cancel"] -command wrcomcan
9426    bind $top <Key-Return> wrcomgo
9427    bind $top <Key-Escape> wrcomcan
9428    grid $top.buts.gen $top.buts.can
9429    grid columnconfigure $top.buts 0 -weight 1 -uniform a
9430    grid columnconfigure $top.buts 1 -weight 1 -uniform a
9431    grid $top.buts - -pady 10 -sticky ew
9432    focus $top.fname
9433}
9434
9435proc wrcomgo {} {
9436    global wrcomtop
9437
9438    set id [$wrcomtop.sha1 get]
9439    set cmd "echo $id | [$wrcomtop.cmd get]"
9440    set fname [$wrcomtop.fname get]
9441    if {[catch {exec sh -c $cmd >$fname &} err]} {
9442        error_popup "[mc "Error writing commit:"] $err" $wrcomtop
9443    }
9444    catch {destroy $wrcomtop}
9445    unset wrcomtop
9446}
9447
9448proc wrcomcan {} {
9449    global wrcomtop
9450
9451    catch {destroy $wrcomtop}
9452    unset wrcomtop
9453}
9454
9455proc mkbranch {} {
9456    global NS rowmenuid
9457
9458    set top .branchdialog
9459
9460    set val(name) ""
9461    set val(id) $rowmenuid
9462    set val(command) [list mkbrgo $top]
9463
9464    set ui(title) [mc "Create branch"]
9465    set ui(accept) [mc "Create"]
9466
9467    branchdia $top val ui
9468}
9469
9470proc mvbranch {} {
9471    global NS
9472    global headmenuid headmenuhead
9473
9474    set top .branchdialog
9475
9476    set val(name) $headmenuhead
9477    set val(id) $headmenuid
9478    set val(command) [list mvbrgo $top $headmenuhead]
9479
9480    set ui(title) [mc "Rename branch %s" $headmenuhead]
9481    set ui(accept) [mc "Rename"]
9482
9483    branchdia $top val ui
9484}
9485
9486proc branchdia {top valvar uivar} {
9487    global NS
9488    upvar $valvar val $uivar ui
9489
9490    catch {destroy $top}
9491    ttk_toplevel $top
9492    make_transient $top .
9493    ${NS}::label $top.title -text $ui(title)
9494    grid $top.title - -pady 10
9495    ${NS}::label $top.id -text [mc "ID:"]
9496    ${NS}::entry $top.sha1 -width 40
9497    $top.sha1 insert 0 $val(id)
9498    $top.sha1 conf -state readonly
9499    grid $top.id $top.sha1 -sticky w
9500    ${NS}::label $top.nlab -text [mc "Name:"]
9501    ${NS}::entry $top.name -width 40
9502    $top.name insert 0 $val(name)
9503    grid $top.nlab $top.name -sticky w
9504    ${NS}::frame $top.buts
9505    ${NS}::button $top.buts.go -text $ui(accept) -command $val(command)
9506    ${NS}::button $top.buts.can -text [mc "Cancel"] -command "catch {destroy $top}"
9507    bind $top <Key-Return> $val(command)
9508    bind $top <Key-Escape> "catch {destroy $top}"
9509    grid $top.buts.go $top.buts.can
9510    grid columnconfigure $top.buts 0 -weight 1 -uniform a
9511    grid columnconfigure $top.buts 1 -weight 1 -uniform a
9512    grid $top.buts - -pady 10 -sticky ew
9513    focus $top.name
9514}
9515
9516proc mkbrgo {top} {
9517    global headids idheads
9518
9519    set name [$top.name get]
9520    set id [$top.sha1 get]
9521    set cmdargs {}
9522    set old_id {}
9523    if {$name eq {}} {
9524        error_popup [mc "Please specify a name for the new branch"] $top
9525        return
9526    }
9527    if {[info exists headids($name)]} {
9528        if {![confirm_popup [mc \
9529                "Branch '%s' already exists. Overwrite?" $name] $top]} {
9530            return
9531        }
9532        set old_id $headids($name)
9533        lappend cmdargs -f
9534    }
9535    catch {destroy $top}
9536    lappend cmdargs $name $id
9537    nowbusy newbranch
9538    update
9539    if {[catch {
9540        eval exec git branch $cmdargs
9541    } err]} {
9542        notbusy newbranch
9543        error_popup $err
9544    } else {
9545        notbusy newbranch
9546        if {$old_id ne {}} {
9547            movehead $id $name
9548            movedhead $id $name
9549            redrawtags $old_id
9550            redrawtags $id
9551        } else {
9552            set headids($name) $id
9553            lappend idheads($id) $name
9554            addedhead $id $name
9555            redrawtags $id
9556        }
9557        dispneartags 0
9558        run refill_reflist
9559    }
9560}
9561
9562proc mvbrgo {top prevname} {
9563    global headids idheads mainhead mainheadid
9564
9565    set name [$top.name get]
9566    set id [$top.sha1 get]
9567    set cmdargs {}
9568    if {$name eq $prevname} {
9569        catch {destroy $top}
9570        return
9571    }
9572    if {$name eq {}} {
9573        error_popup [mc "Please specify a new name for the branch"] $top
9574        return
9575    }
9576    catch {destroy $top}
9577    lappend cmdargs -m $prevname $name
9578    nowbusy renamebranch
9579    update
9580    if {[catch {
9581        eval exec git branch $cmdargs
9582    } err]} {
9583        notbusy renamebranch
9584        error_popup $err
9585    } else {
9586        notbusy renamebranch
9587        removehead $id $prevname
9588        removedhead $id $prevname
9589        set headids($name) $id
9590        lappend idheads($id) $name
9591        addedhead $id $name
9592        if {$prevname eq $mainhead} {
9593            set mainhead $name
9594            set mainheadid $id
9595        }
9596        redrawtags $id
9597        dispneartags 0
9598        run refill_reflist
9599    }
9600}
9601
9602proc exec_citool {tool_args {baseid {}}} {
9603    global commitinfo env
9604
9605    set save_env [array get env GIT_AUTHOR_*]
9606
9607    if {$baseid ne {}} {
9608        if {![info exists commitinfo($baseid)]} {
9609            getcommit $baseid
9610        }
9611        set author [lindex $commitinfo($baseid) 1]
9612        set date [lindex $commitinfo($baseid) 2]
9613        if {[regexp {^\s*(\S.*\S|\S)\s*<(.*)>\s*$} \
9614                    $author author name email]
9615            && $date ne {}} {
9616            set env(GIT_AUTHOR_NAME) $name
9617            set env(GIT_AUTHOR_EMAIL) $email
9618            set env(GIT_AUTHOR_DATE) $date
9619        }
9620    }
9621
9622    eval exec git citool $tool_args &
9623
9624    array unset env GIT_AUTHOR_*
9625    array set env $save_env
9626}
9627
9628proc cherrypick {} {
9629    global rowmenuid curview
9630    global mainhead mainheadid
9631    global gitdir
9632
9633    set oldhead [exec git rev-parse HEAD]
9634    set dheads [descheads $rowmenuid]
9635    if {$dheads ne {} && [lsearch -exact $dheads $oldhead] >= 0} {
9636        set ok [confirm_popup [mc "Commit %s is already\
9637                included in branch %s -- really re-apply it?" \
9638                                   [string range $rowmenuid 0 7] $mainhead]]
9639        if {!$ok} return
9640    }
9641    nowbusy cherrypick [mc "Cherry-picking"]
9642    update
9643    # Unfortunately git-cherry-pick writes stuff to stderr even when
9644    # no error occurs, and exec takes that as an indication of error...
9645    if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} {
9646        notbusy cherrypick
9647        if {[regexp -line \
9648                 {Entry '(.*)' (would be overwritten by merge|not uptodate)} \
9649                 $err msg fname]} {
9650            error_popup [mc "Cherry-pick failed because of local changes\
9651                        to file '%s'.\nPlease commit, reset or stash\
9652                        your changes and try again." $fname]
9653        } elseif {[regexp -line \
9654                       {^(CONFLICT \(.*\):|Automatic cherry-pick failed|error: could not apply)} \
9655                       $err]} {
9656            if {[confirm_popup [mc "Cherry-pick failed because of merge\
9657                        conflict.\nDo you wish to run git citool to\
9658                        resolve it?"]]} {
9659                # Force citool to read MERGE_MSG
9660                file delete [file join $gitdir "GITGUI_MSG"]
9661                exec_citool {} $rowmenuid
9662            }
9663        } else {
9664            error_popup $err
9665        }
9666        run updatecommits
9667        return
9668    }
9669    set newhead [exec git rev-parse HEAD]
9670    if {$newhead eq $oldhead} {
9671        notbusy cherrypick
9672        error_popup [mc "No changes committed"]
9673        return
9674    }
9675    addnewchild $newhead $oldhead
9676    if {[commitinview $oldhead $curview]} {
9677        # XXX this isn't right if we have a path limit...
9678        insertrow $newhead $oldhead $curview
9679        if {$mainhead ne {}} {
9680            movehead $newhead $mainhead
9681            movedhead $newhead $mainhead
9682        }
9683        set mainheadid $newhead
9684        redrawtags $oldhead
9685        redrawtags $newhead
9686        selbyid $newhead
9687    }
9688    notbusy cherrypick
9689}
9690
9691proc revert {} {
9692    global rowmenuid curview
9693    global mainhead mainheadid
9694    global gitdir
9695
9696    set oldhead [exec git rev-parse HEAD]
9697    set dheads [descheads $rowmenuid]
9698    if { $dheads eq {} || [lsearch -exact $dheads $oldhead] == -1 } {
9699       set ok [confirm_popup [mc "Commit %s is not\
9700           included in branch %s -- really revert it?" \
9701                      [string range $rowmenuid 0 7] $mainhead]]
9702       if {!$ok} return
9703    }
9704    nowbusy revert [mc "Reverting"]
9705    update
9706
9707    if [catch {exec git revert --no-edit $rowmenuid} err] {
9708        notbusy revert
9709        if [regexp {files would be overwritten by merge:(\n(( |\t)+[^\n]+\n)+)}\
9710                $err match files] {
9711            regsub {\n( |\t)+} $files "\n" files
9712            error_popup [mc "Revert failed because of local changes to\
9713                the following files:%s Please commit, reset or stash \
9714                your changes and try again." $files]
9715        } elseif [regexp {error: could not revert} $err] {
9716            if [confirm_popup [mc "Revert failed because of merge conflict.\n\
9717                Do you wish to run git citool to resolve it?"]] {
9718                # Force citool to read MERGE_MSG
9719                file delete [file join $gitdir "GITGUI_MSG"]
9720                exec_citool {} $rowmenuid
9721            }
9722        } else { error_popup $err }
9723        run updatecommits
9724        return
9725    }
9726
9727    set newhead [exec git rev-parse HEAD]
9728    if { $newhead eq $oldhead } {
9729        notbusy revert
9730        error_popup [mc "No changes committed"]
9731        return
9732    }
9733
9734    addnewchild $newhead $oldhead
9735
9736    if [commitinview $oldhead $curview] {
9737        # XXX this isn't right if we have a path limit...
9738        insertrow $newhead $oldhead $curview
9739        if {$mainhead ne {}} {
9740            movehead $newhead $mainhead
9741            movedhead $newhead $mainhead
9742        }
9743        set mainheadid $newhead
9744        redrawtags $oldhead
9745        redrawtags $newhead
9746        selbyid $newhead
9747    }
9748
9749    notbusy revert
9750}
9751
9752proc resethead {} {
9753    global mainhead rowmenuid confirm_ok resettype NS
9754
9755    set confirm_ok 0
9756    set w ".confirmreset"
9757    ttk_toplevel $w
9758    make_transient $w .
9759    wm title $w [mc "Confirm reset"]
9760    ${NS}::label $w.m -text \
9761        [mc "Reset branch %s to %s?" $mainhead [string range $rowmenuid 0 7]]
9762    pack $w.m -side top -fill x -padx 20 -pady 20
9763    ${NS}::labelframe $w.f -text [mc "Reset type:"]
9764    set resettype mixed
9765    ${NS}::radiobutton $w.f.soft -value soft -variable resettype \
9766        -text [mc "Soft: Leave working tree and index untouched"]
9767    grid $w.f.soft -sticky w
9768    ${NS}::radiobutton $w.f.mixed -value mixed -variable resettype \
9769        -text [mc "Mixed: Leave working tree untouched, reset index"]
9770    grid $w.f.mixed -sticky w
9771    ${NS}::radiobutton $w.f.hard -value hard -variable resettype \
9772        -text [mc "Hard: Reset working tree and index\n(discard ALL local changes)"]
9773    grid $w.f.hard -sticky w
9774    pack $w.f -side top -fill x -padx 4
9775    ${NS}::button $w.ok -text [mc OK] -command "set confirm_ok 1; destroy $w"
9776    pack $w.ok -side left -fill x -padx 20 -pady 20
9777    ${NS}::button $w.cancel -text [mc Cancel] -command "destroy $w"
9778    bind $w <Key-Escape> [list destroy $w]
9779    pack $w.cancel -side right -fill x -padx 20 -pady 20
9780    bind $w <Visibility> "grab $w; focus $w"
9781    tkwait window $w
9782    if {!$confirm_ok} return
9783    if {[catch {set fd [open \
9784            [list | git reset --$resettype $rowmenuid 2>@1] r]} err]} {
9785        error_popup $err
9786    } else {
9787        dohidelocalchanges
9788        filerun $fd [list readresetstat $fd]
9789        nowbusy reset [mc "Resetting"]
9790        selbyid $rowmenuid
9791    }
9792}
9793
9794proc readresetstat {fd} {
9795    global mainhead mainheadid showlocalchanges rprogcoord
9796
9797    if {[gets $fd line] >= 0} {
9798        if {[regexp {([0-9]+)% \(([0-9]+)/([0-9]+)\)} $line match p m n]} {
9799            set rprogcoord [expr {1.0 * $m / $n}]
9800            adjustprogress
9801        }
9802        return 1
9803    }
9804    set rprogcoord 0
9805    adjustprogress
9806    notbusy reset
9807    if {[catch {close $fd} err]} {
9808        error_popup $err
9809    }
9810    set oldhead $mainheadid
9811    set newhead [exec git rev-parse HEAD]
9812    if {$newhead ne $oldhead} {
9813        movehead $newhead $mainhead
9814        movedhead $newhead $mainhead
9815        set mainheadid $newhead
9816        redrawtags $oldhead
9817        redrawtags $newhead
9818    }
9819    if {$showlocalchanges} {
9820        doshowlocalchanges
9821    }
9822    return 0
9823}
9824
9825# context menu for a head
9826proc headmenu {x y id head} {
9827    global headmenuid headmenuhead headctxmenu mainhead
9828
9829    stopfinding
9830    set headmenuid $id
9831    set headmenuhead $head
9832    array set state {0 normal 1 normal 2 normal}
9833    if {[string match "remotes/*" $head]} {
9834        array set state {0 disabled 1 disabled 2 disabled}
9835    }
9836    if {$head eq $mainhead} {
9837        array set state {0 disabled 2 disabled}
9838    }
9839    foreach i {0 1 2} {
9840        $headctxmenu entryconfigure $i -state $state($i)
9841    }
9842    tk_popup $headctxmenu $x $y
9843}
9844
9845proc cobranch {} {
9846    global headmenuid headmenuhead headids
9847    global showlocalchanges
9848
9849    # check the tree is clean first??
9850    nowbusy checkout [mc "Checking out"]
9851    update
9852    dohidelocalchanges
9853    if {[catch {
9854        set fd [open [list | git checkout $headmenuhead 2>@1] r]
9855    } err]} {
9856        notbusy checkout
9857        error_popup $err
9858        if {$showlocalchanges} {
9859            dodiffindex
9860        }
9861    } else {
9862        filerun $fd [list readcheckoutstat $fd $headmenuhead $headmenuid]
9863    }
9864}
9865
9866proc readcheckoutstat {fd newhead newheadid} {
9867    global mainhead mainheadid headids showlocalchanges progresscoords
9868    global viewmainheadid curview
9869
9870    if {[gets $fd line] >= 0} {
9871        if {[regexp {([0-9]+)% \(([0-9]+)/([0-9]+)\)} $line match p m n]} {
9872            set progresscoords [list 0 [expr {1.0 * $m / $n}]]
9873            adjustprogress
9874        }
9875        return 1
9876    }
9877    set progresscoords {0 0}
9878    adjustprogress
9879    notbusy checkout
9880    if {[catch {close $fd} err]} {
9881        error_popup $err
9882    }
9883    set oldmainid $mainheadid
9884    set mainhead $newhead
9885    set mainheadid $newheadid
9886    set viewmainheadid($curview) $newheadid
9887    redrawtags $oldmainid
9888    redrawtags $newheadid
9889    selbyid $newheadid
9890    if {$showlocalchanges} {
9891        dodiffindex
9892    }
9893}
9894
9895proc rmbranch {} {
9896    global headmenuid headmenuhead mainhead
9897    global idheads
9898
9899    set head $headmenuhead
9900    set id $headmenuid
9901    # this check shouldn't be needed any more...
9902    if {$head eq $mainhead} {
9903        error_popup [mc "Cannot delete the currently checked-out branch"]
9904        return
9905    }
9906    set dheads [descheads $id]
9907    if {[llength $dheads] == 1 && $idheads($dheads) eq $head} {
9908        # the stuff on this branch isn't on any other branch
9909        if {![confirm_popup [mc "The commits on branch %s aren't on any other\
9910                        branch.\nReally delete branch %s?" $head $head]]} return
9911    }
9912    nowbusy rmbranch
9913    update
9914    if {[catch {exec git branch -D $head} err]} {
9915        notbusy rmbranch
9916        error_popup $err
9917        return
9918    }
9919    removehead $id $head
9920    removedhead $id $head
9921    redrawtags $id
9922    notbusy rmbranch
9923    dispneartags 0
9924    run refill_reflist
9925}
9926
9927# Display a list of tags and heads
9928proc showrefs {} {
9929    global showrefstop bgcolor fgcolor selectbgcolor NS
9930    global bglist fglist reflistfilter reflist maincursor
9931
9932    set top .showrefs
9933    set showrefstop $top
9934    if {[winfo exists $top]} {
9935        raise $top
9936        refill_reflist
9937        return
9938    }
9939    ttk_toplevel $top
9940    wm title $top [mc "Tags and heads: %s" [file tail [pwd]]]
9941    make_transient $top .
9942    text $top.list -background $bgcolor -foreground $fgcolor \
9943        -selectbackground $selectbgcolor -font mainfont \
9944        -xscrollcommand "$top.xsb set" -yscrollcommand "$top.ysb set" \
9945        -width 30 -height 20 -cursor $maincursor \
9946        -spacing1 1 -spacing3 1 -state disabled
9947    $top.list tag configure highlight -background $selectbgcolor
9948    if {![lsearch -exact $bglist $top.list]} {
9949        lappend bglist $top.list
9950        lappend fglist $top.list
9951    }
9952    ${NS}::scrollbar $top.ysb -command "$top.list yview" -orient vertical
9953    ${NS}::scrollbar $top.xsb -command "$top.list xview" -orient horizontal
9954    grid $top.list $top.ysb -sticky nsew
9955    grid $top.xsb x -sticky ew
9956    ${NS}::frame $top.f
9957    ${NS}::label $top.f.l -text "[mc "Filter"]: "
9958    ${NS}::entry $top.f.e -width 20 -textvariable reflistfilter
9959    set reflistfilter "*"
9960    trace add variable reflistfilter write reflistfilter_change
9961    pack $top.f.e -side right -fill x -expand 1
9962    pack $top.f.l -side left
9963    grid $top.f - -sticky ew -pady 2
9964    ${NS}::button $top.close -command [list destroy $top] -text [mc "Close"]
9965    bind $top <Key-Escape> [list destroy $top]
9966    grid $top.close -
9967    grid columnconfigure $top 0 -weight 1
9968    grid rowconfigure $top 0 -weight 1
9969    bind $top.list <1> {break}
9970    bind $top.list <B1-Motion> {break}
9971    bind $top.list <ButtonRelease-1> {sel_reflist %W %x %y; break}
9972    set reflist {}
9973    refill_reflist
9974}
9975
9976proc sel_reflist {w x y} {
9977    global showrefstop reflist headids tagids otherrefids
9978
9979    if {![winfo exists $showrefstop]} return
9980    set l [lindex [split [$w index "@$x,$y"] "."] 0]
9981    set ref [lindex $reflist [expr {$l-1}]]
9982    set n [lindex $ref 0]
9983    switch -- [lindex $ref 1] {
9984        "H" {selbyid $headids($n)}
9985        "T" {selbyid $tagids($n)}
9986        "o" {selbyid $otherrefids($n)}
9987    }
9988    $showrefstop.list tag add highlight $l.0 "$l.0 lineend"
9989}
9990
9991proc unsel_reflist {} {
9992    global showrefstop
9993
9994    if {![info exists showrefstop] || ![winfo exists $showrefstop]} return
9995    $showrefstop.list tag remove highlight 0.0 end
9996}
9997
9998proc reflistfilter_change {n1 n2 op} {
9999    global reflistfilter
10000
10001    after cancel refill_reflist
10002    after 200 refill_reflist
10003}
10004
10005proc refill_reflist {} {
10006    global reflist reflistfilter showrefstop headids tagids otherrefids
10007    global curview
10008
10009    if {![info exists showrefstop] || ![winfo exists $showrefstop]} return
10010    set refs {}
10011    foreach n [array names headids] {
10012        if {[string match $reflistfilter $n]} {
10013            if {[commitinview $headids($n) $curview]} {
10014                lappend refs [list $n H]
10015            } else {
10016                interestedin $headids($n) {run refill_reflist}
10017            }
10018        }
10019    }
10020    foreach n [array names tagids] {
10021        if {[string match $reflistfilter $n]} {
10022            if {[commitinview $tagids($n) $curview]} {
10023                lappend refs [list $n T]
10024            } else {
10025                interestedin $tagids($n) {run refill_reflist}
10026            }
10027        }
10028    }
10029    foreach n [array names otherrefids] {
10030        if {[string match $reflistfilter $n]} {
10031            if {[commitinview $otherrefids($n) $curview]} {
10032                lappend refs [list $n o]
10033            } else {
10034                interestedin $otherrefids($n) {run refill_reflist}
10035            }
10036        }
10037    }
10038    set refs [lsort -index 0 $refs]
10039    if {$refs eq $reflist} return
10040
10041    # Update the contents of $showrefstop.list according to the
10042    # differences between $reflist (old) and $refs (new)
10043    $showrefstop.list conf -state normal
10044    $showrefstop.list insert end "\n"
10045    set i 0
10046    set j 0
10047    while {$i < [llength $reflist] || $j < [llength $refs]} {
10048        if {$i < [llength $reflist]} {
10049            if {$j < [llength $refs]} {
10050                set cmp [string compare [lindex $reflist $i 0] \
10051                             [lindex $refs $j 0]]
10052                if {$cmp == 0} {
10053                    set cmp [string compare [lindex $reflist $i 1] \
10054                                 [lindex $refs $j 1]]
10055                }
10056            } else {
10057                set cmp -1
10058            }
10059        } else {
10060            set cmp 1
10061        }
10062        switch -- $cmp {
10063            -1 {
10064                $showrefstop.list delete "[expr {$j+1}].0" "[expr {$j+2}].0"
10065                incr i
10066            }
10067            0 {
10068                incr i
10069                incr j
10070            }
10071            1 {
10072                set l [expr {$j + 1}]
10073                $showrefstop.list image create $l.0 -align baseline \
10074                    -image reficon-[lindex $refs $j 1] -padx 2
10075                $showrefstop.list insert $l.1 "[lindex $refs $j 0]\n"
10076                incr j
10077            }
10078        }
10079    }
10080    set reflist $refs
10081    # delete last newline
10082    $showrefstop.list delete end-2c end-1c
10083    $showrefstop.list conf -state disabled
10084}
10085
10086# Stuff for finding nearby tags
10087proc getallcommits {} {
10088    global allcommits nextarc seeds allccache allcwait cachedarcs allcupdate
10089    global idheads idtags idotherrefs allparents tagobjid
10090    global gitdir
10091
10092    if {![info exists allcommits]} {
10093        set nextarc 0
10094        set allcommits 0
10095        set seeds {}
10096        set allcwait 0
10097        set cachedarcs 0
10098        set allccache [file join $gitdir "gitk.cache"]
10099        if {![catch {
10100            set f [open $allccache r]
10101            set allcwait 1
10102            getcache $f
10103        }]} return
10104    }
10105
10106    if {$allcwait} {
10107        return
10108    }
10109    set cmd [list | git rev-list --parents]
10110    set allcupdate [expr {$seeds ne {}}]
10111    if {!$allcupdate} {
10112        set ids "--all"
10113    } else {
10114        set refs [concat [array names idheads] [array names idtags] \
10115                      [array names idotherrefs]]
10116        set ids {}
10117        set tagobjs {}
10118        foreach name [array names tagobjid] {
10119            lappend tagobjs $tagobjid($name)
10120        }
10121        foreach id [lsort -unique $refs] {
10122            if {![info exists allparents($id)] &&
10123                [lsearch -exact $tagobjs $id] < 0} {
10124                lappend ids $id
10125            }
10126        }
10127        if {$ids ne {}} {
10128            foreach id $seeds {
10129                lappend ids "^$id"
10130            }
10131        }
10132    }
10133    if {$ids ne {}} {
10134        set fd [open [concat $cmd $ids] r]
10135        fconfigure $fd -blocking 0
10136        incr allcommits
10137        nowbusy allcommits
10138        filerun $fd [list getallclines $fd]
10139    } else {
10140        dispneartags 0
10141    }
10142}
10143
10144# Since most commits have 1 parent and 1 child, we group strings of
10145# such commits into "arcs" joining branch/merge points (BMPs), which
10146# are commits that either don't have 1 parent or don't have 1 child.
10147#
10148# arcnos(id) - incoming arcs for BMP, arc we're on for other nodes
10149# arcout(id) - outgoing arcs for BMP
10150# arcids(a) - list of IDs on arc including end but not start
10151# arcstart(a) - BMP ID at start of arc
10152# arcend(a) - BMP ID at end of arc
10153# growing(a) - arc a is still growing
10154# arctags(a) - IDs out of arcids (excluding end) that have tags
10155# archeads(a) - IDs out of arcids (excluding end) that have heads
10156# The start of an arc is at the descendent end, so "incoming" means
10157# coming from descendents, and "outgoing" means going towards ancestors.
10158
10159proc getallclines {fd} {
10160    global allparents allchildren idtags idheads nextarc
10161    global arcnos arcids arctags arcout arcend arcstart archeads growing
10162    global seeds allcommits cachedarcs allcupdate
10163
10164    set nid 0
10165    while {[incr nid] <= 1000 && [gets $fd line] >= 0} {
10166        set id [lindex $line 0]
10167        if {[info exists allparents($id)]} {
10168            # seen it already
10169            continue
10170        }
10171        set cachedarcs 0
10172        set olds [lrange $line 1 end]
10173        set allparents($id) $olds
10174        if {![info exists allchildren($id)]} {
10175            set allchildren($id) {}
10176            set arcnos($id) {}
10177            lappend seeds $id
10178        } else {
10179            set a $arcnos($id)
10180            if {[llength $olds] == 1 && [llength $a] == 1} {
10181                lappend arcids($a) $id
10182                if {[info exists idtags($id)]} {
10183                    lappend arctags($a) $id
10184                }
10185                if {[info exists idheads($id)]} {
10186                    lappend archeads($a) $id
10187                }
10188                if {[info exists allparents($olds)]} {
10189                    # seen parent already
10190                    if {![info exists arcout($olds)]} {
10191                        splitarc $olds
10192                    }
10193                    lappend arcids($a) $olds
10194                    set arcend($a) $olds
10195                    unset growing($a)
10196                }
10197                lappend allchildren($olds) $id
10198                lappend arcnos($olds) $a
10199                continue
10200            }
10201        }
10202        foreach a $arcnos($id) {
10203            lappend arcids($a) $id
10204            set arcend($a) $id
10205            unset growing($a)
10206        }
10207
10208        set ao {}
10209        foreach p $olds {
10210            lappend allchildren($p) $id
10211            set a [incr nextarc]
10212            set arcstart($a) $id
10213            set archeads($a) {}
10214            set arctags($a) {}
10215            set archeads($a) {}
10216            set arcids($a) {}
10217            lappend ao $a
10218            set growing($a) 1
10219            if {[info exists allparents($p)]} {
10220                # seen it already, may need to make a new branch
10221                if {![info exists arcout($p)]} {
10222                    splitarc $p
10223                }
10224                lappend arcids($a) $p
10225                set arcend($a) $p
10226                unset growing($a)
10227            }
10228            lappend arcnos($p) $a
10229        }
10230        set arcout($id) $ao
10231    }
10232    if {$nid > 0} {
10233        global cached_dheads cached_dtags cached_atags
10234        unset -nocomplain cached_dheads
10235        unset -nocomplain cached_dtags
10236        unset -nocomplain cached_atags
10237    }
10238    if {![eof $fd]} {
10239        return [expr {$nid >= 1000? 2: 1}]
10240    }
10241    set cacheok 1
10242    if {[catch {
10243        fconfigure $fd -blocking 1
10244        close $fd
10245    } err]} {
10246        # got an error reading the list of commits
10247        # if we were updating, try rereading the whole thing again
10248        if {$allcupdate} {
10249            incr allcommits -1
10250            dropcache $err
10251            return
10252        }
10253        error_popup "[mc "Error reading commit topology information;\
10254                branch and preceding/following tag information\
10255                will be incomplete."]\n($err)"
10256        set cacheok 0
10257    }
10258    if {[incr allcommits -1] == 0} {
10259        notbusy allcommits
10260        if {$cacheok} {
10261            run savecache
10262        }
10263    }
10264    dispneartags 0
10265    return 0
10266}
10267
10268proc recalcarc {a} {
10269    global arctags archeads arcids idtags idheads
10270
10271    set at {}
10272    set ah {}
10273    foreach id [lrange $arcids($a) 0 end-1] {
10274        if {[info exists idtags($id)]} {
10275            lappend at $id
10276        }
10277        if {[info exists idheads($id)]} {
10278            lappend ah $id
10279        }
10280    }
10281    set arctags($a) $at
10282    set archeads($a) $ah
10283}
10284
10285proc splitarc {p} {
10286    global arcnos arcids nextarc arctags archeads idtags idheads
10287    global arcstart arcend arcout allparents growing
10288
10289    set a $arcnos($p)
10290    if {[llength $a] != 1} {
10291        puts "oops splitarc called but [llength $a] arcs already"
10292        return
10293    }
10294    set a [lindex $a 0]
10295    set i [lsearch -exact $arcids($a) $p]
10296    if {$i < 0} {
10297        puts "oops splitarc $p not in arc $a"
10298        return
10299    }
10300    set na [incr nextarc]
10301    if {[info exists arcend($a)]} {
10302        set arcend($na) $arcend($a)
10303    } else {
10304        set l [lindex $allparents([lindex $arcids($a) end]) 0]
10305        set j [lsearch -exact $arcnos($l) $a]
10306        set arcnos($l) [lreplace $arcnos($l) $j $j $na]
10307    }
10308    set tail [lrange $arcids($a) [expr {$i+1}] end]
10309    set arcids($a) [lrange $arcids($a) 0 $i]
10310    set arcend($a) $p
10311    set arcstart($na) $p
10312    set arcout($p) $na
10313    set arcids($na) $tail
10314    if {[info exists growing($a)]} {
10315        set growing($na) 1
10316        unset growing($a)
10317    }
10318
10319    foreach id $tail {
10320        if {[llength $arcnos($id)] == 1} {
10321            set arcnos($id) $na
10322        } else {
10323            set j [lsearch -exact $arcnos($id) $a]
10324            set arcnos($id) [lreplace $arcnos($id) $j $j $na]
10325        }
10326    }
10327
10328    # reconstruct tags and heads lists
10329    if {$arctags($a) ne {} || $archeads($a) ne {}} {
10330        recalcarc $a
10331        recalcarc $na
10332    } else {
10333        set arctags($na) {}
10334        set archeads($na) {}
10335    }
10336}
10337
10338# Update things for a new commit added that is a child of one
10339# existing commit.  Used when cherry-picking.
10340proc addnewchild {id p} {
10341    global allparents allchildren idtags nextarc
10342    global arcnos arcids arctags arcout arcend arcstart archeads growing
10343    global seeds allcommits
10344
10345    if {![info exists allcommits] || ![info exists arcnos($p)]} return
10346    set allparents($id) [list $p]
10347    set allchildren($id) {}
10348    set arcnos($id) {}
10349    lappend seeds $id
10350    lappend allchildren($p) $id
10351    set a [incr nextarc]
10352    set arcstart($a) $id
10353    set archeads($a) {}
10354    set arctags($a) {}
10355    set arcids($a) [list $p]
10356    set arcend($a) $p
10357    if {![info exists arcout($p)]} {
10358        splitarc $p
10359    }
10360    lappend arcnos($p) $a
10361    set arcout($id) [list $a]
10362}
10363
10364# This implements a cache for the topology information.
10365# The cache saves, for each arc, the start and end of the arc,
10366# the ids on the arc, and the outgoing arcs from the end.
10367proc readcache {f} {
10368    global arcnos arcids arcout arcstart arcend arctags archeads nextarc
10369    global idtags idheads allparents cachedarcs possible_seeds seeds growing
10370    global allcwait
10371
10372    set a $nextarc
10373    set lim $cachedarcs
10374    if {$lim - $a > 500} {
10375        set lim [expr {$a + 500}]
10376    }
10377    if {[catch {
10378        if {$a == $lim} {
10379            # finish reading the cache and setting up arctags, etc.
10380            set line [gets $f]
10381            if {$line ne "1"} {error "bad final version"}
10382            close $f
10383            foreach id [array names idtags] {
10384                if {[info exists arcnos($id)] && [llength $arcnos($id)] == 1 &&
10385                    [llength $allparents($id)] == 1} {
10386                    set a [lindex $arcnos($id) 0]
10387                    if {$arctags($a) eq {}} {
10388                        recalcarc $a
10389                    }
10390                }
10391            }
10392            foreach id [array names idheads] {
10393                if {[info exists arcnos($id)] && [llength $arcnos($id)] == 1 &&
10394                    [llength $allparents($id)] == 1} {
10395                    set a [lindex $arcnos($id) 0]
10396                    if {$archeads($a) eq {}} {
10397                        recalcarc $a
10398                    }
10399                }
10400            }
10401            foreach id [lsort -unique $possible_seeds] {
10402                if {$arcnos($id) eq {}} {
10403                    lappend seeds $id
10404                }
10405            }
10406            set allcwait 0
10407        } else {
10408            while {[incr a] <= $lim} {
10409                set line [gets $f]
10410                if {[llength $line] != 3} {error "bad line"}
10411                set s [lindex $line 0]
10412                set arcstart($a) $s
10413                lappend arcout($s) $a
10414                if {![info exists arcnos($s)]} {
10415                    lappend possible_seeds $s
10416                    set arcnos($s) {}
10417                }
10418                set e [lindex $line 1]
10419                if {$e eq {}} {
10420                    set growing($a) 1
10421                } else {
10422                    set arcend($a) $e
10423                    if {![info exists arcout($e)]} {
10424                        set arcout($e) {}
10425                    }
10426                }
10427                set arcids($a) [lindex $line 2]
10428                foreach id $arcids($a) {
10429                    lappend allparents($s) $id
10430                    set s $id
10431                    lappend arcnos($id) $a
10432                }
10433                if {![info exists allparents($s)]} {
10434                    set allparents($s) {}
10435                }
10436                set arctags($a) {}
10437                set archeads($a) {}
10438            }
10439            set nextarc [expr {$a - 1}]
10440        }
10441    } err]} {
10442        dropcache $err
10443        return 0
10444    }
10445    if {!$allcwait} {
10446        getallcommits
10447    }
10448    return $allcwait
10449}
10450
10451proc getcache {f} {
10452    global nextarc cachedarcs possible_seeds
10453
10454    if {[catch {
10455        set line [gets $f]
10456        if {[llength $line] != 2 || [lindex $line 0] ne "1"} {error "bad version"}
10457        # make sure it's an integer
10458        set cachedarcs [expr {int([lindex $line 1])}]
10459        if {$cachedarcs < 0} {error "bad number of arcs"}
10460        set nextarc 0
10461        set possible_seeds {}
10462        run readcache $f
10463    } err]} {
10464        dropcache $err
10465    }
10466    return 0
10467}
10468
10469proc dropcache {err} {
10470    global allcwait nextarc cachedarcs seeds
10471
10472    #puts "dropping cache ($err)"
10473    foreach v {arcnos arcout arcids arcstart arcend growing \
10474                   arctags archeads allparents allchildren} {
10475        global $v
10476        unset -nocomplain $v
10477    }
10478    set allcwait 0
10479    set nextarc 0
10480    set cachedarcs 0
10481    set seeds {}
10482    getallcommits
10483}
10484
10485proc writecache {f} {
10486    global cachearc cachedarcs allccache
10487    global arcstart arcend arcnos arcids arcout
10488
10489    set a $cachearc
10490    set lim $cachedarcs
10491    if {$lim - $a > 1000} {
10492        set lim [expr {$a + 1000}]
10493    }
10494    if {[catch {
10495        while {[incr a] <= $lim} {
10496            if {[info exists arcend($a)]} {
10497                puts $f [list $arcstart($a) $arcend($a) $arcids($a)]
10498            } else {
10499                puts $f [list $arcstart($a) {} $arcids($a)]
10500            }
10501        }
10502    } err]} {
10503        catch {close $f}
10504        catch {file delete $allccache}
10505        #puts "writing cache failed ($err)"
10506        return 0
10507    }
10508    set cachearc [expr {$a - 1}]
10509    if {$a > $cachedarcs} {
10510        puts $f "1"
10511        close $f
10512        return 0
10513    }
10514    return 1
10515}
10516
10517proc savecache {} {
10518    global nextarc cachedarcs cachearc allccache
10519
10520    if {$nextarc == $cachedarcs} return
10521    set cachearc 0
10522    set cachedarcs $nextarc
10523    catch {
10524        set f [open $allccache w]
10525        puts $f [list 1 $cachedarcs]
10526        run writecache $f
10527    }
10528}
10529
10530# Returns 1 if a is an ancestor of b, -1 if b is an ancestor of a,
10531# or 0 if neither is true.
10532proc anc_or_desc {a b} {
10533    global arcout arcstart arcend arcnos cached_isanc
10534
10535    if {$arcnos($a) eq $arcnos($b)} {
10536        # Both are on the same arc(s); either both are the same BMP,
10537        # or if one is not a BMP, the other is also not a BMP or is
10538        # the BMP at end of the arc (and it only has 1 incoming arc).
10539        # Or both can be BMPs with no incoming arcs.
10540        if {$a eq $b || $arcnos($a) eq {}} {
10541            return 0
10542        }
10543        # assert {[llength $arcnos($a)] == 1}
10544        set arc [lindex $arcnos($a) 0]
10545        set i [lsearch -exact $arcids($arc) $a]
10546        set j [lsearch -exact $arcids($arc) $b]
10547        if {$i < 0 || $i > $j} {
10548            return 1
10549        } else {
10550            return -1
10551        }
10552    }
10553
10554    if {![info exists arcout($a)]} {
10555        set arc [lindex $arcnos($a) 0]
10556        if {[info exists arcend($arc)]} {
10557            set aend $arcend($arc)
10558        } else {
10559            set aend {}
10560        }
10561        set a $arcstart($arc)
10562    } else {
10563        set aend $a
10564    }
10565    if {![info exists arcout($b)]} {
10566        set arc [lindex $arcnos($b) 0]
10567        if {[info exists arcend($arc)]} {
10568            set bend $arcend($arc)
10569        } else {
10570            set bend {}
10571        }
10572        set b $arcstart($arc)
10573    } else {
10574        set bend $b
10575    }
10576    if {$a eq $bend} {
10577        return 1
10578    }
10579    if {$b eq $aend} {
10580        return -1
10581    }
10582    if {[info exists cached_isanc($a,$bend)]} {
10583        if {$cached_isanc($a,$bend)} {
10584            return 1
10585        }
10586    }
10587    if {[info exists cached_isanc($b,$aend)]} {
10588        if {$cached_isanc($b,$aend)} {
10589            return -1
10590        }
10591        if {[info exists cached_isanc($a,$bend)]} {
10592            return 0
10593        }
10594    }
10595
10596    set todo [list $a $b]
10597    set anc($a) a
10598    set anc($b) b
10599    for {set i 0} {$i < [llength $todo]} {incr i} {
10600        set x [lindex $todo $i]
10601        if {$anc($x) eq {}} {
10602            continue
10603        }
10604        foreach arc $arcnos($x) {
10605            set xd $arcstart($arc)
10606            if {$xd eq $bend} {
10607                set cached_isanc($a,$bend) 1
10608                set cached_isanc($b,$aend) 0
10609                return 1
10610            } elseif {$xd eq $aend} {
10611                set cached_isanc($b,$aend) 1
10612                set cached_isanc($a,$bend) 0
10613                return -1
10614            }
10615            if {![info exists anc($xd)]} {
10616                set anc($xd) $anc($x)
10617                lappend todo $xd
10618            } elseif {$anc($xd) ne $anc($x)} {
10619                set anc($xd) {}
10620            }
10621        }
10622    }
10623    set cached_isanc($a,$bend) 0
10624    set cached_isanc($b,$aend) 0
10625    return 0
10626}
10627
10628# This identifies whether $desc has an ancestor that is
10629# a growing tip of the graph and which is not an ancestor of $anc
10630# and returns 0 if so and 1 if not.
10631# If we subsequently discover a tag on such a growing tip, and that
10632# turns out to be a descendent of $anc (which it could, since we
10633# don't necessarily see children before parents), then $desc
10634# isn't a good choice to display as a descendent tag of
10635# $anc (since it is the descendent of another tag which is
10636# a descendent of $anc).  Similarly, $anc isn't a good choice to
10637# display as a ancestor tag of $desc.
10638#
10639proc is_certain {desc anc} {
10640    global arcnos arcout arcstart arcend growing problems
10641
10642    set certain {}
10643    if {[llength $arcnos($anc)] == 1} {
10644        # tags on the same arc are certain
10645        if {$arcnos($desc) eq $arcnos($anc)} {
10646            return 1
10647        }
10648        if {![info exists arcout($anc)]} {
10649            # if $anc is partway along an arc, use the start of the arc instead
10650            set a [lindex $arcnos($anc) 0]
10651            set anc $arcstart($a)
10652        }
10653    }
10654    if {[llength $arcnos($desc)] > 1 || [info exists arcout($desc)]} {
10655        set x $desc
10656    } else {
10657        set a [lindex $arcnos($desc) 0]
10658        set x $arcend($a)
10659    }
10660    if {$x == $anc} {
10661        return 1
10662    }
10663    set anclist [list $x]
10664    set dl($x) 1
10665    set nnh 1
10666    set ngrowanc 0
10667    for {set i 0} {$i < [llength $anclist] && ($nnh > 0 || $ngrowanc > 0)} {incr i} {
10668        set x [lindex $anclist $i]
10669        if {$dl($x)} {
10670            incr nnh -1
10671        }
10672        set done($x) 1
10673        foreach a $arcout($x) {
10674            if {[info exists growing($a)]} {
10675                if {![info exists growanc($x)] && $dl($x)} {
10676                    set growanc($x) 1
10677                    incr ngrowanc
10678                }
10679            } else {
10680                set y $arcend($a)
10681                if {[info exists dl($y)]} {
10682                    if {$dl($y)} {
10683                        if {!$dl($x)} {
10684                            set dl($y) 0
10685                            if {![info exists done($y)]} {
10686                                incr nnh -1
10687                            }
10688                            if {[info exists growanc($x)]} {
10689                                incr ngrowanc -1
10690                            }
10691                            set xl [list $y]
10692                            for {set k 0} {$k < [llength $xl]} {incr k} {
10693                                set z [lindex $xl $k]
10694                                foreach c $arcout($z) {
10695                                    if {[info exists arcend($c)]} {
10696                                        set v $arcend($c)
10697                                        if {[info exists dl($v)] && $dl($v)} {
10698                                            set dl($v) 0
10699                                            if {![info exists done($v)]} {
10700                                                incr nnh -1
10701                                            }
10702                                            if {[info exists growanc($v)]} {
10703                                                incr ngrowanc -1
10704                                            }
10705                                            lappend xl $v
10706                                        }
10707                                    }
10708                                }
10709                            }
10710                        }
10711                    }
10712                } elseif {$y eq $anc || !$dl($x)} {
10713                    set dl($y) 0
10714                    lappend anclist $y
10715                } else {
10716                    set dl($y) 1
10717                    lappend anclist $y
10718                    incr nnh
10719                }
10720            }
10721        }
10722    }
10723    foreach x [array names growanc] {
10724        if {$dl($x)} {
10725            return 0
10726        }
10727        return 0
10728    }
10729    return 1
10730}
10731
10732proc validate_arctags {a} {
10733    global arctags idtags
10734
10735    set i -1
10736    set na $arctags($a)
10737    foreach id $arctags($a) {
10738        incr i
10739        if {![info exists idtags($id)]} {
10740            set na [lreplace $na $i $i]
10741            incr i -1
10742        }
10743    }
10744    set arctags($a) $na
10745}
10746
10747proc validate_archeads {a} {
10748    global archeads idheads
10749
10750    set i -1
10751    set na $archeads($a)
10752    foreach id $archeads($a) {
10753        incr i
10754        if {![info exists idheads($id)]} {
10755            set na [lreplace $na $i $i]
10756            incr i -1
10757        }
10758    }
10759    set archeads($a) $na
10760}
10761
10762# Return the list of IDs that have tags that are descendents of id,
10763# ignoring IDs that are descendents of IDs already reported.
10764proc desctags {id} {
10765    global arcnos arcstart arcids arctags idtags allparents
10766    global growing cached_dtags
10767
10768    if {![info exists allparents($id)]} {
10769        return {}
10770    }
10771    set t1 [clock clicks -milliseconds]
10772    set argid $id
10773    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
10774        # part-way along an arc; check that arc first
10775        set a [lindex $arcnos($id) 0]
10776        if {$arctags($a) ne {}} {
10777            validate_arctags $a
10778            set i [lsearch -exact $arcids($a) $id]
10779            set tid {}
10780            foreach t $arctags($a) {
10781                set j [lsearch -exact $arcids($a) $t]
10782                if {$j >= $i} break
10783                set tid $t
10784            }
10785            if {$tid ne {}} {
10786                return $tid
10787            }
10788        }
10789        set id $arcstart($a)
10790        if {[info exists idtags($id)]} {
10791            return $id
10792        }
10793    }
10794    if {[info exists cached_dtags($id)]} {
10795        return $cached_dtags($id)
10796    }
10797
10798    set origid $id
10799    set todo [list $id]
10800    set queued($id) 1
10801    set nc 1
10802    for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
10803        set id [lindex $todo $i]
10804        set done($id) 1
10805        set ta [info exists hastaggedancestor($id)]
10806        if {!$ta} {
10807            incr nc -1
10808        }
10809        # ignore tags on starting node
10810        if {!$ta && $i > 0} {
10811            if {[info exists idtags($id)]} {
10812                set tagloc($id) $id
10813                set ta 1
10814            } elseif {[info exists cached_dtags($id)]} {
10815                set tagloc($id) $cached_dtags($id)
10816                set ta 1
10817            }
10818        }
10819        foreach a $arcnos($id) {
10820            set d $arcstart($a)
10821            if {!$ta && $arctags($a) ne {}} {
10822                validate_arctags $a
10823                if {$arctags($a) ne {}} {
10824                    lappend tagloc($id) [lindex $arctags($a) end]
10825                }
10826            }
10827            if {$ta || $arctags($a) ne {}} {
10828                set tomark [list $d]
10829                for {set j 0} {$j < [llength $tomark]} {incr j} {
10830                    set dd [lindex $tomark $j]
10831                    if {![info exists hastaggedancestor($dd)]} {
10832                        if {[info exists done($dd)]} {
10833                            foreach b $arcnos($dd) {
10834                                lappend tomark $arcstart($b)
10835                            }
10836                            if {[info exists tagloc($dd)]} {
10837                                unset tagloc($dd)
10838                            }
10839                        } elseif {[info exists queued($dd)]} {
10840                            incr nc -1
10841                        }
10842                        set hastaggedancestor($dd) 1
10843                    }
10844                }
10845            }
10846            if {![info exists queued($d)]} {
10847                lappend todo $d
10848                set queued($d) 1
10849                if {![info exists hastaggedancestor($d)]} {
10850                    incr nc
10851                }
10852            }
10853        }
10854    }
10855    set tags {}
10856    foreach id [array names tagloc] {
10857        if {![info exists hastaggedancestor($id)]} {
10858            foreach t $tagloc($id) {
10859                if {[lsearch -exact $tags $t] < 0} {
10860                    lappend tags $t
10861                }
10862            }
10863        }
10864    }
10865    set t2 [clock clicks -milliseconds]
10866    set loopix $i
10867
10868    # remove tags that are descendents of other tags
10869    for {set i 0} {$i < [llength $tags]} {incr i} {
10870        set a [lindex $tags $i]
10871        for {set j 0} {$j < $i} {incr j} {
10872            set b [lindex $tags $j]
10873            set r [anc_or_desc $a $b]
10874            if {$r == 1} {
10875                set tags [lreplace $tags $j $j]
10876                incr j -1
10877                incr i -1
10878            } elseif {$r == -1} {
10879                set tags [lreplace $tags $i $i]
10880                incr i -1
10881                break
10882            }
10883        }
10884    }
10885
10886    if {[array names growing] ne {}} {
10887        # graph isn't finished, need to check if any tag could get
10888        # eclipsed by another tag coming later.  Simply ignore any
10889        # tags that could later get eclipsed.
10890        set ctags {}
10891        foreach t $tags {
10892            if {[is_certain $t $origid]} {
10893                lappend ctags $t
10894            }
10895        }
10896        if {$tags eq $ctags} {
10897            set cached_dtags($origid) $tags
10898        } else {
10899            set tags $ctags
10900        }
10901    } else {
10902        set cached_dtags($origid) $tags
10903    }
10904    set t3 [clock clicks -milliseconds]
10905    if {0 && $t3 - $t1 >= 100} {
10906        puts "iterating descendents ($loopix/[llength $todo] nodes) took\
10907            [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
10908    }
10909    return $tags
10910}
10911
10912proc anctags {id} {
10913    global arcnos arcids arcout arcend arctags idtags allparents
10914    global growing cached_atags
10915
10916    if {![info exists allparents($id)]} {
10917        return {}
10918    }
10919    set t1 [clock clicks -milliseconds]
10920    set argid $id
10921    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
10922        # part-way along an arc; check that arc first
10923        set a [lindex $arcnos($id) 0]
10924        if {$arctags($a) ne {}} {
10925            validate_arctags $a
10926            set i [lsearch -exact $arcids($a) $id]
10927            foreach t $arctags($a) {
10928                set j [lsearch -exact $arcids($a) $t]
10929                if {$j > $i} {
10930                    return $t
10931                }
10932            }
10933        }
10934        if {![info exists arcend($a)]} {
10935            return {}
10936        }
10937        set id $arcend($a)
10938        if {[info exists idtags($id)]} {
10939            return $id
10940        }
10941    }
10942    if {[info exists cached_atags($id)]} {
10943        return $cached_atags($id)
10944    }
10945
10946    set origid $id
10947    set todo [list $id]
10948    set queued($id) 1
10949    set taglist {}
10950    set nc 1
10951    for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
10952        set id [lindex $todo $i]
10953        set done($id) 1
10954        set td [info exists hastaggeddescendent($id)]
10955        if {!$td} {
10956            incr nc -1
10957        }
10958        # ignore tags on starting node
10959        if {!$td && $i > 0} {
10960            if {[info exists idtags($id)]} {
10961                set tagloc($id) $id
10962                set td 1
10963            } elseif {[info exists cached_atags($id)]} {
10964                set tagloc($id) $cached_atags($id)
10965                set td 1
10966            }
10967        }
10968        foreach a $arcout($id) {
10969            if {!$td && $arctags($a) ne {}} {
10970                validate_arctags $a
10971                if {$arctags($a) ne {}} {
10972                    lappend tagloc($id) [lindex $arctags($a) 0]
10973                }
10974            }
10975            if {![info exists arcend($a)]} continue
10976            set d $arcend($a)
10977            if {$td || $arctags($a) ne {}} {
10978                set tomark [list $d]
10979                for {set j 0} {$j < [llength $tomark]} {incr j} {
10980                    set dd [lindex $tomark $j]
10981                    if {![info exists hastaggeddescendent($dd)]} {
10982                        if {[info exists done($dd)]} {
10983                            foreach b $arcout($dd) {
10984                                if {[info exists arcend($b)]} {
10985                                    lappend tomark $arcend($b)
10986                                }
10987                            }
10988                            if {[info exists tagloc($dd)]} {
10989                                unset tagloc($dd)
10990                            }
10991                        } elseif {[info exists queued($dd)]} {
10992                            incr nc -1
10993                        }
10994                        set hastaggeddescendent($dd) 1
10995                    }
10996                }
10997            }
10998            if {![info exists queued($d)]} {
10999                lappend todo $d
11000                set queued($d) 1
11001                if {![info exists hastaggeddescendent($d)]} {
11002                    incr nc
11003                }
11004            }
11005        }
11006    }
11007    set t2 [clock clicks -milliseconds]
11008    set loopix $i
11009    set tags {}
11010    foreach id [array names tagloc] {
11011        if {![info exists hastaggeddescendent($id)]} {
11012            foreach t $tagloc($id) {
11013                if {[lsearch -exact $tags $t] < 0} {
11014                    lappend tags $t
11015                }
11016            }
11017        }
11018    }
11019
11020    # remove tags that are ancestors of other tags
11021    for {set i 0} {$i < [llength $tags]} {incr i} {
11022        set a [lindex $tags $i]
11023        for {set j 0} {$j < $i} {incr j} {
11024            set b [lindex $tags $j]
11025            set r [anc_or_desc $a $b]
11026            if {$r == -1} {
11027                set tags [lreplace $tags $j $j]
11028                incr j -1
11029                incr i -1
11030            } elseif {$r == 1} {
11031                set tags [lreplace $tags $i $i]
11032                incr i -1
11033                break
11034            }
11035        }
11036    }
11037
11038    if {[array names growing] ne {}} {
11039        # graph isn't finished, need to check if any tag could get
11040        # eclipsed by another tag coming later.  Simply ignore any
11041        # tags that could later get eclipsed.
11042        set ctags {}
11043        foreach t $tags {
11044            if {[is_certain $origid $t]} {
11045                lappend ctags $t
11046            }
11047        }
11048        if {$tags eq $ctags} {
11049            set cached_atags($origid) $tags
11050        } else {
11051            set tags $ctags
11052        }
11053    } else {
11054        set cached_atags($origid) $tags
11055    }
11056    set t3 [clock clicks -milliseconds]
11057    if {0 && $t3 - $t1 >= 100} {
11058        puts "iterating ancestors ($loopix/[llength $todo] nodes) took\
11059            [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
11060    }
11061    return $tags
11062}
11063
11064# Return the list of IDs that have heads that are descendents of id,
11065# including id itself if it has a head.
11066proc descheads {id} {
11067    global arcnos arcstart arcids archeads idheads cached_dheads
11068    global allparents arcout
11069
11070    if {![info exists allparents($id)]} {
11071        return {}
11072    }
11073    set aret {}
11074    if {![info exists arcout($id)]} {
11075        # part-way along an arc; check it first
11076        set a [lindex $arcnos($id) 0]
11077        if {$archeads($a) ne {}} {
11078            validate_archeads $a
11079            set i [lsearch -exact $arcids($a) $id]
11080            foreach t $archeads($a) {
11081                set j [lsearch -exact $arcids($a) $t]
11082                if {$j > $i} break
11083                lappend aret $t
11084            }
11085        }
11086        set id $arcstart($a)
11087    }
11088    set origid $id
11089    set todo [list $id]
11090    set seen($id) 1
11091    set ret {}
11092    for {set i 0} {$i < [llength $todo]} {incr i} {
11093        set id [lindex $todo $i]
11094        if {[info exists cached_dheads($id)]} {
11095            set ret [concat $ret $cached_dheads($id)]
11096        } else {
11097            if {[info exists idheads($id)]} {
11098                lappend ret $id
11099            }
11100            foreach a $arcnos($id) {
11101                if {$archeads($a) ne {}} {
11102                    validate_archeads $a
11103                    if {$archeads($a) ne {}} {
11104                        set ret [concat $ret $archeads($a)]
11105                    }
11106                }
11107                set d $arcstart($a)
11108                if {![info exists seen($d)]} {
11109                    lappend todo $d
11110                    set seen($d) 1
11111                }
11112            }
11113        }
11114    }
11115    set ret [lsort -unique $ret]
11116    set cached_dheads($origid) $ret
11117    return [concat $ret $aret]
11118}
11119
11120proc addedtag {id} {
11121    global arcnos arcout cached_dtags cached_atags
11122
11123    if {![info exists arcnos($id)]} return
11124    if {![info exists arcout($id)]} {
11125        recalcarc [lindex $arcnos($id) 0]
11126    }
11127    unset -nocomplain cached_dtags
11128    unset -nocomplain cached_atags
11129}
11130
11131proc addedhead {hid head} {
11132    global arcnos arcout cached_dheads
11133
11134    if {![info exists arcnos($hid)]} return
11135    if {![info exists arcout($hid)]} {
11136        recalcarc [lindex $arcnos($hid) 0]
11137    }
11138    unset -nocomplain cached_dheads
11139}
11140
11141proc removedhead {hid head} {
11142    global cached_dheads
11143
11144    unset -nocomplain cached_dheads
11145}
11146
11147proc movedhead {hid head} {
11148    global arcnos arcout cached_dheads
11149
11150    if {![info exists arcnos($hid)]} return
11151    if {![info exists arcout($hid)]} {
11152        recalcarc [lindex $arcnos($hid) 0]
11153    }
11154    unset -nocomplain cached_dheads
11155}
11156
11157proc changedrefs {} {
11158    global cached_dheads cached_dtags cached_atags cached_tagcontent
11159    global arctags archeads arcnos arcout idheads idtags
11160
11161    foreach id [concat [array names idheads] [array names idtags]] {
11162        if {[info exists arcnos($id)] && ![info exists arcout($id)]} {
11163            set a [lindex $arcnos($id) 0]
11164            if {![info exists donearc($a)]} {
11165                recalcarc $a
11166                set donearc($a) 1
11167            }
11168        }
11169    }
11170    unset -nocomplain cached_tagcontent
11171    unset -nocomplain cached_dtags
11172    unset -nocomplain cached_atags
11173    unset -nocomplain cached_dheads
11174}
11175
11176proc rereadrefs {} {
11177    global idtags idheads idotherrefs mainheadid
11178
11179    set refids [concat [array names idtags] \
11180                    [array names idheads] [array names idotherrefs]]
11181    foreach id $refids {
11182        if {![info exists ref($id)]} {
11183            set ref($id) [listrefs $id]
11184        }
11185    }
11186    set oldmainhead $mainheadid
11187    readrefs
11188    changedrefs
11189    set refids [lsort -unique [concat $refids [array names idtags] \
11190                        [array names idheads] [array names idotherrefs]]]
11191    foreach id $refids {
11192        set v [listrefs $id]
11193        if {![info exists ref($id)] || $ref($id) != $v} {
11194            redrawtags $id
11195        }
11196    }
11197    if {$oldmainhead ne $mainheadid} {
11198        redrawtags $oldmainhead
11199        redrawtags $mainheadid
11200    }
11201    run refill_reflist
11202}
11203
11204proc listrefs {id} {
11205    global idtags idheads idotherrefs
11206
11207    set x {}
11208    if {[info exists idtags($id)]} {
11209        set x $idtags($id)
11210    }
11211    set y {}
11212    if {[info exists idheads($id)]} {
11213        set y $idheads($id)
11214    }
11215    set z {}
11216    if {[info exists idotherrefs($id)]} {
11217        set z $idotherrefs($id)
11218    }
11219    return [list $x $y $z]
11220}
11221
11222proc add_tag_ctext {tag} {
11223    global ctext cached_tagcontent tagids
11224
11225    if {![info exists cached_tagcontent($tag)]} {
11226        catch {
11227            set cached_tagcontent($tag) [exec git cat-file -p $tag]
11228        }
11229    }
11230    $ctext insert end "[mc "Tag"]: $tag\n" bold
11231    if {[info exists cached_tagcontent($tag)]} {
11232        set text $cached_tagcontent($tag)
11233    } else {
11234        set text "[mc "Id"]:  $tagids($tag)"
11235    }
11236    appendwithlinks $text {}
11237}
11238
11239proc showtag {tag isnew} {
11240    global ctext cached_tagcontent tagids linknum tagobjid
11241
11242    if {$isnew} {
11243        addtohistory [list showtag $tag 0] savectextpos
11244    }
11245    $ctext conf -state normal
11246    clear_ctext
11247    settabs 0
11248    set linknum 0
11249    add_tag_ctext $tag
11250    maybe_scroll_ctext 1
11251    $ctext conf -state disabled
11252    init_flist {}
11253}
11254
11255proc showtags {id isnew} {
11256    global idtags ctext linknum
11257
11258    if {$isnew} {
11259        addtohistory [list showtags $id 0] savectextpos
11260    }
11261    $ctext conf -state normal
11262    clear_ctext
11263    settabs 0
11264    set linknum 0
11265    set sep {}
11266    foreach tag $idtags($id) {
11267        $ctext insert end $sep
11268        add_tag_ctext $tag
11269        set sep "\n\n"
11270    }
11271    maybe_scroll_ctext 1
11272    $ctext conf -state disabled
11273    init_flist {}
11274}
11275
11276proc doquit {} {
11277    global stopped
11278    global gitktmpdir
11279
11280    set stopped 100
11281    savestuff .
11282    destroy .
11283
11284    if {[info exists gitktmpdir]} {
11285        catch {file delete -force $gitktmpdir}
11286    }
11287}
11288
11289proc mkfontdisp {font top which} {
11290    global fontattr fontpref $font NS use_ttk
11291
11292    set fontpref($font) [set $font]
11293    ${NS}::button $top.${font}but -text $which \
11294        -command [list choosefont $font $which]
11295    ${NS}::label $top.$font -relief flat -font $font \
11296        -text $fontattr($font,family) -justify left
11297    grid x $top.${font}but $top.$font -sticky w
11298}
11299
11300proc choosefont {font which} {
11301    global fontparam fontlist fonttop fontattr
11302    global prefstop NS
11303
11304    set fontparam(which) $which
11305    set fontparam(font) $font
11306    set fontparam(family) [font actual $font -family]
11307    set fontparam(size) $fontattr($font,size)
11308    set fontparam(weight) $fontattr($font,weight)
11309    set fontparam(slant) $fontattr($font,slant)
11310    set top .gitkfont
11311    set fonttop $top
11312    if {![winfo exists $top]} {
11313        font create sample
11314        eval font config sample [font actual $font]
11315        ttk_toplevel $top
11316        make_transient $top $prefstop
11317        wm title $top [mc "Gitk font chooser"]
11318        ${NS}::label $top.l -textvariable fontparam(which)
11319        pack $top.l -side top
11320        set fontlist [lsort [font families]]
11321        ${NS}::frame $top.f
11322        listbox $top.f.fam -listvariable fontlist \
11323            -yscrollcommand [list $top.f.sb set]
11324        bind $top.f.fam <<ListboxSelect>> selfontfam
11325        ${NS}::scrollbar $top.f.sb -command [list $top.f.fam yview]
11326        pack $top.f.sb -side right -fill y
11327        pack $top.f.fam -side left -fill both -expand 1
11328        pack $top.f -side top -fill both -expand 1
11329        ${NS}::frame $top.g
11330        spinbox $top.g.size -from 4 -to 40 -width 4 \
11331            -textvariable fontparam(size) \
11332            -validatecommand {string is integer -strict %s}
11333        checkbutton $top.g.bold -padx 5 \
11334            -font {{Times New Roman} 12 bold} -text [mc "B"] -indicatoron 0 \
11335            -variable fontparam(weight) -onvalue bold -offvalue normal
11336        checkbutton $top.g.ital -padx 5 \
11337            -font {{Times New Roman} 12 italic} -text [mc "I"] -indicatoron 0  \
11338            -variable fontparam(slant) -onvalue italic -offvalue roman
11339        pack $top.g.size $top.g.bold $top.g.ital -side left
11340        pack $top.g -side top
11341        canvas $top.c -width 150 -height 50 -border 2 -relief sunk \
11342            -background white
11343        $top.c create text 100 25 -anchor center -text $which -font sample \
11344            -fill black -tags text
11345        bind $top.c <Configure> [list centertext $top.c]
11346        pack $top.c -side top -fill x
11347        ${NS}::frame $top.buts
11348        ${NS}::button $top.buts.ok -text [mc "OK"] -command fontok -default active
11349        ${NS}::button $top.buts.can -text [mc "Cancel"] -command fontcan -default normal
11350        bind $top <Key-Return> fontok
11351        bind $top <Key-Escape> fontcan
11352        grid $top.buts.ok $top.buts.can
11353        grid columnconfigure $top.buts 0 -weight 1 -uniform a
11354        grid columnconfigure $top.buts 1 -weight 1 -uniform a
11355        pack $top.buts -side bottom -fill x
11356        trace add variable fontparam write chg_fontparam
11357    } else {
11358        raise $top
11359        $top.c itemconf text -text $which
11360    }
11361    set i [lsearch -exact $fontlist $fontparam(family)]
11362    if {$i >= 0} {
11363        $top.f.fam selection set $i
11364        $top.f.fam see $i
11365    }
11366}
11367
11368proc centertext {w} {
11369    $w coords text [expr {[winfo width $w] / 2}] [expr {[winfo height $w] / 2}]
11370}
11371
11372proc fontok {} {
11373    global fontparam fontpref prefstop
11374
11375    set f $fontparam(font)
11376    set fontpref($f) [list $fontparam(family) $fontparam(size)]
11377    if {$fontparam(weight) eq "bold"} {
11378        lappend fontpref($f) "bold"
11379    }
11380    if {$fontparam(slant) eq "italic"} {
11381        lappend fontpref($f) "italic"
11382    }
11383    set w $prefstop.notebook.fonts.$f
11384    $w conf -text $fontparam(family) -font $fontpref($f)
11385
11386    fontcan
11387}
11388
11389proc fontcan {} {
11390    global fonttop fontparam
11391
11392    if {[info exists fonttop]} {
11393        catch {destroy $fonttop}
11394        catch {font delete sample}
11395        unset fonttop
11396        unset fontparam
11397    }
11398}
11399
11400if {[package vsatisfies [package provide Tk] 8.6]} {
11401    # In Tk 8.6 we have a native font chooser dialog. Overwrite the above
11402    # function to make use of it.
11403    proc choosefont {font which} {
11404        tk fontchooser configure -title $which -font $font \
11405            -command [list on_choosefont $font $which]
11406        tk fontchooser show
11407    }
11408    proc on_choosefont {font which newfont} {
11409        global fontparam
11410        puts stderr "$font $newfont"
11411        array set f [font actual $newfont]
11412        set fontparam(which) $which
11413        set fontparam(font) $font
11414        set fontparam(family) $f(-family)
11415        set fontparam(size) $f(-size)
11416        set fontparam(weight) $f(-weight)
11417        set fontparam(slant) $f(-slant)
11418        fontok
11419    }
11420}
11421
11422proc selfontfam {} {
11423    global fonttop fontparam
11424
11425    set i [$fonttop.f.fam curselection]
11426    if {$i ne {}} {
11427        set fontparam(family) [$fonttop.f.fam get $i]
11428    }
11429}
11430
11431proc chg_fontparam {v sub op} {
11432    global fontparam
11433
11434    font config sample -$sub $fontparam($sub)
11435}
11436
11437# Create a property sheet tab page
11438proc create_prefs_page {w} {
11439    global NS
11440    set parent [join [lrange [split $w .] 0 end-1] .]
11441    if {[winfo class $parent] eq "TNotebook"} {
11442        ${NS}::frame $w
11443    } else {
11444        ${NS}::labelframe $w
11445    }
11446}
11447
11448proc prefspage_general {notebook} {
11449    global NS maxwidth maxgraphpct showneartags showlocalchanges
11450    global tabstop limitdiffs autoselect autosellen extdifftool perfile_attrs
11451    global hideremotes want_ttk have_ttk maxrefs
11452
11453    set page [create_prefs_page $notebook.general]
11454
11455    ${NS}::label $page.ldisp -text [mc "Commit list display options"]
11456    grid $page.ldisp - -sticky w -pady 10
11457    ${NS}::label $page.spacer -text " "
11458    ${NS}::label $page.maxwidthl -text [mc "Maximum graph width (lines)"]
11459    spinbox $page.maxwidth -from 0 -to 100 -width 4 -textvariable maxwidth
11460    grid $page.spacer $page.maxwidthl $page.maxwidth -sticky w
11461                                         #xgettext:no-tcl-format
11462    ${NS}::label $page.maxpctl -text [mc "Maximum graph width (% of pane)"]
11463    spinbox $page.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct
11464    grid x $page.maxpctl $page.maxpct -sticky w
11465    ${NS}::checkbutton $page.showlocal -text [mc "Show local changes"] \
11466        -variable showlocalchanges
11467    grid x $page.showlocal -sticky w
11468    ${NS}::checkbutton $page.autoselect -text [mc "Auto-select SHA1 (length)"] \
11469        -variable autoselect
11470    spinbox $page.autosellen -from 1 -to 40 -width 4 -textvariable autosellen
11471    grid x $page.autoselect $page.autosellen -sticky w
11472    ${NS}::checkbutton $page.hideremotes -text [mc "Hide remote refs"] \
11473        -variable hideremotes
11474    grid x $page.hideremotes -sticky w
11475
11476    ${NS}::label $page.ddisp -text [mc "Diff display options"]
11477    grid $page.ddisp - -sticky w -pady 10
11478    ${NS}::label $page.tabstopl -text [mc "Tab spacing"]
11479    spinbox $page.tabstop -from 1 -to 20 -width 4 -textvariable tabstop
11480    grid x $page.tabstopl $page.tabstop -sticky w
11481    ${NS}::checkbutton $page.ntag -text [mc "Display nearby tags/heads"] \
11482        -variable showneartags
11483    grid x $page.ntag -sticky w
11484    ${NS}::label $page.maxrefsl -text [mc "Maximum # tags/heads to show"]
11485    spinbox $page.maxrefs -from 1 -to 1000 -width 4 -textvariable maxrefs
11486    grid x $page.maxrefsl $page.maxrefs -sticky w
11487    ${NS}::checkbutton $page.ldiff -text [mc "Limit diffs to listed paths"] \
11488        -variable limitdiffs
11489    grid x $page.ldiff -sticky w
11490    ${NS}::checkbutton $page.lattr -text [mc "Support per-file encodings"] \
11491        -variable perfile_attrs
11492    grid x $page.lattr -sticky w
11493
11494    ${NS}::entry $page.extdifft -textvariable extdifftool
11495    ${NS}::frame $page.extdifff
11496    ${NS}::label $page.extdifff.l -text [mc "External diff tool" ]
11497    ${NS}::button $page.extdifff.b -text [mc "Choose..."] -command choose_extdiff
11498    pack $page.extdifff.l $page.extdifff.b -side left
11499    pack configure $page.extdifff.l -padx 10
11500    grid x $page.extdifff $page.extdifft -sticky ew
11501
11502    ${NS}::label $page.lgen -text [mc "General options"]
11503    grid $page.lgen - -sticky w -pady 10
11504    ${NS}::checkbutton $page.want_ttk -variable want_ttk \
11505        -text [mc "Use themed widgets"]
11506    if {$have_ttk} {
11507        ${NS}::label $page.ttk_note -text [mc "(change requires restart)"]
11508    } else {
11509        ${NS}::label $page.ttk_note -text [mc "(currently unavailable)"]
11510    }
11511    grid x $page.want_ttk $page.ttk_note -sticky w
11512    return $page
11513}
11514
11515proc prefspage_colors {notebook} {
11516    global NS uicolor bgcolor fgcolor ctext diffcolors selectbgcolor markbgcolor
11517
11518    set page [create_prefs_page $notebook.colors]
11519
11520    ${NS}::label $page.cdisp -text [mc "Colors: press to choose"]
11521    grid $page.cdisp - -sticky w -pady 10
11522    label $page.ui -padx 40 -relief sunk -background $uicolor
11523    ${NS}::button $page.uibut -text [mc "Interface"] \
11524       -command [list choosecolor uicolor {} $page.ui [mc "interface"] setui]
11525    grid x $page.uibut $page.ui -sticky w
11526    label $page.bg -padx 40 -relief sunk -background $bgcolor
11527    ${NS}::button $page.bgbut -text [mc "Background"] \
11528        -command [list choosecolor bgcolor {} $page.bg [mc "background"] setbg]
11529    grid x $page.bgbut $page.bg -sticky w
11530    label $page.fg -padx 40 -relief sunk -background $fgcolor
11531    ${NS}::button $page.fgbut -text [mc "Foreground"] \
11532        -command [list choosecolor fgcolor {} $page.fg [mc "foreground"] setfg]
11533    grid x $page.fgbut $page.fg -sticky w
11534    label $page.diffold -padx 40 -relief sunk -background [lindex $diffcolors 0]
11535    ${NS}::button $page.diffoldbut -text [mc "Diff: old lines"] \
11536        -command [list choosecolor diffcolors 0 $page.diffold [mc "diff old lines"] \
11537                      [list $ctext tag conf d0 -foreground]]
11538    grid x $page.diffoldbut $page.diffold -sticky w
11539    label $page.diffnew -padx 40 -relief sunk -background [lindex $diffcolors 1]
11540    ${NS}::button $page.diffnewbut -text [mc "Diff: new lines"] \
11541        -command [list choosecolor diffcolors 1 $page.diffnew [mc "diff new lines"] \
11542                      [list $ctext tag conf dresult -foreground]]
11543    grid x $page.diffnewbut $page.diffnew -sticky w
11544    label $page.hunksep -padx 40 -relief sunk -background [lindex $diffcolors 2]
11545    ${NS}::button $page.hunksepbut -text [mc "Diff: hunk header"] \
11546        -command [list choosecolor diffcolors 2 $page.hunksep \
11547                      [mc "diff hunk header"] \
11548                      [list $ctext tag conf hunksep -foreground]]
11549    grid x $page.hunksepbut $page.hunksep -sticky w
11550    label $page.markbgsep -padx 40 -relief sunk -background $markbgcolor
11551    ${NS}::button $page.markbgbut -text [mc "Marked line bg"] \
11552        -command [list choosecolor markbgcolor {} $page.markbgsep \
11553                      [mc "marked line background"] \
11554                      [list $ctext tag conf omark -background]]
11555    grid x $page.markbgbut $page.markbgsep -sticky w
11556    label $page.selbgsep -padx 40 -relief sunk -background $selectbgcolor
11557    ${NS}::button $page.selbgbut -text [mc "Select bg"] \
11558        -command [list choosecolor selectbgcolor {} $page.selbgsep [mc "background"] setselbg]
11559    grid x $page.selbgbut $page.selbgsep -sticky w
11560    return $page
11561}
11562
11563proc prefspage_fonts {notebook} {
11564    global NS
11565    set page [create_prefs_page $notebook.fonts]
11566    ${NS}::label $page.cfont -text [mc "Fonts: press to choose"]
11567    grid $page.cfont - -sticky w -pady 10
11568    mkfontdisp mainfont $page [mc "Main font"]
11569    mkfontdisp textfont $page [mc "Diff display font"]
11570    mkfontdisp uifont $page [mc "User interface font"]
11571    return $page
11572}
11573
11574proc doprefs {} {
11575    global maxwidth maxgraphpct use_ttk NS
11576    global oldprefs prefstop showneartags showlocalchanges
11577    global uicolor bgcolor fgcolor ctext diffcolors selectbgcolor markbgcolor
11578    global tabstop limitdiffs autoselect autosellen extdifftool perfile_attrs
11579    global hideremotes want_ttk have_ttk
11580
11581    set top .gitkprefs
11582    set prefstop $top
11583    if {[winfo exists $top]} {
11584        raise $top
11585        return
11586    }
11587    foreach v {maxwidth maxgraphpct showneartags showlocalchanges \
11588                   limitdiffs tabstop perfile_attrs hideremotes want_ttk} {
11589        set oldprefs($v) [set $v]
11590    }
11591    ttk_toplevel $top
11592    wm title $top [mc "Gitk preferences"]
11593    make_transient $top .
11594
11595    if {[set use_notebook [expr {$use_ttk && [info command ::ttk::notebook] ne ""}]]} {
11596        set notebook [ttk::notebook $top.notebook]
11597    } else {
11598        set notebook [${NS}::frame $top.notebook -borderwidth 0 -relief flat]
11599    }
11600
11601    lappend pages [prefspage_general $notebook] [mc "General"]
11602    lappend pages [prefspage_colors $notebook] [mc "Colors"]
11603    lappend pages [prefspage_fonts $notebook] [mc "Fonts"]
11604    set col 0
11605    foreach {page title} $pages {
11606        if {$use_notebook} {
11607            $notebook add $page -text $title
11608        } else {
11609            set btn [${NS}::button $notebook.b_[string map {. X} $page] \
11610                         -text $title -command [list raise $page]]
11611            $page configure -text $title
11612            grid $btn -row 0 -column [incr col] -sticky w
11613            grid $page -row 1 -column 0 -sticky news -columnspan 100
11614        }
11615    }
11616
11617    if {!$use_notebook} {
11618        grid columnconfigure $notebook 0 -weight 1
11619        grid rowconfigure $notebook 1 -weight 1
11620        raise [lindex $pages 0]
11621    }
11622
11623    grid $notebook -sticky news -padx 2 -pady 2
11624    grid rowconfigure $top 0 -weight 1
11625    grid columnconfigure $top 0 -weight 1
11626
11627    ${NS}::frame $top.buts
11628    ${NS}::button $top.buts.ok -text [mc "OK"] -command prefsok -default active
11629    ${NS}::button $top.buts.can -text [mc "Cancel"] -command prefscan -default normal
11630    bind $top <Key-Return> prefsok
11631    bind $top <Key-Escape> prefscan
11632    grid $top.buts.ok $top.buts.can
11633    grid columnconfigure $top.buts 0 -weight 1 -uniform a
11634    grid columnconfigure $top.buts 1 -weight 1 -uniform a
11635    grid $top.buts - - -pady 10 -sticky ew
11636    grid columnconfigure $top 2 -weight 1
11637    bind $top <Visibility> [list focus $top.buts.ok]
11638}
11639
11640proc choose_extdiff {} {
11641    global extdifftool
11642
11643    set prog [tk_getOpenFile -title [mc "External diff tool"] -multiple false]
11644    if {$prog ne {}} {
11645        set extdifftool $prog
11646    }
11647}
11648
11649proc choosecolor {v vi w x cmd} {
11650    global $v
11651
11652    set c [tk_chooseColor -initialcolor [lindex [set $v] $vi] \
11653               -title [mc "Gitk: choose color for %s" $x]]
11654    if {$c eq {}} return
11655    $w conf -background $c
11656    lset $v $vi $c
11657    eval $cmd $c
11658}
11659
11660proc setselbg {c} {
11661    global bglist cflist
11662    foreach w $bglist {
11663        if {[winfo exists $w]} {
11664            $w configure -selectbackground $c
11665        }
11666    }
11667    $cflist tag configure highlight \
11668        -background [$cflist cget -selectbackground]
11669    allcanvs itemconf secsel -fill $c
11670}
11671
11672# This sets the background color and the color scheme for the whole UI.
11673# For some reason, tk_setPalette chooses a nasty dark red for selectColor
11674# if we don't specify one ourselves, which makes the checkbuttons and
11675# radiobuttons look bad.  This chooses white for selectColor if the
11676# background color is light, or black if it is dark.
11677proc setui {c} {
11678    if {[tk windowingsystem] eq "win32"} { return }
11679    set bg [winfo rgb . $c]
11680    set selc black
11681    if {[lindex $bg 0] + 1.5 * [lindex $bg 1] + 0.5 * [lindex $bg 2] > 100000} {
11682        set selc white
11683    }
11684    tk_setPalette background $c selectColor $selc
11685}
11686
11687proc setbg {c} {
11688    global bglist
11689
11690    foreach w $bglist {
11691        if {[winfo exists $w]} {
11692            $w conf -background $c
11693        }
11694    }
11695}
11696
11697proc setfg {c} {
11698    global fglist canv
11699
11700    foreach w $fglist {
11701        if {[winfo exists $w]} {
11702            $w conf -foreground $c
11703        }
11704    }
11705    allcanvs itemconf text -fill $c
11706    $canv itemconf circle -outline $c
11707    $canv itemconf markid -outline $c
11708}
11709
11710proc prefscan {} {
11711    global oldprefs prefstop
11712
11713    foreach v {maxwidth maxgraphpct showneartags showlocalchanges \
11714                   limitdiffs tabstop perfile_attrs hideremotes want_ttk} {
11715        global $v
11716        set $v $oldprefs($v)
11717    }
11718    catch {destroy $prefstop}
11719    unset prefstop
11720    fontcan
11721}
11722
11723proc prefsok {} {
11724    global maxwidth maxgraphpct
11725    global oldprefs prefstop showneartags showlocalchanges
11726    global fontpref mainfont textfont uifont
11727    global limitdiffs treediffs perfile_attrs
11728    global hideremotes
11729
11730    catch {destroy $prefstop}
11731    unset prefstop
11732    fontcan
11733    set fontchanged 0
11734    if {$mainfont ne $fontpref(mainfont)} {
11735        set mainfont $fontpref(mainfont)
11736        parsefont mainfont $mainfont
11737        eval font configure mainfont [fontflags mainfont]
11738        eval font configure mainfontbold [fontflags mainfont 1]
11739        setcoords
11740        set fontchanged 1
11741    }
11742    if {$textfont ne $fontpref(textfont)} {
11743        set textfont $fontpref(textfont)
11744        parsefont textfont $textfont
11745        eval font configure textfont [fontflags textfont]
11746        eval font configure textfontbold [fontflags textfont 1]
11747    }
11748    if {$uifont ne $fontpref(uifont)} {
11749        set uifont $fontpref(uifont)
11750        parsefont uifont $uifont
11751        eval font configure uifont [fontflags uifont]
11752    }
11753    settabs
11754    if {$showlocalchanges != $oldprefs(showlocalchanges)} {
11755        if {$showlocalchanges} {
11756            doshowlocalchanges
11757        } else {
11758            dohidelocalchanges
11759        }
11760    }
11761    if {$limitdiffs != $oldprefs(limitdiffs) ||
11762        ($perfile_attrs && !$oldprefs(perfile_attrs))} {
11763        # treediffs elements are limited by path;
11764        # won't have encodings cached if perfile_attrs was just turned on
11765        unset -nocomplain treediffs
11766    }
11767    if {$fontchanged || $maxwidth != $oldprefs(maxwidth)
11768        || $maxgraphpct != $oldprefs(maxgraphpct)} {
11769        redisplay
11770    } elseif {$showneartags != $oldprefs(showneartags) ||
11771          $limitdiffs != $oldprefs(limitdiffs)} {
11772        reselectline
11773    }
11774    if {$hideremotes != $oldprefs(hideremotes)} {
11775        rereadrefs
11776    }
11777}
11778
11779proc formatdate {d} {
11780    global datetimeformat
11781    if {$d ne {}} {
11782        # If $datetimeformat includes a timezone, display in the
11783        # timezone of the argument.  Otherwise, display in local time.
11784        if {[string match {*%[zZ]*} $datetimeformat]} {
11785            if {[catch {set d [clock format [lindex $d 0] -timezone [lindex $d 1] -format $datetimeformat]}]} {
11786                # Tcl < 8.5 does not support -timezone.  Emulate it by
11787                # setting TZ (e.g. TZ=<-0430>+04:30).
11788                global env
11789                if {[info exists env(TZ)]} {
11790                    set savedTZ $env(TZ)
11791                }
11792                set zone [lindex $d 1]
11793                set sign [string map {+ - - +} [string index $zone 0]]
11794                set env(TZ) <$zone>$sign[string range $zone 1 2]:[string range $zone 3 4]
11795                set d [clock format [lindex $d 0] -format $datetimeformat]
11796                if {[info exists savedTZ]} {
11797                    set env(TZ) $savedTZ
11798                } else {
11799                    unset env(TZ)
11800                }
11801            }
11802        } else {
11803            set d [clock format [lindex $d 0] -format $datetimeformat]
11804        }
11805    }
11806    return $d
11807}
11808
11809# This list of encoding names and aliases is distilled from
11810# http://www.iana.org/assignments/character-sets.
11811# Not all of them are supported by Tcl.
11812set encoding_aliases {
11813    { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII
11814      ISO646-US US-ASCII us IBM367 cp367 csASCII }
11815    { ISO-10646-UTF-1 csISO10646UTF1 }
11816    { ISO_646.basic:1983 ref csISO646basic1983 }
11817    { INVARIANT csINVARIANT }
11818    { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion }
11819    { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom }
11820    { NATS-SEFI iso-ir-8-1 csNATSSEFI }
11821    { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD }
11822    { NATS-DANO iso-ir-9-1 csNATSDANO }
11823    { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD }
11824    { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish }
11825    { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames }
11826    { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 }
11827    { ISO-2022-KR csISO2022KR }
11828    { EUC-KR csEUCKR }
11829    { ISO-2022-JP csISO2022JP }
11830    { ISO-2022-JP-2 csISO2022JP2 }
11831    { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7
11832      csISO13JISC6220jp }
11833    { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro }
11834    { IT iso-ir-15 ISO646-IT csISO15Italian }
11835    { PT iso-ir-16 ISO646-PT csISO16Portuguese }
11836    { ES iso-ir-17 ISO646-ES csISO17Spanish }
11837    { greek7-old iso-ir-18 csISO18Greek7Old }
11838    { latin-greek iso-ir-19 csISO19LatinGreek }
11839    { DIN_66003 iso-ir-21 de ISO646-DE csISO21German }
11840    { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French }
11841    { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 }
11842    { ISO_5427 iso-ir-37 csISO5427Cyrillic }
11843    { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 }
11844    { BS_viewdata iso-ir-47 csISO47BSViewdata }
11845    { INIS iso-ir-49 csISO49INIS }
11846    { INIS-8 iso-ir-50 csISO50INIS8 }
11847    { INIS-cyrillic iso-ir-51 csISO51INISCyrillic }
11848    { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 }
11849    { ISO_5428:1980 iso-ir-55 csISO5428Greek }
11850    { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 }
11851    { GB_2312-80 iso-ir-58 chinese csISO58GB231280 }
11852    { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian
11853      csISO60Norwegian1 }
11854    { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 }
11855    { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French }
11856    { videotex-suppl iso-ir-70 csISO70VideotexSupp1 }
11857    { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 }
11858    { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 }
11859    { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian }
11860    { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 }
11861    { greek7 iso-ir-88 csISO88Greek7 }
11862    { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 }
11863    { iso-ir-90 csISO90 }
11864    { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a }
11865    { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b
11866      csISO92JISC62991984b }
11867    { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd }
11868    { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand }
11869    { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add
11870      csISO95JIS62291984handadd }
11871    { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana }
11872    { ISO_2033-1983 iso-ir-98 e13b csISO2033 }
11873    { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS }
11874    { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819
11875      CP819 csISOLatin1 }
11876    { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 }
11877    { T.61-7bit iso-ir-102 csISO102T617bit }
11878    { T.61-8bit T.61 iso-ir-103 csISO103T618bit }
11879    { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 }
11880    { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 }
11881    { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic }
11882    { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 }
11883    { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 }
11884    { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr }
11885    { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708
11886      arabic csISOLatinArabic }
11887    { ISO_8859-6-E csISO88596E ISO-8859-6-E }
11888    { ISO_8859-6-I csISO88596I ISO-8859-6-I }
11889    { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118
11890      greek greek8 csISOLatinGreek }
11891    { T.101-G2 iso-ir-128 csISO128T101G2 }
11892    { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew
11893      csISOLatinHebrew }
11894    { ISO_8859-8-E csISO88598E ISO-8859-8-E }
11895    { ISO_8859-8-I csISO88598I ISO-8859-8-I }
11896    { CSN_369103 iso-ir-139 csISO139CSN369103 }
11897    { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 }
11898    { ISO_6937-2-add iso-ir-142 csISOTextComm }
11899    { IEC_P27-1 iso-ir-143 csISO143IECP271 }
11900    { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic
11901      csISOLatinCyrillic }
11902    { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian }
11903    { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian }
11904    { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 }
11905    { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT }
11906    { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba }
11907    { ISO_6937-2-25 iso-ir-152 csISO6937Add }
11908    { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 }
11909    { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp }
11910    { ISO_10367-box iso-ir-155 csISO10367Box }
11911    { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 }
11912    { latin-lap lap iso-ir-158 csISO158Lap }
11913    { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 }
11914    { DS_2089 DS2089 ISO646-DK dk csISO646Danish }
11915    { us-dk csUSDK }
11916    { dk-us csDKUS }
11917    { JIS_X0201 X0201 csHalfWidthKatakana }
11918    { KSC5636 ISO646-KR csKSC5636 }
11919    { ISO-10646-UCS-2 csUnicode }
11920    { ISO-10646-UCS-4 csUCS4 }
11921    { DEC-MCS dec csDECMCS }
11922    { hp-roman8 roman8 r8 csHPRoman8 }
11923    { macintosh mac csMacintosh }
11924    { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl
11925      csIBM037 }
11926    { IBM038 EBCDIC-INT cp038 csIBM038 }
11927    { IBM273 CP273 csIBM273 }
11928    { IBM274 EBCDIC-BE CP274 csIBM274 }
11929    { IBM275 EBCDIC-BR cp275 csIBM275 }
11930    { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 }
11931    { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 }
11932    { IBM280 CP280 ebcdic-cp-it csIBM280 }
11933    { IBM281 EBCDIC-JP-E cp281 csIBM281 }
11934    { IBM284 CP284 ebcdic-cp-es csIBM284 }
11935    { IBM285 CP285 ebcdic-cp-gb csIBM285 }
11936    { IBM290 cp290 EBCDIC-JP-kana csIBM290 }
11937    { IBM297 cp297 ebcdic-cp-fr csIBM297 }
11938    { IBM420 cp420 ebcdic-cp-ar1 csIBM420 }
11939    { IBM423 cp423 ebcdic-cp-gr csIBM423 }
11940    { IBM424 cp424 ebcdic-cp-he csIBM424 }
11941    { IBM437 cp437 437 csPC8CodePage437 }
11942    { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 }
11943    { IBM775 cp775 csPC775Baltic }
11944    { IBM850 cp850 850 csPC850Multilingual }
11945    { IBM851 cp851 851 csIBM851 }
11946    { IBM852 cp852 852 csPCp852 }
11947    { IBM855 cp855 855 csIBM855 }
11948    { IBM857 cp857 857 csIBM857 }
11949    { IBM860 cp860 860 csIBM860 }
11950    { IBM861 cp861 861 cp-is csIBM861 }
11951    { IBM862 cp862 862 csPC862LatinHebrew }
11952    { IBM863 cp863 863 csIBM863 }
11953    { IBM864 cp864 csIBM864 }
11954    { IBM865 cp865 865 csIBM865 }
11955    { IBM866 cp866 866 csIBM866 }
11956    { IBM868 CP868 cp-ar csIBM868 }
11957    { IBM869 cp869 869 cp-gr csIBM869 }
11958    { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 }
11959    { IBM871 CP871 ebcdic-cp-is csIBM871 }
11960    { IBM880 cp880 EBCDIC-Cyrillic csIBM880 }
11961    { IBM891 cp891 csIBM891 }
11962    { IBM903 cp903 csIBM903 }
11963    { IBM904 cp904 904 csIBBM904 }
11964    { IBM905 CP905 ebcdic-cp-tr csIBM905 }
11965    { IBM918 CP918 ebcdic-cp-ar2 csIBM918 }
11966    { IBM1026 CP1026 csIBM1026 }
11967    { EBCDIC-AT-DE csIBMEBCDICATDE }
11968    { EBCDIC-AT-DE-A csEBCDICATDEA }
11969    { EBCDIC-CA-FR csEBCDICCAFR }
11970    { EBCDIC-DK-NO csEBCDICDKNO }
11971    { EBCDIC-DK-NO-A csEBCDICDKNOA }
11972    { EBCDIC-FI-SE csEBCDICFISE }
11973    { EBCDIC-FI-SE-A csEBCDICFISEA }
11974    { EBCDIC-FR csEBCDICFR }
11975    { EBCDIC-IT csEBCDICIT }
11976    { EBCDIC-PT csEBCDICPT }
11977    { EBCDIC-ES csEBCDICES }
11978    { EBCDIC-ES-A csEBCDICESA }
11979    { EBCDIC-ES-S csEBCDICESS }
11980    { EBCDIC-UK csEBCDICUK }
11981    { EBCDIC-US csEBCDICUS }
11982    { UNKNOWN-8BIT csUnknown8BiT }
11983    { MNEMONIC csMnemonic }
11984    { MNEM csMnem }
11985    { VISCII csVISCII }
11986    { VIQR csVIQR }
11987    { KOI8-R csKOI8R }
11988    { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro }
11989    { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro }
11990    { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro }
11991    { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro }
11992    { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro }
11993    { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro }
11994    { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro }
11995    { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro }
11996    { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro }
11997    { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro }
11998    { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro }
11999    { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro }
12000    { IBM1047 IBM-1047 }
12001    { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian }
12002    { Amiga-1251 Ami1251 Amiga1251 Ami-1251 }
12003    { UNICODE-1-1 csUnicode11 }
12004    { CESU-8 csCESU-8 }
12005    { BOCU-1 csBOCU-1 }
12006    { UNICODE-1-1-UTF-7 csUnicode11UTF7 }
12007    { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic
12008      l8 }
12009    { ISO-8859-15 ISO_8859-15 Latin-9 }
12010    { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 }
12011    { GBK CP936 MS936 windows-936 }
12012    { JIS_Encoding csJISEncoding }
12013    { Shift_JIS MS_Kanji csShiftJIS ShiftJIS Shift-JIS }
12014    { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese
12015      EUC-JP }
12016    { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese }
12017    { ISO-10646-UCS-Basic csUnicodeASCII }
12018    { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 }
12019    { ISO-Unicode-IBM-1261 csUnicodeIBM1261 }
12020    { ISO-Unicode-IBM-1268 csUnicodeIBM1268 }
12021    { ISO-Unicode-IBM-1276 csUnicodeIBM1276 }
12022    { ISO-Unicode-IBM-1264 csUnicodeIBM1264 }
12023    { ISO-Unicode-IBM-1265 csUnicodeIBM1265 }
12024    { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 }
12025    { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 }
12026    { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 }
12027    { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 }
12028    { Adobe-Standard-Encoding csAdobeStandardEncoding }
12029    { Ventura-US csVenturaUS }
12030    { Ventura-International csVenturaInternational }
12031    { PC8-Danish-Norwegian csPC8DanishNorwegian }
12032    { PC8-Turkish csPC8Turkish }
12033    { IBM-Symbols csIBMSymbols }
12034    { IBM-Thai csIBMThai }
12035    { HP-Legal csHPLegal }
12036    { HP-Pi-font csHPPiFont }
12037    { HP-Math8 csHPMath8 }
12038    { Adobe-Symbol-Encoding csHPPSMath }
12039    { HP-DeskTop csHPDesktop }
12040    { Ventura-Math csVenturaMath }
12041    { Microsoft-Publishing csMicrosoftPublishing }
12042    { Windows-31J csWindows31J }
12043    { GB2312 csGB2312 }
12044    { Big5 csBig5 }
12045}
12046
12047proc tcl_encoding {enc} {
12048    global encoding_aliases tcl_encoding_cache
12049    if {[info exists tcl_encoding_cache($enc)]} {
12050        return $tcl_encoding_cache($enc)
12051    }
12052    set names [encoding names]
12053    set lcnames [string tolower $names]
12054    set enc [string tolower $enc]
12055    set i [lsearch -exact $lcnames $enc]
12056    if {$i < 0} {
12057        # look for "isonnn" instead of "iso-nnn" or "iso_nnn"
12058        if {[regsub {^(iso|cp|ibm|jis)[-_]} $enc {\1} encx]} {
12059            set i [lsearch -exact $lcnames $encx]
12060        }
12061    }
12062    if {$i < 0} {
12063        foreach l $encoding_aliases {
12064            set ll [string tolower $l]
12065            if {[lsearch -exact $ll $enc] < 0} continue
12066            # look through the aliases for one that tcl knows about
12067            foreach e $ll {
12068                set i [lsearch -exact $lcnames $e]
12069                if {$i < 0} {
12070                    if {[regsub {^(iso|cp|ibm|jis)[-_]} $e {\1} ex]} {
12071                        set i [lsearch -exact $lcnames $ex]
12072                    }
12073                }
12074                if {$i >= 0} break
12075            }
12076            break
12077        }
12078    }
12079    set tclenc {}
12080    if {$i >= 0} {
12081        set tclenc [lindex $names $i]
12082    }
12083    set tcl_encoding_cache($enc) $tclenc
12084    return $tclenc
12085}
12086
12087proc gitattr {path attr default} {
12088    global path_attr_cache
12089    if {[info exists path_attr_cache($attr,$path)]} {
12090        set r $path_attr_cache($attr,$path)
12091    } else {
12092        set r "unspecified"
12093        if {![catch {set line [exec git check-attr $attr -- $path]}]} {
12094            regexp "(.*): $attr: (.*)" $line m f r
12095        }
12096        set path_attr_cache($attr,$path) $r
12097    }
12098    if {$r eq "unspecified"} {
12099        return $default
12100    }
12101    return $r
12102}
12103
12104proc cache_gitattr {attr pathlist} {
12105    global path_attr_cache
12106    set newlist {}
12107    foreach path $pathlist {
12108        if {![info exists path_attr_cache($attr,$path)]} {
12109            lappend newlist $path
12110        }
12111    }
12112    set lim 1000
12113    if {[tk windowingsystem] == "win32"} {
12114        # windows has a 32k limit on the arguments to a command...
12115        set lim 30
12116    }
12117    while {$newlist ne {}} {
12118        set head [lrange $newlist 0 [expr {$lim - 1}]]
12119        set newlist [lrange $newlist $lim end]
12120        if {![catch {set rlist [eval exec git check-attr $attr -- $head]}]} {
12121            foreach row [split $rlist "\n"] {
12122                if {[regexp "(.*): $attr: (.*)" $row m path value]} {
12123                    if {[string index $path 0] eq "\""} {
12124                        set path [encoding convertfrom [lindex $path 0]]
12125                    }
12126                    set path_attr_cache($attr,$path) $value
12127                }
12128            }
12129        }
12130    }
12131}
12132
12133proc get_path_encoding {path} {
12134    global gui_encoding perfile_attrs
12135    set tcl_enc $gui_encoding
12136    if {$path ne {} && $perfile_attrs} {
12137        set enc2 [tcl_encoding [gitattr $path encoding $tcl_enc]]
12138        if {$enc2 ne {}} {
12139            set tcl_enc $enc2
12140        }
12141    }
12142    return $tcl_enc
12143}
12144
12145## For msgcat loading, first locate the installation location.
12146if { [info exists ::env(GITK_MSGSDIR)] } {
12147    ## Msgsdir was manually set in the environment.
12148    set gitk_msgsdir $::env(GITK_MSGSDIR)
12149} else {
12150    ## Let's guess the prefix from argv0.
12151    set gitk_prefix [file dirname [file dirname [file normalize $argv0]]]
12152    set gitk_libdir [file join $gitk_prefix share gitk lib]
12153    set gitk_msgsdir [file join $gitk_libdir msgs]
12154    unset gitk_prefix
12155}
12156
12157## Internationalization (i18n) through msgcat and gettext. See
12158## http://www.gnu.org/software/gettext/manual/html_node/Tcl.html
12159package require msgcat
12160namespace import ::msgcat::mc
12161## And eventually load the actual message catalog
12162::msgcat::mcload $gitk_msgsdir
12163
12164# First check that Tcl/Tk is recent enough
12165if {[catch {package require Tk 8.4} err]} {
12166    show_error {} . [mc "Sorry, gitk cannot run with this version of Tcl/Tk.\n\
12167                         Gitk requires at least Tcl/Tk 8.4."]
12168    exit 1
12169}
12170
12171# on OSX bring the current Wish process window to front
12172if {[tk windowingsystem] eq "aqua"} {
12173    exec osascript -e [format {
12174        tell application "System Events"
12175            set frontmost of processes whose unix id is %d to true
12176        end tell
12177    } [pid] ]
12178}
12179
12180# Unset GIT_TRACE var if set
12181if { [info exists ::env(GIT_TRACE)] } {
12182    unset ::env(GIT_TRACE)
12183}
12184
12185# defaults...
12186set wrcomcmd "git diff-tree --stdin -p --pretty=email"
12187
12188set gitencoding {}
12189catch {
12190    set gitencoding [exec git config --get i18n.commitencoding]
12191}
12192catch {
12193    set gitencoding [exec git config --get i18n.logoutputencoding]
12194}
12195if {$gitencoding == ""} {
12196    set gitencoding "utf-8"
12197}
12198set tclencoding [tcl_encoding $gitencoding]
12199if {$tclencoding == {}} {
12200    puts stderr "Warning: encoding $gitencoding is not supported by Tcl/Tk"
12201}
12202
12203set gui_encoding [encoding system]
12204catch {
12205    set enc [exec git config --get gui.encoding]
12206    if {$enc ne {}} {
12207        set tclenc [tcl_encoding $enc]
12208        if {$tclenc ne {}} {
12209            set gui_encoding $tclenc
12210        } else {
12211            puts stderr "Warning: encoding $enc is not supported by Tcl/Tk"
12212        }
12213    }
12214}
12215
12216set log_showroot true
12217catch {
12218    set log_showroot [exec git config --bool --get log.showroot]
12219}
12220
12221if {[tk windowingsystem] eq "aqua"} {
12222    set mainfont {{Lucida Grande} 9}
12223    set textfont {Monaco 9}
12224    set uifont {{Lucida Grande} 9 bold}
12225} elseif {![catch {::tk::pkgconfig get fontsystem} xft] && $xft eq "xft"} {
12226    # fontconfig!
12227    set mainfont {sans 9}
12228    set textfont {monospace 9}
12229    set uifont {sans 9 bold}
12230} else {
12231    set mainfont {Helvetica 9}
12232    set textfont {Courier 9}
12233    set uifont {Helvetica 9 bold}
12234}
12235set tabstop 8
12236set findmergefiles 0
12237set maxgraphpct 50
12238set maxwidth 16
12239set revlistorder 0
12240set fastdate 0
12241set uparrowlen 5
12242set downarrowlen 5
12243set mingaplen 100
12244set cmitmode "patch"
12245set wrapcomment "none"
12246set showneartags 1
12247set hideremotes 0
12248set maxrefs 20
12249set visiblerefs {"master"}
12250set maxlinelen 200
12251set showlocalchanges 1
12252set limitdiffs 1
12253set datetimeformat "%Y-%m-%d %H:%M:%S"
12254set autoselect 1
12255set autosellen 40
12256set perfile_attrs 0
12257set want_ttk 1
12258
12259if {[tk windowingsystem] eq "aqua"} {
12260    set extdifftool "opendiff"
12261} else {
12262    set extdifftool "meld"
12263}
12264
12265set colors {lime red blue magenta darkgrey brown orange}
12266if {[tk windowingsystem] eq "win32"} {
12267    set uicolor SystemButtonFace
12268    set uifgcolor SystemButtonText
12269    set uifgdisabledcolor SystemDisabledText
12270    set bgcolor SystemWindow
12271    set fgcolor SystemWindowText
12272    set selectbgcolor SystemHighlight
12273} else {
12274    set uicolor grey85
12275    set uifgcolor black
12276    set uifgdisabledcolor "#999"
12277    set bgcolor white
12278    set fgcolor black
12279    set selectbgcolor gray85
12280}
12281set diffcolors {red "#00a000" blue}
12282set diffcontext 3
12283set mergecolors {red blue lime purple brown "#009090" magenta "#808000" "#009000" "#ff0080" cyan "#b07070" "#70b0f0" "#70f0b0" "#f0b070" "#ff70b0"}
12284set ignorespace 0
12285set worddiff ""
12286set markbgcolor "#e0e0ff"
12287
12288set headbgcolor lime
12289set headfgcolor black
12290set headoutlinecolor black
12291set remotebgcolor #ffddaa
12292set tagbgcolor yellow
12293set tagfgcolor black
12294set tagoutlinecolor black
12295set reflinecolor black
12296set filesepbgcolor #aaaaaa
12297set filesepfgcolor black
12298set linehoverbgcolor #ffff80
12299set linehoverfgcolor black
12300set linehoveroutlinecolor black
12301set mainheadcirclecolor yellow
12302set workingfilescirclecolor red
12303set indexcirclecolor lime
12304set circlecolors {white blue gray blue blue}
12305set linkfgcolor blue
12306set circleoutlinecolor $fgcolor
12307set foundbgcolor yellow
12308set currentsearchhitbgcolor orange
12309
12310# button for popping up context menus
12311if {[tk windowingsystem] eq "aqua"} {
12312    set ctxbut <Button-2>
12313} else {
12314    set ctxbut <Button-3>
12315}
12316
12317catch {
12318    # follow the XDG base directory specification by default. See
12319    # http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
12320    if {[info exists env(XDG_CONFIG_HOME)] && $env(XDG_CONFIG_HOME) ne ""} {
12321        # XDG_CONFIG_HOME environment variable is set
12322        set config_file [file join $env(XDG_CONFIG_HOME) git gitk]
12323        set config_file_tmp [file join $env(XDG_CONFIG_HOME) git gitk-tmp]
12324    } else {
12325        # default XDG_CONFIG_HOME
12326        set config_file "~/.config/git/gitk"
12327        set config_file_tmp "~/.config/git/gitk-tmp"
12328    }
12329    if {![file exists $config_file]} {
12330        # for backward compatibility use the old config file if it exists
12331        if {[file exists "~/.gitk"]} {
12332            set config_file "~/.gitk"
12333            set config_file_tmp "~/.gitk-tmp"
12334        } elseif {![file exists [file dirname $config_file]]} {
12335            file mkdir [file dirname $config_file]
12336        }
12337    }
12338    source $config_file
12339}
12340config_check_tmp_exists 50
12341
12342set config_variables {
12343    mainfont textfont uifont tabstop findmergefiles maxgraphpct maxwidth
12344    cmitmode wrapcomment autoselect autosellen showneartags maxrefs visiblerefs
12345    hideremotes showlocalchanges datetimeformat limitdiffs uicolor want_ttk
12346    bgcolor fgcolor uifgcolor uifgdisabledcolor colors diffcolors mergecolors
12347    markbgcolor diffcontext selectbgcolor foundbgcolor currentsearchhitbgcolor
12348    extdifftool perfile_attrs headbgcolor headfgcolor headoutlinecolor
12349    remotebgcolor tagbgcolor tagfgcolor tagoutlinecolor reflinecolor
12350    filesepbgcolor filesepfgcolor linehoverbgcolor linehoverfgcolor
12351    linehoveroutlinecolor mainheadcirclecolor workingfilescirclecolor
12352    indexcirclecolor circlecolors linkfgcolor circleoutlinecolor
12353}
12354foreach var $config_variables {
12355    config_init_trace $var
12356    trace add variable $var write config_variable_change_cb
12357}
12358
12359parsefont mainfont $mainfont
12360eval font create mainfont [fontflags mainfont]
12361eval font create mainfontbold [fontflags mainfont 1]
12362
12363parsefont textfont $textfont
12364eval font create textfont [fontflags textfont]
12365eval font create textfontbold [fontflags textfont 1]
12366
12367parsefont uifont $uifont
12368eval font create uifont [fontflags uifont]
12369
12370setui $uicolor
12371
12372setoptions
12373
12374# check that we can find a .git directory somewhere...
12375if {[catch {set gitdir [exec git rev-parse --git-dir]}]} {
12376    show_error {} . [mc "Cannot find a git repository here."]
12377    exit 1
12378}
12379
12380set selecthead {}
12381set selectheadid {}
12382
12383set revtreeargs {}
12384set cmdline_files {}
12385set i 0
12386set revtreeargscmd {}
12387foreach arg $argv {
12388    switch -glob -- $arg {
12389        "" { }
12390        "--" {
12391            set cmdline_files [lrange $argv [expr {$i + 1}] end]
12392            break
12393        }
12394        "--select-commit=*" {
12395            set selecthead [string range $arg 16 end]
12396        }
12397        "--argscmd=*" {
12398            set revtreeargscmd [string range $arg 10 end]
12399        }
12400        default {
12401            lappend revtreeargs $arg
12402        }
12403    }
12404    incr i
12405}
12406
12407if {$selecthead eq "HEAD"} {
12408    set selecthead {}
12409}
12410
12411if {$i >= [llength $argv] && $revtreeargs ne {}} {
12412    # no -- on command line, but some arguments (other than --argscmd)
12413    if {[catch {
12414        set f [eval exec git rev-parse --no-revs --no-flags $revtreeargs]
12415        set cmdline_files [split $f "\n"]
12416        set n [llength $cmdline_files]
12417        set revtreeargs [lrange $revtreeargs 0 end-$n]
12418        # Unfortunately git rev-parse doesn't produce an error when
12419        # something is both a revision and a filename.  To be consistent
12420        # with git log and git rev-list, check revtreeargs for filenames.
12421        foreach arg $revtreeargs {
12422            if {[file exists $arg]} {
12423                show_error {} . [mc "Ambiguous argument '%s': both revision\
12424                                 and filename" $arg]
12425                exit 1
12426            }
12427        }
12428    } err]} {
12429        # unfortunately we get both stdout and stderr in $err,
12430        # so look for "fatal:".
12431        set i [string first "fatal:" $err]
12432        if {$i > 0} {
12433            set err [string range $err [expr {$i + 6}] end]
12434        }
12435        show_error {} . "[mc "Bad arguments to gitk:"]\n$err"
12436        exit 1
12437    }
12438}
12439
12440set nullid "0000000000000000000000000000000000000000"
12441set nullid2 "0000000000000000000000000000000000000001"
12442set nullfile "/dev/null"
12443
12444set have_tk85 [expr {[package vcompare $tk_version "8.5"] >= 0}]
12445if {![info exists have_ttk]} {
12446    set have_ttk [llength [info commands ::ttk::style]]
12447}
12448set use_ttk [expr {$have_ttk && $want_ttk}]
12449set NS [expr {$use_ttk ? "ttk" : ""}]
12450
12451if {$use_ttk} {
12452    setttkstyle
12453}
12454
12455regexp {^git version ([\d.]*\d)} [exec git version] _ git_version
12456
12457set show_notes {}
12458if {[package vcompare $git_version "1.6.6.2"] >= 0} {
12459    set show_notes "--show-notes"
12460}
12461
12462set appname "gitk"
12463
12464set runq {}
12465set history {}
12466set historyindex 0
12467set fh_serial 0
12468set nhl_names {}
12469set highlight_paths {}
12470set findpattern {}
12471set searchdirn -forwards
12472set boldids {}
12473set boldnameids {}
12474set diffelide {0 0}
12475set markingmatches 0
12476set linkentercount 0
12477set need_redisplay 0
12478set nrows_drawn 0
12479set firsttabstop 0
12480
12481set nextviewnum 1
12482set curview 0
12483set selectedview 0
12484set selectedhlview [mc "None"]
12485set highlight_related [mc "None"]
12486set highlight_files {}
12487set viewfiles(0) {}
12488set viewperm(0) 0
12489set viewchanged(0) 0
12490set viewargs(0) {}
12491set viewargscmd(0) {}
12492
12493set selectedline {}
12494set numcommits 0
12495set loginstance 0
12496set cmdlineok 0
12497set stopped 0
12498set stuffsaved 0
12499set patchnum 0
12500set lserial 0
12501set hasworktree [hasworktree]
12502set cdup {}
12503if {[expr {[exec git rev-parse --is-inside-work-tree] == "true"}]} {
12504    set cdup [exec git rev-parse --show-cdup]
12505}
12506set worktree [exec git rev-parse --show-toplevel]
12507setcoords
12508makewindow
12509catch {
12510    image create photo gitlogo      -width 16 -height 16
12511
12512    image create photo gitlogominus -width  4 -height  2
12513    gitlogominus put #C00000 -to 0 0 4 2
12514    gitlogo copy gitlogominus -to  1 5
12515    gitlogo copy gitlogominus -to  6 5
12516    gitlogo copy gitlogominus -to 11 5
12517    image delete gitlogominus
12518
12519    image create photo gitlogoplus  -width  4 -height  4
12520    gitlogoplus  put #008000 -to 1 0 3 4
12521    gitlogoplus  put #008000 -to 0 1 4 3
12522    gitlogo copy gitlogoplus  -to  1 9
12523    gitlogo copy gitlogoplus  -to  6 9
12524    gitlogo copy gitlogoplus  -to 11 9
12525    image delete gitlogoplus
12526
12527    image create photo gitlogo32    -width 32 -height 32
12528    gitlogo32 copy gitlogo -zoom 2 2
12529
12530    wm iconphoto . -default gitlogo gitlogo32
12531}
12532# wait for the window to become visible
12533tkwait visibility .
12534set_window_title
12535update
12536readrefs
12537
12538if {$cmdline_files ne {} || $revtreeargs ne {} || $revtreeargscmd ne {}} {
12539    # create a view for the files/dirs specified on the command line
12540    set curview 1
12541    set selectedview 1
12542    set nextviewnum 2
12543    set viewname(1) [mc "Command line"]
12544    set viewfiles(1) $cmdline_files
12545    set viewargs(1) $revtreeargs
12546    set viewargscmd(1) $revtreeargscmd
12547    set viewperm(1) 0
12548    set viewchanged(1) 0
12549    set vdatemode(1) 0
12550    addviewmenu 1
12551    .bar.view entryconf [mca "&Edit view..."] -state normal
12552    .bar.view entryconf [mca "&Delete view"] -state normal
12553}
12554
12555if {[info exists permviews]} {
12556    foreach v $permviews {
12557        set n $nextviewnum
12558        incr nextviewnum
12559        set viewname($n) [lindex $v 0]
12560        set viewfiles($n) [lindex $v 1]
12561        set viewargs($n) [lindex $v 2]
12562        set viewargscmd($n) [lindex $v 3]
12563        set viewperm($n) 1
12564        set viewchanged($n) 0
12565        addviewmenu $n
12566    }
12567}
12568
12569if {[tk windowingsystem] eq "win32"} {
12570    focus -force .
12571}
12572
12573getcommits {}
12574
12575# Local variables:
12576# mode: tcl
12577# indent-tabs-mode: t
12578# tab-width: 8
12579# End: