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:
Johannes Altmanninger 2024-02-03 00:26:56 +01:00 committed by Maxime Coste
parent 36efbf4cbf
commit 86d940c225
2 changed files with 209 additions and 19 deletions

View File

@ -39,11 +39,17 @@ while (defined $ARGV[0]) {
# Inputs
our $directory = $ENV{PWD};
our $strip;
our $in_file;
our $in_file_line;
our $version = "+";
eval $begin if defined $begin;
$in_file = "$directory/$in_file" if defined $in_file;
# Outputs
our $diff_line = 0;
our $commit;
our $file;
our $file_line;
our $diff_line_text;
@ -83,8 +89,13 @@ sub strip {
}
while (<STDIN>) {
$diff_line++;
s/^(> )*//g;
$diff_line_text = $_;
if (m{^commit (\w+)}) {
$commit = $1;
next;
}
if (m{^diff\b}) {
$state = "header";
$is_recursive_diff = 1;
@ -116,6 +127,11 @@ while (<STDIN>) {
$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) {
$file = ($fallback_file or $other_file);

View File

@ -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 str git_blame
declare-option -hidden line-specs git_diff_flags
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
Available commands:
add
apply (alias for "patch git apply")
blame (toggle blame annotations)
apply - alias for "patch git apply"
blame - toggle blame annotations
blame-jump - show the commit that added the line at cursor
checkout
commit
diff
@ -107,6 +109,7 @@ define-command -params 1.. \
printf %s\\n \
apply \
blame \
blame-jump \
checkout \
commit \
diff \
@ -142,6 +145,9 @@ define-command -params 1.. \
exit 1
}
}
kakquote() {
printf "%s" "$1" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"
}
show_git_cmd_output() {
local filetype
@ -158,21 +164,38 @@ define-command -params 1.. \
mkfifo ${output}
( 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*
set-option buffer filetype '${filetype}'
hook -always -once buffer BufCloseFifo .* %{ nop %sh{ rm -r $(dirname ${output}) } }
}"
set-option buffer filetype ${filetype}
hook -always -once buffer BufCloseFifo .* ''
nop %sh{ rm -r $(dirname ${output}) }
$(printf %s "${on_close_fifo}" | sed "s/'/''''/g")
''
'"
}
hide_blame() {
printf %s "
set-option buffer=$kak_bufname git_blame_flags $kak_timestamp
set-option buffer=$kak_bufname 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 %{
add-highlighter window/git-blame flag-lines Information git_blame_flags
echo -to-file ${kak_response_fifo}
@ -180,22 +203,21 @@ define-command -params 1.. \
echo -to-file ${kak_response_fifo} 'hide_blame; exit'
}"
eval $(cat ${kak_response_fifo})
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>}
}'
eval "$prepare_git_blame_args"
(
cd_bufdir
printf %s "evaluate-commands -client '$kak_client' %{
set-option buffer=$kak_bufname git_blame_flags '$kak_timestamp'
set-option buffer=$kak_bufname git_blame ''
}" | 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);
sub quote {
my $SQ = "'\''";
my $token = shift;
$token =~ s/$SQ/$SQ$SQ/g;
return "$SQ$token$SQ";
}
sub send_flags {
my $flush = shift;
if (not defined $line) { return; }
@ -210,11 +232,14 @@ define-command -params 1.. \
return
}
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);
$flags = "";
$raw_blame = "";
$last_sent = $now;
}
$raw_blame .= $_;
chomp;
if (m/^([0-9a-f]+) ([0-9]+) ([0-9]+) ([0-9]+)/) {
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
apply)
shift
@ -401,7 +572,10 @@ define-command -params 1.. \
;;
blame)
shift
run_git_blame "$@"
blame_toggle "$@"
;;
blame-jump)
blame_jump
;;
hide-blame)
hide_blame