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