Merge branch 'sb/packfiles-in-repository' into next
[gitweb.git] / git-add--interactive.perl
index 964c3a75420db4751cf11125b68b6904112632f1..cb48fc3e8e1e046a1076826bddc04ae298a9a9e5 100755 (executable)
@@ -677,7 +677,7 @@ sub add_untracked_cmd {
 sub run_git_apply {
        my $cmd = shift;
        my $fh;
-       open $fh, '| git ' . $cmd . " --recount --allow-overlap";
+       open $fh, '| git ' . $cmd . " --allow-overlap";
        print $fh @_;
        return close $fh;
 }
@@ -705,6 +705,14 @@ sub parse_diff {
        }
        my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
 
+       if (@colored && @colored != @diff) {
+               print STDERR
+                 "fatal: mismatched output from interactive.diffFilter\n",
+                 "hint: Your filter must maintain a one-to-one correspondence\n",
+                 "hint: between its input and output lines.\n";
+               exit 1;
+       }
+
        for (my $i = 0; $i < @diff; $i++) {
                if ($diff[$i] =~ /^@@ /) {
                        push @hunk, { TEXT => [], DISPLAY => [],
@@ -751,6 +759,15 @@ sub parse_hunk_header {
        return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
 }
 
+sub format_hunk_header {
+       my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
+       return ("@@ -$o_ofs" .
+               (($o_cnt != 1) ? ",$o_cnt" : '') .
+               " +$n_ofs" .
+               (($n_cnt != 1) ? ",$n_cnt" : '') .
+               " @@\n");
+}
+
 sub split_hunk {
        my ($text, $display) = @_;
        my @split = ();
@@ -784,6 +801,11 @@ sub split_hunk {
                while (++$i < @$text) {
                        my $line = $text->[$i];
                        my $display = $display->[$i];
+                       if ($line =~ /^\\/) {
+                               push @{$this->{TEXT}}, $line;
+                               push @{$this->{DISPLAY}}, $display;
+                               next;
+                       }
                        if ($line =~ /^ /) {
                                if ($this->{ADDDEL} &&
                                    !defined $next_hunk_start) {
@@ -838,11 +860,7 @@ sub split_hunk {
                my $o_cnt = $hunk->{OCNT};
                my $n_cnt = $hunk->{NCNT};
 
-               my $head = ("@@ -$o_ofs" .
-                           (($o_cnt != 1) ? ",$o_cnt" : '') .
-                           " +$n_ofs" .
-                           (($n_cnt != 1) ? ",$n_cnt" : '') .
-                           " @@\n");
+               my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
                my $display_head = $head;
                unshift @{$hunk->{TEXT}}, $head;
                if ($diff_use_color) {
@@ -886,6 +904,9 @@ sub merge_hunk {
                        $n_cnt++;
                        push @line, $line;
                        next;
+               } elsif ($line =~ /^\\/) {
+                       push @line, $line;
+                       next;
                }
 
                last if ($o1_ofs <= $ofs);
@@ -904,6 +925,9 @@ sub merge_hunk {
                        $n_cnt++;
                        push @line, $line;
                        next;
+               } elsif ($line =~ /^\\/) {
+                       push @line, $line;
+                       next;
                }
                $ofs++;
                $o_cnt++;
@@ -912,11 +936,7 @@ sub merge_hunk {
                }
                push @line, $line;
        }
-       my $head = ("@@ -$o0_ofs" .
-                   (($o_cnt != 1) ? ",$o_cnt" : '') .
-                   " +$n0_ofs" .
-                   (($n_cnt != 1) ? ",$n_cnt" : '') .
-                   " @@\n");
+       my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
        @{$prev->{TEXT}} = ($head, @line);
 }
 
@@ -925,14 +945,35 @@ sub coalesce_overlapping_hunks {
        my @out = ();
 
        my ($last_o_ctx, $last_was_dirty);
+       my $ofs_delta = 0;
 
-       for (grep { $_->{USE} } @in) {
+       for (@in) {
                if ($_->{TYPE} ne 'hunk') {
                        push @out, $_;
                        next;
                }
                my $text = $_->{TEXT};
-               my ($o_ofs) = parse_hunk_header($text->[0]);
+               my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
+                                               parse_hunk_header($text->[0]);
+               unless ($_->{USE}) {
+                       $ofs_delta += $o_cnt - $n_cnt;
+                       # If this hunk has been edited then subtract
+                       # the delta that is due to the edit.
+                       if ($_->{OFS_DELTA}) {
+                               $ofs_delta -= $_->{OFS_DELTA};
+                       }
+                       next;
+               }
+               if ($ofs_delta) {
+                       $n_ofs += $ofs_delta;
+                       $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
+                                                            $n_ofs, $n_cnt);
+               }
+               # If this hunk was edited then adjust the offset delta
+               # to reflect the edit.
+               if ($_->{OFS_DELTA}) {
+                       $ofs_delta += $_->{OFS_DELTA};
+               }
                if (defined $last_o_ctx &&
                    $o_ofs <= $last_o_ctx &&
                    !$_->{DIRTY} &&
@@ -980,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 = <STDIN>;
+               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
@@ -1004,6 +1210,30 @@ sub color_diff {
 marked for applying."),
 );
 
+sub recount_edited_hunk {
+       local $_;
+       my ($oldtext, $newtext) = @_;
+       my ($o_cnt, $n_cnt) = (0, 0);
+       for (@{$newtext}[1..$#{$newtext}]) {
+               my $mode = substr($_, 0, 1);
+               if ($mode eq '-') {
+                       $o_cnt++;
+               } elsif ($mode eq '+') {
+                       $n_cnt++;
+               } elsif ($mode eq ' ') {
+                       $o_cnt++;
+                       $n_cnt++;
+               }
+       }
+       my ($o_ofs, undef, $n_ofs, undef) =
+                                       parse_hunk_header($newtext->[0]);
+       $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
+       my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
+                                       parse_hunk_header($oldtext->[0]);
+       # Return the change in the number of lines inserted by this hunk
+       return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
+}
+
 sub edit_hunk_manually {
        my ($oldtext) = @_;
 
@@ -1102,25 +1332,32 @@ sub prompt_yesno {
 }
 
 sub edit_hunk_loop {
-       my ($head, $hunk, $ix) = @_;
-       my $text = $hunk->[$ix]->{TEXT};
+       my ($head, $hunks, $ix) = @_;
+       my $hunk = $hunks->[$ix];
+       my $text = $hunk->{TEXT};
 
        while (1) {
-               $text = edit_hunk_manually($text);
-               if (!defined $text) {
+               my $newtext = edit_hunk_manually($text);
+               if (!defined $newtext) {
                        return undef;
                }
                my $newhunk = {
-                       TEXT => $text,
-                       TYPE => $hunk->[$ix]->{TYPE},
+                       TEXT => $newtext,
+                       TYPE => $hunk->{TYPE},
                        USE => 1,
                        DIRTY => 1,
                };
+               $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
+               # If this hunk has already been edited then add the
+               # offset delta of the previous edit to get the real
+               # delta from the original unedited hunk.
+               $hunk->{OFS_DELTA} and
+                               $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
                if (diff_applies($head,
-                                @{$hunk}[0..$ix-1],
+                                @{$hunks}[0..$ix-1],
                                 $newhunk,
-                                @{$hunk}[$ix+1..$#{$hunk}])) {
-                       $newhunk->{DISPLAY} = [color_diff(@{$text})];
+                                @{$hunks}[$ix+1..$#{$hunks}])) {
+                       $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
                        return $newhunk;
                }
                else {
@@ -1184,13 +1421,20 @@ sub edit_hunk_loop {
 );
 
 sub help_patch_cmd {
-       print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
+       local $_;
+       my $other = $_[0] . ",?";
+       print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
+               map { "$_\n" } grep {
+                       my $c = quotemeta(substr($_, 0, 1));
+                       $other =~ /,$c/
+               } split "\n", __ <<EOF ;
 g - select a hunk to go to
 / - search for a hunk matching the given regex
 j - leave this hunk undecided, see next undecided hunk
 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
@@ -1302,39 +1546,39 @@ sub display_hunks {
 
 my %patch_update_prompt_modes = (
        stage => {
-               mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
-               deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
-               hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
+               mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
+               deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
+               hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
        },
        stash => {
-               mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
-               deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
-               hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
+               mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
+               deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
+               hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
        },
        reset_head => {
-               mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
-               deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
-               hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
+               mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
+               deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
+               hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
        },
        reset_nothead => {
-               mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
-               deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
-               hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
+               mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
+               deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
+               hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
        },
        checkout_index => {
-               mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
-               deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
-               hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
+               mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
+               deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
+               hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
        },
        checkout_head => {
-               mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
-               deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
-               hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
+               mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
+               deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
+               hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
        },
        checkout_nothead => {
-               mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
-               deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
-               hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
+               mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
+               deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
+               hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
        },
 );
 
@@ -1390,7 +1634,7 @@ sub patch_update_file {
                        $other .= ',J';
                }
                if ($num > 1) {
-                       $other .= ',g';
+                       $other .= ',g,/';
                }
                for ($i = 0; $i < $num; $i++) {
                        if (!defined $hunk[$i]{USE}) {
@@ -1407,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;
                }
@@ -1431,8 +1678,12 @@ sub patch_update_file {
                                }
                                next;
                        }
-                       elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
+                       elsif ($line =~ /^g(.*)/) {
                                my $response = $1;
+                               unless ($other =~ /g/) {
+                                       error_msg __("No other hunks to goto\n");
+                                       next;
+                               }
                                my $no = $ix > 10 ? $ix - 10 : 0;
                                while ($response eq '') {
                                        $no = display_hunks(\@hunk, $no);
@@ -1478,6 +1729,10 @@ sub patch_update_file {
                        }
                        elsif ($line =~ m|^/(.*)|) {
                                my $regex = $1;
+                               unless ($other =~ m|/|) {
+                                       error_msg __("No other hunks to search\n");
+                                       next;
+                               }
                                if ($1 eq "") {
                                        print colored $prompt_color, __("search for regex? ");
                                        $regex = <STDIN>;
@@ -1546,7 +1801,23 @@ sub patch_update_file {
                                        next;
                                }
                        }
-                       elsif ($other =~ /s/ && $line =~ /^s/) {
+                       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");
+                                       next;
+                               }
                                my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
                                if (1 < @split) {
                                        print colored $header_color, sprintf(
@@ -1558,7 +1829,11 @@ sub patch_update_file {
                                $num = scalar @hunk;
                                next;
                        }
-                       elsif ($other =~ /e/ && $line =~ /^e/) {
+                       elsif ($line =~ /^e/) {
+                               unless ($other =~ /e/) {
+                                       error_msg __("Sorry, cannot edit this hunk\n");
+                                       next;
+                               }
                                my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
                                if (defined $newhunk) {
                                        splice @hunk, $ix, 1, $newhunk;