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