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