Code

bundle, fast-import: detect write failure
[git.git] / git-add--interactive.perl
1 #!/usr/bin/perl -w
3 use strict;
4 use Git;
6 my $repo = Git->repository();
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         ) : ();
16 my $diff_use_color = $repo->get_colorbool('color.diff');
17 my ($fraginfo_color) =
18         $diff_use_color ? (
19                 $repo->get_color('color.diff.frag', 'cyan'),
20         ) : ();
22 my $normal_color = $repo->get_color("", "reset");
24 sub colored {
25         my $color = shift;
26         my $string = join("", @_);
28         if (defined $color) {
29                 # Put a color code at the beginning of each line, a reset at the end
30                 # color after newlines that are not at the end of the string
31                 $string =~ s/(\n+)(.)/$1$color$2/g;
32                 # reset before newlines
33                 $string =~ s/(\n+)/$normal_color$1/g;
34                 # codes at beginning and end (if necessary):
35                 $string =~ s/^/$color/;
36                 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
37         }
38         return $string;
39 }
41 # command line options
42 my $patch_mode;
44 sub run_cmd_pipe {
45         if ($^O eq 'MSWin32') {
46                 my @invalid = grep {m/[":*]/} @_;
47                 die "$^O does not support: @invalid\n" if @invalid;
48                 my @args = map { m/ /o ? "\"$_\"": $_ } @_;
49                 return qx{@args};
50         } else {
51                 my $fh = undef;
52                 open($fh, '-|', @_) or die;
53                 return <$fh>;
54         }
55 }
57 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
59 if (!defined $GIT_DIR) {
60         exit(1); # rev-parse would have already said "not a git repo"
61 }
62 chomp($GIT_DIR);
64 sub refresh {
65         my $fh;
66         open $fh, 'git update-index --refresh |'
67             or die;
68         while (<$fh>) {
69                 ;# ignore 'needs update'
70         }
71         close $fh;
72 }
74 sub list_untracked {
75         map {
76                 chomp $_;
77                 $_;
78         }
79         run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
80 }
82 my $status_fmt = '%12s %12s %s';
83 my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path');
85 # Returns list of hashes, contents of each of which are:
86 # VALUE:        pathname
87 # BINARY:       is a binary path
88 # INDEX:        is index different from HEAD?
89 # FILE:         is file different from index?
90 # INDEX_ADDDEL: is it add/delete between HEAD and index?
91 # FILE_ADDDEL:  is it add/delete between index and file?
93 sub list_modified {
94         my ($only) = @_;
95         my (%data, @return);
96         my ($add, $del, $adddel, $file);
97         my @tracked = ();
99         if (@ARGV) {
100                 @tracked = map {
101                         chomp $_; $_;
102                 } run_cmd_pipe(qw(git ls-files --exclude-standard --), @ARGV);
103                 return if (!@tracked);
104         }
106         for (run_cmd_pipe(qw(git diff-index --cached
107                              --numstat --summary HEAD --), @tracked)) {
108                 if (($add, $del, $file) =
109                     /^([-\d]+)  ([-\d]+)        (.*)/) {
110                         my ($change, $bin);
111                         if ($add eq '-' && $del eq '-') {
112                                 $change = 'binary';
113                                 $bin = 1;
114                         }
115                         else {
116                                 $change = "+$add/-$del";
117                         }
118                         $data{$file} = {
119                                 INDEX => $change,
120                                 BINARY => $bin,
121                                 FILE => 'nothing',
122                         }
123                 }
124                 elsif (($adddel, $file) =
125                        /^ (create|delete) mode [0-7]+ (.*)$/) {
126                         $data{$file}{INDEX_ADDDEL} = $adddel;
127                 }
128         }
130         for (run_cmd_pipe(qw(git diff-files --numstat --summary --), @tracked)) {
131                 if (($add, $del, $file) =
132                     /^([-\d]+)  ([-\d]+)        (.*)/) {
133                         if (!exists $data{$file}) {
134                                 $data{$file} = +{
135                                         INDEX => 'unchanged',
136                                         BINARY => 0,
137                                 };
138                         }
139                         my ($change, $bin);
140                         if ($add eq '-' && $del eq '-') {
141                                 $change = 'binary';
142                                 $bin = 1;
143                         }
144                         else {
145                                 $change = "+$add/-$del";
146                         }
147                         $data{$file}{FILE} = $change;
148                         if ($bin) {
149                                 $data{$file}{BINARY} = 1;
150                         }
151                 }
152                 elsif (($adddel, $file) =
153                        /^ (create|delete) mode [0-7]+ (.*)$/) {
154                         $data{$file}{FILE_ADDDEL} = $adddel;
155                 }
156         }
158         for (sort keys %data) {
159                 my $it = $data{$_};
161                 if ($only) {
162                         if ($only eq 'index-only') {
163                                 next if ($it->{INDEX} eq 'unchanged');
164                         }
165                         if ($only eq 'file-only') {
166                                 next if ($it->{FILE} eq 'nothing');
167                         }
168                 }
169                 push @return, +{
170                         VALUE => $_,
171                         %$it,
172                 };
173         }
174         return @return;
177 sub find_unique {
178         my ($string, @stuff) = @_;
179         my $found = undef;
180         for (my $i = 0; $i < @stuff; $i++) {
181                 my $it = $stuff[$i];
182                 my $hit = undef;
183                 if (ref $it) {
184                         if ((ref $it) eq 'ARRAY') {
185                                 $it = $it->[0];
186                         }
187                         else {
188                                 $it = $it->{VALUE};
189                         }
190                 }
191                 eval {
192                         if ($it =~ /^$string/) {
193                                 $hit = 1;
194                         };
195                 };
196                 if (defined $hit && defined $found) {
197                         return undef;
198                 }
199                 if ($hit) {
200                         $found = $i + 1;
201                 }
202         }
203         return $found;
206 # inserts string into trie and updates count for each character
207 sub update_trie {
208         my ($trie, $string) = @_;
209         foreach (split //, $string) {
210                 $trie = $trie->{$_} ||= {COUNT => 0};
211                 $trie->{COUNT}++;
212         }
215 # returns an array of tuples (prefix, remainder)
216 sub find_unique_prefixes {
217         my @stuff = @_;
218         my @return = ();
220         # any single prefix exceeding the soft limit is omitted
221         # if any prefix exceeds the hard limit all are omitted
222         # 0 indicates no limit
223         my $soft_limit = 0;
224         my $hard_limit = 3;
226         # build a trie modelling all possible options
227         my %trie;
228         foreach my $print (@stuff) {
229                 if ((ref $print) eq 'ARRAY') {
230                         $print = $print->[0];
231                 }
232                 elsif ((ref $print) eq 'HASH') {
233                         $print = $print->{VALUE};
234                 }
235                 update_trie(\%trie, $print);
236                 push @return, $print;
237         }
239         # use the trie to find the unique prefixes
240         for (my $i = 0; $i < @return; $i++) {
241                 my $ret = $return[$i];
242                 my @letters = split //, $ret;
243                 my %search = %trie;
244                 my ($prefix, $remainder);
245                 my $j;
246                 for ($j = 0; $j < @letters; $j++) {
247                         my $letter = $letters[$j];
248                         if ($search{$letter}{COUNT} == 1) {
249                                 $prefix = substr $ret, 0, $j + 1;
250                                 $remainder = substr $ret, $j + 1;
251                                 last;
252                         }
253                         else {
254                                 my $prefix = substr $ret, 0, $j;
255                                 return ()
256                                     if ($hard_limit && $j + 1 > $hard_limit);
257                         }
258                         %search = %{$search{$letter}};
259                 }
260                 if ($soft_limit && $j + 1 > $soft_limit) {
261                         $prefix = undef;
262                         $remainder = $ret;
263                 }
264                 $return[$i] = [$prefix, $remainder];
265         }
266         return @return;
269 # filters out prefixes which have special meaning to list_and_choose()
270 sub is_valid_prefix {
271         my $prefix = shift;
272         return (defined $prefix) &&
273             !($prefix =~ /[\s,]/) && # separators
274             !($prefix =~ /^-/) &&    # deselection
275             !($prefix =~ /^\d+/) &&  # selection
276             ($prefix ne '*') &&      # "all" wildcard
277             ($prefix ne '?');        # prompt help
280 # given a prefix/remainder tuple return a string with the prefix highlighted
281 # for now use square brackets; later might use ANSI colors (underline, bold)
282 sub highlight_prefix {
283         my $prefix = shift;
284         my $remainder = shift;
286         if (!defined $prefix) {
287                 return $remainder;
288         }
290         if (!is_valid_prefix($prefix)) {
291                 return "$prefix$remainder";
292         }
294         if (!$menu_use_color) {
295                 return "[$prefix]$remainder";
296         }
298         return "$prompt_color$prefix$normal_color$remainder";
301 sub list_and_choose {
302         my ($opts, @stuff) = @_;
303         my (@chosen, @return);
304         my $i;
305         my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
307       TOPLOOP:
308         while (1) {
309                 my $last_lf = 0;
311                 if ($opts->{HEADER}) {
312                         if (!$opts->{LIST_FLAT}) {
313                                 print "     ";
314                         }
315                         print colored $header_color, "$opts->{HEADER}\n";
316                 }
317                 for ($i = 0; $i < @stuff; $i++) {
318                         my $chosen = $chosen[$i] ? '*' : ' ';
319                         my $print = $stuff[$i];
320                         my $ref = ref $print;
321                         my $highlighted = highlight_prefix(@{$prefixes[$i]})
322                             if @prefixes;
323                         if ($ref eq 'ARRAY') {
324                                 $print = $highlighted || $print->[0];
325                         }
326                         elsif ($ref eq 'HASH') {
327                                 my $value = $highlighted || $print->{VALUE};
328                                 $print = sprintf($status_fmt,
329                                     $print->{INDEX},
330                                     $print->{FILE},
331                                     $value);
332                         }
333                         else {
334                                 $print = $highlighted || $print;
335                         }
336                         printf("%s%2d: %s", $chosen, $i+1, $print);
337                         if (($opts->{LIST_FLAT}) &&
338                             (($i + 1) % ($opts->{LIST_FLAT}))) {
339                                 print "\t";
340                                 $last_lf = 0;
341                         }
342                         else {
343                                 print "\n";
344                                 $last_lf = 1;
345                         }
346                 }
347                 if (!$last_lf) {
348                         print "\n";
349                 }
351                 return if ($opts->{LIST_ONLY});
353                 print colored $prompt_color, $opts->{PROMPT};
354                 if ($opts->{SINGLETON}) {
355                         print "> ";
356                 }
357                 else {
358                         print ">> ";
359                 }
360                 my $line = <STDIN>;
361                 if (!$line) {
362                         print "\n";
363                         $opts->{ON_EOF}->() if $opts->{ON_EOF};
364                         last;
365                 }
366                 chomp $line;
367                 last if $line eq '';
368                 if ($line eq '?') {
369                         $opts->{SINGLETON} ?
370                             singleton_prompt_help_cmd() :
371                             prompt_help_cmd();
372                         next TOPLOOP;
373                 }
374                 for my $choice (split(/[\s,]+/, $line)) {
375                         my $choose = 1;
376                         my ($bottom, $top);
378                         # Input that begins with '-'; unchoose
379                         if ($choice =~ s/^-//) {
380                                 $choose = 0;
381                         }
382                         # A range can be specified like 5-7
383                         if ($choice =~ /^(\d+)-(\d+)$/) {
384                                 ($bottom, $top) = ($1, $2);
385                         }
386                         elsif ($choice =~ /^\d+$/) {
387                                 $bottom = $top = $choice;
388                         }
389                         elsif ($choice eq '*') {
390                                 $bottom = 1;
391                                 $top = 1 + @stuff;
392                         }
393                         else {
394                                 $bottom = $top = find_unique($choice, @stuff);
395                                 if (!defined $bottom) {
396                                         print "Huh ($choice)?\n";
397                                         next TOPLOOP;
398                                 }
399                         }
400                         if ($opts->{SINGLETON} && $bottom != $top) {
401                                 print "Huh ($choice)?\n";
402                                 next TOPLOOP;
403                         }
404                         for ($i = $bottom-1; $i <= $top-1; $i++) {
405                                 next if (@stuff <= $i || $i < 0);
406                                 $chosen[$i] = $choose;
407                         }
408                 }
409                 last if ($opts->{IMMEDIATE} || $line eq '*');
410         }
411         for ($i = 0; $i < @stuff; $i++) {
412                 if ($chosen[$i]) {
413                         push @return, $stuff[$i];
414                 }
415         }
416         return @return;
419 sub singleton_prompt_help_cmd {
420         print colored $help_color, <<\EOF ;
421 Prompt help:
422 1          - select a numbered item
423 foo        - select item based on unique prefix
424            - (empty) select nothing
425 EOF
428 sub prompt_help_cmd {
429         print colored $help_color, <<\EOF ;
430 Prompt help:
431 1          - select a single item
432 3-5        - select a range of items
433 2-3,6-9    - select multiple ranges
434 foo        - select item based on unique prefix
435 -...       - unselect specified items
436 *          - choose all items
437            - (empty) finish selecting
438 EOF
441 sub status_cmd {
442         list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
443                         list_modified());
444         print "\n";
447 sub say_n_paths {
448         my $did = shift @_;
449         my $cnt = scalar @_;
450         print "$did ";
451         if (1 < $cnt) {
452                 print "$cnt paths\n";
453         }
454         else {
455                 print "one path\n";
456         }
459 sub update_cmd {
460         my @mods = list_modified('file-only');
461         return if (!@mods);
463         my @update = list_and_choose({ PROMPT => 'Update',
464                                        HEADER => $status_head, },
465                                      @mods);
466         if (@update) {
467                 system(qw(git update-index --add --remove --),
468                        map { $_->{VALUE} } @update);
469                 say_n_paths('updated', @update);
470         }
471         print "\n";
474 sub revert_cmd {
475         my @update = list_and_choose({ PROMPT => 'Revert',
476                                        HEADER => $status_head, },
477                                      list_modified());
478         if (@update) {
479                 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
480                                          map { $_->{VALUE} } @update);
481                 my $fh;
482                 open $fh, '| git update-index --index-info'
483                     or die;
484                 for (@lines) {
485                         print $fh $_;
486                 }
487                 close($fh);
488                 for (@update) {
489                         if ($_->{INDEX_ADDDEL} &&
490                             $_->{INDEX_ADDDEL} eq 'create') {
491                                 system(qw(git update-index --force-remove --),
492                                        $_->{VALUE});
493                                 print "note: $_->{VALUE} is untracked now.\n";
494                         }
495                 }
496                 refresh();
497                 say_n_paths('reverted', @update);
498         }
499         print "\n";
502 sub add_untracked_cmd {
503         my @add = list_and_choose({ PROMPT => 'Add untracked' },
504                                   list_untracked());
505         if (@add) {
506                 system(qw(git update-index --add --), @add);
507                 say_n_paths('added', @add);
508         }
509         print "\n";
512 sub parse_diff {
513         my ($path) = @_;
514         my @diff = run_cmd_pipe(qw(git diff-files -p --), $path);
515         my @colored = ();
516         if ($diff_use_color) {
517                 @colored = run_cmd_pipe(qw(git diff-files -p --color --), $path);
518         }
519         my (@hunk) = { TEXT => [], DISPLAY => [] };
521         for (my $i = 0; $i < @diff; $i++) {
522                 if ($diff[$i] =~ /^@@ /) {
523                         push @hunk, { TEXT => [], DISPLAY => [] };
524                 }
525                 push @{$hunk[-1]{TEXT}}, $diff[$i];
526                 push @{$hunk[-1]{DISPLAY}},
527                         ($diff_use_color ? $colored[$i] : $diff[$i]);
528         }
529         return @hunk;
532 sub hunk_splittable {
533         my ($text) = @_;
535         my @s = split_hunk($text);
536         return (1 < @s);
539 sub parse_hunk_header {
540         my ($line) = @_;
541         my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
542             $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
543         $o_cnt = 1 unless defined $o_cnt;
544         $n_cnt = 1 unless defined $n_cnt;
545         return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
548 sub split_hunk {
549         my ($text, $display) = @_;
550         my @split = ();
551         if (!defined $display) {
552                 $display = $text;
553         }
554         # If there are context lines in the middle of a hunk,
555         # it can be split, but we would need to take care of
556         # overlaps later.
558         my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
559         my $hunk_start = 1;
561       OUTER:
562         while (1) {
563                 my $next_hunk_start = undef;
564                 my $i = $hunk_start - 1;
565                 my $this = +{
566                         TEXT => [],
567                         DISPLAY => [],
568                         OLD => $o_ofs,
569                         NEW => $n_ofs,
570                         OCNT => 0,
571                         NCNT => 0,
572                         ADDDEL => 0,
573                         POSTCTX => 0,
574                         USE => undef,
575                 };
577                 while (++$i < @$text) {
578                         my $line = $text->[$i];
579                         my $display = $display->[$i];
580                         if ($line =~ /^ /) {
581                                 if ($this->{ADDDEL} &&
582                                     !defined $next_hunk_start) {
583                                         # We have seen leading context and
584                                         # adds/dels and then here is another
585                                         # context, which is trailing for this
586                                         # split hunk and leading for the next
587                                         # one.
588                                         $next_hunk_start = $i;
589                                 }
590                                 push @{$this->{TEXT}}, $line;
591                                 push @{$this->{DISPLAY}}, $display;
592                                 $this->{OCNT}++;
593                                 $this->{NCNT}++;
594                                 if (defined $next_hunk_start) {
595                                         $this->{POSTCTX}++;
596                                 }
597                                 next;
598                         }
600                         # add/del
601                         if (defined $next_hunk_start) {
602                                 # We are done with the current hunk and
603                                 # this is the first real change for the
604                                 # next split one.
605                                 $hunk_start = $next_hunk_start;
606                                 $o_ofs = $this->{OLD} + $this->{OCNT};
607                                 $n_ofs = $this->{NEW} + $this->{NCNT};
608                                 $o_ofs -= $this->{POSTCTX};
609                                 $n_ofs -= $this->{POSTCTX};
610                                 push @split, $this;
611                                 redo OUTER;
612                         }
613                         push @{$this->{TEXT}}, $line;
614                         push @{$this->{DISPLAY}}, $display;
615                         $this->{ADDDEL}++;
616                         if ($line =~ /^-/) {
617                                 $this->{OCNT}++;
618                         }
619                         else {
620                                 $this->{NCNT}++;
621                         }
622                 }
624                 push @split, $this;
625                 last;
626         }
628         for my $hunk (@split) {
629                 $o_ofs = $hunk->{OLD};
630                 $n_ofs = $hunk->{NEW};
631                 my $o_cnt = $hunk->{OCNT};
632                 my $n_cnt = $hunk->{NCNT};
634                 my $head = ("@@ -$o_ofs" .
635                             (($o_cnt != 1) ? ",$o_cnt" : '') .
636                             " +$n_ofs" .
637                             (($n_cnt != 1) ? ",$n_cnt" : '') .
638                             " @@\n");
639                 my $display_head = $head;
640                 unshift @{$hunk->{TEXT}}, $head;
641                 if ($diff_use_color) {
642                         $display_head = colored($fraginfo_color, $head);
643                 }
644                 unshift @{$hunk->{DISPLAY}}, $display_head;
645         }
646         return @split;
649 sub find_last_o_ctx {
650         my ($it) = @_;
651         my $text = $it->{TEXT};
652         my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
653         my $i = @{$text};
654         my $last_o_ctx = $o_ofs + $o_cnt;
655         while (0 < --$i) {
656                 my $line = $text->[$i];
657                 if ($line =~ /^ /) {
658                         $last_o_ctx--;
659                         next;
660                 }
661                 last;
662         }
663         return $last_o_ctx;
666 sub merge_hunk {
667         my ($prev, $this) = @_;
668         my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
669             parse_hunk_header($prev->{TEXT}[0]);
670         my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
671             parse_hunk_header($this->{TEXT}[0]);
673         my (@line, $i, $ofs, $o_cnt, $n_cnt);
674         $ofs = $o0_ofs;
675         $o_cnt = $n_cnt = 0;
676         for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
677                 my $line = $prev->{TEXT}[$i];
678                 if ($line =~ /^\+/) {
679                         $n_cnt++;
680                         push @line, $line;
681                         next;
682                 }
684                 last if ($o1_ofs <= $ofs);
686                 $o_cnt++;
687                 $ofs++;
688                 if ($line =~ /^ /) {
689                         $n_cnt++;
690                 }
691                 push @line, $line;
692         }
694         for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
695                 my $line = $this->{TEXT}[$i];
696                 if ($line =~ /^\+/) {
697                         $n_cnt++;
698                         push @line, $line;
699                         next;
700                 }
701                 $ofs++;
702                 $o_cnt++;
703                 if ($line =~ /^ /) {
704                         $n_cnt++;
705                 }
706                 push @line, $line;
707         }
708         my $head = ("@@ -$o0_ofs" .
709                     (($o_cnt != 1) ? ",$o_cnt" : '') .
710                     " +$n0_ofs" .
711                     (($n_cnt != 1) ? ",$n_cnt" : '') .
712                     " @@\n");
713         @{$prev->{TEXT}} = ($head, @line);
716 sub coalesce_overlapping_hunks {
717         my (@in) = @_;
718         my @out = ();
720         my ($last_o_ctx);
722         for (grep { $_->{USE} } @in) {
723                 my $text = $_->{TEXT};
724                 my ($o_ofs) = parse_hunk_header($text->[0]);
725                 if (defined $last_o_ctx &&
726                     $o_ofs <= $last_o_ctx) {
727                         merge_hunk($out[-1], $_);
728                 }
729                 else {
730                         push @out, $_;
731                 }
732                 $last_o_ctx = find_last_o_ctx($out[-1]);
733         }
734         return @out;
737 sub help_patch_cmd {
738         print colored $help_color, <<\EOF ;
739 y - stage this hunk
740 n - do not stage this hunk
741 a - stage this and all the remaining hunks in the file
742 d - do not stage this hunk nor any of the remaining hunks in the file
743 j - leave this hunk undecided, see next undecided hunk
744 J - leave this hunk undecided, see next hunk
745 k - leave this hunk undecided, see previous undecided hunk
746 K - leave this hunk undecided, see previous hunk
747 s - split the current hunk into smaller hunks
748 ? - print help
749 EOF
752 sub patch_update_cmd {
753         my @mods = grep { !($_->{BINARY}) } list_modified('file-only');
754         my @them;
756         if (!@mods) {
757                 print STDERR "No changes.\n";
758                 return 0;
759         }
760         if ($patch_mode) {
761                 @them = @mods;
762         }
763         else {
764                 @them = list_and_choose({ PROMPT => 'Patch update',
765                                           HEADER => $status_head, },
766                                         @mods);
767         }
768         for (@them) {
769                 patch_update_file($_->{VALUE});
770         }
773 sub patch_update_file {
774         my ($ix, $num);
775         my $path = shift;
776         my ($head, @hunk) = parse_diff($path);
777         for (@{$head->{DISPLAY}}) {
778                 print;
779         }
780         $num = scalar @hunk;
781         $ix = 0;
783         while (1) {
784                 my ($prev, $next, $other, $undecided, $i);
785                 $other = '';
787                 if ($num <= $ix) {
788                         $ix = 0;
789                 }
790                 for ($i = 0; $i < $ix; $i++) {
791                         if (!defined $hunk[$i]{USE}) {
792                                 $prev = 1;
793                                 $other .= '/k';
794                                 last;
795                         }
796                 }
797                 if ($ix) {
798                         $other .= '/K';
799                 }
800                 for ($i = $ix + 1; $i < $num; $i++) {
801                         if (!defined $hunk[$i]{USE}) {
802                                 $next = 1;
803                                 $other .= '/j';
804                                 last;
805                         }
806                 }
807                 if ($ix < $num - 1) {
808                         $other .= '/J';
809                 }
810                 for ($i = 0; $i < $num; $i++) {
811                         if (!defined $hunk[$i]{USE}) {
812                                 $undecided = 1;
813                                 last;
814                         }
815                 }
816                 last if (!$undecided);
818                 if (hunk_splittable($hunk[$ix]{TEXT})) {
819                         $other .= '/s';
820                 }
821                 for (@{$hunk[$ix]{DISPLAY}}) {
822                         print;
823                 }
824                 print colored $prompt_color, "Stage this hunk [y/n/a/d$other/?]? ";
825                 my $line = <STDIN>;
826                 if ($line) {
827                         if ($line =~ /^y/i) {
828                                 $hunk[$ix]{USE} = 1;
829                         }
830                         elsif ($line =~ /^n/i) {
831                                 $hunk[$ix]{USE} = 0;
832                         }
833                         elsif ($line =~ /^a/i) {
834                                 while ($ix < $num) {
835                                         if (!defined $hunk[$ix]{USE}) {
836                                                 $hunk[$ix]{USE} = 1;
837                                         }
838                                         $ix++;
839                                 }
840                                 next;
841                         }
842                         elsif ($line =~ /^d/i) {
843                                 while ($ix < $num) {
844                                         if (!defined $hunk[$ix]{USE}) {
845                                                 $hunk[$ix]{USE} = 0;
846                                         }
847                                         $ix++;
848                                 }
849                                 next;
850                         }
851                         elsif ($other =~ /K/ && $line =~ /^K/) {
852                                 $ix--;
853                                 next;
854                         }
855                         elsif ($other =~ /J/ && $line =~ /^J/) {
856                                 $ix++;
857                                 next;
858                         }
859                         elsif ($other =~ /k/ && $line =~ /^k/) {
860                                 while (1) {
861                                         $ix--;
862                                         last if (!$ix ||
863                                                  !defined $hunk[$ix]{USE});
864                                 }
865                                 next;
866                         }
867                         elsif ($other =~ /j/ && $line =~ /^j/) {
868                                 while (1) {
869                                         $ix++;
870                                         last if ($ix >= $num ||
871                                                  !defined $hunk[$ix]{USE});
872                                 }
873                                 next;
874                         }
875                         elsif ($other =~ /s/ && $line =~ /^s/) {
876                                 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
877                                 if (1 < @split) {
878                                         print colored $header_color, "Split into ",
879                                         scalar(@split), " hunks.\n";
880                                 }
881                                 splice (@hunk, $ix, 1, @split);
882                                 $num = scalar @hunk;
883                                 next;
884                         }
885                         else {
886                                 help_patch_cmd($other);
887                                 next;
888                         }
889                         # soft increment
890                         while (1) {
891                                 $ix++;
892                                 last if ($ix >= $num ||
893                                          !defined $hunk[$ix]{USE});
894                         }
895                 }
896         }
898         @hunk = coalesce_overlapping_hunks(@hunk);
900         my $n_lofs = 0;
901         my @result = ();
902         for (@hunk) {
903                 my $text = $_->{TEXT};
904                 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
905                     parse_hunk_header($text->[0]);
907                 if (!$_->{USE}) {
908                         # We would have added ($n_cnt - $o_cnt) lines
909                         # to the postimage if we were to use this hunk,
910                         # but we didn't.  So the line number that the next
911                         # hunk starts at would be shifted by that much.
912                         $n_lofs -= ($n_cnt - $o_cnt);
913                         next;
914                 }
915                 else {
916                         if ($n_lofs) {
917                                 $n_ofs += $n_lofs;
918                                 $text->[0] = ("@@ -$o_ofs" .
919                                               (($o_cnt != 1)
920                                                ? ",$o_cnt" : '') .
921                                               " +$n_ofs" .
922                                               (($n_cnt != 1)
923                                                ? ",$n_cnt" : '') .
924                                               " @@\n");
925                         }
926                         for (@$text) {
927                                 push @result, $_;
928                         }
929                 }
930         }
932         if (@result) {
933                 my $fh;
935                 open $fh, '| git apply --cached';
936                 for (@{$head->{TEXT}}, @result) {
937                         print $fh $_;
938                 }
939                 if (!close $fh) {
940                         for (@{$head->{TEXT}}, @result) {
941                                 print STDERR $_;
942                         }
943                 }
944                 refresh();
945         }
947         print "\n";
950 sub diff_cmd {
951         my @mods = list_modified('index-only');
952         @mods = grep { !($_->{BINARY}) } @mods;
953         return if (!@mods);
954         my (@them) = list_and_choose({ PROMPT => 'Review diff',
955                                      IMMEDIATE => 1,
956                                      HEADER => $status_head, },
957                                    @mods);
958         return if (!@them);
959         system(qw(git diff -p --cached HEAD --), map { $_->{VALUE} } @them);
962 sub quit_cmd {
963         print "Bye.\n";
964         exit(0);
967 sub help_cmd {
968         print colored $help_color, <<\EOF ;
969 status        - show paths with changes
970 update        - add working tree state to the staged set of changes
971 revert        - revert staged set of changes back to the HEAD version
972 patch         - pick hunks and update selectively
973 diff          - view diff between HEAD and index
974 add untracked - add contents of untracked files to the staged set of changes
975 EOF
978 sub process_args {
979         return unless @ARGV;
980         my $arg = shift @ARGV;
981         if ($arg eq "--patch") {
982                 $patch_mode = 1;
983                 $arg = shift @ARGV or die "missing --";
984                 die "invalid argument $arg, expecting --"
985                     unless $arg eq "--";
986         }
987         elsif ($arg ne "--") {
988                 die "invalid argument $arg, expecting --";
989         }
992 sub main_loop {
993         my @cmd = ([ 'status', \&status_cmd, ],
994                    [ 'update', \&update_cmd, ],
995                    [ 'revert', \&revert_cmd, ],
996                    [ 'add untracked', \&add_untracked_cmd, ],
997                    [ 'patch', \&patch_update_cmd, ],
998                    [ 'diff', \&diff_cmd, ],
999                    [ 'quit', \&quit_cmd, ],
1000                    [ 'help', \&help_cmd, ],
1001         );
1002         while (1) {
1003                 my ($it) = list_and_choose({ PROMPT => 'What now',
1004                                              SINGLETON => 1,
1005                                              LIST_FLAT => 4,
1006                                              HEADER => '*** Commands ***',
1007                                              ON_EOF => \&quit_cmd,
1008                                              IMMEDIATE => 1 }, @cmd);
1009                 if ($it) {
1010                         eval {
1011                                 $it->[1]->();
1012                         };
1013                         if ($@) {
1014                                 print "$@";
1015                         }
1016                 }
1017         }
1020 process_args();
1021 refresh();
1022 if ($patch_mode) {
1023         patch_update_cmd();
1025 else {
1026         status_cmd();
1027         main_loop();