git@vger.kernel.org mailing list mirror (one of many)
 help / Atom feed
From: Phillip Wood <phillip.wood@talktalk.net>
To: Git Mailing List <git@vger.kernel.org>,
	Ævar Arnfjörð Bjarmason  <avarab@gmail.com>
Cc: Igor Djordjevic <igor.d.djordjevic@gmail.com>,
	Junio C Hamano <gitster@pobox.com>,
	Phillip Wood <phillip.wood@dunelm.org.uk>
Subject: [RFC PATCH v5 2/4] add -p: select modified lines correctly
Date: Thu, 26 Jul 2018 16:58:52 +0100
Message-ID: <20180726155854.20832-3-phillip.wood@talktalk.net> (raw)
In-Reply-To: <20180726155854.20832-1-phillip.wood@talktalk.net>

From: Phillip Wood <phillip.wood@dunelm.org.uk>

When a set of lines is modified the hunk contains deletions followed
by insertions. To correctly stage a subset of the modified lines we
need to match up the selected deletions with the selected insertions
otherwise we end up with deletions and context lines followed by
insertions which is not what we want.

For example given the hunk
      1 -* a longer description of the
      2 -  first item
      3 -* second
      4 -* third
      5 +* first
      6 +  second item
      7 +* the third item

If the user selects 1,2,4–5,7 then we should generate
	-* a longer description of the
	-  first item
	+* first
	 * second
	-* third
	+* the third item

not
	-* a longer description of the
	-  first item
	 * second
	-* third
	+* first
	+* the third item

Currently the code can only cope with selections that contain the same
number of groups of insertions and deletions, though each group need
not contain the same number of insertions and deletions. If the user
wants to stage an unpaired deletion or insertion in a hunk where they
also want to stage modified lines they have to do it with two
invocations of 'git add -p'.

It would be possible to add some syntax to allow lines to be excluded
from groups to allow the user to stage such changes in a single
go. It may also be useful to allow users to explicitly group lines. If
in the example above the second item is deleted we have

      1 -* a longer description of the
      2 - first item
      3 -* second
      4 -* third
      5 +* first
      6 +* the third item

Selecting 1,2,4–6 will give an error. If lines could be grouped
explicitly then it would be possible to type something like
1,2,4,[5],6 to indicate that there are two groups of insertions giving

	-* a longer description of the
	- first item
	+* first
	 * second
	-* third
	+* the third item

We may want to be able to stage an insertion before an unselected
deletion to allow the user to stage a new paragraph before the
unmodified original in

      1 -original
      2 +a new paragraph before
      3 +original
      4 +
      5 +modified original

by specifying something like ^2-4 to give

	+a new paragraph before
	+original
	+
	 original

I'm not sure how common these cases are in real life and how much
effort it's worth putting into handling them at the moment when the
user can edit the hunk if need be. Perhaps it would be better to leave
it for future extensions when it becomes clearer what would be most
useful.

Reported-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
---
 git-add--interactive.perl  | 147 ++++++++++++++++++++++++++++++++++---
 t/t3701-add-interactive.sh | 108 ++++++++++++++++++++++++++-
 2 files changed, 242 insertions(+), 13 deletions(-)

