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