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