diff --git a/git-add--interactive.perl b/git-add--interactive.perl
index cbc9e5698a..7e4daee2fc 100755
--- a/git-add--interactive.perl
+++ b/git-add--interactive.perl
@@ -1015,26 +1015,47 @@ sub color_diff {
 
 use constant {
 	NO_NEWLINE => 1,
+	LAST_ADD_DEL => 2,
+	FIRST_ADD => 4
 };
 
 sub label_hunk_lines {
 	my $hunk = shift;
 	my $text = $hunk->{TEXT};
-	my (@line_flags, @lines);
+	# A block contains the insertions and deletions occurring context
+	# lines.
+	my (@blocks, @line_flags, @lines, @modes);
 	my ($block, $label, $last_mode) = (0, 0, '');
 	for my $line (1..$#{$text}) {
 		$line_flags[$line] = 0;
 		my $mode = substr($text->[$line], 0, 1);
 		if ($mode eq '\\') {
 			$line_flags[$line - 1] |= NO_NEWLINE;
 		}
+		if ($mode ne '-' and $last_mode eq '-' or
+		    $mode ne '+' and $last_mode eq '+') {
+			$line_flags[$line - 1] |= LAST_ADD_DEL;
+		}
+		if ($mode eq '+' and $last_mode ne '+') {
+			$line_flags[$line] |= FIRST_ADD;
+		}
 		if ($mode eq '-' or $mode eq '+') {
-			$lines[++$label] = $line;
+			$blocks[++$label] = $block;
+			$lines[$label] = $line;
+			$modes[$label] = $mode;
+		} elsif ($mode eq ' ' and $last_mode ne ' ') {
+			$block++;
 		}
+		$last_mode = $mode;
+	}
+	if ($last_mode eq '-' or $last_mode eq '+') {
+		$line_flags[-1] |= LAST_ADD_DEL;
 	}
 	if ($label > 1) {
 		$hunk->{LABELS} = {
+			BLOCKS => \@blocks,
 			LINES => \@lines,
+			MODES => \@modes
 		};
 		$hunk->{LINE_FLAGS} = \@line_flags;
 		return 1;
@@ -1061,11 +1082,14 @@ sub select_hunk_lines {
 		}
 	};
 
-	my ($lo, $hi) = splice(@$selected, 0, 2);
+	my ($lo, $hi, $pre_insert, $post_insert) = splice(@$selected, 0, 4);
 	# Lines with this mode will become context lines if they are
 	# not selected
 	my $context_mode = $patch_mode_flavour{IS_REVERSE} ? '+' : '-';
 	for $i (1..$#{$text}) {
+		if ($i == $lo and defined($pre_insert)) {
+			$select_lines->(@$pre_insert);
+		}
 		if ($lo <= $i and $i <= $hi) {
 			$select_lines->($i);
 		} else {
@@ -1080,8 +1104,12 @@ sub select_hunk_lines {
 			}
 		}
 		if ($i == $hi) {
+			if (defined($post_insert)) {
+				$select_lines->(@$post_insert);
+			}
 			if (@$selected) {
-				($lo, $hi) = splice(@$selected, 0, 2);
+				($lo, $hi, $pre_insert, $post_insert) =
+						splice(@$selected, 0, 4);
 			}
 		}
 	}
@@ -1104,6 +1132,108 @@ sub select_hunk_lines {
 	return $newhunk;
 }
 
+sub pair_hunk_selection {
+	my $line_flags = shift;
+	my @ret;
+	my $reverse = $patch_mode_flavour{IS_REVERSE};
+	for my $sel (@_) {
+		my $i = 0;
+		my ($del, $add) = @$sel;
+		my $ndel = @$del / 2;
+		my $nadd = @$add / 2;
+		# If there the same number of insertion and deletion
+		# groups then pair up in insertions and deletions.
+		if ($nadd == $ndel) {
+			while ($i < @{$del}) {
+				my ($ra, $rb) = $reverse ? ($add, $del) :
+							   ($del, $add);
+				my ($starta, $enda) = @{$ra}[$i, $i + 1];
+				my ($startb, $endb) = @{$rb}[$i, $i + 1];
+				if ($line_flags->[$enda] & LAST_ADD_DEL and
+				    $line_flags->[$endb] & NO_NEWLINE) {
+					$endb++;
+				}
+				if ($line_flags->[$enda] & NO_NEWLINE) {
+					$enda++;
+				}
+				push @ret, $reverse ?
+					($starta, $enda,
+					 [ $startb..$endb ], undef) :
+					($starta, $enda,
+					 undef, [ $startb..$endb ]);
+				$i += 2;
+			}
+		# If there are only deletions or only insertions or
+		# a) we're adding to the index and there is a single group
+		#    of insertions starting at the first inserted line
+		# or
+		# b) we're resetting the index and there is a single group
+		#    of deletions finishing with the last deleted line
+		# then it is okay just to have deletions followed by
+		# insertions.
+		} elsif (!$nadd or !$ndel or
+			 (!$reverse and $ndel == 1 and
+			  $line_flags->[$del->[1]] & LAST_ADD_DEL) or
+			 ($reverse and $nadd == 1 and
+			  $line_flags->[$add->[0]] & FIRST_ADD)) {
+			while ($i < @$del) {
+				my ($start, $end) = @{$del}[$i, $i + 1];
+				if ($line_flags->[$end] & NO_NEWLINE) {
+					$end++;
+				}
+				push @ret, $start, $end, undef, undef;
+				$i += 2;
+			}
+			$i = 0;
+			while ($i < @$add) {
+				my ($start, $end) = @{$add}[$i, $i + 1];
+				if ($line_flags->[$end] & NO_NEWLINE) {
+					$end++;
+				}
+				push @ret, $start, $end, undef, undef;
+				$i += 2;
+			}
+		} else {
+			printf STDERR __("unable to pair up insertions and deletions\n");
+			return undef;
+		}
+	}
+	return \@ret;
+}
+
+sub process_hunk_selection {
+	my ($labels, $line_flags) = @{shift()}{qw(LABELS LINE_FLAGS)};
+	my ($blocks, $lines, $modes) =
+				@{$labels}{qw(BLOCKS LINES MODES)};
+	my @selection = sort { $a <=> $b } @_;
+	unless (@selection) {
+		return [];
+	}
+	my $last_label = shift @selection;
+	my ($last_block, $last_line, $last_mode) =
+	    ($blocks->[$last_label], $lines->[$last_label], $modes->[$last_label]);
+	my ($del, $add) = ([], []);
+	push @{$last_mode eq '-' ? $del : $add}, $last_line;
+	my @sel;
+	for my $label (@selection) {
+		my ($block, $line, $mode) =
+			($blocks->[$label], $lines->[$label], $modes->[$label]);
+		if ($block != $last_block) {
+			push @{$last_mode eq '-' ? $del : $add}, $last_line;
+			push @sel, [ $del, $add ];
+			($del, $add) = ([], []);
+			push @{$mode eq '-' ? $del : $add}, $line;
+		} elsif ($line != $last_line + 1 or $mode ne $last_mode) {
+			push @{$last_mode eq '-' ? $del : $add}, $last_line;
+			push @{$mode eq '-' ? $del : $add}, $line;
+		}
+		($last_block, $last_line, $last_mode) = ($block, $line, $mode);
+	}
+	push @{$last_mode eq '-' ? $del : $add}, $last_line;
+	push @sel, [ $del, $add ];
+	pair_hunk_selection($line_flags, @sel);
+}
+
 sub check_hunk_label {
 	my ($max_label, $label) = @_;
 	if ($label < 1 or $label > $max_label) {
@@ -1138,14 +1268,7 @@ sub parse_hunk_selection {
 			return undef;
 		}
 	}
-	[ map {
-		my $line = $lines->[$_];
-		if ($hunk->{LINE_FLAGS}->[$line] & NO_NEWLINE) {
-			($line, $line + 1);
-		} else {
-			($line, $line);
-		}
-	} sort { $a <=> $b } keys(%selected) ];
+	return process_hunk_selection($hunk, keys(%selected));
 }
 
 sub display_hunk_lines {
diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh
index 5b535a22d5..1d917ad018 100755
--- a/t/t3701-add-interactive.sh
+++ b/t/t3701-add-interactive.sh
@@ -489,7 +489,7 @@ test_expect_success 'setup expected diff' '
 	EOF
 '
 
-test_expect_failure 'can stage modified lines of patch (1)' '
+test_expect_success 'can stage modified lines of patch (1)' '
 	git reset &&
 	printf "%s\n" l "1,3 7-9 12" |
 	EDITOR=: git add -p 2>error &&
@@ -498,6 +498,112 @@ test_expect_failure 'can stage modified lines of patch (1)' '
 	diff_cmp expected actual
 '
 
+test_expect_success 'setup expected diff' '
+	cat >expected <<-\EOF
+	diff --git a/test b/test
+	index 0889435..cc6163b 100644
+	--- a/test
+	+++ b/test
+	@@ -1,6 +1,7 @@
+	 10
+	-20
+	+twenty
+	+thirty
+	 30
+	 40
+	 50
+	-60
+	+sixty
+	\ No newline at end of file
+	EOF
+'
+
+test_expect_success 'can stage modified lines of patch (2)' '
+	git reset &&
+	printf "%s\n" l "2,6,8,9,12" |
+	EDITOR=: git add -p 2>error &&
+	test_must_be_empty error &&
+	git diff --cached HEAD >actual &&
+	diff_cmp expected actual
+'
+
+test_expect_success 'setup HEAD' '
+	git reset &&
+	git apply --cached <<-\EOF &&
+	diff --git a/test b/test
+	--- a/test
+	+++ b/test
+	@@ -3,4 +3,4 @@
+	 30
+	 40
+	 50
+	-60
+	+60
+	\ No newline at end of file
+	EOF
+	git commit
+'
+
+test_expect_success 'setup expected diff' '
+	cat >expected <<-\EOF
+	diff --git a/test b/test
+	index 0889435..cc6163b 100644
+	--- a/test
+	+++ b/test
+	@@ -1,6 +1,8 @@
+	-10
+	+ten
+	+twenty
+	+thirty
+	 20
+	-30
+	+fifty
+	 40
+	 50
+	 60
+	\ No newline at end of file
+	EOF
+'
+
+test_expect_success 'can stage modified lines of patch (3)' '
+	git reset &&
+	printf "%s\n" l "11,1,3 7-9" |
+	EDITOR=: git add -p 2>error &&
+	test_must_be_empty error &&
+	git diff --cached HEAD >actual &&
+	diff_cmp expected actual
+'
+
+test_expect_success 'setup expected diff' '
+	cat >expected <<-\EOF
+	diff --git a/test b/test
+	index 0889435..cc6163b 100644
+	--- a/test
+	+++ b/test
+	@@ -1,6 +1,8 @@
+	-10
+	+ten
+	+twenty
+	+thirty
+	 20
+	 30
+	 40
+	 50
+	-60
+	\ No newline at end of file
+	+fifty
+	EOF
+'
+
+test_expect_success 'can stage modified lines of patch (4)' '
+	git reset &&
+	printf "%s\n" l "9-6 11,1" |
+	EDITOR=: git add -p 2>error &&
+	test_must_be_empty error &&
+	git diff --cached HEAD >actual &&
+	diff_cmp expected actual
+'
+
 test_expect_success 'patch mode ignores unmerged entries' '
 	git reset --hard &&
 	test_commit conflict &&
-- 
2.18.0


  parent reply index

Thread overview: 53+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-02-19 11:36 [PATCH v1 0/3] add -p: select individual hunk lines Phillip Wood
2018-02-19 11:36 ` [PATCH v1 1/3] " Phillip Wood
2018-02-19 11:36 ` [PATCH v1 2/3] add -p: allow line selection to be inverted Phillip Wood
2018-02-19 11:36 ` [PATCH v1 3/3] add -p: optimize line selection for short hunks Phillip Wood
2018-02-19 12:20 ` [PATCH v1 0/3] add -p: select individual hunk lines Gustavo Leite
2018-03-06 10:17 ` [PATCH v2 " Phillip Wood
2018-03-06 10:17   ` [PATCH v2 1/3] " Phillip Wood
2018-03-06 20:29     ` Igor Djordjevic
2018-03-06 21:33       ` Igor Djordjevic
2018-03-06 10:17   ` [PATCH v2 2/3] add -p: allow line selection to be inverted Phillip Wood
2018-03-06 19:57     ` Junio C Hamano
2018-03-08 11:05       ` Phillip Wood
2018-03-08 17:53         ` Junio C Hamano
2018-03-13 12:06           ` Phillip Wood
2018-03-13 16:32             ` Junio C Hamano
2018-03-14 11:02               ` Phillip Wood
2018-03-06 20:41     ` Igor Djordjevic
2018-03-06 10:17   ` [PATCH v2 3/3] add -p: optimize line selection for short hunks Phillip Wood
2018-03-06 20:33     ` Igor Djordjevic
2018-03-06 20:19   ` [PATCH v2 0/3] add -p: select individual hunk lines Igor Djordjevic
2018-03-06 21:03     ` Junio C Hamano
2018-03-06 21:20       ` Igor Djordjevic
2018-03-16 10:13 ` [PATCH v3 " Phillip Wood
2018-03-16 10:13   ` [PATCH v3 1/3] " Phillip Wood
2018-03-16 10:13   ` [PATCH v3 2/3] add -p: allow line selection to be inverted Phillip Wood
2018-03-16 10:13   ` [PATCH v3 3/3] add -p: optimize line selection for short hunks Phillip Wood
2018-03-29 18:32   ` [PATCH v3 0/3] add -p: select individual hunk lines Junio C Hamano
2018-03-30 11:09     ` Phillip Wood
2018-03-31 19:20       ` Ævar Arnfjörð Bjarmason
2018-04-02 10:55         ` Phillip Wood
2018-04-02 11:39           ` Ævar Arnfjörð Bjarmason
2018-07-26 10:22 ` [RFC PATCH v4 0/4] " Phillip Wood
2018-07-26 10:22   ` [PATCH v4 1/4] " Phillip Wood
2018-07-26 10:22   ` [RFC PATCH v4 2/4] add -p: select modified lines correctly Phillip Wood
2018-07-26 10:22   ` [PATCH v4 3/4] add -p: allow line selection to be inverted Phillip Wood
2018-07-26 10:22   ` [PATCH v4 4/4] add -p: optimize line selection for short hunks Phillip Wood
2018-07-26 15:58 ` [RFC PATCH v5 0/4] add -p: select individual hunk lines Phillip Wood
2018-07-26 15:58   ` [PATCH v5 1/4] " Phillip Wood
2018-07-26 19:36     ` Junio C Hamano
2018-07-27 10:05       ` Phillip Wood
2018-07-27 16:09         ` Junio C Hamano
2018-07-26 15:58   ` Phillip Wood [this message]
2018-07-26 19:30     ` [RFC PATCH v5 2/4] add -p: select modified lines correctly Junio C Hamano
2018-07-27 10:19       ` Phillip Wood
2018-07-27 16:14         ` Junio C Hamano
2018-07-26 15:58   ` [PATCH v5 3/4] add -p: allow line selection to be inverted Phillip Wood
2018-07-26 15:58   ` [PATCH v5 4/4] add -p: optimize line selection for short hunks Phillip Wood
2018-07-27 18:27   ` [RFC PATCH v5 0/4] add -p: select individual hunk lines Ævar Arnfjörð Bjarmason
2018-07-28 10:08     ` Phillip Wood
2018-07-28 12:40   ` Ævar Arnfjörð Bjarmason
2018-08-03 10:01     ` Phillip Wood
2018-08-03 16:51       ` Junio C Hamano
2018-08-03 17:59       ` Ævar Arnfjörð Bjarmason

Reply instructions:

You may reply publically to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: http://vger.kernel.org/majordomo-info.html

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20180726155854.20832-3-phillip.wood@talktalk.net \
    --to=phillip.wood@talktalk.net \
    --cc=avarab@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --cc=igor.d.djordjevic@gmail.com \
    --cc=phillip.wood@dunelm.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

git@vger.kernel.org mailing list mirror (one of many)

Archives are clonable:
	git clone --mirror https://public-inbox.org/git
	git clone --mirror http://ou63pmih66umazou.onion/git
	git clone --mirror http://czquwvybam4bgbro.onion/git
	git clone --mirror http://hjrcffqmbrq6wope.onion/git

Newsgroups are available over NNTP:
	nntp://news.public-inbox.org/inbox.comp.version-control.git
	nntp://ou63pmih66umazou.onion/inbox.comp.version-control.git
	nntp://czquwvybam4bgbro.onion/inbox.comp.version-control.git
	nntp://hjrcffqmbrq6wope.onion/inbox.comp.version-control.git
	nntp://news.gmane.org/gmane.comp.version-control.git

 note: .onion URLs require Tor: https://www.torproject.org/
       or Tor2web: https://www.tor2web.org/

AGPL code for this site: git clone https://public-inbox.org/ public-inbox