diff --git a/rc/tools/git.kak b/rc/tools/git.kak index 13425eaf..b0b457f1 100644 --- a/rc/tools/git.kak +++ b/rc/tools/git.kak @@ -61,6 +61,7 @@ define-command -params 1.. \ All the optional arguments are forwarded to the git utility Available commands: add + apply (alias for "patch git apply") rm reset blame @@ -81,11 +82,12 @@ define-command -params 1.. \ grep } -shell-script-candidates %{ if [ $kak_token_to_complete -eq 0 ]; then - printf "add\nrm\nreset\nblame\ncommit\ncheckout\ndiff\nhide-blame\nhide-diff\nlog\nnext-hunk\nprev-hunk\nshow\nshow-branch\nshow-diff\ninit\nstatus\nupdate-diff\ngrep\n" + printf "add\napply\nrm\nreset\nblame\ncommit\ncheckout\ndiff\nhide-blame\nhide-diff\nlog\nnext-hunk\nprev-hunk\nshow\nshow-branch\nshow-diff\ninit\nstatus\nupdate-diff\ngrep\n" else case "$1" in commit) printf -- "--amend\n--no-edit\n--all\n--reset-author\n--fixup\n--squash\n"; git ls-files -m ;; add) git ls-files -dmo --exclude-standard ;; + apply) printf -- "--reverse\n--cached\n--index\n" ;; rm|grep) git ls-files -c ;; esac fi @@ -326,6 +328,12 @@ define-command -params 1.. \ } case "$1" in + apply) + shift + enquoted="$(printf '"%s" ' "$@")" + echo "require-module patch" + echo "patch git apply $enquoted" + ;; show|show-branch|log|diff|status) show_git_cmd_output "$@" ;; diff --git a/rc/tools/patch-range.pl b/rc/tools/patch-range.pl new file mode 100755 index 00000000..10cc74f4 --- /dev/null +++ b/rc/tools/patch-range.pl @@ -0,0 +1,93 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +my $min_line = $ARGV[0]; +shift @ARGV; +my $max_line = $ARGV[0]; +shift @ARGV; + +my $patch_cmd; +if (defined $ARGV[0] and $ARGV[0] =~ m{^[^-]}) { + $patch_cmd = "@ARGV"; +} else { + $patch_cmd = "patch @ARGV"; +} +my $reverse = grep /^(--reverse|-R)$/, @ARGV; + +my $lineno = 0; +my $original = ""; +my $wheat = ""; +my $chaff = ""; +my $state = undef; +my $hunk_wheat = undef; +my $hunk_chaff = undef; +my $hunk_header = undef; + +sub compute_hunk_header { + my $original_header = shift; + my $hunk = shift; + my $old_lines = 0; + my $new_lines = 0; + for (split /\n/, $hunk) { + $old_lines++ if m{^[ -]}; + $new_lines++ if m{^[ +]}; + } + my $updated_header = $original_header =~ s/^@@ -(\d+),\d+\s+\+(\d+),\d+ @@(.*)/@@ -$1,$old_lines +$2,$new_lines @\@$3/mr; + return $updated_header; +} + +sub finish_hunk { + return unless defined $hunk_header; + if ($hunk_wheat =~ m{^[-+]}m) { + $wheat .= (compute_hunk_header $hunk_header, $hunk_wheat). $hunk_wheat; + } + $chaff .= (compute_hunk_header $hunk_header, $hunk_chaff) . $hunk_chaff; + $hunk_header = undef; +} + +while () { + ++$lineno; + $original .= $_; + if (m{^diff}) { + finish_hunk(); + $state = "diff header"; + } + if (m{^@@}) { + finish_hunk(); + $state = "diff hunk"; + $hunk_header = $_; + $hunk_wheat = ""; + $hunk_chaff = ""; + next; + } + if ($state eq "diff header") { + $wheat .= $_; + $chaff .= $_; + next; + } + my $include = m{^ } || ($lineno >= $min_line && $lineno <= $max_line); + if ($include) { + $hunk_wheat .= $_; + $hunk_chaff .= $_ if m{^ }; + if ($reverse ? m{^[-]} : m{^\+}) { + $hunk_chaff .= " " . substr $_, 1; + } + } else { + if ($reverse ? m{^\+} : m{^-}) { + $hunk_wheat .= " " . substr $_, 1; + } + $hunk_chaff .= $_; + } +} +finish_hunk(); + +open PATCH_COMMAND, "|-", "$patch_cmd 1>&2" or die "patch-range.pl: error running '$patch_cmd': $!"; +print PATCH_COMMAND $wheat; +if (not close PATCH_COMMAND) { + print $original; + print STDERR "patch-range.pl: error running:\n" . "\$ $patch_cmd << EOF\n$wheat" . "EOF\n"; + exit 1; +} +print $chaff; diff --git a/rc/tools/patch.kak b/rc/tools/patch.kak new file mode 100644 index 00000000..fe635acf --- /dev/null +++ b/rc/tools/patch.kak @@ -0,0 +1,66 @@ +define-command patch -params .. -docstring %{ + patch []: apply selections in diff to a file + + Given some selections within a unified diff, apply the changed lines in + each selection by piping them to "patch 1>&2" + (or " 1>&2" if starts with a non-option argument). + If successful, the in-buffer diff will be updated to reflect the applied + changes. + For selections that contain no newline, the entire enclosing diff hunk + is applied (unless the cursor is inside a diff header, in which case + the entire diff is applied). + To revert changes, must contain "--reverse" or "-R". +} %exp{ + evaluate-commands -draft -itersel -save-regs aefs|^ %%{ + set-register f %val{source} + %{ + try %{ + execute-keys \n + } catch %{ + # The selection contains no newline. + execute-keys -save-regs '' Z + execute-keys ^diff + try %{ + execute-keys ^@@ + # If the cursor is in a diff hunk, stage the entire hunk. + execute-keys z + execute-keys /.*?(?:(?=\n@@)|(?=\ndiff)|(?=\n\n)|\z)x^@@ + } catch %{ + # If the cursor is in a diff header, stage the entire diff. + execute-keys ?.*?(?:(?=\ndiff)|(?=\n\n)|\z) + } + } + # We want to apply only the selected lines. Remember them. + execute-keys + set-register s %val{selection_desc} + # Select forward until the end of the last hunk. + execute-keys H?.*?(?:(?=\n@@)|(?=\ndiff)|(?=\n\n)|\z)x + # Select backward to the beginning of the first hunk's diff header. + execute-keys ^diff + # Move cursor to the beginning so we know the diff's offset within the buffer. + execute-keys + set-register a %arg{@} + set-register e nop + set-register | %{ + # The selected range to apply. + IFS=' .,' read min_line _ max_line _ <<-EOF + $kak_reg_s + EOF + min_line=$((min_line - kak_cursor_line + 1)) + max_line=$((max_line - kak_cursor_line + 1)) + + # Since registers are never empty, we get an empty arg even if + # there were no args. This does no harm because we pass it to + # a shell where it expands to nothing. + eval set -- $kak_quoted_reg_a + + "${kak_reg_f%/*}/patch-range.pl" $min_line $max_line "$@" || + echo >$kak_command_fifo "set-register e fail 'patch: failed to apply selections, see *debug* buffer'" + } + execute-keys | + %reg{e} + } + } +} + +provide-module patch %ยงยง