travis-ci: move Travis CI code into dedicated scripts
[gitweb.git] / git-add--interactive.perl
index f6e536ece314316ebb281721ed27d7519577202f..28b325d75481b74e8f7b16485e1fc18ad854161c 100755 (executable)
@@ -1,7 +1,10 @@
-#!/usr/bin/perl -w
+#!/usr/bin/perl
 
+use 5.008;
 use strict;
-use Git;
+use warnings;
+use Git qw(unquote_path);
+use Git::I18N;
 
 binmode(STDOUT, ":raw");
 
 
 my $normal_color = $repo->get_color("", "reset");
 
+my $diff_algorithm = $repo->config('diff.algorithm');
+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")) {
                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 {
@@ -71,10 +91,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 ? "\"$_\"": $_ } @_;
@@ -93,47 +174,6 @@ sub run_cmd_pipe {
 }
 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 |'
@@ -152,8 +192,9 @@ sub list_untracked {
        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;
@@ -168,6 +209,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
@@ -175,31 +227,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 $_;
-                       unquote_path($_);
-               } 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 {
@@ -208,7 +252,7 @@ sub list_modified {
                        $data{$file} = {
                                INDEX => $change,
                                BINARY => $bin,
-                               FILE => 'nothing',
+                               FILE => __('nothing'),
                        }
                }
                elsif (($adddel, $file) =
@@ -218,19 +262,13 @@ sub list_modified {
                }
        }
 
-       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]+)        (.*)/) {
                        $file = unquote_path($file);
-                       if (!exists $data{$file}) {
-                               $data{$file} = +{
-                                       INDEX => 'unchanged',
-                                       BINARY => 0,
-                               };
-                       }
                        my ($change, $bin);
                        if ($add eq '-' && $del eq '-') {
-                               $change = 'binary';
+                               $change = __('binary');
                                $bin = 1;
                        }
                        else {
@@ -246,6 +284,18 @@ sub list_modified {
                        $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) {
@@ -253,10 +303,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, +{
@@ -399,6 +449,9 @@ sub error_msg {
 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};
 
@@ -491,12 +544,12 @@ sub list_and_choose {
                        else {
                                $bottom = $top = find_unique($choice, @stuff);
                                if (!defined $bottom) {
-                                       error_msg "Huh ($choice)?\n";
+                                       error_msg sprintf(__("Huh (%s)?\n"), $choice);
                                        next TOPLOOP;
                                }
                        }
                        if ($opts->{SINGLETON} && $bottom != $top) {
-                               error_msg "Huh ($choice)?\n";
+                               error_msg sprintf(__("Huh (%s)?\n"), $choice);
                                next TOPLOOP;
                        }
                        for ($i = $bottom-1; $i <= $top-1; $i++) {
@@ -515,7 +568,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
@@ -524,7 +577,7 @@ sub singleton_prompt_help_cmd {
 }
 
 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
@@ -545,12 +598,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);
        }
 }
 
@@ -558,7 +617,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) {
@@ -570,7 +629,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) {
@@ -593,7 +652,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});
                                }
                        }
                }
@@ -604,21 +663,45 @@ 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 (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 => [], TYPE => 'header' };
 
@@ -629,7 +712,7 @@ sub parse_diff {
                }
                push @{$hunk[-1]{TEXT}}, $diff[$i];
                push @{$hunk[-1]{DISPLAY}},
-                       ($diff_use_color ? $colored[$i] : $diff[$i]);
+                       (@colored ? $colored[$i] : $diff[$i]);
        }
        return @hunk;
 }
