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