diff options
Diffstat (limited to 'git-add--interactive.perl')
-rwxr-xr-x | git-add--interactive.perl | 862 |
1 files changed, 700 insertions, 162 deletions
diff --git a/git-add--interactive.perl b/git-add--interactive.perl index 3bf0cda4ee..77b4ed53a8 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -1,7 +1,12 @@ -#!/usr/bin/perl -w +#!/usr/bin/perl +use 5.008; use strict; +use warnings; use Git; +use Git::I18N; + +binmode(STDOUT, ":raw"); my $repo = Git->repository(); @@ -12,6 +17,13 @@ my ($prompt_color, $header_color, $help_color) = $repo->get_color('color.interactive.header', 'bold'), $repo->get_color('color.interactive.help', 'red bold'), ) : (); +my $error_color = (); +if ($menu_use_color) { + my $help_color_spec = ($repo->config('color.interactive.help') or + 'red bold'); + $error_color = $repo->get_color('color.interactive.error', + $help_color_spec); +} my $diff_use_color = $repo->get_colorbool('color.diff'); my ($fraginfo_color) = @@ -33,6 +45,35 @@ my ($diff_new_color) = my $normal_color = $repo->get_color("", "reset"); +my $diff_algorithm = $repo->config('diff.algorithm'); +my $diff_indent_heuristic = $repo->config_bool('diff.indentheuristic'); +my $diff_filter = $repo->config('interactive.difffilter'); + +my $use_readkey = 0; +my $use_termcap = 0; +my %term_escapes; + +sub ReadMode; +sub ReadKey; +if ($repo->config_bool("interactive.singlekey")) { + eval { + require Term::ReadKey; + Term::ReadKey->import; + $use_readkey = 1; + }; + if (!$use_readkey) { + print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n"; + } + eval { + require Term::Cap; + my $termcap = Term::Cap->Tgetent; + foreach (values %$termcap) { + $term_escapes{$_} = 1 if /^\e/; + } + $use_termcap = 1; + }; +} + sub colored { my $color = shift; my $string = join("", @_); @@ -51,10 +92,71 @@ sub colored { } # command line options +my $patch_mode_only; my $patch_mode; +my $patch_mode_revision; + +sub apply_patch; +sub apply_patch_for_checkout_commit; +sub apply_patch_for_stash; + +my %patch_modes = ( + 'stage' => { + DIFF => 'diff-files -p', + APPLY => sub { apply_patch 'apply --cached', @_; }, + APPLY_CHECK => 'apply --cached', + FILTER => 'file-only', + IS_REVERSE => 0, + }, + 'stash' => { + DIFF => 'diff-index -p HEAD', + APPLY => sub { apply_patch 'apply --cached', @_; }, + APPLY_CHECK => 'apply --cached', + FILTER => undef, + IS_REVERSE => 0, + }, + 'reset_head' => { + DIFF => 'diff-index -p --cached', + APPLY => sub { apply_patch 'apply -R --cached', @_; }, + APPLY_CHECK => 'apply -R --cached', + FILTER => 'index-only', + IS_REVERSE => 1, + }, + 'reset_nothead' => { + DIFF => 'diff-index -R -p --cached', + APPLY => sub { apply_patch 'apply --cached', @_; }, + APPLY_CHECK => 'apply --cached', + FILTER => 'index-only', + IS_REVERSE => 0, + }, + 'checkout_index' => { + DIFF => 'diff-files -p', + APPLY => sub { apply_patch 'apply -R', @_; }, + APPLY_CHECK => 'apply -R', + FILTER => 'file-only', + IS_REVERSE => 1, + }, + 'checkout_head' => { + DIFF => 'diff-index -p', + APPLY => sub { apply_patch_for_checkout_commit '-R', @_ }, + APPLY_CHECK => 'apply -R', + FILTER => undef, + IS_REVERSE => 1, + }, + 'checkout_nothead' => { + DIFF => 'diff-index -R -p', + APPLY => sub { apply_patch_for_checkout_commit '', @_ }, + APPLY_CHECK => 'apply', + FILTER => undef, + IS_REVERSE => 0, + }, +); + +$patch_mode = 'stage'; +my %patch_mode_flavour = %{$patch_modes{$patch_mode}}; sub run_cmd_pipe { - if ($^O eq 'MSWin32' || $^O eq 'msys') { + if ($^O eq 'MSWin32') { my @invalid = grep {m/[":*]/} @_; die "$^O does not support: @invalid\n" if @invalid; my @args = map { m/ /o ? "\"$_\"": $_ } @_; @@ -73,6 +175,47 @@ if (!defined $GIT_DIR) { } chomp($GIT_DIR); +my %cquote_map = ( + "b" => chr(8), + "t" => chr(9), + "n" => chr(10), + "v" => chr(11), + "f" => chr(12), + "r" => chr(13), + "\\" => "\\", + "\042" => "\042", +); + +sub unquote_path { + local ($_) = @_; + my ($retval, $remainder); + if (!/^\042(.*)\042$/) { + return $_; + } + ($_, $retval) = ($1, ""); + while (/^([^\\]*)\\(.*)$/) { + $remainder = $2; + $retval .= $1; + for ($remainder) { + if (/^([0-3][0-7][0-7])(.*)$/) { + $retval .= chr(oct($1)); + $_ = $2; + last; + } + if (/^([\\\042btnvfr])(.*)$/) { + $retval .= $cquote_map{$1}; + $_ = $2; + last; + } + # This is malformed -- just return it as-is for now. + return $_[0]; + } + $_ = $remainder; + } + $retval .= $_; + return $retval; +} + sub refresh { my $fh; open $fh, 'git update-index --refresh |' @@ -86,13 +229,14 @@ sub refresh { sub list_untracked { map { chomp $_; - $_; + unquote_path($_); } run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV); } -my $status_fmt = '%12s %12s %s'; -my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path'); +# TRANSLATORS: you can adjust this to align "git add -i" status menu +my $status_fmt = __('%12s %12s %s'); +my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path')); { my $initial; @@ -107,6 +251,17 @@ sub get_empty_tree { return '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; } +sub get_diff_reference { + my $ref = shift; + if (defined $ref and $ref ne 'HEAD') { + return $ref; + } elsif (is_initial_commit()) { + return get_empty_tree(); + } else { + return 'HEAD'; + } +} + # Returns list of hashes, contents of each of which are: # VALUE: pathname # BINARY: is a binary path @@ -114,29 +269,23 @@ sub get_empty_tree { # FILE: is file different from index? # INDEX_ADDDEL: is it add/delete between HEAD and index? # FILE_ADDDEL: is it add/delete between index and file? +# UNMERGED: is the path unmerged sub list_modified { my ($only) = @_; my (%data, @return); my ($add, $del, $adddel, $file); - my @tracked = (); - if (@ARGV) { - @tracked = map { - chomp $_; $_; - } run_cmd_pipe(qw(git ls-files --exclude-standard --), @ARGV); - return if (!@tracked); - } - - my $reference = is_initial_commit() ? get_empty_tree() : 'HEAD'; + my $reference = get_diff_reference($patch_mode_revision); for (run_cmd_pipe(qw(git diff-index --cached --numstat --summary), $reference, - '--', @tracked)) { + '--', @ARGV)) { if (($add, $del, $file) = /^([-\d]+) ([-\d]+) (.*)/) { my ($change, $bin); + $file = unquote_path($file); if ($add eq '-' && $del eq '-') { - $change = 'binary'; + $change = __('binary'); $bin = 1; } else { @@ -145,27 +294,23 @@ sub list_modified { $data{$file} = { INDEX => $change, BINARY => $bin, - FILE => 'nothing', + FILE => __('nothing'), } } elsif (($adddel, $file) = /^ (create|delete) mode [0-7]+ (.*)$/) { + $file = unquote_path($file); $data{$file}{INDEX_ADDDEL} = $adddel; } } - for (run_cmd_pipe(qw(git diff-files --numstat --summary --), @tracked)) { + for (run_cmd_pipe(qw(git diff-files --numstat --summary --raw --), @ARGV)) { if (($add, $del, $file) = /^([-\d]+) ([-\d]+) (.*)/) { - if (!exists $data{$file}) { - $data{$file} = +{ - INDEX => 'unchanged', - BINARY => 0, - }; - } + $file = unquote_path($file); my ($change, $bin); if ($add eq '-' && $del eq '-') { - $change = 'binary'; + $change = __('binary'); $bin = 1; } else { @@ -178,8 +323,21 @@ sub list_modified { } elsif (($adddel, $file) = /^ (create|delete) mode [0-7]+ (.*)$/) { + $file = unquote_path($file); $data{$file}{FILE_ADDDEL} = $adddel; } + elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) { + $file = unquote_path($2); + if (!exists $data{$file}) { + $data{$file} = +{ + INDEX => __('unchanged'), + BINARY => 0, + }; + } + if ($1 eq 'U') { + $data{$file}{UNMERGED} = 1; + } + } } for (sort keys %data) { @@ -187,10 +345,10 @@ sub list_modified { if ($only) { if ($only eq 'index-only') { - next if ($it->{INDEX} eq 'unchanged'); + next if ($it->{INDEX} eq __('unchanged')); } if ($only eq 'file-only') { - next if ($it->{FILE} eq 'nothing'); + next if ($it->{FILE} eq __('nothing')); } } push @return, +{ @@ -284,7 +442,8 @@ sub find_unique_prefixes { } %search = %{$search{$letter}}; } - if ($soft_limit && $j + 1 > $soft_limit) { + if (ord($letters[0]) > 127 || + ($soft_limit && $j + 1 > $soft_limit)) { $prefix = undef; $remainder = $ret; } @@ -325,9 +484,16 @@ sub highlight_prefix { return "$prompt_color$prefix$normal_color$remainder"; } +sub error_msg { + print STDERR colored $error_color, @_; +} + sub list_and_choose { my ($opts, @stuff) = @_; my (@chosen, @return); + if (!@stuff) { + return @return; + } my $i; my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY}; @@ -420,12 +586,12 @@ sub list_and_choose { else { $bottom = $top = find_unique($choice, @stuff); if (!defined $bottom) { - print "Huh ($choice)?\n"; + error_msg sprintf(__("Huh (%s)?\n"), $choice); next TOPLOOP; } } if ($opts->{SINGLETON} && $bottom != $top) { - print "Huh ($choice)?\n"; + error_msg sprintf(__("Huh (%s)?\n"), $choice); next TOPLOOP; } for ($i = $bottom-1; $i <= $top-1; $i++) { @@ -444,7 +610,7 @@ sub list_and_choose { } sub singleton_prompt_help_cmd { - print colored $help_color, <<\EOF ; + print colored $help_color, __ <<'EOF' ; Prompt help: 1 - select a numbered item foo - select item based on unique prefix @@ -453,7 +619,7 @@ EOF } sub prompt_help_cmd { - print colored $help_color, <<\EOF ; + print colored $help_color, __ <<'EOF' ; Prompt help: 1 - select a single item 3-5 - select a range of items @@ -474,12 +640,18 @@ sub status_cmd { sub say_n_paths { my $did = shift @_; my $cnt = scalar @_; - print "$did "; - if (1 < $cnt) { - print "$cnt paths\n"; - } - else { - print "one path\n"; + if ($did eq 'added') { + printf(__n("added %d path\n", "added %d paths\n", + $cnt), $cnt); + } elsif ($did eq 'updated') { + printf(__n("updated %d path\n", "updated %d paths\n", + $cnt), $cnt); + } elsif ($did eq 'reverted') { + printf(__n("reverted %d path\n", "reverted %d paths\n", + $cnt), $cnt); + } else { + printf(__n("touched %d path\n", "touched %d paths\n", + $cnt), $cnt); } } @@ -487,7 +659,7 @@ sub update_cmd { my @mods = list_modified('file-only'); return if (!@mods); - my @update = list_and_choose({ PROMPT => 'Update', + my @update = list_and_choose({ PROMPT => __('Update'), HEADER => $status_head, }, @mods); if (@update) { @@ -499,7 +671,7 @@ sub update_cmd { } sub revert_cmd { - my @update = list_and_choose({ PROMPT => 'Revert', + my @update = list_and_choose({ PROMPT => __('Revert'), HEADER => $status_head, }, list_modified()); if (@update) { @@ -522,7 +694,7 @@ sub revert_cmd { $_->{INDEX_ADDDEL} eq 'create') { system(qw(git update-index --force-remove --), $_->{VALUE}); - print "note: $_->{VALUE} is untracked now.\n"; + printf(__("note: %s is untracked now.\n"), $_->{VALUE}); } } } @@ -533,31 +705,59 @@ sub revert_cmd { } sub add_untracked_cmd { - my @add = list_and_choose({ PROMPT => 'Add untracked' }, + my @add = list_and_choose({ PROMPT => __('Add untracked') }, list_untracked()); if (@add) { system(qw(git update-index --add --), @add); say_n_paths('added', @add); + } else { + print __("No untracked files.\n"); } print "\n"; } +sub run_git_apply { + my $cmd = shift; + my $fh; + open $fh, '| git ' . $cmd . " --recount --allow-overlap"; + print $fh @_; + return close $fh; +} + sub parse_diff { my ($path) = @_; - my @diff = run_cmd_pipe(qw(git diff-files -p --), $path); + my @diff_cmd = split(" ", $patch_mode_flavour{DIFF}); + if (defined $diff_algorithm) { + splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}"; + } + if ($diff_indent_heuristic) { + splice @diff_cmd, 1, 0, "--indent-heuristic"; + } + if (defined $patch_mode_revision) { + push @diff_cmd, get_diff_reference($patch_mode_revision); + } + my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path); my @colored = (); if ($diff_use_color) { - @colored = run_cmd_pipe(qw(git diff-files -p --color --), $path); + my @display_cmd = ("git", @diff_cmd, qw(--color --), $path); + if (defined $diff_filter) { + # quotemeta is overkill, but sufficient for shell-quoting + my $diff = join(' ', map { quotemeta } @display_cmd); + @display_cmd = ("$diff | $diff_filter"); + } + + @colored = run_cmd_pipe(@display_cmd); } - my (@hunk) = { TEXT => [], DISPLAY => [] }; + my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' }; for (my $i = 0; $i < @diff; $i++) { if ($diff[$i] =~ /^@@ /) { - push @hunk, { TEXT => [], DISPLAY => [] }; + push @hunk, { TEXT => [], DISPLAY => [], + TYPE => 'hunk' }; } push @{$hunk[-1]{TEXT}}, $diff[$i]; push @{$hunk[-1]{DISPLAY}}, - ($diff_use_color ? $colored[$i] : $diff[$i]); + (@colored ? $colored[$i] : $diff[$i]); } return @hunk; } @@ -565,16 +765,19 @@ sub parse_diff { sub parse_diff_header { my $src = shift; - my $head = { TEXT => [], DISPLAY => [] }; - my $mode = { TEXT => [], DISPLAY => [] }; + my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' }; + my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' }; + my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' }; for (my $i = 0; $i < @{$src->{TEXT}}; $i++) { - my $dest = $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? - $mode : $head; + my $dest = + $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode : + $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion : + $head; push @{$dest->{TEXT}}, $src->{TEXT}->[$i]; push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i]; } - return ($head, $mode); + return ($head, $mode, $deletion); } sub hunk_splittable { @@ -613,6 +816,7 @@ sub split_hunk { my $this = +{ TEXT => [], DISPLAY => [], + TYPE => 'hunk', OLD => $o_ofs, NEW => $n_ofs, OCNT => 0, @@ -694,6 +898,122 @@ sub split_hunk { return @split; } +sub find_last_o_ctx { + my ($it) = @_; + my $text = $it->{TEXT}; + my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]); + my $i = @{$text}; + my $last_o_ctx = $o_ofs + $o_cnt; + while (0 < --$i) { + my $line = $text->[$i]; + if ($line =~ /^ /) { + $last_o_ctx--; + next; + } + last; + } + return $last_o_ctx; +} + +sub merge_hunk { + my ($prev, $this) = @_; + my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) = + parse_hunk_header($prev->{TEXT}[0]); + my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) = + parse_hunk_header($this->{TEXT}[0]); + + my (@line, $i, $ofs, $o_cnt, $n_cnt); + $ofs = $o0_ofs; + $o_cnt = $n_cnt = 0; + for ($i = 1; $i < @{$prev->{TEXT}}; $i++) { + my $line = $prev->{TEXT}[$i]; + if ($line =~ /^\+/) { + $n_cnt++; + push @line, $line; + next; + } + + last if ($o1_ofs <= $ofs); + + $o_cnt++; + $ofs++; + if ($line =~ /^ /) { + $n_cnt++; + } + push @line, $line; + } + + for ($i = 1; $i < @{$this->{TEXT}}; $i++) { + my $line = $this->{TEXT}[$i]; + if ($line =~ /^\+/) { + $n_cnt++; + push @line, $line; + next; + } + $ofs++; + $o_cnt++; + if ($line =~ /^ /) { + $n_cnt++; + } + push @line, $line; + } + my $head = ("@@ -$o0_ofs" . + (($o_cnt != 1) ? ",$o_cnt" : '') . + " +$n0_ofs" . + (($n_cnt != 1) ? ",$n_cnt" : '') . + " @@\n"); + @{$prev->{TEXT}} = ($head, @line); +} + +sub coalesce_overlapping_hunks { + my (@in) = @_; + my @out = (); + + my ($last_o_ctx, $last_was_dirty); + + for (grep { $_->{USE} } @in) { + if ($_->{TYPE} ne 'hunk') { + push @out, $_; + next; + } + my $text = $_->{TEXT}; + my ($o_ofs) = parse_hunk_header($text->[0]); + if (defined $last_o_ctx && + $o_ofs <= $last_o_ctx && + !$_->{DIRTY} && + !$last_was_dirty) { + merge_hunk($out[-1], $_); + } + else { + push @out, $_; + } + $last_o_ctx = find_last_o_ctx($out[-1]); + $last_was_dirty = $_->{DIRTY}; + } + return @out; +} + +sub reassemble_patch { + my $head = shift; + my @patch; + + # Include everything in the header except the beginning of the diff. + push @patch, (grep { !/^[-+]{3}/ } @$head); + + # Then include any headers from the hunk lines, which must + # come before any actual hunk. + while (@_ && $_[0] !~ /^@/) { + push @patch, shift; + } + + # Then begin the diff. + push @patch, grep { /^[-+]{3}/ } @$head; + + # And then the actual hunks. + push @patch, @_; + + return @patch; +} sub color_diff { return map { @@ -705,35 +1025,67 @@ sub color_diff { } @_; } +my %edit_hunk_manually_modes = ( + stage => N__( +"If the patch applies cleanly, the edited hunk will immediately be +marked for staging."), + stash => N__( +"If the patch applies cleanly, the edited hunk will immediately be +marked for stashing."), + reset_head => N__( +"If the patch applies cleanly, the edited hunk will immediately be +marked for unstaging."), + reset_nothead => N__( +"If the patch applies cleanly, the edited hunk will immediately be +marked for applying."), + checkout_index => N__( +"If the patch applies cleanly, the edited hunk will immediately be +marked for discarding"), + checkout_head => N__( +"If the patch applies cleanly, the edited hunk will immediately be +marked for discarding."), + checkout_nothead => N__( +"If the patch applies cleanly, the edited hunk will immediately be +marked for applying."), +); + sub edit_hunk_manually { my ($oldtext) = @_; my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff"; my $fh; open $fh, '>', $hunkfile - or die "failed to open hunk edit file for writing: " . $!; - print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n"; + or die sprintf(__("failed to open hunk edit file for writing: %s"), $!); + print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n"); print $fh @$oldtext; - print $fh <<EOF; -# --- -# To remove '-' lines, make them ' ' lines (context). -# To remove '+' lines, delete them. -# Lines starting with # will be removed. -# -# If the 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. + my $is_reverse = $patch_mode_flavour{IS_REVERSE}; + my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-'); + my $comment_line_char = Git::get_comment_line_char; + print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char), +--- +To remove '%s' lines, make them ' ' lines (context). +To remove '%s' lines, delete them. +Lines starting with %s will be removed. EOF +__($edit_hunk_manually_modes{$patch_mode}), +# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages. +__ <<EOF2 ; +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. +EOF2 close $fh; - my $editor = $ENV{GIT_EDITOR} || $repo->config("core.editor") - || $ENV{VISUAL} || $ENV{EDITOR} || "vi"; + chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR))); system('sh', '-c', $editor.' "$@"', $editor, $hunkfile); + if ($? != 0) { + return undef; + } + open $fh, '<', $hunkfile - or die "failed to open hunk edit file for reading: " . $!; - my @newtext = grep { !/^#/ } <$fh>; + or die sprintf(__("failed to open hunk edit file for reading: %s"), $!); + my @newtext = grep { !/^$comment_line_char/ } <$fh>; close $fh; unlink $hunkfile; @@ -750,19 +1102,44 @@ EOF } sub diff_applies { - my $fh; - open $fh, '| git apply --recount --cached --check'; - for my $h (@_) { - print $fh @{$h->{TEXT}}; + return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check', + map { @{$_->{TEXT}} } @_); +} + +sub _restore_terminal_and_die { + ReadMode 'restore'; + print "\n"; + exit 1; +} + +sub prompt_single_character { + if ($use_readkey) { + local $SIG{TERM} = \&_restore_terminal_and_die; + local $SIG{INT} = \&_restore_terminal_and_die; + ReadMode 'cbreak'; + my $key = ReadKey 0; + ReadMode 'restore'; + if ($use_termcap and $key eq "\e") { + while (!defined $term_escapes{$key}) { + my $next = ReadKey 0.5; + last if (!defined $next); + $key .= $next; + } + $key =~ s/\e/^[/; + } + print "$key" if defined $key; + print "\n"; + return $key; + } else { + return <STDIN>; } - return close $fh; } sub prompt_yesno { my ($prompt) = @_; while (1) { print colored $prompt_color, $prompt; - my $line = <STDIN>; + my $line = prompt_single_character; return 0 if $line =~ /^n/i; return 1 if $line =~ /^y/i; } @@ -777,7 +1154,12 @@ sub edit_hunk_loop { if (!defined $text) { return undef; } - my $newhunk = { TEXT => $text, USE => 1 }; + my $newhunk = { + TEXT => $text, + TYPE => $hunk->[$ix]->{TYPE}, + USE => 1, + DIRTY => 1, + }; if (diff_applies($head, @{$hunk}[0..$ix-1], $newhunk, @@ -787,19 +1169,66 @@ sub edit_hunk_loop { } else { prompt_yesno( - 'Your edited hunk does not apply. Edit again ' - . '(saying "no" discards!) [y/n]? ' + # TRANSLATORS: do not translate [y/n] + # The program will only accept that input + # at this point. + # Consider translating (saying "no" discards!) as + # (saying "n" for "no" discards!) if the translation + # of the word "no" does not start with n. + __('Your edited hunk does not apply. Edit again ' + . '(saying "no" discards!) [y/n]? ') ) or return undef; } } } -sub help_patch_cmd { - print colored $help_color, <<\EOF ; -y - stage this hunk +my %help_patch_modes = ( + stage => N__( +"y - stage this hunk n - do not stage this hunk -a - stage this and all the remaining hunks in the file -d - do not stage this hunk nor any of the remaining hunks in the file +q - quit; do not stage this hunk or any of the remaining ones +a - stage this hunk and all later hunks in the file +d - do not stage this hunk or any of the later hunks in the file"), + stash => N__( +"y - stash this hunk +n - do not stash this hunk +q - quit; do not stash this hunk or any of the remaining ones +a - stash this hunk and all later hunks in the file +d - do not stash this hunk or any of the later hunks in the file"), + reset_head => N__( +"y - unstage this hunk +n - do not unstage this hunk +q - quit; do not unstage this hunk or any of the remaining ones +a - unstage this hunk and all later hunks in the file +d - do not unstage this hunk or any of the later hunks in the file"), + reset_nothead => N__( +"y - apply this hunk to index +n - do not apply this hunk to index +q - quit; do not apply this hunk or any of the remaining ones +a - apply this hunk and all later hunks in the file +d - do not apply this hunk or any of the later hunks in the file"), + checkout_index => N__( +"y - discard this hunk from worktree +n - do not discard this hunk from worktree +q - quit; do not discard this hunk or any of the remaining ones +a - discard this hunk and all later hunks in the file +d - do not discard this hunk or any of the later hunks in the file"), + checkout_head => N__( +"y - discard this hunk from index and worktree +n - do not discard this hunk from index and worktree +q - quit; do not discard this hunk or any of the remaining ones +a - discard this hunk and all later hunks in the file +d - do not discard this hunk or any of the later hunks in the file"), + checkout_nothead => N__( +"y - apply this hunk to index and worktree +n - do not apply this hunk to index and worktree +q - quit; do not apply this hunk or any of the remaining ones +a - apply this hunk and all later hunks in the file +d - do not apply this hunk or any of the later hunks in the file"), +); + +sub help_patch_cmd { + print colored $help_color, __($help_patch_modes{$patch_mode}), "\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 @@ -812,29 +1241,65 @@ e - manually edit the current hunk EOF } +sub apply_patch { + my $cmd = shift; + my $ret = run_git_apply $cmd, @_; + if (!$ret) { + print STDERR @_; + } + return $ret; +} + +sub apply_patch_for_checkout_commit { + my $reverse = shift; + my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_; + my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_; + + if ($applies_worktree && $applies_index) { + run_git_apply 'apply '.$reverse.' --cached', @_; + run_git_apply 'apply '.$reverse, @_; + return 1; + } elsif (!$applies_index) { + print colored $error_color, __("The selected hunks do not apply to the index!\n"); + if (prompt_yesno __("Apply them to the worktree anyway? ")) { + return run_git_apply 'apply '.$reverse, @_; + } else { + print colored $error_color, __("Nothing was applied.\n"); + return 0; + } + } else { + print STDERR @_; + return 0; + } +} + sub patch_update_cmd { - my @all_mods = list_modified('file-only'); + my @all_mods = list_modified($patch_mode_flavour{FILTER}); + error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE}) + for grep { $_->{UNMERGED} } @all_mods; + @all_mods = grep { !$_->{UNMERGED} } @all_mods; + my @mods = grep { !($_->{BINARY}) } @all_mods; my @them; if (!@mods) { if (@all_mods) { - print STDERR "Only binary files changed.\n"; + print STDERR __("Only binary files changed.\n"); } else { - print STDERR "No changes.\n"; + print STDERR __("No changes.\n"); } return 0; } - if ($patch_mode) { + if ($patch_mode_only) { @them = @mods; } else { - @them = list_and_choose({ PROMPT => 'Patch update', + @them = list_and_choose({ PROMPT => __('Patch update'), HEADER => $status_head, }, @mods); } for (@them) { - patch_update_file($_->{VALUE}); + return 0 if patch_update_file($_->{VALUE}); } } @@ -861,7 +1326,7 @@ sub summarize_hunk { # Print a one-line summary of each hunk in the array ref in -# the first argument, starting wih the index in the 2nd. +# the first argument, starting with the index in the 2nd. sub display_hunks { my ($hunks, $i) = @_; my $ctr = 0; @@ -879,42 +1344,63 @@ sub display_hunks { return $i; } +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,?]? "), + }, + 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,?]? "), + }, + 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,?]? "), + }, + 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,?]? "), + }, + 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,?]? "), + }, + 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,?]? "), + }, + 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,?]? "), + }, +); + sub patch_update_file { + my $quit = 0; my ($ix, $num); my $path = shift; my ($head, @hunk) = parse_diff($path); - ($head, my $mode) = parse_diff_header($head); + ($head, my $mode, my $deletion) = parse_diff_header($head); for (@{$head->{DISPLAY}}) { print; } if (@{$mode->{TEXT}}) { - while (1) { - print @{$mode->{DISPLAY}}; - print colored $prompt_color, - "Stage mode change [y/n/a/d/?]? "; - my $line = <STDIN>; - if ($line =~ /^y/i) { - $mode->{USE} = 1; - last; - } - elsif ($line =~ /^n/i) { - $mode->{USE} = 0; - last; - } - elsif ($line =~ /^a/i) { - $_->{USE} = 1 foreach ($mode, @hunk); - last; - } - elsif ($line =~ /^d/i) { - $_->{USE} = 0 foreach ($mode, @hunk); - last; - } - else { - help_patch_cmd(''); - next; - } + unshift @hunk, $mode; + } + if (@{$deletion->{TEXT}}) { + foreach my $hunk (@hunk) { + push @{$deletion->{TEXT}}, @{$hunk->{TEXT}}; + push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}}; } + @hunk = ($deletion); } $num = scalar @hunk; @@ -958,15 +1444,21 @@ sub patch_update_file { } last if (!$undecided); - if (hunk_splittable($hunk[$ix]{TEXT})) { + if ($hunk[$ix]{TYPE} eq 'hunk' && + hunk_splittable($hunk[$ix]{TEXT})) { $other .= ',s'; } - $other .= ',e'; + if ($hunk[$ix]{TYPE} eq 'hunk') { + $other .= ',e'; + } for (@{$hunk[$ix]{DISPLAY}}) { print; } - print colored $prompt_color, "Stage this hunk [y,n,a,d,/$other,?]? "; - my $line = <STDIN>; + print colored $prompt_color, + sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other); + + my $line = prompt_single_character; + last unless defined $line; if ($line) { if ($line =~ /^y/i) { $hunk[$ix]{USE} = 1; @@ -987,12 +1479,12 @@ sub patch_update_file { my $response = $1; my $no = $ix > 10 ? $ix - 10 : 0; while ($response eq '') { - my $extra = ""; $no = display_hunks(\@hunk, $no); if ($no < $num) { - $extra = " (<ret> to see more)"; + print __("go to which hunk (<ret> to see more)? "); + } else { + print __("go to which hunk? "); } - print "go to which hunk$extra? "; $response = <STDIN>; if (!defined $response) { $response = ''; @@ -1000,11 +1492,13 @@ sub patch_update_file { chomp $response; } if ($response !~ /^\s*\d+\s*$/) { - print STDERR "Invalid number: '$response'\n"; + error_msg sprintf(__("Invalid number: '%s'\n"), + $response); } elsif (0 < $response && $response <= $num) { $ix = $response - 1; } else { - print STDERR "Sorry, only $num hunks available.\n"; + error_msg sprintf(__n("Sorry, only %d hunk available.\n", + "Sorry, only %d hunks available.\n", $num), $num); } next; } @@ -1017,15 +1511,32 @@ sub patch_update_file { } next; } + elsif ($line =~ /^q/i) { + for ($i = 0; $i < $num; $i++) { + if (!defined $hunk[$i]{USE}) { + $hunk[$i]{USE} = 0; + } + } + $quit = 1; + last; + } elsif ($line =~ m|^/(.*)|) { + my $regex = $1; + if ($1 eq "") { + print colored $prompt_color, __("search for regex? "); + $regex = <STDIN>; + if (defined $regex) { + chomp $regex; + } + } my $search_string; eval { - $search_string = qr{$1}m; + $search_string = qr{$regex}m; }; if ($@) { my ($err,$exp) = ($@, $1); $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//; - print STDERR "Malformed search regexp $exp: $err\n"; + error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err); next; } my $iy = $ix; @@ -1035,7 +1546,7 @@ sub patch_update_file { $iy++; $iy = 0 if ($iy >= $num); if ($ix == $iy) { - print STDERR "No hunk matches the given pattern\n"; + error_msg __("No hunk matches the given pattern\n"); last; } } @@ -1047,7 +1558,7 @@ sub patch_update_file { $ix--; } else { - print STDERR "No previous hunk\n"; + error_msg __("No previous hunk\n"); } next; } @@ -1056,7 +1567,7 @@ sub patch_update_file { $ix++; } else { - print STDERR "No next hunk\n"; + error_msg __("No next hunk\n"); } next; } @@ -1069,27 +1580,29 @@ sub patch_update_file { } } else { - print STDERR "No previous hunk\n"; + error_msg __("No previous hunk\n"); } next; } elsif ($line =~ /^j/) { if ($other !~ /j/) { - print STDERR "No next hunk\n"; + error_msg __("No next hunk\n"); next; } } elsif ($other =~ /s/ && $line =~ /^s/) { my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY}); if (1 < @split) { - print colored $header_color, "Split into ", - scalar(@split), " hunks.\n"; + print colored $header_color, sprintf( + __n("Split into %d hunk.\n", + "Split into %d hunks.\n", + scalar(@split)), scalar(@split)); } splice (@hunk, $ix, 1, @split); $num = scalar @hunk; next; } - elsif ($line =~ /^e/) { + elsif ($other =~ /e/ && $line =~ /^e/) { my $newhunk = edit_hunk_loop($head, \@hunk, $ix); if (defined $newhunk) { splice @hunk, $ix, 1, $newhunk; @@ -1108,11 +1621,10 @@ sub patch_update_file { } } + @hunk = coalesce_overlapping_hunks(@hunk); + my $n_lofs = 0; my @result = (); - if ($mode->{USE}) { - push @result, @{$mode->{TEXT}}; - } for (@hunk) { if ($_->{USE}) { push @result, @{$_->{TEXT}}; @@ -1120,49 +1632,44 @@ sub patch_update_file { } if (@result) { - my $fh; - - open $fh, '| git apply --cached --recount'; - for (@{$head->{TEXT}}, @result) { - print $fh $_; - } - if (!close $fh) { - for (@{$head->{TEXT}}, @result) { - print STDERR $_; - } - } + my @patch = reassemble_patch($head->{TEXT}, @result); + my $apply_routine = $patch_mode_flavour{APPLY}; + &$apply_routine(@patch); refresh(); } print "\n"; + return $quit; } sub diff_cmd { my @mods = list_modified('index-only'); @mods = grep { !($_->{BINARY}) } @mods; return if (!@mods); - my (@them) = list_and_choose({ PROMPT => 'Review diff', + my (@them) = list_and_choose({ PROMPT => __('Review diff'), IMMEDIATE => 1, HEADER => $status_head, }, @mods); return if (!@them); - my $reference = is_initial_commit() ? get_empty_tree() : 'HEAD'; + my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD'; system(qw(git diff -p --cached), $reference, '--', map { $_->{VALUE} } @them); } sub quit_cmd { - print "Bye.\n"; + print __("Bye.\n"); exit(0); } sub help_cmd { - print colored $help_color, <<\EOF ; +# TRANSLATORS: please do not translate the command names +# 'status', 'update', 'revert', etc. + print colored $help_color, __ <<'EOF' ; status - show paths with changes update - add working tree state to the staged set of changes revert - revert staged set of changes back to the HEAD version patch - pick hunks and update selectively -diff - view diff between HEAD and index +diff - view diff between HEAD and index add untracked - add contents of untracked files to the staged set of changes EOF } @@ -1170,14 +1677,45 @@ EOF sub process_args { return unless @ARGV; my $arg = shift @ARGV; - if ($arg eq "--patch") { - $patch_mode = 1; - $arg = shift @ARGV or die "missing --"; - die "invalid argument $arg, expecting --" - unless $arg eq "--"; + if ($arg =~ /--patch(?:=(.*))?/) { + if (defined $1) { + if ($1 eq 'reset') { + $patch_mode = 'reset_head'; + $patch_mode_revision = 'HEAD'; + $arg = shift @ARGV or die __("missing --"); + if ($arg ne '--') { + $patch_mode_revision = $arg; + $patch_mode = ($arg eq 'HEAD' ? + 'reset_head' : 'reset_nothead'); + $arg = shift @ARGV or die __("missing --"); + } + } elsif ($1 eq 'checkout') { + $arg = shift @ARGV or die __("missing --"); + if ($arg eq '--') { + $patch_mode = 'checkout_index'; + } else { + $patch_mode_revision = $arg; + $patch_mode = ($arg eq 'HEAD' ? + 'checkout_head' : 'checkout_nothead'); + $arg = shift @ARGV or die __("missing --"); + } + } elsif ($1 eq 'stage' or $1 eq 'stash') { + $patch_mode = $1; + $arg = shift @ARGV or die __("missing --"); + } else { + die sprintf(__("unknown --patch mode: %s"), $1); + } + } else { + $patch_mode = 'stage'; + $arg = shift @ARGV or die __("missing --"); + } + die sprintf(__("invalid argument %s, expecting --"), + $arg) unless $arg eq "--"; + %patch_mode_flavour = %{$patch_modes{$patch_mode}}; + $patch_mode_only = 1; } elsif ($arg ne "--") { - die "invalid argument $arg, expecting --"; + die sprintf(__("invalid argument %s, expecting --"), $arg); } } @@ -1192,10 +1730,10 @@ sub main_loop { [ 'help', \&help_cmd, ], ); while (1) { - my ($it) = list_and_choose({ PROMPT => 'What now', + my ($it) = list_and_choose({ PROMPT => __('What now'), SINGLETON => 1, LIST_FLAT => 4, - HEADER => '*** Commands ***', + HEADER => __('*** Commands ***'), ON_EOF => \&quit_cmd, IMMEDIATE => 1 }, @cmd); if ($it) { @@ -1211,7 +1749,7 @@ sub main_loop { process_args(); refresh(); -if ($patch_mode) { +if ($patch_mode_only) { patch_update_cmd(); } else { |