@@ -639,14 +722,17 @@ sub parse_diff_header {
 
        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 {
@@ -767,6 +853,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 {
@@ -778,30 +980,58 @@ 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) {
@@ -809,8 +1039,8 @@ sub edit_hunk_manually {
        }
 
        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 { !/^\Q$comment_line_char\E/ } <$fh>;
        close $fh;
        unlink $hunkfile;
 
@@ -827,12 +1057,8 @@ sub edit_hunk_manually {
 }
 
 sub diff_applies {
-       my $fh;
-       open $fh, '| git apply --recount --cached --check';
-       for my $h (@_) {
-               print $fh @{$h->{TEXT}};
-       }
-       return close $fh;
+       return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
+                            map { @{$_->{TEXT}} } @_);
 }
 
 sub _restore_terminal_and_die {
@@ -848,6 +1074,14 @@ sub prompt_single_character {
                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;
@@ -861,6 +1095,7 @@ sub prompt_yesno {
        while (1) {
                print colored $prompt_color, $prompt;
                my $line = prompt_single_character;
+               return undef unless defined $line;
                return 0 if $line =~ /^n/i;
                return 1 if $line =~ /^y/i;
        }
@@ -878,7 +1113,8 @@ sub edit_hunk_loop {
                my $newhunk = {
                        TEXT => $text,
                        TYPE => $hunk->[$ix]->{TYPE},
-                       USE => 1
+                       USE => 1,
+                       DIRTY => 1,
                };
                if (diff_applies($head,
                                 @{$hunk}[0..$ix-1],
@@ -889,20 +1125,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
-q - quit, do not stage this hunk nor any of the remaining ones
-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
@@ -915,24 +1197,60 @@ sub help_patch_cmd {
 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);
        }
@@ -964,7 +1282,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;
@@ -982,12 +1300,50 @@ 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;
        }
@@ -995,6 +1351,13 @@ sub patch_update_file {
        if (@{$mode->{TEXT}}) {
                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;
        $ix = 0;
@@ -1047,10 +1410,11 @@ sub patch_update_file {
                for (@{$hunk[$ix]{DISPLAY}}) {
                        print;
                }
-               print colored $prompt_color, 'Stage ',
-                 ($hunk[$ix]{TYPE} eq 'mode' ? 'mode change' : 'this hunk'),
-                 " [y,n,q,a,d,/$other,?]? ";
+               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;
@@ -1071,12 +1435,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 = '';
@@ -1084,11 +1448,13 @@ sub patch_update_file {
                                        chomp $response;
                                }
                                if ($response !~ /^\s*\d+\s*$/) {
-                                       error_msg "Invalid number: '$response'\n";
+                                       error_msg sprintf(__("Invalid number: '%s'\n"),
+                                                            $response);
                                } elsif (0 < $response && $response <= $num) {
                                        $ix = $response - 1;
                                } else {
-                                       error_msg "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;
                        }
@@ -1102,19 +1468,18 @@ sub patch_update_file {
                                next;
                        }
                        elsif ($line =~ /^q/i) {
-                               while ($ix < $num) {
-                                       if (!defined $hunk[$ix]{USE}) {
-                                               $hunk[$ix]{USE} = 0;
+                               for ($i = 0; $i < $num; $i++) {
+                                       if (!defined $hunk[$i]{USE}) {
+                                               $hunk[$i]{USE} = 0;
                                        }
-                                       $ix++;
                                }
                                $quit = 1;
-                               next;
+                               last;
                        }
                        elsif ($line =~ m|^/(.*)|) {
                                my $regex = $1;
                                if ($1 eq "") {
-                                       print colored $prompt_color, "search for regex? ";
+                                       print colored $prompt_color, __("search for regex? ");
                                        $regex = <STDIN>;
                                        if (defined $regex) {
                                                chomp $regex;
@@ -1127,7 +1492,7 @@ sub patch_update_file {
                                if ($@) {
                                        my ($err,$exp) = ($@, $1);
                                        $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
-                                       error_msg "Malformed search regexp $exp: $err\n";
+                                       error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
                                        next;
                                }
                                my $iy = $ix;
@@ -1137,7 +1502,7 @@ sub patch_update_file {
                                        $iy++;
                                        $iy = 0 if ($iy >= $num);
                                        if ($ix == $iy) {
-                                               error_msg "No hunk matches the given pattern\n";
+                                               error_msg __("No hunk matches the given pattern\n");
                                                last;
                                        }
                                }
@@ -1149,7 +1514,7 @@ sub patch_update_file {
                                        $ix--;
                                }
                                else {
-                                       error_msg "No previous hunk\n";
+                                       error_msg __("No previous hunk\n");
                                }
                                next;
                        }
@@ -1158,7 +1523,7 @@ sub patch_update_file {
                                        $ix++;
                                }
                                else {
-                                       error_msg "No next hunk\n";
+                                       error_msg __("No next hunk\n");
                                }
                                next;
                        }
@@ -1171,21 +1536,23 @@ sub patch_update_file {
                                        }
                                }
                                else {
-                                       error_msg "No previous hunk\n";
+                                       error_msg __("No previous hunk\n");
                                }
                                next;
                        }
                        elsif ($line =~ /^j/) {
                                if ($other !~ /j/) {
-                                       error_msg "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;
@@ -1210,6 +1577,8 @@ sub patch_update_file {
                }
        }
 
+       @hunk = coalesce_overlapping_hunks(@hunk);
+
        my $n_lofs = 0;
        my @result = ();
        for (@hunk) {
@@ -1219,17 +1588,9 @@ 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();
        }
 
@@ -1241,28 +1602,30 @@ 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
 }
@@ -1270,14 +1633,45 @@ sub help_cmd {
 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);
        }
 }
 
@@ -1292,10 +1686,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) {
@@ -1311,7 +1705,7 @@ sub main_loop {
 
 process_args();
 refresh();
-if ($patch_mode) {
+if ($patch_mode_only) {
        patch_update_cmd();
 }
 else {