rc tools git: command for easy recursive blaming
Our ":git blame" annotates each line with the most recent commit.
However often a line has been modified by several commits.
Introduce ":git blame-jump" which shows the commit that added the
line at cursor. Crucially, it works also in Git diff buffers, so it
can be used recursively to find the full history of a line.
To do the recursive blame from a diff, I need to navigate to the
old (deleted) version of a line. Since old and new line are usually
neighbors. Speed up the common scenario of finding the old version
by making ":git blame-jump" jump to the new version. This means the
initial diff view might not include the commit message etc. Compensate
this by showing the commit's date+author+subject in the status line.
Here are some test cases.
- run blame-jump after "git blame"
- create an uncommitted or unsaved line, run "git blame" and
"blame-jump" on the uncommitted line
- run blame-jump without running "git blame"
- run blame-jump in "git show"
- run blame-jump in "git diff HEAD"
- run blame-jump in "git diff --cached"
- run blame-jump in "git diff" (YMMV if there are cached changes,
could fix that)
Naming: there are some similar commands in the wild [1];
they are usually called "show-blamed" or similar, but they
don't jump to the corresponding line. Also our list of git
commands is getting a bit messy (especially the undocumented
show-diff/hide-diff/next-hunk/prev-hunk; subject first naming seems
better).
[1]: f6e78ec4c0/kakrc (L423)
Future work: to go back to the previously-blamed commit we need to
have had the foresight to use "rename-buffer". Perhaps we want to
add some kind of buffer stack (like Magit does for example).
This commit is contained in:
parent
36efbf4cbf
commit
86d940c225
|
@ -39,11 +39,17 @@ while (defined $ARGV[0]) {
|
||||||
# Inputs
|
# Inputs
|
||||||
our $directory = $ENV{PWD};
|
our $directory = $ENV{PWD};
|
||||||
our $strip;
|
our $strip;
|
||||||
|
our $in_file;
|
||||||
|
our $in_file_line;
|
||||||
our $version = "+";
|
our $version = "+";
|
||||||
|
|
||||||
eval $begin if defined $begin;
|
eval $begin if defined $begin;
|
||||||
|
|
||||||
|
$in_file = "$directory/$in_file" if defined $in_file;
|
||||||
|
|
||||||
# Outputs
|
# Outputs
|
||||||
|
our $diff_line = 0;
|
||||||
|
our $commit;
|
||||||
our $file;
|
our $file;
|
||||||
our $file_line;
|
our $file_line;
|
||||||
our $diff_line_text;
|
our $diff_line_text;
|
||||||
|
@ -83,8 +89,13 @@ sub strip {
|
||||||
}
|
}
|
||||||
|
|
||||||
while (<STDIN>) {
|
while (<STDIN>) {
|
||||||
|
$diff_line++;
|
||||||
s/^(> )*//g;
|
s/^(> )*//g;
|
||||||
$diff_line_text = $_;
|
$diff_line_text = $_;
|
||||||
|
if (m{^commit (\w+)}) {
|
||||||
|
$commit = $1;
|
||||||
|
next;
|
||||||
|
}
|
||||||
if (m{^diff\b}) {
|
if (m{^diff\b}) {
|
||||||
$state = "header";
|
$state = "header";
|
||||||
$is_recursive_diff = 1;
|
$is_recursive_diff = 1;
|
||||||
|
@ -116,6 +127,11 @@ while (<STDIN>) {
|
||||||
$other_file_line++ if defined $other_file_line;
|
$other_file_line++ if defined $other_file_line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (defined $in_file and defined $file and $file eq $in_file) {
|
||||||
|
if (defined $in_file_line and defined $file_line and $file_line >= $in_file_line) {
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (not defined $file) {
|
if (not defined $file) {
|
||||||
$file = ($fallback_file or $other_file);
|
$file = ($fallback_file or $other_file);
|
||||||
|
|
212
rc/tools/git.kak
212
rc/tools/git.kak
|
@ -74,6 +74,7 @@ hook -group git-show-branch-highlight global WinSetOption filetype=git-show-bran
|
||||||
}
|
}
|
||||||
|
|
||||||
declare-option -hidden line-specs git_blame_flags
|
declare-option -hidden line-specs git_blame_flags
|
||||||
|
declare-option -hidden str git_blame
|
||||||
declare-option -hidden line-specs git_diff_flags
|
declare-option -hidden line-specs git_diff_flags
|
||||||
declare-option -hidden int-list git_hunk_list
|
declare-option -hidden int-list git_hunk_list
|
||||||
|
|
||||||
|
@ -83,8 +84,9 @@ define-command -params 1.. \
|
||||||
All the optional arguments are forwarded to the git utility
|
All the optional arguments are forwarded to the git utility
|
||||||
Available commands:
|
Available commands:
|
||||||
add
|
add
|
||||||
apply (alias for "patch git apply")
|
apply - alias for "patch git apply"
|
||||||
blame (toggle blame annotations)
|
blame - toggle blame annotations
|
||||||
|
blame-jump - show the commit that added the line at cursor
|
||||||
checkout
|
checkout
|
||||||
commit
|
commit
|
||||||
diff
|
diff
|
||||||
|
@ -107,6 +109,7 @@ define-command -params 1.. \
|
||||||
printf %s\\n \
|
printf %s\\n \
|
||||||
apply \
|
apply \
|
||||||
blame \
|
blame \
|
||||||
|
blame-jump \
|
||||||
checkout \
|
checkout \
|
||||||
commit \
|
commit \
|
||||||
diff \
|
diff \
|
||||||
|
@ -142,6 +145,9 @@ define-command -params 1.. \
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
kakquote() {
|
||||||
|
printf "%s" "$1" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"
|
||||||
|
}
|
||||||
|
|
||||||
show_git_cmd_output() {
|
show_git_cmd_output() {
|
||||||
local filetype
|
local filetype
|
||||||
|
@ -158,21 +164,38 @@ define-command -params 1.. \
|
||||||
mkfifo ${output}
|
mkfifo ${output}
|
||||||
( git "$@" > ${output} 2>&1 & ) > /dev/null 2>&1 < /dev/null
|
( git "$@" > ${output} 2>&1 & ) > /dev/null 2>&1 < /dev/null
|
||||||
|
|
||||||
printf %s "evaluate-commands -try-client '$kak_opt_docsclient' %{
|
printf %s "evaluate-commands -try-client '$kak_opt_docsclient' '
|
||||||
edit! -fifo ${output} *git*
|
edit! -fifo ${output} *git*
|
||||||
set-option buffer filetype '${filetype}'
|
set-option buffer filetype ${filetype}
|
||||||
hook -always -once buffer BufCloseFifo .* %{ nop %sh{ rm -r $(dirname ${output}) } }
|
hook -always -once buffer BufCloseFifo .* ''
|
||||||
}"
|
nop %sh{ rm -r $(dirname ${output}) }
|
||||||
|
$(printf %s "${on_close_fifo}" | sed "s/'/''''/g")
|
||||||
|
''
|
||||||
|
'"
|
||||||
}
|
}
|
||||||
|
|
||||||
hide_blame() {
|
hide_blame() {
|
||||||
printf %s "
|
printf %s "
|
||||||
set-option buffer=$kak_bufname git_blame_flags $kak_timestamp
|
set-option buffer=$kak_bufname git_blame_flags $kak_timestamp
|
||||||
|
set-option buffer=$kak_bufname git_blame ''
|
||||||
remove-highlighter window/git-blame
|
remove-highlighter window/git-blame
|
||||||
"
|
"
|
||||||
}
|
}
|
||||||
|
|
||||||
run_git_blame() {
|
prepare_git_blame_args='
|
||||||
|
contents_fifo=$(mktemp -d "${TMPDIR:-/tmp}"/kak-git.XXXXXXXX)/fifo
|
||||||
|
mkfifo ${contents_fifo}
|
||||||
|
echo >${kak_command_fifo} "evaluate-commands -save-regs | %{
|
||||||
|
set-register | %{
|
||||||
|
contents=\$(cat; printf .)
|
||||||
|
( printf %s \"\${contents%.}\" >${contents_fifo} ) >/dev/null 2>&1 &
|
||||||
|
}
|
||||||
|
execute-keys -client ${kak_client} -draft %{%<a-|><ret>}
|
||||||
|
}"
|
||||||
|
set -- "$@" --contents - -- "${kak_buffile}"
|
||||||
|
'
|
||||||
|
|
||||||
|
blame_toggle() {
|
||||||
echo >${kak_command_fifo} "try %{
|
echo >${kak_command_fifo} "try %{
|
||||||
add-highlighter window/git-blame flag-lines Information git_blame_flags
|
add-highlighter window/git-blame flag-lines Information git_blame_flags
|
||||||
echo -to-file ${kak_response_fifo}
|
echo -to-file ${kak_response_fifo}
|
||||||
|
@ -180,22 +203,21 @@ define-command -params 1.. \
|
||||||
echo -to-file ${kak_response_fifo} 'hide_blame; exit'
|
echo -to-file ${kak_response_fifo} 'hide_blame; exit'
|
||||||
}"
|
}"
|
||||||
eval $(cat ${kak_response_fifo})
|
eval $(cat ${kak_response_fifo})
|
||||||
contents_fifo=$(mktemp -d "${TMPDIR:-/tmp}"/kak-git.XXXXXXXX)/fifo
|
eval "$prepare_git_blame_args"
|
||||||
mkfifo ${contents_fifo}
|
|
||||||
echo >${kak_command_fifo} 'evaluate-commands -save-regs | %{
|
|
||||||
set-register | %{
|
|
||||||
contents=$(cat; printf .)
|
|
||||||
( printf %s "${contents%.}" >'${contents_fifo}' ) >/dev/null 2>&1 &
|
|
||||||
}
|
|
||||||
execute-keys -client '${kak_client}' -draft %{%<a-|><ret>}
|
|
||||||
}'
|
|
||||||
(
|
(
|
||||||
cd_bufdir
|
cd_bufdir
|
||||||
printf %s "evaluate-commands -client '$kak_client' %{
|
printf %s "evaluate-commands -client '$kak_client' %{
|
||||||
set-option buffer=$kak_bufname git_blame_flags '$kak_timestamp'
|
set-option buffer=$kak_bufname git_blame_flags '$kak_timestamp'
|
||||||
|
set-option buffer=$kak_bufname git_blame ''
|
||||||
}" | kak -p ${kak_session}
|
}" | kak -p ${kak_session}
|
||||||
git blame "$@" --incremental "${kak_buffile}" --contents - <$contents_fifo | perl -wne '
|
git blame --incremental "$@" <${contents_fifo} | perl -wne '
|
||||||
use POSIX qw(strftime);
|
use POSIX qw(strftime);
|
||||||
|
sub quote {
|
||||||
|
my $SQ = "'\''";
|
||||||
|
my $token = shift;
|
||||||
|
$token =~ s/$SQ/$SQ$SQ/g;
|
||||||
|
return "$SQ$token$SQ";
|
||||||
|
}
|
||||||
sub send_flags {
|
sub send_flags {
|
||||||
my $flush = shift;
|
my $flush = shift;
|
||||||
if (not defined $line) { return; }
|
if (not defined $line) { return; }
|
||||||
|
@ -210,11 +232,14 @@ define-command -params 1.. \
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
open CMD, "|-", "kak -p $ENV{kak_session}";
|
open CMD, "|-", "kak -p $ENV{kak_session}";
|
||||||
print CMD "set-option -add buffer=$ENV{kak_bufname} git_blame_flags $flags";
|
print CMD "set-option -add buffer=$ENV{kak_bufname} git_blame_flags $flags;";
|
||||||
|
print CMD "set-option -add buffer=$ENV{kak_bufname} git_blame " . quote $raw_blame;
|
||||||
close(CMD);
|
close(CMD);
|
||||||
$flags = "";
|
$flags = "";
|
||||||
|
$raw_blame = "";
|
||||||
$last_sent = $now;
|
$last_sent = $now;
|
||||||
}
|
}
|
||||||
|
$raw_blame .= $_;
|
||||||
chomp;
|
chomp;
|
||||||
if (m/^([0-9a-f]+) ([0-9]+) ([0-9]+) ([0-9]+)/) {
|
if (m/^([0-9a-f]+) ([0-9]+) ([0-9]+) ([0-9]+)/) {
|
||||||
send_flags(0);
|
send_flags(0);
|
||||||
|
@ -389,6 +414,152 @@ define-command -params 1.. \
|
||||||
} }"
|
} }"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blame_jump() {
|
||||||
|
echo >${kak_command_fifo} "echo -to-file ${kak_response_fifo} -- %opt{git_blame}"
|
||||||
|
blame_info=$(cat < ${kak_response_fifo})
|
||||||
|
cursor_column=${kak_cursor_column}
|
||||||
|
cursor_line=${kak_cursor_line}
|
||||||
|
if [ -z "$blame_info" ] && {
|
||||||
|
[ "${kak_opt_filetype}" = git-diff ] || [ "${kak_opt_filetype}" = git-log ]
|
||||||
|
}; then {
|
||||||
|
printf >${kak_command_fifo} %s '
|
||||||
|
evaluate-commands -draft %{
|
||||||
|
try %{
|
||||||
|
execute-keys <a-l><semicolon><a-?>^commit<ret><a-semicolon>
|
||||||
|
} catch %{
|
||||||
|
# Missing commit line, assume it is an uncommitted change.
|
||||||
|
execute-keys <a-l><semicolon><a-?>\A<ret><a-semicolon>
|
||||||
|
}
|
||||||
|
require-module diff
|
||||||
|
try %{
|
||||||
|
diff-parse BEGIN %{
|
||||||
|
$version = "-";
|
||||||
|
} END %{
|
||||||
|
if ($diff_line_text !~ m{^[ -]}) {
|
||||||
|
print "set-register e fail git blame-jump: recursive blame only works on context or deleted lines";
|
||||||
|
} else {
|
||||||
|
if (not defined $commit) {
|
||||||
|
$commit = "HEAD";
|
||||||
|
} else {
|
||||||
|
$commit = "$commit~" if $diff_line_text =~ m{^[- ]};
|
||||||
|
}
|
||||||
|
printf "echo -to-file '${kak_response_fifo}' -quoting shell %s %s %d %d",
|
||||||
|
$commit, quote($file), $file_line, ('$cursor_column' - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch %{
|
||||||
|
echo -to-file '${kak_response_fifo}' -quoting shell -- %val{error}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'
|
||||||
|
eval set -- "$(cat ${kak_response_fifo})"
|
||||||
|
if [ $# -eq 1 ]; then
|
||||||
|
echo fail -- "$(kakquote "$1")"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
starting_commit=$1
|
||||||
|
file=$2
|
||||||
|
cursor_line=$3
|
||||||
|
cursor_column=$4
|
||||||
|
blame_info=$(git blame --porcelain "$starting_commit" -L"$cursor_line,$cursor_line" -- "$file")
|
||||||
|
} elif [ -z "$blame_info" ]; then {
|
||||||
|
set --
|
||||||
|
eval "$prepare_git_blame_args"
|
||||||
|
blame_info=$(git blame --porcelain -L"$cursor_line,$cursor_line" "$@" <${contents_fifo})
|
||||||
|
rm -r $(dirname $contents_fifo)
|
||||||
|
} fi
|
||||||
|
eval "$(printf %s "$blame_info" |
|
||||||
|
client=${kak_opt_docsclient:-$kak_client} \
|
||||||
|
cursor_line=$cursor_line cursor_column=$cursor_column \
|
||||||
|
perl -wne '
|
||||||
|
BEGIN {
|
||||||
|
use POSIX qw(strftime);
|
||||||
|
our $SQ = "'\''";
|
||||||
|
sub escape {
|
||||||
|
return shift =~ s/$SQ/$SQ$SQ/gr
|
||||||
|
}
|
||||||
|
sub quote {
|
||||||
|
my $token = escape shift;
|
||||||
|
return "$SQ$token$SQ";
|
||||||
|
}
|
||||||
|
sub shellquote {
|
||||||
|
my $token = shift;
|
||||||
|
$token =~ s/$SQ/$SQ\\$SQ$SQ/g;
|
||||||
|
return "$SQ$token$SQ";
|
||||||
|
}
|
||||||
|
sub perlquote {
|
||||||
|
my $token = shift;
|
||||||
|
$token =~ s/\\/\\\\/g;
|
||||||
|
$token =~ s/$SQ/\\$SQ/g;
|
||||||
|
return "$SQ$token$SQ";
|
||||||
|
}
|
||||||
|
$target = $ENV{"cursor_line"};
|
||||||
|
}
|
||||||
|
chomp;
|
||||||
|
if (m/^([0-9a-f]+) ([0-9]+) ([0-9]+) ([0-9]+)/) {
|
||||||
|
if ($done) {
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
$sha = $1;
|
||||||
|
$old_line = $2;
|
||||||
|
$new_line = $3;
|
||||||
|
$count = $4;
|
||||||
|
if ($new_line <= $target and $target < $new_line + $count) {
|
||||||
|
$old_line += $target - $new_line;
|
||||||
|
$done = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m/^filename /) { $old_filenames{$sha} = substr($_,9) }
|
||||||
|
if (m/^author /) { $authors{$sha} = substr($_,7) }
|
||||||
|
if (m/^author-time ([0-9]*)/) { $dates{$sha} = strftime("%F", localtime $1) }
|
||||||
|
if (m/^summary /) { $summaries{$sha} = substr($_,8) }
|
||||||
|
END {
|
||||||
|
if (not defined $sha) {
|
||||||
|
print "echo fail git blame-jump: missing blame info";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (not $done) {
|
||||||
|
print "echo \"fail git blame-jump: line not found in annotations (blame still loading?)\"";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$info = "{Information}{\\}";
|
||||||
|
if ($sha =~ m{^0+$}) {
|
||||||
|
$old_filename = $ENV{"kak_buffile"};
|
||||||
|
$old_filename = substr $old_filename, length($ENV{"PWD"}) + 1;
|
||||||
|
$show_diff = "diff HEAD";
|
||||||
|
$info .= "Not committed yet";
|
||||||
|
} else {
|
||||||
|
$old_filename = $old_filenames{$sha};
|
||||||
|
$author = $authors{$sha};
|
||||||
|
$date = $dates{$sha};
|
||||||
|
$summary = $summaries{$sha};
|
||||||
|
$show_diff = "show $sha";
|
||||||
|
$info .= "$date $author \"$summary\"";
|
||||||
|
}
|
||||||
|
$on_close_fifo = "
|
||||||
|
evaluate-commands -draft $SQ
|
||||||
|
execute-keys <percent>
|
||||||
|
require-module diff
|
||||||
|
diff-parse BEGIN %{
|
||||||
|
\$in_file = " . escape(perlquote($old_filename)) . ";
|
||||||
|
\$in_file_line = $old_line;
|
||||||
|
} END $SQ$SQ
|
||||||
|
print \"execute-keys -client $ENV{client} \${diff_line}g<a-h>$ENV{cursor_column}l;\";
|
||||||
|
printf \"evaluate-commands -client $ENV{client} $SQ$SQ$SQ$SQ
|
||||||
|
hook -once window NormalIdle .* $SQ$SQ$SQ$SQ$SQ$SQ$SQ$SQ
|
||||||
|
execute-keys vv
|
||||||
|
echo -markup -- %s
|
||||||
|
$SQ$SQ$SQ$SQ$SQ$SQ$SQ$SQ
|
||||||
|
$SQ$SQ$SQ$SQ ;\"," . escape(escape(perlquote(escape(escape(quote($info)))))) . ";
|
||||||
|
$SQ$SQ
|
||||||
|
$SQ
|
||||||
|
";
|
||||||
|
printf "on_close_fifo=%s show_git_cmd_output %s",
|
||||||
|
shellquote($on_close_fifo), $show_diff;
|
||||||
|
}
|
||||||
|
')"
|
||||||
|
}
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
apply)
|
apply)
|
||||||
shift
|
shift
|
||||||
|
@ -401,7 +572,10 @@ define-command -params 1.. \
|
||||||
;;
|
;;
|
||||||
blame)
|
blame)
|
||||||
shift
|
shift
|
||||||
run_git_blame "$@"
|
blame_toggle "$@"
|
||||||
|
;;
|
||||||
|
blame-jump)
|
||||||
|
blame_jump
|
||||||
;;
|
;;
|
||||||
hide-blame)
|
hide-blame)
|
||||||
hide_blame
|
hide_blame
|
||||||
|
|
Loading…
Reference in New Issue
Block a user