git-commit-vandalism/lib/blame.tcl
Shawn O. Pearce 60aa065f69 git-gui: Allow vi keys to scroll the diff/blame regions
Users who are used to vi and recent versions of gitk may want
to scroll the diff region using vi style keybindings.  Since
these aren't bound to anything else and that widget does not
accept focus for data input, we can easily support that too.

Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
2007-05-07 23:35:51 -04:00

408 lines
11 KiB
Tcl

# git-gui blame viewer
# Copyright (C) 2006, 2007 Shawn Pearce
proc show_blame {commit path} {
global next_browser_id blame_status blame_data
if {[winfo ismapped .]} {
set w .browser[incr next_browser_id]
set tl $w
toplevel $w
} else {
set w {}
set tl .
}
set blame_status($w) {Loading current file content...}
label $w.path -text "$commit:$path" \
-anchor w \
-justify left \
-borderwidth 1 \
-relief sunken \
-font font_uibold
pack $w.path -side top -fill x
frame $w.out
text $w.out.loaded_t \
-background white -borderwidth 0 \
-state disabled \
-wrap none \
-height 40 \
-width 1 \
-font font_diff
$w.out.loaded_t tag conf annotated -background grey
text $w.out.linenumber_t \
-background white -borderwidth 0 \
-state disabled \
-wrap none \
-height 40 \
-width 5 \
-font font_diff
$w.out.linenumber_t tag conf linenumber -justify right
text $w.out.file_t \
-background white -borderwidth 0 \
-state disabled \
-wrap none \
-height 40 \
-width 80 \
-xscrollcommand [list $w.out.sbx set] \
-font font_diff
scrollbar $w.out.sbx -orient h -command [list $w.out.file_t xview]
scrollbar $w.out.sby -orient v \
-command [list scrollbar2many [list \
$w.out.loaded_t \
$w.out.linenumber_t \
$w.out.file_t \
] yview]
grid \
$w.out.linenumber_t \
$w.out.loaded_t \
$w.out.file_t \
$w.out.sby \
-sticky nsew
grid conf $w.out.sbx -column 2 -sticky we
grid columnconfigure $w.out 2 -weight 1
grid rowconfigure $w.out 0 -weight 1
pack $w.out -fill both -expand 1
label $w.status -textvariable blame_status($w) \
-anchor w \
-justify left \
-borderwidth 1 \
-relief sunken
pack $w.status -side bottom -fill x
frame $w.cm
text $w.cm.t \
-background white -borderwidth 0 \
-state disabled \
-wrap none \
-height 10 \
-width 80 \
-xscrollcommand [list $w.cm.sbx set] \
-yscrollcommand [list $w.cm.sby set] \
-font font_diff
scrollbar $w.cm.sbx -orient h -command [list $w.cm.t xview]
scrollbar $w.cm.sby -orient v -command [list $w.cm.t yview]
pack $w.cm.sby -side right -fill y
pack $w.cm.sbx -side bottom -fill x
pack $w.cm.t -expand 1 -fill both
pack $w.cm -side bottom -fill x
menu $w.ctxm -tearoff 0
$w.ctxm add command -label "Copy Commit" \
-command "blame_copycommit $w \$cursorW @\$cursorX,\$cursorY"
foreach i [list \
$w.out.loaded_t \
$w.out.linenumber_t \
$w.out.file_t] {
$i tag conf in_sel \
-background [$i cget -foreground] \
-foreground [$i cget -background]
$i conf -yscrollcommand \
[list many2scrollbar [list \
$w.out.loaded_t \
$w.out.linenumber_t \
$w.out.file_t \
] yview $w.out.sby]
bind $i <Button-1> "
blame_click {$w} \\
$w.cm.t \\
$w.out.linenumber_t \\
$w.out.file_t \\
$i @%x,%y
focus $i
"
bind_button3 $i "
set cursorX %x
set cursorY %y
set cursorW %W
tk_popup $w.ctxm %X %Y
"
}
foreach i [list \
$w.out.loaded_t \
$w.out.linenumber_t \
$w.out.file_t \
$w.cm.t] {
bind $i <Key-Up> {catch {%W yview scroll -1 units};break}
bind $i <Key-Down> {catch {%W yview scroll 1 units};break}
bind $i <Key-Left> {catch {%W xview scroll -1 units};break}
bind $i <Key-Right> {catch {%W xview scroll 1 units};break}
bind $i <Key-k> {catch {%W yview scroll -1 units};break}
bind $i <Key-j> {catch {%W yview scroll 1 units};break}
bind $i <Key-h> {catch {%W xview scroll -1 units};break}
bind $i <Key-l> {catch {%W xview scroll 1 units};break}
bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break}
bind $i <Control-Key-f> {catch {%W yview scroll 1 pages};break}
}
bind $w.cm.t <Button-1> "focus $w.cm.t"
bind $tl <Visibility> "focus $tl"
bind $tl <Destroy> "
array unset blame_status {$w}
array unset blame_data $w,*
"
wm title $tl "[appname] ([reponame]): File Viewer"
set blame_data($w,commit_count) 0
set blame_data($w,commit_list) {}
set blame_data($w,total_lines) 0
set blame_data($w,blame_lines) 0
set blame_data($w,highlight_commit) {}
set blame_data($w,highlight_line) -1
set cmd [list git cat-file blob "$commit:$path"]
set fd [open "| $cmd" r]
fconfigure $fd -blocking 0 -translation lf -encoding binary
fileevent $fd readable [list read_blame_catfile \
$fd $w $commit $path \
$w.cm.t $w.out.loaded_t $w.out.linenumber_t $w.out.file_t]
}
proc read_blame_catfile {fd w commit path w_cmit w_load w_line w_file} {
global blame_status blame_data
if {![winfo exists $w_file]} {
catch {close $fd}
return
}
set n $blame_data($w,total_lines)
$w_load conf -state normal
$w_line conf -state normal
$w_file conf -state normal
while {[gets $fd line] >= 0} {
regsub "\r\$" $line {} line
incr n
$w_load insert end "\n"
$w_line insert end "$n\n" linenumber
$w_file insert end "$line\n"
}
$w_load conf -state disabled
$w_line conf -state disabled
$w_file conf -state disabled
set blame_data($w,total_lines) $n
if {[eof $fd]} {
close $fd
blame_incremental_status $w
set cmd [list git blame -M -C --incremental]
lappend cmd $commit -- $path
set fd [open "| $cmd" r]
fconfigure $fd -blocking 0 -translation lf -encoding binary
fileevent $fd readable [list read_blame_incremental $fd $w \
$w_load $w_cmit $w_line $w_file]
}
}
proc read_blame_incremental {fd w w_load w_cmit w_line w_file} {
global blame_status blame_data
if {![winfo exists $w_file]} {
catch {close $fd}
return
}
while {[gets $fd line] >= 0} {
if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
cmit original_line final_line line_count]} {
set blame_data($w,commit) $cmit
set blame_data($w,original_line) $original_line
set blame_data($w,final_line) $final_line
set blame_data($w,line_count) $line_count
if {[catch {set g $blame_data($w,$cmit,order)}]} {
$w_line tag conf g$cmit
$w_file tag conf g$cmit
$w_line tag raise in_sel
$w_file tag raise in_sel
$w_file tag raise sel
set blame_data($w,$cmit,order) $blame_data($w,commit_count)
incr blame_data($w,commit_count)
lappend blame_data($w,commit_list) $cmit
}
} elseif {[string match {filename *} $line]} {
set file [string range $line 9 end]
set n $blame_data($w,line_count)
set lno $blame_data($w,final_line)
set cmit $blame_data($w,commit)
while {$n > 0} {
if {[catch {set g g$blame_data($w,line$lno,commit)}]} {
$w_load tag add annotated $lno.0 "$lno.0 lineend + 1c"
} else {
$w_line tag remove g$g $lno.0 "$lno.0 lineend + 1c"
$w_file tag remove g$g $lno.0 "$lno.0 lineend + 1c"
}
set blame_data($w,line$lno,commit) $cmit
set blame_data($w,line$lno,file) $file
$w_line tag add g$cmit $lno.0 "$lno.0 lineend + 1c"
$w_file tag add g$cmit $lno.0 "$lno.0 lineend + 1c"
if {$blame_data($w,highlight_line) == -1} {
if {[lindex [$w_file yview] 0] == 0} {
$w_file see $lno.0
blame_showcommit $w $w_cmit $w_line $w_file $lno
}
} elseif {$blame_data($w,highlight_line) == $lno} {
blame_showcommit $w $w_cmit $w_line $w_file $lno
}
incr n -1
incr lno
incr blame_data($w,blame_lines)
}
set hc $blame_data($w,highlight_commit)
if {$hc ne {}
&& [expr {$blame_data($w,$hc,order) + 1}]
== $blame_data($w,$cmit,order)} {
blame_showcommit $w $w_cmit $w_line $w_file \
$blame_data($w,highlight_line)
}
} elseif {[regexp {^([a-z-]+) (.*)$} $line line header data]} {
set blame_data($w,$blame_data($w,commit),$header) $data
}
}
if {[eof $fd]} {
close $fd
set blame_status($w) {Annotation complete.}
} else {
blame_incremental_status $w
}
}
proc blame_incremental_status {w} {
global blame_status blame_data
set have $blame_data($w,blame_lines)
set total $blame_data($w,total_lines)
set pdone 0
if {$total} {set pdone [expr {100 * $have / $total}]}
set blame_status($w) [format \
"Loading annotations... %i of %i lines annotated (%2i%%)" \
$have $total $pdone]
}
proc blame_click {w w_cmit w_line w_file cur_w pos} {
set lno [lindex [split [$cur_w index $pos] .] 0]
if {$lno eq {}} return
$w_line tag remove in_sel 0.0 end
$w_file tag remove in_sel 0.0 end
$w_line tag add in_sel $lno.0 "$lno.0 + 1 line"
$w_file tag add in_sel $lno.0 "$lno.0 + 1 line"
blame_showcommit $w $w_cmit $w_line $w_file $lno
}
set blame_colors {
#ff4040
#ff40ff
#4040ff
}
proc blame_showcommit {w w_cmit w_line w_file lno} {
global blame_colors blame_data repo_config
set cmit $blame_data($w,highlight_commit)
if {$cmit ne {}} {
set idx $blame_data($w,$cmit,order)
set i 0
foreach c $blame_colors {
set h [lindex $blame_data($w,commit_list) [expr {$idx - 1 + $i}]]
$w_line tag conf g$h -background white
$w_file tag conf g$h -background white
incr i
}
}
$w_cmit conf -state normal
$w_cmit delete 0.0 end
if {[catch {set cmit $blame_data($w,line$lno,commit)}]} {
set cmit {}
$w_cmit insert end "Loading annotation..."
} else {
set idx $blame_data($w,$cmit,order)
set i 0
foreach c $blame_colors {
set h [lindex $blame_data($w,commit_list) [expr {$idx - 1 + $i}]]
$w_line tag conf g$h -background $c
$w_file tag conf g$h -background $c
incr i
}
set author_name {}
set author_email {}
set author_time {}
catch {set author_name $blame_data($w,$cmit,author)}
catch {set author_email $blame_data($w,$cmit,author-mail)}
catch {set author_time [clock format $blame_data($w,$cmit,author-time)]}
set committer_name {}
set committer_email {}
set committer_time {}
catch {set committer_name $blame_data($w,$cmit,committer)}
catch {set committer_email $blame_data($w,$cmit,committer-mail)}
catch {set committer_time [clock format $blame_data($w,$cmit,committer-time)]}
if {[catch {set msg $blame_data($w,$cmit,message)}]} {
set msg {}
catch {
set fd [open "| git cat-file commit $cmit" r]
fconfigure $fd -encoding binary -translation lf
if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
set enc utf-8
}
while {[gets $fd line] > 0} {
if {[string match {encoding *} $line]} {
set enc [string tolower [string range $line 9 end]]
}
}
set msg [encoding convertfrom $enc [read $fd]]
set msg [string trim $msg]
close $fd
set author_name [encoding convertfrom $enc $author_name]
set committer_name [encoding convertfrom $enc $committer_name]
set blame_data($w,$cmit,author) $author_name
set blame_data($w,$cmit,committer) $committer_name
}
set blame_data($w,$cmit,message) $msg
}
$w_cmit insert end "commit $cmit\n"
$w_cmit insert end "Author: $author_name $author_email $author_time\n"
$w_cmit insert end "Committer: $committer_name $committer_email $committer_time\n"
$w_cmit insert end "Original File: [escape_path $blame_data($w,line$lno,file)]\n"
$w_cmit insert end "\n"
$w_cmit insert end $msg
}
$w_cmit conf -state disabled
set blame_data($w,highlight_line) $lno
set blame_data($w,highlight_commit) $cmit
}
proc blame_copycommit {w i pos} {
global blame_data
set lno [lindex [split [$i index $pos] .] 0]
if {![catch {set commit $blame_data($w,line$lno,commit)}]} {
clipboard clear
clipboard append \
-format STRING \
-type STRING \
-- $commit
}
}