gitkon commit gitk: Get rid of the childlist variable (6a90bff)
   1#!/bin/sh
   2# Tcl ignores the next line -*- tcl -*- \
   3exec wish "$0" -- "$@"
   4
   5# Copyright (C) 2005-2006 Paul Mackerras.  All rights reserved.
   6# This program is free software; it may be used, copied, modified
   7# and distributed under the terms of the GNU General Public Licence,
   8# either version 2, or (at your option) any later version.
   9
  10proc gitdir {} {
  11    global env
  12    if {[info exists env(GIT_DIR)]} {
  13        return $env(GIT_DIR)
  14    } else {
  15        return [exec git rev-parse --git-dir]
  16    }
  17}
  18
  19# A simple scheduler for compute-intensive stuff.
  20# The aim is to make sure that event handlers for GUI actions can
  21# run at least every 50-100 ms.  Unfortunately fileevent handlers are
  22# run before X event handlers, so reading from a fast source can
  23# make the GUI completely unresponsive.
  24proc run args {
  25    global isonrunq runq
  26
  27    set script $args
  28    if {[info exists isonrunq($script)]} return
  29    if {$runq eq {}} {
  30        after idle dorunq
  31    }
  32    lappend runq [list {} $script]
  33    set isonrunq($script) 1
  34}
  35
  36proc filerun {fd script} {
  37    fileevent $fd readable [list filereadable $fd $script]
  38}
  39
  40proc filereadable {fd script} {
  41    global runq
  42
  43    fileevent $fd readable {}
  44    if {$runq eq {}} {
  45        after idle dorunq
  46    }
  47    lappend runq [list $fd $script]
  48}
  49
  50proc dorunq {} {
  51    global isonrunq runq
  52
  53    set tstart [clock clicks -milliseconds]
  54    set t0 $tstart
  55    while {$runq ne {}} {
  56        set fd [lindex $runq 0 0]
  57        set script [lindex $runq 0 1]
  58        set repeat [eval $script]
  59        set t1 [clock clicks -milliseconds]
  60        set t [expr {$t1 - $t0}]
  61        set runq [lrange $runq 1 end]
  62        if {$repeat ne {} && $repeat} {
  63            if {$fd eq {} || $repeat == 2} {
  64                # script returns 1 if it wants to be readded
  65                # file readers return 2 if they could do more straight away
  66                lappend runq [list $fd $script]
  67            } else {
  68                fileevent $fd readable [list filereadable $fd $script]
  69            }
  70        } elseif {$fd eq {}} {
  71            unset isonrunq($script)
  72        }
  73        set t0 $t1
  74        if {$t1 - $tstart >= 80} break
  75    }
  76    if {$runq ne {}} {
  77        after idle dorunq
  78    }
  79}
  80
  81# Start off a git rev-list process and arrange to read its output
  82proc start_rev_list {view} {
  83    global startmsecs
  84    global commfd leftover tclencoding datemode
  85    global viewargs viewfiles commitidx
  86    global lookingforhead showlocalchanges
  87
  88    set startmsecs [clock clicks -milliseconds]
  89    set commitidx($view) 0
  90    set args $viewargs($view)
  91    if {$viewfiles($view) ne {}} {
  92        set args [concat $args "--" $viewfiles($view)]
  93    }
  94    set order "--topo-order"
  95    if {$datemode} {
  96        set order "--date-order"
  97    }
  98    if {[catch {
  99        set fd [open [concat | git rev-list --header $order \
 100                          --parents --boundary --default HEAD $args] r]
 101    } err]} {
 102        puts stderr "Error executing git rev-list: $err"
 103        exit 1
 104    }
 105    set commfd($view) $fd
 106    set leftover($view) {}
 107    set lookingforhead $showlocalchanges
 108    fconfigure $fd -blocking 0 -translation lf
 109    if {$tclencoding != {}} {
 110        fconfigure $fd -encoding $tclencoding
 111    }
 112    filerun $fd [list getcommitlines $fd $view]
 113    nowbusy $view
 114}
 115
 116proc stop_rev_list {} {
 117    global commfd curview
 118
 119    if {![info exists commfd($curview)]} return
 120    set fd $commfd($curview)
 121    catch {
 122        set pid [pid $fd]
 123        exec kill $pid
 124    }
 125    catch {close $fd}
 126    unset commfd($curview)
 127}
 128
 129proc getcommits {} {
 130    global phase canv mainfont curview
 131
 132    set phase getcommits
 133    initlayout
 134    start_rev_list $curview
 135    show_status "Reading commits..."
 136}
 137
 138proc getcommitlines {fd view}  {
 139    global commitlisted
 140    global leftover commfd
 141    global displayorder commitidx commitrow commitdata
 142    global parentlist children curview hlview
 143    global vparentlist vdisporder vcmitlisted
 144
 145    set stuff [read $fd 500000]
 146    if {$stuff == {}} {
 147        if {![eof $fd]} {
 148            return 1
 149        }
 150        global viewname
 151        unset commfd($view)
 152        notbusy $view
 153        # set it blocking so we wait for the process to terminate
 154        fconfigure $fd -blocking 1
 155        if {[catch {close $fd} err]} {
 156            set fv {}
 157            if {$view != $curview} {
 158                set fv " for the \"$viewname($view)\" view"
 159            }
 160            if {[string range $err 0 4] == "usage"} {
 161                set err "Gitk: error reading commits$fv:\
 162                        bad arguments to git rev-list."
 163                if {$viewname($view) eq "Command line"} {
 164                    append err \
 165                        "  (Note: arguments to gitk are passed to git rev-list\
 166                         to allow selection of commits to be displayed.)"
 167                }
 168            } else {
 169                set err "Error reading commits$fv: $err"
 170            }
 171            error_popup $err
 172        }
 173        if {$view == $curview} {
 174            run chewcommits $view
 175        }
 176        return 0
 177    }
 178    set start 0
 179    set gotsome 0
 180    while 1 {
 181        set i [string first "\0" $stuff $start]
 182        if {$i < 0} {
 183            append leftover($view) [string range $stuff $start end]
 184            break
 185        }
 186        if {$start == 0} {
 187            set cmit $leftover($view)
 188            append cmit [string range $stuff 0 [expr {$i - 1}]]
 189            set leftover($view) {}
 190        } else {
 191            set cmit [string range $stuff $start [expr {$i - 1}]]
 192        }
 193        set start [expr {$i + 1}]
 194        set j [string first "\n" $cmit]
 195        set ok 0
 196        set listed 1
 197        if {$j >= 0} {
 198            set ids [string range $cmit 0 [expr {$j - 1}]]
 199            if {[string range $ids 0 0] == "-"} {
 200                set listed 0
 201                set ids [string range $ids 1 end]
 202            }
 203            set ok 1
 204            foreach id $ids {
 205                if {[string length $id] != 40} {
 206                    set ok 0
 207                    break
 208                }
 209            }
 210        }
 211        if {!$ok} {
 212            set shortcmit $cmit
 213            if {[string length $shortcmit] > 80} {
 214                set shortcmit "[string range $shortcmit 0 80]..."
 215            }
 216            error_popup "Can't parse git rev-list output: {$shortcmit}"
 217            exit 1
 218        }
 219        set id [lindex $ids 0]
 220        if {$listed} {
 221            set olds [lrange $ids 1 end]
 222            set i 0
 223            foreach p $olds {
 224                if {$i == 0 || [lsearch -exact $olds $p] >= $i} {
 225                    lappend children($view,$p) $id
 226                }
 227                incr i
 228            }
 229        } else {
 230            set olds {}
 231        }
 232        if {![info exists children($view,$id)]} {
 233            set children($view,$id) {}
 234        }
 235        set commitdata($id) [string range $cmit [expr {$j + 1}] end]
 236        set commitrow($view,$id) $commitidx($view)
 237        incr commitidx($view)
 238        if {$view == $curview} {
 239            lappend parentlist $olds
 240            lappend displayorder $id
 241            lappend commitlisted $listed
 242        } else {
 243            lappend vparentlist($view) $olds
 244            lappend vdisporder($view) $id
 245            lappend vcmitlisted($view) $listed
 246        }
 247        set gotsome 1
 248    }
 249    if {$gotsome} {
 250        run chewcommits $view
 251    }
 252    return 2
 253}
 254
 255proc chewcommits {view} {
 256    global curview hlview commfd
 257    global selectedline pending_select
 258
 259    set more 0
 260    if {$view == $curview} {
 261        set allread [expr {![info exists commfd($view)]}]
 262        set tlimit [expr {[clock clicks -milliseconds] + 50}]
 263        set more [layoutmore $tlimit $allread]
 264        if {$allread && !$more} {
 265            global displayorder nullid commitidx phase
 266            global numcommits startmsecs
 267
 268            if {[info exists pending_select]} {
 269                set row [expr {[lindex $displayorder 0] eq $nullid}]
 270                selectline $row 1
 271            }
 272            if {$commitidx($curview) > 0} {
 273                #set ms [expr {[clock clicks -milliseconds] - $startmsecs}]
 274                #puts "overall $ms ms for $numcommits commits"
 275            } else {
 276                show_status "No commits selected"
 277            }
 278            notbusy layout
 279            set phase {}
 280        }
 281    }
 282    if {[info exists hlview] && $view == $hlview} {
 283        vhighlightmore
 284    }
 285    return $more
 286}
 287
 288proc readcommit {id} {
 289    if {[catch {set contents [exec git cat-file commit $id]}]} return
 290    parsecommit $id $contents 0
 291}
 292
 293proc updatecommits {} {
 294    global viewdata curview phase displayorder
 295    global children commitrow selectedline thickerline
 296
 297    if {$phase ne {}} {
 298        stop_rev_list
 299        set phase {}
 300    }
 301    set n $curview
 302    foreach id $displayorder {
 303        catch {unset children($n,$id)}
 304        catch {unset commitrow($n,$id)}
 305    }
 306    set curview -1
 307    catch {unset selectedline}
 308    catch {unset thickerline}
 309    catch {unset viewdata($n)}
 310    readrefs
 311    changedrefs
 312    regetallcommits
 313    showview $n
 314}
 315
 316proc parsecommit {id contents listed} {
 317    global commitinfo cdate
 318
 319    set inhdr 1
 320    set comment {}
 321    set headline {}
 322    set auname {}
 323    set audate {}
 324    set comname {}
 325    set comdate {}
 326    set hdrend [string first "\n\n" $contents]
 327    if {$hdrend < 0} {
 328        # should never happen...
 329        set hdrend [string length $contents]
 330    }
 331    set header [string range $contents 0 [expr {$hdrend - 1}]]
 332    set comment [string range $contents [expr {$hdrend + 2}] end]
 333    foreach line [split $header "\n"] {
 334        set tag [lindex $line 0]
 335        if {$tag == "author"} {
 336            set audate [lindex $line end-1]
 337            set auname [lrange $line 1 end-2]
 338        } elseif {$tag == "committer"} {
 339            set comdate [lindex $line end-1]
 340            set comname [lrange $line 1 end-2]
 341        }
 342    }
 343    set headline {}
 344    # take the first non-blank line of the comment as the headline
 345    set headline [string trimleft $comment]
 346    set i [string first "\n" $headline]
 347    if {$i >= 0} {
 348        set headline [string range $headline 0 $i]
 349    }
 350    set headline [string trimright $headline]
 351    set i [string first "\r" $headline]
 352    if {$i >= 0} {
 353        set headline [string trimright [string range $headline 0 $i]]
 354    }
 355    if {!$listed} {
 356        # git rev-list indents the comment by 4 spaces;
 357        # if we got this via git cat-file, add the indentation
 358        set newcomment {}
 359        foreach line [split $comment "\n"] {
 360            append newcomment "    "
 361            append newcomment $line
 362            append newcomment "\n"
 363        }
 364        set comment $newcomment
 365    }
 366    if {$comdate != {}} {
 367        set cdate($id) $comdate
 368    }
 369    set commitinfo($id) [list $headline $auname $audate \
 370                             $comname $comdate $comment]
 371}
 372
 373proc getcommit {id} {
 374    global commitdata commitinfo
 375
 376    if {[info exists commitdata($id)]} {
 377        parsecommit $id $commitdata($id) 1
 378    } else {
 379        readcommit $id
 380        if {![info exists commitinfo($id)]} {
 381            set commitinfo($id) {"No commit information available"}
 382        }
 383    }
 384    return 1
 385}
 386
 387proc readrefs {} {
 388    global tagids idtags headids idheads tagobjid
 389    global otherrefids idotherrefs mainhead mainheadid
 390
 391    foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
 392        catch {unset $v}
 393    }
 394    set refd [open [list | git show-ref -d] r]
 395    while {[gets $refd line] >= 0} {
 396        if {[string index $line 40] ne " "} continue
 397        set id [string range $line 0 39]
 398        set ref [string range $line 41 end]
 399        if {![string match "refs/*" $ref]} continue
 400        set name [string range $ref 5 end]
 401        if {[string match "remotes/*" $name]} {
 402            if {![string match "*/HEAD" $name]} {
 403                set headids($name) $id
 404                lappend idheads($id) $name
 405            }
 406        } elseif {[string match "heads/*" $name]} {
 407            set name [string range $name 6 end]
 408            set headids($name) $id
 409            lappend idheads($id) $name
 410        } elseif {[string match "tags/*" $name]} {
 411            # this lets refs/tags/foo^{} overwrite refs/tags/foo,
 412            # which is what we want since the former is the commit ID
 413            set name [string range $name 5 end]
 414            if {[string match "*^{}" $name]} {
 415                set name [string range $name 0 end-3]
 416            } else {
 417                set tagobjid($name) $id
 418            }
 419            set tagids($name) $id
 420            lappend idtags($id) $name
 421        } else {
 422            set otherrefids($name) $id
 423            lappend idotherrefs($id) $name
 424        }
 425    }
 426    close $refd
 427    set mainhead {}
 428    set mainheadid {}
 429    catch {
 430        set thehead [exec git symbolic-ref HEAD]
 431        if {[string match "refs/heads/*" $thehead]} {
 432            set mainhead [string range $thehead 11 end]
 433            if {[info exists headids($mainhead)]} {
 434                set mainheadid $headids($mainhead)
 435            }
 436        }
 437    }
 438}
 439
 440# update things for a head moved to a child of its previous location
 441proc movehead {id name} {
 442    global headids idheads
 443
 444    removehead $headids($name) $name
 445    set headids($name) $id
 446    lappend idheads($id) $name
 447}
 448
 449# update things when a head has been removed
 450proc removehead {id name} {
 451    global headids idheads
 452
 453    if {$idheads($id) eq $name} {
 454        unset idheads($id)
 455    } else {
 456        set i [lsearch -exact $idheads($id) $name]
 457        if {$i >= 0} {
 458            set idheads($id) [lreplace $idheads($id) $i $i]
 459        }
 460    }
 461    unset headids($name)
 462}
 463
 464proc show_error {w top msg} {
 465    message $w.m -text $msg -justify center -aspect 400
 466    pack $w.m -side top -fill x -padx 20 -pady 20
 467    button $w.ok -text OK -command "destroy $top"
 468    pack $w.ok -side bottom -fill x
 469    bind $top <Visibility> "grab $top; focus $top"
 470    bind $top <Key-Return> "destroy $top"
 471    tkwait window $top
 472}
 473
 474proc error_popup msg {
 475    set w .error
 476    toplevel $w
 477    wm transient $w .
 478    show_error $w $w $msg
 479}
 480
 481proc confirm_popup msg {
 482    global confirm_ok
 483    set confirm_ok 0
 484    set w .confirm
 485    toplevel $w
 486    wm transient $w .
 487    message $w.m -text $msg -justify center -aspect 400
 488    pack $w.m -side top -fill x -padx 20 -pady 20
 489    button $w.ok -text OK -command "set confirm_ok 1; destroy $w"
 490    pack $w.ok -side left -fill x
 491    button $w.cancel -text Cancel -command "destroy $w"
 492    pack $w.cancel -side right -fill x
 493    bind $w <Visibility> "grab $w; focus $w"
 494    tkwait window $w
 495    return $confirm_ok
 496}
 497
 498proc makewindow {} {
 499    global canv canv2 canv3 linespc charspc ctext cflist
 500    global textfont mainfont uifont tabstop
 501    global findtype findtypemenu findloc findstring fstring geometry
 502    global entries sha1entry sha1string sha1but
 503    global maincursor textcursor curtextcursor
 504    global rowctxmenu fakerowmenu mergemax wrapcomment
 505    global highlight_files gdttype
 506    global searchstring sstring
 507    global bgcolor fgcolor bglist fglist diffcolors selectbgcolor
 508    global headctxmenu
 509
 510    menu .bar
 511    .bar add cascade -label "File" -menu .bar.file
 512    .bar configure -font $uifont
 513    menu .bar.file
 514    .bar.file add command -label "Update" -command updatecommits
 515    .bar.file add command -label "Reread references" -command rereadrefs
 516    .bar.file add command -label "Quit" -command doquit
 517    .bar.file configure -font $uifont
 518    menu .bar.edit
 519    .bar add cascade -label "Edit" -menu .bar.edit
 520    .bar.edit add command -label "Preferences" -command doprefs
 521    .bar.edit configure -font $uifont
 522
 523    menu .bar.view -font $uifont
 524    .bar add cascade -label "View" -menu .bar.view
 525    .bar.view add command -label "New view..." -command {newview 0}
 526    .bar.view add command -label "Edit view..." -command editview \
 527        -state disabled
 528    .bar.view add command -label "Delete view" -command delview -state disabled
 529    .bar.view add separator
 530    .bar.view add radiobutton -label "All files" -command {showview 0} \
 531        -variable selectedview -value 0
 532
 533    menu .bar.help
 534    .bar add cascade -label "Help" -menu .bar.help
 535    .bar.help add command -label "About gitk" -command about
 536    .bar.help add command -label "Key bindings" -command keys
 537    .bar.help configure -font $uifont
 538    . configure -menu .bar
 539
 540    # the gui has upper and lower half, parts of a paned window.
 541    panedwindow .ctop -orient vertical
 542
 543    # possibly use assumed geometry
 544    if {![info exists geometry(pwsash0)]} {
 545        set geometry(topheight) [expr {15 * $linespc}]
 546        set geometry(topwidth) [expr {80 * $charspc}]
 547        set geometry(botheight) [expr {15 * $linespc}]
 548        set geometry(botwidth) [expr {50 * $charspc}]
 549        set geometry(pwsash0) "[expr {40 * $charspc}] 2"
 550        set geometry(pwsash1) "[expr {60 * $charspc}] 2"
 551    }
 552
 553    # the upper half will have a paned window, a scroll bar to the right, and some stuff below
 554    frame .tf -height $geometry(topheight) -width $geometry(topwidth)
 555    frame .tf.histframe
 556    panedwindow .tf.histframe.pwclist -orient horizontal -sashpad 0 -handlesize 4
 557
 558    # create three canvases
 559    set cscroll .tf.histframe.csb
 560    set canv .tf.histframe.pwclist.canv
 561    canvas $canv \
 562        -selectbackground $selectbgcolor \
 563        -background $bgcolor -bd 0 \
 564        -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll"
 565    .tf.histframe.pwclist add $canv
 566    set canv2 .tf.histframe.pwclist.canv2
 567    canvas $canv2 \
 568        -selectbackground $selectbgcolor \
 569        -background $bgcolor -bd 0 -yscrollincr $linespc
 570    .tf.histframe.pwclist add $canv2
 571    set canv3 .tf.histframe.pwclist.canv3
 572    canvas $canv3 \
 573        -selectbackground $selectbgcolor \
 574        -background $bgcolor -bd 0 -yscrollincr $linespc
 575    .tf.histframe.pwclist add $canv3
 576    eval .tf.histframe.pwclist sash place 0 $geometry(pwsash0)
 577    eval .tf.histframe.pwclist sash place 1 $geometry(pwsash1)
 578
 579    # a scroll bar to rule them
 580    scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
 581    pack $cscroll -side right -fill y
 582    bind .tf.histframe.pwclist <Configure> {resizeclistpanes %W %w}
 583    lappend bglist $canv $canv2 $canv3
 584    pack .tf.histframe.pwclist -fill both -expand 1 -side left
 585
 586    # we have two button bars at bottom of top frame. Bar 1
 587    frame .tf.bar
 588    frame .tf.lbar -height 15
 589
 590    set sha1entry .tf.bar.sha1
 591    set entries $sha1entry
 592    set sha1but .tf.bar.sha1label
 593    button $sha1but -text "SHA1 ID: " -state disabled -relief flat \
 594        -command gotocommit -width 8 -font $uifont
 595    $sha1but conf -disabledforeground [$sha1but cget -foreground]
 596    pack .tf.bar.sha1label -side left
 597    entry $sha1entry -width 40 -font $textfont -textvariable sha1string
 598    trace add variable sha1string write sha1change
 599    pack $sha1entry -side left -pady 2
 600
 601    image create bitmap bm-left -data {
 602        #define left_width 16
 603        #define left_height 16
 604        static unsigned char left_bits[] = {
 605        0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
 606        0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
 607        0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
 608    }
 609    image create bitmap bm-right -data {
 610        #define right_width 16
 611        #define right_height 16
 612        static unsigned char right_bits[] = {
 613        0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
 614        0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
 615        0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
 616    }
 617    button .tf.bar.leftbut -image bm-left -command goback \
 618        -state disabled -width 26
 619    pack .tf.bar.leftbut -side left -fill y
 620    button .tf.bar.rightbut -image bm-right -command goforw \
 621        -state disabled -width 26
 622    pack .tf.bar.rightbut -side left -fill y
 623
 624    button .tf.bar.findbut -text "Find" -command dofind -font $uifont
 625    pack .tf.bar.findbut -side left
 626    set findstring {}
 627    set fstring .tf.bar.findstring
 628    lappend entries $fstring
 629    entry $fstring -width 30 -font $textfont -textvariable findstring
 630    trace add variable findstring write find_change
 631    pack $fstring -side left -expand 1 -fill x -in .tf.bar
 632    set findtype Exact
 633    set findtypemenu [tk_optionMenu .tf.bar.findtype \
 634                      findtype Exact IgnCase Regexp]
 635    trace add variable findtype write find_change
 636    .tf.bar.findtype configure -font $uifont
 637    .tf.bar.findtype.menu configure -font $uifont
 638    set findloc "All fields"
 639    tk_optionMenu .tf.bar.findloc findloc "All fields" Headline \
 640        Comments Author Committer
 641    trace add variable findloc write find_change
 642    .tf.bar.findloc configure -font $uifont
 643    .tf.bar.findloc.menu configure -font $uifont
 644    pack .tf.bar.findloc -side right
 645    pack .tf.bar.findtype -side right
 646
 647    # build up the bottom bar of upper window
 648    label .tf.lbar.flabel -text "Highlight:  Commits " \
 649    -font $uifont
 650    pack .tf.lbar.flabel -side left -fill y
 651    set gdttype "touching paths:"
 652    set gm [tk_optionMenu .tf.lbar.gdttype gdttype "touching paths:" \
 653        "adding/removing string:"]
 654    trace add variable gdttype write hfiles_change
 655    $gm conf -font $uifont
 656    .tf.lbar.gdttype conf -font $uifont
 657    pack .tf.lbar.gdttype -side left -fill y
 658    entry .tf.lbar.fent -width 25 -font $textfont \
 659        -textvariable highlight_files
 660    trace add variable highlight_files write hfiles_change
 661    lappend entries .tf.lbar.fent
 662    pack .tf.lbar.fent -side left -fill x -expand 1
 663    label .tf.lbar.vlabel -text " OR in view" -font $uifont
 664    pack .tf.lbar.vlabel -side left -fill y
 665    global viewhlmenu selectedhlview
 666    set viewhlmenu [tk_optionMenu .tf.lbar.vhl selectedhlview None]
 667    $viewhlmenu entryconf None -command delvhighlight
 668    $viewhlmenu conf -font $uifont
 669    .tf.lbar.vhl conf -font $uifont
 670    pack .tf.lbar.vhl -side left -fill y
 671    label .tf.lbar.rlabel -text " OR " -font $uifont
 672    pack .tf.lbar.rlabel -side left -fill y
 673    global highlight_related
 674    set m [tk_optionMenu .tf.lbar.relm highlight_related None \
 675        "Descendent" "Not descendent" "Ancestor" "Not ancestor"]
 676    $m conf -font $uifont
 677    .tf.lbar.relm conf -font $uifont
 678    trace add variable highlight_related write vrel_change
 679    pack .tf.lbar.relm -side left -fill y
 680
 681    # Finish putting the upper half of the viewer together
 682    pack .tf.lbar -in .tf -side bottom -fill x
 683    pack .tf.bar -in .tf -side bottom -fill x
 684    pack .tf.histframe -fill both -side top -expand 1
 685    .ctop add .tf
 686    .ctop paneconfigure .tf -height $geometry(topheight)
 687    .ctop paneconfigure .tf -width $geometry(topwidth)
 688
 689    # now build up the bottom
 690    panedwindow .pwbottom -orient horizontal
 691
 692    # lower left, a text box over search bar, scroll bar to the right
 693    # if we know window height, then that will set the lower text height, otherwise
 694    # we set lower text height which will drive window height
 695    if {[info exists geometry(main)]} {
 696        frame .bleft -width $geometry(botwidth)
 697    } else {
 698        frame .bleft -width $geometry(botwidth) -height $geometry(botheight)
 699    }
 700    frame .bleft.top
 701    frame .bleft.mid
 702
 703    button .bleft.top.search -text "Search" -command dosearch \
 704        -font $uifont
 705    pack .bleft.top.search -side left -padx 5
 706    set sstring .bleft.top.sstring
 707    entry $sstring -width 20 -font $textfont -textvariable searchstring
 708    lappend entries $sstring
 709    trace add variable searchstring write incrsearch
 710    pack $sstring -side left -expand 1 -fill x
 711    radiobutton .bleft.mid.diff -text "Diff" \
 712        -command changediffdisp -variable diffelide -value {0 0}
 713    radiobutton .bleft.mid.old -text "Old version" \
 714        -command changediffdisp -variable diffelide -value {0 1}
 715    radiobutton .bleft.mid.new -text "New version" \
 716        -command changediffdisp -variable diffelide -value {1 0}
 717    pack .bleft.mid.diff .bleft.mid.old .bleft.mid.new -side left
 718    set ctext .bleft.ctext
 719    text $ctext -background $bgcolor -foreground $fgcolor \
 720        -tabs "[expr {$tabstop * $charspc}]" \
 721        -state disabled -font $textfont \
 722        -yscrollcommand scrolltext -wrap none
 723    scrollbar .bleft.sb -command "$ctext yview"
 724    pack .bleft.top -side top -fill x
 725    pack .bleft.mid -side top -fill x
 726    pack .bleft.sb -side right -fill y
 727    pack $ctext -side left -fill both -expand 1
 728    lappend bglist $ctext
 729    lappend fglist $ctext
 730
 731    $ctext tag conf comment -wrap $wrapcomment
 732    $ctext tag conf filesep -font [concat $textfont bold] -back "#aaaaaa"
 733    $ctext tag conf hunksep -fore [lindex $diffcolors 2]
 734    $ctext tag conf d0 -fore [lindex $diffcolors 0]
 735    $ctext tag conf d1 -fore [lindex $diffcolors 1]
 736    $ctext tag conf m0 -fore red
 737    $ctext tag conf m1 -fore blue
 738    $ctext tag conf m2 -fore green
 739    $ctext tag conf m3 -fore purple
 740    $ctext tag conf m4 -fore brown
 741    $ctext tag conf m5 -fore "#009090"
 742    $ctext tag conf m6 -fore magenta
 743    $ctext tag conf m7 -fore "#808000"
 744    $ctext tag conf m8 -fore "#009000"
 745    $ctext tag conf m9 -fore "#ff0080"
 746    $ctext tag conf m10 -fore cyan
 747    $ctext tag conf m11 -fore "#b07070"
 748    $ctext tag conf m12 -fore "#70b0f0"
 749    $ctext tag conf m13 -fore "#70f0b0"
 750    $ctext tag conf m14 -fore "#f0b070"
 751    $ctext tag conf m15 -fore "#ff70b0"
 752    $ctext tag conf mmax -fore darkgrey
 753    set mergemax 16
 754    $ctext tag conf mresult -font [concat $textfont bold]
 755    $ctext tag conf msep -font [concat $textfont bold]
 756    $ctext tag conf found -back yellow
 757
 758    .pwbottom add .bleft
 759    .pwbottom paneconfigure .bleft -width $geometry(botwidth)
 760
 761    # lower right
 762    frame .bright
 763    frame .bright.mode
 764    radiobutton .bright.mode.patch -text "Patch" \
 765        -command reselectline -variable cmitmode -value "patch"
 766    .bright.mode.patch configure -font $uifont
 767    radiobutton .bright.mode.tree -text "Tree" \
 768        -command reselectline -variable cmitmode -value "tree"
 769    .bright.mode.tree configure -font $uifont
 770    grid .bright.mode.patch .bright.mode.tree -sticky ew
 771    pack .bright.mode -side top -fill x
 772    set cflist .bright.cfiles
 773    set indent [font measure $mainfont "nn"]
 774    text $cflist \
 775        -selectbackground $selectbgcolor \
 776        -background $bgcolor -foreground $fgcolor \
 777        -font $mainfont \
 778        -tabs [list $indent [expr {2 * $indent}]] \
 779        -yscrollcommand ".bright.sb set" \
 780        -cursor [. cget -cursor] \
 781        -spacing1 1 -spacing3 1
 782    lappend bglist $cflist
 783    lappend fglist $cflist
 784    scrollbar .bright.sb -command "$cflist yview"
 785    pack .bright.sb -side right -fill y
 786    pack $cflist -side left -fill both -expand 1
 787    $cflist tag configure highlight \
 788        -background [$cflist cget -selectbackground]
 789    $cflist tag configure bold -font [concat $mainfont bold]
 790
 791    .pwbottom add .bright
 792    .ctop add .pwbottom
 793
 794    # restore window position if known
 795    if {[info exists geometry(main)]} {
 796        wm geometry . "$geometry(main)"
 797    }
 798
 799    bind .pwbottom <Configure> {resizecdetpanes %W %w}
 800    pack .ctop -fill both -expand 1
 801    bindall <1> {selcanvline %W %x %y}
 802    #bindall <B1-Motion> {selcanvline %W %x %y}
 803    bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
 804    bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
 805    bindall <2> "canvscan mark %W %x %y"
 806    bindall <B2-Motion> "canvscan dragto %W %x %y"
 807    bindkey <Home> selfirstline
 808    bindkey <End> sellastline
 809    bind . <Key-Up> "selnextline -1"
 810    bind . <Key-Down> "selnextline 1"
 811    bind . <Shift-Key-Up> "next_highlight -1"
 812    bind . <Shift-Key-Down> "next_highlight 1"
 813    bindkey <Key-Right> "goforw"
 814    bindkey <Key-Left> "goback"
 815    bind . <Key-Prior> "selnextpage -1"
 816    bind . <Key-Next> "selnextpage 1"
 817    bind . <Control-Home> "allcanvs yview moveto 0.0"
 818    bind . <Control-End> "allcanvs yview moveto 1.0"
 819    bind . <Control-Key-Up> "allcanvs yview scroll -1 units"
 820    bind . <Control-Key-Down> "allcanvs yview scroll 1 units"
 821    bind . <Control-Key-Prior> "allcanvs yview scroll -1 pages"
 822    bind . <Control-Key-Next> "allcanvs yview scroll 1 pages"
 823    bindkey <Key-Delete> "$ctext yview scroll -1 pages"
 824    bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
 825    bindkey <Key-space> "$ctext yview scroll 1 pages"
 826    bindkey p "selnextline -1"
 827    bindkey n "selnextline 1"
 828    bindkey z "goback"
 829    bindkey x "goforw"
 830    bindkey i "selnextline -1"
 831    bindkey k "selnextline 1"
 832    bindkey j "goback"
 833    bindkey l "goforw"
 834    bindkey b "$ctext yview scroll -1 pages"
 835    bindkey d "$ctext yview scroll 18 units"
 836    bindkey u "$ctext yview scroll -18 units"
 837    bindkey / {findnext 1}
 838    bindkey <Key-Return> {findnext 0}
 839    bindkey ? findprev
 840    bindkey f nextfile
 841    bindkey <F5> updatecommits
 842    bind . <Control-q> doquit
 843    bind . <Control-f> dofind
 844    bind . <Control-g> {findnext 0}
 845    bind . <Control-r> dosearchback
 846    bind . <Control-s> dosearch
 847    bind . <Control-equal> {incrfont 1}
 848    bind . <Control-KP_Add> {incrfont 1}
 849    bind . <Control-minus> {incrfont -1}
 850    bind . <Control-KP_Subtract> {incrfont -1}
 851    wm protocol . WM_DELETE_WINDOW doquit
 852    bind . <Button-1> "click %W"
 853    bind $fstring <Key-Return> dofind
 854    bind $sha1entry <Key-Return> gotocommit
 855    bind $sha1entry <<PasteSelection>> clearsha1
 856    bind $cflist <1> {sel_flist %W %x %y; break}
 857    bind $cflist <B1-Motion> {sel_flist %W %x %y; break}
 858    bind $cflist <ButtonRelease-1> {treeclick %W %x %y}
 859
 860    set maincursor [. cget -cursor]
 861    set textcursor [$ctext cget -cursor]
 862    set curtextcursor $textcursor
 863
 864    set rowctxmenu .rowctxmenu
 865    menu $rowctxmenu -tearoff 0
 866    $rowctxmenu add command -label "Diff this -> selected" \
 867        -command {diffvssel 0}
 868    $rowctxmenu add command -label "Diff selected -> this" \
 869        -command {diffvssel 1}
 870    $rowctxmenu add command -label "Make patch" -command mkpatch
 871    $rowctxmenu add command -label "Create tag" -command mktag
 872    $rowctxmenu add command -label "Write commit to file" -command writecommit
 873    $rowctxmenu add command -label "Create new branch" -command mkbranch
 874    $rowctxmenu add command -label "Cherry-pick this commit" \
 875        -command cherrypick
 876
 877    set fakerowmenu .fakerowmenu
 878    menu $fakerowmenu -tearoff 0
 879    $fakerowmenu add command -label "Diff this -> selected" \
 880        -command {diffvssel 0}
 881    $fakerowmenu add command -label "Diff selected -> this" \
 882        -command {diffvssel 1}
 883    $fakerowmenu add command -label "Make patch" -command mkpatch
 884#    $fakerowmenu add command -label "Commit" -command {mkcommit 0}
 885#    $fakerowmenu add command -label "Commit all" -command {mkcommit 1}
 886#    $fakerowmenu add command -label "Revert local changes" -command revertlocal
 887
 888    set headctxmenu .headctxmenu
 889    menu $headctxmenu -tearoff 0
 890    $headctxmenu add command -label "Check out this branch" \
 891        -command cobranch
 892    $headctxmenu add command -label "Remove this branch" \
 893        -command rmbranch
 894}
 895
 896# mouse-2 makes all windows scan vertically, but only the one
 897# the cursor is in scans horizontally
 898proc canvscan {op w x y} {
 899    global canv canv2 canv3
 900    foreach c [list $canv $canv2 $canv3] {
 901        if {$c == $w} {
 902            $c scan $op $x $y
 903        } else {
 904            $c scan $op 0 $y
 905        }
 906    }
 907}
 908
 909proc scrollcanv {cscroll f0 f1} {
 910    $cscroll set $f0 $f1
 911    drawfrac $f0 $f1
 912    flushhighlights
 913}
 914
 915# when we make a key binding for the toplevel, make sure
 916# it doesn't get triggered when that key is pressed in the
 917# find string entry widget.
 918proc bindkey {ev script} {
 919    global entries
 920    bind . $ev $script
 921    set escript [bind Entry $ev]
 922    if {$escript == {}} {
 923        set escript [bind Entry <Key>]
 924    }
 925    foreach e $entries {
 926        bind $e $ev "$escript; break"
 927    }
 928}
 929
 930# set the focus back to the toplevel for any click outside
 931# the entry widgets
 932proc click {w} {
 933    global entries
 934    foreach e $entries {
 935        if {$w == $e} return
 936    }
 937    focus .
 938}
 939
 940proc savestuff {w} {
 941    global canv canv2 canv3 ctext cflist mainfont textfont uifont tabstop
 942    global stuffsaved findmergefiles maxgraphpct
 943    global maxwidth showneartags showlocalchanges
 944    global viewname viewfiles viewargs viewperm nextviewnum
 945    global cmitmode wrapcomment
 946    global colors bgcolor fgcolor diffcolors selectbgcolor
 947
 948    if {$stuffsaved} return
 949    if {![winfo viewable .]} return
 950    catch {
 951        set f [open "~/.gitk-new" w]
 952        puts $f [list set mainfont $mainfont]
 953        puts $f [list set textfont $textfont]
 954        puts $f [list set uifont $uifont]
 955        puts $f [list set tabstop $tabstop]
 956        puts $f [list set findmergefiles $findmergefiles]
 957        puts $f [list set maxgraphpct $maxgraphpct]
 958        puts $f [list set maxwidth $maxwidth]
 959        puts $f [list set cmitmode $cmitmode]
 960        puts $f [list set wrapcomment $wrapcomment]
 961        puts $f [list set showneartags $showneartags]
 962        puts $f [list set showlocalchanges $showlocalchanges]
 963        puts $f [list set bgcolor $bgcolor]
 964        puts $f [list set fgcolor $fgcolor]
 965        puts $f [list set colors $colors]
 966        puts $f [list set diffcolors $diffcolors]
 967        puts $f [list set selectbgcolor $selectbgcolor]
 968
 969        puts $f "set geometry(main) [wm geometry .]"
 970        puts $f "set geometry(topwidth) [winfo width .tf]"
 971        puts $f "set geometry(topheight) [winfo height .tf]"
 972        puts $f "set geometry(pwsash0) \"[.tf.histframe.pwclist sash coord 0]\""
 973        puts $f "set geometry(pwsash1) \"[.tf.histframe.pwclist sash coord 1]\""
 974        puts $f "set geometry(botwidth) [winfo width .bleft]"
 975        puts $f "set geometry(botheight) [winfo height .bleft]"
 976
 977        puts -nonewline $f "set permviews {"
 978        for {set v 0} {$v < $nextviewnum} {incr v} {
 979            if {$viewperm($v)} {
 980                puts $f "{[list $viewname($v) $viewfiles($v) $viewargs($v)]}"
 981            }
 982        }
 983        puts $f "}"
 984        close $f
 985        file rename -force "~/.gitk-new" "~/.gitk"
 986    }
 987    set stuffsaved 1
 988}
 989
 990proc resizeclistpanes {win w} {
 991    global oldwidth
 992    if {[info exists oldwidth($win)]} {
 993        set s0 [$win sash coord 0]
 994        set s1 [$win sash coord 1]
 995        if {$w < 60} {
 996            set sash0 [expr {int($w/2 - 2)}]
 997            set sash1 [expr {int($w*5/6 - 2)}]
 998        } else {
 999            set factor [expr {1.0 * $w / $oldwidth($win)}]
1000            set sash0 [expr {int($factor * [lindex $s0 0])}]
1001            set sash1 [expr {int($factor * [lindex $s1 0])}]
1002            if {$sash0 < 30} {
1003                set sash0 30
1004            }
1005            if {$sash1 < $sash0 + 20} {
1006                set sash1 [expr {$sash0 + 20}]
1007            }
1008            if {$sash1 > $w - 10} {
1009                set sash1 [expr {$w - 10}]
1010                if {$sash0 > $sash1 - 20} {
1011                    set sash0 [expr {$sash1 - 20}]
1012                }
1013            }
1014        }
1015        $win sash place 0 $sash0 [lindex $s0 1]
1016        $win sash place 1 $sash1 [lindex $s1 1]
1017    }
1018    set oldwidth($win) $w
1019}
1020
1021proc resizecdetpanes {win w} {
1022    global oldwidth
1023    if {[info exists oldwidth($win)]} {
1024        set s0 [$win sash coord 0]
1025        if {$w < 60} {
1026            set sash0 [expr {int($w*3/4 - 2)}]
1027        } else {
1028            set factor [expr {1.0 * $w / $oldwidth($win)}]
1029            set sash0 [expr {int($factor * [lindex $s0 0])}]
1030            if {$sash0 < 45} {
1031                set sash0 45
1032            }
1033            if {$sash0 > $w - 15} {
1034                set sash0 [expr {$w - 15}]
1035            }
1036        }
1037        $win sash place 0 $sash0 [lindex $s0 1]
1038    }
1039    set oldwidth($win) $w
1040}
1041
1042proc allcanvs args {
1043    global canv canv2 canv3
1044    eval $canv $args
1045    eval $canv2 $args
1046    eval $canv3 $args
1047}
1048
1049proc bindall {event action} {
1050    global canv canv2 canv3
1051    bind $canv $event $action
1052    bind $canv2 $event $action
1053    bind $canv3 $event $action
1054}
1055
1056proc about {} {
1057    global uifont
1058    set w .about
1059    if {[winfo exists $w]} {
1060        raise $w
1061        return
1062    }
1063    toplevel $w
1064    wm title $w "About gitk"
1065    message $w.m -text {
1066Gitk - a commit viewer for git
1067
1068Copyright © 2005-2006 Paul Mackerras
1069
1070Use and redistribute under the terms of the GNU General Public License} \
1071            -justify center -aspect 400 -border 2 -bg white -relief groove
1072    pack $w.m -side top -fill x -padx 2 -pady 2
1073    $w.m configure -font $uifont
1074    button $w.ok -text Close -command "destroy $w" -default active
1075    pack $w.ok -side bottom
1076    $w.ok configure -font $uifont
1077    bind $w <Visibility> "focus $w.ok"
1078    bind $w <Key-Escape> "destroy $w"
1079    bind $w <Key-Return> "destroy $w"
1080}
1081
1082proc keys {} {
1083    global uifont
1084    set w .keys
1085    if {[winfo exists $w]} {
1086        raise $w
1087        return
1088    }
1089    toplevel $w
1090    wm title $w "Gitk key bindings"
1091    message $w.m -text {
1092Gitk key bindings:
1093
1094<Ctrl-Q>                Quit
1095<Home>          Move to first commit
1096<End>           Move to last commit
1097<Up>, p, i      Move up one commit
1098<Down>, n, k    Move down one commit
1099<Left>, z, j    Go back in history list
1100<Right>, x, l   Go forward in history list
1101<PageUp>        Move up one page in commit list
1102<PageDown>      Move down one page in commit list
1103<Ctrl-Home>     Scroll to top of commit list
1104<Ctrl-End>      Scroll to bottom of commit list
1105<Ctrl-Up>       Scroll commit list up one line
1106<Ctrl-Down>     Scroll commit list down one line
1107<Ctrl-PageUp>   Scroll commit list up one page
1108<Ctrl-PageDown> Scroll commit list down one page
1109<Shift-Up>      Move to previous highlighted line
1110<Shift-Down>    Move to next highlighted line
1111<Delete>, b     Scroll diff view up one page
1112<Backspace>     Scroll diff view up one page
1113<Space>         Scroll diff view down one page
1114u               Scroll diff view up 18 lines
1115d               Scroll diff view down 18 lines
1116<Ctrl-F>                Find
1117<Ctrl-G>                Move to next find hit
1118<Return>        Move to next find hit
1119/               Move to next find hit, or redo find
1120?               Move to previous find hit
1121f               Scroll diff view to next file
1122<Ctrl-S>                Search for next hit in diff view
1123<Ctrl-R>                Search for previous hit in diff view
1124<Ctrl-KP+>      Increase font size
1125<Ctrl-plus>     Increase font size
1126<Ctrl-KP->      Decrease font size
1127<Ctrl-minus>    Decrease font size
1128<F5>            Update
1129} \
1130            -justify left -bg white -border 2 -relief groove
1131    pack $w.m -side top -fill both -padx 2 -pady 2
1132    $w.m configure -font $uifont
1133    button $w.ok -text Close -command "destroy $w" -default active
1134    pack $w.ok -side bottom
1135    $w.ok configure -font $uifont
1136    bind $w <Visibility> "focus $w.ok"
1137    bind $w <Key-Escape> "destroy $w"
1138    bind $w <Key-Return> "destroy $w"
1139}
1140
1141# Procedures for manipulating the file list window at the
1142# bottom right of the overall window.
1143
1144proc treeview {w l openlevs} {
1145    global treecontents treediropen treeheight treeparent treeindex
1146
1147    set ix 0
1148    set treeindex() 0
1149    set lev 0
1150    set prefix {}
1151    set prefixend -1
1152    set prefendstack {}
1153    set htstack {}
1154    set ht 0
1155    set treecontents() {}
1156    $w conf -state normal
1157    foreach f $l {
1158        while {[string range $f 0 $prefixend] ne $prefix} {
1159            if {$lev <= $openlevs} {
1160                $w mark set e:$treeindex($prefix) "end -1c"
1161                $w mark gravity e:$treeindex($prefix) left
1162            }
1163            set treeheight($prefix) $ht
1164            incr ht [lindex $htstack end]
1165            set htstack [lreplace $htstack end end]
1166            set prefixend [lindex $prefendstack end]
1167            set prefendstack [lreplace $prefendstack end end]
1168            set prefix [string range $prefix 0 $prefixend]
1169            incr lev -1
1170        }
1171        set tail [string range $f [expr {$prefixend+1}] end]
1172        while {[set slash [string first "/" $tail]] >= 0} {
1173            lappend htstack $ht
1174            set ht 0
1175            lappend prefendstack $prefixend
1176            incr prefixend [expr {$slash + 1}]
1177            set d [string range $tail 0 $slash]
1178            lappend treecontents($prefix) $d
1179            set oldprefix $prefix
1180            append prefix $d
1181            set treecontents($prefix) {}
1182            set treeindex($prefix) [incr ix]
1183            set treeparent($prefix) $oldprefix
1184            set tail [string range $tail [expr {$slash+1}] end]
1185            if {$lev <= $openlevs} {
1186                set ht 1
1187                set treediropen($prefix) [expr {$lev < $openlevs}]
1188                set bm [expr {$lev == $openlevs? "tri-rt": "tri-dn"}]
1189                $w mark set d:$ix "end -1c"
1190                $w mark gravity d:$ix left
1191                set str "\n"
1192                for {set i 0} {$i < $lev} {incr i} {append str "\t"}
1193                $w insert end $str
1194                $w image create end -align center -image $bm -padx 1 \
1195                    -name a:$ix
1196                $w insert end $d [highlight_tag $prefix]
1197                $w mark set s:$ix "end -1c"
1198                $w mark gravity s:$ix left
1199            }
1200            incr lev
1201        }
1202        if {$tail ne {}} {
1203            if {$lev <= $openlevs} {
1204                incr ht
1205                set str "\n"
1206                for {set i 0} {$i < $lev} {incr i} {append str "\t"}
1207                $w insert end $str
1208                $w insert end $tail [highlight_tag $f]
1209            }
1210            lappend treecontents($prefix) $tail
1211        }
1212    }
1213    while {$htstack ne {}} {
1214        set treeheight($prefix) $ht
1215        incr ht [lindex $htstack end]
1216        set htstack [lreplace $htstack end end]
1217    }
1218    $w conf -state disabled
1219}
1220
1221proc linetoelt {l} {
1222    global treeheight treecontents
1223
1224    set y 2
1225    set prefix {}
1226    while {1} {
1227        foreach e $treecontents($prefix) {
1228            if {$y == $l} {
1229                return "$prefix$e"
1230            }
1231            set n 1
1232            if {[string index $e end] eq "/"} {
1233                set n $treeheight($prefix$e)
1234                if {$y + $n > $l} {
1235                    append prefix $e
1236                    incr y
1237                    break
1238                }
1239            }
1240            incr y $n
1241        }
1242    }
1243}
1244
1245proc highlight_tree {y prefix} {
1246    global treeheight treecontents cflist
1247
1248    foreach e $treecontents($prefix) {
1249        set path $prefix$e
1250        if {[highlight_tag $path] ne {}} {
1251            $cflist tag add bold $y.0 "$y.0 lineend"
1252        }
1253        incr y
1254        if {[string index $e end] eq "/" && $treeheight($path) > 1} {
1255            set y [highlight_tree $y $path]
1256        }
1257    }
1258    return $y
1259}
1260
1261proc treeclosedir {w dir} {
1262    global treediropen treeheight treeparent treeindex
1263
1264    set ix $treeindex($dir)
1265    $w conf -state normal
1266    $w delete s:$ix e:$ix
1267    set treediropen($dir) 0
1268    $w image configure a:$ix -image tri-rt
1269    $w conf -state disabled
1270    set n [expr {1 - $treeheight($dir)}]
1271    while {$dir ne {}} {
1272        incr treeheight($dir) $n
1273        set dir $treeparent($dir)
1274    }
1275}
1276
1277proc treeopendir {w dir} {
1278    global treediropen treeheight treeparent treecontents treeindex
1279
1280    set ix $treeindex($dir)
1281    $w conf -state normal
1282    $w image configure a:$ix -image tri-dn
1283    $w mark set e:$ix s:$ix
1284    $w mark gravity e:$ix right
1285    set lev 0
1286    set str "\n"
1287    set n [llength $treecontents($dir)]
1288    for {set x $dir} {$x ne {}} {set x $treeparent($x)} {
1289        incr lev
1290        append str "\t"
1291        incr treeheight($x) $n
1292    }
1293    foreach e $treecontents($dir) {
1294        set de $dir$e
1295        if {[string index $e end] eq "/"} {
1296            set iy $treeindex($de)
1297            $w mark set d:$iy e:$ix
1298            $w mark gravity d:$iy left
1299            $w insert e:$ix $str
1300            set treediropen($de) 0
1301            $w image create e:$ix -align center -image tri-rt -padx 1 \
1302                -name a:$iy
1303            $w insert e:$ix $e [highlight_tag $de]
1304            $w mark set s:$iy e:$ix
1305            $w mark gravity s:$iy left
1306            set treeheight($de) 1
1307        } else {
1308            $w insert e:$ix $str
1309            $w insert e:$ix $e [highlight_tag $de]
1310        }
1311    }
1312    $w mark gravity e:$ix left
1313    $w conf -state disabled
1314    set treediropen($dir) 1
1315    set top [lindex [split [$w index @0,0] .] 0]
1316    set ht [$w cget -height]
1317    set l [lindex [split [$w index s:$ix] .] 0]
1318    if {$l < $top} {
1319        $w yview $l.0
1320    } elseif {$l + $n + 1 > $top + $ht} {
1321        set top [expr {$l + $n + 2 - $ht}]
1322        if {$l < $top} {
1323            set top $l
1324        }
1325        $w yview $top.0
1326    }
1327}
1328
1329proc treeclick {w x y} {
1330    global treediropen cmitmode ctext cflist cflist_top
1331
1332    if {$cmitmode ne "tree"} return
1333    if {![info exists cflist_top]} return
1334    set l [lindex [split [$w index "@$x,$y"] "."] 0]
1335    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
1336    $cflist tag add highlight $l.0 "$l.0 lineend"
1337    set cflist_top $l
1338    if {$l == 1} {
1339        $ctext yview 1.0
1340        return
1341    }
1342    set e [linetoelt $l]
1343    if {[string index $e end] ne "/"} {
1344        showfile $e
1345    } elseif {$treediropen($e)} {
1346        treeclosedir $w $e
1347    } else {
1348        treeopendir $w $e
1349    }
1350}
1351
1352proc setfilelist {id} {
1353    global treefilelist cflist
1354
1355    treeview $cflist $treefilelist($id) 0
1356}
1357
1358image create bitmap tri-rt -background black -foreground blue -data {
1359    #define tri-rt_width 13
1360    #define tri-rt_height 13
1361    static unsigned char tri-rt_bits[] = {
1362       0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x70, 0x00, 0xf0, 0x00,
1363       0xf0, 0x01, 0xf0, 0x00, 0x70, 0x00, 0x30, 0x00, 0x10, 0x00, 0x00, 0x00,
1364       0x00, 0x00};
1365} -maskdata {
1366    #define tri-rt-mask_width 13
1367    #define tri-rt-mask_height 13
1368    static unsigned char tri-rt-mask_bits[] = {
1369       0x08, 0x00, 0x18, 0x00, 0x38, 0x00, 0x78, 0x00, 0xf8, 0x00, 0xf8, 0x01,
1370       0xf8, 0x03, 0xf8, 0x01, 0xf8, 0x00, 0x78, 0x00, 0x38, 0x00, 0x18, 0x00,
1371       0x08, 0x00};
1372}
1373image create bitmap tri-dn -background black -foreground blue -data {
1374    #define tri-dn_width 13
1375    #define tri-dn_height 13
1376    static unsigned char tri-dn_bits[] = {
1377       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x07, 0xf8, 0x03,
1378       0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1379       0x00, 0x00};
1380} -maskdata {
1381    #define tri-dn-mask_width 13
1382    #define tri-dn-mask_height 13
1383    static unsigned char tri-dn-mask_bits[] = {
1384       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x1f, 0xfe, 0x0f, 0xfc, 0x07,
1385       0xf8, 0x03, 0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
1386       0x00, 0x00};
1387}
1388
1389proc init_flist {first} {
1390    global cflist cflist_top selectedline difffilestart
1391
1392    $cflist conf -state normal
1393    $cflist delete 0.0 end
1394    if {$first ne {}} {
1395        $cflist insert end $first
1396        set cflist_top 1
1397        $cflist tag add highlight 1.0 "1.0 lineend"
1398    } else {
1399        catch {unset cflist_top}
1400    }
1401    $cflist conf -state disabled
1402    set difffilestart {}
1403}
1404
1405proc highlight_tag {f} {
1406    global highlight_paths
1407
1408    foreach p $highlight_paths {
1409        if {[string match $p $f]} {
1410            return "bold"
1411        }
1412    }
1413    return {}
1414}
1415
1416proc highlight_filelist {} {
1417    global cmitmode cflist
1418
1419    $cflist conf -state normal
1420    if {$cmitmode ne "tree"} {
1421        set end [lindex [split [$cflist index end] .] 0]
1422        for {set l 2} {$l < $end} {incr l} {
1423            set line [$cflist get $l.0 "$l.0 lineend"]
1424            if {[highlight_tag $line] ne {}} {
1425                $cflist tag add bold $l.0 "$l.0 lineend"
1426            }
1427        }
1428    } else {
1429        highlight_tree 2 {}
1430    }
1431    $cflist conf -state disabled
1432}
1433
1434proc unhighlight_filelist {} {
1435    global cflist
1436
1437    $cflist conf -state normal
1438    $cflist tag remove bold 1.0 end
1439    $cflist conf -state disabled
1440}
1441
1442proc add_flist {fl} {
1443    global cflist
1444
1445    $cflist conf -state normal
1446    foreach f $fl {
1447        $cflist insert end "\n"
1448        $cflist insert end $f [highlight_tag $f]
1449    }
1450    $cflist conf -state disabled
1451}
1452
1453proc sel_flist {w x y} {
1454    global ctext difffilestart cflist cflist_top cmitmode
1455
1456    if {$cmitmode eq "tree"} return
1457    if {![info exists cflist_top]} return
1458    set l [lindex [split [$w index "@$x,$y"] "."] 0]
1459    $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend"
1460    $cflist tag add highlight $l.0 "$l.0 lineend"
1461    set cflist_top $l
1462    if {$l == 1} {
1463        $ctext yview 1.0
1464    } else {
1465        catch {$ctext yview [lindex $difffilestart [expr {$l - 2}]]}
1466    }
1467}
1468
1469# Functions for adding and removing shell-type quoting
1470
1471proc shellquote {str} {
1472    if {![string match "*\['\"\\ \t]*" $str]} {
1473        return $str
1474    }
1475    if {![string match "*\['\"\\]*" $str]} {
1476        return "\"$str\""
1477    }
1478    if {![string match "*'*" $str]} {
1479        return "'$str'"
1480    }
1481    return "\"[string map {\" \\\" \\ \\\\} $str]\""
1482}
1483
1484proc shellarglist {l} {
1485    set str {}
1486    foreach a $l {
1487        if {$str ne {}} {
1488            append str " "
1489        }
1490        append str [shellquote $a]
1491    }
1492    return $str
1493}
1494
1495proc shelldequote {str} {
1496    set ret {}
1497    set used -1
1498    while {1} {
1499        incr used
1500        if {![regexp -start $used -indices "\['\"\\\\ \t]" $str first]} {
1501            append ret [string range $str $used end]
1502            set used [string length $str]
1503            break
1504        }
1505        set first [lindex $first 0]
1506        set ch [string index $str $first]
1507        if {$first > $used} {
1508            append ret [string range $str $used [expr {$first - 1}]]
1509            set used $first
1510        }
1511        if {$ch eq " " || $ch eq "\t"} break
1512        incr used
1513        if {$ch eq "'"} {
1514            set first [string first "'" $str $used]
1515            if {$first < 0} {
1516                error "unmatched single-quote"
1517            }
1518            append ret [string range $str $used [expr {$first - 1}]]
1519            set used $first
1520            continue
1521        }
1522        if {$ch eq "\\"} {
1523            if {$used >= [string length $str]} {
1524                error "trailing backslash"
1525            }
1526            append ret [string index $str $used]
1527            continue
1528        }
1529        # here ch == "\""
1530        while {1} {
1531            if {![regexp -start $used -indices "\[\"\\\\]" $str first]} {
1532                error "unmatched double-quote"
1533            }
1534            set first [lindex $first 0]
1535            set ch [string index $str $first]
1536            if {$first > $used} {
1537                append ret [string range $str $used [expr {$first - 1}]]
1538                set used $first
1539            }
1540            if {$ch eq "\""} break
1541            incr used
1542            append ret [string index $str $used]
1543            incr used
1544        }
1545    }
1546    return [list $used $ret]
1547}
1548
1549proc shellsplit {str} {
1550    set l {}
1551    while {1} {
1552        set str [string trimleft $str]
1553        if {$str eq {}} break
1554        set dq [shelldequote $str]
1555        set n [lindex $dq 0]
1556        set word [lindex $dq 1]
1557        set str [string range $str $n end]
1558        lappend l $word
1559    }
1560    return $l
1561}
1562
1563# Code to implement multiple views
1564
1565proc newview {ishighlight} {
1566    global nextviewnum newviewname newviewperm uifont newishighlight
1567    global newviewargs revtreeargs
1568
1569    set newishighlight $ishighlight
1570    set top .gitkview
1571    if {[winfo exists $top]} {
1572        raise $top
1573        return
1574    }
1575    set newviewname($nextviewnum) "View $nextviewnum"
1576    set newviewperm($nextviewnum) 0
1577    set newviewargs($nextviewnum) [shellarglist $revtreeargs]
1578    vieweditor $top $nextviewnum "Gitk view definition"
1579}
1580
1581proc editview {} {
1582    global curview
1583    global viewname viewperm newviewname newviewperm
1584    global viewargs newviewargs
1585
1586    set top .gitkvedit-$curview
1587    if {[winfo exists $top]} {
1588        raise $top
1589        return
1590    }
1591    set newviewname($curview) $viewname($curview)
1592    set newviewperm($curview) $viewperm($curview)
1593    set newviewargs($curview) [shellarglist $viewargs($curview)]
1594    vieweditor $top $curview "Gitk: edit view $viewname($curview)"
1595}
1596
1597proc vieweditor {top n title} {
1598    global newviewname newviewperm viewfiles
1599    global uifont
1600
1601    toplevel $top
1602    wm title $top $title
1603    label $top.nl -text "Name" -font $uifont
1604    entry $top.name -width 20 -textvariable newviewname($n) -font $uifont
1605    grid $top.nl $top.name -sticky w -pady 5
1606    checkbutton $top.perm -text "Remember this view" -variable newviewperm($n) \
1607        -font $uifont
1608    grid $top.perm - -pady 5 -sticky w
1609    message $top.al -aspect 1000 -font $uifont \
1610        -text "Commits to include (arguments to git rev-list):"
1611    grid $top.al - -sticky w -pady 5
1612    entry $top.args -width 50 -textvariable newviewargs($n) \
1613        -background white -font $uifont
1614    grid $top.args - -sticky ew -padx 5
1615    message $top.l -aspect 1000 -font $uifont \
1616        -text "Enter files and directories to include, one per line:"
1617    grid $top.l - -sticky w
1618    text $top.t -width 40 -height 10 -background white -font $uifont
1619    if {[info exists viewfiles($n)]} {
1620        foreach f $viewfiles($n) {
1621            $top.t insert end $f
1622            $top.t insert end "\n"
1623        }
1624        $top.t delete {end - 1c} end
1625        $top.t mark set insert 0.0
1626    }
1627    grid $top.t - -sticky ew -padx 5
1628    frame $top.buts
1629    button $top.buts.ok -text "OK" -command [list newviewok $top $n] \
1630        -font $uifont
1631    button $top.buts.can -text "Cancel" -command [list destroy $top] \
1632        -font $uifont
1633    grid $top.buts.ok $top.buts.can
1634    grid columnconfigure $top.buts 0 -weight 1 -uniform a
1635    grid columnconfigure $top.buts 1 -weight 1 -uniform a
1636    grid $top.buts - -pady 10 -sticky ew
1637    focus $top.t
1638}
1639
1640proc doviewmenu {m first cmd op argv} {
1641    set nmenu [$m index end]
1642    for {set i $first} {$i <= $nmenu} {incr i} {
1643        if {[$m entrycget $i -command] eq $cmd} {
1644            eval $m $op $i $argv
1645            break
1646        }
1647    }
1648}
1649
1650proc allviewmenus {n op args} {
1651    global viewhlmenu
1652
1653    doviewmenu .bar.view 5 [list showview $n] $op $args
1654    doviewmenu $viewhlmenu 1 [list addvhighlight $n] $op $args
1655}
1656
1657proc newviewok {top n} {
1658    global nextviewnum newviewperm newviewname newishighlight
1659    global viewname viewfiles viewperm selectedview curview
1660    global viewargs newviewargs viewhlmenu
1661
1662    if {[catch {
1663        set newargs [shellsplit $newviewargs($n)]
1664    } err]} {
1665        error_popup "Error in commit selection arguments: $err"
1666        wm raise $top
1667        focus $top
1668        return
1669    }
1670    set files {}
1671    foreach f [split [$top.t get 0.0 end] "\n"] {
1672        set ft [string trim $f]
1673        if {$ft ne {}} {
1674            lappend files $ft
1675        }
1676    }
1677    if {![info exists viewfiles($n)]} {
1678        # creating a new view
1679        incr nextviewnum
1680        set viewname($n) $newviewname($n)
1681        set viewperm($n) $newviewperm($n)
1682        set viewfiles($n) $files
1683        set viewargs($n) $newargs
1684        addviewmenu $n
1685        if {!$newishighlight} {
1686            run showview $n
1687        } else {
1688            run addvhighlight $n
1689        }
1690    } else {
1691        # editing an existing view
1692        set viewperm($n) $newviewperm($n)
1693        if {$newviewname($n) ne $viewname($n)} {
1694            set viewname($n) $newviewname($n)
1695            doviewmenu .bar.view 5 [list showview $n] \
1696                entryconf [list -label $viewname($n)]
1697            doviewmenu $viewhlmenu 1 [list addvhighlight $n] \
1698                entryconf [list -label $viewname($n) -value $viewname($n)]
1699        }
1700        if {$files ne $viewfiles($n) || $newargs ne $viewargs($n)} {
1701            set viewfiles($n) $files
1702            set viewargs($n) $newargs
1703            if {$curview == $n} {
1704                run updatecommits
1705            }
1706        }
1707    }
1708    catch {destroy $top}
1709}
1710
1711proc delview {} {
1712    global curview viewdata viewperm hlview selectedhlview
1713
1714    if {$curview == 0} return
1715    if {[info exists hlview] && $hlview == $curview} {
1716        set selectedhlview None
1717        unset hlview
1718    }
1719    allviewmenus $curview delete
1720    set viewdata($curview) {}
1721    set viewperm($curview) 0
1722    showview 0
1723}
1724
1725proc addviewmenu {n} {
1726    global viewname viewhlmenu
1727
1728    .bar.view add radiobutton -label $viewname($n) \
1729        -command [list showview $n] -variable selectedview -value $n
1730    $viewhlmenu add radiobutton -label $viewname($n) \
1731        -command [list addvhighlight $n] -variable selectedhlview
1732}
1733
1734proc flatten {var} {
1735    global $var
1736
1737    set ret {}
1738    foreach i [array names $var] {
1739        lappend ret $i [set $var\($i\)]
1740    }
1741    return $ret
1742}
1743
1744proc unflatten {var l} {
1745    global $var
1746
1747    catch {unset $var}
1748    foreach {i v} $l {
1749        set $var\($i\) $v
1750    }
1751}
1752
1753proc showview {n} {
1754    global curview viewdata viewfiles
1755    global displayorder parentlist rowidlist rowoffsets
1756    global colormap rowtextx commitrow nextcolor canvxmax
1757    global numcommits rowrangelist commitlisted idrowranges rowchk
1758    global selectedline currentid canv canvy0
1759    global matchinglines treediffs
1760    global pending_select phase
1761    global commitidx rowlaidout rowoptim
1762    global commfd
1763    global selectedview selectfirst
1764    global vparentlist vdisporder vcmitlisted
1765    global hlview selectedhlview
1766
1767    if {$n == $curview} return
1768    set selid {}
1769    if {[info exists selectedline]} {
1770        set selid $currentid
1771        set y [yc $selectedline]
1772        set ymax [lindex [$canv cget -scrollregion] 3]
1773        set span [$canv yview]
1774        set ytop [expr {[lindex $span 0] * $ymax}]
1775        set ybot [expr {[lindex $span 1] * $ymax}]
1776        if {$ytop < $y && $y < $ybot} {
1777            set yscreen [expr {$y - $ytop}]
1778        } else {
1779            set yscreen [expr {($ybot - $ytop) / 2}]
1780        }
1781    } elseif {[info exists pending_select]} {
1782        set selid $pending_select
1783        unset pending_select
1784    }
1785    unselectline
1786    normalline
1787    stopfindproc
1788    if {$curview >= 0} {
1789        set vparentlist($curview) $parentlist
1790        set vdisporder($curview) $displayorder
1791        set vcmitlisted($curview) $commitlisted
1792        if {$phase ne {}} {
1793            set viewdata($curview) \
1794                [list $phase $rowidlist $rowoffsets $rowrangelist \
1795                     [flatten idrowranges] [flatten idinlist] \
1796                     $rowlaidout $rowoptim $numcommits]
1797        } elseif {![info exists viewdata($curview)]
1798                  || [lindex $viewdata($curview) 0] ne {}} {
1799            set viewdata($curview) \
1800                [list {} $rowidlist $rowoffsets $rowrangelist]
1801        }
1802    }
1803    catch {unset matchinglines}
1804    catch {unset treediffs}
1805    clear_display
1806    if {[info exists hlview] && $hlview == $n} {
1807        unset hlview
1808        set selectedhlview None
1809    }
1810
1811    set curview $n
1812    set selectedview $n
1813    .bar.view entryconf Edit* -state [expr {$n == 0? "disabled": "normal"}]
1814    .bar.view entryconf Delete* -state [expr {$n == 0? "disabled": "normal"}]
1815
1816    if {![info exists viewdata($n)]} {
1817        if {$selid ne {}} {
1818            set pending_select $selid
1819        }
1820        getcommits
1821        return
1822    }
1823
1824    set v $viewdata($n)
1825    set phase [lindex $v 0]
1826    set displayorder $vdisporder($n)
1827    set parentlist $vparentlist($n)
1828    set commitlisted $vcmitlisted($n)
1829    set rowidlist [lindex $v 1]
1830    set rowoffsets [lindex $v 2]
1831    set rowrangelist [lindex $v 3]
1832    if {$phase eq {}} {
1833        set numcommits [llength $displayorder]
1834        catch {unset idrowranges}
1835    } else {
1836        unflatten idrowranges [lindex $v 4]
1837        unflatten idinlist [lindex $v 5]
1838        set rowlaidout [lindex $v 6]
1839        set rowoptim [lindex $v 7]
1840        set numcommits [lindex $v 8]
1841        catch {unset rowchk}
1842    }
1843
1844    catch {unset colormap}
1845    catch {unset rowtextx}
1846    set nextcolor 0
1847    set canvxmax [$canv cget -width]
1848    set curview $n
1849    set row 0
1850    setcanvscroll
1851    set yf 0
1852    set row {}
1853    set selectfirst 0
1854    if {$selid ne {} && [info exists commitrow($n,$selid)]} {
1855        set row $commitrow($n,$selid)
1856        # try to get the selected row in the same position on the screen
1857        set ymax [lindex [$canv cget -scrollregion] 3]
1858        set ytop [expr {[yc $row] - $yscreen}]
1859        if {$ytop < 0} {
1860            set ytop 0
1861        }
1862        set yf [expr {$ytop * 1.0 / $ymax}]
1863    }
1864    allcanvs yview moveto $yf
1865    drawvisible
1866    if {$row ne {}} {
1867        selectline $row 0
1868    } elseif {$selid ne {}} {
1869        set pending_select $selid
1870    } else {
1871        set row [expr {[lindex $displayorder 0] eq $nullid}]
1872        if {$row < $numcommits} {
1873            selectline $row 0
1874        } else {
1875            set selectfirst 1
1876        }
1877    }
1878    if {$phase ne {}} {
1879        if {$phase eq "getcommits"} {
1880            show_status "Reading commits..."
1881        }
1882        run chewcommits $n
1883    } elseif {$numcommits == 0} {
1884        show_status "No commits selected"
1885    }
1886}
1887
1888# Stuff relating to the highlighting facility
1889
1890proc ishighlighted {row} {
1891    global vhighlights fhighlights nhighlights rhighlights
1892
1893    if {[info exists nhighlights($row)] && $nhighlights($row) > 0} {
1894        return $nhighlights($row)
1895    }
1896    if {[info exists vhighlights($row)] && $vhighlights($row) > 0} {
1897        return $vhighlights($row)
1898    }
1899    if {[info exists fhighlights($row)] && $fhighlights($row) > 0} {
1900        return $fhighlights($row)
1901    }
1902    if {[info exists rhighlights($row)] && $rhighlights($row) > 0} {
1903        return $rhighlights($row)
1904    }
1905    return 0
1906}
1907
1908proc bolden {row font} {
1909    global canv linehtag selectedline boldrows
1910
1911    lappend boldrows $row
1912    $canv itemconf $linehtag($row) -font $font
1913    if {[info exists selectedline] && $row == $selectedline} {
1914        $canv delete secsel
1915        set t [eval $canv create rect [$canv bbox $linehtag($row)] \
1916                   -outline {{}} -tags secsel \
1917                   -fill [$canv cget -selectbackground]]
1918        $canv lower $t
1919    }
1920}
1921
1922proc bolden_name {row font} {
1923    global canv2 linentag selectedline boldnamerows
1924
1925    lappend boldnamerows $row
1926    $canv2 itemconf $linentag($row) -font $font
1927    if {[info exists selectedline] && $row == $selectedline} {
1928        $canv2 delete secsel
1929        set t [eval $canv2 create rect [$canv2 bbox $linentag($row)] \
1930                   -outline {{}} -tags secsel \
1931                   -fill [$canv2 cget -selectbackground]]
1932        $canv2 lower $t
1933    }
1934}
1935
1936proc unbolden {} {
1937    global mainfont boldrows
1938
1939    set stillbold {}
1940    foreach row $boldrows {
1941        if {![ishighlighted $row]} {
1942            bolden $row $mainfont
1943        } else {
1944            lappend stillbold $row
1945        }
1946    }
1947    set boldrows $stillbold
1948}
1949
1950proc addvhighlight {n} {
1951    global hlview curview viewdata vhl_done vhighlights commitidx
1952
1953    if {[info exists hlview]} {
1954        delvhighlight
1955    }
1956    set hlview $n
1957    if {$n != $curview && ![info exists viewdata($n)]} {
1958        set viewdata($n) [list getcommits {{}} {{}} {} {} {} 0 0 0 {}]
1959        set vparentlist($n) {}
1960        set vdisporder($n) {}
1961        set vcmitlisted($n) {}
1962        start_rev_list $n
1963    }
1964    set vhl_done $commitidx($hlview)
1965    if {$vhl_done > 0} {
1966        drawvisible
1967    }
1968}
1969
1970proc delvhighlight {} {
1971    global hlview vhighlights
1972
1973    if {![info exists hlview]} return
1974    unset hlview
1975    catch {unset vhighlights}
1976    unbolden
1977}
1978
1979proc vhighlightmore {} {
1980    global hlview vhl_done commitidx vhighlights
1981    global displayorder vdisporder curview mainfont
1982
1983    set font [concat $mainfont bold]
1984    set max $commitidx($hlview)
1985    if {$hlview == $curview} {
1986        set disp $displayorder
1987    } else {
1988        set disp $vdisporder($hlview)
1989    }
1990    set vr [visiblerows]
1991    set r0 [lindex $vr 0]
1992    set r1 [lindex $vr 1]
1993    for {set i $vhl_done} {$i < $max} {incr i} {
1994        set id [lindex $disp $i]
1995        if {[info exists commitrow($curview,$id)]} {
1996            set row $commitrow($curview,$id)
1997            if {$r0 <= $row && $row <= $r1} {
1998                if {![highlighted $row]} {
1999                    bolden $row $font
2000                }
2001                set vhighlights($row) 1
2002            }
2003        }
2004    }
2005    set vhl_done $max
2006}
2007
2008proc askvhighlight {row id} {
2009    global hlview vhighlights commitrow iddrawn mainfont
2010
2011    if {[info exists commitrow($hlview,$id)]} {
2012        if {[info exists iddrawn($id)] && ![ishighlighted $row]} {
2013            bolden $row [concat $mainfont bold]
2014        }
2015        set vhighlights($row) 1
2016    } else {
2017        set vhighlights($row) 0
2018    }
2019}
2020
2021proc hfiles_change {name ix op} {
2022    global highlight_files filehighlight fhighlights fh_serial
2023    global mainfont highlight_paths
2024
2025    if {[info exists filehighlight]} {
2026        # delete previous highlights
2027        catch {close $filehighlight}
2028        unset filehighlight
2029        catch {unset fhighlights}
2030        unbolden
2031        unhighlight_filelist
2032    }
2033    set highlight_paths {}
2034    after cancel do_file_hl $fh_serial
2035    incr fh_serial
2036    if {$highlight_files ne {}} {
2037        after 300 do_file_hl $fh_serial
2038    }
2039}
2040
2041proc makepatterns {l} {
2042    set ret {}
2043    foreach e $l {
2044        set ee [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} $e]
2045        if {[string index $ee end] eq "/"} {
2046            lappend ret "$ee*"
2047        } else {
2048            lappend ret $ee
2049            lappend ret "$ee/*"
2050        }
2051    }
2052    return $ret
2053}
2054
2055proc do_file_hl {serial} {
2056    global highlight_files filehighlight highlight_paths gdttype fhl_list
2057
2058    if {$gdttype eq "touching paths:"} {
2059        if {[catch {set paths [shellsplit $highlight_files]}]} return
2060        set highlight_paths [makepatterns $paths]
2061        highlight_filelist
2062        set gdtargs [concat -- $paths]
2063    } else {
2064        set gdtargs [list "-S$highlight_files"]
2065    }
2066    set cmd [concat | git diff-tree -r -s --stdin $gdtargs]
2067    set filehighlight [open $cmd r+]
2068    fconfigure $filehighlight -blocking 0
2069    filerun $filehighlight readfhighlight
2070    set fhl_list {}
2071    drawvisible
2072    flushhighlights
2073}
2074
2075proc flushhighlights {} {
2076    global filehighlight fhl_list
2077
2078    if {[info exists filehighlight]} {
2079        lappend fhl_list {}
2080        puts $filehighlight ""
2081        flush $filehighlight
2082    }
2083}
2084
2085proc askfilehighlight {row id} {
2086    global filehighlight fhighlights fhl_list
2087
2088    lappend fhl_list $id
2089    set fhighlights($row) -1
2090    puts $filehighlight $id
2091}
2092
2093proc readfhighlight {} {
2094    global filehighlight fhighlights commitrow curview mainfont iddrawn
2095    global fhl_list
2096
2097    if {![info exists filehighlight]} {
2098        return 0
2099    }
2100    set nr 0
2101    while {[incr nr] <= 100 && [gets $filehighlight line] >= 0} {
2102        set line [string trim $line]
2103        set i [lsearch -exact $fhl_list $line]
2104        if {$i < 0} continue
2105        for {set j 0} {$j < $i} {incr j} {
2106            set id [lindex $fhl_list $j]
2107            if {[info exists commitrow($curview,$id)]} {
2108                set fhighlights($commitrow($curview,$id)) 0
2109            }
2110        }
2111        set fhl_list [lrange $fhl_list [expr {$i+1}] end]
2112        if {$line eq {}} continue
2113        if {![info exists commitrow($curview,$line)]} continue
2114        set row $commitrow($curview,$line)
2115        if {[info exists iddrawn($line)] && ![ishighlighted $row]} {
2116            bolden $row [concat $mainfont bold]
2117        }
2118        set fhighlights($row) 1
2119    }
2120    if {[eof $filehighlight]} {
2121        # strange...
2122        puts "oops, git diff-tree died"
2123        catch {close $filehighlight}
2124        unset filehighlight
2125        return 0
2126    }
2127    next_hlcont
2128    return 1
2129}
2130
2131proc find_change {name ix op} {
2132    global nhighlights mainfont boldnamerows
2133    global findstring findpattern findtype
2134
2135    # delete previous highlights, if any
2136    foreach row $boldnamerows {
2137        bolden_name $row $mainfont
2138    }
2139    set boldnamerows {}
2140    catch {unset nhighlights}
2141    unbolden
2142    if {$findtype ne "Regexp"} {
2143        set e [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} \
2144                   $findstring]
2145        set findpattern "*$e*"
2146    }
2147    drawvisible
2148}
2149
2150proc askfindhighlight {row id} {
2151    global nhighlights commitinfo iddrawn mainfont
2152    global findstring findtype findloc findpattern
2153
2154    if {![info exists commitinfo($id)]} {
2155        getcommit $id
2156    }
2157    set info $commitinfo($id)
2158    set isbold 0
2159    set fldtypes {Headline Author Date Committer CDate Comments}
2160    foreach f $info ty $fldtypes {
2161        if {$findloc ne "All fields" && $findloc ne $ty} {
2162            continue
2163        }
2164        if {$findtype eq "Regexp"} {
2165            set doesmatch [regexp $findstring $f]
2166        } elseif {$findtype eq "IgnCase"} {
2167            set doesmatch [string match -nocase $findpattern $f]
2168        } else {
2169            set doesmatch [string match $findpattern $f]
2170        }
2171        if {$doesmatch} {
2172            if {$ty eq "Author"} {
2173                set isbold 2
2174            } else {
2175                set isbold 1
2176            }
2177        }
2178    }
2179    if {[info exists iddrawn($id)]} {
2180        if {$isbold && ![ishighlighted $row]} {
2181            bolden $row [concat $mainfont bold]
2182        }
2183        if {$isbold >= 2} {
2184            bolden_name $row [concat $mainfont bold]
2185        }
2186    }
2187    set nhighlights($row) $isbold
2188}
2189
2190proc vrel_change {name ix op} {
2191    global highlight_related
2192
2193    rhighlight_none
2194    if {$highlight_related ne "None"} {
2195        run drawvisible
2196    }
2197}
2198
2199# prepare for testing whether commits are descendents or ancestors of a
2200proc rhighlight_sel {a} {
2201    global descendent desc_todo ancestor anc_todo
2202    global highlight_related rhighlights
2203
2204    catch {unset descendent}
2205    set desc_todo [list $a]
2206    catch {unset ancestor}
2207    set anc_todo [list $a]
2208    if {$highlight_related ne "None"} {
2209        rhighlight_none
2210        run drawvisible
2211    }
2212}
2213
2214proc rhighlight_none {} {
2215    global rhighlights
2216
2217    catch {unset rhighlights}
2218    unbolden
2219}
2220
2221proc is_descendent {a} {
2222    global curview children commitrow descendent desc_todo
2223
2224    set v $curview
2225    set la $commitrow($v,$a)
2226    set todo $desc_todo
2227    set leftover {}
2228    set done 0
2229    for {set i 0} {$i < [llength $todo]} {incr i} {
2230        set do [lindex $todo $i]
2231        if {$commitrow($v,$do) < $la} {
2232            lappend leftover $do
2233            continue
2234        }
2235        foreach nk $children($v,$do) {
2236            if {![info exists descendent($nk)]} {
2237                set descendent($nk) 1
2238                lappend todo $nk
2239                if {$nk eq $a} {
2240                    set done 1
2241                }
2242            }
2243        }
2244        if {$done} {
2245            set desc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
2246            return
2247        }
2248    }
2249    set descendent($a) 0
2250    set desc_todo $leftover
2251}
2252
2253proc is_ancestor {a} {
2254    global curview parentlist commitrow ancestor anc_todo
2255
2256    set v $curview
2257    set la $commitrow($v,$a)
2258    set todo $anc_todo
2259    set leftover {}
2260    set done 0
2261    for {set i 0} {$i < [llength $todo]} {incr i} {
2262        set do [lindex $todo $i]
2263        if {![info exists commitrow($v,$do)] || $commitrow($v,$do) > $la} {
2264            lappend leftover $do
2265            continue
2266        }
2267        foreach np [lindex $parentlist $commitrow($v,$do)] {
2268            if {![info exists ancestor($np)]} {
2269                set ancestor($np) 1
2270                lappend todo $np
2271                if {$np eq $a} {
2272                    set done 1
2273                }
2274            }
2275        }
2276        if {$done} {
2277            set anc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
2278            return
2279        }
2280    }
2281    set ancestor($a) 0
2282    set anc_todo $leftover
2283}
2284
2285proc askrelhighlight {row id} {
2286    global descendent highlight_related iddrawn mainfont rhighlights
2287    global selectedline ancestor
2288
2289    if {![info exists selectedline]} return
2290    set isbold 0
2291    if {$highlight_related eq "Descendent" ||
2292        $highlight_related eq "Not descendent"} {
2293        if {![info exists descendent($id)]} {
2294            is_descendent $id
2295        }
2296        if {$descendent($id) == ($highlight_related eq "Descendent")} {
2297            set isbold 1
2298        }
2299    } elseif {$highlight_related eq "Ancestor" ||
2300              $highlight_related eq "Not ancestor"} {
2301        if {![info exists ancestor($id)]} {
2302            is_ancestor $id
2303        }
2304        if {$ancestor($id) == ($highlight_related eq "Ancestor")} {
2305            set isbold 1
2306        }
2307    }
2308    if {[info exists iddrawn($id)]} {
2309        if {$isbold && ![ishighlighted $row]} {
2310            bolden $row [concat $mainfont bold]
2311        }
2312    }
2313    set rhighlights($row) $isbold
2314}
2315
2316proc next_hlcont {} {
2317    global fhl_row fhl_dirn displayorder numcommits
2318    global vhighlights fhighlights nhighlights rhighlights
2319    global hlview filehighlight findstring highlight_related
2320
2321    if {![info exists fhl_dirn] || $fhl_dirn == 0} return
2322    set row $fhl_row
2323    while {1} {
2324        if {$row < 0 || $row >= $numcommits} {
2325            bell
2326            set fhl_dirn 0
2327            return
2328        }
2329        set id [lindex $displayorder $row]
2330        if {[info exists hlview]} {
2331            if {![info exists vhighlights($row)]} {
2332                askvhighlight $row $id
2333            }
2334            if {$vhighlights($row) > 0} break
2335        }
2336        if {$findstring ne {}} {
2337            if {![info exists nhighlights($row)]} {
2338                askfindhighlight $row $id
2339            }
2340            if {$nhighlights($row) > 0} break
2341        }
2342        if {$highlight_related ne "None"} {
2343            if {![info exists rhighlights($row)]} {
2344                askrelhighlight $row $id
2345            }
2346            if {$rhighlights($row) > 0} break
2347        }
2348        if {[info exists filehighlight]} {
2349            if {![info exists fhighlights($row)]} {
2350                # ask for a few more while we're at it...
2351                set r $row
2352                for {set n 0} {$n < 100} {incr n} {
2353                    if {![info exists fhighlights($r)]} {
2354                        askfilehighlight $r [lindex $displayorder $r]
2355                    }
2356                    incr r $fhl_dirn
2357                    if {$r < 0 || $r >= $numcommits} break
2358                }
2359                flushhighlights
2360            }
2361            if {$fhighlights($row) < 0} {
2362                set fhl_row $row
2363                return
2364            }
2365            if {$fhighlights($row) > 0} break
2366        }
2367        incr row $fhl_dirn
2368    }
2369    set fhl_dirn 0
2370    selectline $row 1
2371}
2372
2373proc next_highlight {dirn} {
2374    global selectedline fhl_row fhl_dirn
2375    global hlview filehighlight findstring highlight_related
2376
2377    if {![info exists selectedline]} return
2378    if {!([info exists hlview] || $findstring ne {} ||
2379          $highlight_related ne "None" || [info exists filehighlight])} return
2380    set fhl_row [expr {$selectedline + $dirn}]
2381    set fhl_dirn $dirn
2382    next_hlcont
2383}
2384
2385proc cancel_next_highlight {} {
2386    global fhl_dirn
2387
2388    set fhl_dirn 0
2389}
2390
2391# Graph layout functions
2392
2393proc shortids {ids} {
2394    set res {}
2395    foreach id $ids {
2396        if {[llength $id] > 1} {
2397            lappend res [shortids $id]
2398        } elseif {[regexp {^[0-9a-f]{40}$} $id]} {
2399            lappend res [string range $id 0 7]
2400        } else {
2401            lappend res $id
2402        }
2403    }
2404    return $res
2405}
2406
2407proc incrange {l x o} {
2408    set n [llength $l]
2409    while {$x < $n} {
2410        set e [lindex $l $x]
2411        if {$e ne {}} {
2412            lset l $x [expr {$e + $o}]
2413        }
2414        incr x
2415    }
2416    return $l
2417}
2418
2419proc ntimes {n o} {
2420    set ret {}
2421    for {} {$n > 0} {incr n -1} {
2422        lappend ret $o
2423    }
2424    return $ret
2425}
2426
2427proc usedinrange {id l1 l2} {
2428    global children commitrow curview
2429
2430    if {[info exists commitrow($curview,$id)]} {
2431        set r $commitrow($curview,$id)
2432        if {$l1 <= $r && $r <= $l2} {
2433            return [expr {$r - $l1 + 1}]
2434        }
2435    }
2436    set kids $children($curview,$id)
2437    foreach c $kids {
2438        set r $commitrow($curview,$c)
2439        if {$l1 <= $r && $r <= $l2} {
2440            return [expr {$r - $l1 + 1}]
2441        }
2442    }
2443    return 0
2444}
2445
2446proc sanity {row {full 0}} {
2447    global rowidlist rowoffsets
2448
2449    set col -1
2450    set ids [lindex $rowidlist $row]
2451    foreach id $ids {
2452        incr col
2453        if {$id eq {}} continue
2454        if {$col < [llength $ids] - 1 &&
2455            [lsearch -exact -start [expr {$col+1}] $ids $id] >= 0} {
2456            puts "oops: [shortids $id] repeated in row $row col $col: {[shortids [lindex $rowidlist $row]]}"
2457        }
2458        set o [lindex $rowoffsets $row $col]
2459        set y $row
2460        set x $col
2461        while {$o ne {}} {
2462            incr y -1
2463            incr x $o
2464            if {[lindex $rowidlist $y $x] != $id} {
2465                puts "oops: rowoffsets wrong at row [expr {$y+1}] col [expr {$x-$o}]"
2466                puts "  id=[shortids $id] check started at row $row"
2467                for {set i $row} {$i >= $y} {incr i -1} {
2468                    puts "  row $i ids={[shortids [lindex $rowidlist $i]]} offs={[lindex $rowoffsets $i]}"
2469                }
2470                break
2471            }
2472            if {!$full} break
2473            set o [lindex $rowoffsets $y $x]
2474        }
2475    }
2476}
2477
2478proc makeuparrow {oid x y z} {
2479    global rowidlist rowoffsets uparrowlen idrowranges displayorder
2480
2481    for {set i 1} {$i < $uparrowlen && $y > 1} {incr i} {
2482        incr y -1
2483        incr x $z
2484        set off0 [lindex $rowoffsets $y]
2485        for {set x0 $x} {1} {incr x0} {
2486            if {$x0 >= [llength $off0]} {
2487                set x0 [llength [lindex $rowoffsets [expr {$y-1}]]]
2488                break
2489            }
2490            set z [lindex $off0 $x0]
2491            if {$z ne {}} {
2492                incr x0 $z
2493                break
2494            }
2495        }
2496        set z [expr {$x0 - $x}]
2497        lset rowidlist $y [linsert [lindex $rowidlist $y] $x $oid]
2498        lset rowoffsets $y [linsert [lindex $rowoffsets $y] $x $z]
2499    }
2500    set tmp [lreplace [lindex $rowoffsets $y] $x $x {}]
2501    lset rowoffsets $y [incrange $tmp [expr {$x+1}] -1]
2502    lappend idrowranges($oid) [lindex $displayorder $y]
2503}
2504
2505proc initlayout {} {
2506    global rowidlist rowoffsets displayorder commitlisted
2507    global rowlaidout rowoptim
2508    global idinlist rowchk rowrangelist idrowranges
2509    global numcommits canvxmax canv
2510    global nextcolor
2511    global parentlist
2512    global colormap rowtextx
2513    global selectfirst
2514
2515    set numcommits 0
2516    set displayorder {}
2517    set commitlisted {}
2518    set parentlist {}
2519    set rowrangelist {}
2520    set nextcolor 0
2521    set rowidlist {{}}
2522    set rowoffsets {{}}
2523    catch {unset idinlist}
2524    catch {unset rowchk}
2525    set rowlaidout 0
2526    set rowoptim 0
2527    set canvxmax [$canv cget -width]
2528    catch {unset colormap}
2529    catch {unset rowtextx}
2530    catch {unset idrowranges}
2531    set selectfirst 1
2532}
2533
2534proc setcanvscroll {} {
2535    global canv canv2 canv3 numcommits linespc canvxmax canvy0
2536
2537    set ymax [expr {$canvy0 + ($numcommits - 0.5) * $linespc + 2}]
2538    $canv conf -scrollregion [list 0 0 $canvxmax $ymax]
2539    $canv2 conf -scrollregion [list 0 0 0 $ymax]
2540    $canv3 conf -scrollregion [list 0 0 0 $ymax]
2541}
2542
2543proc visiblerows {} {
2544    global canv numcommits linespc
2545
2546    set ymax [lindex [$canv cget -scrollregion] 3]
2547    if {$ymax eq {} || $ymax == 0} return
2548    set f [$canv yview]
2549    set y0 [expr {int([lindex $f 0] * $ymax)}]
2550    set r0 [expr {int(($y0 - 3) / $linespc) - 1}]
2551    if {$r0 < 0} {
2552        set r0 0
2553    }
2554    set y1 [expr {int([lindex $f 1] * $ymax)}]
2555    set r1 [expr {int(($y1 - 3) / $linespc) + 1}]
2556    if {$r1 >= $numcommits} {
2557        set r1 [expr {$numcommits - 1}]
2558    }
2559    return [list $r0 $r1]
2560}
2561
2562proc layoutmore {tmax allread} {
2563    global rowlaidout rowoptim commitidx numcommits optim_delay
2564    global uparrowlen curview rowidlist idinlist
2565
2566    set showlast 0
2567    set showdelay $optim_delay
2568    set optdelay [expr {$uparrowlen + 1}]
2569    while {1} {
2570        if {$rowoptim - $showdelay > $numcommits} {
2571            showstuff [expr {$rowoptim - $showdelay}] $showlast
2572        } elseif {$rowlaidout - $optdelay > $rowoptim} {
2573            set nr [expr {$rowlaidout - $optdelay - $rowoptim}]
2574            if {$nr > 100} {
2575                set nr 100
2576            }
2577            optimize_rows $rowoptim 0 [expr {$rowoptim + $nr}]
2578            incr rowoptim $nr
2579        } elseif {$commitidx($curview) > $rowlaidout} {
2580            set nr [expr {$commitidx($curview) - $rowlaidout}]
2581            # may need to increase this threshold if uparrowlen or
2582            # mingaplen are increased...
2583            if {$nr > 150} {
2584                set nr 150
2585            }
2586            set row $rowlaidout
2587            set rowlaidout [layoutrows $row [expr {$row + $nr}] $allread]
2588            if {$rowlaidout == $row} {
2589                return 0
2590            }
2591        } elseif {$allread} {
2592            set optdelay 0
2593            set nrows $commitidx($curview)
2594            if {[lindex $rowidlist $nrows] ne {} ||
2595                [array names idinlist] ne {}} {
2596                layouttail
2597                set rowlaidout $commitidx($curview)
2598            } elseif {$rowoptim == $nrows} {
2599                set showdelay 0
2600                set showlast 1
2601                if {$numcommits == $nrows} {
2602                    return 0
2603                }
2604            }
2605        } else {
2606            return 0
2607        }
2608        if {$tmax ne {} && [clock clicks -milliseconds] >= $tmax} {
2609            return 1
2610        }
2611    }
2612}
2613
2614proc showstuff {canshow last} {
2615    global numcommits commitrow pending_select selectedline curview
2616    global lookingforhead mainheadid displayorder nullid selectfirst
2617
2618    if {$numcommits == 0} {
2619        global phase
2620        set phase "incrdraw"
2621        allcanvs delete all
2622    }
2623    set r0 $numcommits
2624    set numcommits $canshow
2625    setcanvscroll
2626    set rows [visiblerows]
2627    set r1 [lindex $rows 1]
2628    if {$r1 >= $canshow} {
2629        set r1 [expr {$canshow - 1}]
2630    }
2631    if {$r0 <= $r1} {
2632        drawcommits $r0 $r1
2633    }
2634    if {[info exists pending_select] &&
2635        [info exists commitrow($curview,$pending_select)] &&
2636        $commitrow($curview,$pending_select) < $numcommits} {
2637        selectline $commitrow($curview,$pending_select) 1
2638    }
2639    if {$selectfirst} {
2640        if {[info exists selectedline] || [info exists pending_select]} {
2641            set selectfirst 0
2642        } else {
2643            set l [expr {[lindex $displayorder 0] eq $nullid}]
2644            selectline $l 1
2645            set selectfirst 0
2646        }
2647    }
2648    if {$lookingforhead && [info exists commitrow($curview,$mainheadid)]
2649        && ($last || $commitrow($curview,$mainheadid) < $numcommits - 1)} {
2650        set lookingforhead 0
2651        dodiffindex
2652    }
2653}
2654
2655proc doshowlocalchanges {} {
2656    global lookingforhead curview mainheadid phase commitrow
2657
2658    if {[info exists commitrow($curview,$mainheadid)] &&
2659        ($phase eq {} || $commitrow($curview,$mainheadid) < $numcommits - 1)} {
2660        dodiffindex
2661    } elseif {$phase ne {}} {
2662        set lookingforhead 1
2663    }
2664}
2665
2666proc dohidelocalchanges {} {
2667    global lookingforhead localrow lserial
2668
2669    set lookingforhead 0
2670    if {$localrow >= 0} {
2671        removerow $localrow
2672        set localrow -1
2673    }
2674    incr lserial
2675}
2676
2677# spawn off a process to do git diff-index HEAD
2678proc dodiffindex {} {
2679    global localrow lserial
2680
2681    incr lserial
2682    set localrow -1
2683    set fd [open "|git diff-index HEAD" r]
2684    fconfigure $fd -blocking 0
2685    filerun $fd [list readdiffindex $fd $lserial]
2686}
2687
2688proc readdiffindex {fd serial} {
2689    global localrow commitrow mainheadid nullid curview
2690    global commitinfo commitdata lserial
2691
2692    if {[gets $fd line] < 0} {
2693        if {[eof $fd]} {
2694            close $fd
2695            return 0
2696        }
2697        return 1
2698    }
2699    # we only need to see one line and we don't really care what it says...
2700    close $fd
2701
2702    if {$serial == $lserial && $localrow == -1} {
2703        # add the line for the local diff to the graph
2704        set localrow $commitrow($curview,$mainheadid)
2705        set hl "Local uncommitted changes"
2706        set commitinfo($nullid) [list  $hl {} {} {} {} "    $hl\n"]
2707        set commitdata($nullid) "\n    $hl\n"
2708        insertrow $localrow $nullid
2709    }
2710    return 0
2711}
2712
2713proc layoutrows {row endrow last} {
2714    global rowidlist rowoffsets displayorder
2715    global uparrowlen downarrowlen maxwidth mingaplen
2716    global children parentlist
2717    global idrowranges
2718    global commitidx curview
2719    global idinlist rowchk rowrangelist
2720
2721    set idlist [lindex $rowidlist $row]
2722    set offs [lindex $rowoffsets $row]
2723    while {$row < $endrow} {
2724        set id [lindex $displayorder $row]
2725        set oldolds {}
2726        set newolds {}
2727        foreach p [lindex $parentlist $row] {
2728            if {![info exists idinlist($p)]} {
2729                lappend newolds $p
2730            } elseif {!$idinlist($p)} {
2731                lappend oldolds $p
2732            }
2733        }
2734        set nev [expr {[llength $idlist] + [llength $newolds]
2735                       + [llength $oldolds] - $maxwidth + 1}]
2736        if {$nev > 0} {
2737            if {!$last &&
2738                $row + $uparrowlen + $mingaplen >= $commitidx($curview)} break
2739            for {set x [llength $idlist]} {[incr x -1] >= 0} {} {
2740                set i [lindex $idlist $x]
2741                if {![info exists rowchk($i)] || $row >= $rowchk($i)} {
2742                    set r [usedinrange $i [expr {$row - $downarrowlen}] \
2743                               [expr {$row + $uparrowlen + $mingaplen}]]
2744                    if {$r == 0} {
2745                        set idlist [lreplace $idlist $x $x]
2746                        set offs [lreplace $offs $x $x]
2747                        set offs [incrange $offs $x 1]
2748                        set idinlist($i) 0
2749                        set rm1 [expr {$row - 1}]
2750                        lappend idrowranges($i) [lindex $displayorder $rm1]
2751                        if {[incr nev -1] <= 0} break
2752                        continue
2753                    }
2754                    set rowchk($id) [expr {$row + $r}]
2755                }
2756            }
2757            lset rowidlist $row $idlist
2758            lset rowoffsets $row $offs
2759        }
2760        set col [lsearch -exact $idlist $id]
2761        if {$col < 0} {
2762            set col [llength $idlist]
2763            lappend idlist $id
2764            lset rowidlist $row $idlist
2765            set z {}
2766            if {$children($curview,$id) ne {}} {
2767                set z [expr {[llength [lindex $rowidlist [expr {$row-1}]]] - $col}]
2768                unset idinlist($id)
2769            }
2770            lappend offs $z
2771            lset rowoffsets $row $offs
2772            if {$z ne {}} {
2773                makeuparrow $id $col $row $z
2774            }
2775        } else {
2776            unset idinlist($id)
2777        }
2778        set ranges {}
2779        if {[info exists idrowranges($id)]} {
2780            set ranges $idrowranges($id)
2781            lappend ranges $id
2782            unset idrowranges($id)
2783        }
2784        lappend rowrangelist $ranges
2785        incr row
2786        set offs [ntimes [llength $idlist] 0]
2787        set l [llength $newolds]
2788        set idlist [eval lreplace \$idlist $col $col $newolds]
2789        set o 0
2790        if {$l != 1} {
2791            set offs [lrange $offs 0 [expr {$col - 1}]]
2792            foreach x $newolds {
2793                lappend offs {}
2794                incr o -1
2795            }
2796            incr o
2797            set tmp [expr {[llength $idlist] - [llength $offs]}]
2798            if {$tmp > 0} {
2799                set offs [concat $offs [ntimes $tmp $o]]
2800            }
2801        } else {
2802            lset offs $col {}
2803        }
2804        foreach i $newolds {
2805            set idinlist($i) 1
2806            set idrowranges($i) $id
2807        }
2808        incr col $l
2809        foreach oid $oldolds {
2810            set idinlist($oid) 1
2811            set idlist [linsert $idlist $col $oid]
2812            set offs [linsert $offs $col $o]
2813            makeuparrow $oid $col $row $o
2814            incr col
2815        }
2816        lappend rowidlist $idlist
2817        lappend rowoffsets $offs
2818    }
2819    return $row
2820}
2821
2822proc addextraid {id row} {
2823    global displayorder commitrow commitinfo
2824    global commitidx commitlisted
2825    global parentlist children curview
2826
2827    incr commitidx($curview)
2828    lappend displayorder $id
2829    lappend commitlisted 0
2830    lappend parentlist {}
2831    set commitrow($curview,$id) $row
2832    readcommit $id
2833    if {![info exists commitinfo($id)]} {
2834        set commitinfo($id) {"No commit information available"}
2835    }
2836    if {![info exists children($curview,$id)]} {
2837        set children($curview,$id) {}
2838    }
2839}
2840
2841proc layouttail {} {
2842    global rowidlist rowoffsets idinlist commitidx curview
2843    global idrowranges rowrangelist
2844
2845    set row $commitidx($curview)
2846    set idlist [lindex $rowidlist $row]
2847    while {$idlist ne {}} {
2848        set col [expr {[llength $idlist] - 1}]
2849        set id [lindex $idlist $col]
2850        addextraid $id $row
2851        unset idinlist($id)
2852        lappend idrowranges($id) $row
2853        lappend rowrangelist $idrowranges($id)
2854        unset idrowranges($id)
2855        incr row
2856        set offs [ntimes $col 0]
2857        set idlist [lreplace $idlist $col $col]
2858        lappend rowidlist $idlist
2859        lappend rowoffsets $offs
2860    }
2861
2862    foreach id [array names idinlist] {
2863        unset idinlist($id)
2864        addextraid $id $row
2865        lset rowidlist $row [list $id]
2866        lset rowoffsets $row 0
2867        makeuparrow $id 0 $row 0
2868        lappend idrowranges($id) $row
2869        lappend rowrangelist $idrowranges($id)
2870        unset idrowranges($id)
2871        incr row
2872        lappend rowidlist {}
2873        lappend rowoffsets {}
2874    }
2875}
2876
2877proc insert_pad {row col npad} {
2878    global rowidlist rowoffsets
2879
2880    set pad [ntimes $npad {}]
2881    lset rowidlist $row [eval linsert [list [lindex $rowidlist $row]] $col $pad]
2882    set tmp [eval linsert [list [lindex $rowoffsets $row]] $col $pad]
2883    lset rowoffsets $row [incrange $tmp [expr {$col + $npad}] [expr {-$npad}]]
2884}
2885
2886proc optimize_rows {row col endrow} {
2887    global rowidlist rowoffsets displayorder
2888
2889    for {} {$row < $endrow} {incr row} {
2890        set idlist [lindex $rowidlist $row]
2891        set offs [lindex $rowoffsets $row]
2892        set haspad 0
2893        for {} {$col < [llength $offs]} {incr col} {
2894            if {[lindex $idlist $col] eq {}} {
2895                set haspad 1
2896                continue
2897            }
2898            set z [lindex $offs $col]
2899            if {$z eq {}} continue
2900            set isarrow 0
2901            set x0 [expr {$col + $z}]
2902            set y0 [expr {$row - 1}]
2903            set z0 [lindex $rowoffsets $y0 $x0]
2904            if {$z0 eq {}} {
2905                set id [lindex $idlist $col]
2906                set ranges [rowranges $id]
2907                if {$ranges ne {} && $y0 > [lindex $ranges 0]} {
2908                    set isarrow 1
2909                }
2910            }
2911            # Looking at lines from this row to the previous row,
2912            # make them go straight up if they end in an arrow on
2913            # the previous row; otherwise make them go straight up
2914            # or at 45 degrees.
2915            if {$z < -1 || ($z < 0 && $isarrow)} {
2916                # Line currently goes left too much;
2917                # insert pads in the previous row, then optimize it
2918                set npad [expr {-1 - $z + $isarrow}]
2919                set offs [incrange $offs $col $npad]
2920                insert_pad $y0 $x0 $npad
2921                if {$y0 > 0} {
2922                    optimize_rows $y0 $x0 $row
2923                }
2924                set z [lindex $offs $col]
2925                set x0 [expr {$col + $z}]
2926                set z0 [lindex $rowoffsets $y0 $x0]
2927            } elseif {$z > 1 || ($z > 0 && $isarrow)} {
2928                # Line currently goes right too much;
2929                # insert pads in this line and adjust the next's rowoffsets
2930                set npad [expr {$z - 1 + $isarrow}]
2931                set y1 [expr {$row + 1}]
2932                set offs2 [lindex $rowoffsets $y1]
2933                set x1 -1
2934                foreach z $offs2 {
2935                    incr x1
2936                    if {$z eq {} || $x1 + $z < $col} continue
2937                    if {$x1 + $z > $col} {
2938                        incr npad
2939                    }
2940                    lset rowoffsets $y1 [incrange $offs2 $x1 $npad]
2941                    break
2942                }
2943                set pad [ntimes $npad {}]
2944                set idlist [eval linsert \$idlist $col $pad]
2945                set tmp [eval linsert \$offs $col $pad]
2946                incr col $npad
2947                set offs [incrange $tmp $col [expr {-$npad}]]
2948                set z [lindex $offs $col]
2949                set haspad 1
2950            }
2951            if {$z0 eq {} && !$isarrow} {
2952                # this line links to its first child on row $row-2
2953                set rm2 [expr {$row - 2}]
2954                set id [lindex $displayorder $rm2]
2955                set xc [lsearch -exact [lindex $rowidlist $rm2] $id]
2956                if {$xc >= 0} {
2957                    set z0 [expr {$xc - $x0}]
2958                }
2959            }
2960            # avoid lines jigging left then immediately right
2961            if {$z0 ne {} && $z < 0 && $z0 > 0} {
2962                insert_pad $y0 $x0 1
2963                set offs [incrange $offs $col 1]
2964                optimize_rows $y0 [expr {$x0 + 1}] $row
2965            }
2966        }
2967        if {!$haspad} {
2968            set o {}
2969            # Find the first column that doesn't have a line going right
2970            for {set col [llength $idlist]} {[incr col -1] >= 0} {} {
2971                set o [lindex $offs $col]
2972                if {$o eq {}} {
2973                    # check if this is the link to the first child
2974                    set id [lindex $idlist $col]
2975                    set ranges [rowranges $id]
2976                    if {$ranges ne {} && $row == [lindex $ranges 0]} {
2977                        # it is, work out offset to child
2978                        set y0 [expr {$row - 1}]
2979                        set id [lindex $displayorder $y0]
2980                        set x0 [lsearch -exact [lindex $rowidlist $y0] $id]
2981                        if {$x0 >= 0} {
2982                            set o [expr {$x0 - $col}]
2983                        }
2984                    }
2985                }
2986                if {$o eq {} || $o <= 0} break
2987            }
2988            # Insert a pad at that column as long as it has a line and
2989            # isn't the last column, and adjust the next row' offsets
2990            if {$o ne {} && [incr col] < [llength $idlist]} {
2991                set y1 [expr {$row + 1}]
2992                set offs2 [lindex $rowoffsets $y1]
2993                set x1 -1
2994                foreach z $offs2 {
2995                    incr x1
2996                    if {$z eq {} || $x1 + $z < $col} continue
2997                    lset rowoffsets $y1 [incrange $offs2 $x1 1]
2998                    break
2999                }
3000                set idlist [linsert $idlist $col {}]
3001                set tmp [linsert $offs $col {}]
3002                incr col
3003                set offs [incrange $tmp $col -1]
3004            }
3005        }
3006        lset rowidlist $row $idlist
3007        lset rowoffsets $row $offs
3008        set col 0
3009    }
3010}
3011
3012proc xc {row col} {
3013    global canvx0 linespc
3014    return [expr {$canvx0 + $col * $linespc}]
3015}
3016
3017proc yc {row} {
3018    global canvy0 linespc
3019    return [expr {$canvy0 + $row * $linespc}]
3020}
3021
3022proc linewidth {id} {
3023    global thickerline lthickness
3024
3025    set wid $lthickness
3026    if {[info exists thickerline] && $id eq $thickerline} {
3027        set wid [expr {2 * $lthickness}]
3028    }
3029    return $wid
3030}
3031
3032proc rowranges {id} {
3033    global phase idrowranges commitrow rowlaidout rowrangelist curview
3034
3035    set ranges {}
3036    if {$phase eq {} ||
3037        ([info exists commitrow($curview,$id)]
3038         && $commitrow($curview,$id) < $rowlaidout)} {
3039        set ranges [lindex $rowrangelist $commitrow($curview,$id)]
3040    } elseif {[info exists idrowranges($id)]} {
3041        set ranges $idrowranges($id)
3042    }
3043    set linenos {}
3044    foreach rid $ranges {
3045        lappend linenos $commitrow($curview,$rid)
3046    }
3047    if {$linenos ne {}} {
3048        lset linenos 0 [expr {[lindex $linenos 0] + 1}]
3049    }
3050    return $linenos
3051}
3052
3053# work around tk8.4 refusal to draw arrows on diagonal segments
3054proc adjarrowhigh {coords} {
3055    global linespc
3056
3057    set x0 [lindex $coords 0]
3058    set x1 [lindex $coords 2]
3059    if {$x0 != $x1} {
3060        set y0 [lindex $coords 1]
3061        set y1 [lindex $coords 3]
3062        if {$y0 - $y1 <= 2 * $linespc && $x1 == [lindex $coords 4]} {
3063            # we have a nearby vertical segment, just trim off the diag bit
3064            set coords [lrange $coords 2 end]
3065        } else {
3066            set slope [expr {($x0 - $x1) / ($y0 - $y1)}]
3067            set xi [expr {$x0 - $slope * $linespc / 2}]
3068            set yi [expr {$y0 - $linespc / 2}]
3069            set coords [lreplace $coords 0 1 $xi $y0 $xi $yi]
3070        }
3071    }
3072    return $coords
3073}
3074
3075proc drawlineseg {id row endrow arrowlow} {
3076    global rowidlist displayorder iddrawn linesegs
3077    global canv colormap linespc curview maxlinelen
3078
3079    set cols [list [lsearch -exact [lindex $rowidlist $row] $id]]
3080    set le [expr {$row + 1}]
3081    set arrowhigh 1
3082    while {1} {
3083        set c [lsearch -exact [lindex $rowidlist $le] $id]
3084        if {$c < 0} {
3085            incr le -1
3086            break
3087        }
3088        lappend cols $c
3089        set x [lindex $displayorder $le]
3090        if {$x eq $id} {
3091            set arrowhigh 0
3092            break
3093        }
3094        if {[info exists iddrawn($x)] || $le == $endrow} {
3095            set c [lsearch -exact [lindex $rowidlist [expr {$le+1}]] $id]
3096            if {$c >= 0} {
3097                lappend cols $c
3098                set arrowhigh 0
3099            }
3100            break
3101        }
3102        incr le
3103    }
3104    if {$le <= $row} {
3105        return $row
3106    }
3107
3108    set lines {}
3109    set i 0
3110    set joinhigh 0
3111    if {[info exists linesegs($id)]} {
3112        set lines $linesegs($id)
3113        foreach li $lines {
3114            set r0 [lindex $li 0]
3115            if {$r0 > $row} {
3116                if {$r0 == $le && [lindex $li 1] - $row <= $maxlinelen} {
3117                    set joinhigh 1
3118                }
3119                break
3120            }
3121            incr i
3122        }
3123    }
3124    set joinlow 0
3125    if {$i > 0} {
3126        set li [lindex $lines [expr {$i-1}]]
3127        set r1 [lindex $li 1]
3128        if {$r1 == $row && $le - [lindex $li 0] <= $maxlinelen} {
3129            set joinlow 1
3130        }
3131    }
3132
3133    set x [lindex $cols [expr {$le - $row}]]
3134    set xp [lindex $cols [expr {$le - 1 - $row}]]
3135    set dir [expr {$xp - $x}]
3136    if {$joinhigh} {
3137        set ith [lindex $lines $i 2]
3138        set coords [$canv coords $ith]
3139        set ah [$canv itemcget $ith -arrow]
3140        set arrowhigh [expr {$ah eq "first" || $ah eq "both"}]
3141        set x2 [lindex $cols [expr {$le + 1 - $row}]]
3142        if {$x2 ne {} && $x - $x2 == $dir} {
3143            set coords [lrange $coords 0 end-2]
3144        }
3145    } else {
3146        set coords [list [xc $le $x] [yc $le]]
3147    }
3148    if {$joinlow} {
3149        set itl [lindex $lines [expr {$i-1}] 2]
3150        set al [$canv itemcget $itl -arrow]
3151        set arrowlow [expr {$al eq "last" || $al eq "both"}]
3152    } elseif {$arrowlow &&
3153              [lsearch -exact [lindex $rowidlist [expr {$row-1}]] $id] >= 0} {
3154        set arrowlow 0
3155    }
3156    set arrow [lindex {none first last both} [expr {$arrowhigh + 2*$arrowlow}]]
3157    for {set y $le} {[incr y -1] > $row} {} {
3158        set x $xp
3159        set xp [lindex $cols [expr {$y - 1 - $row}]]
3160        set ndir [expr {$xp - $x}]
3161        if {$dir != $ndir || $xp < 0} {
3162            lappend coords [xc $y $x] [yc $y]
3163        }
3164        set dir $ndir
3165    }
3166    if {!$joinlow} {
3167        if {$xp < 0} {
3168            # join parent line to first child
3169            set ch [lindex $displayorder $row]
3170            set xc [lsearch -exact [lindex $rowidlist $row] $ch]
3171            if {$xc < 0} {
3172                puts "oops: drawlineseg: child $ch not on row $row"
3173            } else {
3174                if {$xc < $x - 1} {
3175                    lappend coords [xc $row [expr {$x-1}]] [yc $row]
3176                } elseif {$xc > $x + 1} {
3177                    lappend coords [xc $row [expr {$x+1}]] [yc $row]
3178                }
3179                set x $xc
3180            }
3181            lappend coords [xc $row $x] [yc $row]
3182        } else {
3183            set xn [xc $row $xp]
3184            set yn [yc $row]
3185            # work around tk8.4 refusal to draw arrows on diagonal segments
3186            if {$arrowlow && $xn != [lindex $coords end-1]} {
3187                if {[llength $coords] < 4 ||
3188                    [lindex $coords end-3] != [lindex $coords end-1] ||
3189                    [lindex $coords end] - $yn > 2 * $linespc} {
3190                    set xn [xc $row [expr {$xp - 0.5 * $dir}]]
3191                    set yo [yc [expr {$row + 0.5}]]
3192                    lappend coords $xn $yo $xn $yn
3193                }
3194            } else {
3195                lappend coords $xn $yn
3196            }
3197        }
3198        if {!$joinhigh} {
3199            if {$arrowhigh} {
3200                set coords [adjarrowhigh $coords]
3201            }
3202            assigncolor $id
3203            set t [$canv create line $coords -width [linewidth $id] \
3204                       -fill $colormap($id) -tags lines.$id -arrow $arrow]
3205            $canv lower $t
3206            bindline $t $id
3207            set lines [linsert $lines $i [list $row $le $t]]
3208        } else {
3209            $canv coords $ith $coords
3210            if {$arrow ne $ah} {
3211                $canv itemconf $ith -arrow $arrow
3212            }
3213            lset lines $i 0 $row
3214        }
3215    } else {
3216        set xo [lsearch -exact [lindex $rowidlist [expr {$row - 1}]] $id]
3217        set ndir [expr {$xo - $xp}]
3218        set clow [$canv coords $itl]
3219        if {$dir == $ndir} {
3220            set clow [lrange $clow 2 end]
3221        }
3222        set coords [concat $coords $clow]
3223        if {!$joinhigh} {
3224            lset lines [expr {$i-1}] 1 $le
3225            if {$arrowhigh} {
3226                set coords [adjarrowhigh $coords]
3227            }
3228        } else {
3229            # coalesce two pieces
3230            $canv delete $ith
3231            set b [lindex $lines [expr {$i-1}] 0]
3232            set e [lindex $lines $i 1]
3233            set lines [lreplace $lines [expr {$i-1}] $i [list $b $e $itl]]
3234        }
3235        $canv coords $itl $coords
3236        if {$arrow ne $al} {
3237            $canv itemconf $itl -arrow $arrow
3238        }
3239    }
3240
3241    set linesegs($id) $lines
3242    return $le
3243}
3244
3245proc drawparentlinks {id row} {
3246    global rowidlist canv colormap curview parentlist
3247    global idpos
3248
3249    set rowids [lindex $rowidlist $row]
3250    set col [lsearch -exact $rowids $id]
3251    if {$col < 0} return
3252    set olds [lindex $parentlist $row]
3253    set row2 [expr {$row + 1}]
3254    set x [xc $row $col]
3255    set y [yc $row]
3256    set y2 [yc $row2]
3257    set ids [lindex $rowidlist $row2]
3258    # rmx = right-most X coord used
3259    set rmx 0
3260    foreach p $olds {
3261        set i [lsearch -exact $ids $p]
3262        if {$i < 0} {
3263            puts "oops, parent $p of $id not in list"
3264            continue
3265        }
3266        set x2 [xc $row2 $i]
3267        if {$x2 > $rmx} {
3268            set rmx $x2
3269        }
3270        if {[lsearch -exact $rowids $p] < 0} {
3271            # drawlineseg will do this one for us
3272            continue
3273        }
3274        assigncolor $p
3275        # should handle duplicated parents here...
3276        set coords [list $x $y]
3277        if {$i < $col - 1} {
3278            lappend coords [xc $row [expr {$i + 1}]] $y
3279        } elseif {$i > $col + 1} {
3280            lappend coords [xc $row [expr {$i - 1}]] $y
3281        }
3282        lappend coords $x2 $y2
3283        set t [$canv create line $coords -width [linewidth $p] \
3284                   -fill $colormap($p) -tags lines.$p]
3285        $canv lower $t
3286        bindline $t $p
3287    }
3288    if {$rmx > [lindex $idpos($id) 1]} {
3289        lset idpos($id) 1 $rmx
3290        redrawtags $id
3291    }
3292}
3293
3294proc drawlines {id} {
3295    global canv
3296
3297    $canv itemconf lines.$id -width [linewidth $id]
3298}
3299
3300proc drawcmittext {id row col} {
3301    global linespc canv canv2 canv3 canvy0 fgcolor
3302    global commitlisted commitinfo rowidlist parentlist
3303    global rowtextx idpos idtags idheads idotherrefs
3304    global linehtag linentag linedtag
3305    global mainfont canvxmax boldrows boldnamerows fgcolor nullid
3306
3307    if {$id eq $nullid} {
3308        set ofill red
3309    } else {
3310        set ofill [expr {[lindex $commitlisted $row]? "blue": "white"}]
3311    }
3312    set x [xc $row $col]
3313    set y [yc $row]
3314    set orad [expr {$linespc / 3}]
3315    set t [$canv create oval [expr {$x - $orad}] [expr {$y - $orad}] \
3316               [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \
3317               -fill $ofill -outline $fgcolor -width 1 -tags circle]
3318    $canv raise $t
3319    $canv bind $t <1> {selcanvline {} %x %y}
3320    set rmx [llength [lindex $rowidlist $row]]
3321    set olds [lindex $parentlist $row]
3322    if {$olds ne {}} {
3323        set nextids [lindex $rowidlist [expr {$row + 1}]]
3324        foreach p $olds {
3325            set i [lsearch -exact $nextids $p]
3326            if {$i > $rmx} {
3327                set rmx $i
3328            }
3329        }
3330    }
3331    set xt [xc $row $rmx]
3332    set rowtextx($row) $xt
3333    set idpos($id) [list $x $xt $y]
3334    if {[info exists idtags($id)] || [info exists idheads($id)]
3335        || [info exists idotherrefs($id)]} {
3336        set xt [drawtags $id $x $xt $y]
3337    }
3338    set headline [lindex $commitinfo($id) 0]
3339    set name [lindex $commitinfo($id) 1]
3340    set date [lindex $commitinfo($id) 2]
3341    set date [formatdate $date]
3342    set font $mainfont
3343    set nfont $mainfont
3344    set isbold [ishighlighted $row]
3345    if {$isbold > 0} {
3346        lappend boldrows $row
3347        lappend font bold
3348        if {$isbold > 1} {
3349            lappend boldnamerows $row
3350            lappend nfont bold
3351        }
3352    }
3353    set linehtag($row) [$canv create text $xt $y -anchor w -fill $fgcolor \
3354                            -text $headline -font $font -tags text]
3355    $canv bind $linehtag($row) <Button-3> "rowmenu %X %Y $id"
3356    set linentag($row) [$canv2 create text 3 $y -anchor w -fill $fgcolor \
3357                            -text $name -font $nfont -tags text]
3358    set linedtag($row) [$canv3 create text 3 $y -anchor w -fill $fgcolor \
3359                            -text $date -font $mainfont -tags text]
3360    set xr [expr {$xt + [font measure $mainfont $headline]}]
3361    if {$xr > $canvxmax} {
3362        set canvxmax $xr
3363        setcanvscroll
3364    }
3365}
3366
3367proc drawcmitrow {row} {
3368    global displayorder rowidlist
3369    global iddrawn
3370    global commitinfo parentlist numcommits
3371    global filehighlight fhighlights findstring nhighlights
3372    global hlview vhighlights
3373    global highlight_related rhighlights
3374
3375    if {$row >= $numcommits} return
3376
3377    set id [lindex $displayorder $row]
3378    if {[info exists hlview] && ![info exists vhighlights($row)]} {
3379        askvhighlight $row $id
3380    }
3381    if {[info exists filehighlight] && ![info exists fhighlights($row)]} {
3382        askfilehighlight $row $id
3383    }
3384    if {$findstring ne {} && ![info exists nhighlights($row)]} {
3385        askfindhighlight $row $id
3386    }
3387    if {$highlight_related ne "None" && ![info exists rhighlights($row)]} {
3388        askrelhighlight $row $id
3389    }
3390    if {[info exists iddrawn($id)]} return
3391    set col [lsearch -exact [lindex $rowidlist $row] $id]
3392    if {$col < 0} {
3393        puts "oops, row $row id $id not in list"
3394        return
3395    }
3396    if {![info exists commitinfo($id)]} {
3397        getcommit $id
3398    }
3399    assigncolor $id
3400    drawcmittext $id $row $col
3401    set iddrawn($id) 1
3402}
3403
3404proc drawcommits {row {endrow {}}} {
3405    global numcommits iddrawn displayorder curview
3406    global parentlist rowidlist
3407
3408    if {$row < 0} {
3409        set row 0
3410    }
3411    if {$endrow eq {}} {
3412        set endrow $row
3413    }
3414    if {$endrow >= $numcommits} {
3415        set endrow [expr {$numcommits - 1}]
3416    }
3417
3418    # make the lines join to already-drawn rows either side
3419    set r [expr {$row - 1}]
3420    if {$r < 0 || ![info exists iddrawn([lindex $displayorder $r])]} {
3421        set r $row
3422    }
3423    set er [expr {$endrow + 1}]
3424    if {$er >= $numcommits ||
3425        ![info exists iddrawn([lindex $displayorder $er])]} {
3426        set er $endrow
3427    }
3428    for {} {$r <= $er} {incr r} {
3429        set id [lindex $displayorder $r]
3430        set wasdrawn [info exists iddrawn($id)]
3431        if {!$wasdrawn} {
3432            drawcmitrow $r
3433        }
3434        if {$r == $er} break
3435        set nextid [lindex $displayorder [expr {$r + 1}]]
3436        if {$wasdrawn && [info exists iddrawn($nextid)]} {
3437            catch {unset prevlines}
3438            continue
3439        }
3440        drawparentlinks $id $r
3441
3442        if {[info exists lineends($r)]} {
3443            foreach lid $lineends($r) {
3444                unset prevlines($lid)
3445            }
3446        }
3447        set rowids [lindex $rowidlist $r]
3448        foreach lid $rowids {
3449            if {$lid eq {}} continue
3450            if {$lid eq $id} {
3451                # see if this is the first child of any of its parents
3452                foreach p [lindex $parentlist $r] {
3453                    if {[lsearch -exact $rowids $p] < 0} {
3454                        # make this line extend up to the child
3455                        set le [drawlineseg $p $r $er 0]
3456                        lappend lineends($le) $p
3457                        set prevlines($p) 1
3458                    }
3459                }
3460            } elseif {![info exists prevlines($lid)]} {
3461                set le [drawlineseg $lid $r $er 1]
3462                lappend lineends($le) $lid
3463                set prevlines($lid) 1
3464            }
3465        }
3466    }
3467}
3468
3469proc drawfrac {f0 f1} {
3470    global canv linespc
3471
3472    set ymax [lindex [$canv cget -scrollregion] 3]
3473    if {$ymax eq {} || $ymax == 0} return
3474    set y0 [expr {int($f0 * $ymax)}]
3475    set row [expr {int(($y0 - 3) / $linespc) - 1}]
3476    set y1 [expr {int($f1 * $ymax)}]
3477    set endrow [expr {int(($y1 - 3) / $linespc) + 1}]
3478    drawcommits $row $endrow
3479}
3480
3481proc drawvisible {} {
3482    global canv
3483    eval drawfrac [$canv yview]
3484}
3485
3486proc clear_display {} {
3487    global iddrawn linesegs
3488    global vhighlights fhighlights nhighlights rhighlights
3489
3490    allcanvs delete all
3491    catch {unset iddrawn}
3492    catch {unset linesegs}
3493    catch {unset vhighlights}
3494    catch {unset fhighlights}
3495    catch {unset nhighlights}
3496    catch {unset rhighlights}
3497}
3498
3499proc findcrossings {id} {
3500    global rowidlist parentlist numcommits rowoffsets displayorder
3501
3502    set cross {}
3503    set ccross {}
3504    foreach {s e} [rowranges $id] {
3505        if {$e >= $numcommits} {
3506            set e [expr {$numcommits - 1}]
3507        }
3508        if {$e <= $s} continue
3509        set x [lsearch -exact [lindex $rowidlist $e] $id]
3510        if {$x < 0} {
3511            puts "findcrossings: oops, no [shortids $id] in row $e"
3512            continue
3513        }
3514        for {set row $e} {[incr row -1] >= $s} {} {
3515            set olds [lindex $parentlist $row]
3516            set kid [lindex $displayorder $row]
3517            set kidx [lsearch -exact [lindex $rowidlist $row] $kid]
3518            if {$kidx < 0} continue
3519            set nextrow [lindex $rowidlist [expr {$row + 1}]]
3520            foreach p $olds {
3521                set px [lsearch -exact $nextrow $p]
3522                if {$px < 0} continue
3523                if {($kidx < $x && $x < $px) || ($px < $x && $x < $kidx)} {
3524                    if {[lsearch -exact $ccross $p] >= 0} continue
3525                    if {$x == $px + ($kidx < $px? -1: 1)} {
3526                        lappend ccross $p
3527                    } elseif {[lsearch -exact $cross $p] < 0} {
3528                        lappend cross $p
3529                    }
3530                }
3531            }
3532            set inc [lindex $rowoffsets $row $x]
3533            if {$inc eq {}} break
3534            incr x $inc
3535        }
3536    }
3537    return [concat $ccross {{}} $cross]
3538}
3539
3540proc assigncolor {id} {
3541    global colormap colors nextcolor
3542    global commitrow parentlist children children curview
3543
3544    if {[info exists colormap($id)]} return
3545    set ncolors [llength $colors]
3546    if {[info exists children($curview,$id)]} {
3547        set kids $children($curview,$id)
3548    } else {
3549        set kids {}
3550    }
3551    if {[llength $kids] == 1} {
3552        set child [lindex $kids 0]
3553        if {[info exists colormap($child)]
3554            && [llength [lindex $parentlist $commitrow($curview,$child)]] == 1} {
3555            set colormap($id) $colormap($child)
3556            return
3557        }
3558    }
3559    set badcolors {}
3560    set origbad {}
3561    foreach x [findcrossings $id] {
3562        if {$x eq {}} {
3563            # delimiter between corner crossings and other crossings
3564            if {[llength $badcolors] >= $ncolors - 1} break
3565            set origbad $badcolors
3566        }
3567        if {[info exists colormap($x)]
3568            && [lsearch -exact $badcolors $colormap($x)] < 0} {
3569            lappend badcolors $colormap($x)
3570        }
3571    }
3572    if {[llength $badcolors] >= $ncolors} {
3573        set badcolors $origbad
3574    }
3575    set origbad $badcolors
3576    if {[llength $badcolors] < $ncolors - 1} {
3577        foreach child $kids {
3578            if {[info exists colormap($child)]
3579                && [lsearch -exact $badcolors $colormap($child)] < 0} {
3580                lappend badcolors $colormap($child)
3581            }
3582            foreach p [lindex $parentlist $commitrow($curview,$child)] {
3583                if {[info exists colormap($p)]
3584                    && [lsearch -exact $badcolors $colormap($p)] < 0} {
3585                    lappend badcolors $colormap($p)
3586                }
3587            }
3588        }
3589        if {[llength $badcolors] >= $ncolors} {
3590            set badcolors $origbad
3591        }
3592    }
3593    for {set i 0} {$i <= $ncolors} {incr i} {
3594        set c [lindex $colors $nextcolor]
3595        if {[incr nextcolor] >= $ncolors} {
3596            set nextcolor 0
3597        }
3598        if {[lsearch -exact $badcolors $c]} break
3599    }
3600    set colormap($id) $c
3601}
3602
3603proc bindline {t id} {
3604    global canv
3605
3606    $canv bind $t <Enter> "lineenter %x %y $id"
3607    $canv bind $t <Motion> "linemotion %x %y $id"
3608    $canv bind $t <Leave> "lineleave $id"
3609    $canv bind $t <Button-1> "lineclick %x %y $id 1"
3610}
3611
3612proc drawtags {id x xt y1} {
3613    global idtags idheads idotherrefs mainhead
3614    global linespc lthickness
3615    global canv mainfont commitrow rowtextx curview fgcolor bgcolor
3616
3617    set marks {}
3618    set ntags 0
3619    set nheads 0
3620    if {[info exists idtags($id)]} {
3621        set marks $idtags($id)
3622        set ntags [llength $marks]
3623    }
3624    if {[info exists idheads($id)]} {
3625        set marks [concat $marks $idheads($id)]
3626        set nheads [llength $idheads($id)]
3627    }
3628    if {[info exists idotherrefs($id)]} {
3629        set marks [concat $marks $idotherrefs($id)]
3630    }
3631    if {$marks eq {}} {
3632        return $xt
3633    }
3634
3635    set delta [expr {int(0.5 * ($linespc - $lthickness))}]
3636    set yt [expr {$y1 - 0.5 * $linespc}]
3637    set yb [expr {$yt + $linespc - 1}]
3638    set xvals {}
3639    set wvals {}
3640    set i -1
3641    foreach tag $marks {
3642        incr i
3643        if {$i >= $ntags && $i < $ntags + $nheads && $tag eq $mainhead} {
3644            set wid [font measure [concat $mainfont bold] $tag]
3645        } else {
3646            set wid [font measure $mainfont $tag]
3647        }
3648        lappend xvals $xt
3649        lappend wvals $wid
3650        set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
3651    }
3652    set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
3653               -width $lthickness -fill black -tags tag.$id]
3654    $canv lower $t
3655    foreach tag $marks x $xvals wid $wvals {
3656        set xl [expr {$x + $delta}]
3657        set xr [expr {$x + $delta + $wid + $lthickness}]
3658        set font $mainfont
3659        if {[incr ntags -1] >= 0} {
3660            # draw a tag
3661            set t [$canv create polygon $x [expr {$yt + $delta}] $xl $yt \
3662                       $xr $yt $xr $yb $xl $yb $x [expr {$yb - $delta}] \
3663                       -width 1 -outline black -fill yellow -tags tag.$id]
3664            $canv bind $t <1> [list showtag $tag 1]
3665            set rowtextx($commitrow($curview,$id)) [expr {$xr + $linespc}]
3666        } else {
3667            # draw a head or other ref
3668            if {[incr nheads -1] >= 0} {
3669                set col green
3670                if {$tag eq $mainhead} {
3671                    lappend font bold
3672                }
3673            } else {
3674                set col "#ddddff"
3675            }
3676            set xl [expr {$xl - $delta/2}]
3677            $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
3678                -width 1 -outline black -fill $col -tags tag.$id
3679            if {[regexp {^(remotes/.*/|remotes/)} $tag match remoteprefix]} {
3680                set rwid [font measure $mainfont $remoteprefix]
3681                set xi [expr {$x + 1}]
3682                set yti [expr {$yt + 1}]
3683                set xri [expr {$x + $rwid}]
3684                $canv create polygon $xi $yti $xri $yti $xri $yb $xi $yb \
3685                        -width 0 -fill "#ffddaa" -tags tag.$id
3686            }
3687        }
3688        set t [$canv create text $xl $y1 -anchor w -text $tag -fill $fgcolor \
3689                   -font $font -tags [list tag.$id text]]
3690        if {$ntags >= 0} {
3691            $canv bind $t <1> [list showtag $tag 1]
3692        } elseif {$nheads >= 0} {
3693            $canv bind $t <Button-3> [list headmenu %X %Y $id $tag]
3694        }
3695    }
3696    return $xt
3697}
3698
3699proc xcoord {i level ln} {
3700    global canvx0 xspc1 xspc2
3701
3702    set x [expr {$canvx0 + $i * $xspc1($ln)}]
3703    if {$i > 0 && $i == $level} {
3704        set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
3705    } elseif {$i > $level} {
3706        set x [expr {$x + $xspc2 - $xspc1($ln)}]
3707    }
3708    return $x
3709}
3710
3711proc show_status {msg} {
3712    global canv mainfont fgcolor
3713
3714    clear_display
3715    $canv create text 3 3 -anchor nw -text $msg -font $mainfont \
3716        -tags text -fill $fgcolor
3717}
3718
3719# Insert a new commit as the child of the commit on row $row.
3720# The new commit will be displayed on row $row and the commits
3721# on that row and below will move down one row.
3722proc insertrow {row newcmit} {
3723    global displayorder parentlist commitlisted children
3724    global commitrow curview rowidlist rowoffsets numcommits
3725    global rowrangelist rowlaidout rowoptim numcommits
3726    global selectedline rowchk commitidx
3727
3728    if {$row >= $numcommits} {
3729        puts "oops, inserting new row $row but only have $numcommits rows"
3730        return
3731    }
3732    set p [lindex $displayorder $row]
3733    set displayorder [linsert $displayorder $row $newcmit]
3734    set parentlist [linsert $parentlist $row $p]
3735    set kids $children($curview,$p)
3736    lappend kids $newcmit
3737    set children($curview,$p) $kids
3738    set children($curview,$newcmit) {}
3739    set commitlisted [linsert $commitlisted $row 1]
3740    set l [llength $displayorder]
3741    for {set r $row} {$r < $l} {incr r} {
3742        set id [lindex $displayorder $r]
3743        set commitrow($curview,$id) $r
3744    }
3745    incr commitidx($curview)
3746
3747    set idlist [lindex $rowidlist $row]
3748    set offs [lindex $rowoffsets $row]
3749    set newoffs {}
3750    foreach x $idlist {
3751        if {$x eq {} || ($x eq $p && [llength $kids] == 1)} {
3752            lappend newoffs {}
3753        } else {
3754            lappend newoffs 0
3755        }
3756    }
3757    if {[llength $kids] == 1} {
3758        set col [lsearch -exact $idlist $p]
3759        lset idlist $col $newcmit
3760    } else {
3761        set col [llength $idlist]
3762        lappend idlist $newcmit
3763        lappend offs {}
3764        lset rowoffsets $row $offs
3765    }
3766    set rowidlist [linsert $rowidlist $row $idlist]
3767    set rowoffsets [linsert $rowoffsets [expr {$row+1}] $newoffs]
3768
3769    set rowrangelist [linsert $rowrangelist $row {}]
3770    if {[llength $kids] > 1} {
3771        set rp1 [expr {$row + 1}]
3772        set ranges [lindex $rowrangelist $rp1]
3773        if {$ranges eq {}} {
3774            set ranges [list $newcmit $p]
3775        } elseif {[lindex $ranges end-1] eq $p} {
3776            lset ranges end-1 $newcmit
3777        }
3778        lset rowrangelist $rp1 $ranges
3779    }
3780
3781    catch {unset rowchk}
3782
3783    incr rowlaidout
3784    incr rowoptim
3785    incr numcommits
3786
3787    if {[info exists selectedline] && $selectedline >= $row} {
3788        incr selectedline
3789    }
3790    redisplay
3791}
3792
3793# Remove a commit that was inserted with insertrow on row $row.
3794proc removerow {row} {
3795    global displayorder parentlist commitlisted children
3796    global commitrow curview rowidlist rowoffsets numcommits
3797    global rowrangelist idrowranges rowlaidout rowoptim numcommits
3798    global linesegends selectedline rowchk commitidx
3799
3800    if {$row >= $numcommits} {
3801        puts "oops, removing row $row but only have $numcommits rows"
3802        return
3803    }
3804    set rp1 [expr {$row + 1}]
3805    set id [lindex $displayorder $row]
3806    set p [lindex $parentlist $row]
3807    set displayorder [lreplace $displayorder $row $row]
3808    set parentlist [lreplace $parentlist $row $row]
3809    set commitlisted [lreplace $commitlisted $row $row]
3810    set kids $children($curview,$p)
3811    set i [lsearch -exact $kids $id]
3812    if {$i >= 0} {
3813        set kids [lreplace $kids $i $i]
3814        set children($curview,$p) $kids
3815    }
3816    set l [llength $displayorder]
3817    for {set r $row} {$r < $l} {incr r} {
3818        set id [lindex $displayorder $r]
3819        set commitrow($curview,$id) $r
3820    }
3821    incr commitidx($curview) -1
3822
3823    set rowidlist [lreplace $rowidlist $row $row]
3824    set rowoffsets [lreplace $rowoffsets $rp1 $rp1]
3825    if {$kids ne {}} {
3826        set offs [lindex $rowoffsets $row]
3827        set offs [lreplace $offs end end]
3828        lset rowoffsets $row $offs
3829    }
3830
3831    set rowrangelist [lreplace $rowrangelist $row $row]
3832    if {[llength $kids] > 0} {
3833        set ranges [lindex $rowrangelist $row]
3834        if {[lindex $ranges end-1] eq $id} {
3835            set ranges [lreplace $ranges end-1 end]
3836            lset rowrangelist $row $ranges
3837        }
3838    }
3839
3840    catch {unset rowchk}
3841
3842    incr rowlaidout -1
3843    incr rowoptim -1
3844    incr numcommits -1
3845
3846    if {[info exists selectedline] && $selectedline > $row} {
3847        incr selectedline -1
3848    }
3849    redisplay
3850}
3851
3852# Don't change the text pane cursor if it is currently the hand cursor,
3853# showing that we are over a sha1 ID link.
3854proc settextcursor {c} {
3855    global ctext curtextcursor
3856
3857    if {[$ctext cget -cursor] == $curtextcursor} {
3858        $ctext config -cursor $c
3859    }
3860    set curtextcursor $c
3861}
3862
3863proc nowbusy {what} {
3864    global isbusy
3865
3866    if {[array names isbusy] eq {}} {
3867        . config -cursor watch
3868        settextcursor watch
3869    }
3870    set isbusy($what) 1
3871}
3872
3873proc notbusy {what} {
3874    global isbusy maincursor textcursor
3875
3876    catch {unset isbusy($what)}
3877    if {[array names isbusy] eq {}} {
3878        . config -cursor $maincursor
3879        settextcursor $textcursor
3880    }
3881}
3882
3883proc findmatches {f} {
3884    global findtype foundstring foundstrlen
3885    if {$findtype == "Regexp"} {
3886        set matches [regexp -indices -all -inline $foundstring $f]
3887    } else {
3888        if {$findtype == "IgnCase"} {
3889            set str [string tolower $f]
3890        } else {
3891            set str $f
3892        }
3893        set matches {}
3894        set i 0
3895        while {[set j [string first $foundstring $str $i]] >= 0} {
3896            lappend matches [list $j [expr {$j+$foundstrlen-1}]]
3897            set i [expr {$j + $foundstrlen}]
3898        }
3899    }
3900    return $matches
3901}
3902
3903proc dofind {} {
3904    global findtype findloc findstring markedmatches commitinfo
3905    global numcommits displayorder linehtag linentag linedtag
3906    global mainfont canv canv2 canv3 selectedline
3907    global matchinglines foundstring foundstrlen matchstring
3908    global commitdata
3909
3910    stopfindproc
3911    unmarkmatches
3912    cancel_next_highlight
3913    focus .
3914    set matchinglines {}
3915    if {$findtype == "IgnCase"} {
3916        set foundstring [string tolower $findstring]
3917    } else {
3918        set foundstring $findstring
3919    }
3920    set foundstrlen [string length $findstring]
3921    if {$foundstrlen == 0} return
3922    regsub -all {[*?\[\\]} $foundstring {\\&} matchstring
3923    set matchstring "*$matchstring*"
3924    if {![info exists selectedline]} {
3925        set oldsel -1
3926    } else {
3927        set oldsel $selectedline
3928    }
3929    set didsel 0
3930    set fldtypes {Headline Author Date Committer CDate Comments}
3931    set l -1
3932    foreach id $displayorder {
3933        set d $commitdata($id)
3934        incr l
3935        if {$findtype == "Regexp"} {
3936            set doesmatch [regexp $foundstring $d]
3937        } elseif {$findtype == "IgnCase"} {
3938            set doesmatch [string match -nocase $matchstring $d]
3939        } else {
3940            set doesmatch [string match $matchstring $d]
3941        }
3942        if {!$doesmatch} continue
3943        if {![info exists commitinfo($id)]} {
3944            getcommit $id
3945        }
3946        set info $commitinfo($id)
3947        set doesmatch 0
3948        foreach f $info ty $fldtypes {
3949            if {$findloc != "All fields" && $findloc != $ty} {
3950                continue
3951            }
3952            set matches [findmatches $f]
3953            if {$matches == {}} continue
3954            set doesmatch 1
3955            if {$ty == "Headline"} {
3956                drawcommits $l
3957                markmatches $canv $l $f $linehtag($l) $matches $mainfont
3958            } elseif {$ty == "Author"} {
3959                drawcommits $l
3960                markmatches $canv2 $l $f $linentag($l) $matches $mainfont
3961            } elseif {$ty == "Date"} {
3962                drawcommits $l
3963                markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
3964            }
3965        }
3966        if {$doesmatch} {
3967            lappend matchinglines $l
3968            if {!$didsel && $l > $oldsel} {
3969                findselectline $l
3970                set didsel 1
3971            }
3972        }
3973    }
3974    if {$matchinglines == {}} {
3975        bell
3976    } elseif {!$didsel} {
3977        findselectline [lindex $matchinglines 0]
3978    }
3979}
3980
3981proc findselectline {l} {
3982    global findloc commentend ctext
3983    selectline $l 1
3984    if {$findloc == "All fields" || $findloc == "Comments"} {
3985        # highlight the matches in the comments
3986        set f [$ctext get 1.0 $commentend]
3987        set matches [findmatches $f]
3988        foreach match $matches {
3989            set start [lindex $match 0]
3990            set end [expr {[lindex $match 1] + 1}]
3991            $ctext tag add found "1.0 + $start c" "1.0 + $end c"
3992        }
3993    }
3994}
3995
3996proc findnext {restart} {
3997    global matchinglines selectedline
3998    if {![info exists matchinglines]} {
3999        if {$restart} {
4000            dofind
4001        }
4002        return
4003    }
4004    if {![info exists selectedline]} return
4005    foreach l $matchinglines {
4006        if {$l > $selectedline} {
4007            findselectline $l
4008            return
4009        }
4010    }
4011    bell
4012}
4013
4014proc findprev {} {
4015    global matchinglines selectedline
4016    if {![info exists matchinglines]} {
4017        dofind
4018        return
4019    }
4020    if {![info exists selectedline]} return
4021    set prev {}
4022    foreach l $matchinglines {
4023        if {$l >= $selectedline} break
4024        set prev $l
4025    }
4026    if {$prev != {}} {
4027        findselectline $prev
4028    } else {
4029        bell
4030    }
4031}
4032
4033proc stopfindproc {{done 0}} {
4034    global findprocpid findprocfile findids
4035    global ctext findoldcursor phase maincursor textcursor
4036    global findinprogress
4037
4038    catch {unset findids}
4039    if {[info exists findprocpid]} {
4040        if {!$done} {
4041            catch {exec kill $findprocpid}
4042        }
4043        catch {close $findprocfile}
4044        unset findprocpid
4045    }
4046    catch {unset findinprogress}
4047    notbusy find
4048}
4049
4050# mark a commit as matching by putting a yellow background
4051# behind the headline
4052proc markheadline {l id} {
4053    global canv mainfont linehtag
4054
4055    drawcommits $l
4056    set bbox [$canv bbox $linehtag($l)]
4057    set t [$canv create rect $bbox -outline {} -tags matches -fill yellow]
4058    $canv lower $t
4059}
4060
4061# mark the bits of a headline, author or date that match a find string
4062proc markmatches {canv l str tag matches font} {
4063    set bbox [$canv bbox $tag]
4064    set x0 [lindex $bbox 0]
4065    set y0 [lindex $bbox 1]
4066    set y1 [lindex $bbox 3]
4067    foreach match $matches {
4068        set start [lindex $match 0]
4069        set end [lindex $match 1]
4070        if {$start > $end} continue
4071        set xoff [font measure $font [string range $str 0 [expr {$start-1}]]]
4072        set xlen [font measure $font [string range $str 0 [expr {$end}]]]
4073        set t [$canv create rect [expr {$x0+$xoff}] $y0 \
4074                   [expr {$x0+$xlen+2}] $y1 \
4075                   -outline {} -tags matches -fill yellow]
4076        $canv lower $t
4077    }
4078}
4079
4080proc unmarkmatches {} {
4081    global matchinglines findids
4082    allcanvs delete matches
4083    catch {unset matchinglines}
4084    catch {unset findids}
4085}
4086
4087proc selcanvline {w x y} {
4088    global canv canvy0 ctext linespc
4089    global rowtextx
4090    set ymax [lindex [$canv cget -scrollregion] 3]
4091    if {$ymax == {}} return
4092    set yfrac [lindex [$canv yview] 0]
4093    set y [expr {$y + $yfrac * $ymax}]
4094    set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
4095    if {$l < 0} {
4096        set l 0
4097    }
4098    if {$w eq $canv} {
4099        if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
4100    }
4101    unmarkmatches
4102    selectline $l 1
4103}
4104
4105proc commit_descriptor {p} {
4106    global commitinfo
4107    if {![info exists commitinfo($p)]} {
4108        getcommit $p
4109    }
4110    set l "..."
4111    if {[llength $commitinfo($p)] > 1} {
4112        set l [lindex $commitinfo($p) 0]
4113    }
4114    return "$p ($l)\n"
4115}
4116
4117# append some text to the ctext widget, and make any SHA1 ID
4118# that we know about be a clickable link.
4119proc appendwithlinks {text tags} {
4120    global ctext commitrow linknum curview
4121
4122    set start [$ctext index "end - 1c"]
4123    $ctext insert end $text $tags
4124    set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
4125    foreach l $links {
4126        set s [lindex $l 0]
4127        set e [lindex $l 1]
4128        set linkid [string range $text $s $e]
4129        if {![info exists commitrow($curview,$linkid)]} continue
4130        incr e
4131        $ctext tag add link "$start + $s c" "$start + $e c"
4132        $ctext tag add link$linknum "$start + $s c" "$start + $e c"
4133        $ctext tag bind link$linknum <1> \
4134            [list selectline $commitrow($curview,$linkid) 1]
4135        incr linknum
4136    }
4137    $ctext tag conf link -foreground blue -underline 1
4138    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
4139    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
4140}
4141
4142proc viewnextline {dir} {
4143    global canv linespc
4144
4145    $canv delete hover
4146    set ymax [lindex [$canv cget -scrollregion] 3]
4147    set wnow [$canv yview]
4148    set wtop [expr {[lindex $wnow 0] * $ymax}]
4149    set newtop [expr {$wtop + $dir * $linespc}]
4150    if {$newtop < 0} {
4151        set newtop 0
4152    } elseif {$newtop > $ymax} {
4153        set newtop $ymax
4154    }
4155    allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
4156}
4157
4158# add a list of tag or branch names at position pos
4159# returns the number of names inserted
4160proc appendrefs {pos ids var} {
4161    global ctext commitrow linknum curview $var maxrefs
4162
4163    if {[catch {$ctext index $pos}]} {
4164        return 0
4165    }
4166    $ctext conf -state normal
4167    $ctext delete $pos "$pos lineend"
4168    set tags {}
4169    foreach id $ids {
4170        foreach tag [set $var\($id\)] {
4171            lappend tags [list $tag $id]
4172        }
4173    }
4174    if {[llength $tags] > $maxrefs} {
4175        $ctext insert $pos "many ([llength $tags])"
4176    } else {
4177        set tags [lsort -index 0 -decreasing $tags]
4178        set sep {}
4179        foreach ti $tags {
4180            set id [lindex $ti 1]
4181            set lk link$linknum
4182            incr linknum
4183            $ctext tag delete $lk
4184            $ctext insert $pos $sep
4185            $ctext insert $pos [lindex $ti 0] $lk
4186            if {[info exists commitrow($curview,$id)]} {
4187                $ctext tag conf $lk -foreground blue
4188                $ctext tag bind $lk <1> \
4189                    [list selectline $commitrow($curview,$id) 1]
4190                $ctext tag conf $lk -underline 1
4191                $ctext tag bind $lk <Enter> { %W configure -cursor hand2 }
4192                $ctext tag bind $lk <Leave> \
4193                    { %W configure -cursor $curtextcursor }
4194            }
4195            set sep ", "
4196        }
4197    }
4198    $ctext conf -state disabled
4199    return [llength $tags]
4200}
4201
4202# called when we have finished computing the nearby tags
4203proc dispneartags {delay} {
4204    global selectedline currentid showneartags tagphase
4205
4206    if {![info exists selectedline] || !$showneartags} return
4207    after cancel dispnexttag
4208    if {$delay} {
4209        after 200 dispnexttag
4210        set tagphase -1
4211    } else {
4212        after idle dispnexttag
4213        set tagphase 0
4214    }
4215}
4216
4217proc dispnexttag {} {
4218    global selectedline currentid showneartags tagphase ctext
4219
4220    if {![info exists selectedline] || !$showneartags} return
4221    switch -- $tagphase {
4222        0 {
4223            set dtags [desctags $currentid]
4224            if {$dtags ne {}} {
4225                appendrefs precedes $dtags idtags
4226            }
4227        }
4228        1 {
4229            set atags [anctags $currentid]
4230            if {$atags ne {}} {
4231                appendrefs follows $atags idtags
4232            }
4233        }
4234        2 {
4235            set dheads [descheads $currentid]
4236            if {$dheads ne {}} {
4237                if {[appendrefs branch $dheads idheads] > 1
4238                    && [$ctext get "branch -3c"] eq "h"} {
4239                    # turn "Branch" into "Branches"
4240                    $ctext conf -state normal
4241                    $ctext insert "branch -2c" "es"
4242                    $ctext conf -state disabled
4243                }
4244            }
4245        }
4246    }
4247    if {[incr tagphase] <= 2} {
4248        after idle dispnexttag
4249    }
4250}
4251
4252proc selectline {l isnew} {
4253    global canv canv2 canv3 ctext commitinfo selectedline
4254    global displayorder linehtag linentag linedtag
4255    global canvy0 linespc parentlist children curview
4256    global currentid sha1entry
4257    global commentend idtags linknum
4258    global mergemax numcommits pending_select
4259    global cmitmode showneartags allcommits
4260
4261    catch {unset pending_select}
4262    $canv delete hover
4263    normalline
4264    cancel_next_highlight
4265    if {$l < 0 || $l >= $numcommits} return
4266    set y [expr {$canvy0 + $l * $linespc}]
4267    set ymax [lindex [$canv cget -scrollregion] 3]
4268    set ytop [expr {$y - $linespc - 1}]
4269    set ybot [expr {$y + $linespc + 1}]
4270    set wnow [$canv yview]
4271    set wtop [expr {[lindex $wnow 0] * $ymax}]
4272    set wbot [expr {[lindex $wnow 1] * $ymax}]
4273    set wh [expr {$wbot - $wtop}]
4274    set newtop $wtop
4275    if {$ytop < $wtop} {
4276        if {$ybot < $wtop} {
4277            set newtop [expr {$y - $wh / 2.0}]
4278        } else {
4279            set newtop $ytop
4280            if {$newtop > $wtop - $linespc} {
4281                set newtop [expr {$wtop - $linespc}]
4282            }
4283        }
4284    } elseif {$ybot > $wbot} {
4285        if {$ytop > $wbot} {
4286            set newtop [expr {$y - $wh / 2.0}]
4287        } else {
4288            set newtop [expr {$ybot - $wh}]
4289            if {$newtop < $wtop + $linespc} {
4290                set newtop [expr {$wtop + $linespc}]
4291            }
4292        }
4293    }
4294    if {$newtop != $wtop} {
4295        if {$newtop < 0} {
4296            set newtop 0
4297        }
4298        allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
4299        drawvisible
4300    }
4301
4302    if {![info exists linehtag($l)]} return
4303    $canv delete secsel
4304    set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
4305               -tags secsel -fill [$canv cget -selectbackground]]
4306    $canv lower $t
4307    $canv2 delete secsel
4308    set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
4309               -tags secsel -fill [$canv2 cget -selectbackground]]
4310    $canv2 lower $t
4311    $canv3 delete secsel
4312    set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
4313               -tags secsel -fill [$canv3 cget -selectbackground]]
4314    $canv3 lower $t
4315
4316    if {$isnew} {
4317        addtohistory [list selectline $l 0]
4318    }
4319
4320    set selectedline $l
4321
4322    set id [lindex $displayorder $l]
4323    set currentid $id
4324    $sha1entry delete 0 end
4325    $sha1entry insert 0 $id
4326    $sha1entry selection from 0
4327    $sha1entry selection to end
4328    rhighlight_sel $id
4329
4330    $ctext conf -state normal
4331    clear_ctext
4332    set linknum 0
4333    set info $commitinfo($id)
4334    set date [formatdate [lindex $info 2]]
4335    $ctext insert end "Author: [lindex $info 1]  $date\n"
4336    set date [formatdate [lindex $info 4]]
4337    $ctext insert end "Committer: [lindex $info 3]  $date\n"
4338    if {[info exists idtags($id)]} {
4339        $ctext insert end "Tags:"
4340        foreach tag $idtags($id) {
4341            $ctext insert end " $tag"
4342        }
4343        $ctext insert end "\n"
4344    }
4345
4346    set headers {}
4347    set olds [lindex $parentlist $l]
4348    if {[llength $olds] > 1} {
4349        set np 0
4350        foreach p $olds {
4351            if {$np >= $mergemax} {
4352                set tag mmax
4353            } else {
4354                set tag m$np
4355            }
4356            $ctext insert end "Parent: " $tag
4357            appendwithlinks [commit_descriptor $p] {}
4358            incr np
4359        }
4360    } else {
4361        foreach p $olds {
4362            append headers "Parent: [commit_descriptor $p]"
4363        }
4364    }
4365
4366    foreach c $children($curview,$id) {
4367        append headers "Child:  [commit_descriptor $c]"
4368    }
4369
4370    # make anything that looks like a SHA1 ID be a clickable link
4371    appendwithlinks $headers {}
4372    if {$showneartags} {
4373        if {![info exists allcommits]} {
4374            getallcommits
4375        }
4376        $ctext insert end "Branch: "
4377        $ctext mark set branch "end -1c"
4378        $ctext mark gravity branch left
4379        $ctext insert end "\nFollows: "
4380        $ctext mark set follows "end -1c"
4381        $ctext mark gravity follows left
4382        $ctext insert end "\nPrecedes: "
4383        $ctext mark set precedes "end -1c"
4384        $ctext mark gravity precedes left
4385        $ctext insert end "\n"
4386        dispneartags 1
4387    }
4388    $ctext insert end "\n"
4389    set comment [lindex $info 5]
4390    if {[string first "\r" $comment] >= 0} {
4391        set comment [string map {"\r" "\n    "} $comment]
4392    }
4393    appendwithlinks $comment {comment}
4394
4395    $ctext tag delete Comments
4396    $ctext tag remove found 1.0 end
4397    $ctext conf -state disabled
4398    set commentend [$ctext index "end - 1c"]
4399
4400    init_flist "Comments"
4401    if {$cmitmode eq "tree"} {
4402        gettree $id
4403    } elseif {[llength $olds] <= 1} {
4404        startdiff $id
4405    } else {
4406        mergediff $id $l
4407    }
4408}
4409
4410proc selfirstline {} {
4411    unmarkmatches
4412    selectline 0 1
4413}
4414
4415proc sellastline {} {
4416    global numcommits
4417    unmarkmatches
4418    set l [expr {$numcommits - 1}]
4419    selectline $l 1
4420}
4421
4422proc selnextline {dir} {
4423    global selectedline
4424    if {![info exists selectedline]} return
4425    set l [expr {$selectedline + $dir}]
4426    unmarkmatches
4427    selectline $l 1
4428}
4429
4430proc selnextpage {dir} {
4431    global canv linespc selectedline numcommits
4432
4433    set lpp [expr {([winfo height $canv] - 2) / $linespc}]
4434    if {$lpp < 1} {
4435        set lpp 1
4436    }
4437    allcanvs yview scroll [expr {$dir * $lpp}] units
4438    drawvisible
4439    if {![info exists selectedline]} return
4440    set l [expr {$selectedline + $dir * $lpp}]
4441    if {$l < 0} {
4442        set l 0
4443    } elseif {$l >= $numcommits} {
4444        set l [expr $numcommits - 1]
4445    }
4446    unmarkmatches
4447    selectline $l 1
4448}
4449
4450proc unselectline {} {
4451    global selectedline currentid
4452
4453    catch {unset selectedline}
4454    catch {unset currentid}
4455    allcanvs delete secsel
4456    rhighlight_none
4457    cancel_next_highlight
4458}
4459
4460proc reselectline {} {
4461    global selectedline
4462
4463    if {[info exists selectedline]} {
4464        selectline $selectedline 0
4465    }
4466}
4467
4468proc addtohistory {cmd} {
4469    global history historyindex curview
4470
4471    set elt [list $curview $cmd]
4472    if {$historyindex > 0
4473        && [lindex $history [expr {$historyindex - 1}]] == $elt} {
4474        return
4475    }
4476
4477    if {$historyindex < [llength $history]} {
4478        set history [lreplace $history $historyindex end $elt]
4479    } else {
4480        lappend history $elt
4481    }
4482    incr historyindex
4483    if {$historyindex > 1} {
4484        .tf.bar.leftbut conf -state normal
4485    } else {
4486        .tf.bar.leftbut conf -state disabled
4487    }
4488    .tf.bar.rightbut conf -state disabled
4489}
4490
4491proc godo {elt} {
4492    global curview
4493
4494    set view [lindex $elt 0]
4495    set cmd [lindex $elt 1]
4496    if {$curview != $view} {
4497        showview $view
4498    }
4499    eval $cmd
4500}
4501
4502proc goback {} {
4503    global history historyindex
4504
4505    if {$historyindex > 1} {
4506        incr historyindex -1
4507        godo [lindex $history [expr {$historyindex - 1}]]
4508        .tf.bar.rightbut conf -state normal
4509    }
4510    if {$historyindex <= 1} {
4511        .tf.bar.leftbut conf -state disabled
4512    }
4513}
4514
4515proc goforw {} {
4516    global history historyindex
4517
4518    if {$historyindex < [llength $history]} {
4519        set cmd [lindex $history $historyindex]
4520        incr historyindex
4521        godo $cmd
4522        .tf.bar.leftbut conf -state normal
4523    }
4524    if {$historyindex >= [llength $history]} {
4525        .tf.bar.rightbut conf -state disabled
4526    }
4527}
4528
4529proc gettree {id} {
4530    global treefilelist treeidlist diffids diffmergeid treepending nullid
4531
4532    set diffids $id
4533    catch {unset diffmergeid}
4534    if {![info exists treefilelist($id)]} {
4535        if {![info exists treepending]} {
4536            if {$id ne $nullid} {
4537                set cmd [concat | git ls-tree -r $id]
4538            } else {
4539                set cmd [concat | git ls-files]
4540            }
4541            if {[catch {set gtf [open $cmd r]}]} {
4542                return
4543            }
4544            set treepending $id
4545            set treefilelist($id) {}
4546            set treeidlist($id) {}
4547            fconfigure $gtf -blocking 0
4548            filerun $gtf [list gettreeline $gtf $id]
4549        }
4550    } else {
4551        setfilelist $id
4552    }
4553}
4554
4555proc gettreeline {gtf id} {
4556    global treefilelist treeidlist treepending cmitmode diffids nullid
4557
4558    set nl 0
4559    while {[incr nl] <= 1000 && [gets $gtf line] >= 0} {
4560        if {$diffids ne $nullid} {
4561            set tl [split $line "\t"]
4562            if {[lindex $tl 0 1] ne "blob"} continue
4563            set sha1 [lindex $tl 0 2]
4564            set fname [lindex $tl 1]
4565            if {[string index $fname 0] eq "\""} {
4566                set fname [lindex $fname 0]
4567            }
4568            lappend treeidlist($id) $sha1
4569        } else {
4570            set fname $line
4571        }
4572        lappend treefilelist($id) $fname
4573    }
4574    if {![eof $gtf]} {
4575        return [expr {$nl >= 1000? 2: 1}]
4576    }
4577    close $gtf
4578    unset treepending
4579    if {$cmitmode ne "tree"} {
4580        if {![info exists diffmergeid]} {
4581            gettreediffs $diffids
4582        }
4583    } elseif {$id ne $diffids} {
4584        gettree $diffids
4585    } else {
4586        setfilelist $id
4587    }
4588    return 0
4589}
4590
4591proc showfile {f} {
4592    global treefilelist treeidlist diffids nullid
4593    global ctext commentend
4594
4595    set i [lsearch -exact $treefilelist($diffids) $f]
4596    if {$i < 0} {
4597        puts "oops, $f not in list for id $diffids"
4598        return
4599    }
4600    if {$diffids ne $nullid} {
4601        set blob [lindex $treeidlist($diffids) $i]
4602        if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} {
4603            puts "oops, error reading blob $blob: $err"
4604            return
4605        }
4606    } else {
4607        if {[catch {set bf [open $f r]} err]} {
4608            puts "oops, can't read $f: $err"
4609            return
4610        }
4611    }
4612    fconfigure $bf -blocking 0
4613    filerun $bf [list getblobline $bf $diffids]
4614    $ctext config -state normal
4615    clear_ctext $commentend
4616    $ctext insert end "\n"
4617    $ctext insert end "$f\n" filesep
4618    $ctext config -state disabled
4619    $ctext yview $commentend
4620}
4621
4622proc getblobline {bf id} {
4623    global diffids cmitmode ctext
4624
4625    if {$id ne $diffids || $cmitmode ne "tree"} {
4626        catch {close $bf}
4627        return 0
4628    }
4629    $ctext config -state normal
4630    set nl 0
4631    while {[incr nl] <= 1000 && [gets $bf line] >= 0} {
4632        $ctext insert end "$line\n"
4633    }
4634    if {[eof $bf]} {
4635        # delete last newline
4636        $ctext delete "end - 2c" "end - 1c"
4637        close $bf
4638        return 0
4639    }
4640    $ctext config -state disabled
4641    return [expr {$nl >= 1000? 2: 1}]
4642}
4643
4644proc mergediff {id l} {
4645    global diffmergeid diffopts mdifffd
4646    global diffids
4647    global parentlist
4648
4649    set diffmergeid $id
4650    set diffids $id
4651    # this doesn't seem to actually affect anything...
4652    set env(GIT_DIFF_OPTS) $diffopts
4653    set cmd [concat | git diff-tree --no-commit-id --cc $id]
4654    if {[catch {set mdf [open $cmd r]} err]} {
4655        error_popup "Error getting merge diffs: $err"
4656        return
4657    }
4658    fconfigure $mdf -blocking 0
4659    set mdifffd($id) $mdf
4660    set np [llength [lindex $parentlist $l]]
4661    filerun $mdf [list getmergediffline $mdf $id $np]
4662}
4663
4664proc getmergediffline {mdf id np} {
4665    global diffmergeid ctext cflist mergemax
4666    global difffilestart mdifffd
4667
4668    $ctext conf -state normal
4669    set nr 0
4670    while {[incr nr] <= 1000 && [gets $mdf line] >= 0} {
4671        if {![info exists diffmergeid] || $id != $diffmergeid
4672            || $mdf != $mdifffd($id)} {
4673            close $mdf
4674            return 0
4675        }
4676        if {[regexp {^diff --cc (.*)} $line match fname]} {
4677            # start of a new file
4678            $ctext insert end "\n"
4679            set here [$ctext index "end - 1c"]
4680            lappend difffilestart $here
4681            add_flist [list $fname]
4682            set l [expr {(78 - [string length $fname]) / 2}]
4683            set pad [string range "----------------------------------------" 1 $l]
4684            $ctext insert end "$pad $fname $pad\n" filesep
4685        } elseif {[regexp {^@@} $line]} {
4686            $ctext insert end "$line\n" hunksep
4687        } elseif {[regexp {^[0-9a-f]{40}$} $line] || [regexp {^index} $line]} {
4688            # do nothing
4689        } else {
4690            # parse the prefix - one ' ', '-' or '+' for each parent
4691            set spaces {}
4692            set minuses {}
4693            set pluses {}
4694            set isbad 0
4695            for {set j 0} {$j < $np} {incr j} {
4696                set c [string range $line $j $j]
4697                if {$c == " "} {
4698                    lappend spaces $j
4699                } elseif {$c == "-"} {
4700                    lappend minuses $j
4701                } elseif {$c == "+"} {
4702                    lappend pluses $j
4703                } else {
4704                    set isbad 1
4705                    break
4706                }
4707            }
4708            set tags {}
4709            set num {}
4710            if {!$isbad && $minuses ne {} && $pluses eq {}} {
4711                # line doesn't appear in result, parents in $minuses have the line
4712                set num [lindex $minuses 0]
4713            } elseif {!$isbad && $pluses ne {} && $minuses eq {}} {
4714                # line appears in result, parents in $pluses don't have the line
4715                lappend tags mresult
4716                set num [lindex $spaces 0]
4717            }
4718            if {$num ne {}} {
4719                if {$num >= $mergemax} {
4720                    set num "max"
4721                }
4722                lappend tags m$num
4723            }
4724            $ctext insert end "$line\n" $tags
4725        }
4726    }
4727    $ctext conf -state disabled
4728    if {[eof $mdf]} {
4729        close $mdf
4730        return 0
4731    }
4732    return [expr {$nr >= 1000? 2: 1}]
4733}
4734
4735proc startdiff {ids} {
4736    global treediffs diffids treepending diffmergeid nullid
4737
4738    set diffids $ids
4739    catch {unset diffmergeid}
4740    if {![info exists treediffs($ids)] || [lsearch -exact $ids $nullid] >= 0} {
4741        if {![info exists treepending]} {
4742            gettreediffs $ids
4743        }
4744    } else {
4745        addtocflist $ids
4746    }
4747}
4748
4749proc addtocflist {ids} {
4750    global treediffs cflist
4751    add_flist $treediffs($ids)
4752    getblobdiffs $ids
4753}
4754
4755proc diffcmd {ids flags} {
4756    global nullid
4757
4758    set i [lsearch -exact $ids $nullid]
4759    if {$i >= 0} {
4760        set cmd [concat | git diff-index $flags]
4761        if {[llength $ids] > 1} {
4762            if {$i == 0} {
4763                lappend cmd -R [lindex $ids 1]
4764            } else {
4765                lappend cmd [lindex $ids 0]
4766            }
4767        } else {
4768            lappend cmd HEAD
4769        }
4770    } else {
4771        set cmd [concat | git diff-tree --no-commit-id -r $flags $ids]
4772    }
4773    return $cmd
4774}
4775
4776proc gettreediffs {ids} {
4777    global treediff treepending
4778
4779    set treepending $ids
4780    set treediff {}
4781    if {[catch {set gdtf [open [diffcmd $ids {}] r]}]} return
4782    fconfigure $gdtf -blocking 0
4783    filerun $gdtf [list gettreediffline $gdtf $ids]
4784}
4785
4786proc gettreediffline {gdtf ids} {
4787    global treediff treediffs treepending diffids diffmergeid
4788    global cmitmode
4789
4790    set nr 0
4791    while {[incr nr] <= 1000 && [gets $gdtf line] >= 0} {
4792        set file [lindex $line 5]
4793        lappend treediff $file
4794    }
4795    if {![eof $gdtf]} {
4796        return [expr {$nr >= 1000? 2: 1}]
4797    }
4798    close $gdtf
4799    set treediffs($ids) $treediff
4800    unset treepending
4801    if {$cmitmode eq "tree"} {
4802        gettree $diffids
4803    } elseif {$ids != $diffids} {
4804        if {![info exists diffmergeid]} {
4805            gettreediffs $diffids
4806        }
4807    } else {
4808        addtocflist $ids
4809    }
4810    return 0
4811}
4812
4813proc getblobdiffs {ids} {
4814    global diffopts blobdifffd diffids env curdifftag curtagstart
4815    global diffinhdr treediffs
4816
4817    set env(GIT_DIFF_OPTS) $diffopts
4818    if {[catch {set bdf [open [diffcmd $ids {-p -C}] r]} err]} {
4819        puts "error getting diffs: $err"
4820        return
4821    }
4822    set diffinhdr 0
4823    fconfigure $bdf -blocking 0
4824    set blobdifffd($ids) $bdf
4825    set curdifftag Comments
4826    set curtagstart 0.0
4827    filerun $bdf [list getblobdiffline $bdf $diffids]
4828}
4829
4830proc setinlist {var i val} {
4831    global $var
4832
4833    while {[llength [set $var]] < $i} {
4834        lappend $var {}
4835    }
4836    if {[llength [set $var]] == $i} {
4837        lappend $var $val
4838    } else {
4839        lset $var $i $val
4840    }
4841}
4842
4843proc getblobdiffline {bdf ids} {
4844    global diffids blobdifffd ctext curdifftag curtagstart
4845    global diffnexthead diffnextnote difffilestart
4846    global diffinhdr treediffs
4847
4848    set nr 0
4849    $ctext conf -state normal
4850    while {[incr nr] <= 1000 && [gets $bdf line] >= 0} {
4851        if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
4852            close $bdf
4853            return 0
4854        }
4855        if {[regexp {^diff --git a/(.*) b/(.*)} $line match fname newname]} {
4856            # start of a new file
4857            $ctext insert end "\n"
4858            $ctext tag add $curdifftag $curtagstart end
4859            set here [$ctext index "end - 1c"]
4860            set curtagstart $here
4861            set header $newname
4862            set i [lsearch -exact $treediffs($ids) $fname]
4863            if {$i >= 0} {
4864                setinlist difffilestart $i $here
4865            }
4866            if {$newname ne $fname} {
4867                set i [lsearch -exact $treediffs($ids) $newname]
4868                if {$i >= 0} {
4869                    setinlist difffilestart $i $here
4870                }
4871            }
4872            set curdifftag "f:$fname"
4873            $ctext tag delete $curdifftag
4874            set l [expr {(78 - [string length $header]) / 2}]
4875            set pad [string range "----------------------------------------" \
4876                         1 $l]
4877            $ctext insert end "$pad $header $pad\n" filesep
4878            set diffinhdr 1
4879        } elseif {$diffinhdr && [string compare -length 3 $line "---"] == 0} {
4880            # do nothing
4881        } elseif {$diffinhdr && [string compare -length 3 $line "+++"] == 0} {
4882            set diffinhdr 0
4883        } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
4884                       $line match f1l f1c f2l f2c rest]} {
4885            $ctext insert end "$line\n" hunksep
4886            set diffinhdr 0
4887        } else {
4888            set x [string range $line 0 0]
4889            if {$x == "-" || $x == "+"} {
4890                set tag [expr {$x == "+"}]
4891                $ctext insert end "$line\n" d$tag
4892            } elseif {$x == " "} {
4893                $ctext insert end "$line\n"
4894            } elseif {$diffinhdr || $x == "\\"} {
4895                # e.g. "\ No newline at end of file"
4896                $ctext insert end "$line\n" filesep
4897            } else {
4898                # Something else we don't recognize
4899                if {$curdifftag != "Comments"} {
4900                    $ctext insert end "\n"
4901                    $ctext tag add $curdifftag $curtagstart end
4902                    set curtagstart [$ctext index "end - 1c"]
4903                    set curdifftag Comments
4904                }
4905                $ctext insert end "$line\n" filesep
4906            }
4907        }
4908    }
4909    $ctext conf -state disabled
4910    if {[eof $bdf]} {
4911        close $bdf
4912        if {$ids == $diffids && $bdf == $blobdifffd($ids)} {
4913            $ctext tag add $curdifftag $curtagstart end
4914        }
4915        return 0
4916    }
4917    return [expr {$nr >= 1000? 2: 1}]
4918}
4919
4920proc changediffdisp {} {
4921    global ctext diffelide
4922
4923    $ctext tag conf d0 -elide [lindex $diffelide 0]
4924    $ctext tag conf d1 -elide [lindex $diffelide 1]
4925}
4926
4927proc prevfile {} {
4928    global difffilestart ctext
4929    set prev [lindex $difffilestart 0]
4930    set here [$ctext index @0,0]
4931    foreach loc $difffilestart {
4932        if {[$ctext compare $loc >= $here]} {
4933            $ctext yview $prev
4934            return
4935        }
4936        set prev $loc
4937    }
4938    $ctext yview $prev
4939}
4940
4941proc nextfile {} {
4942    global difffilestart ctext
4943    set here [$ctext index @0,0]
4944    foreach loc $difffilestart {
4945        if {[$ctext compare $loc > $here]} {
4946            $ctext yview $loc
4947            return
4948        }
4949    }
4950}
4951
4952proc clear_ctext {{first 1.0}} {
4953    global ctext smarktop smarkbot
4954
4955    set l [lindex [split $first .] 0]
4956    if {![info exists smarktop] || [$ctext compare $first < $smarktop.0]} {
4957        set smarktop $l
4958    }
4959    if {![info exists smarkbot] || [$ctext compare $first < $smarkbot.0]} {
4960        set smarkbot $l
4961    }
4962    $ctext delete $first end
4963}
4964
4965proc incrsearch {name ix op} {
4966    global ctext searchstring searchdirn
4967
4968    $ctext tag remove found 1.0 end
4969    if {[catch {$ctext index anchor}]} {
4970        # no anchor set, use start of selection, or of visible area
4971        set sel [$ctext tag ranges sel]
4972        if {$sel ne {}} {
4973            $ctext mark set anchor [lindex $sel 0]
4974        } elseif {$searchdirn eq "-forwards"} {
4975            $ctext mark set anchor @0,0
4976        } else {
4977            $ctext mark set anchor @0,[winfo height $ctext]
4978        }
4979    }
4980    if {$searchstring ne {}} {
4981        set here [$ctext search $searchdirn -- $searchstring anchor]
4982        if {$here ne {}} {
4983            $ctext see $here
4984        }
4985        searchmarkvisible 1
4986    }
4987}
4988
4989proc dosearch {} {
4990    global sstring ctext searchstring searchdirn
4991
4992    focus $sstring
4993    $sstring icursor end
4994    set searchdirn -forwards
4995    if {$searchstring ne {}} {
4996        set sel [$ctext tag ranges sel]
4997        if {$sel ne {}} {
4998            set start "[lindex $sel 0] + 1c"
4999        } elseif {[catch {set start [$ctext index anchor]}]} {
5000            set start "@0,0"
5001        }
5002        set match [$ctext search -count mlen -- $searchstring $start]
5003        $ctext tag remove sel 1.0 end
5004        if {$match eq {}} {
5005            bell
5006            return
5007        }
5008        $ctext see $match
5009        set mend "$match + $mlen c"
5010        $ctext tag add sel $match $mend
5011        $ctext mark unset anchor
5012    }
5013}
5014
5015proc dosearchback {} {
5016    global sstring ctext searchstring searchdirn
5017
5018    focus $sstring
5019    $sstring icursor end
5020    set searchdirn -backwards
5021    if {$searchstring ne {}} {
5022        set sel [$ctext tag ranges sel]
5023        if {$sel ne {}} {
5024            set start [lindex $sel 0]
5025        } elseif {[catch {set start [$ctext index anchor]}]} {
5026            set start @0,[winfo height $ctext]
5027        }
5028        set match [$ctext search -backwards -count ml -- $searchstring $start]
5029        $ctext tag remove sel 1.0 end
5030        if {$match eq {}} {
5031            bell
5032            return
5033        }
5034        $ctext see $match
5035        set mend "$match + $ml c"
5036        $ctext tag add sel $match $mend
5037        $ctext mark unset anchor
5038    }
5039}
5040
5041proc searchmark {first last} {
5042    global ctext searchstring
5043
5044    set mend $first.0
5045    while {1} {
5046        set match [$ctext search -count mlen -- $searchstring $mend $last.end]
5047        if {$match eq {}} break
5048        set mend "$match + $mlen c"
5049        $ctext tag add found $match $mend
5050    }
5051}
5052
5053proc searchmarkvisible {doall} {
5054    global ctext smarktop smarkbot
5055
5056    set topline [lindex [split [$ctext index @0,0] .] 0]
5057    set botline [lindex [split [$ctext index @0,[winfo height $ctext]] .] 0]
5058    if {$doall || $botline < $smarktop || $topline > $smarkbot} {
5059        # no overlap with previous
5060        searchmark $topline $botline
5061        set smarktop $topline
5062        set smarkbot $botline
5063    } else {
5064        if {$topline < $smarktop} {
5065            searchmark $topline [expr {$smarktop-1}]
5066            set smarktop $topline
5067        }
5068        if {$botline > $smarkbot} {
5069            searchmark [expr {$smarkbot+1}] $botline
5070            set smarkbot $botline
5071        }
5072    }
5073}
5074
5075proc scrolltext {f0 f1} {
5076    global searchstring
5077
5078    .bleft.sb set $f0 $f1
5079    if {$searchstring ne {}} {
5080        searchmarkvisible 0
5081    }
5082}
5083
5084proc setcoords {} {
5085    global linespc charspc canvx0 canvy0 mainfont
5086    global xspc1 xspc2 lthickness
5087
5088    set linespc [font metrics $mainfont -linespace]
5089    set charspc [font measure $mainfont "m"]
5090    set canvy0 [expr {int(3 + 0.5 * $linespc)}]
5091    set canvx0 [expr {int(3 + 0.5 * $linespc)}]
5092    set lthickness [expr {int($linespc / 9) + 1}]
5093    set xspc1(0) $linespc
5094    set xspc2 $linespc
5095}
5096
5097proc redisplay {} {
5098    global canv
5099    global selectedline
5100
5101    set ymax [lindex [$canv cget -scrollregion] 3]
5102    if {$ymax eq {} || $ymax == 0} return
5103    set span [$canv yview]
5104    clear_display
5105    setcanvscroll
5106    allcanvs yview moveto [lindex $span 0]
5107    drawvisible
5108    if {[info exists selectedline]} {
5109        selectline $selectedline 0
5110        allcanvs yview moveto [lindex $span 0]
5111    }
5112}
5113
5114proc incrfont {inc} {
5115    global mainfont textfont ctext canv phase cflist
5116    global charspc tabstop
5117    global stopped entries
5118    unmarkmatches
5119    set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
5120    set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
5121    setcoords
5122    $ctext conf -font $textfont -tabs "[expr {$tabstop * $charspc}]"
5123    $cflist conf -font $textfont
5124    $ctext tag conf filesep -font [concat $textfont bold]
5125    foreach e $entries {
5126        $e conf -font $mainfont
5127    }
5128    if {$phase eq "getcommits"} {
5129        $canv itemconf textitems -font $mainfont
5130    }
5131    redisplay
5132}
5133
5134proc clearsha1 {} {
5135    global sha1entry sha1string
5136    if {[string length $sha1string] == 40} {
5137        $sha1entry delete 0 end
5138    }
5139}
5140
5141proc sha1change {n1 n2 op} {
5142    global sha1string currentid sha1but
5143    if {$sha1string == {}
5144        || ([info exists currentid] && $sha1string == $currentid)} {
5145        set state disabled
5146    } else {
5147        set state normal
5148    }
5149    if {[$sha1but cget -state] == $state} return
5150    if {$state == "normal"} {
5151        $sha1but conf -state normal -relief raised -text "Goto: "
5152    } else {
5153        $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
5154    }
5155}
5156
5157proc gotocommit {} {
5158    global sha1string currentid commitrow tagids headids
5159    global displayorder numcommits curview
5160
5161    if {$sha1string == {}
5162        || ([info exists currentid] && $sha1string == $currentid)} return
5163    if {[info exists tagids($sha1string)]} {
5164        set id $tagids($sha1string)
5165    } elseif {[info exists headids($sha1string)]} {
5166        set id $headids($sha1string)
5167    } else {
5168        set id [string tolower $sha1string]
5169        if {[regexp {^[0-9a-f]{4,39}$} $id]} {
5170            set matches {}
5171            foreach i $displayorder {
5172                if {[string match $id* $i]} {
5173                    lappend matches $i
5174                }
5175            }
5176            if {$matches ne {}} {
5177                if {[llength $matches] > 1} {
5178                    error_popup "Short SHA1 id $id is ambiguous"
5179                    return
5180                }
5181                set id [lindex $matches 0]
5182            }
5183        }
5184    }
5185    if {[info exists commitrow($curview,$id)]} {
5186        selectline $commitrow($curview,$id) 1
5187        return
5188    }
5189    if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
5190        set type "SHA1 id"
5191    } else {
5192        set type "Tag/Head"
5193    }
5194    error_popup "$type $sha1string is not known"
5195}
5196
5197proc lineenter {x y id} {
5198    global hoverx hovery hoverid hovertimer
5199    global commitinfo canv
5200
5201    if {![info exists commitinfo($id)] && ![getcommit $id]} return
5202    set hoverx $x
5203    set hovery $y
5204    set hoverid $id
5205    if {[info exists hovertimer]} {
5206        after cancel $hovertimer
5207    }
5208    set hovertimer [after 500 linehover]
5209    $canv delete hover
5210}
5211
5212proc linemotion {x y id} {
5213    global hoverx hovery hoverid hovertimer
5214
5215    if {[info exists hoverid] && $id == $hoverid} {
5216        set hoverx $x
5217        set hovery $y
5218        if {[info exists hovertimer]} {
5219            after cancel $hovertimer
5220        }
5221        set hovertimer [after 500 linehover]
5222    }
5223}
5224
5225proc lineleave {id} {
5226    global hoverid hovertimer canv
5227
5228    if {[info exists hoverid] && $id == $hoverid} {
5229        $canv delete hover
5230        if {[info exists hovertimer]} {
5231            after cancel $hovertimer
5232            unset hovertimer
5233        }
5234        unset hoverid
5235    }
5236}
5237
5238proc linehover {} {
5239    global hoverx hovery hoverid hovertimer
5240    global canv linespc lthickness
5241    global commitinfo mainfont
5242
5243    set text [lindex $commitinfo($hoverid) 0]
5244    set ymax [lindex [$canv cget -scrollregion] 3]
5245    if {$ymax == {}} return
5246    set yfrac [lindex [$canv yview] 0]
5247    set x [expr {$hoverx + 2 * $linespc}]
5248    set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
5249    set x0 [expr {$x - 2 * $lthickness}]
5250    set y0 [expr {$y - 2 * $lthickness}]
5251    set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}]
5252    set y1 [expr {$y + $linespc + 2 * $lthickness}]
5253    set t [$canv create rectangle $x0 $y0 $x1 $y1 \
5254               -fill \#ffff80 -outline black -width 1 -tags hover]
5255    $canv raise $t
5256    set t [$canv create text $x $y -anchor nw -text $text -tags hover \
5257               -font $mainfont]
5258    $canv raise $t
5259}
5260
5261proc clickisonarrow {id y} {
5262    global lthickness
5263
5264    set ranges [rowranges $id]
5265    set thresh [expr {2 * $lthickness + 6}]
5266    set n [expr {[llength $ranges] - 1}]
5267    for {set i 1} {$i < $n} {incr i} {
5268        set row [lindex $ranges $i]
5269        if {abs([yc $row] - $y) < $thresh} {
5270            return $i
5271        }
5272    }
5273    return {}
5274}
5275
5276proc arrowjump {id n y} {
5277    global canv
5278
5279    # 1 <-> 2, 3 <-> 4, etc...
5280    set n [expr {(($n - 1) ^ 1) + 1}]
5281    set row [lindex [rowranges $id] $n]
5282    set yt [yc $row]
5283    set ymax [lindex [$canv cget -scrollregion] 3]
5284    if {$ymax eq {} || $ymax <= 0} return
5285    set view [$canv yview]
5286    set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
5287    set yfrac [expr {$yt / $ymax - $yspan / 2}]
5288    if {$yfrac < 0} {
5289        set yfrac 0
5290    }
5291    allcanvs yview moveto $yfrac
5292}
5293
5294proc lineclick {x y id isnew} {
5295    global ctext commitinfo children canv thickerline curview
5296
5297    if {![info exists commitinfo($id)] && ![getcommit $id]} return
5298    unmarkmatches
5299    unselectline
5300    normalline
5301    $canv delete hover
5302    # draw this line thicker than normal
5303    set thickerline $id
5304    drawlines $id
5305    if {$isnew} {
5306        set ymax [lindex [$canv cget -scrollregion] 3]
5307        if {$ymax eq {}} return
5308        set yfrac [lindex [$canv yview] 0]
5309        set y [expr {$y + $yfrac * $ymax}]
5310    }
5311    set dirn [clickisonarrow $id $y]
5312    if {$dirn ne {}} {
5313        arrowjump $id $dirn $y
5314        return
5315    }
5316
5317    if {$isnew} {
5318        addtohistory [list lineclick $x $y $id 0]
5319    }
5320    # fill the details pane with info about this line
5321    $ctext conf -state normal
5322    clear_ctext
5323    $ctext tag conf link -foreground blue -underline 1
5324    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
5325    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
5326    $ctext insert end "Parent:\t"
5327    $ctext insert end $id [list link link0]
5328    $ctext tag bind link0 <1> [list selbyid $id]
5329    set info $commitinfo($id)
5330    $ctext insert end "\n\t[lindex $info 0]\n"
5331    $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
5332    set date [formatdate [lindex $info 2]]
5333    $ctext insert end "\tDate:\t$date\n"
5334    set kids $children($curview,$id)
5335    if {$kids ne {}} {
5336        $ctext insert end "\nChildren:"
5337        set i 0
5338        foreach child $kids {
5339            incr i
5340            if {![info exists commitinfo($child)] && ![getcommit $child]} continue
5341            set info $commitinfo($child)
5342            $ctext insert end "\n\t"
5343            $ctext insert end $child [list link link$i]
5344            $ctext tag bind link$i <1> [list selbyid $child]
5345            $ctext insert end "\n\t[lindex $info 0]"
5346            $ctext insert end "\n\tAuthor:\t[lindex $info 1]"
5347            set date [formatdate [lindex $info 2]]
5348            $ctext insert end "\n\tDate:\t$date\n"
5349        }
5350    }
5351    $ctext conf -state disabled
5352    init_flist {}
5353}
5354
5355proc normalline {} {
5356    global thickerline
5357    if {[info exists thickerline]} {
5358        set id $thickerline
5359        unset thickerline
5360        drawlines $id
5361    }
5362}
5363
5364proc selbyid {id} {
5365    global commitrow curview
5366    if {[info exists commitrow($curview,$id)]} {
5367        selectline $commitrow($curview,$id) 1
5368    }
5369}
5370
5371proc mstime {} {
5372    global startmstime
5373    if {![info exists startmstime]} {
5374        set startmstime [clock clicks -milliseconds]
5375    }
5376    return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
5377}
5378
5379proc rowmenu {x y id} {
5380    global rowctxmenu commitrow selectedline rowmenuid curview nullid
5381    global fakerowmenu
5382
5383    set rowmenuid $id
5384    if {![info exists selectedline]
5385        || $commitrow($curview,$id) eq $selectedline} {
5386        set state disabled
5387    } else {
5388        set state normal
5389    }
5390    if {$id ne $nullid} {
5391        set menu $rowctxmenu
5392    } else {
5393        set menu $fakerowmenu
5394    }
5395    $menu entryconfigure "Diff this*" -state $state
5396    $menu entryconfigure "Diff selected*" -state $state
5397    $menu entryconfigure "Make patch" -state $state
5398    tk_popup $menu $x $y
5399}
5400
5401proc diffvssel {dirn} {
5402    global rowmenuid selectedline displayorder
5403
5404    if {![info exists selectedline]} return
5405    if {$dirn} {
5406        set oldid [lindex $displayorder $selectedline]
5407        set newid $rowmenuid
5408    } else {
5409        set oldid $rowmenuid
5410        set newid [lindex $displayorder $selectedline]
5411    }
5412    addtohistory [list doseldiff $oldid $newid]
5413    doseldiff $oldid $newid
5414}
5415
5416proc doseldiff {oldid newid} {
5417    global ctext
5418    global commitinfo
5419
5420    $ctext conf -state normal
5421    clear_ctext
5422    init_flist "Top"
5423    $ctext insert end "From "
5424    $ctext tag conf link -foreground blue -underline 1
5425    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
5426    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
5427    $ctext tag bind link0 <1> [list selbyid $oldid]
5428    $ctext insert end $oldid [list link link0]
5429    $ctext insert end "\n     "
5430    $ctext insert end [lindex $commitinfo($oldid) 0]
5431    $ctext insert end "\n\nTo   "
5432    $ctext tag bind link1 <1> [list selbyid $newid]
5433    $ctext insert end $newid [list link link1]
5434    $ctext insert end "\n     "
5435    $ctext insert end [lindex $commitinfo($newid) 0]
5436    $ctext insert end "\n"
5437    $ctext conf -state disabled
5438    $ctext tag delete Comments
5439    $ctext tag remove found 1.0 end
5440    startdiff [list $oldid $newid]
5441}
5442
5443proc mkpatch {} {
5444    global rowmenuid currentid commitinfo patchtop patchnum
5445
5446    if {![info exists currentid]} return
5447    set oldid $currentid
5448    set oldhead [lindex $commitinfo($oldid) 0]
5449    set newid $rowmenuid
5450    set newhead [lindex $commitinfo($newid) 0]
5451    set top .patch
5452    set patchtop $top
5453    catch {destroy $top}
5454    toplevel $top
5455    label $top.title -text "Generate patch"
5456    grid $top.title - -pady 10
5457    label $top.from -text "From:"
5458    entry $top.fromsha1 -width 40 -relief flat
5459    $top.fromsha1 insert 0 $oldid
5460    $top.fromsha1 conf -state readonly
5461    grid $top.from $top.fromsha1 -sticky w
5462    entry $top.fromhead -width 60 -relief flat
5463    $top.fromhead insert 0 $oldhead
5464    $top.fromhead conf -state readonly
5465    grid x $top.fromhead -sticky w
5466    label $top.to -text "To:"
5467    entry $top.tosha1 -width 40 -relief flat
5468    $top.tosha1 insert 0 $newid
5469    $top.tosha1 conf -state readonly
5470    grid $top.to $top.tosha1 -sticky w
5471    entry $top.tohead -width 60 -relief flat
5472    $top.tohead insert 0 $newhead
5473    $top.tohead conf -state readonly
5474    grid x $top.tohead -sticky w
5475    button $top.rev -text "Reverse" -command mkpatchrev -padx 5
5476    grid $top.rev x -pady 10
5477    label $top.flab -text "Output file:"
5478    entry $top.fname -width 60
5479    $top.fname insert 0 [file normalize "patch$patchnum.patch"]
5480    incr patchnum
5481    grid $top.flab $top.fname -sticky w
5482    frame $top.buts
5483    button $top.buts.gen -text "Generate" -command mkpatchgo
5484    button $top.buts.can -text "Cancel" -command mkpatchcan
5485    grid $top.buts.gen $top.buts.can
5486    grid columnconfigure $top.buts 0 -weight 1 -uniform a
5487    grid columnconfigure $top.buts 1 -weight 1 -uniform a
5488    grid $top.buts - -pady 10 -sticky ew
5489    focus $top.fname
5490}
5491
5492proc mkpatchrev {} {
5493    global patchtop
5494
5495    set oldid [$patchtop.fromsha1 get]
5496    set oldhead [$patchtop.fromhead get]
5497    set newid [$patchtop.tosha1 get]
5498    set newhead [$patchtop.tohead get]
5499    foreach e [list fromsha1 fromhead tosha1 tohead] \
5500            v [list $newid $newhead $oldid $oldhead] {
5501        $patchtop.$e conf -state normal
5502        $patchtop.$e delete 0 end
5503        $patchtop.$e insert 0 $v
5504        $patchtop.$e conf -state readonly
5505    }
5506}
5507
5508proc mkpatchgo {} {
5509    global patchtop nullid
5510
5511    set oldid [$patchtop.fromsha1 get]
5512    set newid [$patchtop.tosha1 get]
5513    set fname [$patchtop.fname get]
5514    if {$newid eq $nullid} {
5515        set cmd [list git diff-index -p $oldid]
5516    } elseif {$oldid eq $nullid} {
5517        set cmd [list git diff-index -p -R $newid]
5518    } else {
5519        set cmd [list git diff-tree -p $oldid $newid]
5520    }
5521    lappend cmd >$fname &
5522    if {[catch {eval exec $cmd} err]} {
5523        error_popup "Error creating patch: $err"
5524    }
5525    catch {destroy $patchtop}
5526    unset patchtop
5527}
5528
5529proc mkpatchcan {} {
5530    global patchtop
5531
5532    catch {destroy $patchtop}
5533    unset patchtop
5534}
5535
5536proc mktag {} {
5537    global rowmenuid mktagtop commitinfo
5538
5539    set top .maketag
5540    set mktagtop $top
5541    catch {destroy $top}
5542    toplevel $top
5543    label $top.title -text "Create tag"
5544    grid $top.title - -pady 10
5545    label $top.id -text "ID:"
5546    entry $top.sha1 -width 40 -relief flat
5547    $top.sha1 insert 0 $rowmenuid
5548    $top.sha1 conf -state readonly
5549    grid $top.id $top.sha1 -sticky w
5550    entry $top.head -width 60 -relief flat
5551    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
5552    $top.head conf -state readonly
5553    grid x $top.head -sticky w
5554    label $top.tlab -text "Tag name:"
5555    entry $top.tag -width 60
5556    grid $top.tlab $top.tag -sticky w
5557    frame $top.buts
5558    button $top.buts.gen -text "Create" -command mktaggo
5559    button $top.buts.can -text "Cancel" -command mktagcan
5560    grid $top.buts.gen $top.buts.can
5561    grid columnconfigure $top.buts 0 -weight 1 -uniform a
5562    grid columnconfigure $top.buts 1 -weight 1 -uniform a
5563    grid $top.buts - -pady 10 -sticky ew
5564    focus $top.tag
5565}
5566
5567proc domktag {} {
5568    global mktagtop env tagids idtags
5569
5570    set id [$mktagtop.sha1 get]
5571    set tag [$mktagtop.tag get]
5572    if {$tag == {}} {
5573        error_popup "No tag name specified"
5574        return
5575    }
5576    if {[info exists tagids($tag)]} {
5577        error_popup "Tag \"$tag\" already exists"
5578        return
5579    }
5580    if {[catch {
5581        set dir [gitdir]
5582        set fname [file join $dir "refs/tags" $tag]
5583        set f [open $fname w]
5584        puts $f $id
5585        close $f
5586    } err]} {
5587        error_popup "Error creating tag: $err"
5588        return
5589    }
5590
5591    set tagids($tag) $id
5592    lappend idtags($id) $tag
5593    redrawtags $id
5594    addedtag $id
5595}
5596
5597proc redrawtags {id} {
5598    global canv linehtag commitrow idpos selectedline curview
5599    global mainfont canvxmax iddrawn
5600
5601    if {![info exists commitrow($curview,$id)]} return
5602    if {![info exists iddrawn($id)]} return
5603    drawcommits $commitrow($curview,$id)
5604    $canv delete tag.$id
5605    set xt [eval drawtags $id $idpos($id)]
5606    $canv coords $linehtag($commitrow($curview,$id)) $xt [lindex $idpos($id) 2]
5607    set text [$canv itemcget $linehtag($commitrow($curview,$id)) -text]
5608    set xr [expr {$xt + [font measure $mainfont $text]}]
5609    if {$xr > $canvxmax} {
5610        set canvxmax $xr
5611        setcanvscroll
5612    }
5613    if {[info exists selectedline]
5614        && $selectedline == $commitrow($curview,$id)} {
5615        selectline $selectedline 0
5616    }
5617}
5618
5619proc mktagcan {} {
5620    global mktagtop
5621
5622    catch {destroy $mktagtop}
5623    unset mktagtop
5624}
5625
5626proc mktaggo {} {
5627    domktag
5628    mktagcan
5629}
5630
5631proc writecommit {} {
5632    global rowmenuid wrcomtop commitinfo wrcomcmd
5633
5634    set top .writecommit
5635    set wrcomtop $top
5636    catch {destroy $top}
5637    toplevel $top
5638    label $top.title -text "Write commit to file"
5639    grid $top.title - -pady 10
5640    label $top.id -text "ID:"
5641    entry $top.sha1 -width 40 -relief flat
5642    $top.sha1 insert 0 $rowmenuid
5643    $top.sha1 conf -state readonly
5644    grid $top.id $top.sha1 -sticky w
5645    entry $top.head -width 60 -relief flat
5646    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
5647    $top.head conf -state readonly
5648    grid x $top.head -sticky w
5649    label $top.clab -text "Command:"
5650    entry $top.cmd -width 60 -textvariable wrcomcmd
5651    grid $top.clab $top.cmd -sticky w -pady 10
5652    label $top.flab -text "Output file:"
5653    entry $top.fname -width 60
5654    $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
5655    grid $top.flab $top.fname -sticky w
5656    frame $top.buts
5657    button $top.buts.gen -text "Write" -command wrcomgo
5658    button $top.buts.can -text "Cancel" -command wrcomcan
5659    grid $top.buts.gen $top.buts.can
5660    grid columnconfigure $top.buts 0 -weight 1 -uniform a
5661    grid columnconfigure $top.buts 1 -weight 1 -uniform a
5662    grid $top.buts - -pady 10 -sticky ew
5663    focus $top.fname
5664}
5665
5666proc wrcomgo {} {
5667    global wrcomtop
5668
5669    set id [$wrcomtop.sha1 get]
5670    set cmd "echo $id | [$wrcomtop.cmd get]"
5671    set fname [$wrcomtop.fname get]
5672    if {[catch {exec sh -c $cmd >$fname &} err]} {
5673        error_popup "Error writing commit: $err"
5674    }
5675    catch {destroy $wrcomtop}
5676    unset wrcomtop
5677}
5678
5679proc wrcomcan {} {
5680    global wrcomtop
5681
5682    catch {destroy $wrcomtop}
5683    unset wrcomtop
5684}
5685
5686proc mkbranch {} {
5687    global rowmenuid mkbrtop
5688
5689    set top .makebranch
5690    catch {destroy $top}
5691    toplevel $top
5692    label $top.title -text "Create new branch"
5693    grid $top.title - -pady 10
5694    label $top.id -text "ID:"
5695    entry $top.sha1 -width 40 -relief flat
5696    $top.sha1 insert 0 $rowmenuid
5697    $top.sha1 conf -state readonly
5698    grid $top.id $top.sha1 -sticky w
5699    label $top.nlab -text "Name:"
5700    entry $top.name -width 40
5701    grid $top.nlab $top.name -sticky w
5702    frame $top.buts
5703    button $top.buts.go -text "Create" -command [list mkbrgo $top]
5704    button $top.buts.can -text "Cancel" -command "catch {destroy $top}"
5705    grid $top.buts.go $top.buts.can
5706    grid columnconfigure $top.buts 0 -weight 1 -uniform a
5707    grid columnconfigure $top.buts 1 -weight 1 -uniform a
5708    grid $top.buts - -pady 10 -sticky ew
5709    focus $top.name
5710}
5711
5712proc mkbrgo {top} {
5713    global headids idheads
5714
5715    set name [$top.name get]
5716    set id [$top.sha1 get]
5717    if {$name eq {}} {
5718        error_popup "Please specify a name for the new branch"
5719        return
5720    }
5721    catch {destroy $top}
5722    nowbusy newbranch
5723    update
5724    if {[catch {
5725        exec git branch $name $id
5726    } err]} {
5727        notbusy newbranch
5728        error_popup $err
5729    } else {
5730        set headids($name) $id
5731        lappend idheads($id) $name
5732        addedhead $id $name
5733        notbusy newbranch
5734        redrawtags $id
5735        dispneartags 0
5736    }
5737}
5738
5739proc cherrypick {} {
5740    global rowmenuid curview commitrow
5741    global mainhead
5742
5743    set oldhead [exec git rev-parse HEAD]
5744    set dheads [descheads $rowmenuid]
5745    if {$dheads ne {} && [lsearch -exact $dheads $oldhead] >= 0} {
5746        set ok [confirm_popup "Commit [string range $rowmenuid 0 7] is already\
5747                        included in branch $mainhead -- really re-apply it?"]
5748        if {!$ok} return
5749    }
5750    nowbusy cherrypick
5751    update
5752    # Unfortunately git-cherry-pick writes stuff to stderr even when
5753    # no error occurs, and exec takes that as an indication of error...
5754    if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} {
5755        notbusy cherrypick
5756        error_popup $err
5757        return
5758    }
5759    set newhead [exec git rev-parse HEAD]
5760    if {$newhead eq $oldhead} {
5761        notbusy cherrypick
5762        error_popup "No changes committed"
5763        return
5764    }
5765    addnewchild $newhead $oldhead
5766    if {[info exists commitrow($curview,$oldhead)]} {
5767        insertrow $commitrow($curview,$oldhead) $newhead
5768        if {$mainhead ne {}} {
5769            movehead $newhead $mainhead
5770            movedhead $newhead $mainhead
5771        }
5772        redrawtags $oldhead
5773        redrawtags $newhead
5774    }
5775    notbusy cherrypick
5776}
5777
5778# context menu for a head
5779proc headmenu {x y id head} {
5780    global headmenuid headmenuhead headctxmenu mainhead
5781
5782    set headmenuid $id
5783    set headmenuhead $head
5784    set state normal
5785    if {$head eq $mainhead} {
5786        set state disabled
5787    }
5788    $headctxmenu entryconfigure 0 -state $state
5789    $headctxmenu entryconfigure 1 -state $state
5790    tk_popup $headctxmenu $x $y
5791}
5792
5793proc cobranch {} {
5794    global headmenuid headmenuhead mainhead headids
5795    global showlocalchanges mainheadid
5796
5797    # check the tree is clean first??
5798    set oldmainhead $mainhead
5799    nowbusy checkout
5800    update
5801    dohidelocalchanges
5802    if {[catch {
5803        exec git checkout -q $headmenuhead
5804    } err]} {
5805        notbusy checkout
5806        error_popup $err
5807    } else {
5808        notbusy checkout
5809        set mainhead $headmenuhead
5810        set mainheadid $headmenuid
5811        if {[info exists headids($oldmainhead)]} {
5812            redrawtags $headids($oldmainhead)
5813        }
5814        redrawtags $headmenuid
5815        if {$showlocalchanges} {
5816            dodiffindex
5817        }
5818    }
5819}
5820
5821proc rmbranch {} {
5822    global headmenuid headmenuhead mainhead
5823    global headids idheads
5824
5825    set head $headmenuhead
5826    set id $headmenuid
5827    # this check shouldn't be needed any more...
5828    if {$head eq $mainhead} {
5829        error_popup "Cannot delete the currently checked-out branch"
5830        return
5831    }
5832    set dheads [descheads $id]
5833    if {$dheads eq $headids($head)} {
5834        # the stuff on this branch isn't on any other branch
5835        if {![confirm_popup "The commits on branch $head aren't on any other\
5836                        branch.\nReally delete branch $head?"]} return
5837    }
5838    nowbusy rmbranch
5839    update
5840    if {[catch {exec git branch -D $head} err]} {
5841        notbusy rmbranch
5842        error_popup $err
5843        return
5844    }
5845    removehead $id $head
5846    removedhead $id $head
5847    redrawtags $id
5848    notbusy rmbranch
5849    dispneartags 0
5850}
5851
5852# Stuff for finding nearby tags
5853proc getallcommits {} {
5854    global allcommits allids nbmp nextarc seeds
5855
5856    set allids {}
5857    set nbmp 0
5858    set nextarc 0
5859    set allcommits 0
5860    set seeds {}
5861    regetallcommits
5862}
5863
5864# Called when the graph might have changed
5865proc regetallcommits {} {
5866    global allcommits seeds
5867
5868    set cmd [concat | git rev-list --all --parents]
5869    foreach id $seeds {
5870        lappend cmd "^$id"
5871    }
5872    set fd [open $cmd r]
5873    fconfigure $fd -blocking 0
5874    incr allcommits
5875    nowbusy allcommits
5876    filerun $fd [list getallclines $fd]
5877}
5878
5879# Since most commits have 1 parent and 1 child, we group strings of
5880# such commits into "arcs" joining branch/merge points (BMPs), which
5881# are commits that either don't have 1 parent or don't have 1 child.
5882#
5883# arcnos(id) - incoming arcs for BMP, arc we're on for other nodes
5884# arcout(id) - outgoing arcs for BMP
5885# arcids(a) - list of IDs on arc including end but not start
5886# arcstart(a) - BMP ID at start of arc
5887# arcend(a) - BMP ID at end of arc
5888# growing(a) - arc a is still growing
5889# arctags(a) - IDs out of arcids (excluding end) that have tags
5890# archeads(a) - IDs out of arcids (excluding end) that have heads
5891# The start of an arc is at the descendent end, so "incoming" means
5892# coming from descendents, and "outgoing" means going towards ancestors.
5893
5894proc getallclines {fd} {
5895    global allids allparents allchildren idtags nextarc nbmp
5896    global arcnos arcids arctags arcout arcend arcstart archeads growing
5897    global seeds allcommits
5898
5899    set nid 0
5900    while {[incr nid] <= 1000 && [gets $fd line] >= 0} {
5901        set id [lindex $line 0]
5902        if {[info exists allparents($id)]} {
5903            # seen it already
5904            continue
5905        }
5906        lappend allids $id
5907        set olds [lrange $line 1 end]
5908        set allparents($id) $olds
5909        if {![info exists allchildren($id)]} {
5910            set allchildren($id) {}
5911            set arcnos($id) {}
5912            lappend seeds $id
5913        } else {
5914            set a $arcnos($id)
5915            if {[llength $olds] == 1 && [llength $a] == 1} {
5916                lappend arcids($a) $id
5917                if {[info exists idtags($id)]} {
5918                    lappend arctags($a) $id
5919                }
5920                if {[info exists idheads($id)]} {
5921                    lappend archeads($a) $id
5922                }
5923                if {[info exists allparents($olds)]} {
5924                    # seen parent already
5925                    if {![info exists arcout($olds)]} {
5926                        splitarc $olds
5927                    }
5928                    lappend arcids($a) $olds
5929                    set arcend($a) $olds
5930                    unset growing($a)
5931                }
5932                lappend allchildren($olds) $id
5933                lappend arcnos($olds) $a
5934                continue
5935            }
5936        }
5937        incr nbmp
5938        foreach a $arcnos($id) {
5939            lappend arcids($a) $id
5940            set arcend($a) $id
5941            unset growing($a)
5942        }
5943
5944        set ao {}
5945        foreach p $olds {
5946            lappend allchildren($p) $id
5947            set a [incr nextarc]
5948            set arcstart($a) $id
5949            set archeads($a) {}
5950            set arctags($a) {}
5951            set archeads($a) {}
5952            set arcids($a) {}
5953            lappend ao $a
5954            set growing($a) 1
5955            if {[info exists allparents($p)]} {
5956                # seen it already, may need to make a new branch
5957                if {![info exists arcout($p)]} {
5958                    splitarc $p
5959                }
5960                lappend arcids($a) $p
5961                set arcend($a) $p
5962                unset growing($a)
5963            }
5964            lappend arcnos($p) $a
5965        }
5966        set arcout($id) $ao
5967    }
5968    if {![eof $fd]} {
5969        return [expr {$nid >= 1000? 2: 1}]
5970    }
5971    close $fd
5972    if {[incr allcommits -1] == 0} {
5973        notbusy allcommits
5974    }
5975    dispneartags 0
5976    return 0
5977}
5978
5979proc recalcarc {a} {
5980    global arctags archeads arcids idtags idheads
5981
5982    set at {}
5983    set ah {}
5984    foreach id [lrange $arcids($a) 0 end-1] {
5985        if {[info exists idtags($id)]} {
5986            lappend at $id
5987        }
5988        if {[info exists idheads($id)]} {
5989            lappend ah $id
5990        }
5991    }
5992    set arctags($a) $at
5993    set archeads($a) $ah
5994}
5995
5996proc splitarc {p} {
5997    global arcnos arcids nextarc nbmp arctags archeads idtags idheads
5998    global arcstart arcend arcout allparents growing
5999
6000    set a $arcnos($p)
6001    if {[llength $a] != 1} {
6002        puts "oops splitarc called but [llength $a] arcs already"
6003        return
6004    }
6005    set a [lindex $a 0]
6006    set i [lsearch -exact $arcids($a) $p]
6007    if {$i < 0} {
6008        puts "oops splitarc $p not in arc $a"
6009        return
6010    }
6011    set na [incr nextarc]
6012    if {[info exists arcend($a)]} {
6013        set arcend($na) $arcend($a)
6014    } else {
6015        set l [lindex $allparents([lindex $arcids($a) end]) 0]
6016        set j [lsearch -exact $arcnos($l) $a]
6017        set arcnos($l) [lreplace $arcnos($l) $j $j $na]
6018    }
6019    set tail [lrange $arcids($a) [expr {$i+1}] end]
6020    set arcids($a) [lrange $arcids($a) 0 $i]
6021    set arcend($a) $p
6022    set arcstart($na) $p
6023    set arcout($p) $na
6024    set arcids($na) $tail
6025    if {[info exists growing($a)]} {
6026        set growing($na) 1
6027        unset growing($a)
6028    }
6029    incr nbmp
6030
6031    foreach id $tail {
6032        if {[llength $arcnos($id)] == 1} {
6033            set arcnos($id) $na
6034        } else {
6035            set j [lsearch -exact $arcnos($id) $a]
6036            set arcnos($id) [lreplace $arcnos($id) $j $j $na]
6037        }
6038    }
6039
6040    # reconstruct tags and heads lists
6041    if {$arctags($a) ne {} || $archeads($a) ne {}} {
6042        recalcarc $a
6043        recalcarc $na
6044    } else {
6045        set arctags($na) {}
6046        set archeads($na) {}
6047    }
6048}
6049
6050# Update things for a new commit added that is a child of one
6051# existing commit.  Used when cherry-picking.
6052proc addnewchild {id p} {
6053    global allids allparents allchildren idtags nextarc nbmp
6054    global arcnos arcids arctags arcout arcend arcstart archeads growing
6055    global seeds
6056
6057    lappend allids $id
6058    set allparents($id) [list $p]
6059    set allchildren($id) {}
6060    set arcnos($id) {}
6061    lappend seeds $id
6062    incr nbmp
6063    lappend allchildren($p) $id
6064    set a [incr nextarc]
6065    set arcstart($a) $id
6066    set archeads($a) {}
6067    set arctags($a) {}
6068    set arcids($a) [list $p]
6069    set arcend($a) $p
6070    if {![info exists arcout($p)]} {
6071        splitarc $p
6072    }
6073    lappend arcnos($p) $a
6074    set arcout($id) [list $a]
6075}
6076
6077# Returns 1 if a is an ancestor of b, -1 if b is an ancestor of a,
6078# or 0 if neither is true.
6079proc anc_or_desc {a b} {
6080    global arcout arcstart arcend arcnos cached_isanc
6081
6082    if {$arcnos($a) eq $arcnos($b)} {
6083        # Both are on the same arc(s); either both are the same BMP,
6084        # or if one is not a BMP, the other is also not a BMP or is
6085        # the BMP at end of the arc (and it only has 1 incoming arc).
6086        if {$a eq $b} {
6087            return 0
6088        }
6089        # assert {[llength $arcnos($a)] == 1}
6090        set arc [lindex $arcnos($a) 0]
6091        set i [lsearch -exact $arcids($arc) $a]
6092        set j [lsearch -exact $arcids($arc) $b]
6093        if {$i < 0 || $i > $j} {
6094            return 1
6095        } else {
6096            return -1
6097        }
6098    }
6099
6100    if {![info exists arcout($a)]} {
6101        set arc [lindex $arcnos($a) 0]
6102        if {[info exists arcend($arc)]} {
6103            set aend $arcend($arc)
6104        } else {
6105            set aend {}
6106        }
6107        set a $arcstart($arc)
6108    } else {
6109        set aend $a
6110    }
6111    if {![info exists arcout($b)]} {
6112        set arc [lindex $arcnos($b) 0]
6113        if {[info exists arcend($arc)]} {
6114            set bend $arcend($arc)
6115        } else {
6116            set bend {}
6117        }
6118        set b $arcstart($arc)
6119    } else {
6120        set bend $b
6121    }
6122    if {$a eq $bend} {
6123        return 1
6124    }
6125    if {$b eq $aend} {
6126        return -1
6127    }
6128    if {[info exists cached_isanc($a,$bend)]} {
6129        if {$cached_isanc($a,$bend)} {
6130            return 1
6131        }
6132    }
6133    if {[info exists cached_isanc($b,$aend)]} {
6134        if {$cached_isanc($b,$aend)} {
6135            return -1
6136        }
6137        if {[info exists cached_isanc($a,$bend)]} {
6138            return 0
6139        }
6140    }
6141
6142    set todo [list $a $b]
6143    set anc($a) a
6144    set anc($b) b
6145    for {set i 0} {$i < [llength $todo]} {incr i} {
6146        set x [lindex $todo $i]
6147        if {$anc($x) eq {}} {
6148            continue
6149        }
6150        foreach arc $arcnos($x) {
6151            set xd $arcstart($arc)
6152            if {$xd eq $bend} {
6153                set cached_isanc($a,$bend) 1
6154                set cached_isanc($b,$aend) 0
6155                return 1
6156            } elseif {$xd eq $aend} {
6157                set cached_isanc($b,$aend) 1
6158                set cached_isanc($a,$bend) 0
6159                return -1
6160            }
6161            if {![info exists anc($xd)]} {
6162                set anc($xd) $anc($x)
6163                lappend todo $xd
6164            } elseif {$anc($xd) ne $anc($x)} {
6165                set anc($xd) {}
6166            }
6167        }
6168    }
6169    set cached_isanc($a,$bend) 0
6170    set cached_isanc($b,$aend) 0
6171    return 0
6172}
6173
6174# This identifies whether $desc has an ancestor that is
6175# a growing tip of the graph and which is not an ancestor of $anc
6176# and returns 0 if so and 1 if not.
6177# If we subsequently discover a tag on such a growing tip, and that
6178# turns out to be a descendent of $anc (which it could, since we
6179# don't necessarily see children before parents), then $desc
6180# isn't a good choice to display as a descendent tag of
6181# $anc (since it is the descendent of another tag which is
6182# a descendent of $anc).  Similarly, $anc isn't a good choice to
6183# display as a ancestor tag of $desc.
6184#
6185proc is_certain {desc anc} {
6186    global arcnos arcout arcstart arcend growing problems
6187
6188    set certain {}
6189    if {[llength $arcnos($anc)] == 1} {
6190        # tags on the same arc are certain
6191        if {$arcnos($desc) eq $arcnos($anc)} {
6192            return 1
6193        }
6194        if {![info exists arcout($anc)]} {
6195            # if $anc is partway along an arc, use the start of the arc instead
6196            set a [lindex $arcnos($anc) 0]
6197            set anc $arcstart($a)
6198        }
6199    }
6200    if {[llength $arcnos($desc)] > 1 || [info exists arcout($desc)]} {
6201        set x $desc
6202    } else {
6203        set a [lindex $arcnos($desc) 0]
6204        set x $arcend($a)
6205    }
6206    if {$x == $anc} {
6207        return 1
6208    }
6209    set anclist [list $x]
6210    set dl($x) 1
6211    set nnh 1
6212    set ngrowanc 0
6213    for {set i 0} {$i < [llength $anclist] && ($nnh > 0 || $ngrowanc > 0)} {incr i} {
6214        set x [lindex $anclist $i]
6215        if {$dl($x)} {
6216            incr nnh -1
6217        }
6218        set done($x) 1
6219        foreach a $arcout($x) {
6220            if {[info exists growing($a)]} {
6221                if {![info exists growanc($x)] && $dl($x)} {
6222                    set growanc($x) 1
6223                    incr ngrowanc
6224                }
6225            } else {
6226                set y $arcend($a)
6227                if {[info exists dl($y)]} {
6228                    if {$dl($y)} {
6229                        if {!$dl($x)} {
6230                            set dl($y) 0
6231                            if {![info exists done($y)]} {
6232                                incr nnh -1
6233                            }
6234                            if {[info exists growanc($x)]} {
6235                                incr ngrowanc -1
6236                            }
6237                            set xl [list $y]
6238                            for {set k 0} {$k < [llength $xl]} {incr k} {
6239                                set z [lindex $xl $k]
6240                                foreach c $arcout($z) {
6241                                    if {[info exists arcend($c)]} {
6242                                        set v $arcend($c)
6243                                        if {[info exists dl($v)] && $dl($v)} {
6244                                            set dl($v) 0
6245                                            if {![info exists done($v)]} {
6246                                                incr nnh -1
6247                                            }
6248                                            if {[info exists growanc($v)]} {
6249                                                incr ngrowanc -1
6250                                            }
6251                                            lappend xl $v
6252                                        }
6253                                    }
6254                                }
6255                            }
6256                        }
6257                    }
6258                } elseif {$y eq $anc || !$dl($x)} {
6259                    set dl($y) 0
6260                    lappend anclist $y
6261                } else {
6262                    set dl($y) 1
6263                    lappend anclist $y
6264                    incr nnh
6265                }
6266            }
6267        }
6268    }
6269    foreach x [array names growanc] {
6270        if {$dl($x)} {
6271            return 0
6272        }
6273        return 0
6274    }
6275    return 1
6276}
6277
6278proc validate_arctags {a} {
6279    global arctags idtags
6280
6281    set i -1
6282    set na $arctags($a)
6283    foreach id $arctags($a) {
6284        incr i
6285        if {![info exists idtags($id)]} {
6286            set na [lreplace $na $i $i]
6287            incr i -1
6288        }
6289    }
6290    set arctags($a) $na
6291}
6292
6293proc validate_archeads {a} {
6294    global archeads idheads
6295
6296    set i -1
6297    set na $archeads($a)
6298    foreach id $archeads($a) {
6299        incr i
6300        if {![info exists idheads($id)]} {
6301            set na [lreplace $na $i $i]
6302            incr i -1
6303        }
6304    }
6305    set archeads($a) $na
6306}
6307
6308# Return the list of IDs that have tags that are descendents of id,
6309# ignoring IDs that are descendents of IDs already reported.
6310proc desctags {id} {
6311    global arcnos arcstart arcids arctags idtags allparents
6312    global growing cached_dtags
6313
6314    if {![info exists allparents($id)]} {
6315        return {}
6316    }
6317    set t1 [clock clicks -milliseconds]
6318    set argid $id
6319    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
6320        # part-way along an arc; check that arc first
6321        set a [lindex $arcnos($id) 0]
6322        if {$arctags($a) ne {}} {
6323            validate_arctags $a
6324            set i [lsearch -exact $arcids($a) $id]
6325            set tid {}
6326            foreach t $arctags($a) {
6327                set j [lsearch -exact $arcids($a) $t]
6328                if {$j >= $i} break
6329                set tid $t
6330            }
6331            if {$tid ne {}} {
6332                return $tid
6333            }
6334        }
6335        set id $arcstart($a)
6336        if {[info exists idtags($id)]} {
6337            return $id
6338        }
6339    }
6340    if {[info exists cached_dtags($id)]} {
6341        return $cached_dtags($id)
6342    }
6343
6344    set origid $id
6345    set todo [list $id]
6346    set queued($id) 1
6347    set nc 1
6348    for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
6349        set id [lindex $todo $i]
6350        set done($id) 1
6351        set ta [info exists hastaggedancestor($id)]
6352        if {!$ta} {
6353            incr nc -1
6354        }
6355        # ignore tags on starting node
6356        if {!$ta && $i > 0} {
6357            if {[info exists idtags($id)]} {
6358                set tagloc($id) $id
6359                set ta 1
6360            } elseif {[info exists cached_dtags($id)]} {
6361                set tagloc($id) $cached_dtags($id)
6362                set ta 1
6363            }
6364        }
6365        foreach a $arcnos($id) {
6366            set d $arcstart($a)
6367            if {!$ta && $arctags($a) ne {}} {
6368                validate_arctags $a
6369                if {$arctags($a) ne {}} {
6370                    lappend tagloc($id) [lindex $arctags($a) end]
6371                }
6372            }
6373            if {$ta || $arctags($a) ne {}} {
6374                set tomark [list $d]
6375                for {set j 0} {$j < [llength $tomark]} {incr j} {
6376                    set dd [lindex $tomark $j]
6377                    if {![info exists hastaggedancestor($dd)]} {
6378                        if {[info exists done($dd)]} {
6379                            foreach b $arcnos($dd) {
6380                                lappend tomark $arcstart($b)
6381                            }
6382                            if {[info exists tagloc($dd)]} {
6383                                unset tagloc($dd)
6384                            }
6385                        } elseif {[info exists queued($dd)]} {
6386                            incr nc -1
6387                        }
6388                        set hastaggedancestor($dd) 1
6389                    }
6390                }
6391            }
6392            if {![info exists queued($d)]} {
6393                lappend todo $d
6394                set queued($d) 1
6395                if {![info exists hastaggedancestor($d)]} {
6396                    incr nc
6397                }
6398            }
6399        }
6400    }
6401    set tags {}
6402    foreach id [array names tagloc] {
6403        if {![info exists hastaggedancestor($id)]} {
6404            foreach t $tagloc($id) {
6405                if {[lsearch -exact $tags $t] < 0} {
6406                    lappend tags $t
6407                }
6408            }
6409        }
6410    }
6411    set t2 [clock clicks -milliseconds]
6412    set loopix $i
6413
6414    # remove tags that are descendents of other tags
6415    for {set i 0} {$i < [llength $tags]} {incr i} {
6416        set a [lindex $tags $i]
6417        for {set j 0} {$j < $i} {incr j} {
6418            set b [lindex $tags $j]
6419            set r [anc_or_desc $a $b]
6420            if {$r == 1} {
6421                set tags [lreplace $tags $j $j]
6422                incr j -1
6423                incr i -1
6424            } elseif {$r == -1} {
6425                set tags [lreplace $tags $i $i]
6426                incr i -1
6427                break
6428            }
6429        }
6430    }
6431
6432    if {[array names growing] ne {}} {
6433        # graph isn't finished, need to check if any tag could get
6434        # eclipsed by another tag coming later.  Simply ignore any
6435        # tags that could later get eclipsed.
6436        set ctags {}
6437        foreach t $tags {
6438            if {[is_certain $t $origid]} {
6439                lappend ctags $t
6440            }
6441        }
6442        if {$tags eq $ctags} {
6443            set cached_dtags($origid) $tags
6444        } else {
6445            set tags $ctags
6446        }
6447    } else {
6448        set cached_dtags($origid) $tags
6449    }
6450    set t3 [clock clicks -milliseconds]
6451    if {0 && $t3 - $t1 >= 100} {
6452        puts "iterating descendents ($loopix/[llength $todo] nodes) took\
6453            [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
6454    }
6455    return $tags
6456}
6457
6458proc anctags {id} {
6459    global arcnos arcids arcout arcend arctags idtags allparents
6460    global growing cached_atags
6461
6462    if {![info exists allparents($id)]} {
6463        return {}
6464    }
6465    set t1 [clock clicks -milliseconds]
6466    set argid $id
6467    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
6468        # part-way along an arc; check that arc first
6469        set a [lindex $arcnos($id) 0]
6470        if {$arctags($a) ne {}} {
6471            validate_arctags $a
6472            set i [lsearch -exact $arcids($a) $id]
6473            foreach t $arctags($a) {
6474                set j [lsearch -exact $arcids($a) $t]
6475                if {$j > $i} {
6476                    return $t
6477                }
6478            }
6479        }
6480        if {![info exists arcend($a)]} {
6481            return {}
6482        }
6483        set id $arcend($a)
6484        if {[info exists idtags($id)]} {
6485            return $id
6486        }
6487    }
6488    if {[info exists cached_atags($id)]} {
6489        return $cached_atags($id)
6490    }
6491
6492    set origid $id
6493    set todo [list $id]
6494    set queued($id) 1
6495    set taglist {}
6496    set nc 1
6497    for {set i 0} {$i < [llength $todo] && $nc > 0} {incr i} {
6498        set id [lindex $todo $i]
6499        set done($id) 1
6500        set td [info exists hastaggeddescendent($id)]
6501        if {!$td} {
6502            incr nc -1
6503        }
6504        # ignore tags on starting node
6505        if {!$td && $i > 0} {
6506            if {[info exists idtags($id)]} {
6507                set tagloc($id) $id
6508                set td 1
6509            } elseif {[info exists cached_atags($id)]} {
6510                set tagloc($id) $cached_atags($id)
6511                set td 1
6512            }
6513        }
6514        foreach a $arcout($id) {
6515            if {!$td && $arctags($a) ne {}} {
6516                validate_arctags $a
6517                if {$arctags($a) ne {}} {
6518                    lappend tagloc($id) [lindex $arctags($a) 0]
6519                }
6520            }
6521            if {![info exists arcend($a)]} continue
6522            set d $arcend($a)
6523            if {$td || $arctags($a) ne {}} {
6524                set tomark [list $d]
6525                for {set j 0} {$j < [llength $tomark]} {incr j} {
6526                    set dd [lindex $tomark $j]
6527                    if {![info exists hastaggeddescendent($dd)]} {
6528                        if {[info exists done($dd)]} {
6529                            foreach b $arcout($dd) {
6530                                if {[info exists arcend($b)]} {
6531                                    lappend tomark $arcend($b)
6532                                }
6533                            }
6534                            if {[info exists tagloc($dd)]} {
6535                                unset tagloc($dd)
6536                            }
6537                        } elseif {[info exists queued($dd)]} {
6538                            incr nc -1
6539                        }
6540                        set hastaggeddescendent($dd) 1
6541                    }
6542                }
6543            }
6544            if {![info exists queued($d)]} {
6545                lappend todo $d
6546                set queued($d) 1
6547                if {![info exists hastaggeddescendent($d)]} {
6548                    incr nc
6549                }
6550            }
6551        }
6552    }
6553    set t2 [clock clicks -milliseconds]
6554    set loopix $i
6555    set tags {}
6556    foreach id [array names tagloc] {
6557        if {![info exists hastaggeddescendent($id)]} {
6558            foreach t $tagloc($id) {
6559                if {[lsearch -exact $tags $t] < 0} {
6560                    lappend tags $t
6561                }
6562            }
6563        }
6564    }
6565
6566    # remove tags that are ancestors of other tags
6567    for {set i 0} {$i < [llength $tags]} {incr i} {
6568        set a [lindex $tags $i]
6569        for {set j 0} {$j < $i} {incr j} {
6570            set b [lindex $tags $j]
6571            set r [anc_or_desc $a $b]
6572            if {$r == -1} {
6573                set tags [lreplace $tags $j $j]
6574                incr j -1
6575                incr i -1
6576            } elseif {$r == 1} {
6577                set tags [lreplace $tags $i $i]
6578                incr i -1
6579                break
6580            }
6581        }
6582    }
6583
6584    if {[array names growing] ne {}} {
6585        # graph isn't finished, need to check if any tag could get
6586        # eclipsed by another tag coming later.  Simply ignore any
6587        # tags that could later get eclipsed.
6588        set ctags {}
6589        foreach t $tags {
6590            if {[is_certain $origid $t]} {
6591                lappend ctags $t
6592            }
6593        }
6594        if {$tags eq $ctags} {
6595            set cached_atags($origid) $tags
6596        } else {
6597            set tags $ctags
6598        }
6599    } else {
6600        set cached_atags($origid) $tags
6601    }
6602    set t3 [clock clicks -milliseconds]
6603    if {0 && $t3 - $t1 >= 100} {
6604        puts "iterating ancestors ($loopix/[llength $todo] nodes) took\
6605            [expr {$t2-$t1}]+[expr {$t3-$t2}]ms, $nc candidates left"
6606    }
6607    return $tags
6608}
6609
6610# Return the list of IDs that have heads that are descendents of id,
6611# including id itself if it has a head.
6612proc descheads {id} {
6613    global arcnos arcstart arcids archeads idheads cached_dheads
6614    global allparents
6615
6616    if {![info exists allparents($id)]} {
6617        return {}
6618    }
6619    set ret {}
6620    if {[llength $arcnos($id)] == 1 && [llength $allparents($id)] == 1} {
6621        # part-way along an arc; check it first
6622        set a [lindex $arcnos($id) 0]
6623        if {$archeads($a) ne {}} {
6624            validate_archeads $a
6625            set i [lsearch -exact $arcids($a) $id]
6626            foreach t $archeads($a) {
6627                set j [lsearch -exact $arcids($a) $t]
6628                if {$j > $i} break
6629                lappend $ret $t
6630            }
6631        }
6632        set id $arcstart($a)
6633    }
6634    set origid $id
6635    set todo [list $id]
6636    set seen($id) 1
6637    for {set i 0} {$i < [llength $todo]} {incr i} {
6638        set id [lindex $todo $i]
6639        if {[info exists cached_dheads($id)]} {
6640            set ret [concat $ret $cached_dheads($id)]
6641        } else {
6642            if {[info exists idheads($id)]} {
6643                lappend ret $id
6644            }
6645            foreach a $arcnos($id) {
6646                if {$archeads($a) ne {}} {
6647                    set ret [concat $ret $archeads($a)]
6648                }
6649                set d $arcstart($a)
6650                if {![info exists seen($d)]} {
6651                    lappend todo $d
6652                    set seen($d) 1
6653                }
6654            }
6655        }
6656    }
6657    set ret [lsort -unique $ret]
6658    set cached_dheads($origid) $ret
6659}
6660
6661proc addedtag {id} {
6662    global arcnos arcout cached_dtags cached_atags
6663
6664    if {![info exists arcnos($id)]} return
6665    if {![info exists arcout($id)]} {
6666        recalcarc [lindex $arcnos($id) 0]
6667    }
6668    catch {unset cached_dtags}
6669    catch {unset cached_atags}
6670}
6671
6672proc addedhead {hid head} {
6673    global arcnos arcout cached_dheads
6674
6675    if {![info exists arcnos($hid)]} return
6676    if {![info exists arcout($hid)]} {
6677        recalcarc [lindex $arcnos($hid) 0]
6678    }
6679    catch {unset cached_dheads}
6680}
6681
6682proc removedhead {hid head} {
6683    global cached_dheads
6684
6685    catch {unset cached_dheads}
6686}
6687
6688proc movedhead {hid head} {
6689    global arcnos arcout cached_dheads
6690
6691    if {![info exists arcnos($hid)]} return
6692    if {![info exists arcout($hid)]} {
6693        recalcarc [lindex $arcnos($hid) 0]
6694    }
6695    catch {unset cached_dheads}
6696}
6697
6698proc changedrefs {} {
6699    global cached_dheads cached_dtags cached_atags
6700    global arctags archeads arcnos arcout idheads idtags
6701
6702    foreach id [concat [array names idheads] [array names idtags]] {
6703        if {[info exists arcnos($id)] && ![info exists arcout($id)]} {
6704            set a [lindex $arcnos($id) 0]
6705            if {![info exists donearc($a)]} {
6706                recalcarc $a
6707                set donearc($a) 1
6708            }
6709        }
6710    }
6711    catch {unset cached_dtags}
6712    catch {unset cached_atags}
6713    catch {unset cached_dheads}
6714}
6715
6716proc rereadrefs {} {
6717    global idtags idheads idotherrefs mainhead
6718
6719    set refids [concat [array names idtags] \
6720                    [array names idheads] [array names idotherrefs]]
6721    foreach id $refids {
6722        if {![info exists ref($id)]} {
6723            set ref($id) [listrefs $id]
6724        }
6725    }
6726    set oldmainhead $mainhead
6727    readrefs
6728    changedrefs
6729    set refids [lsort -unique [concat $refids [array names idtags] \
6730                        [array names idheads] [array names idotherrefs]]]
6731    foreach id $refids {
6732        set v [listrefs $id]
6733        if {![info exists ref($id)] || $ref($id) != $v ||
6734            ($id eq $oldmainhead && $id ne $mainhead) ||
6735            ($id eq $mainhead && $id ne $oldmainhead)} {
6736            redrawtags $id
6737        }
6738    }
6739}
6740
6741proc listrefs {id} {
6742    global idtags idheads idotherrefs
6743
6744    set x {}
6745    if {[info exists idtags($id)]} {
6746        set x $idtags($id)
6747    }
6748    set y {}
6749    if {[info exists idheads($id)]} {
6750        set y $idheads($id)
6751    }
6752    set z {}
6753    if {[info exists idotherrefs($id)]} {
6754        set z $idotherrefs($id)
6755    }
6756    return [list $x $y $z]
6757}
6758
6759proc showtag {tag isnew} {
6760    global ctext tagcontents tagids linknum tagobjid
6761
6762    if {$isnew} {
6763        addtohistory [list showtag $tag 0]
6764    }
6765    $ctext conf -state normal
6766    clear_ctext
6767    set linknum 0
6768    if {![info exists tagcontents($tag)]} {
6769        catch {
6770            set tagcontents($tag) [exec git cat-file tag $tagobjid($tag)]
6771        }
6772    }
6773    if {[info exists tagcontents($tag)]} {
6774        set text $tagcontents($tag)
6775    } else {
6776        set text "Tag: $tag\nId:  $tagids($tag)"
6777    }
6778    appendwithlinks $text {}
6779    $ctext conf -state disabled
6780    init_flist {}
6781}
6782
6783proc doquit {} {
6784    global stopped
6785    set stopped 100
6786    savestuff .
6787    destroy .
6788}
6789
6790proc doprefs {} {
6791    global maxwidth maxgraphpct diffopts
6792    global oldprefs prefstop showneartags showlocalchanges
6793    global bgcolor fgcolor ctext diffcolors selectbgcolor
6794    global uifont tabstop
6795
6796    set top .gitkprefs
6797    set prefstop $top
6798    if {[winfo exists $top]} {
6799        raise $top
6800        return
6801    }
6802    foreach v {maxwidth maxgraphpct diffopts showneartags showlocalchanges} {
6803        set oldprefs($v) [set $v]
6804    }
6805    toplevel $top
6806    wm title $top "Gitk preferences"
6807    label $top.ldisp -text "Commit list display options"
6808    $top.ldisp configure -font $uifont
6809    grid $top.ldisp - -sticky w -pady 10
6810    label $top.spacer -text " "
6811    label $top.maxwidthl -text "Maximum graph width (lines)" \
6812        -font optionfont
6813    spinbox $top.maxwidth -from 0 -to 100 -width 4 -textvariable maxwidth
6814    grid $top.spacer $top.maxwidthl $top.maxwidth -sticky w
6815    label $top.maxpctl -text "Maximum graph width (% of pane)" \
6816        -font optionfont
6817    spinbox $top.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct
6818    grid x $top.maxpctl $top.maxpct -sticky w
6819    frame $top.showlocal
6820    label $top.showlocal.l -text "Show local changes" -font optionfont
6821    checkbutton $top.showlocal.b -variable showlocalchanges
6822    pack $top.showlocal.b $top.showlocal.l -side left
6823    grid x $top.showlocal -sticky w
6824
6825    label $top.ddisp -text "Diff display options"
6826    $top.ddisp configure -font $uifont
6827    grid $top.ddisp - -sticky w -pady 10
6828    label $top.diffoptl -text "Options for diff program" \
6829        -font optionfont
6830    entry $top.diffopt -width 20 -textvariable diffopts
6831    grid x $top.diffoptl $top.diffopt -sticky w
6832    frame $top.ntag
6833    label $top.ntag.l -text "Display nearby tags" -font optionfont
6834    checkbutton $top.ntag.b -variable showneartags
6835    pack $top.ntag.b $top.ntag.l -side left
6836    grid x $top.ntag -sticky w
6837    label $top.tabstopl -text "tabstop" -font optionfont
6838    entry $top.tabstop -width 10 -textvariable tabstop
6839    grid x $top.tabstopl $top.tabstop -sticky w
6840
6841    label $top.cdisp -text "Colors: press to choose"
6842    $top.cdisp configure -font $uifont
6843    grid $top.cdisp - -sticky w -pady 10
6844    label $top.bg -padx 40 -relief sunk -background $bgcolor
6845    button $top.bgbut -text "Background" -font optionfont \
6846        -command [list choosecolor bgcolor 0 $top.bg background setbg]
6847    grid x $top.bgbut $top.bg -sticky w
6848    label $top.fg -padx 40 -relief sunk -background $fgcolor
6849    button $top.fgbut -text "Foreground" -font optionfont \
6850        -command [list choosecolor fgcolor 0 $top.fg foreground setfg]
6851    grid x $top.fgbut $top.fg -sticky w
6852    label $top.diffold -padx 40 -relief sunk -background [lindex $diffcolors 0]
6853    button $top.diffoldbut -text "Diff: old lines" -font optionfont \
6854        -command [list choosecolor diffcolors 0 $top.diffold "diff old lines" \
6855                      [list $ctext tag conf d0 -foreground]]
6856    grid x $top.diffoldbut $top.diffold -sticky w
6857    label $top.diffnew -padx 40 -relief sunk -background [lindex $diffcolors 1]
6858    button $top.diffnewbut -text "Diff: new lines" -font optionfont \
6859        -command [list choosecolor diffcolors 1 $top.diffnew "diff new lines" \
6860                      [list $ctext tag conf d1 -foreground]]
6861    grid x $top.diffnewbut $top.diffnew -sticky w
6862    label $top.hunksep -padx 40 -relief sunk -background [lindex $diffcolors 2]
6863    button $top.hunksepbut -text "Diff: hunk header" -font optionfont \
6864        -command [list choosecolor diffcolors 2 $top.hunksep \
6865                      "diff hunk header" \
6866                      [list $ctext tag conf hunksep -foreground]]
6867    grid x $top.hunksepbut $top.hunksep -sticky w
6868    label $top.selbgsep -padx 40 -relief sunk -background $selectbgcolor
6869    button $top.selbgbut -text "Select bg" -font optionfont \
6870        -command [list choosecolor selectbgcolor 0 $top.bg background setselbg]
6871    grid x $top.selbgbut $top.selbgsep -sticky w
6872
6873    frame $top.buts
6874    button $top.buts.ok -text "OK" -command prefsok -default active
6875    $top.buts.ok configure -font $uifont
6876    button $top.buts.can -text "Cancel" -command prefscan -default normal
6877    $top.buts.can configure -font $uifont
6878    grid $top.buts.ok $top.buts.can
6879    grid columnconfigure $top.buts 0 -weight 1 -uniform a
6880    grid columnconfigure $top.buts 1 -weight 1 -uniform a
6881    grid $top.buts - - -pady 10 -sticky ew
6882    bind $top <Visibility> "focus $top.buts.ok"
6883}
6884
6885proc choosecolor {v vi w x cmd} {
6886    global $v
6887
6888    set c [tk_chooseColor -initialcolor [lindex [set $v] $vi] \
6889               -title "Gitk: choose color for $x"]
6890    if {$c eq {}} return
6891    $w conf -background $c
6892    lset $v $vi $c
6893    eval $cmd $c
6894}
6895
6896proc setselbg {c} {
6897    global bglist cflist
6898    foreach w $bglist {
6899        $w configure -selectbackground $c
6900    }
6901    $cflist tag configure highlight \
6902        -background [$cflist cget -selectbackground]
6903    allcanvs itemconf secsel -fill $c
6904}
6905
6906proc setbg {c} {
6907    global bglist
6908
6909    foreach w $bglist {
6910        $w conf -background $c
6911    }
6912}
6913
6914proc setfg {c} {
6915    global fglist canv
6916
6917    foreach w $fglist {
6918        $w conf -foreground $c
6919    }
6920    allcanvs itemconf text -fill $c
6921    $canv itemconf circle -outline $c
6922}
6923
6924proc prefscan {} {
6925    global maxwidth maxgraphpct diffopts
6926    global oldprefs prefstop showneartags showlocalchanges
6927
6928    foreach v {maxwidth maxgraphpct diffopts showneartags showlocalchanges} {
6929        set $v $oldprefs($v)
6930    }
6931    catch {destroy $prefstop}
6932    unset prefstop
6933}
6934
6935proc prefsok {} {
6936    global maxwidth maxgraphpct
6937    global oldprefs prefstop showneartags showlocalchanges
6938    global charspc ctext tabstop
6939
6940    catch {destroy $prefstop}
6941    unset prefstop
6942    $ctext configure -tabs "[expr {$tabstop * $charspc}]"
6943    if {$showlocalchanges != $oldprefs(showlocalchanges)} {
6944        if {$showlocalchanges} {
6945            doshowlocalchanges
6946        } else {
6947            dohidelocalchanges
6948        }
6949    }
6950    if {$maxwidth != $oldprefs(maxwidth)
6951        || $maxgraphpct != $oldprefs(maxgraphpct)} {
6952        redisplay
6953    } elseif {$showneartags != $oldprefs(showneartags)} {
6954        reselectline
6955    }
6956}
6957
6958proc formatdate {d} {
6959    if {$d ne {}} {
6960        set d [clock format $d -format "%Y-%m-%d %H:%M:%S"]
6961    }
6962    return $d
6963}
6964
6965# This list of encoding names and aliases is distilled from
6966# http://www.iana.org/assignments/character-sets.
6967# Not all of them are supported by Tcl.
6968set encoding_aliases {
6969    { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII
6970      ISO646-US US-ASCII us IBM367 cp367 csASCII }
6971    { ISO-10646-UTF-1 csISO10646UTF1 }
6972    { ISO_646.basic:1983 ref csISO646basic1983 }
6973    { INVARIANT csINVARIANT }
6974    { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion }
6975    { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom }
6976    { NATS-SEFI iso-ir-8-1 csNATSSEFI }
6977    { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD }
6978    { NATS-DANO iso-ir-9-1 csNATSDANO }
6979    { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD }
6980    { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish }
6981    { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames }
6982    { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 }
6983    { ISO-2022-KR csISO2022KR }
6984    { EUC-KR csEUCKR }
6985    { ISO-2022-JP csISO2022JP }
6986    { ISO-2022-JP-2 csISO2022JP2 }
6987    { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7
6988      csISO13JISC6220jp }
6989    { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro }
6990    { IT iso-ir-15 ISO646-IT csISO15Italian }
6991    { PT iso-ir-16 ISO646-PT csISO16Portuguese }
6992    { ES iso-ir-17 ISO646-ES csISO17Spanish }
6993    { greek7-old iso-ir-18 csISO18Greek7Old }
6994    { latin-greek iso-ir-19 csISO19LatinGreek }
6995    { DIN_66003 iso-ir-21 de ISO646-DE csISO21German }
6996    { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French }
6997    { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 }
6998    { ISO_5427 iso-ir-37 csISO5427Cyrillic }
6999    { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 }
7000    { BS_viewdata iso-ir-47 csISO47BSViewdata }
7001    { INIS iso-ir-49 csISO49INIS }
7002    { INIS-8 iso-ir-50 csISO50INIS8 }
7003    { INIS-cyrillic iso-ir-51 csISO51INISCyrillic }
7004    { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 }
7005    { ISO_5428:1980 iso-ir-55 csISO5428Greek }
7006    { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 }
7007    { GB_2312-80 iso-ir-58 chinese csISO58GB231280 }
7008    { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian
7009      csISO60Norwegian1 }
7010    { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 }
7011    { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French }
7012    { videotex-suppl iso-ir-70 csISO70VideotexSupp1 }
7013    { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 }
7014    { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 }
7015    { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian }
7016    { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 }
7017    { greek7 iso-ir-88 csISO88Greek7 }
7018    { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 }
7019    { iso-ir-90 csISO90 }
7020    { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a }
7021    { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b
7022      csISO92JISC62991984b }
7023    { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd }
7024    { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand }
7025    { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add
7026      csISO95JIS62291984handadd }
7027    { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana }
7028    { ISO_2033-1983 iso-ir-98 e13b csISO2033 }
7029    { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS }
7030    { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819
7031      CP819 csISOLatin1 }
7032    { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 }
7033    { T.61-7bit iso-ir-102 csISO102T617bit }
7034    { T.61-8bit T.61 iso-ir-103 csISO103T618bit }
7035    { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 }
7036    { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 }
7037    { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic }
7038    { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 }
7039    { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 }
7040    { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr }
7041    { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708
7042      arabic csISOLatinArabic }
7043    { ISO_8859-6-E csISO88596E ISO-8859-6-E }
7044    { ISO_8859-6-I csISO88596I ISO-8859-6-I }
7045    { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118
7046      greek greek8 csISOLatinGreek }
7047    { T.101-G2 iso-ir-128 csISO128T101G2 }
7048    { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew
7049      csISOLatinHebrew }
7050    { ISO_8859-8-E csISO88598E ISO-8859-8-E }
7051    { ISO_8859-8-I csISO88598I ISO-8859-8-I }
7052    { CSN_369103 iso-ir-139 csISO139CSN369103 }
7053    { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 }
7054    { ISO_6937-2-add iso-ir-142 csISOTextComm }
7055    { IEC_P27-1 iso-ir-143 csISO143IECP271 }
7056    { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic
7057      csISOLatinCyrillic }
7058    { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian }
7059    { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian }
7060    { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 }
7061    { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT }
7062    { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba }
7063    { ISO_6937-2-25 iso-ir-152 csISO6937Add }
7064    { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 }
7065    { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp }
7066    { ISO_10367-box iso-ir-155 csISO10367Box }
7067    { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 }
7068    { latin-lap lap iso-ir-158 csISO158Lap }
7069    { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 }
7070    { DS_2089 DS2089 ISO646-DK dk csISO646Danish }
7071    { us-dk csUSDK }
7072    { dk-us csDKUS }
7073    { JIS_X0201 X0201 csHalfWidthKatakana }
7074    { KSC5636 ISO646-KR csKSC5636 }
7075    { ISO-10646-UCS-2 csUnicode }
7076    { ISO-10646-UCS-4 csUCS4 }
7077    { DEC-MCS dec csDECMCS }
7078    { hp-roman8 roman8 r8 csHPRoman8 }
7079    { macintosh mac csMacintosh }
7080    { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl
7081      csIBM037 }
7082    { IBM038 EBCDIC-INT cp038 csIBM038 }
7083    { IBM273 CP273 csIBM273 }
7084    { IBM274 EBCDIC-BE CP274 csIBM274 }
7085    { IBM275 EBCDIC-BR cp275 csIBM275 }
7086    { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 }
7087    { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 }
7088    { IBM280 CP280 ebcdic-cp-it csIBM280 }
7089    { IBM281 EBCDIC-JP-E cp281 csIBM281 }
7090    { IBM284 CP284 ebcdic-cp-es csIBM284 }
7091    { IBM285 CP285 ebcdic-cp-gb csIBM285 }
7092    { IBM290 cp290 EBCDIC-JP-kana csIBM290 }
7093    { IBM297 cp297 ebcdic-cp-fr csIBM297 }
7094    { IBM420 cp420 ebcdic-cp-ar1 csIBM420 }
7095    { IBM423 cp423 ebcdic-cp-gr csIBM423 }
7096    { IBM424 cp424 ebcdic-cp-he csIBM424 }
7097    { IBM437 cp437 437 csPC8CodePage437 }
7098    { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 }
7099    { IBM775 cp775 csPC775Baltic }
7100    { IBM850 cp850 850 csPC850Multilingual }
7101    { IBM851 cp851 851 csIBM851 }
7102    { IBM852 cp852 852 csPCp852 }
7103    { IBM855 cp855 855 csIBM855 }
7104    { IBM857 cp857 857 csIBM857 }
7105    { IBM860 cp860 860 csIBM860 }
7106    { IBM861 cp861 861 cp-is csIBM861 }
7107    { IBM862 cp862 862 csPC862LatinHebrew }
7108    { IBM863 cp863 863 csIBM863 }
7109    { IBM864 cp864 csIBM864 }
7110    { IBM865 cp865 865 csIBM865 }
7111    { IBM866 cp866 866 csIBM866 }
7112    { IBM868 CP868 cp-ar csIBM868 }
7113    { IBM869 cp869 869 cp-gr csIBM869 }
7114    { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 }
7115    { IBM871 CP871 ebcdic-cp-is csIBM871 }
7116    { IBM880 cp880 EBCDIC-Cyrillic csIBM880 }
7117    { IBM891 cp891 csIBM891 }
7118    { IBM903 cp903 csIBM903 }
7119    { IBM904 cp904 904 csIBBM904 }
7120    { IBM905 CP905 ebcdic-cp-tr csIBM905 }
7121    { IBM918 CP918 ebcdic-cp-ar2 csIBM918 }
7122    { IBM1026 CP1026 csIBM1026 }
7123    { EBCDIC-AT-DE csIBMEBCDICATDE }
7124    { EBCDIC-AT-DE-A csEBCDICATDEA }
7125    { EBCDIC-CA-FR csEBCDICCAFR }
7126    { EBCDIC-DK-NO csEBCDICDKNO }
7127    { EBCDIC-DK-NO-A csEBCDICDKNOA }
7128    { EBCDIC-FI-SE csEBCDICFISE }
7129    { EBCDIC-FI-SE-A csEBCDICFISEA }
7130    { EBCDIC-FR csEBCDICFR }
7131    { EBCDIC-IT csEBCDICIT }
7132    { EBCDIC-PT csEBCDICPT }
7133    { EBCDIC-ES csEBCDICES }
7134    { EBCDIC-ES-A csEBCDICESA }
7135    { EBCDIC-ES-S csEBCDICESS }
7136    { EBCDIC-UK csEBCDICUK }
7137    { EBCDIC-US csEBCDICUS }
7138    { UNKNOWN-8BIT csUnknown8BiT }
7139    { MNEMONIC csMnemonic }
7140    { MNEM csMnem }
7141    { VISCII csVISCII }
7142    { VIQR csVIQR }
7143    { KOI8-R csKOI8R }
7144    { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro }
7145    { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro }
7146    { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro }
7147    { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro }
7148    { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro }
7149    { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro }
7150    { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro }
7151    { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro }
7152    { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro }
7153    { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro }
7154    { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro }
7155    { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro }
7156    { IBM1047 IBM-1047 }
7157    { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian }
7158    { Amiga-1251 Ami1251 Amiga1251 Ami-1251 }
7159    { UNICODE-1-1 csUnicode11 }
7160    { CESU-8 csCESU-8 }
7161    { BOCU-1 csBOCU-1 }
7162    { UNICODE-1-1-UTF-7 csUnicode11UTF7 }
7163    { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic
7164      l8 }
7165    { ISO-8859-15 ISO_8859-15 Latin-9 }
7166    { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 }
7167    { GBK CP936 MS936 windows-936 }
7168    { JIS_Encoding csJISEncoding }
7169    { Shift_JIS MS_Kanji csShiftJIS }
7170    { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese
7171      EUC-JP }
7172    { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese }
7173    { ISO-10646-UCS-Basic csUnicodeASCII }
7174    { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 }
7175    { ISO-Unicode-IBM-1261 csUnicodeIBM1261 }
7176    { ISO-Unicode-IBM-1268 csUnicodeIBM1268 }
7177    { ISO-Unicode-IBM-1276 csUnicodeIBM1276 }
7178    { ISO-Unicode-IBM-1264 csUnicodeIBM1264 }
7179    { ISO-Unicode-IBM-1265 csUnicodeIBM1265 }
7180    { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 }
7181    { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 }
7182    { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 }
7183    { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 }
7184    { Adobe-Standard-Encoding csAdobeStandardEncoding }
7185    { Ventura-US csVenturaUS }
7186    { Ventura-International csVenturaInternational }
7187    { PC8-Danish-Norwegian csPC8DanishNorwegian }
7188    { PC8-Turkish csPC8Turkish }
7189    { IBM-Symbols csIBMSymbols }
7190    { IBM-Thai csIBMThai }
7191    { HP-Legal csHPLegal }
7192    { HP-Pi-font csHPPiFont }
7193    { HP-Math8 csHPMath8 }
7194    { Adobe-Symbol-Encoding csHPPSMath }
7195    { HP-DeskTop csHPDesktop }
7196    { Ventura-Math csVenturaMath }
7197    { Microsoft-Publishing csMicrosoftPublishing }
7198    { Windows-31J csWindows31J }
7199    { GB2312 csGB2312 }
7200    { Big5 csBig5 }
7201}
7202
7203proc tcl_encoding {enc} {
7204    global encoding_aliases
7205    set names [encoding names]
7206    set lcnames [string tolower $names]
7207    set enc [string tolower $enc]
7208    set i [lsearch -exact $lcnames $enc]
7209    if {$i < 0} {
7210        # look for "isonnn" instead of "iso-nnn" or "iso_nnn"
7211        if {[regsub {^iso[-_]} $enc iso encx]} {
7212            set i [lsearch -exact $lcnames $encx]
7213        }
7214    }
7215    if {$i < 0} {
7216        foreach l $encoding_aliases {
7217            set ll [string tolower $l]
7218            if {[lsearch -exact $ll $enc] < 0} continue
7219            # look through the aliases for one that tcl knows about
7220            foreach e $ll {
7221                set i [lsearch -exact $lcnames $e]
7222                if {$i < 0} {
7223                    if {[regsub {^iso[-_]} $e iso ex]} {
7224                        set i [lsearch -exact $lcnames $ex]
7225                    }
7226                }
7227                if {$i >= 0} break
7228            }
7229            break
7230        }
7231    }
7232    if {$i >= 0} {
7233        return [lindex $names $i]
7234    }
7235    return {}
7236}
7237
7238# defaults...
7239set datemode 0
7240set diffopts "-U 5 -p"
7241set wrcomcmd "git diff-tree --stdin -p --pretty"
7242
7243set gitencoding {}
7244catch {
7245    set gitencoding [exec git config --get i18n.commitencoding]
7246}
7247if {$gitencoding == ""} {
7248    set gitencoding "utf-8"
7249}
7250set tclencoding [tcl_encoding $gitencoding]
7251if {$tclencoding == {}} {
7252    puts stderr "Warning: encoding $gitencoding is not supported by Tcl/Tk"
7253}
7254
7255set mainfont {Helvetica 9}
7256set textfont {Courier 9}
7257set uifont {Helvetica 9 bold}
7258set tabstop 8
7259set findmergefiles 0
7260set maxgraphpct 50
7261set maxwidth 16
7262set revlistorder 0
7263set fastdate 0
7264set uparrowlen 7
7265set downarrowlen 7
7266set mingaplen 30
7267set cmitmode "patch"
7268set wrapcomment "none"
7269set showneartags 1
7270set maxrefs 20
7271set maxlinelen 200
7272set showlocalchanges 1
7273
7274set colors {green red blue magenta darkgrey brown orange}
7275set bgcolor white
7276set fgcolor black
7277set diffcolors {red "#00a000" blue}
7278set selectbgcolor gray85
7279
7280catch {source ~/.gitk}
7281
7282font create optionfont -family sans-serif -size -12
7283
7284set revtreeargs {}
7285foreach arg $argv {
7286    switch -regexp -- $arg {
7287        "^$" { }
7288        "^-d" { set datemode 1 }
7289        default {
7290            lappend revtreeargs $arg
7291        }
7292    }
7293}
7294
7295# check that we can find a .git directory somewhere...
7296set gitdir [gitdir]
7297if {![file isdirectory $gitdir]} {
7298    show_error {} . "Cannot find the git directory \"$gitdir\"."
7299    exit 1
7300}
7301
7302set cmdline_files {}
7303set i [lsearch -exact $revtreeargs "--"]
7304if {$i >= 0} {
7305    set cmdline_files [lrange $revtreeargs [expr {$i + 1}] end]
7306    set revtreeargs [lrange $revtreeargs 0 [expr {$i - 1}]]
7307} elseif {$revtreeargs ne {}} {
7308    if {[catch {
7309        set f [eval exec git rev-parse --no-revs --no-flags $revtreeargs]
7310        set cmdline_files [split $f "\n"]
7311        set n [llength $cmdline_files]
7312        set revtreeargs [lrange $revtreeargs 0 end-$n]
7313    } err]} {
7314        # unfortunately we get both stdout and stderr in $err,
7315        # so look for "fatal:".
7316        set i [string first "fatal:" $err]
7317        if {$i > 0} {
7318            set err [string range $err [expr {$i + 6}] end]
7319        }
7320        show_error {} . "Bad arguments to gitk:\n$err"
7321        exit 1
7322    }
7323}
7324
7325set nullid "0000000000000000000000000000000000000000"
7326
7327set runq {}
7328set history {}
7329set historyindex 0
7330set fh_serial 0
7331set nhl_names {}
7332set highlight_paths {}
7333set searchdirn -forwards
7334set boldrows {}
7335set boldnamerows {}
7336set diffelide {0 0}
7337
7338set optim_delay 16
7339
7340set nextviewnum 1
7341set curview 0
7342set selectedview 0
7343set selectedhlview None
7344set viewfiles(0) {}
7345set viewperm(0) 0
7346set viewargs(0) {}
7347
7348set cmdlineok 0
7349set stopped 0
7350set stuffsaved 0
7351set patchnum 0
7352set lookingforhead 0
7353set localrow -1
7354set lserial 0
7355setcoords
7356makewindow
7357wm title . "[file tail $argv0]: [file tail [pwd]]"
7358readrefs
7359
7360if {$cmdline_files ne {} || $revtreeargs ne {}} {
7361    # create a view for the files/dirs specified on the command line
7362    set curview 1
7363    set selectedview 1
7364    set nextviewnum 2
7365    set viewname(1) "Command line"
7366    set viewfiles(1) $cmdline_files
7367    set viewargs(1) $revtreeargs
7368    set viewperm(1) 0
7369    addviewmenu 1
7370    .bar.view entryconf Edit* -state normal
7371    .bar.view entryconf Delete* -state normal
7372}
7373
7374if {[info exists permviews]} {
7375    foreach v $permviews {
7376        set n $nextviewnum
7377        incr nextviewnum
7378        set viewname($n) [lindex $v 0]
7379        set viewfiles($n) [lindex $v 1]
7380        set viewargs($n) [lindex $v 2]
7381        set viewperm($n) 1
7382        addviewmenu $n
7383    }
7384}
7385getcommits