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