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