From: Junio C Hamano Date: Fri, 30 Mar 2018 19:42:06 +0000 (-0700) Subject: Merge branch 'pw/add-p-select' into next X-Git-Url: https://git.lorimer.id.au/gitweb.git/diff_plain/eae69f5ded514f6c7a961372f0dd67e6429e861c?hp=c48e98c1b1705fa4d1792757dce16d3b6264008c Merge branch 'pw/add-p-select' into next "git add -p" interactive interface learned to let users choose individual added/removed lines to be used in the operation, instead of accepting or rejecting a whole hunk. * pw/add-p-select: add -p: optimize line selection for short hunks add -p: allow line selection to be inverted add -p: select individual hunk lines --- diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt index d50fa339dc..f3c81dfb11 100644 --- a/Documentation/git-add.txt +++ b/Documentation/git-add.txt @@ -332,10 +332,20 @@ patch:: J - leave this hunk undecided, see next hunk k - leave this hunk undecided, see previous undecided hunk K - leave this hunk undecided, see previous hunk + l - select hunk lines to use s - split the current hunk into smaller hunks e - manually edit the current hunk ? - print help + +If you press "l" then the hunk will be reprinted with each insertion or +deletion labelled with a number and you will be prompted to enter which +lines you wish to select. Individual line numbers should be separated by +a space or comma (these can be omitted if there are fewer than ten +labelled lines), to specify a range of lines use a dash between them. If +the upper bound of a range of lines is omitted it defaults to the last +line. To invert the selection prefix it with "-" so "-3-5,8" will select +everything except lines 3, 4, 5 and 8. ++ After deciding the fate for all hunks, if there is any hunk that was chosen, the index is updated with the selected hunks. + diff --git a/git-add--interactive.perl b/git-add--interactive.perl index d190469cd8..cb48fc3e8e 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -1021,6 +1021,171 @@ sub color_diff { } @_; } +sub label_hunk_lines { + local $_; + my $hunk = shift; + my $i = 0; + my $labels = [ map { /^[-+]/ ? ++$i : 0 } @{$hunk->{TEXT}} ]; + if ($i > 1) { + @{$hunk}{qw(LABELS MAX_LABEL)} = ($labels, $i); + return 1; + } + return 0; +} + +sub select_hunk_lines { + my ($hunk, $selected) = @_; + my ($text, $labels) = @{$hunk}{qw(TEXT LABELS)}; + my ($i, $o_cnt, $n_cnt) = (0, 0, 0); + my ($push_eol, @newtext); + # 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}) { + my $mode = substr($text->[$i], 0, 1); + if ($mode eq '\\') { + push @newtext, $text->[$i] if ($push_eol); + undef $push_eol; + } elsif ($labels->[$i] and $selected->[$labels->[$i]]) { + push @newtext, $text->[$i]; + if ($mode eq '+') { + $n_cnt++; + } else { + $o_cnt++; + } + $push_eol = 1; + } elsif ($mode eq ' ' or $mode eq $context_mode) { + push @newtext, ' ' . substr($text->[$i], 1); + $o_cnt++; $n_cnt++; + $push_eol = 1; + } else { + undef $push_eol; + } + } + my ($o_ofs, $orig_o_cnt, $n_ofs, $orig_n_cnt) = + parse_hunk_header($text->[0]); + unshift @newtext, format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt); + my $newhunk = { + TEXT => \@newtext, + DISPLAY => [ color_diff(@newtext) ], + OFS_DELTA => $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt, + TYPE => $hunk->{TYPE}, + USE => 1, + }; + # If this hunk has previously been edited add the offset delta + # of the old hunk to get the real delta from the original + # hunk. + if ($hunk->{OFS_DELTA}) { + $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA}; + } + return $newhunk; +} + +sub check_hunk_label { + my ($max_label, $label) = ($_[0]->{MAX_LABEL}, $_[1]); + if ($label < 1 or $label > $max_label) { + error_msg sprintf(__("invalid hunk line '%d'\n"), $label); + return 0; + } + return 1; +} + +sub split_hunk_selection { + local $_; + my @fields = @_; + my @ret; + for (@fields) { + while ($_ ne '') { + if (/^[0-9]-$/) { + push @ret, $_; + last; + } elsif (/^([0-9](?:-[0-9])?)(.*)/) { + push @ret, $1; + $_ = $2; + } else { + error_msg sprintf + __("invalid hunk line '%s'\n"), + substr($_, 0, 1); + return (); + } + } + } + return @ret; +} + +sub parse_hunk_selection { + local $_; + my ($hunk, $line) = @_; + my ($max_label, $invert) = ($hunk->{MAX_LABEL}, undef); + my @selected = (0) x ($max_label + 1); + my @fields = split(/[,\s]+/, $line); + if ($fields[0] =~ /^-(.*)/) { + $invert = 1; + if ($1 ne '') { + $fields[0] = $1; + } else { + shift @fields; + unless (@fields) { + error_msg __("no lines to invert\n"); + return undef; + } + } + } + if ($max_label < 10) { + @fields = split_hunk_selection(@fields) or return undef; + } + for (@fields) { + if (my ($lo, $hi) = /^([0-9]+)-([0-9]*)$/) { + if ($hi eq '') { + $hi = $max_label; + } + check_hunk_label($hunk, $lo) or return undef; + check_hunk_label($hunk, $hi) or return undef; + if ($hi < $lo) { + ($lo, $hi) = ($hi, $lo); + } + @selected[$lo..$hi] = (1) x (1 + $hi - $lo); + } elsif (/^([0-9]+)$/) { + check_hunk_label($hunk, $1) or return undef; + $selected[$1] = 1; + } else { + error_msg sprintf(__("invalid hunk line '%s'\n"), $_); + return undef; + } + } + if ($invert) { + @selected = map { !$_ } @selected; + } + return \@selected; +} + +sub display_hunk_lines { + my ($display, $labels, $max_label) = + @{$_[0]}{qw(DISPLAY LABELS MAX_LABEL)}; + my $width = int(log($max_label) / log(10)) + 1; + my $padding = ' ' x ($width + 1); + for my $i (0..$#{$display}) { + if ($labels->[$i]) { + printf '%*d %s', $width, $labels->[$i], $display->[$i]; + } else { + print $padding . $display->[$i]; + } + } +} + +sub select_lines_loop { + my $hunk = shift; + display_hunk_lines($hunk); + my $selection = undef; + until (defined $selection) { + print colored $prompt_color, __("select lines? "); + my $text = ; + defined $text and $text =~ /\S/ or return undef; + $selection = parse_hunk_selection($hunk, $text); + } + return select_hunk_lines($hunk, $selection); +} + my %edit_hunk_manually_modes = ( stage => N__( "If the patch applies cleanly, the edited hunk will immediately be @@ -1269,6 +1434,7 @@ sub help_patch_cmd { J - leave this hunk undecided, see next hunk k - leave this hunk undecided, see previous undecided hunk K - leave this hunk undecided, see previous hunk +l - select hunk lines to use s - split the current hunk into smaller hunks e - manually edit the current hunk ? - print help @@ -1485,6 +1651,9 @@ sub patch_update_file { if ($hunk[$ix]{TYPE} eq 'hunk') { $other .= ',e'; } + if (label_hunk_lines($hunk[$ix])) { + $other .= ',l'; + } for (@{$hunk[$ix]{DISPLAY}}) { print; } @@ -1632,6 +1801,18 @@ sub patch_update_file { next; } } + elsif ($line =~ /^l/) { + unless ($other =~ /l/) { + error_msg __("Cannot select line by line\n"); + next; + } + my $newhunk = select_lines_loop($hunk[$ix]); + if ($newhunk) { + splice @hunk, $ix, 1, $newhunk; + } else { + next; + } + } elsif ($line =~ /^s/) { unless ($other =~ /s/) { error_msg __("Sorry, cannot split this hunk\n"); diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index b170fb02b8..8954481995 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -360,6 +360,63 @@ test_expect_failure 'split hunk "add -p (no, yes, edit)"' ' ! grep "^+31" actual ' +test_expect_success 'setup expected diff' ' + cat >expected <<-\EOF + diff --git a/test b/test + index 0889435..341cc6b 100644 + --- a/test + +++ b/test + @@ -1,6 +1,9 @@ + +5 + 10 + 20 + +21 + 30 + 40 + 50 + 60 + +61 + \ No newline at end of file + EOF +' + +test_expect_success 'can stage individual lines of patch' ' + git reset && + printf 61 >>test && + printf "%s\n" l "1,2 4-" | + 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 @@ + +5 + 10 + 20 + 30 + 40 + 50 + 60 + +61 + \ No newline at end of file + EOF +' + +test_expect_success 'can reset individual lines of patch' ' + printf "%s\n" l -13 | + EDITOR=: git reset -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 && @@ -596,4 +653,12 @@ test_expect_success 'add -p patch editing works with pathological context lines' test_cmp expected-2 actual ' +test_expect_success 'add -p selecting lines works with pathological context lines' ' + git reset && + printf "%s\n" l 2 y | + GIT_EDITOR=./editor git add -p && + git cat-file blob :a >actual && + test_cmp expected-2 actual +' + test_done