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