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