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