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