summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes Altmanninger <aclopte@gmail.com>2024-09-14 12:25:02 +0200
committerMaxime Coste <mawww@kakoune.org>2024-09-16 15:23:18 +1000
commit54992c08aecb2fc91aecacccc15d3a17ae7390dc (patch)
treefb8cc822bb2105111b93bb1f1ad96759ac22262f
parentaac32e0f5ad3332775a4f3e3092a4fd8a6c0db62 (diff)
rc git: teach "git apply" to work on content, not just diffs
Staging/unstaging/reverting (parts of) the current buffer's file can be a common use case. Today "git apply" can do that based on a selection within a diff. When the selection is on uncommitted content, we can probably assume that the intent is to use the part of the selection that overlaps with the +-side of "git diff" (or "git diff --cached" for "git apply --cached"). Make "git apply" treat selections as content if the buffile is tracked by Git. This differentiator is not perfect but I don't know why anyone would want to use the existing "git apply" semantics on a tracked file. Maybe we should pick a different name. This feature couples well with "git show-diff", which shows all lines with unstaged changes (in future it should probably show staged changes as well). Whereas on diffs, "git apply" stages the entire hunk if the selection contains no newline, this does not happen when operating on content. I didn't yet try implementing that. I guess the hunks are not as explicit here. Closes #5225
-rw-r--r--doc/pages/changelog.asciidoc4
-rwxr-xr-xrc/filetype/diff-parse.pl8
-rw-r--r--rc/tools/git.kak125
-rwxr-xr-xrc/tools/patch-range.pl20
-rw-r--r--src/main.cc1
5 files changed, 140 insertions, 18 deletions
diff --git a/doc/pages/changelog.asciidoc b/doc/pages/changelog.asciidoc
index 3a6804e5..f443f207 100644
--- a/doc/pages/changelog.asciidoc
+++ b/doc/pages/changelog.asciidoc
@@ -5,8 +5,10 @@ released versions.
== Development version
-* Expose env vars that are mentionned in the arguments passed to shell expansions
+* Expose env vars that are mentioned in the arguments passed to shell expansions
* Support for colored double underlines
+* `git apply` can now operate on selected changes in the current buffer's
+ file (useful for quick (un)staging and reverting)
== Kakoune 2024.05.18
diff --git a/rc/filetype/diff-parse.pl b/rc/filetype/diff-parse.pl
index 7453b2b3..5da4e349 100755
--- a/rc/filetype/diff-parse.pl
+++ b/rc/filetype/diff-parse.pl
@@ -45,13 +45,15 @@ our $version = "+";
eval $begin if defined $begin;
-$in_file = "$directory/$in_file" if defined $in_file;
+$in_file = "$directory/$in_file" if defined $in_file && $in_file ne "";
# Outputs
our $diff_line = 0;
our $commit;
our $file;
our $file_line;
+our $other_file;
+our $other_file_line;
our $diff_line_text;
my $other_version;
@@ -63,8 +65,6 @@ if ($version eq "+") {
my $is_recursive_diff = 0;
my $state = "header";
my $fallback_file;
-my $other_file;
-my $other_file_line;
sub strip {
my $is_recursive_diff = shift;
@@ -127,7 +127,7 @@ 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 and defined $file and ($in_file eq "" or $file eq $in_file)) {
if (defined $in_file_line and defined $file_line and $file_line >= $in_file_line) {
last;
}
diff --git a/rc/tools/git.kak b/rc/tools/git.kak
index 0d837c8d..b98974d1 100644
--- a/rc/tools/git.kak
+++ b/rc/tools/git.kak
@@ -86,7 +86,8 @@ define-command -params 1.. \
All the optional arguments are forwarded to the git utility
Available commands:
add
- apply - alias for "patch git apply"
+ apply - run "patch git apply [<arguments>]"; if buffile is
+ tracked, use the changes to selected lines instead
blame - toggle blame annotations
blame-jump - show the commit that added the line at cursor
checkout
@@ -192,13 +193,15 @@ define-command -params 1.. \
diff_buffer_against_rev() {
rev=$1 # empty means index
shift
- buffile_relative=${kak_buffile#"$PWD/"}
+ buffile_relative=${kak_buffile#"$(git rev-parse --show-toplevel)/"}
echo >${kak_command_fifo} "evaluate-commands -save-regs | %{
set-register | %{ cat >${kak_response_fifo} }
execute-keys -client ${kak_client} -draft %{%<a-|><ret>}
}"
- git show "$rev:./${buffile_relative}" |
- git diff --no-index - ${kak_response_fifo} "$@"
+ git show "$rev:${buffile_relative}" |
+ diff - ${kak_response_fifo} "$@" |
+ sed -e "1c--- a/$buffile_relative" \
+ -e "2c+++ b/$buffile_relative"
}
blame_toggle() {
@@ -435,7 +438,7 @@ define-command -params 1.. \
}
}
}
- print "set-option buffer git_diff_flags $flags"
+ print "set-option buffer git_diff_flags $flags\n"
' )
}
@@ -728,12 +731,118 @@ define-command -params 1.. \
')"
}
- case "$1" in
- apply)
- shift
+ apply_selections() {
+ if [ -z "$(cd_bufdir >/dev/null 2>&1; git ls-files -- ":(literal)${kak_buffile}")" ]; then {
enquoted="$(printf '"%s" ' "$@")"
echo "require-module patch"
echo "patch git apply $enquoted"
+ return
+ } fi
+ base_rev=HEAD
+ index_only=false
+ index=false
+ reverse=false
+ for arg; do
+ case "$arg" in
+ (--cached) index_only=true ; base_rev= ;;
+ (--index) index=true ;;
+ (--reverse|-R) reverse=true ;;
+ esac
+ done
+ if ! $reverse && ! $index_only; then
+ echo "fail %{git apply on buffer contents doesn't make sense without --reverse or --cached}"
+ exit
+ fi
+ cd_bufdir
+ num_inserted=0
+ num_deleted=0
+ for selection_desc in $kak_selections_desc; do {
+ IFS=' .,' read anchor_line _ cursor_line _ <<-EOF
+ $selection_desc
+ EOF
+ if [ $anchor_line -lt $cursor_line ]; then
+ min_line=$anchor_line
+ max_line=$cursor_line
+ else
+ min_line=$cursor_line
+ max_line=$anchor_line
+ fi
+ intended_diff='diff_buffer_against_rev "$base_rev" -u'
+ if $index; then {
+ git update-index --refresh "${kak_buffile}" >/dev/null
+ intended_diff='git diff --no-ext-diff HEAD -- ":(literal)${kak_buffile}"'
+ } elif $index_only && $reverse; then {
+ diff=$(eval "$intended_diff")
+ if [ -n "$diff" ]; then {
+ # Convert from buffile lines to index lines.
+ for line in min_line max_line; do {
+ if ! index_line_or_error_message=$(
+ eval file_line=\$$line
+ printf %s "$diff" |
+ perl "${kak_runtime}/rc/filetype/diff-parse.pl" \
+ BEGIN '
+ $in_file = ""; # no need to check filename, there is only one
+ $in_file_line = '"$file_line"';
+ ' END '
+ $other_file_line++ if $diff_line_text =~ m{^\+};
+ $other_file_line += $in_file_line - $file_line;
+ print "$other_file_line\n";
+ '
+ ); then
+ echo fail "git apply: $index_line_or_error_message"
+ exit
+ fi
+ eval $line=$index_line_or_error_message
+ } done
+ } fi
+ intended_diff='git diff --no-ext-diff --cached -- ":(literal)${kak_buffile}"'
+ } fi
+ diff=$(eval "$intended_diff" |
+ perl "${kak_runtime}"/rc/tools/patch-range.pl -line-numbers-from-new-file \
+ $min_line $max_line sh -c cat -- "$@" # forward any --reverse arg
+ printf .) # avoid stripping newline
+ diff=${diff%.}
+ if ! printf %s "$diff" | git apply "$@"; then
+ printf >&2 "git apply: error running:\n\$ git apply %s << EOF\n" "$*"
+ printf >&2 %s "$diff"
+ printf >&2 'EOF\n'
+ echo "fail 'git apply: failed to apply selections, see *debug* buffer'"
+ exit
+ fi
+ count() {
+ printf %s "$diff" | awk '
+ BEGIN { n = 0 }
+ /^@@/,/^$/ { if ($0 ~ /^'"$1"'/) { n++ } }
+ END { print n }'
+ }
+ num_inserted=$(( $num_inserted + $(count +) ))
+ num_deleted=$(( $num_deleted + $(count -) ))
+ } done
+ if ! $index_only && ! $kak_modified; then
+ echo edit!
+ echo git update-diff
+ else
+ update_diff
+ fi
+ msg=
+ case $index_only,$reverse,$index in
+ (true,false,*) msg=Staged ;;
+ (true,true,*) msg=Unstaged ;;
+ (false,true,false) msg=Reverted ;;
+ (false,true,true) msg='Unstaged and reverted' ;;
+ esac
+ case $num_inserted,$num_deleted in
+ (*,0) msg="$msg $num_inserted inserted line(s)";;
+ (0,*) msg="$msg $num_deleted deleted line(s)";;
+ (*,*) msg="$msg $num_inserted inserted and $num_deleted deleted lines";;
+ esac
+ echo "echo -markup '{Information}{\\}$msg'"
+ }
+
+ case "$1" in
+ apply)
+ shift
+ apply_selections "$@"
;;
show|show-branch|log|diff|status)
show_git_cmd_output "$@"
diff --git a/rc/tools/patch-range.pl b/rc/tools/patch-range.pl
index 2d54ab26..01611abe 100755
--- a/rc/tools/patch-range.pl
+++ b/rc/tools/patch-range.pl
@@ -9,6 +9,12 @@ if ($ARGV[0] eq "-print-remaining-diff") {
shift @ARGV;
}
+my $line_number_kind = "diff";
+if ($ARGV[0] eq "-line-numbers-from-new-file") {
+ $line_number_kind = "new-file";
+ shift @ARGV;
+}
+
my $min_line = $ARGV[0];
shift @ARGV;
my $max_line = $ARGV[0];
@@ -22,7 +28,7 @@ if (defined $ARGV[0] and $ARGV[0] =~ m{^[^-]}) {
}
my $reverse = grep /^(--reverse|-R)$/, @ARGV;
-my $lineno = 0;
+my $lineno = $line_number_kind eq "diff" ? 0 : undef;
my $original = "";
my $diff_header = "";
my $wheat = "";
@@ -63,9 +69,9 @@ sub finish_hunk {
}
while (<STDIN>) {
- ++$lineno;
+ ++$lineno if $line_number_kind eq "diff";
$original .= $_;
- if (m{^diff}) {
+ if (m{^diff} || (not defined $state and m{^---})) {
finish_hunk();
$state = "diff header";
$diff_header = "";
@@ -74,7 +80,8 @@ while (<STDIN>) {
$signature .= $_ if $print_remaining;
next;
}
- if (m{^@@ -\d+(?:,(\d)+)? \+\d+(?:,\d+)? @@}) {
+ if (m{^@@ -\d+(?:,(\d)+)? \+(\d+)(?:,\d+)? @@}) {
+ $lineno = $2 - 1 if $line_number_kind eq "new-file";
$hunk_remaining_lines = $1 or 1;
finish_hunk();
$state = "diff hunk";
@@ -96,8 +103,11 @@ while (<STDIN>) {
$signature .= $_ if $print_remaining;
next;
}
+ ++$lineno if $line_number_kind eq "new-file" && m{^[ +]};
--$hunk_remaining_lines if m{^[ -]};
- my $include = m{^ } || ($lineno >= $min_line && $lineno <= $max_line);
+ my $include = m{^ } ||
+ ($lineno >= $min_line && $lineno <= $max_line) ||
+ ($line_number_kind eq "new-file" && m{^-} && $lineno == $min_line - 1);
if ($include) {
$hunk_wheat .= $_;
if ($print_remaining) {
diff --git a/src/main.cc b/src/main.cc
index 09c9b718..5eb30bc6 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -48,6 +48,7 @@ struct {
0,
"» kak_* appearing in shell arguments will be added to the environment\n"
"» {+U}double underline{} support\n"
+ "» {+u}git apply{} can stage/revert selected changes to current buffer\n"
}, {
20240518,
"» Fix tests failing on some platforms\n"