@matheusbsbs / matheus-bagliano / commits / ac083c47ea
Fork 0

git-add--interactive: manual hunk editing mode

Adds a new option 'e' to the 'add -p' command loop that lets you edit the current hunk in your favourite editor. If the resulting patch applies cleanly, the edited hunk will immediately be marked for staging. If it does not apply cleanly, you will be given an opportunity to edit again. If all lines of the hunk are removed, then the edit is aborted and the hunk is left unchanged. Applying the changed hunk(s) relies on Johannes Schindelin's new --recount option for git-apply. Note that the "real patch" test intentionally uses (echo e; echo n; echo d) | git add -p even though the 'n' and 'd' are superfluous at first sight. They serve to get out of the interaction loop if git add -p wrongly concludes the patch does not apply. Many thanks to Jeff King <peff@peff.net> for lots of help and suggestions. Signed-off-by: Thomas Rast <trast@student.ethz.ch> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Thomas Rast committed Jul 3, 2008 at 00:00 UTC ac083c47ea226b470afab39d975e718a475a3c78
3 files changed +187
Documentation/git-add.txt
+1
@@ -236,6 +236,7 @@ patch::
236 k - leave this hunk undecided, see previous undecided hunk
237 K - leave this hunk undecided, see previous hunk
238 s - split the current hunk into smaller hunks
239 + e - manually edit the current hunk
240 ? - print help
241 +
242 After deciding the fate for all hunks, if there is any hunk
git-add--interactive.perl
+119
@@ -18,6 +18,18 @@ my ($fraginfo_color) =
18 $diff_use_color ? (
19 $repo->get_color('color.diff.frag', 'cyan'),
20 ) : ();
21 +my ($diff_plain_color) =
22 + $diff_use_color ? (
23 + $repo->get_color('color.diff.plain', ''),
24 + ) : ();
25 +my ($diff_old_color) =
26 + $diff_use_color ? (
27 + $repo->get_color('color.diff.old', 'red'),
28 + ) : ();
29 +my ($diff_new_color) =
30 + $diff_use_color ? (
31 + $repo->get_color('color.diff.new', 'green'),
32 + ) : ();
33
34 my $normal_color = $repo->get_color("", "reset");
35
@@ -683,6 +695,105 @@ sub split_hunk {
695 }
696
697
698 +sub color_diff {
699 + return map {
700 + colored((/^@/ ? $fraginfo_color :
701 + /^\+/ ? $diff_new_color :
702 + /^-/ ? $diff_old_color :
703 + $diff_plain_color),
704 + $_);
705 + } @_;
706 +}
707 +
708 +sub edit_hunk_manually {
709 + my ($oldtext) = @_;
710 +
711 + my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
712 + my $fh;
713 + open $fh, '>', $hunkfile
714 + or die "failed to open hunk edit file for writing: " . $!;
715 + print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n";
716 + print $fh @$oldtext;
717 + print $fh <<EOF;
718 +# ---
719 +# To remove '-' lines, make them ' ' lines (context).
720 +# To remove '+' lines, delete them.
721 +# Lines starting with # will be removed.
722 +#
723 +# If the patch applies cleanly, the edited hunk will immediately be
724 +# marked for staging. If it does not apply cleanly, you will be given
725 +# an opportunity to edit again. If all lines of the hunk are removed,
726 +# then the edit is aborted and the hunk is left unchanged.
727 +EOF
728 + close $fh;
729 +
730 + my $editor = $ENV{GIT_EDITOR} || $repo->config("core.editor")
731 + || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
732 + system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
733 +
734 + open $fh, '<', $hunkfile
735 + or die "failed to open hunk edit file for reading: " . $!;
736 + my @newtext = grep { !/^#/ } <$fh>;
737 + close $fh;
738 + unlink $hunkfile;
739 +
740 + # Abort if nothing remains
741 + if (!grep { /\S/ } @newtext) {
742 + return undef;
743 + }
744 +
745 + # Reinsert the first hunk header if the user accidentally deleted it
746 + if ($newtext[0] !~ /^@/) {
747 + unshift @newtext, $oldtext->[0];
748 + }
749 + return \@newtext;
750 +}
751 +
752 +sub diff_applies {
753 + my $fh;
754 + open $fh, '| git apply --recount --cached --check';
755 + for my $h (@_) {
756 + print $fh @{$h->{TEXT}};
757 + }
758 + return close $fh;
759 +}
760 +
761 +sub prompt_yesno {
762 + my ($prompt) = @_;
763 + while (1) {
764 + print colored $prompt_color, $prompt;
765 + my $line = <STDIN>;
766 + return 0 if $line =~ /^n/i;
767 + return 1 if $line =~ /^y/i;
768 + }
769 +}
770 +
771 +sub edit_hunk_loop {
772 + my ($head, $hunk, $ix) = @_;
773 + my $text = $hunk->[$ix]->{TEXT};
774 +
775 + while (1) {
776 + $text = edit_hunk_manually($text);
777 + if (!defined $text) {
778 + return undef;
779 + }
780 + my $newhunk = { TEXT => $text, USE => 1 };
781 + if (diff_applies($head,
782 + @{$hunk}[0..$ix-1],
783 + $newhunk,
784 + @{$hunk}[$ix+1..$#{$hunk}])) {
785 + $newhunk->{DISPLAY} = [color_diff(@{$text})];
786 + return $newhunk;
787 + }
788 + else {
789 + prompt_yesno(
790 + 'Your edited hunk does not apply. Edit again '
791 + . '(saying "no" discards!) [y/n]? '
792 + ) or return undef;
793 + }
794 + }
795 +}
796 +
797 sub help_patch_cmd {
798 print colored $help_color, <<\EOF ;
799 y - stage this hunk
@@ -694,6 +805,7 @@ J - leave this hunk undecided, see next hunk
805 k - leave this hunk undecided, see previous undecided hunk
806 K - leave this hunk undecided, see previous hunk
807 s - split the current hunk into smaller hunks
808 +e - manually edit the current hunk
809 ? - print help
810 EOF
811 }
@@ -798,6 +910,7 @@ sub patch_update_file {
910 if (hunk_splittable($hunk[$ix]{TEXT})) {
911 $other .= '/s';
912 }
913 + $other .= '/e';
914 for (@{$hunk[$ix]{DISPLAY}}) {
915 print;
916 }
@@ -862,6 +975,12 @@ sub patch_update_file {
975 $num = scalar @hunk;
976 next;
977 }
978 + elsif ($line =~ /^e/) {
979 + my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
980 + if (defined $newhunk) {
981 + splice @hunk, $ix, 1, $newhunk;
982 + }
983 + }
984 else {
985 help_patch_cmd($other);
986 next;
t/t3701-add-interactive.sh
+67
@@ -66,6 +66,73 @@ test_expect_success 'revert works (commit)' '
66 grep "unchanged *+3/-0 file" output
67 '
68
69 +cat >expected <<EOF
70 +EOF
71 +cat >fake_editor.sh <<EOF
72 +EOF
73 +chmod a+x fake_editor.sh
74 +test_set_editor "$(pwd)/fake_editor.sh"
75 +test_expect_success 'dummy edit works' '
76 + (echo e; echo a) | git add -p &&
77 + git diff > diff &&
78 + test_cmp expected diff
79 +'
80 +
81 +cat >patch <<EOF
82 +@@ -1,1 +1,4 @@
83 + this
84 ++patch
85 +-doesn't
86 + apply
87 +EOF
88 +echo "#!$SHELL_PATH" >fake_editor.sh
89 +cat >>fake_editor.sh <<\EOF
90 +mv -f "$1" oldpatch &&
91 +mv -f patch "$1"
92 +EOF
93 +chmod a+x fake_editor.sh
94 +test_set_editor "$(pwd)/fake_editor.sh"
95 +test_expect_success 'bad edit rejected' '
96 + git reset &&
97 + (echo e; echo n; echo d) | git add -p >output &&
98 + grep "hunk does not apply" output
99 +'
100 +
101 +cat >patch <<EOF
102 +this patch
103 +is garbage
104 +EOF
105 +test_expect_success 'garbage edit rejected' '
106 + git reset &&
107 + (echo e; echo n; echo d) | git add -p >output &&
108 + grep "hunk does not apply" output
109 +'
110 +
111 +cat >patch <<EOF
112 +@@ -1,0 +1,0 @@
113 + baseline
114 ++content
115 ++newcontent
116 ++lines
117 +EOF
118 +cat >expected <<EOF
119 +diff --git a/file b/file
120 +index b5dd6c9..f910ae9 100644
121 +--- a/file
122 ++++ b/file
123 +@@ -1,4 +1,4 @@
124 + baseline
125 + content
126 +-newcontent
127 ++more
128 + lines
129 +EOF
130 +test_expect_success 'real edit works' '
131 + (echo e; echo n; echo d) | git add -p &&
132 + git diff >output &&
133 + test_cmp expected output
134 +'
135 +
136 if test "$(git config --bool core.filemode)" = false
137 then
138 say 'skipping filemode tests (filesystem does not properly support modes)'