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