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