expose a helper function peel_to_type().
[gitweb.git] / git-add--interactive.perl
index 83d017d23ceb5a634e8d8e79b154dd1d7e79d640..17ca5b84f0c077d61b9392bd7900954ce35d6cbf 100755 (executable)
@@ -1,11 +1,57 @@
 #!/usr/bin/perl -w
 
 use strict;
+use Git;
+
+my $repo = Git->repository();
+
+my $menu_use_color = $repo->get_colorbool('color.interactive');
+my ($prompt_color, $header_color, $help_color) =
+       $menu_use_color ? (
+               $repo->get_color('color.interactive.prompt', 'bold blue'),
+               $repo->get_color('color.interactive.header', 'bold'),
+               $repo->get_color('color.interactive.help', 'red bold'),
+       ) : ();
+
+my $diff_use_color = $repo->get_colorbool('color.diff');
+my ($fraginfo_color) =
+       $diff_use_color ? (
+               $repo->get_color('color.diff.frag', 'cyan'),
+       ) : ();
+
+my $normal_color = $repo->get_color("", "reset");
+
+sub colored {
+       my $color = shift;
+       my $string = join("", @_);
+
+       if (defined $color) {
+               # Put a color code at the beginning of each line, a reset at the end
+               # color after newlines that are not at the end of the string
+               $string =~ s/(\n+)(.)/$1$color$2/g;
+               # reset before newlines
+               $string =~ s/(\n+)/$normal_color$1/g;
+               # codes at beginning and end (if necessary):
+               $string =~ s/^/$color/;
+               $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
+       }
+       return $string;
+}
+
+# command line options
+my $patch_mode;
 
 sub run_cmd_pipe {
-       my $fh = undef;
-       open($fh, '-|', @_) or die;
-       return <$fh>;
+       if ($^O eq 'MSWin32') {
+               my @invalid = grep {m/[":*]/} @_;
+               die "$^O does not support: @invalid\n" if @invalid;
+               my @args = map { m/ /o ? "\"$_\"": $_ } @_;
+               return qx{@args};
+       } else {
+               my $fh = undef;
+               open($fh, '-|', @_) or die;
+               return <$fh>;
+       }
 }
 
 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
@@ -17,7 +63,7 @@ sub run_cmd_pipe {
 
 sub refresh {
        my $fh;
-       open $fh, '-|', qw(git update-index --refresh)
+       open $fh, 'git update-index --refresh |'
            or die;
        while (<$fh>) {
                ;# ignore 'needs update'
@@ -30,17 +76,13 @@ sub list_untracked {
                chomp $_;
                $_;
        }
-       run_cmd_pipe(qw(git ls-files --others
-                       --exclude-per-directory=.gitignore),
-                    "--exclude-from=$GIT_DIR/info/exclude",
-                    '--', @_);
+       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');
 
 # Returns list of hashes, contents of each of which are:
-# PRINT:       print message
 # VALUE:       pathname
 # BINARY:      is a binary path
 # INDEX:       is index different from HEAD?
@@ -52,9 +94,17 @@ 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);
+       }
 
        for (run_cmd_pipe(qw(git diff-index --cached
-                            --numstat --summary HEAD))) {
+                            --numstat --summary HEAD --), @tracked)) {
                if (($add, $del, $file) =
                    /^([-\d]+)  ([-\d]+)        (.*)/) {
                        my ($change, $bin);
@@ -77,7 +127,7 @@ sub list_modified {
                }
        }
 
-       for (run_cmd_pipe(qw(git diff-files --numstat --summary))) {
+       for (run_cmd_pipe(qw(git diff-files --numstat --summary --), @tracked)) {
                if (($add, $del, $file) =
                    /^([-\d]+)  ([-\d]+)        (.*)/) {
                        if (!exists $data{$file}) {
@@ -118,8 +168,6 @@ sub list_modified {
                }
                push @return, +{
                        VALUE => $_,
-                       PRINT => (sprintf $status_fmt,
-                                 $it->{INDEX}, $it->{FILE}, $_),
                        %$it,
                };
        }
@@ -155,10 +203,106 @@ sub find_unique {
        return $found;
 }
 
+# inserts string into trie and updates count for each character
+sub update_trie {
+       my ($trie, $string) = @_;
+       foreach (split //, $string) {
+               $trie = $trie->{$_} ||= {COUNT => 0};
+               $trie->{COUNT}++;
+       }
+}
+
+# returns an array of tuples (prefix, remainder)
+sub find_unique_prefixes {
+       my @stuff = @_;
+       my @return = ();
+
+       # any single prefix exceeding the soft limit is omitted
+       # if any prefix exceeds the hard limit all are omitted
+       # 0 indicates no limit
+       my $soft_limit = 0;
+       my $hard_limit = 3;
+
+       # build a trie modelling all possible options
+       my %trie;
+       foreach my $print (@stuff) {
+               if ((ref $print) eq 'ARRAY') {
+                       $print = $print->[0];
+               }
+               elsif ((ref $print) eq 'HASH') {
+                       $print = $print->{VALUE};
+               }
+               update_trie(\%trie, $print);
+               push @return, $print;
+       }
+
+       # use the trie to find the unique prefixes
+       for (my $i = 0; $i < @return; $i++) {
+               my $ret = $return[$i];
+               my @letters = split //, $ret;
+               my %search = %trie;
+               my ($prefix, $remainder);
+               my $j;
+               for ($j = 0; $j < @letters; $j++) {
+                       my $letter = $letters[$j];
+                       if ($search{$letter}{COUNT} == 1) {
+                               $prefix = substr $ret, 0, $j + 1;
+                               $remainder = substr $ret, $j + 1;
+                               last;
+                       }
+                       else {
+                               my $prefix = substr $ret, 0, $j;
+                               return ()
+                                   if ($hard_limit && $j + 1 > $hard_limit);
+                       }
+                       %search = %{$search{$letter}};
+               }
+               if ($soft_limit && $j + 1 > $soft_limit) {
+                       $prefix = undef;
+                       $remainder = $ret;
+               }
+               $return[$i] = [$prefix, $remainder];
+       }
+       return @return;
+}
+
+# filters out prefixes which have special meaning to list_and_choose()
+sub is_valid_prefix {
+       my $prefix = shift;
+       return (defined $prefix) &&
+           !($prefix =~ /[\s,]/) && # separators
+           !($prefix =~ /^-/) &&    # deselection
+           !($prefix =~ /^\d+/) &&  # selection
+           ($prefix ne '*') &&      # "all" wildcard
+           ($prefix ne '?');        # prompt help
+}
+
+# given a prefix/remainder tuple return a string with the prefix highlighted
+# for now use square brackets; later might use ANSI colors (underline, bold)
+sub highlight_prefix {
+       my $prefix = shift;
+       my $remainder = shift;
+
+       if (!defined $prefix) {
+               return $remainder;
+       }
+
+       if (!is_valid_prefix($prefix)) {
+               return "$prefix$remainder";
+       }
+
+       if (!$menu_use_color) {
+               return "[$prefix]$remainder";
+       }
+
+       return "$prompt_color$prefix$normal_color$remainder";
+}
+
 sub list_and_choose {
        my ($opts, @stuff) = @_;
        my (@chosen, @return);
        my $i;
+       my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
 
       TOPLOOP:
        while (1) {
@@ -168,18 +312,26 @@ sub list_and_choose {
                        if (!$opts->{LIST_FLAT}) {
                                print "     ";
                        }
-                       print "$opts->{HEADER}\n";
+                       print colored $header_color, "$opts->{HEADER}\n";
                }
                for ($i = 0; $i < @stuff; $i++) {
                        my $chosen = $chosen[$i] ? '*' : ' ';
                        my $print = $stuff[$i];
-                       if (ref $print) {
-                               if ((ref $print) eq 'ARRAY') {
-                                       $print = $print->[0];
-                               }
-                               else {
-                                       $print = $print->{PRINT};
-                               }
+                       my $ref = ref $print;
+                       my $highlighted = highlight_prefix(@{$prefixes[$i]})
+                           if @prefixes;
+                       if ($ref eq 'ARRAY') {
+                               $print = $highlighted || $print->[0];
+                       }
+                       elsif ($ref eq 'HASH') {
+                               my $value = $highlighted || $print->{VALUE};
+                               $print = sprintf($status_fmt,
+                                   $print->{INDEX},
+                                   $print->{FILE},
+                                   $value);
+                       }
+                       else {
+                               $print = $highlighted || $print;
                        }
                        printf("%s%2d: %s", $chosen, $i+1, $print);
                        if (($opts->{LIST_FLAT}) &&
@@ -198,7 +350,7 @@ sub list_and_choose {
 
                return if ($opts->{LIST_ONLY});
 
-               print $opts->{PROMPT};
+               print colored $prompt_color, $opts->{PROMPT};
                if ($opts->{SINGLETON}) {
                        print "> ";
                }
@@ -206,9 +358,19 @@ sub list_and_choose {
                        print ">> ";
                }
                my $line = <STDIN>;
-               last if (!$line);
+               if (!$line) {
+                       print "\n";
+                       $opts->{ON_EOF}->() if $opts->{ON_EOF};
+                       last;
+               }
                chomp $line;
-               my $donesomething = 0;
+               last if $line eq '';
+               if ($line eq '?') {
+                       $opts->{SINGLETON} ?
+                           singleton_prompt_help_cmd() :
+                           prompt_help_cmd();
+                       next TOPLOOP;
+               }
                for my $choice (split(/[\s,]+/, $line)) {
                        my $choose = 1;
                        my ($bottom, $top);
@@ -240,12 +402,11 @@ sub list_and_choose {
                                next TOPLOOP;
                        }
                        for ($i = $bottom-1; $i <= $top-1; $i++) {
-                               next if (@stuff <= $i);
+                               next if (@stuff <= $i || $i < 0);
                                $chosen[$i] = $choose;
-                               $donesomething++;
                        }
                }
-               last if (!$donesomething || $opts->{IMMEDIATE});
+               last if ($opts->{IMMEDIATE} || $line eq '*');
        }
        for ($i = 0; $i < @stuff; $i++) {
                if ($chosen[$i]) {
@@ -255,6 +416,28 @@ sub list_and_choose {
        return @return;
 }
 
+sub singleton_prompt_help_cmd {
+       print colored $help_color, <<\EOF ;
+Prompt help:
+1          - select a numbered item
+foo        - select item based on unique prefix
+           - (empty) select nothing
+EOF
+}
+
+sub prompt_help_cmd {
+       print colored $help_color, <<\EOF ;
+Prompt help:
+1          - select a single item
+3-5        - select a range of items
+2-3,6-9    - select multiple ranges
+foo        - select item based on unique prefix
+-...       - unselect specified items
+*          - choose all items
+           - (empty) finish selecting
+EOF
+}
+
 sub status_cmd {
        list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
                        list_modified());
@@ -281,7 +464,7 @@ sub update_cmd {
                                       HEADER => $status_head, },
                                     @mods);
        if (@update) {
-               system(qw(git update-index --add --),
+               system(qw(git update-index --add --remove --),
                       map { $_->{VALUE} } @update);
                say_n_paths('updated', @update);
        }
@@ -296,7 +479,7 @@ sub revert_cmd {
                my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
                                         map { $_->{VALUE} } @update);
                my $fh;
-               open $fh, '|-', qw(git update-index --index-info)
+               open $fh, '| git update-index --index-info'
                    or die;
                for (@lines) {
                        print $fh $_;
@@ -329,59 +512,282 @@ sub add_untracked_cmd {
 sub parse_diff {
        my ($path) = @_;
        my @diff = run_cmd_pipe(qw(git diff-files -p --), $path);
-       my (@hunk) = { TEXT => [] };
+       my @colored = ();
+       if ($diff_use_color) {
+               @colored = run_cmd_pipe(qw(git diff-files -p --color --), $path);
+       }
+       my (@hunk) = { TEXT => [], DISPLAY => [] };
 
-       for (@diff) {
-               if (/^@@ /) {
-                       push @hunk, { TEXT => [] };
+       for (my $i = 0; $i < @diff; $i++) {
+               if ($diff[$i] =~ /^@@ /) {
+                       push @hunk, { TEXT => [], DISPLAY => [] };
                }
-               push @{$hunk[-1]{TEXT}}, $_;
+               push @{$hunk[-1]{TEXT}}, $diff[$i];
+               push @{$hunk[-1]{DISPLAY}},
+                       ($diff_use_color ? $colored[$i] : $diff[$i]);
        }
        return @hunk;
 }
 
+sub hunk_splittable {
+       my ($text) = @_;
+
+       my @s = split_hunk($text);
+       return (1 < @s);
+}
+
+sub parse_hunk_header {
+       my ($line) = @_;
+       my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
+           $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
+       $o_cnt = 1 unless defined $o_cnt;
+       $n_cnt = 1 unless defined $n_cnt;
+       return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
+}
+
+sub split_hunk {
+       my ($text, $display) = @_;
+       my @split = ();
+       if (!defined $display) {
+               $display = $text;
+       }
+       # If there are context lines in the middle of a hunk,
+       # it can be split, but we would need to take care of
+       # overlaps later.
+
+       my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
+       my $hunk_start = 1;
+
+      OUTER:
+       while (1) {
+               my $next_hunk_start = undef;
+               my $i = $hunk_start - 1;
+               my $this = +{
+                       TEXT => [],
+                       DISPLAY => [],
+                       OLD => $o_ofs,
+                       NEW => $n_ofs,
+                       OCNT => 0,
+                       NCNT => 0,
+                       ADDDEL => 0,
+                       POSTCTX => 0,
+                       USE => undef,
+               };
+
+               while (++$i < @$text) {
+                       my $line = $text->[$i];
+                       my $display = $display->[$i];
+                       if ($line =~ /^ /) {
+                               if ($this->{ADDDEL} &&
+                                   !defined $next_hunk_start) {
+                                       # We have seen leading context and
+                                       # adds/dels and then here is another
+                                       # context, which is trailing for this
+                                       # split hunk and leading for the next
+                                       # one.
+                                       $next_hunk_start = $i;
+                               }
+                               push @{$this->{TEXT}}, $line;
+                               push @{$this->{DISPLAY}}, $display;
+                               $this->{OCNT}++;
+                               $this->{NCNT}++;
+                               if (defined $next_hunk_start) {
+                                       $this->{POSTCTX}++;
+                               }
+                               next;
+                       }
+
+                       # add/del
+                       if (defined $next_hunk_start) {
+                               # We are done with the current hunk and
+                               # this is the first real change for the
+                               # next split one.
+                               $hunk_start = $next_hunk_start;
+                               $o_ofs = $this->{OLD} + $this->{OCNT};
+                               $n_ofs = $this->{NEW} + $this->{NCNT};
+                               $o_ofs -= $this->{POSTCTX};
+                               $n_ofs -= $this->{POSTCTX};
+                               push @split, $this;
+                               redo OUTER;
+                       }
+                       push @{$this->{TEXT}}, $line;
+                       push @{$this->{DISPLAY}}, $display;
+                       $this->{ADDDEL}++;
+                       if ($line =~ /^-/) {
+                               $this->{OCNT}++;
+                       }
+                       else {
+                               $this->{NCNT}++;
+                       }
+               }
+
+               push @split, $this;
+               last;
+       }
+
+       for my $hunk (@split) {
+               $o_ofs = $hunk->{OLD};
+               $n_ofs = $hunk->{NEW};
+               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 $display_head = $head;
+               unshift @{$hunk->{TEXT}}, $head;
+               if ($diff_use_color) {
+                       $display_head = colored($fraginfo_color, $head);
+               }
+               unshift @{$hunk->{DISPLAY}}, $display_head;
+       }
+       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);
+
+       for (grep { $_->{USE} } @in) {
+               my $text = $_->{TEXT};
+               my ($o_ofs) = parse_hunk_header($text->[0]);
+               if (defined $last_o_ctx &&
+                   $o_ofs <= $last_o_ctx) {
+                       merge_hunk($out[-1], $_);
+               }
+               else {
+                       push @out, $_;
+               }
+               $last_o_ctx = find_last_o_ctx($out[-1]);
+       }
+       return @out;
+}
+
 sub help_patch_cmd {
-       print <<\EOF ;
+       print colored $help_color, <<\EOF ;
 y - stage this hunk
 n - do not stage this hunk
-a - stage this and all the remaining hunks
-d - do not stage this hunk nor any of the remaining hunks
+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
 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
+s - split the current hunk into smaller hunks
+? - print help
 EOF
 }
 
 sub patch_update_cmd {
-       my @mods = list_modified('file-only');
-       @mods = grep { !($_->{BINARY}) } @mods;
-       return if (!@mods);
+       my @mods = grep { !($_->{BINARY}) } list_modified('file-only');
+       my @them;
 
-       my ($it) = list_and_choose({ PROMPT => 'Patch update',
-                                    SINGLETON => 1,
-                                    IMMEDIATE => 1,
-                                    HEADER => $status_head, },
-                                  @mods);
-       return if (!$it);
+       if (!@mods) {
+               print STDERR "No changes.\n";
+               return 0;
+       }
+       if ($patch_mode) {
+               @them = @mods;
+       }
+       else {
+               @them = list_and_choose({ PROMPT => 'Patch update',
+                                         HEADER => $status_head, },
+                                       @mods);
+       }
+       for (@them) {
+               patch_update_file($_->{VALUE});
+       }
+}
 
+sub patch_update_file {
        my ($ix, $num);
-       my $path = $it->{VALUE};
+       my $path = shift;
        my ($head, @hunk) = parse_diff($path);
-       for (@{$head->{TEXT}}) {
+       for (@{$head->{DISPLAY}}) {
                print;
        }
        $num = scalar @hunk;
        $ix = 0;
 
        while (1) {
-               my ($prev, $next, $other, $undecided);
+               my ($prev, $next, $other, $undecided, $i);
                $other = '';
 
                if ($num <= $ix) {
                        $ix = 0;
                }
-               for (my $i = 0; $i < $ix; $i++) {
+               for ($i = 0; $i < $ix; $i++) {
                        if (!defined $hunk[$i]{USE}) {
                                $prev = 1;
                                $other .= '/k';
@@ -391,7 +797,7 @@ sub patch_update_cmd {
                if ($ix) {
                        $other .= '/K';
                }
-               for (my $i = $ix + 1; $i < $num; $i++) {
+               for ($i = $ix + 1; $i < $num; $i++) {
                        if (!defined $hunk[$i]{USE}) {
                                $next = 1;
                                $other .= '/j';
@@ -401,7 +807,7 @@ sub patch_update_cmd {
                if ($ix < $num - 1) {
                        $other .= '/J';
                }
-               for (my $i = 0; $i < $num; $i++) {
+               for ($i = 0; $i < $num; $i++) {
                        if (!defined $hunk[$i]{USE}) {
                                $undecided = 1;
                                last;
@@ -409,10 +815,13 @@ sub patch_update_cmd {
                }
                last if (!$undecided);
 
-               for (@{$hunk[$ix]{TEXT}}) {
+               if (hunk_splittable($hunk[$ix]{TEXT})) {
+                       $other .= '/s';
+               }
+               for (@{$hunk[$ix]{DISPLAY}}) {
                        print;
                }
-               print "Stage this hunk [y/n/a/d$other/?]? ";
+               print colored $prompt_color, "Stage this hunk [y/n/a/d$other/?]? ";
                my $line = <STDIN>;
                if ($line) {
                        if ($line =~ /^y/i) {
@@ -463,6 +872,16 @@ sub patch_update_cmd {
                                }
                                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";
+                               }
+                               splice (@hunk, $ix, 1, @split);
+                               $num = scalar @hunk;
+                               next;
+                       }
                        else {
                                help_patch_cmd($other);
                                next;
@@ -476,17 +895,34 @@ sub patch_update_cmd {
                }
        }
 
-       my ($o_lno, $n_lno);
+       @hunk = coalesce_overlapping_hunks(@hunk);
+
+       my $n_lofs = 0;
        my @result = ();
        for (@hunk) {
                my $text = $_->{TEXT};
                my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
-                   $text->[0] =~ /^@@ -(\d+)(?:,(\d+)) \+(\d+)(?:,(\d+)) @@/;
+                   parse_hunk_header($text->[0]);
+
                if (!$_->{USE}) {
-                       # Adjust offset here.
+                       # We would have added ($n_cnt - $o_cnt) lines
+                       # to the postimage if we were to use this hunk,
+                       # but we didn't.  So the line number that the next
+                       # hunk starts at would be shifted by that much.
+                       $n_lofs -= ($n_cnt - $o_cnt);
                        next;
                }
                else {
+                       if ($n_lofs) {
+                               $n_ofs += $n_lofs;
+                               $text->[0] = ("@@ -$o_ofs" .
+                                             (($o_cnt != 1)
+                                              ? ",$o_cnt" : '') .
+                                             " +$n_ofs" .
+                                             (($n_cnt != 1)
+                                              ? ",$n_cnt" : '') .
+                                             " @@\n");
+                       }
                        for (@$text) {
                                push @result, $_;
                        }
@@ -496,11 +932,15 @@ sub patch_update_cmd {
        if (@result) {
                my $fh;
 
-               open $fh, '|-', qw(git apply --cached);
+               open $fh, '| git apply --cached';
                for (@{$head->{TEXT}}, @result) {
                        print $fh $_;
                }
-               close $fh;
+               if (!close $fh) {
+                       for (@{$head->{TEXT}}, @result) {
+                               print STDERR $_;
+                       }
+               }
                refresh();
        }
 
@@ -516,8 +956,7 @@ sub diff_cmd {
                                     HEADER => $status_head, },
                                   @mods);
        return if (!@them);
-       system(qw(git diff-index -p --cached HEAD --),
-              map { $_->{VALUE} } @them);
+       system(qw(git diff -p --cached HEAD --), map { $_->{VALUE} } @them);
 }
 
 sub quit_cmd {
@@ -526,7 +965,7 @@ sub quit_cmd {
 }
 
 sub help_cmd {
-       print <<\EOF ;
+       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
@@ -536,6 +975,20 @@ sub help_cmd {
 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 "--";
+       }
+       elsif ($arg ne "--") {
+               die "invalid argument $arg, expecting --";
+       }
+}
+
 sub main_loop {
        my @cmd = ([ 'status', \&status_cmd, ],
                   [ 'update', \&update_cmd, ],
@@ -551,6 +1004,7 @@ sub main_loop {
                                             SINGLETON => 1,
                                             LIST_FLAT => 4,
                                             HEADER => '*** Commands ***',
+                                            ON_EOF => \&quit_cmd,
                                             IMMEDIATE => 1 }, @cmd);
                if ($it) {
                        eval {
@@ -563,8 +1017,12 @@ sub main_loop {
        }
 }
 
-my @z;
-
+process_args();
 refresh();
-status_cmd();
-main_loop();
+if ($patch_mode) {
+       patch_update_cmd();
+}
+else {
+       status_cmd();
+       main_loop();
+}