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