perl: bump the required Perl version to 5.8 from 5.6.[21]
[git] / git-add--interactive.perl
1 #!/usr/bin/perl -w
2
3 use 5.008;
4 use strict;
5 use Git;
6
7 binmode(STDOUT, ":raw");
8
9 my $repo = Git->repository();
10
11 my $menu_use_color = $repo->get_colorbool('color.interactive');
12 my ($prompt_color, $header_color, $help_color) =
13         $menu_use_color ? (
14                 $repo->get_color('color.interactive.prompt', 'bold blue'),
15                 $repo->get_color('color.interactive.header', 'bold'),
16                 $repo->get_color('color.interactive.help', 'red bold'),
17         ) : ();
18 my $error_color = ();
19 if ($menu_use_color) {
20         my $help_color_spec = ($repo->config('color.interactive.help') or
21                                 'red bold');
22         $error_color = $repo->get_color('color.interactive.error',
23                                         $help_color_spec);
24 }
25
26 my $diff_use_color = $repo->get_colorbool('color.diff');
27 my ($fraginfo_color) =
28         $diff_use_color ? (
29                 $repo->get_color('color.diff.frag', 'cyan'),
30         ) : ();
31 my ($diff_plain_color) =
32         $diff_use_color ? (
33                 $repo->get_color('color.diff.plain', ''),
34         ) : ();
35 my ($diff_old_color) =
36         $diff_use_color ? (
37                 $repo->get_color('color.diff.old', 'red'),
38         ) : ();
39 my ($diff_new_color) =
40         $diff_use_color ? (
41                 $repo->get_color('color.diff.new', 'green'),
42         ) : ();
43
44 my $normal_color = $repo->get_color("", "reset");
45
46 my $use_readkey = 0;
47 sub ReadMode;
48 sub ReadKey;
49 if ($repo->config_bool("interactive.singlekey")) {
50         eval {
51                 require Term::ReadKey;
52                 Term::ReadKey->import;
53                 $use_readkey = 1;
54         };
55 }
56
57 sub colored {
58         my $color = shift;
59         my $string = join("", @_);
60
61         if (defined $color) {
62                 # Put a color code at the beginning of each line, a reset at the end
63                 # color after newlines that are not at the end of the string
64                 $string =~ s/(\n+)(.)/$1$color$2/g;
65                 # reset before newlines
66                 $string =~ s/(\n+)/$normal_color$1/g;
67                 # codes at beginning and end (if necessary):
68                 $string =~ s/^/$color/;
69                 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
70         }
71         return $string;
72 }
73
74 # command line options
75 my $patch_mode;
76 my $patch_mode_revision;
77
78 sub apply_patch;
79 sub apply_patch_for_checkout_commit;
80 sub apply_patch_for_stash;
81
82 my %patch_modes = (
83         'stage' => {
84                 DIFF => 'diff-files -p',
85                 APPLY => sub { apply_patch 'apply --cached', @_; },
86                 APPLY_CHECK => 'apply --cached',
87                 VERB => 'Stage',
88                 TARGET => '',
89                 PARTICIPLE => 'staging',
90                 FILTER => 'file-only',
91         },
92         'stash' => {
93                 DIFF => 'diff-index -p HEAD',
94                 APPLY => sub { apply_patch 'apply --cached', @_; },
95                 APPLY_CHECK => 'apply --cached',
96                 VERB => 'Stash',
97                 TARGET => '',
98                 PARTICIPLE => 'stashing',
99                 FILTER => undef,
100         },
101         'reset_head' => {
102                 DIFF => 'diff-index -p --cached',
103                 APPLY => sub { apply_patch 'apply -R --cached', @_; },
104                 APPLY_CHECK => 'apply -R --cached',
105                 VERB => 'Unstage',
106                 TARGET => '',
107                 PARTICIPLE => 'unstaging',
108                 FILTER => 'index-only',
109         },
110         'reset_nothead' => {
111                 DIFF => 'diff-index -R -p --cached',
112                 APPLY => sub { apply_patch 'apply --cached', @_; },
113                 APPLY_CHECK => 'apply --cached',
114                 VERB => 'Apply',
115                 TARGET => ' to index',
116                 PARTICIPLE => 'applying',
117                 FILTER => 'index-only',
118         },
119         'checkout_index' => {
120                 DIFF => 'diff-files -p',
121                 APPLY => sub { apply_patch 'apply -R', @_; },
122                 APPLY_CHECK => 'apply -R',
123                 VERB => 'Discard',
124                 TARGET => ' from worktree',
125                 PARTICIPLE => 'discarding',
126                 FILTER => 'file-only',
127         },
128         'checkout_head' => {
129                 DIFF => 'diff-index -p',
130                 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
131                 APPLY_CHECK => 'apply -R',
132                 VERB => 'Discard',
133                 TARGET => ' from index and worktree',
134                 PARTICIPLE => 'discarding',
135                 FILTER => undef,
136         },
137         'checkout_nothead' => {
138                 DIFF => 'diff-index -R -p',
139                 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
140                 APPLY_CHECK => 'apply',
141                 VERB => 'Apply',
142                 TARGET => ' to index and worktree',
143                 PARTICIPLE => 'applying',
144                 FILTER => undef,
145         },
146 );
147
148 my %patch_mode_flavour = %{$patch_modes{stage}};
149
150 sub run_cmd_pipe {
151         if ($^O eq 'MSWin32' || $^O eq 'msys') {
152                 my @invalid = grep {m/[":*]/} @_;
153                 die "$^O does not support: @invalid\n" if @invalid;
154                 my @args = map { m/ /o ? "\"$_\"": $_ } @_;
155                 return qx{@args};
156         } else {
157                 my $fh = undef;
158                 open($fh, '-|', @_) or die;
159                 return <$fh>;
160         }
161 }
162
163 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
164
165 if (!defined $GIT_DIR) {
166         exit(1); # rev-parse would have already said "not a git repo"
167 }
168 chomp($GIT_DIR);
169
170 my %cquote_map = (
171  "b" => chr(8),
172  "t" => chr(9),
173  "n" => chr(10),
174  "v" => chr(11),
175  "f" => chr(12),
176  "r" => chr(13),
177  "\\" => "\\",
178  "\042" => "\042",
179 );
180
181 sub unquote_path {
182         local ($_) = @_;
183         my ($retval, $remainder);
184         if (!/^\042(.*)\042$/) {
185                 return $_;
186         }
187         ($_, $retval) = ($1, "");
188         while (/^([^\\]*)\\(.*)$/) {
189                 $remainder = $2;
190                 $retval .= $1;
191                 for ($remainder) {
192                         if (/^([0-3][0-7][0-7])(.*)$/) {
193                                 $retval .= chr(oct($1));
194                                 $_ = $2;
195                                 last;
196                         }
197                         if (/^([\\\042btnvfr])(.*)$/) {
198                                 $retval .= $cquote_map{$1};
199                                 $_ = $2;
200                                 last;
201                         }
202                         # This is malformed -- just return it as-is for now.
203                         return $_[0];
204                 }
205                 $_ = $remainder;
206         }
207         $retval .= $_;
208         return $retval;
209 }
210
211 sub refresh {
212         my $fh;
213         open $fh, 'git update-index --refresh |'
214             or die;
215         while (<$fh>) {
216                 ;# ignore 'needs update'
217         }
218         close $fh;
219 }
220
221 sub list_untracked {
222         map {
223                 chomp $_;
224                 unquote_path($_);
225         }
226         run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
227 }
228
229 my $status_fmt = '%12s %12s %s';
230 my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path');
231
232 {
233         my $initial;
234         sub is_initial_commit {
235                 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
236                         unless defined $initial;
237                 return $initial;
238         }
239 }
240
241 sub get_empty_tree {
242         return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
243 }
244
245 # Returns list of hashes, contents of each of which are:
246 # VALUE:        pathname
247 # BINARY:       is a binary path
248 # INDEX:        is index different from HEAD?
249 # FILE:         is file different from index?
250 # INDEX_ADDDEL: is it add/delete between HEAD and index?
251 # FILE_ADDDEL:  is it add/delete between index and file?
252
253 sub list_modified {
254         my ($only) = @_;
255         my (%data, @return);
256         my ($add, $del, $adddel, $file);
257         my @tracked = ();
258
259         if (@ARGV) {
260                 @tracked = map {
261                         chomp $_;
262                         unquote_path($_);
263                 } run_cmd_pipe(qw(git ls-files --), @ARGV);
264                 return if (!@tracked);
265         }
266
267         my $reference;
268         if (defined $patch_mode_revision and $patch_mode_revision ne 'HEAD') {
269                 $reference = $patch_mode_revision;
270         } elsif (is_initial_commit()) {
271                 $reference = get_empty_tree();
272         } else {
273                 $reference = 'HEAD';
274         }
275         for (run_cmd_pipe(qw(git diff-index --cached
276                              --numstat --summary), $reference,
277                              '--', @tracked)) {
278                 if (($add, $del, $file) =
279                     /^([-\d]+)  ([-\d]+)        (.*)/) {
280                         my ($change, $bin);
281                         $file = unquote_path($file);
282                         if ($add eq '-' && $del eq '-') {
283                                 $change = 'binary';
284                                 $bin = 1;
285                         }
286                         else {
287                                 $change = "+$add/-$del";
288                         }
289                         $data{$file} = {
290                                 INDEX => $change,
291                                 BINARY => $bin,
292                                 FILE => 'nothing',
293                         }
294                 }
295                 elsif (($adddel, $file) =
296                        /^ (create|delete) mode [0-7]+ (.*)$/) {
297                         $file = unquote_path($file);
298                         $data{$file}{INDEX_ADDDEL} = $adddel;
299                 }
300         }
301
302         for (run_cmd_pipe(qw(git diff-files --numstat --summary --), @tracked)) {
303                 if (($add, $del, $file) =
304                     /^([-\d]+)  ([-\d]+)        (.*)/) {
305                         $file = unquote_path($file);
306                         if (!exists $data{$file}) {
307                                 $data{$file} = +{
308                                         INDEX => 'unchanged',
309                                         BINARY => 0,
310                                 };
311                         }
312                         my ($change, $bin);
313                         if ($add eq '-' && $del eq '-') {
314                                 $change = 'binary';
315                                 $bin = 1;
316                         }
317                         else {
318                                 $change = "+$add/-$del";
319                         }
320                         $data{$file}{FILE} = $change;
321                         if ($bin) {
322                                 $data{$file}{BINARY} = 1;
323                         }
324                 }
325                 elsif (($adddel, $file) =
326                        /^ (create|delete) mode [0-7]+ (.*)$/) {
327                         $file = unquote_path($file);
328                         $data{$file}{FILE_ADDDEL} = $adddel;
329                 }
330         }
331
332         for (sort keys %data) {
333                 my $it = $data{$_};
334
335                 if ($only) {
336                         if ($only eq 'index-only') {
337                                 next if ($it->{INDEX} eq 'unchanged');
338                         }
339                         if ($only eq 'file-only') {
340                                 next if ($it->{FILE} eq 'nothing');
341                         }
342                 }
343                 push @return, +{
344                         VALUE => $_,
345                         %$it,
346                 };
347         }
348         return @return;
349 }
350
351 sub find_unique {
352         my ($string, @stuff) = @_;
353         my $found = undef;
354         for (my $i = 0; $i < @stuff; $i++) {
355                 my $it = $stuff[$i];
356                 my $hit = undef;
357                 if (ref $it) {
358                         if ((ref $it) eq 'ARRAY') {
359                                 $it = $it->[0];
360                         }
361                         else {
362                                 $it = $it->{VALUE};
363                         }
364                 }
365                 eval {
366                         if ($it =~ /^$string/) {
367                                 $hit = 1;
368                         };
369                 };
370                 if (defined $hit && defined $found) {
371                         return undef;
372                 }
373                 if ($hit) {
374                         $found = $i + 1;
375                 }
376         }
377         return $found;
378 }
379
380 # inserts string into trie and updates count for each character
381 sub update_trie {
382         my ($trie, $string) = @_;
383         foreach (split //, $string) {
384                 $trie = $trie->{$_} ||= {COUNT => 0};
385                 $trie->{COUNT}++;
386         }
387 }
388
389 # returns an array of tuples (prefix, remainder)
390 sub find_unique_prefixes {
391         my @stuff = @_;
392         my @return = ();
393
394         # any single prefix exceeding the soft limit is omitted
395         # if any prefix exceeds the hard limit all are omitted
396         # 0 indicates no limit
397         my $soft_limit = 0;
398         my $hard_limit = 3;
399
400         # build a trie modelling all possible options
401         my %trie;
402         foreach my $print (@stuff) {
403                 if ((ref $print) eq 'ARRAY') {
404                         $print = $print->[0];
405                 }
406                 elsif ((ref $print) eq 'HASH') {
407                         $print = $print->{VALUE};
408                 }
409                 update_trie(\%trie, $print);
410                 push @return, $print;
411         }
412
413         # use the trie to find the unique prefixes
414         for (my $i = 0; $i < @return; $i++) {
415                 my $ret = $return[$i];
416                 my @letters = split //, $ret;
417                 my %search = %trie;
418                 my ($prefix, $remainder);
419                 my $j;
420                 for ($j = 0; $j < @letters; $j++) {
421                         my $letter = $letters[$j];
422                         if ($search{$letter}{COUNT} == 1) {
423                                 $prefix = substr $ret, 0, $j + 1;
424                                 $remainder = substr $ret, $j + 1;
425                                 last;
426                         }
427                         else {
428                                 my $prefix = substr $ret, 0, $j;
429                                 return ()
430                                     if ($hard_limit && $j + 1 > $hard_limit);
431                         }
432                         %search = %{$search{$letter}};
433                 }
434                 if (ord($letters[0]) > 127 ||
435                     ($soft_limit && $j + 1 > $soft_limit)) {
436                         $prefix = undef;
437                         $remainder = $ret;
438                 }
439                 $return[$i] = [$prefix, $remainder];
440         }
441         return @return;
442 }
443
444 # filters out prefixes which have special meaning to list_and_choose()
445 sub is_valid_prefix {
446         my $prefix = shift;
447         return (defined $prefix) &&
448             !($prefix =~ /[\s,]/) && # separators
449             !($prefix =~ /^-/) &&    # deselection
450             !($prefix =~ /^\d+/) &&  # selection
451             ($prefix ne '*') &&      # "all" wildcard
452             ($prefix ne '?');        # prompt help
453 }
454
455 # given a prefix/remainder tuple return a string with the prefix highlighted
456 # for now use square brackets; later might use ANSI colors (underline, bold)
457 sub highlight_prefix {
458         my $prefix = shift;
459         my $remainder = shift;
460
461         if (!defined $prefix) {
462                 return $remainder;
463         }
464
465         if (!is_valid_prefix($prefix)) {
466                 return "$prefix$remainder";
467         }
468
469         if (!$menu_use_color) {
470                 return "[$prefix]$remainder";
471         }
472
473         return "$prompt_color$prefix$normal_color$remainder";
474 }
475
476 sub error_msg {
477         print STDERR colored $error_color, @_;
478 }
479
480 sub list_and_choose {
481         my ($opts, @stuff) = @_;
482         my (@chosen, @return);
483         my $i;
484         my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
485
486       TOPLOOP:
487         while (1) {
488                 my $last_lf = 0;
489
490                 if ($opts->{HEADER}) {
491                         if (!$opts->{LIST_FLAT}) {
492                                 print "     ";
493                         }
494                         print colored $header_color, "$opts->{HEADER}\n";
495                 }
496                 for ($i = 0; $i < @stuff; $i++) {
497                         my $chosen = $chosen[$i] ? '*' : ' ';
498                         my $print = $stuff[$i];
499                         my $ref = ref $print;
500                         my $highlighted = highlight_prefix(@{$prefixes[$i]})
501                             if @prefixes;
502                         if ($ref eq 'ARRAY') {
503                                 $print = $highlighted || $print->[0];
504                         }
505                         elsif ($ref eq 'HASH') {
506                                 my $value = $highlighted || $print->{VALUE};
507                                 $print = sprintf($status_fmt,
508                                     $print->{INDEX},
509                                     $print->{FILE},
510                                     $value);
511                         }
512                         else {
513                                 $print = $highlighted || $print;
514                         }
515                         printf("%s%2d: %s", $chosen, $i+1, $print);
516                         if (($opts->{LIST_FLAT}) &&
517                             (($i + 1) % ($opts->{LIST_FLAT}))) {
518                                 print "\t";
519                                 $last_lf = 0;
520                         }
521                         else {
522                                 print "\n";
523                                 $last_lf = 1;
524                         }
525                 }
526                 if (!$last_lf) {
527                         print "\n";
528                 }
529
530                 return if ($opts->{LIST_ONLY});
531
532                 print colored $prompt_color, $opts->{PROMPT};
533                 if ($opts->{SINGLETON}) {
534                         print "> ";
535                 }
536                 else {
537                         print ">> ";
538                 }
539                 my $line = <STDIN>;
540                 if (!$line) {
541                         print "\n";
542                         $opts->{ON_EOF}->() if $opts->{ON_EOF};
543                         last;
544                 }
545                 chomp $line;
546                 last if $line eq '';
547                 if ($line eq '?') {
548                         $opts->{SINGLETON} ?
549                             singleton_prompt_help_cmd() :
550                             prompt_help_cmd();
551                         next TOPLOOP;
552                 }
553                 for my $choice (split(/[\s,]+/, $line)) {
554                         my $choose = 1;
555                         my ($bottom, $top);
556
557                         # Input that begins with '-'; unchoose
558                         if ($choice =~ s/^-//) {
559                                 $choose = 0;
560                         }
561                         # A range can be specified like 5-7 or 5-.
562                         if ($choice =~ /^(\d+)-(\d*)$/) {
563                                 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
564                         }
565                         elsif ($choice =~ /^\d+$/) {
566                                 $bottom = $top = $choice;
567                         }
568                         elsif ($choice eq '*') {
569                                 $bottom = 1;
570                                 $top = 1 + @stuff;
571                         }
572                         else {
573                                 $bottom = $top = find_unique($choice, @stuff);
574                                 if (!defined $bottom) {
575                                         error_msg "Huh ($choice)?\n";
576                                         next TOPLOOP;
577                                 }
578                         }
579                         if ($opts->{SINGLETON} && $bottom != $top) {
580                                 error_msg "Huh ($choice)?\n";
581                                 next TOPLOOP;
582                         }
583                         for ($i = $bottom-1; $i <= $top-1; $i++) {
584                                 next if (@stuff <= $i || $i < 0);
585                                 $chosen[$i] = $choose;
586                         }
587                 }
588                 last if ($opts->{IMMEDIATE} || $line eq '*');
589         }
590         for ($i = 0; $i < @stuff; $i++) {
591                 if ($chosen[$i]) {
592                         push @return, $stuff[$i];
593                 }
594         }
595         return @return;
596 }
597
598 sub singleton_prompt_help_cmd {
599         print colored $help_color, <<\EOF ;
600 Prompt help:
601 1          - select a numbered item
602 foo        - select item based on unique prefix
603            - (empty) select nothing
604 EOF
605 }
606
607 sub prompt_help_cmd {
608         print colored $help_color, <<\EOF ;
609 Prompt help:
610 1          - select a single item
611 3-5        - select a range of items
612 2-3,6-9    - select multiple ranges
613 foo        - select item based on unique prefix
614 -...       - unselect specified items
615 *          - choose all items
616            - (empty) finish selecting
617 EOF
618 }
619
620 sub status_cmd {
621         list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
622                         list_modified());
623         print "\n";
624 }
625
626 sub say_n_paths {
627         my $did = shift @_;
628         my $cnt = scalar @_;
629         print "$did ";
630         if (1 < $cnt) {
631                 print "$cnt paths\n";
632         }
633         else {
634                 print "one path\n";
635         }
636 }
637
638 sub update_cmd {
639         my @mods = list_modified('file-only');
640         return if (!@mods);
641
642         my @update = list_and_choose({ PROMPT => 'Update',
643                                        HEADER => $status_head, },
644                                      @mods);
645         if (@update) {
646                 system(qw(git update-index --add --remove --),
647                        map { $_->{VALUE} } @update);
648                 say_n_paths('updated', @update);
649         }
650         print "\n";
651 }
652
653 sub revert_cmd {
654         my @update = list_and_choose({ PROMPT => 'Revert',
655                                        HEADER => $status_head, },
656                                      list_modified());
657         if (@update) {
658                 if (is_initial_commit()) {
659                         system(qw(git rm --cached),
660                                 map { $_->{VALUE} } @update);
661                 }
662                 else {
663                         my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
664                                                  map { $_->{VALUE} } @update);
665                         my $fh;
666                         open $fh, '| git update-index --index-info'
667                             or die;
668                         for (@lines) {
669                                 print $fh $_;
670                         }
671                         close($fh);
672                         for (@update) {
673                                 if ($_->{INDEX_ADDDEL} &&
674                                     $_->{INDEX_ADDDEL} eq 'create') {
675                                         system(qw(git update-index --force-remove --),
676                                                $_->{VALUE});
677                                         print "note: $_->{VALUE} is untracked now.\n";
678                                 }
679                         }
680                 }
681                 refresh();
682                 say_n_paths('reverted', @update);
683         }
684         print "\n";
685 }
686
687 sub add_untracked_cmd {
688         my @add = list_and_choose({ PROMPT => 'Add untracked' },
689                                   list_untracked());
690         if (@add) {
691                 system(qw(git update-index --add --), @add);
692                 say_n_paths('added', @add);
693         }
694         print "\n";
695 }
696
697 sub run_git_apply {
698         my $cmd = shift;
699         my $fh;
700         open $fh, '| git ' . $cmd;
701         print $fh @_;
702         return close $fh;
703 }
704
705 sub parse_diff {
706         my ($path) = @_;
707         my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
708         if (defined $patch_mode_revision) {
709                 push @diff_cmd, $patch_mode_revision;
710         }
711         my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
712         my @colored = ();
713         if ($diff_use_color) {
714                 @colored = run_cmd_pipe("git", @diff_cmd, qw(--color --), $path);
715         }
716         my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
717
718         for (my $i = 0; $i < @diff; $i++) {
719                 if ($diff[$i] =~ /^@@ /) {
720                         push @hunk, { TEXT => [], DISPLAY => [],
721                                 TYPE => 'hunk' };
722                 }
723                 push @{$hunk[-1]{TEXT}}, $diff[$i];
724                 push @{$hunk[-1]{DISPLAY}},
725                         ($diff_use_color ? $colored[$i] : $diff[$i]);
726         }
727         return @hunk;
728 }
729
730 sub parse_diff_header {
731         my $src = shift;
732
733         my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
734         my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
735         my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
736
737         for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
738                 my $dest =
739                    $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
740                    $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
741                    $head;
742                 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
743                 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
744         }
745         return ($head, $mode, $deletion);
746 }
747
748 sub hunk_splittable {
749         my ($text) = @_;
750
751         my @s = split_hunk($text);
752         return (1 < @s);
753 }
754
755 sub parse_hunk_header {
756         my ($line) = @_;
757         my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
758             $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
759         $o_cnt = 1 unless defined $o_cnt;
760         $n_cnt = 1 unless defined $n_cnt;
761         return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
762 }
763
764 sub split_hunk {
765         my ($text, $display) = @_;
766         my @split = ();
767         if (!defined $display) {
768                 $display = $text;
769         }
770         # If there are context lines in the middle of a hunk,
771         # it can be split, but we would need to take care of
772         # overlaps later.
773
774         my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
775         my $hunk_start = 1;
776
777       OUTER:
778         while (1) {
779                 my $next_hunk_start = undef;
780                 my $i = $hunk_start - 1;
781                 my $this = +{
782                         TEXT => [],
783                         DISPLAY => [],
784                         TYPE => 'hunk',
785                         OLD => $o_ofs,
786                         NEW => $n_ofs,
787                         OCNT => 0,
788                         NCNT => 0,
789                         ADDDEL => 0,
790                         POSTCTX => 0,
791                         USE => undef,
792                 };
793
794                 while (++$i < @$text) {
795                         my $line = $text->[$i];
796                         my $display = $display->[$i];
797                         if ($line =~ /^ /) {
798                                 if ($this->{ADDDEL} &&
799                                     !defined $next_hunk_start) {
800                                         # We have seen leading context and
801                                         # adds/dels and then here is another
802                                         # context, which is trailing for this
803                                         # split hunk and leading for the next
804                                         # one.
805                                         $next_hunk_start = $i;
806                                 }
807                                 push @{$this->{TEXT}}, $line;
808                                 push @{$this->{DISPLAY}}, $display;
809                                 $this->{OCNT}++;
810                                 $this->{NCNT}++;
811                                 if (defined $next_hunk_start) {
812                                         $this->{POSTCTX}++;
813                                 }
814                                 next;
815                         }
816
817                         # add/del
818                         if (defined $next_hunk_start) {
819                                 # We are done with the current hunk and
820                                 # this is the first real change for the
821                                 # next split one.
822                                 $hunk_start = $next_hunk_start;
823                                 $o_ofs = $this->{OLD} + $this->{OCNT};
824                                 $n_ofs = $this->{NEW} + $this->{NCNT};
825                                 $o_ofs -= $this->{POSTCTX};
826                                 $n_ofs -= $this->{POSTCTX};
827                                 push @split, $this;
828                                 redo OUTER;
829                         }
830                         push @{$this->{TEXT}}, $line;
831                         push @{$this->{DISPLAY}}, $display;
832                         $this->{ADDDEL}++;
833                         if ($line =~ /^-/) {
834                                 $this->{OCNT}++;
835                         }
836                         else {
837                                 $this->{NCNT}++;
838                         }
839                 }
840
841                 push @split, $this;
842                 last;
843         }
844
845         for my $hunk (@split) {
846                 $o_ofs = $hunk->{OLD};
847                 $n_ofs = $hunk->{NEW};
848                 my $o_cnt = $hunk->{OCNT};
849                 my $n_cnt = $hunk->{NCNT};
850
851                 my $head = ("@@ -$o_ofs" .
852                             (($o_cnt != 1) ? ",$o_cnt" : '') .
853                             " +$n_ofs" .
854                             (($n_cnt != 1) ? ",$n_cnt" : '') .
855                             " @@\n");
856                 my $display_head = $head;
857                 unshift @{$hunk->{TEXT}}, $head;
858                 if ($diff_use_color) {
859                         $display_head = colored($fraginfo_color, $head);
860                 }
861                 unshift @{$hunk->{DISPLAY}}, $display_head;
862         }
863         return @split;
864 }
865
866 sub find_last_o_ctx {
867         my ($it) = @_;
868         my $text = $it->{TEXT};
869         my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
870         my $i = @{$text};
871         my $last_o_ctx = $o_ofs + $o_cnt;
872         while (0 < --$i) {
873                 my $line = $text->[$i];
874                 if ($line =~ /^ /) {
875                         $last_o_ctx--;
876                         next;
877                 }
878                 last;
879         }
880         return $last_o_ctx;
881 }
882
883 sub merge_hunk {
884         my ($prev, $this) = @_;
885         my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
886             parse_hunk_header($prev->{TEXT}[0]);
887         my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
888             parse_hunk_header($this->{TEXT}[0]);
889
890         my (@line, $i, $ofs, $o_cnt, $n_cnt);
891         $ofs = $o0_ofs;
892         $o_cnt = $n_cnt = 0;
893         for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
894                 my $line = $prev->{TEXT}[$i];
895                 if ($line =~ /^\+/) {
896                         $n_cnt++;
897                         push @line, $line;
898                         next;
899                 }
900
901                 last if ($o1_ofs <= $ofs);
902
903                 $o_cnt++;
904                 $ofs++;
905                 if ($line =~ /^ /) {
906                         $n_cnt++;
907                 }
908                 push @line, $line;
909         }
910
911         for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
912                 my $line = $this->{TEXT}[$i];
913                 if ($line =~ /^\+/) {
914                         $n_cnt++;
915                         push @line, $line;
916                         next;
917                 }
918                 $ofs++;
919                 $o_cnt++;
920                 if ($line =~ /^ /) {
921                         $n_cnt++;
922                 }
923                 push @line, $line;
924         }
925         my $head = ("@@ -$o0_ofs" .
926                     (($o_cnt != 1) ? ",$o_cnt" : '') .
927                     " +$n0_ofs" .
928                     (($n_cnt != 1) ? ",$n_cnt" : '') .
929                     " @@\n");
930         @{$prev->{TEXT}} = ($head, @line);
931 }
932
933 sub coalesce_overlapping_hunks {
934         my (@in) = @_;
935         my @out = ();
936
937         my ($last_o_ctx, $last_was_dirty);
938
939         for (grep { $_->{USE} } @in) {
940                 if ($_->{TYPE} ne 'hunk') {
941                         push @out, $_;
942                         next;
943                 }
944                 my $text = $_->{TEXT};
945                 my ($o_ofs) = parse_hunk_header($text->[0]);
946                 if (defined $last_o_ctx &&
947                     $o_ofs <= $last_o_ctx &&
948                     !$_->{DIRTY} &&
949                     !$last_was_dirty) {
950                         merge_hunk($out[-1], $_);
951                 }
952                 else {
953                         push @out, $_;
954                 }
955                 $last_o_ctx = find_last_o_ctx($out[-1]);
956                 $last_was_dirty = $_->{DIRTY};
957         }
958         return @out;
959 }
960
961 sub reassemble_patch {
962         my $head = shift;
963         my @patch;
964
965         # Include everything in the header except the beginning of the diff.
966         push @patch, (grep { !/^[-+]{3}/ } @$head);
967
968         # Then include any headers from the hunk lines, which must
969         # come before any actual hunk.
970         while (@_ && $_[0] !~ /^@/) {
971                 push @patch, shift;
972         }
973
974         # Then begin the diff.
975         push @patch, grep { /^[-+]{3}/ } @$head;
976
977         # And then the actual hunks.
978         push @patch, @_;
979
980         return @patch;
981 }
982
983 sub color_diff {
984         return map {
985                 colored((/^@/  ? $fraginfo_color :
986                          /^\+/ ? $diff_new_color :
987                          /^-/  ? $diff_old_color :
988                          $diff_plain_color),
989                         $_);
990         } @_;
991 }
992
993 sub edit_hunk_manually {
994         my ($oldtext) = @_;
995
996         my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
997         my $fh;
998         open $fh, '>', $hunkfile
999                 or die "failed to open hunk edit file for writing: " . $!;
1000         print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n";
1001         print $fh @$oldtext;
1002         my $participle = $patch_mode_flavour{PARTICIPLE};
1003         print $fh <<EOF;
1004 # ---
1005 # To remove '-' lines, make them ' ' lines (context).
1006 # To remove '+' lines, delete them.
1007 # Lines starting with # will be removed.
1008 #
1009 # If the patch applies cleanly, the edited hunk will immediately be
1010 # marked for $participle. If it does not apply cleanly, you will be given
1011 # an opportunity to edit again. If all lines of the hunk are removed,
1012 # then the edit is aborted and the hunk is left unchanged.
1013 EOF
1014         close $fh;
1015
1016         chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1017         system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1018
1019         if ($? != 0) {
1020                 return undef;
1021         }
1022
1023         open $fh, '<', $hunkfile
1024                 or die "failed to open hunk edit file for reading: " . $!;
1025         my @newtext = grep { !/^#/ } <$fh>;
1026         close $fh;
1027         unlink $hunkfile;
1028
1029         # Abort if nothing remains
1030         if (!grep { /\S/ } @newtext) {
1031                 return undef;
1032         }
1033
1034         # Reinsert the first hunk header if the user accidentally deleted it
1035         if ($newtext[0] !~ /^@/) {
1036                 unshift @newtext, $oldtext->[0];
1037         }
1038         return \@newtext;
1039 }
1040
1041 sub diff_applies {
1042         my $fh;
1043         return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --recount --check',
1044                              map { @{$_->{TEXT}} } @_);
1045 }
1046
1047 sub _restore_terminal_and_die {
1048         ReadMode 'restore';
1049         print "\n";
1050         exit 1;
1051 }
1052
1053 sub prompt_single_character {
1054         if ($use_readkey) {
1055                 local $SIG{TERM} = \&_restore_terminal_and_die;
1056                 local $SIG{INT} = \&_restore_terminal_and_die;
1057                 ReadMode 'cbreak';
1058                 my $key = ReadKey 0;
1059                 ReadMode 'restore';
1060                 print "$key" if defined $key;
1061                 print "\n";
1062                 return $key;
1063         } else {
1064                 return <STDIN>;
1065         }
1066 }
1067
1068 sub prompt_yesno {
1069         my ($prompt) = @_;
1070         while (1) {
1071                 print colored $prompt_color, $prompt;
1072                 my $line = prompt_single_character;
1073                 return 0 if $line =~ /^n/i;
1074                 return 1 if $line =~ /^y/i;
1075         }
1076 }
1077
1078 sub edit_hunk_loop {
1079         my ($head, $hunk, $ix) = @_;
1080         my $text = $hunk->[$ix]->{TEXT};
1081
1082         while (1) {
1083                 $text = edit_hunk_manually($text);
1084                 if (!defined $text) {
1085                         return undef;
1086                 }
1087                 my $newhunk = {
1088                         TEXT => $text,
1089                         TYPE => $hunk->[$ix]->{TYPE},
1090                         USE => 1,
1091                         DIRTY => 1,
1092                 };
1093                 if (diff_applies($head,
1094                                  @{$hunk}[0..$ix-1],
1095                                  $newhunk,
1096                                  @{$hunk}[$ix+1..$#{$hunk}])) {
1097                         $newhunk->{DISPLAY} = [color_diff(@{$text})];
1098                         return $newhunk;
1099                 }
1100                 else {
1101                         prompt_yesno(
1102                                 'Your edited hunk does not apply. Edit again '
1103                                 . '(saying "no" discards!) [y/n]? '
1104                                 ) or return undef;
1105                 }
1106         }
1107 }
1108
1109 sub help_patch_cmd {
1110         my $verb = lc $patch_mode_flavour{VERB};
1111         my $target = $patch_mode_flavour{TARGET};
1112         print colored $help_color, <<EOF ;
1113 y - $verb this hunk$target
1114 n - do not $verb this hunk$target
1115 q - quit; do not $verb this hunk nor any of the remaining ones
1116 a - $verb this hunk and all later hunks in the file
1117 d - do not $verb this hunk nor any of the later hunks in the file
1118 g - select a hunk to go to
1119 / - search for a hunk matching the given regex
1120 j - leave this hunk undecided, see next undecided hunk
1121 J - leave this hunk undecided, see next hunk
1122 k - leave this hunk undecided, see previous undecided hunk
1123 K - leave this hunk undecided, see previous hunk
1124 s - split the current hunk into smaller hunks
1125 e - manually edit the current hunk
1126 ? - print help
1127 EOF
1128 }
1129
1130 sub apply_patch {
1131         my $cmd = shift;
1132         my $ret = run_git_apply $cmd . ' --recount', @_;
1133         if (!$ret) {
1134                 print STDERR @_;
1135         }
1136         return $ret;
1137 }
1138
1139 sub apply_patch_for_checkout_commit {
1140         my $reverse = shift;
1141         my $applies_index = run_git_apply 'apply '.$reverse.' --cached --recount --check', @_;
1142         my $applies_worktree = run_git_apply 'apply '.$reverse.' --recount --check', @_;
1143
1144         if ($applies_worktree && $applies_index) {
1145                 run_git_apply 'apply '.$reverse.' --cached --recount', @_;
1146                 run_git_apply 'apply '.$reverse.' --recount', @_;
1147                 return 1;
1148         } elsif (!$applies_index) {
1149                 print colored $error_color, "The selected hunks do not apply to the index!\n";
1150                 if (prompt_yesno "Apply them to the worktree anyway? ") {
1151                         return run_git_apply 'apply '.$reverse.' --recount', @_;
1152                 } else {
1153                         print colored $error_color, "Nothing was applied.\n";
1154                         return 0;
1155                 }
1156         } else {
1157                 print STDERR @_;
1158                 return 0;
1159         }
1160 }
1161
1162 sub patch_update_cmd {
1163         my @all_mods = list_modified($patch_mode_flavour{FILTER});
1164         my @mods = grep { !($_->{BINARY}) } @all_mods;
1165         my @them;
1166
1167         if (!@mods) {
1168                 if (@all_mods) {
1169                         print STDERR "Only binary files changed.\n";
1170                 } else {
1171                         print STDERR "No changes.\n";
1172                 }
1173                 return 0;
1174         }
1175         if ($patch_mode) {
1176                 @them = @mods;
1177         }
1178         else {
1179                 @them = list_and_choose({ PROMPT => 'Patch update',
1180                                           HEADER => $status_head, },
1181                                         @mods);
1182         }
1183         for (@them) {
1184                 return 0 if patch_update_file($_->{VALUE});
1185         }
1186 }
1187
1188 # Generate a one line summary of a hunk.
1189 sub summarize_hunk {
1190         my $rhunk = shift;
1191         my $summary = $rhunk->{TEXT}[0];
1192
1193         # Keep the line numbers, discard extra context.
1194         $summary =~ s/@@(.*?)@@.*/$1 /s;
1195         $summary .= " " x (20 - length $summary);
1196
1197         # Add some user context.
1198         for my $line (@{$rhunk->{TEXT}}) {
1199                 if ($line =~ m/^[+-].*\w/) {
1200                         $summary .= $line;
1201                         last;
1202                 }
1203         }
1204
1205         chomp $summary;
1206         return substr($summary, 0, 80) . "\n";
1207 }
1208
1209
1210 # Print a one-line summary of each hunk in the array ref in
1211 # the first argument, starting wih the index in the 2nd.
1212 sub display_hunks {
1213         my ($hunks, $i) = @_;
1214         my $ctr = 0;
1215         $i ||= 0;
1216         for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1217                 my $status = " ";
1218                 if (defined $hunks->[$i]{USE}) {
1219                         $status = $hunks->[$i]{USE} ? "+" : "-";
1220                 }
1221                 printf "%s%2d: %s",
1222                         $status,
1223                         $i + 1,
1224                         summarize_hunk($hunks->[$i]);
1225         }
1226         return $i;
1227 }
1228
1229 sub patch_update_file {
1230         my $quit = 0;
1231         my ($ix, $num);
1232         my $path = shift;
1233         my ($head, @hunk) = parse_diff($path);
1234         ($head, my $mode, my $deletion) = parse_diff_header($head);
1235         for (@{$head->{DISPLAY}}) {
1236                 print;
1237         }
1238
1239         if (@{$mode->{TEXT}}) {
1240                 unshift @hunk, $mode;
1241         }
1242         if (@{$deletion->{TEXT}}) {
1243                 foreach my $hunk (@hunk) {
1244                         push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1245                         push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1246                 }
1247                 @hunk = ($deletion);
1248         }
1249
1250         $num = scalar @hunk;
1251         $ix = 0;
1252
1253         while (1) {
1254                 my ($prev, $next, $other, $undecided, $i);
1255                 $other = '';
1256
1257                 if ($num <= $ix) {
1258                         $ix = 0;
1259                 }
1260                 for ($i = 0; $i < $ix; $i++) {
1261                         if (!defined $hunk[$i]{USE}) {
1262                                 $prev = 1;
1263                                 $other .= ',k';
1264                                 last;
1265                         }
1266                 }
1267                 if ($ix) {
1268                         $other .= ',K';
1269                 }
1270                 for ($i = $ix + 1; $i < $num; $i++) {
1271                         if (!defined $hunk[$i]{USE}) {
1272                                 $next = 1;
1273                                 $other .= ',j';
1274                                 last;
1275                         }
1276                 }
1277                 if ($ix < $num - 1) {
1278                         $other .= ',J';
1279                 }
1280                 if ($num > 1) {
1281                         $other .= ',g';
1282                 }
1283                 for ($i = 0; $i < $num; $i++) {
1284                         if (!defined $hunk[$i]{USE}) {
1285                                 $undecided = 1;
1286                                 last;
1287                         }
1288                 }
1289                 last if (!$undecided);
1290
1291                 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1292                     hunk_splittable($hunk[$ix]{TEXT})) {
1293                         $other .= ',s';
1294                 }
1295                 if ($hunk[$ix]{TYPE} eq 'hunk') {
1296                         $other .= ',e';
1297                 }
1298                 for (@{$hunk[$ix]{DISPLAY}}) {
1299                         print;
1300                 }
1301                 print colored $prompt_color, $patch_mode_flavour{VERB},
1302                   ($hunk[$ix]{TYPE} eq 'mode' ? ' mode change' :
1303                    $hunk[$ix]{TYPE} eq 'deletion' ? ' deletion' :
1304                    ' this hunk'),
1305                   $patch_mode_flavour{TARGET},
1306                   " [y,n,q,a,d,/$other,?]? ";
1307                 my $line = prompt_single_character;
1308                 if ($line) {
1309                         if ($line =~ /^y/i) {
1310                                 $hunk[$ix]{USE} = 1;
1311                         }
1312                         elsif ($line =~ /^n/i) {
1313                                 $hunk[$ix]{USE} = 0;
1314                         }
1315                         elsif ($line =~ /^a/i) {
1316                                 while ($ix < $num) {
1317                                         if (!defined $hunk[$ix]{USE}) {
1318                                                 $hunk[$ix]{USE} = 1;
1319                                         }
1320                                         $ix++;
1321                                 }
1322                                 next;
1323                         }
1324                         elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1325                                 my $response = $1;
1326                                 my $no = $ix > 10 ? $ix - 10 : 0;
1327                                 while ($response eq '') {
1328                                         my $extra = "";
1329                                         $no = display_hunks(\@hunk, $no);
1330                                         if ($no < $num) {
1331                                                 $extra = " (<ret> to see more)";
1332                                         }
1333                                         print "go to which hunk$extra? ";
1334                                         $response = <STDIN>;
1335                                         if (!defined $response) {
1336                                                 $response = '';
1337                                         }
1338                                         chomp $response;
1339                                 }
1340                                 if ($response !~ /^\s*\d+\s*$/) {
1341                                         error_msg "Invalid number: '$response'\n";
1342                                 } elsif (0 < $response && $response <= $num) {
1343                                         $ix = $response - 1;
1344                                 } else {
1345                                         error_msg "Sorry, only $num hunks available.\n";
1346                                 }
1347                                 next;
1348                         }
1349                         elsif ($line =~ /^d/i) {
1350                                 while ($ix < $num) {
1351                                         if (!defined $hunk[$ix]{USE}) {
1352                                                 $hunk[$ix]{USE} = 0;
1353                                         }
1354                                         $ix++;
1355                                 }
1356                                 next;
1357                         }
1358                         elsif ($line =~ /^q/i) {
1359                                 while ($ix < $num) {
1360                                         if (!defined $hunk[$ix]{USE}) {
1361                                                 $hunk[$ix]{USE} = 0;
1362                                         }
1363                                         $ix++;
1364                                 }
1365                                 $quit = 1;
1366                                 next;
1367                         }
1368                         elsif ($line =~ m|^/(.*)|) {
1369                                 my $regex = $1;
1370                                 if ($1 eq "") {
1371                                         print colored $prompt_color, "search for regex? ";
1372                                         $regex = <STDIN>;
1373                                         if (defined $regex) {
1374                                                 chomp $regex;
1375                                         }
1376                                 }
1377                                 my $search_string;
1378                                 eval {
1379                                         $search_string = qr{$regex}m;
1380                                 };
1381                                 if ($@) {
1382                                         my ($err,$exp) = ($@, $1);
1383                                         $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1384                                         error_msg "Malformed search regexp $exp: $err\n";
1385                                         next;
1386                                 }
1387                                 my $iy = $ix;
1388                                 while (1) {
1389                                         my $text = join ("", @{$hunk[$iy]{TEXT}});
1390                                         last if ($text =~ $search_string);
1391                                         $iy++;
1392                                         $iy = 0 if ($iy >= $num);
1393                                         if ($ix == $iy) {
1394                                                 error_msg "No hunk matches the given pattern\n";
1395                                                 last;
1396                                         }
1397                                 }
1398                                 $ix = $iy;
1399                                 next;
1400                         }
1401                         elsif ($line =~ /^K/) {
1402                                 if ($other =~ /K/) {
1403                                         $ix--;
1404                                 }
1405                                 else {
1406                                         error_msg "No previous hunk\n";
1407                                 }
1408                                 next;
1409                         }
1410                         elsif ($line =~ /^J/) {
1411                                 if ($other =~ /J/) {
1412                                         $ix++;
1413                                 }
1414                                 else {
1415                                         error_msg "No next hunk\n";
1416                                 }
1417                                 next;
1418                         }
1419                         elsif ($line =~ /^k/) {
1420                                 if ($other =~ /k/) {
1421                                         while (1) {
1422                                                 $ix--;
1423                                                 last if (!$ix ||
1424                                                          !defined $hunk[$ix]{USE});
1425                                         }
1426                                 }
1427                                 else {
1428                                         error_msg "No previous hunk\n";
1429                                 }
1430                                 next;
1431                         }
1432                         elsif ($line =~ /^j/) {
1433                                 if ($other !~ /j/) {
1434                                         error_msg "No next hunk\n";
1435                                         next;
1436                                 }
1437                         }
1438                         elsif ($other =~ /s/ && $line =~ /^s/) {
1439                                 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1440                                 if (1 < @split) {
1441                                         print colored $header_color, "Split into ",
1442                                         scalar(@split), " hunks.\n";
1443                                 }
1444                                 splice (@hunk, $ix, 1, @split);
1445                                 $num = scalar @hunk;
1446                                 next;
1447                         }
1448                         elsif ($other =~ /e/ && $line =~ /^e/) {
1449                                 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1450                                 if (defined $newhunk) {
1451                                         splice @hunk, $ix, 1, $newhunk;
1452                                 }
1453                         }
1454                         else {
1455                                 help_patch_cmd($other);
1456                                 next;
1457                         }
1458                         # soft increment
1459                         while (1) {
1460                                 $ix++;
1461                                 last if ($ix >= $num ||
1462                                          !defined $hunk[$ix]{USE});
1463                         }
1464                 }
1465         }
1466
1467         @hunk = coalesce_overlapping_hunks(@hunk);
1468
1469         my $n_lofs = 0;
1470         my @result = ();
1471         for (@hunk) {
1472                 if ($_->{USE}) {
1473                         push @result, @{$_->{TEXT}};
1474                 }
1475         }
1476
1477         if (@result) {
1478                 my $fh;
1479                 my @patch = reassemble_patch($head->{TEXT}, @result);
1480                 my $apply_routine = $patch_mode_flavour{APPLY};
1481                 &$apply_routine(@patch);
1482                 refresh();
1483         }
1484
1485         print "\n";
1486         return $quit;
1487 }
1488
1489 sub diff_cmd {
1490         my @mods = list_modified('index-only');
1491         @mods = grep { !($_->{BINARY}) } @mods;
1492         return if (!@mods);
1493         my (@them) = list_and_choose({ PROMPT => 'Review diff',
1494                                      IMMEDIATE => 1,
1495                                      HEADER => $status_head, },
1496                                    @mods);
1497         return if (!@them);
1498         my $reference = is_initial_commit() ? get_empty_tree() : 'HEAD';
1499         system(qw(git diff -p --cached), $reference, '--',
1500                 map { $_->{VALUE} } @them);
1501 }
1502
1503 sub quit_cmd {
1504         print "Bye.\n";
1505         exit(0);
1506 }
1507
1508 sub help_cmd {
1509         print colored $help_color, <<\EOF ;
1510 status        - show paths with changes
1511 update        - add working tree state to the staged set of changes
1512 revert        - revert staged set of changes back to the HEAD version
1513 patch         - pick hunks and update selectively
1514 diff          - view diff between HEAD and index
1515 add untracked - add contents of untracked files to the staged set of changes
1516 EOF
1517 }
1518
1519 sub process_args {
1520         return unless @ARGV;
1521         my $arg = shift @ARGV;
1522         if ($arg =~ /--patch(?:=(.*))?/) {
1523                 if (defined $1) {
1524                         if ($1 eq 'reset') {
1525                                 $patch_mode = 'reset_head';
1526                                 $patch_mode_revision = 'HEAD';
1527                                 $arg = shift @ARGV or die "missing --";
1528                                 if ($arg ne '--') {
1529                                         $patch_mode_revision = $arg;
1530                                         $patch_mode = ($arg eq 'HEAD' ?
1531                                                        'reset_head' : 'reset_nothead');
1532                                         $arg = shift @ARGV or die "missing --";
1533                                 }
1534                         } elsif ($1 eq 'checkout') {
1535                                 $arg = shift @ARGV or die "missing --";
1536                                 if ($arg eq '--') {
1537                                         $patch_mode = 'checkout_index';
1538                                 } else {
1539                                         $patch_mode_revision = $arg;
1540                                         $patch_mode = ($arg eq 'HEAD' ?
1541                                                        'checkout_head' : 'checkout_nothead');
1542                                         $arg = shift @ARGV or die "missing --";
1543                                 }
1544                         } elsif ($1 eq 'stage' or $1 eq 'stash') {
1545                                 $patch_mode = $1;
1546                                 $arg = shift @ARGV or die "missing --";
1547                         } else {
1548                                 die "unknown --patch mode: $1";
1549                         }
1550                 } else {
1551                         $patch_mode = 'stage';
1552                         $arg = shift @ARGV or die "missing --";
1553                 }
1554                 die "invalid argument $arg, expecting --"
1555                     unless $arg eq "--";
1556                 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1557         }
1558         elsif ($arg ne "--") {
1559                 die "invalid argument $arg, expecting --";
1560         }
1561 }
1562
1563 sub main_loop {
1564         my @cmd = ([ 'status', \&status_cmd, ],
1565                    [ 'update', \&update_cmd, ],
1566                    [ 'revert', \&revert_cmd, ],
1567                    [ 'add untracked', \&add_untracked_cmd, ],
1568                    [ 'patch', \&patch_update_cmd, ],
1569                    [ 'diff', \&diff_cmd, ],
1570                    [ 'quit', \&quit_cmd, ],
1571                    [ 'help', \&help_cmd, ],
1572         );
1573         while (1) {
1574                 my ($it) = list_and_choose({ PROMPT => 'What now',
1575                                              SINGLETON => 1,
1576                                              LIST_FLAT => 4,
1577                                              HEADER => '*** Commands ***',
1578                                              ON_EOF => \&quit_cmd,
1579                                              IMMEDIATE => 1 }, @cmd);
1580                 if ($it) {
1581                         eval {
1582                                 $it->[1]->();
1583                         };
1584                         if ($@) {
1585                                 print "$@";
1586                         }
1587                 }
1588         }
1589 }
1590
1591 process_args();
1592 refresh();
1593 if ($patch_mode) {
1594         patch_update_cmd();
1595 }
1596 else {
1597         status_cmd();
1598         main_loop();
1599 }