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