diff --git a/rc/filetype/diff-parse.pl b/rc/filetype/diff-parse.pl index f7858eb7a..203e9b195 100755 --- a/rc/filetype/diff-parse.pl +++ b/rc/filetype/diff-parse.pl @@ -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 () { + $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 () { $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); diff --git a/rc/tools/git.kak b/rc/tools/git.kak index b03a6d059..d7e251860 100644 --- a/rc/tools/git.kak +++ b/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 %{%} + }" + 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 %{%} - }' + 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 ^commit + } catch %{ + # Missing commit line, assume it is an uncommitted change. + execute-keys \A + } + 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 + 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$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