+# it seems Tk can't draw arrows on the end of diagonal line segments...
+proc trimdiagend {line} {
+ while {[llength $line] > 4} {
+ set x1 [lindex $line end-3]
+ set y1 [lindex $line end-2]
+ set x2 [lindex $line end-1]
+ set y2 [lindex $line end]
+ if {($x1 == $x2) != ($y1 == $y2)} break
+ set line [lreplace $line end-1 end]
+ }
+ return $line
+}
+
+proc trimdiagstart {line} {
+ while {[llength $line] > 4} {
+ set x1 [lindex $line 0]
+ set y1 [lindex $line 1]
+ set x2 [lindex $line 2]
+ set y2 [lindex $line 3]
+ if {($x1 == $x2) != ($y1 == $y2)} break
+ set line [lreplace $line 0 1]
+ }
+ return $line
+}
+
+proc drawslants {id needonscreen nohs} {
+ global canv mainline mainlinearrow sidelines
+ global canvx0 canvy xspc1 xspc2 lthickness
+ global currentparents dupparents
+ global lthickness linespc canvy colormap lineno geometry
+ global maxgraphpct maxwidth
+ global displist onscreen lastuse
+ global parents commitlisted
+ global oldnlines olddlevel olddisplist
+ global nhyperspace numcommits nnewparents
+
+ if {$lineno < 0} {
+ lappend displist $id
+ set onscreen($id) 1
+ return 0
+ }
+
+ set y1 [expr {$canvy - $linespc}]
+ set y2 $canvy
+
+ # work out what we need to get back on screen
+ set reins {}
+ if {$onscreen($id) < 0} {
+ # next to do isn't displayed, better get it on screen...
+ lappend reins [list $id 0]
+ }
+ # make sure all the previous commits's parents are on the screen
+ foreach p $currentparents {
+ if {$onscreen($p) < 0} {
+ lappend reins [list $p 0]
+ }
+ }
+ # bring back anything requested by caller
+ if {$needonscreen ne {}} {
+ lappend reins $needonscreen
+ }
+
+ # try the shortcut
+ if {$currentparents == $id && $onscreen($id) == 0 && $reins eq {}} {
+ set dlevel $olddlevel
+ set x [xcoord $dlevel $dlevel $lineno]
+ set mainline($id) [list $x $y1]
+ set mainlinearrow($id) none
+ set lastuse($id) $lineno
+ set displist [lreplace $displist $dlevel $dlevel $id]
+ set onscreen($id) 1
+ set xspc1([expr {$lineno + 1}]) $xspc1($lineno)
+ return $dlevel
+ }
+
+ # update displist
+ set displist [lreplace $displist $olddlevel $olddlevel]
+ set j $olddlevel
+ foreach p $currentparents {
+ set lastuse($p) $lineno
+ if {$onscreen($p) == 0} {
+ set displist [linsert $displist $j $p]
+ set onscreen($p) 1
+ incr j
+ }
+ }
+ if {$onscreen($id) == 0} {
+ lappend displist $id
+ set onscreen($id) 1
+ }
+
+ # remove the null entry if present
+ set nullentry [lsearch -exact $displist {}]
+ if {$nullentry >= 0} {
+ set displist [lreplace $displist $nullentry $nullentry]
+ }
+
+ # bring back the ones we need now (if we did it earlier
+ # it would change displist and invalidate olddlevel)
+ foreach pi $reins {
+ # test again in case of duplicates in reins
+ set p [lindex $pi 0]
+ if {$onscreen($p) < 0} {
+ set onscreen($p) 1
+ set lastuse($p) $lineno
+ set displist [linsert $displist [lindex $pi 1] $p]
+ incr nhyperspace -1
+ }
+ }
+
+ set lastuse($id) $lineno
+
+ # see if we need to make any lines jump off into hyperspace
+ set displ [llength $displist]
+ if {$displ > $maxwidth} {
+ set ages {}
+ foreach x $displist {
+ lappend ages [list $lastuse($x) $x]
+ }
+ set ages [lsort -integer -index 0 $ages]
+ set k 0
+ while {$displ > $maxwidth} {
+ set use [lindex $ages $k 0]
+ set victim [lindex $ages $k 1]
+ if {$use >= $lineno - 5} break
+ incr k
+ if {[lsearch -exact $nohs $victim] >= 0} continue
+ set i [lsearch -exact $displist $victim]
+ set displist [lreplace $displist $i $i]
+ set onscreen($victim) -1
+ incr nhyperspace
+ incr displ -1
+ if {$i < $nullentry} {
+ incr nullentry -1
+ }
+ set x [lindex $mainline($victim) end-1]
+ lappend mainline($victim) $x $y1
+ set line [trimdiagend $mainline($victim)]
+ set arrow "last"
+ if {$mainlinearrow($victim) ne "none"} {
+ set line [trimdiagstart $line]
+ set arrow "both"
+ }
+ lappend sidelines($victim) [list $line 1 $arrow]
+ unset mainline($victim)
+ }
+ }
+
+ set dlevel [lsearch -exact $displist $id]
+
+ # If we are reducing, put in a null entry
+ if {$displ < $oldnlines} {
+ # does the next line look like a merge?
+ # i.e. does it have > 1 new parent?
+ if {$nnewparents($id) > 1} {
+ set i [expr {$dlevel + 1}]
+ } elseif {$nnewparents([lindex $olddisplist $olddlevel]) == 0} {
+ set i $olddlevel
+ if {$nullentry >= 0 && $nullentry < $i} {
+ incr i -1
+ }
+ } elseif {$nullentry >= 0} {
+ set i $nullentry
+ while {$i < $displ
+ && [lindex $olddisplist $i] == [lindex $displist $i]} {
+ incr i
+ }
+ } else {
+ set i $olddlevel
+ if {$dlevel >= $i} {
+ incr i
+ }
+ }
+ if {$i < $displ} {
+ set displist [linsert $displist $i {}]
+ incr displ
+ if {$dlevel >= $i} {
+ incr dlevel
+ }
+ }
+ }
+
+ # decide on the line spacing for the next line
+ set lj [expr {$lineno + 1}]
+ set maxw [expr {$maxgraphpct * $geometry(canv1) / 100}]
+ if {$displ <= 1 || $canvx0 + $displ * $xspc2 <= $maxw} {
+ set xspc1($lj) $xspc2
+ } else {
+ set xspc1($lj) [expr {($maxw - $canvx0 - $xspc2) / ($displ - 1)}]
+ if {$xspc1($lj) < $lthickness} {
+ set xspc1($lj) $lthickness
+ }
+ }
+
+ foreach idi $reins {
+ set id [lindex $idi 0]
+ set j [lsearch -exact $displist $id]
+ set xj [xcoord $j $dlevel $lj]
+ set mainline($id) [list $xj $y2]
+ set mainlinearrow($id) first
+ }
+
+ set i -1
+ foreach id $olddisplist {
+ incr i
+ if {$id == {}} continue
+ if {$onscreen($id) <= 0} continue
+ set xi [xcoord $i $olddlevel $lineno]
+ if {$i == $olddlevel} {
+ foreach p $currentparents {
+ set j [lsearch -exact $displist $p]
+ set coords [list $xi $y1]
+ set xj [xcoord $j $dlevel $lj]
+ if {$xj < $xi - $linespc} {
+ lappend coords [expr {$xj + $linespc}] $y1
+ notecrossings $p $j $i [expr {$j + 1}]
+ } elseif {$xj > $xi + $linespc} {
+ lappend coords [expr {$xj - $linespc}] $y1
+ notecrossings $p $i $j [expr {$j - 1}]
+ }
+ if {[lsearch -exact $dupparents $p] >= 0} {
+ # draw a double-width line to indicate the doubled parent
+ lappend coords $xj $y2
+ lappend sidelines($p) [list $coords 2 none]
+ if {![info exists mainline($p)]} {
+ set mainline($p) [list $xj $y2]
+ set mainlinearrow($p) none
+ }
+ } else {
+ # normal case, no parent duplicated
+ set yb $y2
+ set dx [expr {abs($xi - $xj)}]
+ if {0 && $dx < $linespc} {
+ set yb [expr {$y1 + $dx}]
+ }
+ if {![info exists mainline($p)]} {
+ if {$xi != $xj} {
+ lappend coords $xj $yb
+ }
+ set mainline($p) $coords
+ set mainlinearrow($p) none
+ } else {
+ lappend coords $xj $yb
+ if {$yb < $y2} {
+ lappend coords $xj $y2
+ }
+ lappend sidelines($p) [list $coords 1 none]
+ }
+ }
+ }
+ } else {
+ set j $i
+ if {[lindex $displist $i] != $id} {
+ set j [lsearch -exact $displist $id]
+ }
+ if {$j != $i || $xspc1($lineno) != $xspc1($lj)
+ || ($olddlevel < $i && $i < $dlevel)
+ || ($dlevel < $i && $i < $olddlevel)} {
+ set xj [xcoord $j $dlevel $lj]
+ lappend mainline($id) $xi $y1 $xj $y2
+ }
+ }
+ }
+ return $dlevel
+}
+
+# search for x in a list of lists
+proc llsearch {llist x} {
+ set i 0
+ foreach l $llist {
+ if {$l == $x || [lsearch -exact $l $x] >= 0} {
+ return $i
+ }
+ incr i
+ }
+ return -1
+}
+
+proc drawmore {reading} {
+ global displayorder numcommits ncmupdate nextupdate
+ global stopped nhyperspace parents commitlisted
+ global maxwidth onscreen displist currentparents olddlevel
+
+ set n [llength $displayorder]
+ while {$numcommits < $n} {
+ set id [lindex $displayorder $numcommits]
+ set ctxend [expr {$numcommits + 10}]
+ if {!$reading && $ctxend > $n} {
+ set ctxend $n
+ }
+ set dlist {}
+ if {$numcommits > 0} {
+ set dlist [lreplace $displist $olddlevel $olddlevel]
+ set i $olddlevel
+ foreach p $currentparents {
+ if {$onscreen($p) == 0} {
+ set dlist [linsert $dlist $i $p]
+ incr i
+ }
+ }
+ }
+ set nohs {}
+ set reins {}
+ set isfat [expr {[llength $dlist] > $maxwidth}]
+ if {$nhyperspace > 0 || $isfat} {
+ if {$ctxend > $n} break
+ # work out what to bring back and
+ # what we want to don't want to send into hyperspace
+ set room 1
+ for {set k $numcommits} {$k < $ctxend} {incr k} {
+ set x [lindex $displayorder $k]
+ set i [llsearch $dlist $x]
+ if {$i < 0} {
+ set i [llength $dlist]
+ lappend dlist $x
+ }
+ if {[lsearch -exact $nohs $x] < 0} {
+ lappend nohs $x
+ }
+ if {$reins eq {} && $onscreen($x) < 0 && $room} {
+ set reins [list $x $i]
+ }
+ set newp {}
+ if {[info exists commitlisted($x)]} {
+ set right 0
+ foreach p $parents($x) {
+ if {[llsearch $dlist $p] < 0} {
+ lappend newp $p
+ if {[lsearch -exact $nohs $p] < 0} {
+ lappend nohs $p
+ }
+ if {$reins eq {} && $onscreen($p) < 0 && $room} {
+ set reins [list $p [expr {$i + $right}]]
+ }
+ }
+ set right 1
+ }
+ }
+ set l [lindex $dlist $i]
+ if {[llength $l] == 1} {
+ set l $newp
+ } else {
+ set j [lsearch -exact $l $x]
+ set l [concat [lreplace $l $j $j] $newp]
+ }
+ set dlist [lreplace $dlist $i $i $l]
+ if {$room && $isfat && [llength $newp] <= 1} {
+ set room 0
+ }
+ }
+ }
+
+ set dlevel [drawslants $id $reins $nohs]
+ drawcommitline $dlevel
+ if {[clock clicks -milliseconds] >= $nextupdate
+ && $numcommits >= $ncmupdate} {
+ doupdate $reading
+ if {$stopped} break
+ }
+ }
+}
+
+# level here is an index in todo
+proc updatetodo {level noshortcut} {
+ global ncleft todo nnewparents
+ global commitlisted parents onscreen
+
+ set id [lindex $todo $level]
+ set olds {}
+ if {[info exists commitlisted($id)]} {
+ foreach p $parents($id) {
+ if {[lsearch -exact $olds $p] < 0} {
+ lappend olds $p
+ }
+ }
+ }
+ if {!$noshortcut && [llength $olds] == 1} {
+ set p [lindex $olds 0]
+ if {$ncleft($p) == 1 && [lsearch -exact $todo $p] < 0} {
+ set ncleft($p) 0
+ set todo [lreplace $todo $level $level $p]
+ set onscreen($p) 0
+ set nnewparents($id) 1
+ return 0
+ }
+ }
+
+ set todo [lreplace $todo $level $level]
+ set i $level
+ set n 0
+ foreach p $olds {
+ incr ncleft($p) -1
+ set k [lsearch -exact $todo $p]
+ if {$k < 0} {
+ set todo [linsert $todo $i $p]
+ set onscreen($p) 0
+ incr i
+ incr n
+ }
+ }
+ set nnewparents($id) $n
+
+ return 1
+}
+
+proc decidenext {{noread 0}} {
+ global ncleft todo
+ global datemode cdate
+ global commitinfo
+
+ # choose which one to do next time around
+ set todol [llength $todo]
+ set level -1
+ set latest {}
+ for {set k $todol} {[incr k -1] >= 0} {} {
+ set p [lindex $todo $k]
+ if {$ncleft($p) == 0} {
+ if {$datemode} {
+ if {![info exists commitinfo($p)]} {
+ if {$noread} {
+ return {}
+ }
+ readcommit $p
+ }
+ if {$latest == {} || $cdate($p) > $latest} {
+ set level $k
+ set latest $cdate($p)
+ }
+ } else {
+ set level $k
+ break
+ }
+ }
+ }
+
+ return $level
+}
+
+proc drawcommit {id reading} {
+ global phase todo nchildren datemode nextupdate revlistorder ncleft
+ global numcommits ncmupdate displayorder todo onscreen parents
+ global commitlisted commitordered
+
+ if {$phase != "incrdraw"} {
+ set phase incrdraw
+ set displayorder {}
+ set todo {}
+ initgraph
+ catch {unset commitordered}
+ }
+ set commitordered($id) 1
+ if {$nchildren($id) == 0} {
+ lappend todo $id
+ set onscreen($id) 0
+ }
+ if {$revlistorder} {
+ set level [lsearch -exact $todo $id]
+ if {$level < 0} {
+ error_popup "oops, $id isn't in todo"
+ return
+ }
+ lappend displayorder $id
+ updatetodo $level 0
+ } else {
+ set level [decidenext 1]
+ if {$level == {} || $level < 0} return
+ while 1 {
+ set id [lindex $todo $level]
+ if {![info exists commitordered($id)]} {
+ break
+ }
+ lappend displayorder [lindex $todo $level]
+ if {[updatetodo $level $datemode]} {
+ set level [decidenext 1]
+ if {$level == {} || $level < 0} break
+ }
+ }
+ }
+ drawmore $reading
+}
+
+proc finishcommits {} {
+ global phase oldcommits commits
+ global canv mainfont ctext maincursor textcursor
+ global parents displayorder todo
+
+ if {$phase == "incrdraw" || $phase == "removecommits"} {
+ foreach id $oldcommits {
+ lappend commits $id
+ drawcommit $id 0
+ }
+ set oldcommits {}
+ drawrest
+ } elseif {$phase == "updatecommits"} {
+ # there were no new commits, in fact
+ set commits $oldcommits
+ set oldcommits {}
+ set phase {}
+ } else {
+ $canv delete all
+ $canv create text 3 3 -anchor nw -text "No commits selected" \
+ -font $mainfont -tags textitems
+ set phase {}
+ }
+ . config -cursor $maincursor
+ settextcursor $textcursor
+}
+
+# Don't change the text pane cursor if it is currently the hand cursor,
+# showing that we are over a sha1 ID link.
+proc settextcursor {c} {
+ global ctext curtextcursor
+
+ if {[$ctext cget -cursor] == $curtextcursor} {
+ $ctext config -cursor $c
+ }
+ set curtextcursor $c
+}
+
+proc drawgraph {} {
+ global nextupdate startmsecs ncmupdate
+ global displayorder onscreen
+
+ if {$displayorder == {}} return
+ set startmsecs [clock clicks -milliseconds]
+ set nextupdate [expr {$startmsecs + 100}]
+ set ncmupdate 1
+ initgraph
+ foreach id $displayorder {
+ set onscreen($id) 0
+ }
+ drawmore 0
+}
+
+proc drawrest {} {
+ global phase stopped redisplaying selectedline
+ global datemode todo displayorder ncleft
+ global numcommits ncmupdate
+ global nextupdate startmsecs revlistorder
+
+ set level [decidenext]
+ if {$level >= 0} {
+ set phase drawgraph
+ while 1 {
+ lappend displayorder [lindex $todo $level]
+ set hard [updatetodo $level $datemode]
+ if {$hard} {
+ set level [decidenext]
+ if {$level < 0} break
+ }
+ }
+ }
+ if {$todo != {}} {
+ puts "ERROR: none of the pending commits can be done yet:"
+ foreach p $todo {
+ puts " $p ($ncleft($p))"
+ }
+ }
+
+ drawmore 0
+ set phase {}
+ set drawmsecs [expr {[clock clicks -milliseconds] - $startmsecs}]
+ #puts "overall $drawmsecs ms for $numcommits commits"
+ if {$redisplaying} {
+ if {$stopped == 0 && [info exists selectedline]} {
+ selectline $selectedline 0
+ }
+ if {$stopped == 1} {
+ set stopped 0
+ after idle drawgraph
+ } else {
+ set redisplaying 0
+ }
+ }
+}
+
+proc findmatches {f} {
+ global findtype foundstring foundstrlen
+ if {$findtype == "Regexp"} {
+ set matches [regexp -indices -all -inline $foundstring $f]
+ } else {
+ if {$findtype == "IgnCase"} {
+ set str [string tolower $f]
+ } else {
+ set str $f
+ }
+ set matches {}
+ set i 0
+ while {[set j [string first $foundstring $str $i]] >= 0} {
+ lappend matches [list $j [expr {$j+$foundstrlen-1}]]
+ set i [expr {$j + $foundstrlen}]
+ }
+ }
+ return $matches
+}
+
+proc dofind {} {
+ global findtype findloc findstring markedmatches commitinfo
+ global numcommits lineid linehtag linentag linedtag
+ global mainfont namefont canv canv2 canv3 selectedline
+ global matchinglines foundstring foundstrlen
+
+ stopfindproc
+ unmarkmatches
+ focus .
+ set matchinglines {}
+ if {$findloc == "Pickaxe"} {
+ findpatches
+ return
+ }
+ if {$findtype == "IgnCase"} {
+ set foundstring [string tolower $findstring]
+ } else {
+ set foundstring $findstring
+ }
+ set foundstrlen [string length $findstring]
+ if {$foundstrlen == 0} return
+ if {$findloc == "Files"} {
+ findfiles
+ return
+ }
+ if {![info exists selectedline]} {
+ set oldsel -1
+ } else {
+ set oldsel $selectedline
+ }
+ set didsel 0
+ set fldtypes {Headline Author Date Committer CDate Comment}
+ for {set l 0} {$l < $numcommits} {incr l} {
+ set id $lineid($l)
+ set info $commitinfo($id)
+ set doesmatch 0
+ foreach f $info ty $fldtypes {
+ if {$findloc != "All fields" && $findloc != $ty} {
+ continue
+ }
+ set matches [findmatches $f]
+ if {$matches == {}} continue
+ set doesmatch 1
+ if {$ty == "Headline"} {
+ markmatches $canv $l $f $linehtag($l) $matches $mainfont
+ } elseif {$ty == "Author"} {
+ markmatches $canv2 $l $f $linentag($l) $matches $namefont
+ } elseif {$ty == "Date"} {
+ markmatches $canv3 $l $f $linedtag($l) $matches $mainfont
+ }
+ }
+ if {$doesmatch} {
+ lappend matchinglines $l
+ if {!$didsel && $l > $oldsel} {
+ findselectline $l
+ set didsel 1
+ }
+ }
+ }
+ if {$matchinglines == {}} {
+ bell
+ } elseif {!$didsel} {
+ findselectline [lindex $matchinglines 0]
+ }
+}
+
+proc findselectline {l} {
+ global findloc commentend ctext
+ selectline $l 1
+ if {$findloc == "All fields" || $findloc == "Comments"} {
+ # highlight the matches in the comments
+ set f [$ctext get 1.0 $commentend]
+ set matches [findmatches $f]
+ foreach match $matches {
+ set start [lindex $match 0]
+ set end [expr {[lindex $match 1] + 1}]
+ $ctext tag add found "1.0 + $start c" "1.0 + $end c"
+ }
+ }
+}
+
+proc findnext {restart} {
+ global matchinglines selectedline
+ if {![info exists matchinglines]} {
+ if {$restart} {
+ dofind
+ }
+ return
+ }
+ if {![info exists selectedline]} return
+ foreach l $matchinglines {
+ if {$l > $selectedline} {
+ findselectline $l
+ return
+ }
+ }
+ bell
+}
+
+proc findprev {} {
+ global matchinglines selectedline
+ if {![info exists matchinglines]} {
+ dofind
+ return
+ }
+ if {![info exists selectedline]} return
+ set prev {}
+ foreach l $matchinglines {
+ if {$l >= $selectedline} break
+ set prev $l
+ }
+ if {$prev != {}} {
+ findselectline $prev
+ } else {
+ bell
+ }
+}
+
+proc findlocchange {name ix op} {
+ global findloc findtype findtypemenu
+ if {$findloc == "Pickaxe"} {
+ set findtype Exact
+ set state disabled
+ } else {
+ set state normal
+ }
+ $findtypemenu entryconf 1 -state $state
+ $findtypemenu entryconf 2 -state $state
+}
+
+proc stopfindproc {{done 0}} {
+ global findprocpid findprocfile findids
+ global ctext findoldcursor phase maincursor textcursor
+ global findinprogress
+
+ catch {unset findids}
+ if {[info exists findprocpid]} {
+ if {!$done} {
+ catch {exec kill $findprocpid}
+ }
+ catch {close $findprocfile}
+ unset findprocpid
+ }
+ if {[info exists findinprogress]} {
+ unset findinprogress
+ if {$phase != "incrdraw"} {
+ . config -cursor $maincursor
+ settextcursor $textcursor
+ }
+ }
+}
+
+proc findpatches {} {
+ global findstring selectedline numcommits
+ global findprocpid findprocfile
+ global finddidsel ctext lineid findinprogress
+ global findinsertpos
+
+ if {$numcommits == 0} return
+
+ # make a list of all the ids to search, starting at the one
+ # after the selected line (if any)
+ if {[info exists selectedline]} {
+ set l $selectedline
+ } else {
+ set l -1
+ }
+ set inputids {}
+ for {set i 0} {$i < $numcommits} {incr i} {
+ if {[incr l] >= $numcommits} {
+ set l 0
+ }
+ append inputids $lineid($l) "\n"
+ }
+
+ if {[catch {
+ set f [open [list | git-diff-tree --stdin -s -r -S$findstring \
+ << $inputids] r]
+ } err]} {
+ error_popup "Error starting search process: $err"
+ return
+ }
+
+ set findinsertpos end
+ set findprocfile $f
+ set findprocpid [pid $f]
+ fconfigure $f -blocking 0
+ fileevent $f readable readfindproc
+ set finddidsel 0
+ . config -cursor watch
+ settextcursor watch
+ set findinprogress 1
+}
+
+proc readfindproc {} {
+ global findprocfile finddidsel
+ global idline matchinglines findinsertpos
+
+ set n [gets $findprocfile line]
+ if {$n < 0} {
+ if {[eof $findprocfile]} {
+ stopfindproc 1
+ if {!$finddidsel} {
+ bell
+ }
+ }
+ return
+ }
+ if {![regexp {^[0-9a-f]{40}} $line id]} {
+ error_popup "Can't parse git-diff-tree output: $line"
+ stopfindproc
+ return
+ }
+ if {![info exists idline($id)]} {
+ puts stderr "spurious id: $id"
+ return
+ }
+ set l $idline($id)
+ insertmatch $l $id
+}
+
+proc insertmatch {l id} {
+ global matchinglines findinsertpos finddidsel
+
+ if {$findinsertpos == "end"} {
+ if {$matchinglines != {} && $l < [lindex $matchinglines 0]} {
+ set matchinglines [linsert $matchinglines 0 $l]
+ set findinsertpos 1
+ } else {
+ lappend matchinglines $l
+ }
+ } else {
+ set matchinglines [linsert $matchinglines $findinsertpos $l]
+ incr findinsertpos
+ }
+ markheadline $l $id
+ if {!$finddidsel} {
+ findselectline $l
+ set finddidsel 1
+ }
+}
+
+proc findfiles {} {
+ global selectedline numcommits lineid ctext
+ global ffileline finddidsel parents nparents
+ global findinprogress findstartline findinsertpos
+ global treediffs fdiffids fdiffsneeded fdiffpos
+ global findmergefiles
+
+ if {$numcommits == 0} return
+
+ if {[info exists selectedline]} {
+ set l [expr {$selectedline + 1}]
+ } else {
+ set l 0
+ }
+ set ffileline $l
+ set findstartline $l
+ set diffsneeded {}
+ set fdiffsneeded {}
+ while 1 {
+ set id $lineid($l)
+ if {$findmergefiles || $nparents($id) == 1} {
+ foreach p $parents($id) {
+ if {![info exists treediffs([list $id $p])]} {
+ append diffsneeded "$id $p\n"
+ lappend fdiffsneeded [list $id $p]
+ }
+ }
+ }
+ if {[incr l] >= $numcommits} {
+ set l 0
+ }
+ if {$l == $findstartline} break
+ }
+
+ # start off a git-diff-tree process if needed
+ if {$diffsneeded ne {}} {
+ if {[catch {
+ set df [open [list | git-diff-tree -r --stdin << $diffsneeded] r]
+ } err ]} {
+ error_popup "Error starting search process: $err"
+ return
+ }
+ catch {unset fdiffids}
+ set fdiffpos 0
+ fconfigure $df -blocking 0
+ fileevent $df readable [list readfilediffs $df]
+ }
+
+ set finddidsel 0
+ set findinsertpos end
+ set id $lineid($l)
+ set p [lindex $parents($id) 0]
+ . config -cursor watch
+ settextcursor watch
+ set findinprogress 1
+ findcont [list $id $p]
+ update
+}
+
+proc readfilediffs {df} {
+ global findids fdiffids fdiffs
+
+ set n [gets $df line]
+ if {$n < 0} {
+ if {[eof $df]} {
+ donefilediff
+ if {[catch {close $df} err]} {
+ stopfindproc
+ bell
+ error_popup "Error in git-diff-tree: $err"
+ } elseif {[info exists findids]} {
+ set ids $findids
+ stopfindproc
+ bell
+ error_popup "Couldn't find diffs for {$ids}"
+ }
+ }
+ return
+ }
+ if {[regexp {^([0-9a-f]{40}) \(from ([0-9a-f]{40})\)} $line match id p]} {
+ # start of a new string of diffs
+ donefilediff
+ set fdiffids [list $id $p]
+ set fdiffs {}
+ } elseif {[string match ":*" $line]} {
+ lappend fdiffs [lindex $line 5]
+ }
+}
+
+proc donefilediff {} {
+ global fdiffids fdiffs treediffs findids
+ global fdiffsneeded fdiffpos
+
+ if {[info exists fdiffids]} {
+ while {[lindex $fdiffsneeded $fdiffpos] ne $fdiffids
+ && $fdiffpos < [llength $fdiffsneeded]} {
+ # git-diff-tree doesn't output anything for a commit
+ # which doesn't change anything
+ set nullids [lindex $fdiffsneeded $fdiffpos]
+ set treediffs($nullids) {}
+ if {[info exists findids] && $nullids eq $findids} {
+ unset findids
+ findcont $nullids
+ }
+ incr fdiffpos
+ }
+ incr fdiffpos
+
+ if {![info exists treediffs($fdiffids)]} {
+ set treediffs($fdiffids) $fdiffs
+ }
+ if {[info exists findids] && $fdiffids eq $findids} {
+ unset findids
+ findcont $fdiffids
+ }
+ }
+}
+
+proc findcont {ids} {
+ global findids treediffs parents nparents
+ global ffileline findstartline finddidsel
+ global lineid numcommits matchinglines findinprogress
+ global findmergefiles
+
+ set id [lindex $ids 0]
+ set p [lindex $ids 1]
+ set pi [lsearch -exact $parents($id) $p]
+ set l $ffileline
+ while 1 {
+ if {$findmergefiles || $nparents($id) == 1} {
+ if {![info exists treediffs($ids)]} {
+ set findids $ids
+ set ffileline $l
+ return
+ }
+ set doesmatch 0
+ foreach f $treediffs($ids) {
+ set x [findmatches $f]
+ if {$x != {}} {
+ set doesmatch 1
+ break
+ }
+ }
+ if {$doesmatch} {
+ insertmatch $l $id
+ set pi $nparents($id)
+ }
+ } else {
+ set pi $nparents($id)
+ }
+ if {[incr pi] >= $nparents($id)} {
+ set pi 0
+ if {[incr l] >= $numcommits} {
+ set l 0
+ }
+ if {$l == $findstartline} break
+ set id $lineid($l)
+ }
+ set p [lindex $parents($id) $pi]
+ set ids [list $id $p]
+ }
+ stopfindproc
+ if {!$finddidsel} {
+ bell
+ }
+}
+
+# mark a commit as matching by putting a yellow background
+# behind the headline
+proc markheadline {l id} {
+ global canv mainfont linehtag commitinfo
+
+ set bbox [$canv bbox $linehtag($l)]
+ set t [$canv create rect $bbox -outline {} -tags matches -fill yellow]
+ $canv lower $t
+}
+
+# mark the bits of a headline, author or date that match a find string
+proc markmatches {canv l str tag matches font} {
+ set bbox [$canv bbox $tag]
+ set x0 [lindex $bbox 0]
+ set y0 [lindex $bbox 1]
+ set y1 [lindex $bbox 3]
+ foreach match $matches {
+ set start [lindex $match 0]
+ set end [lindex $match 1]
+ if {$start > $end} continue
+ set xoff [font measure $font [string range $str 0 [expr {$start-1}]]]
+ set xlen [font measure $font [string range $str 0 [expr {$end}]]]
+ set t [$canv create rect [expr {$x0+$xoff}] $y0 \
+ [expr {$x0+$xlen+2}] $y1 \
+ -outline {} -tags matches -fill yellow]
+ $canv lower $t
+ }
+}
+
+proc unmarkmatches {} {
+ global matchinglines findids
+ allcanvs delete matches
+ catch {unset matchinglines}
+ catch {unset findids}
+}
+
+proc selcanvline {w x y} {
+ global canv canvy0 ctext linespc
+ global lineid linehtag linentag linedtag rowtextx
+ set ymax [lindex [$canv cget -scrollregion] 3]
+ if {$ymax == {}} return
+ set yfrac [lindex [$canv yview] 0]
+ set y [expr {$y + $yfrac * $ymax}]
+ set l [expr {int(($y - $canvy0) / $linespc + 0.5)}]
+ if {$l < 0} {
+ set l 0
+ }
+ if {$w eq $canv} {
+ if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return
+ }
+ unmarkmatches
+ selectline $l 1
+}
+
+proc commit_descriptor {p} {
+ global commitinfo
+ set l "..."
+ if {[info exists commitinfo($p)]} {
+ set l [lindex $commitinfo($p) 0]
+ }
+ return "$p ($l)"
+}
+
+# append some text to the ctext widget, and make any SHA1 ID
+# that we know about be a clickable link.
+proc appendwithlinks {text} {
+ global ctext idline linknum
+
+ set start [$ctext index "end - 1c"]
+ $ctext insert end $text
+ $ctext insert end "\n"
+ set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
+ foreach l $links {
+ set s [lindex $l 0]
+ set e [lindex $l 1]
+ set linkid [string range $text $s $e]
+ if {![info exists idline($linkid)]} continue
+ incr e
+ $ctext tag add link "$start + $s c" "$start + $e c"
+ $ctext tag add link$linknum "$start + $s c" "$start + $e c"
+ $ctext tag bind link$linknum <1> [list selectline $idline($linkid) 1]
+ incr linknum
+ }
+ $ctext tag conf link -foreground blue -underline 1
+ $ctext tag bind link <Enter> { %W configure -cursor hand2 }
+ $ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
+}
+
+proc selectline {l isnew} {
+ global canv canv2 canv3 ctext commitinfo selectedline
+ global lineid linehtag linentag linedtag
+ global canvy0 linespc parents nparents children
+ global cflist currentid sha1entry
+ global commentend idtags idline linknum
+
+ $canv delete hover
+ normalline
+ if {![info exists lineid($l)] || ![info exists linehtag($l)]} return