Code

git-gui: Allow digging through history in blame viewer
authorShawn O. Pearce <spearce@spearce.org>
Sat, 2 Jun 2007 01:47:12 +0000 (21:47 -0400)
committerShawn O. Pearce <spearce@spearce.org>
Wed, 6 Jun 2007 05:26:47 +0000 (01:26 -0400)
gitweb has long had a feature where the user can click on any
commit the blame display and go visit that commit's information
page.  From the user could go get the blame display for the file
they are tracking, and try to digg through the history of any
part of the code they are interested in seeing.

We now offer somewhat similiar functionality in git-gui.  The 4
digit commit abreviation in the first column of our blame view is
now offered as a hyperlink if the commit isn't the one we are now
viewing the blame output for (as there is no point in linking back
to yourself).  Clicking on that link will stop the current blame
engine (if still running), push the new target commit onto the
history stack, and restart the blame viewer at that commit, using
the "original file name" as supplied by git-blame for that chunk
of the output.

Users can navigate back to a version they had been viewing before
by way of a back button, which offers the prior commits in a popup
menu displayed right below the back button.  I'm always showing the
menu here as the cost of switching between views is very high; you
don't want to jump to a commit you are not interested in looking at
again.

During switches we throw away all data except the cached commit data,
as that is relatively small compared to most source files and their
annotation marks.  Unfortunately throwing this per-file data away in
Tcl seems to take some time; I probably should move the line indexed
arrays to proper lists and use [lindex] rather than the array lookup
(usually lists are faster).

We now start the git-blame process using "nice", so that its priority
will drop hopefully below our own.  If I don't do this the blame engine
gets a lot of CPU under Windows 2000 and the git-gui user interface is
almost non-responsive, even though Tcl is just sitting there waiting
for events.

Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
lib/blame.tcl

