gitkon commit Small fixes in Documentation/git-mv.txt (b933e81)
   1#!/bin/sh
   2# Tcl ignores the next line -*- tcl -*- \
   3exec wish "$0" -- "$@"
   4
   5# Copyright (C) 2005 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 ".git"
  16    }
  17}
  18
  19proc getcommits {rargs} {
  20    global commits commfd phase canv mainfont env
  21    global startmsecs nextupdate ncmupdate
  22    global ctext maincursor textcursor leftover
  23
  24    # check that we can find a .git directory somewhere...
  25    set gitdir [gitdir]
  26    if {![file isdirectory $gitdir]} {
  27        error_popup "Cannot find the git directory \"$gitdir\"."
  28        exit 1
  29    }
  30    set commits {}
  31    set phase getcommits
  32    set startmsecs [clock clicks -milliseconds]
  33    set nextupdate [expr $startmsecs + 100]
  34    set ncmupdate 1
  35    if [catch {
  36        set parse_args [concat --default HEAD $rargs]
  37        set parsed_args [split [eval exec git-rev-parse $parse_args] "\n"]
  38    }] {
  39        # if git-rev-parse failed for some reason...
  40        if {$rargs == {}} {
  41            set rargs HEAD
  42        }
  43        set parsed_args $rargs
  44    }
  45    if [catch {
  46        set commfd [open "|git-rev-list --header --topo-order --parents $parsed_args" r]
  47    } err] {
  48        puts stderr "Error executing git-rev-list: $err"
  49        exit 1
  50    }
  51    set leftover {}
  52    fconfigure $commfd -blocking 0 -translation lf
  53    fileevent $commfd readable [list getcommitlines $commfd]
  54    $canv delete all
  55    $canv create text 3 3 -anchor nw -text "Reading commits..." \
  56        -font $mainfont -tags textitems
  57    . config -cursor watch
  58    settextcursor watch
  59}
  60
  61proc getcommitlines {commfd}  {
  62    global commits parents cdate children
  63    global commitlisted phase nextupdate
  64    global stopped redisplaying leftover
  65
  66    set stuff [read $commfd]
  67    if {$stuff == {}} {
  68        if {![eof $commfd]} return
  69        # set it blocking so we wait for the process to terminate
  70        fconfigure $commfd -blocking 1
  71        if {![catch {close $commfd} err]} {
  72            after idle finishcommits
  73            return
  74        }
  75        if {[string range $err 0 4] == "usage"} {
  76            set err \
  77{Gitk: error reading commits: bad arguments to git-rev-list.
  78(Note: arguments to gitk are passed to git-rev-list
  79to allow selection of commits to be displayed.)}
  80        } else {
  81            set err "Error reading commits: $err"
  82        }
  83        error_popup $err
  84        exit 1
  85    }
  86    set start 0
  87    while 1 {
  88        set i [string first "\0" $stuff $start]
  89        if {$i < 0} {
  90            append leftover [string range $stuff $start end]
  91            return
  92        }
  93        set cmit [string range $stuff $start [expr {$i - 1}]]
  94        if {$start == 0} {
  95            set cmit "$leftover$cmit"
  96            set leftover {}
  97        }
  98        set start [expr {$i + 1}]
  99        set j [string first "\n" $cmit]
 100        set ok 0
 101        if {$j >= 0} {
 102            set ids [string range $cmit 0 [expr {$j - 1}]]
 103            set ok 1
 104            foreach id $ids {
 105                if {![regexp {^[0-9a-f]{40}$} $id]} {
 106                    set ok 0
 107                    break
 108                }
 109            }
 110        }
 111        if {!$ok} {
 112            set shortcmit $cmit
 113            if {[string length $shortcmit] > 80} {
 114                set shortcmit "[string range $shortcmit 0 80]..."
 115            }
 116            error_popup "Can't parse git-rev-list output: {$shortcmit}"
 117            exit 1
 118        }
 119        set id [lindex $ids 0]
 120        set olds [lrange $ids 1 end]
 121        set cmit [string range $cmit [expr {$j + 1}] end]
 122        lappend commits $id
 123        set commitlisted($id) 1
 124        parsecommit $id $cmit 1 [lrange $ids 1 end]
 125        drawcommit $id
 126        if {[clock clicks -milliseconds] >= $nextupdate} {
 127            doupdate 1
 128        }
 129        while {$redisplaying} {
 130            set redisplaying 0
 131            if {$stopped == 1} {
 132                set stopped 0
 133                set phase "getcommits"
 134                foreach id $commits {
 135                    drawcommit $id
 136                    if {$stopped} break
 137                    if {[clock clicks -milliseconds] >= $nextupdate} {
 138                        doupdate 1
 139                    }
 140                }
 141            }
 142        }
 143    }
 144}
 145
 146proc doupdate {reading} {
 147    global commfd nextupdate numcommits ncmupdate
 148
 149    if {$reading} {
 150        fileevent $commfd readable {}
 151    }
 152    update
 153    set nextupdate [expr {[clock clicks -milliseconds] + 100}]
 154    if {$numcommits < 100} {
 155        set ncmupdate [expr {$numcommits + 1}]
 156    } elseif {$numcommits < 10000} {
 157        set ncmupdate [expr {$numcommits + 10}]
 158    } else {
 159        set ncmupdate [expr {$numcommits + 100}]
 160    }
 161    if {$reading} {
 162        fileevent $commfd readable [list getcommitlines $commfd]
 163    }
 164}
 165
 166proc readcommit {id} {
 167    if [catch {set contents [exec git-cat-file commit $id]}] return
 168    parsecommit $id $contents 0 {}
 169}
 170
 171proc parsecommit {id contents listed olds} {
 172    global commitinfo children nchildren parents nparents cdate ncleft
 173
 174    set inhdr 1
 175    set comment {}
 176    set headline {}
 177    set auname {}
 178    set audate {}
 179    set comname {}
 180    set comdate {}
 181    if {![info exists nchildren($id)]} {
 182        set children($id) {}
 183        set nchildren($id) 0
 184        set ncleft($id) 0
 185    }
 186    set parents($id) $olds
 187    set nparents($id) [llength $olds]
 188    foreach p $olds {
 189        if {![info exists nchildren($p)]} {
 190            set children($p) [list $id]
 191            set nchildren($p) 1
 192            set ncleft($p) 1
 193        } elseif {[lsearch -exact $children($p) $id] < 0} {
 194            lappend children($p) $id
 195            incr nchildren($p)
 196            incr ncleft($p)
 197        }
 198    }
 199    set hdrend [string first "\n\n" $contents]
 200    if {$hdrend < 0} {
 201        # should never happen...
 202        set hdrend [string length $contents]
 203    }
 204    set header [string range $contents 0 [expr {$hdrend - 1}]]
 205    set comment [string range $contents [expr {$hdrend + 2}] end]
 206    foreach line [split $header "\n"] {
 207        set tag [lindex $line 0]
 208        if {$tag == "author"} {
 209            set audate [lindex $line end-1]
 210            set auname [lrange $line 1 end-2]
 211        } elseif {$tag == "committer"} {
 212            set comdate [lindex $line end-1]
 213            set comname [lrange $line 1 end-2]
 214        }
 215    }
 216    set headline {}
 217    # take the first line of the comment as the headline
 218    set i [string first "\n" $comment]
 219    if {$i >= 0} {
 220        set headline [string trim [string range $comment 0 $i]]
 221    } else {
 222        set headline $comment
 223    }
 224    if {!$listed} {
 225        # git-rev-list indents the comment by 4 spaces;
 226        # if we got this via git-cat-file, add the indentation
 227        set newcomment {}
 228        foreach line [split $comment "\n"] {
 229            append newcomment "    "
 230            append newcomment $line
 231            append newcomment "\n"
 232        }
 233        set comment $newcomment
 234    }
 235    if {$comdate != {}} {
 236        set cdate($id) $comdate
 237    }
 238    set commitinfo($id) [list $headline $auname $audate \
 239                             $comname $comdate $comment]
 240}
 241
 242proc readrefs {} {
 243    global tagids idtags headids idheads tagcontents
 244    global otherrefids idotherrefs
 245
 246    set refd [open [list | git-ls-remote [gitdir]] r]
 247    while {0 <= [set n [gets $refd line]]} {
 248        if {![regexp {^([0-9a-f]{40})   refs/([^^]*)$} $line \
 249            match id path]} {
 250            continue
 251        }
 252        if {![regexp {^(tags|heads)/(.*)$} $path match type name]} {
 253            set type others
 254            set name $path
 255        }
 256        if {$type == "tags"} {
 257            set tagids($name) $id
 258            lappend idtags($id) $name
 259            set obj {}
 260            set type {}
 261            set tag {}
 262            catch {
 263                set commit [exec git-rev-parse "$id^0"]
 264                if {"$commit" != "$id"} {
 265                    set tagids($name) $commit
 266                    lappend idtags($commit) $name
 267                }
 268            }           
 269            catch {
 270                set tagcontents($name) [exec git-cat-file tag "$id"]
 271            }
 272        } elseif { $type == "heads" } {
 273            set headids($name) $id
 274            lappend idheads($id) $name
 275        } else {
 276            set otherrefids($name) $id
 277            lappend idotherrefs($id) $name
 278        }
 279    }
 280    close $refd
 281}
 282
 283proc error_popup msg {
 284    set w .error
 285    toplevel $w
 286    wm transient $w .
 287    message $w.m -text $msg -justify center -aspect 400
 288    pack $w.m -side top -fill x -padx 20 -pady 20
 289    button $w.ok -text OK -command "destroy $w"
 290    pack $w.ok -side bottom -fill x
 291    bind $w <Visibility> "grab $w; focus $w"
 292    tkwait window $w
 293}
 294
 295proc makewindow {} {
 296    global canv canv2 canv3 linespc charspc ctext cflist textfont
 297    global findtype findtypemenu findloc findstring fstring geometry
 298    global entries sha1entry sha1string sha1but
 299    global maincursor textcursor curtextcursor
 300    global rowctxmenu gaudydiff mergemax
 301
 302    menu .bar
 303    .bar add cascade -label "File" -menu .bar.file
 304    menu .bar.file
 305    .bar.file add command -label "Reread references" -command rereadrefs
 306    .bar.file add command -label "Quit" -command doquit
 307    menu .bar.help
 308    .bar add cascade -label "Help" -menu .bar.help
 309    .bar.help add command -label "About gitk" -command about
 310    . configure -menu .bar
 311
 312    if {![info exists geometry(canv1)]} {
 313        set geometry(canv1) [expr 45 * $charspc]
 314        set geometry(canv2) [expr 30 * $charspc]
 315        set geometry(canv3) [expr 15 * $charspc]
 316        set geometry(canvh) [expr 25 * $linespc + 4]
 317        set geometry(ctextw) 80
 318        set geometry(ctexth) 30
 319        set geometry(cflistw) 30
 320    }
 321    panedwindow .ctop -orient vertical
 322    if {[info exists geometry(width)]} {
 323        .ctop conf -width $geometry(width) -height $geometry(height)
 324        set texth [expr {$geometry(height) - $geometry(canvh) - 56}]
 325        set geometry(ctexth) [expr {($texth - 8) /
 326                                    [font metrics $textfont -linespace]}]
 327    }
 328    frame .ctop.top
 329    frame .ctop.top.bar
 330    pack .ctop.top.bar -side bottom -fill x
 331    set cscroll .ctop.top.csb
 332    scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
 333    pack $cscroll -side right -fill y
 334    panedwindow .ctop.top.clist -orient horizontal -sashpad 0 -handlesize 4
 335    pack .ctop.top.clist -side top -fill both -expand 1
 336    .ctop add .ctop.top
 337    set canv .ctop.top.clist.canv
 338    canvas $canv -height $geometry(canvh) -width $geometry(canv1) \
 339        -bg white -bd 0 \
 340        -yscrollincr $linespc -yscrollcommand "$cscroll set"
 341    .ctop.top.clist add $canv
 342    set canv2 .ctop.top.clist.canv2
 343    canvas $canv2 -height $geometry(canvh) -width $geometry(canv2) \
 344        -bg white -bd 0 -yscrollincr $linespc
 345    .ctop.top.clist add $canv2
 346    set canv3 .ctop.top.clist.canv3
 347    canvas $canv3 -height $geometry(canvh) -width $geometry(canv3) \
 348        -bg white -bd 0 -yscrollincr $linespc
 349    .ctop.top.clist add $canv3
 350    bind .ctop.top.clist <Configure> {resizeclistpanes %W %w}
 351
 352    set sha1entry .ctop.top.bar.sha1
 353    set entries $sha1entry
 354    set sha1but .ctop.top.bar.sha1label
 355    button $sha1but -text "SHA1 ID: " -state disabled -relief flat \
 356        -command gotocommit -width 8
 357    $sha1but conf -disabledforeground [$sha1but cget -foreground]
 358    pack .ctop.top.bar.sha1label -side left
 359    entry $sha1entry -width 40 -font $textfont -textvariable sha1string
 360    trace add variable sha1string write sha1change
 361    pack $sha1entry -side left -pady 2
 362
 363    image create bitmap bm-left -data {
 364        #define left_width 16
 365        #define left_height 16
 366        static unsigned char left_bits[] = {
 367        0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00,
 368        0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00,
 369        0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01};
 370    }
 371    image create bitmap bm-right -data {
 372        #define right_width 16
 373        #define right_height 16
 374        static unsigned char right_bits[] = {
 375        0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c,
 376        0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c,
 377        0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01};
 378    }
 379    button .ctop.top.bar.leftbut -image bm-left -command goback \
 380        -state disabled -width 26
 381    pack .ctop.top.bar.leftbut -side left -fill y
 382    button .ctop.top.bar.rightbut -image bm-right -command goforw \
 383        -state disabled -width 26
 384    pack .ctop.top.bar.rightbut -side left -fill y
 385
 386    button .ctop.top.bar.findbut -text "Find" -command dofind
 387    pack .ctop.top.bar.findbut -side left
 388    set findstring {}
 389    set fstring .ctop.top.bar.findstring
 390    lappend entries $fstring
 391    entry $fstring -width 30 -font $textfont -textvariable findstring
 392    pack $fstring -side left -expand 1 -fill x
 393    set findtype Exact
 394    set findtypemenu [tk_optionMenu .ctop.top.bar.findtype \
 395                          findtype Exact IgnCase Regexp]
 396    set findloc "All fields"
 397    tk_optionMenu .ctop.top.bar.findloc findloc "All fields" Headline \
 398        Comments Author Committer Files Pickaxe
 399    pack .ctop.top.bar.findloc -side right
 400    pack .ctop.top.bar.findtype -side right
 401    # for making sure type==Exact whenever loc==Pickaxe
 402    trace add variable findloc write findlocchange
 403
 404    panedwindow .ctop.cdet -orient horizontal
 405    .ctop add .ctop.cdet
 406    frame .ctop.cdet.left
 407    set ctext .ctop.cdet.left.ctext
 408    text $ctext -bg white -state disabled -font $textfont \
 409        -width $geometry(ctextw) -height $geometry(ctexth) \
 410        -yscrollcommand ".ctop.cdet.left.sb set" -wrap none
 411    scrollbar .ctop.cdet.left.sb -command "$ctext yview"
 412    pack .ctop.cdet.left.sb -side right -fill y
 413    pack $ctext -side left -fill both -expand 1
 414    .ctop.cdet add .ctop.cdet.left
 415
 416    $ctext tag conf filesep -font [concat $textfont bold] -back "#aaaaaa"
 417    if {$gaudydiff} {
 418        $ctext tag conf hunksep -back blue -fore white
 419        $ctext tag conf d0 -back "#ff8080"
 420        $ctext tag conf d1 -back green
 421    } else {
 422        $ctext tag conf hunksep -fore blue
 423        $ctext tag conf d0 -fore red
 424        $ctext tag conf d1 -fore "#00a000"
 425        $ctext tag conf m0 -fore red
 426        $ctext tag conf m1 -fore blue
 427        $ctext tag conf m2 -fore green
 428        $ctext tag conf m3 -fore purple
 429        $ctext tag conf m4 -fore brown
 430        $ctext tag conf mmax -fore darkgrey
 431        set mergemax 5
 432        $ctext tag conf mresult -font [concat $textfont bold]
 433        $ctext tag conf msep -font [concat $textfont bold]
 434        $ctext tag conf found -back yellow
 435    }
 436
 437    frame .ctop.cdet.right
 438    set cflist .ctop.cdet.right.cfiles
 439    listbox $cflist -bg white -selectmode extended -width $geometry(cflistw) \
 440        -yscrollcommand ".ctop.cdet.right.sb set"
 441    scrollbar .ctop.cdet.right.sb -command "$cflist yview"
 442    pack .ctop.cdet.right.sb -side right -fill y
 443    pack $cflist -side left -fill both -expand 1
 444    .ctop.cdet add .ctop.cdet.right
 445    bind .ctop.cdet <Configure> {resizecdetpanes %W %w}
 446
 447    pack .ctop -side top -fill both -expand 1
 448
 449    bindall <1> {selcanvline %W %x %y}
 450    #bindall <B1-Motion> {selcanvline %W %x %y}
 451    bindall <ButtonRelease-4> "allcanvs yview scroll -5 units"
 452    bindall <ButtonRelease-5> "allcanvs yview scroll 5 units"
 453    bindall <2> "allcanvs scan mark 0 %y"
 454    bindall <B2-Motion> "allcanvs scan dragto 0 %y"
 455    bind . <Key-Up> "selnextline -1"
 456    bind . <Key-Down> "selnextline 1"
 457    bind . <Key-Right> "goforw"
 458    bind . <Key-Left> "goback"
 459    bind . <Key-Prior> "allcanvs yview scroll -1 pages"
 460    bind . <Key-Next> "allcanvs yview scroll 1 pages"
 461    bindkey <Key-Delete> "$ctext yview scroll -1 pages"
 462    bindkey <Key-BackSpace> "$ctext yview scroll -1 pages"
 463    bindkey <Key-space> "$ctext yview scroll 1 pages"
 464    bindkey p "selnextline -1"
 465    bindkey n "selnextline 1"
 466    bindkey z "goback"
 467    bindkey x "goforw"
 468    bindkey i "selnextline -1"
 469    bindkey k "selnextline 1"
 470    bindkey j "goback"
 471    bindkey l "goforw"
 472    bindkey b "$ctext yview scroll -1 pages"
 473    bindkey d "$ctext yview scroll 18 units"
 474    bindkey u "$ctext yview scroll -18 units"
 475    bindkey / {findnext 1}
 476    bindkey <Key-Return> {findnext 0}
 477    bindkey ? findprev
 478    bindkey f nextfile
 479    bind . <Control-q> doquit
 480    bind . <Control-f> dofind
 481    bind . <Control-g> {findnext 0}
 482    bind . <Control-r> findprev
 483    bind . <Control-equal> {incrfont 1}
 484    bind . <Control-KP_Add> {incrfont 1}
 485    bind . <Control-minus> {incrfont -1}
 486    bind . <Control-KP_Subtract> {incrfont -1}
 487    bind $cflist <<ListboxSelect>> listboxsel
 488    bind . <Destroy> {savestuff %W}
 489    bind . <Button-1> "click %W"
 490    bind $fstring <Key-Return> dofind
 491    bind $sha1entry <Key-Return> gotocommit
 492    bind $sha1entry <<PasteSelection>> clearsha1
 493
 494    set maincursor [. cget -cursor]
 495    set textcursor [$ctext cget -cursor]
 496    set curtextcursor $textcursor
 497
 498    set rowctxmenu .rowctxmenu
 499    menu $rowctxmenu -tearoff 0
 500    $rowctxmenu add command -label "Diff this -> selected" \
 501        -command {diffvssel 0}
 502    $rowctxmenu add command -label "Diff selected -> this" \
 503        -command {diffvssel 1}
 504    $rowctxmenu add command -label "Make patch" -command mkpatch
 505    $rowctxmenu add command -label "Create tag" -command mktag
 506    $rowctxmenu add command -label "Write commit to file" -command writecommit
 507}
 508
 509# when we make a key binding for the toplevel, make sure
 510# it doesn't get triggered when that key is pressed in the
 511# find string entry widget.
 512proc bindkey {ev script} {
 513    global entries
 514    bind . $ev $script
 515    set escript [bind Entry $ev]
 516    if {$escript == {}} {
 517        set escript [bind Entry <Key>]
 518    }
 519    foreach e $entries {
 520        bind $e $ev "$escript; break"
 521    }
 522}
 523
 524# set the focus back to the toplevel for any click outside
 525# the entry widgets
 526proc click {w} {
 527    global entries
 528    foreach e $entries {
 529        if {$w == $e} return
 530    }
 531    focus .
 532}
 533
 534proc savestuff {w} {
 535    global canv canv2 canv3 ctext cflist mainfont textfont
 536    global stuffsaved findmergefiles gaudydiff maxgraphpct
 537    global maxwidth
 538
 539    if {$stuffsaved} return
 540    if {![winfo viewable .]} return
 541    catch {
 542        set f [open "~/.gitk-new" w]
 543        puts $f [list set mainfont $mainfont]
 544        puts $f [list set textfont $textfont]
 545        puts $f [list set findmergefiles $findmergefiles]
 546        puts $f [list set gaudydiff $gaudydiff]
 547        puts $f [list set maxgraphpct $maxgraphpct]
 548        puts $f [list set maxwidth $maxwidth]
 549        puts $f "set geometry(width) [winfo width .ctop]"
 550        puts $f "set geometry(height) [winfo height .ctop]"
 551        puts $f "set geometry(canv1) [expr [winfo width $canv]-2]"
 552        puts $f "set geometry(canv2) [expr [winfo width $canv2]-2]"
 553        puts $f "set geometry(canv3) [expr [winfo width $canv3]-2]"
 554        puts $f "set geometry(canvh) [expr [winfo height $canv]-2]"
 555        set wid [expr {([winfo width $ctext] - 8) \
 556                           / [font measure $textfont "0"]}]
 557        puts $f "set geometry(ctextw) $wid"
 558        set wid [expr {([winfo width $cflist] - 11) \
 559                           / [font measure [$cflist cget -font] "0"]}]
 560        puts $f "set geometry(cflistw) $wid"
 561        close $f
 562        file rename -force "~/.gitk-new" "~/.gitk"
 563    }
 564    set stuffsaved 1
 565}
 566
 567proc resizeclistpanes {win w} {
 568    global oldwidth
 569    if [info exists oldwidth($win)] {
 570        set s0 [$win sash coord 0]
 571        set s1 [$win sash coord 1]
 572        if {$w < 60} {
 573            set sash0 [expr {int($w/2 - 2)}]
 574            set sash1 [expr {int($w*5/6 - 2)}]
 575        } else {
 576            set factor [expr {1.0 * $w / $oldwidth($win)}]
 577            set sash0 [expr {int($factor * [lindex $s0 0])}]
 578            set sash1 [expr {int($factor * [lindex $s1 0])}]
 579            if {$sash0 < 30} {
 580                set sash0 30
 581            }
 582            if {$sash1 < $sash0 + 20} {
 583                set sash1 [expr $sash0 + 20]
 584            }
 585            if {$sash1 > $w - 10} {
 586                set sash1 [expr $w - 10]
 587                if {$sash0 > $sash1 - 20} {
 588                    set sash0 [expr $sash1 - 20]
 589                }
 590            }
 591        }
 592        $win sash place 0 $sash0 [lindex $s0 1]
 593        $win sash place 1 $sash1 [lindex $s1 1]
 594    }
 595    set oldwidth($win) $w
 596}
 597
 598proc resizecdetpanes {win w} {
 599    global oldwidth
 600    if [info exists oldwidth($win)] {
 601        set s0 [$win sash coord 0]
 602        if {$w < 60} {
 603            set sash0 [expr {int($w*3/4 - 2)}]
 604        } else {
 605            set factor [expr {1.0 * $w / $oldwidth($win)}]
 606            set sash0 [expr {int($factor * [lindex $s0 0])}]
 607            if {$sash0 < 45} {
 608                set sash0 45
 609            }
 610            if {$sash0 > $w - 15} {
 611                set sash0 [expr $w - 15]
 612            }
 613        }
 614        $win sash place 0 $sash0 [lindex $s0 1]
 615    }
 616    set oldwidth($win) $w
 617}
 618
 619proc allcanvs args {
 620    global canv canv2 canv3
 621    eval $canv $args
 622    eval $canv2 $args
 623    eval $canv3 $args
 624}
 625
 626proc bindall {event action} {
 627    global canv canv2 canv3
 628    bind $canv $event $action
 629    bind $canv2 $event $action
 630    bind $canv3 $event $action
 631}
 632
 633proc about {} {
 634    set w .about
 635    if {[winfo exists $w]} {
 636        raise $w
 637        return
 638    }
 639    toplevel $w
 640    wm title $w "About gitk"
 641    message $w.m -text {
 642Gitk version 1.2
 643
 644Copyright © 2005 Paul Mackerras
 645
 646Use and redistribute under the terms of the GNU General Public License} \
 647            -justify center -aspect 400
 648    pack $w.m -side top -fill x -padx 20 -pady 20
 649    button $w.ok -text Close -command "destroy $w"
 650    pack $w.ok -side bottom
 651}
 652
 653proc assigncolor {id} {
 654    global colormap commcolors colors nextcolor
 655    global parents nparents children nchildren
 656    global cornercrossings crossings
 657
 658    if [info exists colormap($id)] return
 659    set ncolors [llength $colors]
 660    if {$nparents($id) <= 1 && $nchildren($id) == 1} {
 661        set child [lindex $children($id) 0]
 662        if {[info exists colormap($child)]
 663            && $nparents($child) == 1} {
 664            set colormap($id) $colormap($child)
 665            return
 666        }
 667    }
 668    set badcolors {}
 669    if {[info exists cornercrossings($id)]} {
 670        foreach x $cornercrossings($id) {
 671            if {[info exists colormap($x)]
 672                && [lsearch -exact $badcolors $colormap($x)] < 0} {
 673                lappend badcolors $colormap($x)
 674            }
 675        }
 676        if {[llength $badcolors] >= $ncolors} {
 677            set badcolors {}
 678        }
 679    }
 680    set origbad $badcolors
 681    if {[llength $badcolors] < $ncolors - 1} {
 682        if {[info exists crossings($id)]} {
 683            foreach x $crossings($id) {
 684                if {[info exists colormap($x)]
 685                    && [lsearch -exact $badcolors $colormap($x)] < 0} {
 686                    lappend badcolors $colormap($x)
 687                }
 688            }
 689            if {[llength $badcolors] >= $ncolors} {
 690                set badcolors $origbad
 691            }
 692        }
 693        set origbad $badcolors
 694    }
 695    if {[llength $badcolors] < $ncolors - 1} {
 696        foreach child $children($id) {
 697            if {[info exists colormap($child)]
 698                && [lsearch -exact $badcolors $colormap($child)] < 0} {
 699                lappend badcolors $colormap($child)
 700            }
 701            if {[info exists parents($child)]} {
 702                foreach p $parents($child) {
 703                    if {[info exists colormap($p)]
 704                        && [lsearch -exact $badcolors $colormap($p)] < 0} {
 705                        lappend badcolors $colormap($p)
 706                    }
 707                }
 708            }
 709        }
 710        if {[llength $badcolors] >= $ncolors} {
 711            set badcolors $origbad
 712        }
 713    }
 714    for {set i 0} {$i <= $ncolors} {incr i} {
 715        set c [lindex $colors $nextcolor]
 716        if {[incr nextcolor] >= $ncolors} {
 717            set nextcolor 0
 718        }
 719        if {[lsearch -exact $badcolors $c]} break
 720    }
 721    set colormap($id) $c
 722}
 723
 724proc initgraph {} {
 725    global canvy canvy0 lineno numcommits nextcolor linespc
 726    global mainline mainlinearrow sidelines
 727    global nchildren ncleft
 728    global displist nhyperspace
 729
 730    allcanvs delete all
 731    set nextcolor 0
 732    set canvy $canvy0
 733    set lineno -1
 734    set numcommits 0
 735    catch {unset mainline}
 736    catch {unset mainlinearrow}
 737    catch {unset sidelines}
 738    foreach id [array names nchildren] {
 739        set ncleft($id) $nchildren($id)
 740    }
 741    set displist {}
 742    set nhyperspace 0
 743}
 744
 745proc bindline {t id} {
 746    global canv
 747
 748    $canv bind $t <Enter> "lineenter %x %y $id"
 749    $canv bind $t <Motion> "linemotion %x %y $id"
 750    $canv bind $t <Leave> "lineleave $id"
 751    $canv bind $t <Button-1> "lineclick %x %y $id 1"
 752}
 753
 754proc drawlines {id xtra delold} {
 755    global mainline mainlinearrow sidelines lthickness colormap canv
 756
 757    if {$delold} {
 758        $canv delete lines.$id
 759    }
 760    if {[info exists mainline($id)]} {
 761        set t [$canv create line $mainline($id) \
 762                   -width [expr {($xtra + 1) * $lthickness}] \
 763                   -fill $colormap($id) -tags lines.$id \
 764                   -arrow $mainlinearrow($id)]
 765        $canv lower $t
 766        bindline $t $id
 767    }
 768    if {[info exists sidelines($id)]} {
 769        foreach ls $sidelines($id) {
 770            set coords [lindex $ls 0]
 771            set thick [lindex $ls 1]
 772            set arrow [lindex $ls 2]
 773            set t [$canv create line $coords -fill $colormap($id) \
 774                       -width [expr {($thick + $xtra) * $lthickness}] \
 775                       -arrow $arrow -tags lines.$id]
 776            $canv lower $t
 777            bindline $t $id
 778        }
 779    }
 780}
 781
 782# level here is an index in displist
 783proc drawcommitline {level} {
 784    global parents children nparents displist
 785    global canv canv2 canv3 mainfont namefont canvy linespc
 786    global lineid linehtag linentag linedtag commitinfo
 787    global colormap numcommits currentparents dupparents
 788    global idtags idline idheads idotherrefs
 789    global lineno lthickness mainline mainlinearrow sidelines
 790    global commitlisted rowtextx idpos lastuse displist
 791    global oldnlines olddlevel olddisplist
 792
 793    incr numcommits
 794    incr lineno
 795    set id [lindex $displist $level]
 796    set lastuse($id) $lineno
 797    set lineid($lineno) $id
 798    set idline($id) $lineno
 799    set ofill [expr {[info exists commitlisted($id)]? "blue": "white"}]
 800    if {![info exists commitinfo($id)]} {
 801        readcommit $id
 802        if {![info exists commitinfo($id)]} {
 803            set commitinfo($id) {"No commit information available"}
 804            set nparents($id) 0
 805        }
 806    }
 807    assigncolor $id
 808    set currentparents {}
 809    set dupparents {}
 810    if {[info exists commitlisted($id)] && [info exists parents($id)]} {
 811        foreach p $parents($id) {
 812            if {[lsearch -exact $currentparents $p] < 0} {
 813                lappend currentparents $p
 814            } else {
 815                # remember that this parent was listed twice
 816                lappend dupparents $p
 817            }
 818        }
 819    }
 820    set x [xcoord $level $level $lineno]
 821    set y1 $canvy
 822    set canvy [expr $canvy + $linespc]
 823    allcanvs conf -scrollregion \
 824        [list 0 0 0 [expr $y1 + 0.5 * $linespc + 2]]
 825    if {[info exists mainline($id)]} {
 826        lappend mainline($id) $x $y1
 827        if {$mainlinearrow($id) ne "none"} {
 828            set mainline($id) [trimdiagstart $mainline($id)]
 829        }
 830    }
 831    drawlines $id 0 0
 832    set orad [expr {$linespc / 3}]
 833    set t [$canv create oval [expr $x - $orad] [expr $y1 - $orad] \
 834               [expr $x + $orad - 1] [expr $y1 + $orad - 1] \
 835               -fill $ofill -outline black -width 1]
 836    $canv raise $t
 837    $canv bind $t <1> {selcanvline {} %x %y}
 838    set xt [xcoord [llength $displist] $level $lineno]
 839    if {[llength $currentparents] > 2} {
 840        set xt [expr {$xt + ([llength $currentparents] - 2) * $linespc}]
 841    }
 842    set rowtextx($lineno) $xt
 843    set idpos($id) [list $x $xt $y1]
 844    if {[info exists idtags($id)] || [info exists idheads($id)]
 845        || [info exists idotherrefs($id)]} {
 846        set xt [drawtags $id $x $xt $y1]
 847    }
 848    set headline [lindex $commitinfo($id) 0]
 849    set name [lindex $commitinfo($id) 1]
 850    set date [lindex $commitinfo($id) 2]
 851    set date [formatdate $date]
 852    set linehtag($lineno) [$canv create text $xt $y1 -anchor w \
 853                               -text $headline -font $mainfont ]
 854    $canv bind $linehtag($lineno) <Button-3> "rowmenu %X %Y $id"
 855    set linentag($lineno) [$canv2 create text 3 $y1 -anchor w \
 856                               -text $name -font $namefont]
 857    set linedtag($lineno) [$canv3 create text 3 $y1 -anchor w \
 858                               -text $date -font $mainfont]
 859
 860    set olddlevel $level
 861    set olddisplist $displist
 862    set oldnlines [llength $displist]
 863}
 864
 865proc drawtags {id x xt y1} {
 866    global idtags idheads idotherrefs
 867    global linespc lthickness
 868    global canv mainfont idline rowtextx
 869
 870    set marks {}
 871    set ntags 0
 872    set nheads 0
 873    if {[info exists idtags($id)]} {
 874        set marks $idtags($id)
 875        set ntags [llength $marks]
 876    }
 877    if {[info exists idheads($id)]} {
 878        set marks [concat $marks $idheads($id)]
 879        set nheads [llength $idheads($id)]
 880    }
 881    if {[info exists idotherrefs($id)]} {
 882        set marks [concat $marks $idotherrefs($id)]
 883    }
 884    if {$marks eq {}} {
 885        return $xt
 886    }
 887
 888    set delta [expr {int(0.5 * ($linespc - $lthickness))}]
 889    set yt [expr $y1 - 0.5 * $linespc]
 890    set yb [expr $yt + $linespc - 1]
 891    set xvals {}
 892    set wvals {}
 893    foreach tag $marks {
 894        set wid [font measure $mainfont $tag]
 895        lappend xvals $xt
 896        lappend wvals $wid
 897        set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}]
 898    }
 899    set t [$canv create line $x $y1 [lindex $xvals end] $y1 \
 900               -width $lthickness -fill black -tags tag.$id]
 901    $canv lower $t
 902    foreach tag $marks x $xvals wid $wvals {
 903        set xl [expr $x + $delta]
 904        set xr [expr $x + $delta + $wid + $lthickness]
 905        if {[incr ntags -1] >= 0} {
 906            # draw a tag
 907            set t [$canv create polygon $x [expr $yt + $delta] $xl $yt \
 908                       $xr $yt $xr $yb $xl $yb $x [expr $yb - $delta] \
 909                       -width 1 -outline black -fill yellow -tags tag.$id]
 910            $canv bind $t <1> [list showtag $tag 1]
 911            set rowtextx($idline($id)) [expr {$xr + $linespc}]
 912        } else {
 913            # draw a head or other ref
 914            if {[incr nheads -1] >= 0} {
 915                set col green
 916            } else {
 917                set col "#ddddff"
 918            }
 919            set xl [expr $xl - $delta/2]
 920            $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \
 921                -width 1 -outline black -fill $col -tags tag.$id
 922        }
 923        set t [$canv create text $xl $y1 -anchor w -text $tag \
 924                   -font $mainfont -tags tag.$id]
 925        if {$ntags >= 0} {
 926            $canv bind $t <1> [list showtag $tag 1]
 927        }
 928    }
 929    return $xt
 930}
 931
 932proc notecrossings {id lo hi corner} {
 933    global olddisplist crossings cornercrossings
 934
 935    for {set i $lo} {[incr i] < $hi} {} {
 936        set p [lindex $olddisplist $i]
 937        if {$p == {}} continue
 938        if {$i == $corner} {
 939            if {![info exists cornercrossings($id)]
 940                || [lsearch -exact $cornercrossings($id) $p] < 0} {
 941                lappend cornercrossings($id) $p
 942            }
 943            if {![info exists cornercrossings($p)]
 944                || [lsearch -exact $cornercrossings($p) $id] < 0} {
 945                lappend cornercrossings($p) $id
 946            }
 947        } else {
 948            if {![info exists crossings($id)]
 949                || [lsearch -exact $crossings($id) $p] < 0} {
 950                lappend crossings($id) $p
 951            }
 952            if {![info exists crossings($p)]
 953                || [lsearch -exact $crossings($p) $id] < 0} {
 954                lappend crossings($p) $id
 955            }
 956        }
 957    }
 958}
 959
 960proc xcoord {i level ln} {
 961    global canvx0 xspc1 xspc2
 962
 963    set x [expr {$canvx0 + $i * $xspc1($ln)}]
 964    if {$i > 0 && $i == $level} {
 965        set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}]
 966    } elseif {$i > $level} {
 967        set x [expr {$x + $xspc2 - $xspc1($ln)}]
 968    }
 969    return $x
 970}
 971
 972# it seems Tk can't draw arrows on the end of diagonal line segments...
 973proc trimdiagend {line} {
 974    while {[llength $line] > 4} {
 975        set x1 [lindex $line end-3]
 976        set y1 [lindex $line end-2]
 977        set x2 [lindex $line end-1]
 978        set y2 [lindex $line end]
 979        if {($x1 == $x2) != ($y1 == $y2)} break
 980        set line [lreplace $line end-1 end]
 981    }
 982    return $line
 983}
 984
 985proc trimdiagstart {line} {
 986    while {[llength $line] > 4} {
 987        set x1 [lindex $line 0]
 988        set y1 [lindex $line 1]
 989        set x2 [lindex $line 2]
 990        set y2 [lindex $line 3]
 991        if {($x1 == $x2) != ($y1 == $y2)} break
 992        set line [lreplace $line 0 1]
 993    }
 994    return $line
 995}
 996
 997proc drawslants {id needonscreen nohs} {
 998    global canv mainline mainlinearrow sidelines
 999    global canvx0 canvy xspc1 xspc2 lthickness
1000    global currentparents dupparents
1001    global lthickness linespc canvy colormap lineno geometry
1002    global maxgraphpct maxwidth
1003    global displist onscreen lastuse
1004    global parents commitlisted
1005    global oldnlines olddlevel olddisplist
1006    global nhyperspace numcommits nnewparents
1007
1008    if {$lineno < 0} {
1009        lappend displist $id
1010        set onscreen($id) 1
1011        return 0
1012    }
1013
1014    set y1 [expr {$canvy - $linespc}]
1015    set y2 $canvy
1016
1017    # work out what we need to get back on screen
1018    set reins {}
1019    if {$onscreen($id) < 0} {
1020        # next to do isn't displayed, better get it on screen...
1021        lappend reins [list $id 0]
1022    }
1023    # make sure all the previous commits's parents are on the screen
1024    foreach p $currentparents {
1025        if {$onscreen($p) < 0} {
1026            lappend reins [list $p 0]
1027        }
1028    }
1029    # bring back anything requested by caller
1030    if {$needonscreen ne {}} {
1031        lappend reins $needonscreen
1032    }
1033
1034    # try the shortcut
1035    if {$currentparents == $id && $onscreen($id) == 0 && $reins eq {}} {
1036        set dlevel $olddlevel
1037        set x [xcoord $dlevel $dlevel $lineno]
1038        set mainline($id) [list $x $y1]
1039        set mainlinearrow($id) none
1040        set lastuse($id) $lineno
1041        set displist [lreplace $displist $dlevel $dlevel $id]
1042        set onscreen($id) 1
1043        set xspc1([expr {$lineno + 1}]) $xspc1($lineno)
1044        return $dlevel
1045    }
1046
1047    # update displist
1048    set displist [lreplace $displist $olddlevel $olddlevel]
1049    set j $olddlevel
1050    foreach p $currentparents {
1051        set lastuse($p) $lineno
1052        if {$onscreen($p) == 0} {
1053            set displist [linsert $displist $j $p]
1054            set onscreen($p) 1
1055            incr j
1056        }
1057    }
1058    if {$onscreen($id) == 0} {
1059        lappend displist $id
1060        set onscreen($id) 1
1061    }
1062
1063    # remove the null entry if present
1064    set nullentry [lsearch -exact $displist {}]
1065    if {$nullentry >= 0} {
1066        set displist [lreplace $displist $nullentry $nullentry]
1067    }
1068
1069    # bring back the ones we need now (if we did it earlier
1070    # it would change displist and invalidate olddlevel)
1071    foreach pi $reins {
1072        # test again in case of duplicates in reins
1073        set p [lindex $pi 0]
1074        if {$onscreen($p) < 0} {
1075            set onscreen($p) 1
1076            set lastuse($p) $lineno
1077            set displist [linsert $displist [lindex $pi 1] $p]
1078            incr nhyperspace -1
1079        }
1080    }
1081
1082    set lastuse($id) $lineno
1083
1084    # see if we need to make any lines jump off into hyperspace
1085    set displ [llength $displist]
1086    if {$displ > $maxwidth} {
1087        set ages {}
1088        foreach x $displist {
1089            lappend ages [list $lastuse($x) $x]
1090        }
1091        set ages [lsort -integer -index 0 $ages]
1092        set k 0
1093        while {$displ > $maxwidth} {
1094            set use [lindex $ages $k 0]
1095            set victim [lindex $ages $k 1]
1096            if {$use >= $lineno - 5} break
1097            incr k
1098            if {[lsearch -exact $nohs $victim] >= 0} continue
1099            set i [lsearch -exact $displist $victim]
1100            set displist [lreplace $displist $i $i]
1101            set onscreen($victim) -1
1102            incr nhyperspace
1103            incr displ -1
1104            if {$i < $nullentry} {
1105                incr nullentry -1
1106            }
1107            set x [lindex $mainline($victim) end-1]
1108            lappend mainline($victim) $x $y1
1109            set line [trimdiagend $mainline($victim)]
1110            set arrow "last"
1111            if {$mainlinearrow($victim) ne "none"} {
1112                set line [trimdiagstart $line]
1113                set arrow "both"
1114            }
1115            lappend sidelines($victim) [list $line 1 $arrow]
1116            unset mainline($victim)
1117        }
1118    }
1119
1120    set dlevel [lsearch -exact $displist $id]
1121
1122    # If we are reducing, put in a null entry
1123    if {$displ < $oldnlines} {
1124        # does the next line look like a merge?
1125        # i.e. does it have > 1 new parent?
1126        if {$nnewparents($id) > 1} {
1127            set i [expr {$dlevel + 1}]
1128        } elseif {$nnewparents([lindex $olddisplist $olddlevel]) == 0} {
1129            set i $olddlevel
1130            if {$nullentry >= 0 && $nullentry < $i} {
1131                incr i -1
1132            }
1133        } elseif {$nullentry >= 0} {
1134            set i $nullentry
1135            while {$i < $displ
1136                   && [lindex $olddisplist $i] == [lindex $displist $i]} {
1137                incr i
1138            }
1139        } else {
1140            set i $olddlevel
1141            if {$dlevel >= $i} {
1142                incr i
1143            }
1144        }
1145        if {$i < $displ} {
1146            set displist [linsert $displist $i {}]
1147            incr displ
1148            if {$dlevel >= $i} {
1149                incr dlevel
1150            }
1151        }
1152    }
1153
1154    # decide on the line spacing for the next line
1155    set lj [expr {$lineno + 1}]
1156    set maxw [expr {$maxgraphpct * $geometry(canv1) / 100}]
1157    if {$displ <= 1 || $canvx0 + $displ * $xspc2 <= $maxw} {
1158        set xspc1($lj) $xspc2
1159    } else {
1160        set xspc1($lj) [expr {($maxw - $canvx0 - $xspc2) / ($displ - 1)}]
1161        if {$xspc1($lj) < $lthickness} {
1162            set xspc1($lj) $lthickness
1163        }
1164    }
1165
1166    foreach idi $reins {
1167        set id [lindex $idi 0]
1168        set j [lsearch -exact $displist $id]
1169        set xj [xcoord $j $dlevel $lj]
1170        set mainline($id) [list $xj $y2]
1171        set mainlinearrow($id) first
1172    }
1173
1174    set i -1
1175    foreach id $olddisplist {
1176        incr i
1177        if {$id == {}} continue
1178        if {$onscreen($id) <= 0} continue
1179        set xi [xcoord $i $olddlevel $lineno]
1180        if {$i == $olddlevel} {
1181            foreach p $currentparents {
1182                set j [lsearch -exact $displist $p]
1183                set coords [list $xi $y1]
1184                set xj [xcoord $j $dlevel $lj]
1185                if {$xj < $xi - $linespc} {
1186                    lappend coords [expr {$xj + $linespc}] $y1
1187                    notecrossings $p $j $i [expr {$j + 1}]
1188                } elseif {$xj > $xi + $linespc} {
1189                    lappend coords [expr {$xj - $linespc}] $y1
1190                    notecrossings $p $i $j [expr {$j - 1}]
1191                }
1192                if {[lsearch -exact $dupparents $p] >= 0} {
1193                    # draw a double-width line to indicate the doubled parent
1194                    lappend coords $xj $y2
1195                    lappend sidelines($p) [list $coords 2 none]
1196                    if {![info exists mainline($p)]} {
1197                        set mainline($p) [list $xj $y2]
1198                        set mainlinearrow($p) none
1199                    }
1200                } else {
1201                    # normal case, no parent duplicated
1202                    set yb $y2
1203                    set dx [expr {abs($xi - $xj)}]
1204                    if {0 && $dx < $linespc} {
1205                        set yb [expr {$y1 + $dx}]
1206                    }
1207                    if {![info exists mainline($p)]} {
1208                        if {$xi != $xj} {
1209                            lappend coords $xj $yb
1210                        }
1211                        set mainline($p) $coords
1212                        set mainlinearrow($p) none
1213                    } else {
1214                        lappend coords $xj $yb
1215                        if {$yb < $y2} {
1216                            lappend coords $xj $y2
1217                        }
1218                        lappend sidelines($p) [list $coords 1 none]
1219                    }
1220                }
1221            }
1222        } else {
1223            set j $i
1224            if {[lindex $displist $i] != $id} {
1225                set j [lsearch -exact $displist $id]
1226            }
1227            if {$j != $i || $xspc1($lineno) != $xspc1($lj)
1228                || ($olddlevel < $i && $i < $dlevel)
1229                || ($dlevel < $i && $i < $olddlevel)} {
1230                set xj [xcoord $j $dlevel $lj]
1231                lappend mainline($id) $xi $y1 $xj $y2
1232            }
1233        }
1234    }
1235    return $dlevel
1236}
1237
1238# search for x in a list of lists
1239proc llsearch {llist x} {
1240    set i 0
1241    foreach l $llist {
1242        if {$l == $x || [lsearch -exact $l $x] >= 0} {
1243            return $i
1244        }
1245        incr i
1246    }
1247    return -1
1248}
1249
1250proc drawmore {reading} {
1251    global displayorder numcommits ncmupdate nextupdate
1252    global stopped nhyperspace parents commitlisted
1253    global maxwidth onscreen displist currentparents olddlevel
1254
1255    set n [llength $displayorder]
1256    while {$numcommits < $n} {
1257        set id [lindex $displayorder $numcommits]
1258        set ctxend [expr {$numcommits + 10}]
1259        if {!$reading && $ctxend > $n} {
1260            set ctxend $n
1261        }
1262        set dlist {}
1263        if {$numcommits > 0} {
1264            set dlist [lreplace $displist $olddlevel $olddlevel]
1265            set i $olddlevel
1266            foreach p $currentparents {
1267                if {$onscreen($p) == 0} {
1268                    set dlist [linsert $dlist $i $p]
1269                    incr i
1270                }
1271            }
1272        }
1273        set nohs {}
1274        set reins {}
1275        set isfat [expr {[llength $dlist] > $maxwidth}]
1276        if {$nhyperspace > 0 || $isfat} {
1277            if {$ctxend > $n} break
1278            # work out what to bring back and
1279            # what we want to don't want to send into hyperspace
1280            set room 1
1281            for {set k $numcommits} {$k < $ctxend} {incr k} {
1282                set x [lindex $displayorder $k]
1283                set i [llsearch $dlist $x]
1284                if {$i < 0} {
1285                    set i [llength $dlist]
1286                    lappend dlist $x
1287                }
1288                if {[lsearch -exact $nohs $x] < 0} {
1289                    lappend nohs $x
1290                }
1291                if {$reins eq {} && $onscreen($x) < 0 && $room} {
1292                    set reins [list $x $i]
1293                }
1294                set newp {}
1295                if {[info exists commitlisted($x)]} {
1296                    set right 0
1297                    foreach p $parents($x) {
1298                        if {[llsearch $dlist $p] < 0} {
1299                            lappend newp $p
1300                            if {[lsearch -exact $nohs $p] < 0} {
1301                                lappend nohs $p
1302                            }
1303                            if {$reins eq {} && $onscreen($p) < 0 && $room} {
1304                                set reins [list $p [expr {$i + $right}]]
1305                            }
1306                        }
1307                        set right 1
1308                    }
1309                }
1310                set l [lindex $dlist $i]
1311                if {[llength $l] == 1} {
1312                    set l $newp
1313                } else {
1314                    set j [lsearch -exact $l $x]
1315                    set l [concat [lreplace $l $j $j] $newp]
1316                }
1317                set dlist [lreplace $dlist $i $i $l]
1318                if {$room && $isfat && [llength $newp] <= 1} {
1319                    set room 0
1320                }
1321            }
1322        }
1323
1324        set dlevel [drawslants $id $reins $nohs]
1325        drawcommitline $dlevel
1326        if {[clock clicks -milliseconds] >= $nextupdate
1327            && $numcommits >= $ncmupdate} {
1328            doupdate $reading
1329            if {$stopped} break
1330        }
1331    }
1332}
1333
1334# level here is an index in todo
1335proc updatetodo {level noshortcut} {
1336    global ncleft todo nnewparents
1337    global commitlisted parents onscreen
1338
1339    set id [lindex $todo $level]
1340    set olds {}
1341    if {[info exists commitlisted($id)]} {
1342        foreach p $parents($id) {
1343            if {[lsearch -exact $olds $p] < 0} {
1344                lappend olds $p
1345            }
1346        }
1347    }
1348    if {!$noshortcut && [llength $olds] == 1} {
1349        set p [lindex $olds 0]
1350        if {$ncleft($p) == 1 && [lsearch -exact $todo $p] < 0} {
1351            set ncleft($p) 0
1352            set todo [lreplace $todo $level $level $p]
1353            set onscreen($p) 0
1354            set nnewparents($id) 1
1355            return 0
1356        }
1357    }
1358
1359    set todo [lreplace $todo $level $level]
1360    set i $level
1361    set n 0
1362    foreach p $olds {
1363        incr ncleft($p) -1
1364        set k [lsearch -exact $todo $p]
1365        if {$k < 0} {
1366            set todo [linsert $todo $i $p]
1367            set onscreen($p) 0
1368            incr i
1369            incr n
1370        }
1371    }
1372    set nnewparents($id) $n
1373
1374    return 1
1375}
1376
1377proc decidenext {{noread 0}} {
1378    global ncleft todo
1379    global datemode cdate
1380    global commitinfo
1381
1382    # choose which one to do next time around
1383    set todol [llength $todo]
1384    set level -1
1385    set latest {}
1386    for {set k $todol} {[incr k -1] >= 0} {} {
1387        set p [lindex $todo $k]
1388        if {$ncleft($p) == 0} {
1389            if {$datemode} {
1390                if {![info exists commitinfo($p)]} {
1391                    if {$noread} {
1392                        return {}
1393                    }
1394                    readcommit $p
1395                }
1396                if {$latest == {} || $cdate($p) > $latest} {
1397                    set level $k
1398                    set latest $cdate($p)
1399                }
1400            } else {
1401                set level $k
1402                break
1403            }
1404        }
1405    }
1406    if {$level < 0} {
1407        if {$todo != {}} {
1408            puts "ERROR: none of the pending commits can be done yet:"
1409            foreach p $todo {
1410                puts "  $p ($ncleft($p))"
1411            }
1412        }
1413        return -1
1414    }
1415
1416    return $level
1417}
1418
1419proc drawcommit {id} {
1420    global phase todo nchildren datemode nextupdate revlistorder
1421    global numcommits ncmupdate displayorder todo onscreen parents
1422
1423    if {$phase != "incrdraw"} {
1424        set phase incrdraw
1425        set displayorder {}
1426        set todo {}
1427        initgraph
1428    }
1429    if {$nchildren($id) == 0} {
1430        lappend todo $id
1431        set onscreen($id) 0
1432    }
1433    if {$revlistorder} {
1434        set level [lsearch -exact $todo $id]
1435        if {$level < 0} {
1436            error_popup "oops, $id isn't in todo"
1437            return
1438        }
1439        lappend displayorder $id
1440        updatetodo $level 0
1441    } else {
1442        set level [decidenext 1]
1443        if {$level == {} || $id != [lindex $todo $level]} {
1444            return
1445        }
1446        while 1 {
1447            lappend displayorder [lindex $todo $level]
1448            if {[updatetodo $level $datemode]} {
1449                set level [decidenext 1]
1450                if {$level == {}} break
1451            }
1452            set id [lindex $todo $level]
1453            if {![info exists commitlisted($id)]} {
1454                break
1455            }
1456        }
1457    }
1458    drawmore 1
1459}
1460
1461proc finishcommits {} {
1462    global phase
1463    global canv mainfont ctext maincursor textcursor
1464
1465    if {$phase != "incrdraw"} {
1466        $canv delete all
1467        $canv create text 3 3 -anchor nw -text "No commits selected" \
1468            -font $mainfont -tags textitems
1469        set phase {}
1470    } else {
1471        drawrest
1472    }
1473    . config -cursor $maincursor
1474    settextcursor $textcursor
1475}
1476
1477# Don't change the text pane cursor if it is currently the hand cursor,
1478# showing that we are over a sha1 ID link.
1479proc settextcursor {c} {
1480    global ctext curtextcursor
1481
1482    if {[$ctext cget -cursor] == $curtextcursor} {
1483        $ctext config -cursor $c
1484    }
1485    set curtextcursor $c
1486}
1487
1488proc drawgraph {} {
1489    global nextupdate startmsecs ncmupdate
1490    global displayorder onscreen
1491
1492    if {$displayorder == {}} return
1493    set startmsecs [clock clicks -milliseconds]
1494    set nextupdate [expr $startmsecs + 100]
1495    set ncmupdate 1
1496    initgraph
1497    foreach id $displayorder {
1498        set onscreen($id) 0
1499    }
1500    drawmore 0
1501}
1502
1503proc drawrest {} {
1504    global phase stopped redisplaying selectedline
1505    global datemode todo displayorder
1506    global numcommits ncmupdate
1507    global nextupdate startmsecs revlistorder
1508
1509    set level [decidenext]
1510    if {$level >= 0} {
1511        set phase drawgraph
1512        while 1 {
1513            lappend displayorder [lindex $todo $level]
1514            set hard [updatetodo $level $datemode]
1515            if {$hard} {
1516                set level [decidenext]
1517                if {$level < 0} break
1518            }
1519        }
1520    }
1521    drawmore 0
1522    set phase {}
1523    set drawmsecs [expr [clock clicks -milliseconds] - $startmsecs]
1524    #puts "overall $drawmsecs ms for $numcommits commits"
1525    if {$redisplaying} {
1526        if {$stopped == 0 && [info exists selectedline]} {
1527            selectline $selectedline 0
1528        }
1529        if {$stopped == 1} {
1530            set stopped 0
1531            after idle drawgraph
1532        } else {
1533            set redisplaying 0
1534        }
1535    }
1536}
1537
1538proc findmatches {f} {
1539    global findtype foundstring foundstrlen
1540    if {$findtype == "Regexp"} {
1541        set matches [regexp -indices -all -inline $foundstring $f]
1542    } else {
1543        if {$findtype == "IgnCase"} {
1544            set str [string tolower $f]
1545        } else {
1546            set str $f
1547        }
1548        set matches {}
1549        set i 0
1550        while {[set j [string first $foundstring $str $i]] >= 0} {
1551            lappend matches [list $j [expr $j+$foundstrlen-1]]
1552            set i [expr $j + $foundstrlen]
1553        }
1554    }
1555    return $matches
1556}
1557
1558proc dofind {} {
1559    global findtype findloc findstring markedmatches commitinfo
1560    global numcommits lineid linehtag linentag linedtag
1561    global mainfont namefont canv canv2 canv3 selectedline
1562    global matchinglines foundstring foundstrlen
1563
1564    stopfindproc
1565    unmarkmatches
1566    focus .
1567    set matchinglines {}
1568    if {$findloc == "Pickaxe"} {
1569        findpatches
1570        return
1571    }
1572    if {$findtype == "IgnCase"} {
1573        set foundstring [string tolower $findstring]
1574    } else {
1575        set foundstring $findstring
1576    }
1577    set foundstrlen [string length $findstring]
1578    if {$foundstrlen == 0} return
1579    if {$findloc == "Files"} {
1580        findfiles
1581        return
1582    }
1583    if {![info exists selectedline]} {
1584        set oldsel -1
1585    } else {
1586        set oldsel $selectedline
1587    }
1588    set didsel 0
1589    set fldtypes {Headline Author Date Committer CDate Comment}
1590    for {set l 0} {$l < $numcommits} {incr l} {
1591        set id $lineid($l)
1592        set info $commitinfo($id)
1593        set doesmatch 0
1594        foreach f $info ty $fldtypes {
1595            if {$findloc != "All fields" && $findloc != $ty} {
1596                continue
1597            }
1598            set matches [findmatches $f]
1599            if {$matches == {}} continue
1600            set doesmatch 1
1601            if {$ty == "Headline"} {
1602                markmatches $canv $l $f $linehtag($l) $matches $mainfont
1603            } elseif {$ty == "Author"} {
1604                markmatches $canv2 $l $f $linentag($l) $matches $namefont
1605            } elseif {$ty == "Date"} {
1606                markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
1607            }
1608        }
1609        if {$doesmatch} {
1610            lappend matchinglines $l
1611            if {!$didsel && $l > $oldsel} {
1612                findselectline $l
1613                set didsel 1
1614            }
1615        }
1616    }
1617    if {$matchinglines == {}} {
1618        bell
1619    } elseif {!$didsel} {
1620        findselectline [lindex $matchinglines 0]
1621    }
1622}
1623
1624proc findselectline {l} {
1625    global findloc commentend ctext
1626    selectline $l 1
1627    if {$findloc == "All fields" || $findloc == "Comments"} {
1628        # highlight the matches in the comments
1629        set f [$ctext get 1.0 $commentend]
1630        set matches [findmatches $f]
1631        foreach match $matches {
1632            set start [lindex $match 0]
1633            set end [expr [lindex $match 1] + 1]
1634            $ctext tag add found "1.0 + $start c" "1.0 + $end c"
1635        }
1636    }
1637}
1638
1639proc findnext {restart} {
1640    global matchinglines selectedline
1641    if {![info exists matchinglines]} {
1642        if {$restart} {
1643            dofind
1644        }
1645        return
1646    }
1647    if {![info exists selectedline]} return
1648    foreach l $matchinglines {
1649        if {$l > $selectedline} {
1650            findselectline $l
1651            return
1652        }
1653    }
1654    bell
1655}
1656
1657proc findprev {} {
1658    global matchinglines selectedline
1659    if {![info exists matchinglines]} {
1660        dofind
1661        return
1662    }
1663    if {![info exists selectedline]} return
1664    set prev {}
1665    foreach l $matchinglines {
1666        if {$l >= $selectedline} break
1667        set prev $l
1668    }
1669    if {$prev != {}} {
1670        findselectline $prev
1671    } else {
1672        bell
1673    }
1674}
1675
1676proc findlocchange {name ix op} {
1677    global findloc findtype findtypemenu
1678    if {$findloc == "Pickaxe"} {
1679        set findtype Exact
1680        set state disabled
1681    } else {
1682        set state normal
1683    }
1684    $findtypemenu entryconf 1 -state $state
1685    $findtypemenu entryconf 2 -state $state
1686}
1687
1688proc stopfindproc {{done 0}} {
1689    global findprocpid findprocfile findids
1690    global ctext findoldcursor phase maincursor textcursor
1691    global findinprogress
1692
1693    catch {unset findids}
1694    if {[info exists findprocpid]} {
1695        if {!$done} {
1696            catch {exec kill $findprocpid}
1697        }
1698        catch {close $findprocfile}
1699        unset findprocpid
1700    }
1701    if {[info exists findinprogress]} {
1702        unset findinprogress
1703        if {$phase != "incrdraw"} {
1704            . config -cursor $maincursor
1705            settextcursor $textcursor
1706        }
1707    }
1708}
1709
1710proc findpatches {} {
1711    global findstring selectedline numcommits
1712    global findprocpid findprocfile
1713    global finddidsel ctext lineid findinprogress
1714    global findinsertpos
1715
1716    if {$numcommits == 0} return
1717
1718    # make a list of all the ids to search, starting at the one
1719    # after the selected line (if any)
1720    if {[info exists selectedline]} {
1721        set l $selectedline
1722    } else {
1723        set l -1
1724    }
1725    set inputids {}
1726    for {set i 0} {$i < $numcommits} {incr i} {
1727        if {[incr l] >= $numcommits} {
1728            set l 0
1729        }
1730        append inputids $lineid($l) "\n"
1731    }
1732
1733    if {[catch {
1734        set f [open [list | git-diff-tree --stdin -s -r -S$findstring \
1735                         << $inputids] r]
1736    } err]} {
1737        error_popup "Error starting search process: $err"
1738        return
1739    }
1740
1741    set findinsertpos end
1742    set findprocfile $f
1743    set findprocpid [pid $f]
1744    fconfigure $f -blocking 0
1745    fileevent $f readable readfindproc
1746    set finddidsel 0
1747    . config -cursor watch
1748    settextcursor watch
1749    set findinprogress 1
1750}
1751
1752proc readfindproc {} {
1753    global findprocfile finddidsel
1754    global idline matchinglines findinsertpos
1755
1756    set n [gets $findprocfile line]
1757    if {$n < 0} {
1758        if {[eof $findprocfile]} {
1759            stopfindproc 1
1760            if {!$finddidsel} {
1761                bell
1762            }
1763        }
1764        return
1765    }
1766    if {![regexp {^[0-9a-f]{40}} $line id]} {
1767        error_popup "Can't parse git-diff-tree output: $line"
1768        stopfindproc
1769        return
1770    }
1771    if {![info exists idline($id)]} {
1772        puts stderr "spurious id: $id"
1773        return
1774    }
1775    set l $idline($id)
1776    insertmatch $l $id
1777}
1778
1779proc insertmatch {l id} {
1780    global matchinglines findinsertpos finddidsel
1781
1782    if {$findinsertpos == "end"} {
1783        if {$matchinglines != {} && $l < [lindex $matchinglines 0]} {
1784            set matchinglines [linsert $matchinglines 0 $l]
1785            set findinsertpos 1
1786        } else {
1787            lappend matchinglines $l
1788        }
1789    } else {
1790        set matchinglines [linsert $matchinglines $findinsertpos $l]
1791        incr findinsertpos
1792    }
1793    markheadline $l $id
1794    if {!$finddidsel} {
1795        findselectline $l
1796        set finddidsel 1
1797    }
1798}
1799
1800proc findfiles {} {
1801    global selectedline numcommits lineid ctext
1802    global ffileline finddidsel parents nparents
1803    global findinprogress findstartline findinsertpos
1804    global treediffs fdiffids fdiffsneeded fdiffpos
1805    global findmergefiles
1806
1807    if {$numcommits == 0} return
1808
1809    if {[info exists selectedline]} {
1810        set l [expr {$selectedline + 1}]
1811    } else {
1812        set l 0
1813    }
1814    set ffileline $l
1815    set findstartline $l
1816    set diffsneeded {}
1817    set fdiffsneeded {}
1818    while 1 {
1819        set id $lineid($l)
1820        if {$findmergefiles || $nparents($id) == 1} {
1821            foreach p $parents($id) {
1822                if {![info exists treediffs([list $id $p])]} {
1823                    append diffsneeded "$id $p\n"
1824                    lappend fdiffsneeded [list $id $p]
1825                }
1826            }
1827        }
1828        if {[incr l] >= $numcommits} {
1829            set l 0
1830        }
1831        if {$l == $findstartline} break
1832    }
1833
1834    # start off a git-diff-tree process if needed
1835    if {$diffsneeded ne {}} {
1836        if {[catch {
1837            set df [open [list | git-diff-tree -r --stdin << $diffsneeded] r]
1838        } err ]} {
1839            error_popup "Error starting search process: $err"
1840            return
1841        }
1842        catch {unset fdiffids}
1843        set fdiffpos 0
1844        fconfigure $df -blocking 0
1845        fileevent $df readable [list readfilediffs $df]
1846    }
1847
1848    set finddidsel 0
1849    set findinsertpos end
1850    set id $lineid($l)
1851    set p [lindex $parents($id) 0]
1852    . config -cursor watch
1853    settextcursor watch
1854    set findinprogress 1
1855    findcont [list $id $p]
1856    update
1857}
1858
1859proc readfilediffs {df} {
1860    global findids fdiffids fdiffs
1861
1862    set n [gets $df line]
1863    if {$n < 0} {
1864        if {[eof $df]} {
1865            donefilediff
1866            if {[catch {close $df} err]} {
1867                stopfindproc
1868                bell
1869                error_popup "Error in git-diff-tree: $err"
1870            } elseif {[info exists findids]} {
1871                set ids $findids
1872                stopfindproc
1873                bell
1874                error_popup "Couldn't find diffs for {$ids}"
1875            }
1876        }
1877        return
1878    }
1879    if {[regexp {^([0-9a-f]{40}) \(from ([0-9a-f]{40})\)} $line match id p]} {
1880        # start of a new string of diffs
1881        donefilediff
1882        set fdiffids [list $id $p]
1883        set fdiffs {}
1884    } elseif {[string match ":*" $line]} {
1885        lappend fdiffs [lindex $line 5]
1886    }
1887}
1888
1889proc donefilediff {} {
1890    global fdiffids fdiffs treediffs findids
1891    global fdiffsneeded fdiffpos
1892
1893    if {[info exists fdiffids]} {
1894        while {[lindex $fdiffsneeded $fdiffpos] ne $fdiffids
1895               && $fdiffpos < [llength $fdiffsneeded]} {
1896            # git-diff-tree doesn't output anything for a commit
1897            # which doesn't change anything
1898            set nullids [lindex $fdiffsneeded $fdiffpos]
1899            set treediffs($nullids) {}
1900            if {[info exists findids] && $nullids eq $findids} {
1901                unset findids
1902                findcont $nullids
1903            }
1904            incr fdiffpos
1905        }
1906        incr fdiffpos
1907
1908        if {![info exists treediffs($fdiffids)]} {
1909            set treediffs($fdiffids) $fdiffs
1910        }
1911        if {[info exists findids] && $fdiffids eq $findids} {
1912            unset findids
1913            findcont $fdiffids
1914        }
1915    }
1916}
1917
1918proc findcont {ids} {
1919    global findids treediffs parents nparents
1920    global ffileline findstartline finddidsel
1921    global lineid numcommits matchinglines findinprogress
1922    global findmergefiles
1923
1924    set id [lindex $ids 0]
1925    set p [lindex $ids 1]
1926    set pi [lsearch -exact $parents($id) $p]
1927    set l $ffileline
1928    while 1 {
1929        if {$findmergefiles || $nparents($id) == 1} {
1930            if {![info exists treediffs($ids)]} {
1931                set findids $ids
1932                set ffileline $l
1933                return
1934            }
1935            set doesmatch 0
1936            foreach f $treediffs($ids) {
1937                set x [findmatches $f]
1938                if {$x != {}} {
1939                    set doesmatch 1
1940                    break
1941                }
1942            }
1943            if {$doesmatch} {
1944                insertmatch $l $id
1945                set pi $nparents($id)
1946            }
1947        } else {
1948            set pi $nparents($id)
1949        }
1950        if {[incr pi] >= $nparents($id)} {
1951            set pi 0
1952            if {[incr l] >= $numcommits} {
1953                set l 0
1954            }
1955            if {$l == $findstartline} break
1956            set id $lineid($l)
1957        }
1958        set p [lindex $parents($id) $pi]
1959        set ids [list $id $p]
1960    }
1961    stopfindproc
1962    if {!$finddidsel} {
1963        bell
1964    }
1965}
1966
1967# mark a commit as matching by putting a yellow background
1968# behind the headline
1969proc markheadline {l id} {
1970    global canv mainfont linehtag commitinfo
1971
1972    set bbox [$canv bbox $linehtag($l)]
1973    set t [$canv create rect $bbox -outline {} -tags matches -fill yellow]
1974    $canv lower $t
1975}
1976
1977# mark the bits of a headline, author or date that match a find string
1978proc markmatches {canv l str tag matches font} {
1979    set bbox [$canv bbox $tag]
1980    set x0 [lindex $bbox 0]
1981    set y0 [lindex $bbox 1]
1982    set y1 [lindex $bbox 3]
1983    foreach match $matches {
1984        set start [lindex $match 0]
1985        set end [lindex $match 1]
1986        if {$start > $end} continue
1987        set xoff [font measure $font [string range $str 0 [expr $start-1]]]
1988        set xlen [font measure $font [string range $str 0 [expr $end]]]
1989        set t [$canv create rect [expr $x0+$xoff] $y0 [expr $x0+$xlen+2] $y1 \
1990                   -outline {} -tags matches -fill yellow]
1991        $canv lower $t
1992    }
1993}
1994
1995proc unmarkmatches {} {
1996    global matchinglines findids
1997    allcanvs delete matches
1998    catch {unset matchinglines}
1999    catch {unset findids}
2000}
2001
2002proc selcanvline {w x y} {
2003    global canv canvy0 ctext linespc
2004    global lineid linehtag linentag linedtag rowtextx
2005    set ymax [lindex [$canv cget -scrollregion] 3]
2006    if {$ymax == {}} return
2007    set yfrac [lindex [$canv yview] 0]
2008    set y [expr {$y + $yfrac * $ymax}]
2009    set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
2010    if {$l < 0} {
2011        set l 0
2012    }
2013    if {$w eq $canv} {
2014        if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
2015    }
2016    unmarkmatches
2017    selectline $l 1
2018}
2019
2020proc commit_descriptor {p} {
2021    global commitinfo
2022    set l "..."
2023    if {[info exists commitinfo($p)]} {
2024        set l [lindex $commitinfo($p) 0]
2025    }
2026    return "$p ($l)"
2027}
2028
2029# append some text to the ctext widget, and make any SHA1 ID
2030# that we know about be a clickable link.
2031proc appendwithlinks {text} {
2032    global ctext idline linknum
2033
2034    set start [$ctext index "end - 1c"]
2035    $ctext insert end $text
2036    $ctext insert end "\n"
2037    set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
2038    foreach l $links {
2039        set s [lindex $l 0]
2040        set e [lindex $l 1]
2041        set linkid [string range $text $s $e]
2042        if {![info exists idline($linkid)]} continue
2043        incr e
2044        $ctext tag add link "$start + $s c" "$start + $e c"
2045        $ctext tag add link$linknum "$start + $s c" "$start + $e c"
2046        $ctext tag bind link$linknum <1> [list selectline $idline($linkid) 1]
2047        incr linknum
2048    }
2049    $ctext tag conf link -foreground blue -underline 1
2050    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
2051    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
2052}
2053
2054proc selectline {l isnew} {
2055    global canv canv2 canv3 ctext commitinfo selectedline
2056    global lineid linehtag linentag linedtag
2057    global canvy0 linespc parents nparents children
2058    global cflist currentid sha1entry
2059    global commentend idtags idline linknum
2060
2061    $canv delete hover
2062    normalline
2063    if {![info exists lineid($l)] || ![info exists linehtag($l)]} return
2064    $canv delete secsel
2065    set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \
2066               -tags secsel -fill [$canv cget -selectbackground]]
2067    $canv lower $t
2068    $canv2 delete secsel
2069    set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \
2070               -tags secsel -fill [$canv2 cget -selectbackground]]
2071    $canv2 lower $t
2072    $canv3 delete secsel
2073    set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \
2074               -tags secsel -fill [$canv3 cget -selectbackground]]
2075    $canv3 lower $t
2076    set y [expr {$canvy0 + $l * $linespc}]
2077    set ymax [lindex [$canv cget -scrollregion] 3]
2078    set ytop [expr {$y - $linespc - 1}]
2079    set ybot [expr {$y + $linespc + 1}]
2080    set wnow [$canv yview]
2081    set wtop [expr [lindex $wnow 0] * $ymax]
2082    set wbot [expr [lindex $wnow 1] * $ymax]
2083    set wh [expr {$wbot - $wtop}]
2084    set newtop $wtop
2085    if {$ytop < $wtop} {
2086        if {$ybot < $wtop} {
2087            set newtop [expr {$y - $wh / 2.0}]
2088        } else {
2089            set newtop $ytop
2090            if {$newtop > $wtop - $linespc} {
2091                set newtop [expr {$wtop - $linespc}]
2092            }
2093        }
2094    } elseif {$ybot > $wbot} {
2095        if {$ytop > $wbot} {
2096            set newtop [expr {$y - $wh / 2.0}]
2097        } else {
2098            set newtop [expr {$ybot - $wh}]
2099            if {$newtop < $wtop + $linespc} {
2100                set newtop [expr {$wtop + $linespc}]
2101            }
2102        }
2103    }
2104    if {$newtop != $wtop} {
2105        if {$newtop < 0} {
2106            set newtop 0
2107        }
2108        allcanvs yview moveto [expr $newtop * 1.0 / $ymax]
2109    }
2110
2111    if {$isnew} {
2112        addtohistory [list selectline $l 0]
2113    }
2114
2115    set selectedline $l
2116
2117    set id $lineid($l)
2118    set currentid $id
2119    $sha1entry delete 0 end
2120    $sha1entry insert 0 $id
2121    $sha1entry selection from 0
2122    $sha1entry selection to end
2123
2124    $ctext conf -state normal
2125    $ctext delete 0.0 end
2126    set linknum 0
2127    $ctext mark set fmark.0 0.0
2128    $ctext mark gravity fmark.0 left
2129    set info $commitinfo($id)
2130    set date [formatdate [lindex $info 2]]
2131    $ctext insert end "Author: [lindex $info 1]  $date\n"
2132    set date [formatdate [lindex $info 4]]
2133    $ctext insert end "Committer: [lindex $info 3]  $date\n"
2134    if {[info exists idtags($id)]} {
2135        $ctext insert end "Tags:"
2136        foreach tag $idtags($id) {
2137            $ctext insert end " $tag"
2138        }
2139        $ctext insert end "\n"
2140    }
2141 
2142    set comment {}
2143    if {[info exists parents($id)]} {
2144        foreach p $parents($id) {
2145            append comment "Parent: [commit_descriptor $p]\n"
2146        }
2147    }
2148    if {[info exists children($id)]} {
2149        foreach c $children($id) {
2150            append comment "Child:  [commit_descriptor $c]\n"
2151        }
2152    }
2153    append comment "\n"
2154    append comment [lindex $info 5]
2155
2156    # make anything that looks like a SHA1 ID be a clickable link
2157    appendwithlinks $comment
2158
2159    $ctext tag delete Comments
2160    $ctext tag remove found 1.0 end
2161    $ctext conf -state disabled
2162    set commentend [$ctext index "end - 1c"]
2163
2164    $cflist delete 0 end
2165    $cflist insert end "Comments"
2166    if {$nparents($id) == 1} {
2167        startdiff [concat $id $parents($id)]
2168    } elseif {$nparents($id) > 1} {
2169        mergediff $id
2170    }
2171}
2172
2173proc selnextline {dir} {
2174    global selectedline
2175    if {![info exists selectedline]} return
2176    set l [expr $selectedline + $dir]
2177    unmarkmatches
2178    selectline $l 1
2179}
2180
2181proc unselectline {} {
2182    global selectedline
2183
2184    catch {unset selectedline}
2185    allcanvs delete secsel
2186}
2187
2188proc addtohistory {cmd} {
2189    global history historyindex
2190
2191    if {$historyindex > 0
2192        && [lindex $history [expr {$historyindex - 1}]] == $cmd} {
2193        return
2194    }
2195
2196    if {$historyindex < [llength $history]} {
2197        set history [lreplace $history $historyindex end $cmd]
2198    } else {
2199        lappend history $cmd
2200    }
2201    incr historyindex
2202    if {$historyindex > 1} {
2203        .ctop.top.bar.leftbut conf -state normal
2204    } else {
2205        .ctop.top.bar.leftbut conf -state disabled
2206    }
2207    .ctop.top.bar.rightbut conf -state disabled
2208}
2209
2210proc goback {} {
2211    global history historyindex
2212
2213    if {$historyindex > 1} {
2214        incr historyindex -1
2215        set cmd [lindex $history [expr {$historyindex - 1}]]
2216        eval $cmd
2217        .ctop.top.bar.rightbut conf -state normal
2218    }
2219    if {$historyindex <= 1} {
2220        .ctop.top.bar.leftbut conf -state disabled
2221    }
2222}
2223
2224proc goforw {} {
2225    global history historyindex
2226
2227    if {$historyindex < [llength $history]} {
2228        set cmd [lindex $history $historyindex]
2229        incr historyindex
2230        eval $cmd
2231        .ctop.top.bar.leftbut conf -state normal
2232    }
2233    if {$historyindex >= [llength $history]} {
2234        .ctop.top.bar.rightbut conf -state disabled
2235    }
2236}
2237
2238proc mergediff {id} {
2239    global parents diffmergeid diffmergegca mergefilelist diffpindex
2240
2241    set diffmergeid $id
2242    set diffpindex -1
2243    set diffmergegca [findgca $parents($id)]
2244    if {[info exists mergefilelist($id)]} {
2245        if {$mergefilelist($id) ne {}} {
2246            showmergediff
2247        }
2248    } else {
2249        contmergediff {}
2250    }
2251}
2252
2253proc findgca {ids} {
2254    set gca {}
2255    foreach id $ids {
2256        if {$gca eq {}} {
2257            set gca $id
2258        } else {
2259            if {[catch {
2260                set gca [exec git-merge-base $gca $id]
2261            } err]} {
2262                return {}
2263            }
2264        }
2265    }
2266    return $gca
2267}
2268
2269proc contmergediff {ids} {
2270    global diffmergeid diffpindex parents nparents diffmergegca
2271    global treediffs mergefilelist diffids treepending
2272
2273    # diff the child against each of the parents, and diff
2274    # each of the parents against the GCA.
2275    while 1 {
2276        if {[lindex $ids 0] == $diffmergeid && $diffmergegca ne {}} {
2277            set ids [list [lindex $ids 1] $diffmergegca]
2278        } else {
2279            if {[incr diffpindex] >= $nparents($diffmergeid)} break
2280            set p [lindex $parents($diffmergeid) $diffpindex]
2281            set ids [list $diffmergeid $p]
2282        }
2283        if {![info exists treediffs($ids)]} {
2284            set diffids $ids
2285            if {![info exists treepending]} {
2286                gettreediffs $ids
2287            }
2288            return
2289        }
2290    }
2291
2292    # If a file in some parent is different from the child and also
2293    # different from the GCA, then it's interesting.
2294    # If we don't have a GCA, then a file is interesting if it is
2295    # different from the child in all the parents.
2296    if {$diffmergegca ne {}} {
2297        set files {}
2298        foreach p $parents($diffmergeid) {
2299            set gcadiffs $treediffs([list $p $diffmergegca])
2300            foreach f $treediffs([list $diffmergeid $p]) {
2301                if {[lsearch -exact $files $f] < 0
2302                    && [lsearch -exact $gcadiffs $f] >= 0} {
2303                    lappend files $f
2304                }
2305            }
2306        }
2307        set files [lsort $files]
2308    } else {
2309        set p [lindex $parents($diffmergeid) 0]
2310        set files $treediffs([list $diffmergeid $p])
2311        for {set i 1} {$i < $nparents($diffmergeid) && $files ne {}} {incr i} {
2312            set p [lindex $parents($diffmergeid) $i]
2313            set df $treediffs([list $diffmergeid $p])
2314            set nf {}
2315            foreach f $files {
2316                if {[lsearch -exact $df $f] >= 0} {
2317                    lappend nf $f
2318                }
2319            }
2320            set files $nf
2321        }
2322    }
2323
2324    set mergefilelist($diffmergeid) $files
2325    if {$files ne {}} {
2326        showmergediff
2327    }
2328}
2329
2330proc showmergediff {} {
2331    global cflist diffmergeid mergefilelist parents
2332    global diffopts diffinhunk currentfile currenthunk filelines
2333    global diffblocked groupfilelast mergefds groupfilenum grouphunks
2334
2335    set files $mergefilelist($diffmergeid)
2336    foreach f $files {
2337        $cflist insert end $f
2338    }
2339    set env(GIT_DIFF_OPTS) $diffopts
2340    set flist {}
2341    catch {unset currentfile}
2342    catch {unset currenthunk}
2343    catch {unset filelines}
2344    catch {unset groupfilenum}
2345    catch {unset grouphunks}
2346    set groupfilelast -1
2347    foreach p $parents($diffmergeid) {
2348        set cmd [list | git-diff-tree -p $p $diffmergeid]
2349        set cmd [concat $cmd $mergefilelist($diffmergeid)]
2350        if {[catch {set f [open $cmd r]} err]} {
2351            error_popup "Error getting diffs: $err"
2352            foreach f $flist {
2353                catch {close $f}
2354            }
2355            return
2356        }
2357        lappend flist $f
2358        set ids [list $diffmergeid $p]
2359        set mergefds($ids) $f
2360        set diffinhunk($ids) 0
2361        set diffblocked($ids) 0
2362        fconfigure $f -blocking 0
2363        fileevent $f readable [list getmergediffline $f $ids $diffmergeid]
2364    }
2365}
2366
2367proc getmergediffline {f ids id} {
2368    global diffmergeid diffinhunk diffoldlines diffnewlines
2369    global currentfile currenthunk
2370    global diffoldstart diffnewstart diffoldlno diffnewlno
2371    global diffblocked mergefilelist
2372    global noldlines nnewlines difflcounts filelines
2373
2374    set n [gets $f line]
2375    if {$n < 0} {
2376        if {![eof $f]} return
2377    }
2378
2379    if {!([info exists diffmergeid] && $diffmergeid == $id)} {
2380        if {$n < 0} {
2381            close $f
2382        }
2383        return
2384    }
2385
2386    if {$diffinhunk($ids) != 0} {
2387        set fi $currentfile($ids)
2388        if {$n > 0 && [regexp {^[-+ \\]} $line match]} {
2389            # continuing an existing hunk
2390            set line [string range $line 1 end]
2391            set p [lindex $ids 1]
2392            if {$match eq "-" || $match eq " "} {
2393                set filelines($p,$fi,$diffoldlno($ids)) $line
2394                incr diffoldlno($ids)
2395            }
2396            if {$match eq "+" || $match eq " "} {
2397                set filelines($id,$fi,$diffnewlno($ids)) $line
2398                incr diffnewlno($ids)
2399            }
2400            if {$match eq " "} {
2401                if {$diffinhunk($ids) == 2} {
2402                    lappend difflcounts($ids) \
2403                        [list $noldlines($ids) $nnewlines($ids)]
2404                    set noldlines($ids) 0
2405                    set diffinhunk($ids) 1
2406                }
2407                incr noldlines($ids)
2408            } elseif {$match eq "-" || $match eq "+"} {
2409                if {$diffinhunk($ids) == 1} {
2410                    lappend difflcounts($ids) [list $noldlines($ids)]
2411                    set noldlines($ids) 0
2412                    set nnewlines($ids) 0
2413                    set diffinhunk($ids) 2
2414                }
2415                if {$match eq "-"} {
2416                    incr noldlines($ids)
2417                } else {
2418                    incr nnewlines($ids)
2419                }
2420            }
2421            # and if it's \ No newline at end of line, then what?
2422            return
2423        }
2424        # end of a hunk
2425        if {$diffinhunk($ids) == 1 && $noldlines($ids) != 0} {
2426            lappend difflcounts($ids) [list $noldlines($ids)]
2427        } elseif {$diffinhunk($ids) == 2
2428                  && ($noldlines($ids) != 0 || $nnewlines($ids) != 0)} {
2429            lappend difflcounts($ids) [list $noldlines($ids) $nnewlines($ids)]
2430        }
2431        set currenthunk($ids) [list $currentfile($ids) \
2432                                   $diffoldstart($ids) $diffnewstart($ids) \
2433                                   $diffoldlno($ids) $diffnewlno($ids) \
2434                                   $difflcounts($ids)]
2435        set diffinhunk($ids) 0
2436        # -1 = need to block, 0 = unblocked, 1 = is blocked
2437        set diffblocked($ids) -1
2438        processhunks
2439        if {$diffblocked($ids) == -1} {
2440            fileevent $f readable {}
2441            set diffblocked($ids) 1
2442        }
2443    }
2444
2445    if {$n < 0} {
2446        # eof
2447        if {!$diffblocked($ids)} {
2448            close $f
2449            set currentfile($ids) [llength $mergefilelist($diffmergeid)]
2450            set currenthunk($ids) [list $currentfile($ids) 0 0 0 0 {}]
2451            processhunks
2452        }
2453    } elseif {[regexp {^diff --git a/(.*) b/} $line match fname]} {
2454        # start of a new file
2455        set currentfile($ids) \
2456            [lsearch -exact $mergefilelist($diffmergeid) $fname]
2457    } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
2458                   $line match f1l f1c f2l f2c rest]} {
2459        if {[info exists currentfile($ids)] && $currentfile($ids) >= 0} {
2460            # start of a new hunk
2461            if {$f1l == 0 && $f1c == 0} {
2462                set f1l 1
2463            }
2464            if {$f2l == 0 && $f2c == 0} {
2465                set f2l 1
2466            }
2467            set diffinhunk($ids) 1
2468            set diffoldstart($ids) $f1l
2469            set diffnewstart($ids) $f2l
2470            set diffoldlno($ids) $f1l
2471            set diffnewlno($ids) $f2l
2472            set difflcounts($ids) {}
2473            set noldlines($ids) 0
2474            set nnewlines($ids) 0
2475        }
2476    }
2477}
2478
2479proc processhunks {} {
2480    global diffmergeid parents nparents currenthunk
2481    global mergefilelist diffblocked mergefds
2482    global grouphunks grouplinestart grouplineend groupfilenum
2483
2484    set nfiles [llength $mergefilelist($diffmergeid)]
2485    while 1 {
2486        set fi $nfiles
2487        set lno 0
2488        # look for the earliest hunk
2489        foreach p $parents($diffmergeid) {
2490            set ids [list $diffmergeid $p]
2491            if {![info exists currenthunk($ids)]} return
2492            set i [lindex $currenthunk($ids) 0]
2493            set l [lindex $currenthunk($ids) 2]
2494            if {$i < $fi || ($i == $fi && $l < $lno)} {
2495                set fi $i
2496                set lno $l
2497                set pi $p
2498            }
2499        }
2500
2501        if {$fi < $nfiles} {
2502            set ids [list $diffmergeid $pi]
2503            set hunk $currenthunk($ids)
2504            unset currenthunk($ids)
2505            if {$diffblocked($ids) > 0} {
2506                fileevent $mergefds($ids) readable \
2507                    [list getmergediffline $mergefds($ids) $ids $diffmergeid]
2508            }
2509            set diffblocked($ids) 0
2510
2511            if {[info exists groupfilenum] && $groupfilenum == $fi
2512                && $lno <= $grouplineend} {
2513                # add this hunk to the pending group
2514                lappend grouphunks($pi) $hunk
2515                set endln [lindex $hunk 4]
2516                if {$endln > $grouplineend} {
2517                    set grouplineend $endln
2518                }
2519                continue
2520            }
2521        }
2522
2523        # succeeding stuff doesn't belong in this group, so
2524        # process the group now
2525        if {[info exists groupfilenum]} {
2526            processgroup
2527            unset groupfilenum
2528            unset grouphunks
2529        }
2530
2531        if {$fi >= $nfiles} break
2532
2533        # start a new group
2534        set groupfilenum $fi
2535        set grouphunks($pi) [list $hunk]
2536        set grouplinestart $lno
2537        set grouplineend [lindex $hunk 4]
2538    }
2539}
2540
2541proc processgroup {} {
2542    global groupfilelast groupfilenum difffilestart
2543    global mergefilelist diffmergeid ctext filelines
2544    global parents diffmergeid diffoffset
2545    global grouphunks grouplinestart grouplineend nparents
2546    global mergemax
2547
2548    $ctext conf -state normal
2549    set id $diffmergeid
2550    set f $groupfilenum
2551    if {$groupfilelast != $f} {
2552        $ctext insert end "\n"
2553        set here [$ctext index "end - 1c"]
2554        set difffilestart($f) $here
2555        set mark fmark.[expr {$f + 1}]
2556        $ctext mark set $mark $here
2557        $ctext mark gravity $mark left
2558        set header [lindex $mergefilelist($id) $f]
2559        set l [expr {(78 - [string length $header]) / 2}]
2560        set pad [string range "----------------------------------------" 1 $l]
2561        $ctext insert end "$pad $header $pad\n" filesep
2562        set groupfilelast $f
2563        foreach p $parents($id) {
2564            set diffoffset($p) 0
2565        }
2566    }
2567
2568    $ctext insert end "@@" msep
2569    set nlines [expr {$grouplineend - $grouplinestart}]
2570    set events {}
2571    set pnum 0
2572    foreach p $parents($id) {
2573        set startline [expr {$grouplinestart + $diffoffset($p)}]
2574        set ol $startline
2575        set nl $grouplinestart
2576        if {[info exists grouphunks($p)]} {
2577            foreach h $grouphunks($p) {
2578                set l [lindex $h 2]
2579                if {$nl < $l} {
2580                    for {} {$nl < $l} {incr nl} {
2581                        set filelines($p,$f,$ol) $filelines($id,$f,$nl)
2582                        incr ol
2583                    }
2584                }
2585                foreach chunk [lindex $h 5] {
2586                    if {[llength $chunk] == 2} {
2587                        set olc [lindex $chunk 0]
2588                        set nlc [lindex $chunk 1]
2589                        set nnl [expr {$nl + $nlc}]
2590                        lappend events [list $nl $nnl $pnum $olc $nlc]
2591                        incr ol $olc
2592                        set nl $nnl
2593                    } else {
2594                        incr ol [lindex $chunk 0]
2595                        incr nl [lindex $chunk 0]
2596                    }
2597                }
2598            }
2599        }
2600        if {$nl < $grouplineend} {
2601            for {} {$nl < $grouplineend} {incr nl} {
2602                set filelines($p,$f,$ol) $filelines($id,$f,$nl)
2603                incr ol
2604            }
2605        }
2606        set nlines [expr {$ol - $startline}]
2607        $ctext insert end " -$startline,$nlines" msep
2608        incr pnum
2609    }
2610
2611    set nlines [expr {$grouplineend - $grouplinestart}]
2612    $ctext insert end " +$grouplinestart,$nlines @@\n" msep
2613
2614    set events [lsort -integer -index 0 $events]
2615    set nevents [llength $events]
2616    set nmerge $nparents($diffmergeid)
2617    set l $grouplinestart
2618    for {set i 0} {$i < $nevents} {set i $j} {
2619        set nl [lindex $events $i 0]
2620        while {$l < $nl} {
2621            $ctext insert end " $filelines($id,$f,$l)\n"
2622            incr l
2623        }
2624        set e [lindex $events $i]
2625        set enl [lindex $e 1]
2626        set j $i
2627        set active {}
2628        while 1 {
2629            set pnum [lindex $e 2]
2630            set olc [lindex $e 3]
2631            set nlc [lindex $e 4]
2632            if {![info exists delta($pnum)]} {
2633                set delta($pnum) [expr {$olc - $nlc}]
2634                lappend active $pnum
2635            } else {
2636                incr delta($pnum) [expr {$olc - $nlc}]
2637            }
2638            if {[incr j] >= $nevents} break
2639            set e [lindex $events $j]
2640            if {[lindex $e 0] >= $enl} break
2641            if {[lindex $e 1] > $enl} {
2642                set enl [lindex $e 1]
2643            }
2644        }
2645        set nlc [expr {$enl - $l}]
2646        set ncol mresult
2647        set bestpn -1
2648        if {[llength $active] == $nmerge - 1} {
2649            # no diff for one of the parents, i.e. it's identical
2650            for {set pnum 0} {$pnum < $nmerge} {incr pnum} {
2651                if {![info exists delta($pnum)]} {
2652                    if {$pnum < $mergemax} {
2653                        lappend ncol m$pnum
2654                    } else {
2655                        lappend ncol mmax
2656                    }
2657                    break
2658                }
2659            }
2660        } elseif {[llength $active] == $nmerge} {
2661            # all parents are different, see if one is very similar
2662            set bestsim 30
2663            for {set pnum 0} {$pnum < $nmerge} {incr pnum} {
2664                set sim [similarity $pnum $l $nlc $f \
2665                             [lrange $events $i [expr {$j-1}]]]
2666                if {$sim > $bestsim} {
2667                    set bestsim $sim
2668                    set bestpn $pnum
2669                }
2670            }
2671            if {$bestpn >= 0} {
2672                lappend ncol m$bestpn
2673            }
2674        }
2675        set pnum -1
2676        foreach p $parents($id) {
2677            incr pnum
2678            if {![info exists delta($pnum)] || $pnum == $bestpn} continue
2679            set olc [expr {$nlc + $delta($pnum)}]
2680            set ol [expr {$l + $diffoffset($p)}]
2681            incr diffoffset($p) $delta($pnum)
2682            unset delta($pnum)
2683            for {} {$olc > 0} {incr olc -1} {
2684                $ctext insert end "-$filelines($p,$f,$ol)\n" m$pnum
2685                incr ol
2686            }
2687        }
2688        set endl [expr {$l + $nlc}]
2689        if {$bestpn >= 0} {
2690            # show this pretty much as a normal diff
2691            set p [lindex $parents($id) $bestpn]
2692            set ol [expr {$l + $diffoffset($p)}]
2693            incr diffoffset($p) $delta($bestpn)
2694            unset delta($bestpn)
2695            for {set k $i} {$k < $j} {incr k} {
2696                set e [lindex $events $k]
2697                if {[lindex $e 2] != $bestpn} continue
2698                set nl [lindex $e 0]
2699                set ol [expr {$ol + $nl - $l}]
2700                for {} {$l < $nl} {incr l} {
2701                    $ctext insert end "+$filelines($id,$f,$l)\n" $ncol
2702                }
2703                set c [lindex $e 3]
2704                for {} {$c > 0} {incr c -1} {
2705                    $ctext insert end "-$filelines($p,$f,$ol)\n" m$bestpn
2706                    incr ol
2707                }
2708                set nl [lindex $e 1]
2709                for {} {$l < $nl} {incr l} {
2710                    $ctext insert end "+$filelines($id,$f,$l)\n" mresult
2711                }
2712            }
2713        }
2714        for {} {$l < $endl} {incr l} {
2715            $ctext insert end "+$filelines($id,$f,$l)\n" $ncol
2716        }
2717    }
2718    while {$l < $grouplineend} {
2719        $ctext insert end " $filelines($id,$f,$l)\n"
2720        incr l
2721    }
2722    $ctext conf -state disabled
2723}
2724
2725proc similarity {pnum l nlc f events} {
2726    global diffmergeid parents diffoffset filelines
2727
2728    set id $diffmergeid
2729    set p [lindex $parents($id) $pnum]
2730    set ol [expr {$l + $diffoffset($p)}]
2731    set endl [expr {$l + $nlc}]
2732    set same 0
2733    set diff 0
2734    foreach e $events {
2735        if {[lindex $e 2] != $pnum} continue
2736        set nl [lindex $e 0]
2737        set ol [expr {$ol + $nl - $l}]
2738        for {} {$l < $nl} {incr l} {
2739            incr same [string length $filelines($id,$f,$l)]
2740            incr same
2741        }
2742        set oc [lindex $e 3]
2743        for {} {$oc > 0} {incr oc -1} {
2744            incr diff [string length $filelines($p,$f,$ol)]
2745            incr diff
2746            incr ol
2747        }
2748        set nl [lindex $e 1]
2749        for {} {$l < $nl} {incr l} {
2750            incr diff [string length $filelines($id,$f,$l)]
2751            incr diff
2752        }
2753    }
2754    for {} {$l < $endl} {incr l} {
2755        incr same [string length $filelines($id,$f,$l)]
2756        incr same
2757    }
2758    if {$same == 0} {
2759        return 0
2760    }
2761    return [expr {200 * $same / (2 * $same + $diff)}]
2762}
2763
2764proc startdiff {ids} {
2765    global treediffs diffids treepending diffmergeid
2766
2767    set diffids $ids
2768    catch {unset diffmergeid}
2769    if {![info exists treediffs($ids)]} {
2770        if {![info exists treepending]} {
2771            gettreediffs $ids
2772        }
2773    } else {
2774        addtocflist $ids
2775    }
2776}
2777
2778proc addtocflist {ids} {
2779    global treediffs cflist
2780    foreach f $treediffs($ids) {
2781        $cflist insert end $f
2782    }
2783    getblobdiffs $ids
2784}
2785
2786proc gettreediffs {ids} {
2787    global treediff parents treepending
2788    set treepending $ids
2789    set treediff {}
2790    set id [lindex $ids 0]
2791    if [catch {set gdtf [open "|git-diff-tree --no-commit-id -r $id" r]}] return
2792    fconfigure $gdtf -blocking 0
2793    fileevent $gdtf readable [list gettreediffline $gdtf $ids]
2794}
2795
2796proc gettreediffline {gdtf ids} {
2797    global treediff treediffs treepending diffids diffmergeid
2798
2799    set n [gets $gdtf line]
2800    if {$n < 0} {
2801        if {![eof $gdtf]} return
2802        close $gdtf
2803        set treediffs($ids) $treediff
2804        unset treepending
2805        if {$ids != $diffids} {
2806            gettreediffs $diffids
2807        } else {
2808            if {[info exists diffmergeid]} {
2809                contmergediff $ids
2810            } else {
2811                addtocflist $ids
2812            }
2813        }
2814        return
2815    }
2816    set file [lindex $line 5]
2817    lappend treediff $file
2818}
2819
2820proc getblobdiffs {ids} {
2821    global diffopts blobdifffd diffids env curdifftag curtagstart
2822    global difffilestart nextupdate diffinhdr treediffs
2823
2824    set id [lindex $ids 0]
2825    set env(GIT_DIFF_OPTS) $diffopts
2826    set cmd [list | git-diff-tree --no-commit-id -r -p -C $id]
2827    if {[catch {set bdf [open $cmd r]} err]} {
2828        puts "error getting diffs: $err"
2829        return
2830    }
2831    set diffinhdr 0
2832    fconfigure $bdf -blocking 0
2833    set blobdifffd($ids) $bdf
2834    set curdifftag Comments
2835    set curtagstart 0.0
2836    catch {unset difffilestart}
2837    fileevent $bdf readable [list getblobdiffline $bdf $diffids]
2838    set nextupdate [expr {[clock clicks -milliseconds] + 100}]
2839}
2840
2841proc getblobdiffline {bdf ids} {
2842    global diffids blobdifffd ctext curdifftag curtagstart
2843    global diffnexthead diffnextnote difffilestart
2844    global nextupdate diffinhdr treediffs
2845    global gaudydiff
2846
2847    set n [gets $bdf line]
2848    if {$n < 0} {
2849        if {[eof $bdf]} {
2850            close $bdf
2851            if {$ids == $diffids && $bdf == $blobdifffd($ids)} {
2852                $ctext tag add $curdifftag $curtagstart end
2853            }
2854        }
2855        return
2856    }
2857    if {$ids != $diffids || $bdf != $blobdifffd($ids)} {
2858        return
2859    }
2860    $ctext conf -state normal
2861    if {[regexp {^diff --git a/(.*) b/(.*)} $line match fname newname]} {
2862        # start of a new file
2863        $ctext insert end "\n"
2864        $ctext tag add $curdifftag $curtagstart end
2865        set curtagstart [$ctext index "end - 1c"]
2866        set header $newname
2867        set here [$ctext index "end - 1c"]
2868        set i [lsearch -exact $treediffs($diffids) $fname]
2869        if {$i >= 0} {
2870            set difffilestart($i) $here
2871            incr i
2872            $ctext mark set fmark.$i $here
2873            $ctext mark gravity fmark.$i left
2874        }
2875        if {$newname != $fname} {
2876            set i [lsearch -exact $treediffs($diffids) $newname]
2877            if {$i >= 0} {
2878                set difffilestart($i) $here
2879                incr i
2880                $ctext mark set fmark.$i $here
2881                $ctext mark gravity fmark.$i left
2882            }
2883        }
2884        set curdifftag "f:$fname"
2885        $ctext tag delete $curdifftag
2886        set l [expr {(78 - [string length $header]) / 2}]
2887        set pad [string range "----------------------------------------" 1 $l]
2888        $ctext insert end "$pad $header $pad\n" filesep
2889        set diffinhdr 1
2890    } elseif {[regexp {^(---|\+\+\+)} $line]} {
2891        set diffinhdr 0
2892    } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \
2893                   $line match f1l f1c f2l f2c rest]} {
2894        if {$gaudydiff} {
2895            $ctext insert end "\t" hunksep
2896            $ctext insert end "    $f1l    " d0 "    $f2l    " d1
2897            $ctext insert end "    $rest \n" hunksep
2898        } else {
2899            $ctext insert end "$line\n" hunksep
2900        }
2901        set diffinhdr 0
2902    } else {
2903        set x [string range $line 0 0]
2904        if {$x == "-" || $x == "+"} {
2905            set tag [expr {$x == "+"}]
2906            if {$gaudydiff} {
2907                set line [string range $line 1 end]
2908            }
2909            $ctext insert end "$line\n" d$tag
2910        } elseif {$x == " "} {
2911            if {$gaudydiff} {
2912                set line [string range $line 1 end]
2913            }
2914            $ctext insert end "$line\n"
2915        } elseif {$diffinhdr || $x == "\\"} {
2916            # e.g. "\ No newline at end of file"
2917            $ctext insert end "$line\n" filesep
2918        } else {
2919            # Something else we don't recognize
2920            if {$curdifftag != "Comments"} {
2921                $ctext insert end "\n"
2922                $ctext tag add $curdifftag $curtagstart end
2923                set curtagstart [$ctext index "end - 1c"]
2924                set curdifftag Comments
2925            }
2926            $ctext insert end "$line\n" filesep
2927        }
2928    }
2929    $ctext conf -state disabled
2930    if {[clock clicks -milliseconds] >= $nextupdate} {
2931        incr nextupdate 100
2932        fileevent $bdf readable {}
2933        update
2934        fileevent $bdf readable "getblobdiffline $bdf {$ids}"
2935    }
2936}
2937
2938proc nextfile {} {
2939    global difffilestart ctext
2940    set here [$ctext index @0,0]
2941    for {set i 0} {[info exists difffilestart($i)]} {incr i} {
2942        if {[$ctext compare $difffilestart($i) > $here]} {
2943            if {![info exists pos]
2944                || [$ctext compare $difffilestart($i) < $pos]} {
2945                set pos $difffilestart($i)
2946            }
2947        }
2948    }
2949    if {[info exists pos]} {
2950        $ctext yview $pos
2951    }
2952}
2953
2954proc listboxsel {} {
2955    global ctext cflist currentid
2956    if {![info exists currentid]} return
2957    set sel [lsort [$cflist curselection]]
2958    if {$sel eq {}} return
2959    set first [lindex $sel 0]
2960    catch {$ctext yview fmark.$first}
2961}
2962
2963proc setcoords {} {
2964    global linespc charspc canvx0 canvy0 mainfont
2965    global xspc1 xspc2 lthickness
2966
2967    set linespc [font metrics $mainfont -linespace]
2968    set charspc [font measure $mainfont "m"]
2969    set canvy0 [expr 3 + 0.5 * $linespc]
2970    set canvx0 [expr 3 + 0.5 * $linespc]
2971    set lthickness [expr {int($linespc / 9) + 1}]
2972    set xspc1(0) $linespc
2973    set xspc2 $linespc
2974}
2975
2976proc redisplay {} {
2977    global stopped redisplaying phase
2978    if {$stopped > 1} return
2979    if {$phase == "getcommits"} return
2980    set redisplaying 1
2981    if {$phase == "drawgraph" || $phase == "incrdraw"} {
2982        set stopped 1
2983    } else {
2984        drawgraph
2985    }
2986}
2987
2988proc incrfont {inc} {
2989    global mainfont namefont textfont ctext canv phase
2990    global stopped entries
2991    unmarkmatches
2992    set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]]
2993    set namefont [lreplace $namefont 1 1 [expr {[lindex $namefont 1] + $inc}]]
2994    set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]]
2995    setcoords
2996    $ctext conf -font $textfont
2997    $ctext tag conf filesep -font [concat $textfont bold]
2998    foreach e $entries {
2999        $e conf -font $mainfont
3000    }
3001    if {$phase == "getcommits"} {
3002        $canv itemconf textitems -font $mainfont
3003    }
3004    redisplay
3005}
3006
3007proc clearsha1 {} {
3008    global sha1entry sha1string
3009    if {[string length $sha1string] == 40} {
3010        $sha1entry delete 0 end
3011    }
3012}
3013
3014proc sha1change {n1 n2 op} {
3015    global sha1string currentid sha1but
3016    if {$sha1string == {}
3017        || ([info exists currentid] && $sha1string == $currentid)} {
3018        set state disabled
3019    } else {
3020        set state normal
3021    }
3022    if {[$sha1but cget -state] == $state} return
3023    if {$state == "normal"} {
3024        $sha1but conf -state normal -relief raised -text "Goto: "
3025    } else {
3026        $sha1but conf -state disabled -relief flat -text "SHA1 ID: "
3027    }
3028}
3029
3030proc gotocommit {} {
3031    global sha1string currentid idline tagids
3032    global lineid numcommits
3033
3034    if {$sha1string == {}
3035        || ([info exists currentid] && $sha1string == $currentid)} return
3036    if {[info exists tagids($sha1string)]} {
3037        set id $tagids($sha1string)
3038    } else {
3039        set id [string tolower $sha1string]
3040        if {[regexp {^[0-9a-f]{4,39}$} $id]} {
3041            set matches {}
3042            for {set l 0} {$l < $numcommits} {incr l} {
3043                if {[string match $id* $lineid($l)]} {
3044                    lappend matches $lineid($l)
3045                }
3046            }
3047            if {$matches ne {}} {
3048                if {[llength $matches] > 1} {
3049                    error_popup "Short SHA1 id $id is ambiguous"
3050                    return
3051                }
3052                set id [lindex $matches 0]
3053            }
3054        }
3055    }
3056    if {[info exists idline($id)]} {
3057        selectline $idline($id) 1
3058        return
3059    }
3060    if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} {
3061        set type "SHA1 id"
3062    } else {
3063        set type "Tag"
3064    }
3065    error_popup "$type $sha1string is not known"
3066}
3067
3068proc lineenter {x y id} {
3069    global hoverx hovery hoverid hovertimer
3070    global commitinfo canv
3071
3072    if {![info exists commitinfo($id)]} return
3073    set hoverx $x
3074    set hovery $y
3075    set hoverid $id
3076    if {[info exists hovertimer]} {
3077        after cancel $hovertimer
3078    }
3079    set hovertimer [after 500 linehover]
3080    $canv delete hover
3081}
3082
3083proc linemotion {x y id} {
3084    global hoverx hovery hoverid hovertimer
3085
3086    if {[info exists hoverid] && $id == $hoverid} {
3087        set hoverx $x
3088        set hovery $y
3089        if {[info exists hovertimer]} {
3090            after cancel $hovertimer
3091        }
3092        set hovertimer [after 500 linehover]
3093    }
3094}
3095
3096proc lineleave {id} {
3097    global hoverid hovertimer canv
3098
3099    if {[info exists hoverid] && $id == $hoverid} {
3100        $canv delete hover
3101        if {[info exists hovertimer]} {
3102            after cancel $hovertimer
3103            unset hovertimer
3104        }
3105        unset hoverid
3106    }
3107}
3108
3109proc linehover {} {
3110    global hoverx hovery hoverid hovertimer
3111    global canv linespc lthickness
3112    global commitinfo mainfont
3113
3114    set text [lindex $commitinfo($hoverid) 0]
3115    set ymax [lindex [$canv cget -scrollregion] 3]
3116    if {$ymax == {}} return
3117    set yfrac [lindex [$canv yview] 0]
3118    set x [expr {$hoverx + 2 * $linespc}]
3119    set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}]
3120    set x0 [expr {$x - 2 * $lthickness}]
3121    set y0 [expr {$y - 2 * $lthickness}]
3122    set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}]
3123    set y1 [expr {$y + $linespc + 2 * $lthickness}]
3124    set t [$canv create rectangle $x0 $y0 $x1 $y1 \
3125               -fill \#ffff80 -outline black -width 1 -tags hover]
3126    $canv raise $t
3127    set t [$canv create text $x $y -anchor nw -text $text -tags hover -font $mainfont]
3128    $canv raise $t
3129}
3130
3131proc clickisonarrow {id y} {
3132    global mainline mainlinearrow sidelines lthickness
3133
3134    set thresh [expr {2 * $lthickness + 6}]
3135    if {[info exists mainline($id)]} {
3136        if {$mainlinearrow($id) ne "none"} {
3137            if {abs([lindex $mainline($id) 1] - $y) < $thresh} {
3138                return "up"
3139            }
3140        }
3141    }
3142    if {[info exists sidelines($id)]} {
3143        foreach ls $sidelines($id) {
3144            set coords [lindex $ls 0]
3145            set arrow [lindex $ls 2]
3146            if {$arrow eq "first" || $arrow eq "both"} {
3147                if {abs([lindex $coords 1] - $y) < $thresh} {
3148                    return "up"
3149                }
3150            }
3151            if {$arrow eq "last" || $arrow eq "both"} {
3152                if {abs([lindex $coords end] - $y) < $thresh} {
3153                    return "down"
3154                }
3155            }
3156        }
3157    }
3158    return {}
3159}
3160
3161proc arrowjump {id dirn y} {
3162    global mainline sidelines canv canv2 canv3
3163
3164    set yt {}
3165    if {$dirn eq "down"} {
3166        if {[info exists mainline($id)]} {
3167            set y1 [lindex $mainline($id) 1]
3168            if {$y1 > $y} {
3169                set yt $y1
3170            }
3171        }
3172        if {[info exists sidelines($id)]} {
3173            foreach ls $sidelines($id) {
3174                set y1 [lindex $ls 0 1]
3175                if {$y1 > $y && ($yt eq {} || $y1 < $yt)} {
3176                    set yt $y1
3177                }
3178            }
3179        }
3180    } else {
3181        if {[info exists sidelines($id)]} {
3182            foreach ls $sidelines($id) {
3183                set y1 [lindex $ls 0 end]
3184                if {$y1 < $y && ($yt eq {} || $y1 > $yt)} {
3185                    set yt $y1
3186                }
3187            }
3188        }
3189    }
3190    if {$yt eq {}} return
3191    set ymax [lindex [$canv cget -scrollregion] 3]
3192    if {$ymax eq {} || $ymax <= 0} return
3193    set view [$canv yview]
3194    set yspan [expr {[lindex $view 1] - [lindex $view 0]}]
3195    set yfrac [expr {$yt / $ymax - $yspan / 2}]
3196    if {$yfrac < 0} {
3197        set yfrac 0
3198    }
3199    $canv yview moveto $yfrac
3200    $canv2 yview moveto $yfrac
3201    $canv3 yview moveto $yfrac
3202}
3203
3204proc lineclick {x y id isnew} {
3205    global ctext commitinfo children cflist canv thickerline
3206
3207    unmarkmatches
3208    unselectline
3209    normalline
3210    $canv delete hover
3211    # draw this line thicker than normal
3212    drawlines $id 1 1
3213    set thickerline $id
3214    if {$isnew} {
3215        set ymax [lindex [$canv cget -scrollregion] 3]
3216        if {$ymax eq {}} return
3217        set yfrac [lindex [$canv yview] 0]
3218        set y [expr {$y + $yfrac * $ymax}]
3219    }
3220    set dirn [clickisonarrow $id $y]
3221    if {$dirn ne {}} {
3222        arrowjump $id $dirn $y
3223        return
3224    }
3225
3226    if {$isnew} {
3227        addtohistory [list lineclick $x $y $id 0]
3228    }
3229    # fill the details pane with info about this line
3230    $ctext conf -state normal
3231    $ctext delete 0.0 end
3232    $ctext tag conf link -foreground blue -underline 1
3233    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
3234    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
3235    $ctext insert end "Parent:\t"
3236    $ctext insert end $id [list link link0]
3237    $ctext tag bind link0 <1> [list selbyid $id]
3238    set info $commitinfo($id)
3239    $ctext insert end "\n\t[lindex $info 0]\n"
3240    $ctext insert end "\tAuthor:\t[lindex $info 1]\n"
3241    set date [formatdate [lindex $info 2]]
3242    $ctext insert end "\tDate:\t$date\n"
3243    if {[info exists children($id)]} {
3244        $ctext insert end "\nChildren:"
3245        set i 0
3246        foreach child $children($id) {
3247            incr i
3248            set info $commitinfo($child)
3249            $ctext insert end "\n\t"
3250            $ctext insert end $child [list link link$i]
3251            $ctext tag bind link$i <1> [list selbyid $child]
3252            $ctext insert end "\n\t[lindex $info 0]"
3253            $ctext insert end "\n\tAuthor:\t[lindex $info 1]"
3254            set date [formatdate [lindex $info 2]]
3255            $ctext insert end "\n\tDate:\t$date\n"
3256        }
3257    }
3258    $ctext conf -state disabled
3259
3260    $cflist delete 0 end
3261}
3262
3263proc normalline {} {
3264    global thickerline
3265    if {[info exists thickerline]} {
3266        drawlines $thickerline 0 1
3267        unset thickerline
3268    }
3269}
3270
3271proc selbyid {id} {
3272    global idline
3273    if {[info exists idline($id)]} {
3274        selectline $idline($id) 1
3275    }
3276}
3277
3278proc mstime {} {
3279    global startmstime
3280    if {![info exists startmstime]} {
3281        set startmstime [clock clicks -milliseconds]
3282    }
3283    return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
3284}
3285
3286proc rowmenu {x y id} {
3287    global rowctxmenu idline selectedline rowmenuid
3288
3289    if {![info exists selectedline] || $idline($id) eq $selectedline} {
3290        set state disabled
3291    } else {
3292        set state normal
3293    }
3294    $rowctxmenu entryconfigure 0 -state $state
3295    $rowctxmenu entryconfigure 1 -state $state
3296    $rowctxmenu entryconfigure 2 -state $state
3297    set rowmenuid $id
3298    tk_popup $rowctxmenu $x $y
3299}
3300
3301proc diffvssel {dirn} {
3302    global rowmenuid selectedline lineid
3303
3304    if {![info exists selectedline]} return
3305    if {$dirn} {
3306        set oldid $lineid($selectedline)
3307        set newid $rowmenuid
3308    } else {
3309        set oldid $rowmenuid
3310        set newid $lineid($selectedline)
3311    }
3312    addtohistory [list doseldiff $oldid $newid]
3313    doseldiff $oldid $newid
3314}
3315
3316proc doseldiff {oldid newid} {
3317    global ctext cflist
3318    global commitinfo
3319
3320    $ctext conf -state normal
3321    $ctext delete 0.0 end
3322    $ctext mark set fmark.0 0.0
3323    $ctext mark gravity fmark.0 left
3324    $cflist delete 0 end
3325    $cflist insert end "Top"
3326    $ctext insert end "From "
3327    $ctext tag conf link -foreground blue -underline 1
3328    $ctext tag bind link <Enter> { %W configure -cursor hand2 }
3329    $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
3330    $ctext tag bind link0 <1> [list selbyid $oldid]
3331    $ctext insert end $oldid [list link link0]
3332    $ctext insert end "\n     "
3333    $ctext insert end [lindex $commitinfo($oldid) 0]
3334    $ctext insert end "\n\nTo   "
3335    $ctext tag bind link1 <1> [list selbyid $newid]
3336    $ctext insert end $newid [list link link1]
3337    $ctext insert end "\n     "
3338    $ctext insert end [lindex $commitinfo($newid) 0]
3339    $ctext insert end "\n"
3340    $ctext conf -state disabled
3341    $ctext tag delete Comments
3342    $ctext tag remove found 1.0 end
3343    startdiff [list $newid $oldid]
3344}
3345
3346proc mkpatch {} {
3347    global rowmenuid currentid commitinfo patchtop patchnum
3348
3349    if {![info exists currentid]} return
3350    set oldid $currentid
3351    set oldhead [lindex $commitinfo($oldid) 0]
3352    set newid $rowmenuid
3353    set newhead [lindex $commitinfo($newid) 0]
3354    set top .patch
3355    set patchtop $top
3356    catch {destroy $top}
3357    toplevel $top
3358    label $top.title -text "Generate patch"
3359    grid $top.title - -pady 10
3360    label $top.from -text "From:"
3361    entry $top.fromsha1 -width 40 -relief flat
3362    $top.fromsha1 insert 0 $oldid
3363    $top.fromsha1 conf -state readonly
3364    grid $top.from $top.fromsha1 -sticky w
3365    entry $top.fromhead -width 60 -relief flat
3366    $top.fromhead insert 0 $oldhead
3367    $top.fromhead conf -state readonly
3368    grid x $top.fromhead -sticky w
3369    label $top.to -text "To:"
3370    entry $top.tosha1 -width 40 -relief flat
3371    $top.tosha1 insert 0 $newid
3372    $top.tosha1 conf -state readonly
3373    grid $top.to $top.tosha1 -sticky w
3374    entry $top.tohead -width 60 -relief flat
3375    $top.tohead insert 0 $newhead
3376    $top.tohead conf -state readonly
3377    grid x $top.tohead -sticky w
3378    button $top.rev -text "Reverse" -command mkpatchrev -padx 5
3379    grid $top.rev x -pady 10
3380    label $top.flab -text "Output file:"
3381    entry $top.fname -width 60
3382    $top.fname insert 0 [file normalize "patch$patchnum.patch"]
3383    incr patchnum
3384    grid $top.flab $top.fname -sticky w
3385    frame $top.buts
3386    button $top.buts.gen -text "Generate" -command mkpatchgo
3387    button $top.buts.can -text "Cancel" -command mkpatchcan
3388    grid $top.buts.gen $top.buts.can
3389    grid columnconfigure $top.buts 0 -weight 1 -uniform a
3390    grid columnconfigure $top.buts 1 -weight 1 -uniform a
3391    grid $top.buts - -pady 10 -sticky ew
3392    focus $top.fname
3393}
3394
3395proc mkpatchrev {} {
3396    global patchtop
3397
3398    set oldid [$patchtop.fromsha1 get]
3399    set oldhead [$patchtop.fromhead get]
3400    set newid [$patchtop.tosha1 get]
3401    set newhead [$patchtop.tohead get]
3402    foreach e [list fromsha1 fromhead tosha1 tohead] \
3403            v [list $newid $newhead $oldid $oldhead] {
3404        $patchtop.$e conf -state normal
3405        $patchtop.$e delete 0 end
3406        $patchtop.$e insert 0 $v
3407        $patchtop.$e conf -state readonly
3408    }
3409}
3410
3411proc mkpatchgo {} {
3412    global patchtop
3413
3414    set oldid [$patchtop.fromsha1 get]
3415    set newid [$patchtop.tosha1 get]
3416    set fname [$patchtop.fname get]
3417    if {[catch {exec git-diff-tree -p $oldid $newid >$fname &} err]} {
3418        error_popup "Error creating patch: $err"
3419    }
3420    catch {destroy $patchtop}
3421    unset patchtop
3422}
3423
3424proc mkpatchcan {} {
3425    global patchtop
3426
3427    catch {destroy $patchtop}
3428    unset patchtop
3429}
3430
3431proc mktag {} {
3432    global rowmenuid mktagtop commitinfo
3433
3434    set top .maketag
3435    set mktagtop $top
3436    catch {destroy $top}
3437    toplevel $top
3438    label $top.title -text "Create tag"
3439    grid $top.title - -pady 10
3440    label $top.id -text "ID:"
3441    entry $top.sha1 -width 40 -relief flat
3442    $top.sha1 insert 0 $rowmenuid
3443    $top.sha1 conf -state readonly
3444    grid $top.id $top.sha1 -sticky w
3445    entry $top.head -width 60 -relief flat
3446    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
3447    $top.head conf -state readonly
3448    grid x $top.head -sticky w
3449    label $top.tlab -text "Tag name:"
3450    entry $top.tag -width 60
3451    grid $top.tlab $top.tag -sticky w
3452    frame $top.buts
3453    button $top.buts.gen -text "Create" -command mktaggo
3454    button $top.buts.can -text "Cancel" -command mktagcan
3455    grid $top.buts.gen $top.buts.can
3456    grid columnconfigure $top.buts 0 -weight 1 -uniform a
3457    grid columnconfigure $top.buts 1 -weight 1 -uniform a
3458    grid $top.buts - -pady 10 -sticky ew
3459    focus $top.tag
3460}
3461
3462proc domktag {} {
3463    global mktagtop env tagids idtags
3464
3465    set id [$mktagtop.sha1 get]
3466    set tag [$mktagtop.tag get]
3467    if {$tag == {}} {
3468        error_popup "No tag name specified"
3469        return
3470    }
3471    if {[info exists tagids($tag)]} {
3472        error_popup "Tag \"$tag\" already exists"
3473        return
3474    }
3475    if {[catch {
3476        set dir [gitdir]
3477        set fname [file join $dir "refs/tags" $tag]
3478        set f [open $fname w]
3479        puts $f $id
3480        close $f
3481    } err]} {
3482        error_popup "Error creating tag: $err"
3483        return
3484    }
3485
3486    set tagids($tag) $id
3487    lappend idtags($id) $tag
3488    redrawtags $id
3489}
3490
3491proc redrawtags {id} {
3492    global canv linehtag idline idpos selectedline
3493
3494    if {![info exists idline($id)]} return
3495    $canv delete tag.$id
3496    set xt [eval drawtags $id $idpos($id)]
3497    $canv coords $linehtag($idline($id)) $xt [lindex $idpos($id) 2]
3498    if {[info exists selectedline] && $selectedline == $idline($id)} {
3499        selectline $selectedline 0
3500    }
3501}
3502
3503proc mktagcan {} {
3504    global mktagtop
3505
3506    catch {destroy $mktagtop}
3507    unset mktagtop
3508}
3509
3510proc mktaggo {} {
3511    domktag
3512    mktagcan
3513}
3514
3515proc writecommit {} {
3516    global rowmenuid wrcomtop commitinfo wrcomcmd
3517
3518    set top .writecommit
3519    set wrcomtop $top
3520    catch {destroy $top}
3521    toplevel $top
3522    label $top.title -text "Write commit to file"
3523    grid $top.title - -pady 10
3524    label $top.id -text "ID:"
3525    entry $top.sha1 -width 40 -relief flat
3526    $top.sha1 insert 0 $rowmenuid
3527    $top.sha1 conf -state readonly
3528    grid $top.id $top.sha1 -sticky w
3529    entry $top.head -width 60 -relief flat
3530    $top.head insert 0 [lindex $commitinfo($rowmenuid) 0]
3531    $top.head conf -state readonly
3532    grid x $top.head -sticky w
3533    label $top.clab -text "Command:"
3534    entry $top.cmd -width 60 -textvariable wrcomcmd
3535    grid $top.clab $top.cmd -sticky w -pady 10
3536    label $top.flab -text "Output file:"
3537    entry $top.fname -width 60
3538    $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"]
3539    grid $top.flab $top.fname -sticky w
3540    frame $top.buts
3541    button $top.buts.gen -text "Write" -command wrcomgo
3542    button $top.buts.can -text "Cancel" -command wrcomcan
3543    grid $top.buts.gen $top.buts.can
3544    grid columnconfigure $top.buts 0 -weight 1 -uniform a
3545    grid columnconfigure $top.buts 1 -weight 1 -uniform a
3546    grid $top.buts - -pady 10 -sticky ew
3547    focus $top.fname
3548}
3549
3550proc wrcomgo {} {
3551    global wrcomtop
3552
3553    set id [$wrcomtop.sha1 get]
3554    set cmd "echo $id | [$wrcomtop.cmd get]"
3555    set fname [$wrcomtop.fname get]
3556    if {[catch {exec sh -c $cmd >$fname &} err]} {
3557        error_popup "Error writing commit: $err"
3558    }
3559    catch {destroy $wrcomtop}
3560    unset wrcomtop
3561}
3562
3563proc wrcomcan {} {
3564    global wrcomtop
3565
3566    catch {destroy $wrcomtop}
3567    unset wrcomtop
3568}
3569
3570proc listrefs {id} {
3571    global idtags idheads idotherrefs
3572
3573    set x {}
3574    if {[info exists idtags($id)]} {
3575        set x $idtags($id)
3576    }
3577    set y {}
3578    if {[info exists idheads($id)]} {
3579        set y $idheads($id)
3580    }
3581    set z {}
3582    if {[info exists idotherrefs($id)]} {
3583        set z $idotherrefs($id)
3584    }
3585    return [list $x $y $z]
3586}
3587
3588proc rereadrefs {} {
3589    global idtags idheads idotherrefs
3590    global tagids headids otherrefids
3591
3592    set refids [concat [array names idtags] \
3593                    [array names idheads] [array names idotherrefs]]
3594    foreach id $refids {
3595        if {![info exists ref($id)]} {
3596            set ref($id) [listrefs $id]
3597        }
3598    }
3599    foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
3600        catch {unset $v}
3601    }
3602    readrefs
3603    set refids [lsort -unique [concat $refids [array names idtags] \
3604                        [array names idheads] [array names idotherrefs]]]
3605    foreach id $refids {
3606        set v [listrefs $id]
3607        if {![info exists ref($id)] || $ref($id) != $v} {
3608            redrawtags $id
3609        }
3610    }
3611}
3612
3613proc showtag {tag isnew} {
3614    global ctext cflist tagcontents tagids linknum
3615
3616    if {$isnew} {
3617        addtohistory [list showtag $tag 0]
3618    }
3619    $ctext conf -state normal
3620    $ctext delete 0.0 end
3621    set linknum 0
3622    if {[info exists tagcontents($tag)]} {
3623        set text $tagcontents($tag)
3624    } else {
3625        set text "Tag: $tag\nId:  $tagids($tag)"
3626    }
3627    appendwithlinks $text
3628    $ctext conf -state disabled
3629    $cflist delete 0 end
3630}
3631
3632proc doquit {} {
3633    global stopped
3634    set stopped 100
3635    destroy .
3636}
3637
3638proc formatdate {d} {
3639    global hours nhours tfd fastdate
3640
3641    if {!$fastdate} {
3642        return [clock format $d -format "%Y-%m-%d %H:%M:%S"]
3643    }
3644    set hr [expr {$d / 3600}]
3645    set ms [expr {$d % 3600}]
3646    if {![info exists hours($hr)]} {
3647        set hours($hr) [clock format $d -format "%Y-%m-%d %H"]
3648        set nhours($hr) 0
3649    }
3650    incr nhours($hr)
3651    set minsec [format "%.2d:%.2d" [expr {$ms/60}] [expr {$ms%60}]]
3652    return "$hours($hr):$minsec"
3653}
3654
3655# defaults...
3656set datemode 0
3657set boldnames 0
3658set diffopts "-U 5 -p"
3659set wrcomcmd "git-diff-tree --stdin -p --pretty"
3660
3661set mainfont {Helvetica 9}
3662set textfont {Courier 9}
3663set findmergefiles 0
3664set gaudydiff 0
3665set maxgraphpct 50
3666set maxwidth 16
3667set revlistorder 0
3668set fastdate 0
3669
3670set colors {green red blue magenta darkgrey brown orange}
3671
3672catch {source ~/.gitk}
3673
3674set namefont $mainfont
3675if {$boldnames} {
3676    lappend namefont bold
3677}
3678
3679set revtreeargs {}
3680foreach arg $argv {
3681    switch -regexp -- $arg {
3682        "^$" { }
3683        "^-b" { set boldnames 1 }
3684        "^-d" { set datemode 1 }
3685        "^-r" { set revlistorder 1 }
3686        default {
3687            lappend revtreeargs $arg
3688        }
3689    }
3690}
3691
3692set history {}
3693set historyindex 0
3694
3695set stopped 0
3696set redisplaying 0
3697set stuffsaved 0
3698set patchnum 0
3699setcoords
3700makewindow
3701readrefs
3702getcommits $revtreeargs