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
|
||||
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);
|
||||
|
|
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 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
|
||||
|
|
Loading…
Reference in New Issue
Block a user