index 1223c567ab526347cb3b4b58955d8328cb678bed..33e8489d067c4955f72075fe1de22482f0463e30 100644 (file)
@@ -3,18 +3,24 @@
 
 class blame {
 
-field commit  ; # input commit to blame
-field path    ; # input filename to view in $commit
-
-field w
-field w_line
-field w_cgrp
-field w_load
-field w_file
-field w_cmit
-field status
-field old_height
-
+image create photo ::blame::img_back_arrow -data {R0lGODlhGAAYAIUAAPwCBEzKXFTSZIz+nGzmhGzqfGTidIT+nEzGXHTqhGzmfGzifFzadETCVES+VARWDFzWbHzyjAReDGTadFTOZDSyRDyyTCymPARaFGTedFzSbDy2TCyqRCyqPARaDAyCHES6VDy6VCyiPAR6HCSeNByWLARyFARiDARqFGTifARiFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAYABgAAAajQIBwSCwaj8ikcsk0BppJwRPqHEypQwHBis0WDAdEFyBIKBaMAKLBdjQeSkFBYTBAIvgEoS6JmhUTEwIUDQ4VFhcMGEhyCgoZExoUaxsWHB0THkgfAXUGAhoBDSAVFR0XBnCbDRmgog0hpSIiDJpJIyEQhBUcJCIlwA22SSYVogknEg8eD82qSigdDSknY0IqJQXPYxIl1dZCGNvWw+Dm510GQQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7}
+
+field commit    ; # input commit to blame
+field path      ; # input filename to view in $commit
+field history {}; # viewer history: {commit path}
+
+field w          ; # top window in this viewer
+field w_back     ; # our back button
+field w_path     ; # label showing the current file path
+field w_line     ; # text column: all line numbers
+field w_cgrp     ; # text column: abbreviated commit SHA-1s
+field w_load     ; # text column: loaded indicator
+field w_file     ; # text column: actual file data
+field w_cmit     ; # pane showing commit message
+field status     ; # text variable bound to status bar
+field old_height ; # last known height of $w.file_pane
+
+field current_fd       {} ; # background process running
 field highlight_line   -1 ; # current line selected
 field highlight_commit {} ; # sha1 of commit selected
 
@@ -52,14 +58,42 @@ constructor new {i_commit i_path} {
 
        make_toplevel top w
        wm title $top "[appname] ([reponame]): File Viewer"
-       set status "Loading $commit:$path..."
 
-       label $w.path -text "$commit:$path" \
+       frame $w.header -background orange
+       label $w.header.commit_l \
+               -text {Commit:} \
+               -background orange \
                -anchor w \
-               -justify left \
-               -borderwidth 1 \
-               -relief sunken \
-               -font font_uibold
+               -justify left
+       set w_back $w.header.commit_b
+       button $w_back \
+               -command [cb _history_menu] \
+               -image ::blame::img_back_arrow \
+               -borderwidth 0 \
+               -relief flat \
+               -state disabled \
+               -background orange \
+               -activebackground orange
+       label $w.header.commit \
+               -textvariable @commit \
+               -background orange \
+               -anchor w \
+               -justify left
+       label $w.header.path_l \
+               -text {File:} \
+               -background orange \
+               -anchor w \
+               -justify left
+       set w_path $w.header.path
+       label $w_path \
+               -background orange \
+               -anchor w \
+               -justify left
+       pack $w.header.commit_l -side left
+       pack $w_back -side left
+       pack $w.header.commit -side left
+       pack $w_path -fill x -side right
+       pack $w.header.path_l -side right
 
        panedwindow $w.file_pane -orient vertical
        frame $w.file_pane.out
@@ -103,6 +137,13 @@ constructor new {i_commit i_path} {
                -height 40 \
                -width 4 \
                -font font_diff
+       $w_cgrp tag conf curr_commit
+       $w_cgrp tag conf prior_commit \
+               -foreground blue \
+               -underline 1
+       $w_cgrp tag bind prior_commit \
+               <Button-1> \
+               "[cb _load_commit @%x,%y];break"
 
        set w_file $w.file_pane.out.file_t
        text $w_file \
@@ -171,15 +212,7 @@ constructor new {i_commit i_path} {
                -textvariable @status \
                -anchor w \
                -justify left
-       canvas $w.status.c \
-               -width 100 \
-               -height [expr {int([winfo reqheight $w.status.l] * 0.6)}] \
-               -borderwidth 1 \
-               -relief groove \
-               -highlightt 0
-       $w.status.c create rectangle 0 0 0 20 -tags bar -fill navy
        pack $w.status.l -side left
-       pack $w.status.c -side right
 
        menu $w.ctxm -tearoff 0
        $w.ctxm add command \
@@ -238,7 +271,7 @@ constructor new {i_commit i_path} {
        bind $top <Visibility> [list focus $top]
        bind $w_file <Destroy> [list delete_this $this]
 
-       grid configure $w.path -sticky ew
+       grid configure $w.header -sticky ew
        grid configure $w.file_pane -sticky nsew
        grid configure $w.status -sticky ew
        grid columnconfigure $top 0 -weight 1
@@ -261,6 +294,66 @@ constructor new {i_commit i_path} {
        bind $w.file_pane <Configure> \
        "if {{$w.file_pane} eq {%W}} {[cb _resize %h]}"
 
+       _load $this
+}
+
+method _load {} {
+       _hide_tooltip $this
+
+       if {$total_lines != 0 || $current_fd ne {}} {
+               if {$current_fd ne {}} {
+                       catch {close $current_fd}
+                       set current_fd {}
+               }
+
+               set highlight_line -1
+               set highlight_commit {}
+               set total_lines 0
+               set blame_lines 0
+               set commit_count 0
+               set commit_list {}
+               array unset order
+               array unset line_commit
+               array unset line_file
+
+               $w_load conf -state normal
+               $w_cgrp conf -state normal
+               $w_line conf -state normal
+               $w_file conf -state normal
+
+               $w_load delete 0.0 end
+               $w_cgrp delete 0.0 end
+               $w_line delete 0.0 end
+               $w_file delete 0.0 end
+
+               $w_load conf -state disabled
+               $w_cgrp conf -state disabled
+               $w_line conf -state disabled
+               $w_file conf -state disabled
+       }
+
+       if {[winfo exists $w.status.c]} {
+               $w.status.c coords bar 0 0 0 20
+       } else {
+               canvas $w.status.c \
+                       -width 100 \
+                       -height [expr {int([winfo reqheight $w.status.l] * 0.6)}] \
+                       -borderwidth 1 \
+                       -relief groove \
+                       -highlightt 0
+               $w.status.c create rectangle 0 0 0 20 -tags bar -fill navy
+               pack $w.status.c -side right
+       }
+
+       if {$history eq {}} {
+               $w_back conf -state disabled
+       } else {
+               $w_back conf -state normal
+       }
+       lappend history [list $commit $path]
+
+       set status "Loading $commit:[escape_path $path]..."
+       $w_path conf -text [escape_path $path]
        if {$commit eq {}} {
                set fd [open $path r]
        } else {
@@ -269,9 +362,52 @@ constructor new {i_commit i_path} {
        }
        fconfigure $fd -blocking 0 -translation lf -encoding binary
        fileevent $fd readable [cb _read_file $fd]
+       set current_fd $fd
+}
+
+method _history_menu {} {
+       set m $w.backmenu
+       if {[winfo exists $m]} {
+               $m delete 0 end
+       } else {
+               menu $m -tearoff 0
+       }
+
+       for {set i [expr {[llength $history] - 2}]
+               } {$i >= 0} {incr i -1} {
+               set e [lindex $history $i]
+               set c [lindex $e 0]
+               set f [lindex $e 1]
+
+               if {[regexp {^[0-9a-f]{40}$} $c]} {
+                       set t [string range $c 0 8]...
+               } else {
+                       set t $c
+               }
+               if {![catch {set summary $header($c,summary)}]} {
+                       append t " $summary"
+               }
+
+               $m add command -label $t -command [cb _goback $i $c $f]
+       }
+       set X [winfo rootx $w_back]
+       set Y [expr {[winfo rooty $w_back] + [winfo height $w_back]}]
+       tk_popup $m $X $Y
+}
+
+method _goback {i c f} {
+       set history [lrange $history 0 [expr {$i - 1}]]
+       set commit $c
+       set path $f
+       _load $this
 }
 
 method _read_file {fd} {
+       if {$fd ne $current_fd} {
+               catch {close $fd}
+               return
+       }
+
        $w_load conf -state normal
        $w_cgrp conf -state normal
        $w_line conf -state normal
@@ -298,7 +434,7 @@ method _read_file {fd} {
        if {[eof $fd]} {
                close $fd
                _status $this
-               set cmd [list git blame -M -C --incremental]
+               set cmd {nice git blame -M -C --incremental}
                if {$commit eq {}} {
                        lappend cmd --contents $path
                } else {
@@ -308,12 +444,18 @@ method _read_file {fd} {
                set fd [open "| $cmd" r]
                fconfigure $fd -blocking 0 -translation lf -encoding binary
                fileevent $fd readable [cb _read_blame $fd]
+               set current_fd $fd
        }
 } ifdeleted { catch {close $fd} }
 
 method _read_blame {fd} {
        variable group_colors
 
+       if {$fd ne $current_fd} {
+               catch {close $fd}
+               return
+       }
+
        $w_cgrp conf -state normal
        while {[gets $fd line] >= 0} {
                if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
@@ -344,7 +486,12 @@ method _read_blame {fd} {
 
                        if {[regexp {^0{40}$} $cmit]} {
                                set commit_abbr work
+                               set commit_type curr_commit
+                       } elseif {$cmit eq $commit} {
+                               set commit_abbr this
+                               set commit_type curr_commit
                        } else {
+                               set commit_type prior_commit
                                set commit_abbr [string range $cmit 0 4]
                        }
 
@@ -396,7 +543,7 @@ method _read_blame {fd} {
 
                                $w_cgrp delete $lno.0 "$lno.0 lineend"
                                if {$lno == $first_lno} {
-                                       $w_cgrp insert $lno.0 $commit_abbr
+                                       $w_cgrp insert $lno.0 $commit_abbr $commit_type
                                } elseif {$lno == [expr {$first_lno + 1}]} {
                                        $w_cgrp insert $lno.0 $author_abbr
                                } else {
@@ -430,7 +577,7 @@ method _read_blame {fd} {
                                $w_cgrp delete $lno.0 "$lno.0 lineend"
 
                                if {$lno == $first_lno} {
-                                       $w_cgrp insert $lno.0 $commit_abbr
+                                       $w_cgrp insert $lno.0 $commit_abbr $commit_type
                                } elseif {$lno == [expr {$first_lno + 1}]} {
                                        $w_cgrp insert $lno.0 $author_abbr
                                } else {
@@ -447,6 +594,7 @@ method _read_blame {fd} {
 
        if {[eof $fd]} {
                close $fd
+               set current_fd {}
                set status {Annotation complete.}
                destroy $w.status.c
        } else {
@@ -472,6 +620,16 @@ method _click {cur_w pos} {
        _showcommit $this $lno
 }
 
+method _load_commit {pos} {
+       set lno [lindex [split [$w_cgrp index $pos] .] 0]
+       if {[catch {set cmit $line_commit($lno)}]} return
+       if {[catch {set file $line_file($lno)  }]} return
+
+       set commit $cmit
+       set path $file
+       _load $this
+}
+
 method _showcommit {lno} {
        global repo_config
        variable active_color