add--interactive: detect bogus diffFilter output
[git/debian.git] / git-add--interactive.perl
blobff02008abeceada0f2e1e423393b6119c4b731af
1 #!/usr/bin/perl
3 use 5.008;
4 use strict;
5 use warnings;
6 use Git qw(unquote_path);
7 use Git::I18N;
9 binmode(STDOUT, ":raw");
11 my $repo = Git->repository();
13 my $menu_use_color = $repo->get_colorbool('color.interactive');
14 my ($prompt_color, $header_color, $help_color) =
15 $menu_use_color ? (
16 $repo->get_color('color.interactive.prompt', 'bold blue'),
17 $repo->get_color('color.interactive.header', 'bold'),
18 $repo->get_color('color.interactive.help', 'red bold'),
19 ) : ();
20 my $error_color = ();
21 if ($menu_use_color) {
22 my $help_color_spec = ($repo->config('color.interactive.help') or
23 'red bold');
24 $error_color = $repo->get_color('color.interactive.error',
25 $help_color_spec);
28 my $diff_use_color = $repo->get_colorbool('color.diff');
29 my ($fraginfo_color) =
30 $diff_use_color ? (
31 $repo->get_color('color.diff.frag', 'cyan'),
32 ) : ();
33 my ($diff_plain_color) =
34 $diff_use_color ? (
35 $repo->get_color('color.diff.plain', ''),
36 ) : ();
37 my ($diff_old_color) =
38 $diff_use_color ? (
39 $repo->get_color('color.diff.old', 'red'),
40 ) : ();
41 my ($diff_new_color) =
42 $diff_use_color ? (
43 $repo->get_color('color.diff.new', 'green'),
44 ) : ();
46 my $normal_color = $repo->get_color("", "reset");
48 my $diff_algorithm = $repo->config('diff.algorithm');
49 my $diff_filter = $repo->config('interactive.difffilter');
51 my $use_readkey = 0;
52 my $use_termcap = 0;
53 my %term_escapes;
55 sub ReadMode;
56 sub ReadKey;
57 if ($repo->config_bool("interactive.singlekey")) {
58 eval {
59 require Term::ReadKey;
60 Term::ReadKey->import;
61 $use_readkey = 1;
63 if (!$use_readkey) {
64 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
66 eval {
67 require Term::Cap;
68 my $termcap = Term::Cap->Tgetent;
69 foreach (values %$termcap) {
70 $term_escapes{$_} = 1 if /^\e/;
72 $use_termcap = 1;
76 sub colored {
77 my $color = shift;
78 my $string = join("", @_);
80 if (defined $color) {
81 # Put a color code at the beginning of each line, a reset at the end
82 # color after newlines that are not at the end of the string
83 $string =~ s/(\n+)(.)/$1$color$2/g;
84 # reset before newlines
85 $string =~ s/(\n+)/$normal_color$1/g;
86 # codes at beginning and end (if necessary):
87 $string =~ s/^/$color/;
88 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
90 return $string;
93 # command line options
94 my $patch_mode_only;
95 my $patch_mode;
96 my $patch_mode_revision;
98 sub apply_patch;
99 sub apply_patch_for_checkout_commit;
100 sub apply_patch_for_stash;
102 my %patch_modes = (
103 'stage' => {
104 DIFF => 'diff-files -p',
105 APPLY => sub { apply_patch 'apply --cached', @_; },
106 APPLY_CHECK => 'apply --cached',
107 FILTER => 'file-only',
108 IS_REVERSE => 0,
110 'stash' => {
111 DIFF => 'diff-index -p HEAD',
112 APPLY => sub { apply_patch 'apply --cached', @_; },
113 APPLY_CHECK => 'apply --cached',
114 FILTER => undef,
115 IS_REVERSE => 0,
117 'reset_head' => {
118 DIFF => 'diff-index -p --cached',
119 APPLY => sub { apply_patch 'apply -R --cached', @_; },
120 APPLY_CHECK => 'apply -R --cached',
121 FILTER => 'index-only',
122 IS_REVERSE => 1,
124 'reset_nothead' => {
125 DIFF => 'diff-index -R -p --cached',
126 APPLY => sub { apply_patch 'apply --cached', @_; },
127 APPLY_CHECK => 'apply --cached',
128 FILTER => 'index-only',
129 IS_REVERSE => 0,
131 'checkout_index' => {
132 DIFF => 'diff-files -p',
133 APPLY => sub { apply_patch 'apply -R', @_; },
134 APPLY_CHECK => 'apply -R',
135 FILTER => 'file-only',
136 IS_REVERSE => 1,
138 'checkout_head' => {
139 DIFF => 'diff-index -p',
140 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
141 APPLY_CHECK => 'apply -R',
142 FILTER => undef,
143 IS_REVERSE => 1,
145 'checkout_nothead' => {
146 DIFF => 'diff-index -R -p',
147 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
148 APPLY_CHECK => 'apply',
149 FILTER => undef,
150 IS_REVERSE => 0,
154 $patch_mode = 'stage';
155 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
157 sub run_cmd_pipe {
158 if ($^O eq 'MSWin32') {
159 my @invalid = grep {m/[":*]/} @_;
160 die "$^O does not support: @invalid\n" if @invalid;
161 my @args = map { m/ /o ? "\"$_\"": $_ } @_;
162 return qx{@args};
163 } else {
164 my $fh = undef;
165 open($fh, '-|', @_) or die;
166 return <$fh>;
170 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
172 if (!defined $GIT_DIR) {
173 exit(1); # rev-parse would have already said "not a git repo"
175 chomp($GIT_DIR);
177 sub refresh {
178 my $fh;
179 open $fh, 'git update-index --refresh |'
180 or die;
181 while (<$fh>) {
182 ;# ignore 'needs update'
184 close $fh;
187 sub list_untracked {
188 map {
189 chomp $_;
190 unquote_path($_);
192 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
195 # TRANSLATORS: you can adjust this to align "git add -i" status menu
196 my $status_fmt = __('%12s %12s %s');
197 my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
200 my $initial;
201 sub is_initial_commit {
202 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
203 unless defined $initial;
204 return $initial;
208 sub get_empty_tree {
209 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
212 sub get_diff_reference {
213 my $ref = shift;
214 if (defined $ref and $ref ne 'HEAD') {
215 return $ref;
216 } elsif (is_initial_commit()) {
217 return get_empty_tree();
218 } else {
219 return 'HEAD';
223 # Returns list of hashes, contents of each of which are:
224 # VALUE: pathname
225 # BINARY: is a binary path
226 # INDEX: is index different from HEAD?
227 # FILE: is file different from index?
228 # INDEX_ADDDEL: is it add/delete between HEAD and index?
229 # FILE_ADDDEL: is it add/delete between index and file?
230 # UNMERGED: is the path unmerged
232 sub list_modified {
233 my ($only) = @_;
234 my (%data, @return);
235 my ($add, $del, $adddel, $file);
237 my $reference = get_diff_reference($patch_mode_revision);
238 for (run_cmd_pipe(qw(git diff-index --cached
239 --numstat --summary), $reference,
240 '--', @ARGV)) {
241 if (($add, $del, $file) =
242 /^([-\d]+) ([-\d]+) (.*)/) {
243 my ($change, $bin);
244 $file = unquote_path($file);
245 if ($add eq '-' && $del eq '-') {
246 $change = __('binary');
247 $bin = 1;
249 else {
250 $change = "+$add/-$del";
252 $data{$file} = {
253 INDEX => $change,
254 BINARY => $bin,
255 FILE => __('nothing'),
258 elsif (($adddel, $file) =
259 /^ (create|delete) mode [0-7]+ (.*)$/) {
260 $file = unquote_path($file);
261 $data{$file}{INDEX_ADDDEL} = $adddel;
265 for (run_cmd_pipe(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) {
266 if (($add, $del, $file) =
267 /^([-\d]+) ([-\d]+) (.*)/) {
268 $file = unquote_path($file);
269 my ($change, $bin);
270 if ($add eq '-' && $del eq '-') {
271 $change = __('binary');
272 $bin = 1;
274 else {
275 $change = "+$add/-$del";
277 $data{$file}{FILE} = $change;
278 if ($bin) {
279 $data{$file}{BINARY} = 1;
282 elsif (($adddel, $file) =
283 /^ (create|delete) mode [0-7]+ (.*)$/) {
284 $file = unquote_path($file);
285 $data{$file}{FILE_ADDDEL} = $adddel;
287 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
288 $file = unquote_path($2);
289 if (!exists $data{$file}) {
290 $data{$file} = +{
291 INDEX => __('unchanged'),
292 BINARY => 0,
295 if ($1 eq 'U') {
296 $data{$file}{UNMERGED} = 1;
301 for (sort keys %data) {
302 my $it = $data{$_};
304 if ($only) {
305 if ($only eq 'index-only') {
306 next if ($it->{INDEX} eq __('unchanged'));
308 if ($only eq 'file-only') {
309 next if ($it->{FILE} eq __('nothing'));
312 push @return, +{
313 VALUE => $_,
314 %$it,
317 return @return;
320 sub find_unique {
321 my ($string, @stuff) = @_;
322 my $found = undef;
323 for (my $i = 0; $i < @stuff; $i++) {
324 my $it = $stuff[$i];
325 my $hit = undef;
326 if (ref $it) {
327 if ((ref $it) eq 'ARRAY') {
328 $it = $it->[0];
330 else {
331 $it = $it->{VALUE};
334 eval {
335 if ($it =~ /^$string/) {
336 $hit = 1;
339 if (defined $hit && defined $found) {
340 return undef;
342 if ($hit) {
343 $found = $i + 1;
346 return $found;
349 # inserts string into trie and updates count for each character
350 sub update_trie {
351 my ($trie, $string) = @_;
352 foreach (split //, $string) {
353 $trie = $trie->{$_} ||= {COUNT => 0};
354 $trie->{COUNT}++;
358 # returns an array of tuples (prefix, remainder)
359 sub find_unique_prefixes {
360 my @stuff = @_;
361 my @return = ();
363 # any single prefix exceeding the soft limit is omitted
364 # if any prefix exceeds the hard limit all are omitted
365 # 0 indicates no limit
366 my $soft_limit = 0;
367 my $hard_limit = 3;
369 # build a trie modelling all possible options
370 my %trie;
371 foreach my $print (@stuff) {
372 if ((ref $print) eq 'ARRAY') {
373 $print = $print->[0];
375 elsif ((ref $print) eq 'HASH') {
376 $print = $print->{VALUE};
378 update_trie(\%trie, $print);
379 push @return, $print;
382 # use the trie to find the unique prefixes
383 for (my $i = 0; $i < @return; $i++) {
384 my $ret = $return[$i];
385 my @letters = split //, $ret;
386 my %search = %trie;
387 my ($prefix, $remainder);
388 my $j;
389 for ($j = 0; $j < @letters; $j++) {
390 my $letter = $letters[$j];
391 if ($search{$letter}{COUNT} == 1) {
392 $prefix = substr $ret, 0, $j + 1;
393 $remainder = substr $ret, $j + 1;
394 last;
396 else {
397 my $prefix = substr $ret, 0, $j;
398 return ()
399 if ($hard_limit && $j + 1 > $hard_limit);
401 %search = %{$search{$letter}};
403 if (ord($letters[0]) > 127 ||
404 ($soft_limit && $j + 1 > $soft_limit)) {
405 $prefix = undef;
406 $remainder = $ret;
408 $return[$i] = [$prefix, $remainder];
410 return @return;
413 # filters out prefixes which have special meaning to list_and_choose()
414 sub is_valid_prefix {
415 my $prefix = shift;
416 return (defined $prefix) &&
417 !($prefix =~ /[\s,]/) && # separators
418 !($prefix =~ /^-/) && # deselection
419 !($prefix =~ /^\d+/) && # selection
420 ($prefix ne '*') && # "all" wildcard
421 ($prefix ne '?'); # prompt help
424 # given a prefix/remainder tuple return a string with the prefix highlighted
425 # for now use square brackets; later might use ANSI colors (underline, bold)
426 sub highlight_prefix {
427 my $prefix = shift;
428 my $remainder = shift;
430 if (!defined $prefix) {
431 return $remainder;
434 if (!is_valid_prefix($prefix)) {
435 return "$prefix$remainder";
438 if (!$menu_use_color) {
439 return "[$prefix]$remainder";
442 return "$prompt_color$prefix$normal_color$remainder";
445 sub error_msg {
446 print STDERR colored $error_color, @_;
449 sub list_and_choose {
450 my ($opts, @stuff) = @_;
451 my (@chosen, @return);
452 if (!@stuff) {
453 return @return;
455 my $i;
456 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
458 TOPLOOP:
459 while (1) {
460 my $last_lf = 0;
462 if ($opts->{HEADER}) {
463 if (!$opts->{LIST_FLAT}) {
464 print " ";
466 print colored $header_color, "$opts->{HEADER}\n";
468 for ($i = 0; $i < @stuff; $i++) {
469 my $chosen = $chosen[$i] ? '*' : ' ';
470 my $print = $stuff[$i];
471 my $ref = ref $print;
472 my $highlighted = highlight_prefix(@{$prefixes[$i]})
473 if @prefixes;
474 if ($ref eq 'ARRAY') {
475 $print = $highlighted || $print->[0];
477 elsif ($ref eq 'HASH') {
478 my $value = $highlighted || $print->{VALUE};
479 $print = sprintf($status_fmt,
480 $print->{INDEX},
481 $print->{FILE},
482 $value);
484 else {
485 $print = $highlighted || $print;
487 printf("%s%2d: %s", $chosen, $i+1, $print);
488 if (($opts->{LIST_FLAT}) &&
489 (($i + 1) % ($opts->{LIST_FLAT}))) {
490 print "\t";
491 $last_lf = 0;
493 else {
494 print "\n";
495 $last_lf = 1;
498 if (!$last_lf) {
499 print "\n";
502 return if ($opts->{LIST_ONLY});
504 print colored $prompt_color, $opts->{PROMPT};
505 if ($opts->{SINGLETON}) {
506 print "> ";
508 else {
509 print ">> ";
511 my $line = <STDIN>;
512 if (!$line) {
513 print "\n";
514 $opts->{ON_EOF}->() if $opts->{ON_EOF};
515 last;
517 chomp $line;
518 last if $line eq '';
519 if ($line eq '?') {
520 $opts->{SINGLETON} ?
521 singleton_prompt_help_cmd() :
522 prompt_help_cmd();
523 next TOPLOOP;
525 for my $choice (split(/[\s,]+/, $line)) {
526 my $choose = 1;
527 my ($bottom, $top);
529 # Input that begins with '-'; unchoose
530 if ($choice =~ s/^-//) {
531 $choose = 0;
533 # A range can be specified like 5-7 or 5-.
534 if ($choice =~ /^(\d+)-(\d*)$/) {
535 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
537 elsif ($choice =~ /^\d+$/) {
538 $bottom = $top = $choice;
540 elsif ($choice eq '*') {
541 $bottom = 1;
542 $top = 1 + @stuff;
544 else {
545 $bottom = $top = find_unique($choice, @stuff);
546 if (!defined $bottom) {
547 error_msg sprintf(__("Huh (%s)?\n"), $choice);
548 next TOPLOOP;
551 if ($opts->{SINGLETON} && $bottom != $top) {
552 error_msg sprintf(__("Huh (%s)?\n"), $choice);
553 next TOPLOOP;
555 for ($i = $bottom-1; $i <= $top-1; $i++) {
556 next if (@stuff <= $i || $i < 0);
557 $chosen[$i] = $choose;
560 last if ($opts->{IMMEDIATE} || $line eq '*');
562 for ($i = 0; $i < @stuff; $i++) {
563 if ($chosen[$i]) {
564 push @return, $stuff[$i];
567 return @return;
570 sub singleton_prompt_help_cmd {
571 print colored $help_color, __ <<'EOF' ;
572 Prompt help:
573 1 - select a numbered item
574 foo - select item based on unique prefix
575 - (empty) select nothing
579 sub prompt_help_cmd {
580 print colored $help_color, __ <<'EOF' ;
581 Prompt help:
582 1 - select a single item
583 3-5 - select a range of items
584 2-3,6-9 - select multiple ranges
585 foo - select item based on unique prefix
586 -... - unselect specified items
587 * - choose all items
588 - (empty) finish selecting
592 sub status_cmd {
593 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
594 list_modified());
595 print "\n";
598 sub say_n_paths {
599 my $did = shift @_;
600 my $cnt = scalar @_;
601 if ($did eq 'added') {
602 printf(__n("added %d path\n", "added %d paths\n",
603 $cnt), $cnt);
604 } elsif ($did eq 'updated') {
605 printf(__n("updated %d path\n", "updated %d paths\n",
606 $cnt), $cnt);
607 } elsif ($did eq 'reverted') {
608 printf(__n("reverted %d path\n", "reverted %d paths\n",
609 $cnt), $cnt);
610 } else {
611 printf(__n("touched %d path\n", "touched %d paths\n",
612 $cnt), $cnt);
616 sub update_cmd {
617 my @mods = list_modified('file-only');
618 return if (!@mods);
620 my @update = list_and_choose({ PROMPT => __('Update'),
621 HEADER => $status_head, },
622 @mods);
623 if (@update) {
624 system(qw(git update-index --add --remove --),
625 map { $_->{VALUE} } @update);
626 say_n_paths('updated', @update);
628 print "\n";
631 sub revert_cmd {
632 my @update = list_and_choose({ PROMPT => __('Revert'),
633 HEADER => $status_head, },
634 list_modified());
635 if (@update) {
636 if (is_initial_commit()) {
637 system(qw(git rm --cached),
638 map { $_->{VALUE} } @update);
640 else {
641 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
642 map { $_->{VALUE} } @update);
643 my $fh;
644 open $fh, '| git update-index --index-info'
645 or die;
646 for (@lines) {
647 print $fh $_;
649 close($fh);
650 for (@update) {
651 if ($_->{INDEX_ADDDEL} &&
652 $_->{INDEX_ADDDEL} eq 'create') {
653 system(qw(git update-index --force-remove --),
654 $_->{VALUE});
655 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
659 refresh();
660 say_n_paths('reverted', @update);
662 print "\n";
665 sub add_untracked_cmd {
666 my @add = list_and_choose({ PROMPT => __('Add untracked') },
667 list_untracked());
668 if (@add) {
669 system(qw(git update-index --add --), @add);
670 say_n_paths('added', @add);
671 } else {
672 print __("No untracked files.\n");
674 print "\n";
677 sub run_git_apply {
678 my $cmd = shift;
679 my $fh;
680 open $fh, '| git ' . $cmd . " --recount --allow-overlap";
681 print $fh @_;
682 return close $fh;
685 sub parse_diff {
686 my ($path) = @_;
687 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
688 if (defined $diff_algorithm) {
689 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
691 if (defined $patch_mode_revision) {
692 push @diff_cmd, get_diff_reference($patch_mode_revision);
694 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
695 my @colored = ();
696 if ($diff_use_color) {
697 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
698 if (defined $diff_filter) {
699 # quotemeta is overkill, but sufficient for shell-quoting
700 my $diff = join(' ', map { quotemeta } @display_cmd);
701 @display_cmd = ("$diff | $diff_filter");
704 @colored = run_cmd_pipe(@display_cmd);
706 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
708 if (@colored && @colored != @diff) {
709 print STDERR
710 "fatal: mismatched output from interactive.diffFilter\n",
711 "hint: Your filter must maintain a one-to-one correspondence\n",
712 "hint: between its input and output lines.\n";
713 exit 1;
716 for (my $i = 0; $i < @diff; $i++) {
717 if ($diff[$i] =~ /^@@ /) {
718 push @hunk, { TEXT => [], DISPLAY => [],
719 TYPE => 'hunk' };
721 push @{$hunk[-1]{TEXT}}, $diff[$i];
722 push @{$hunk[-1]{DISPLAY}},
723 (@colored ? $colored[$i] : $diff[$i]);
725 return @hunk;
728 sub parse_diff_header {
729 my $src = shift;
731 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
732 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
733 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
735 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
736 my $dest =
737 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
738 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
739 $head;
740 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
741 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
743 return ($head, $mode, $deletion);
746 sub hunk_splittable {
747 my ($text) = @_;
749 my @s = split_hunk($text);
750 return (1 < @s);
753 sub parse_hunk_header {
754 my ($line) = @_;
755 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
756 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
757 $o_cnt = 1 unless defined $o_cnt;
758 $n_cnt = 1 unless defined $n_cnt;
759 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
762 sub split_hunk {
763 my ($text, $display) = @_;
764 my @split = ();
765 if (!defined $display) {
766 $display = $text;
768 # If there are context lines in the middle of a hunk,
769 # it can be split, but we would need to take care of
770 # overlaps later.
772 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
773 my $hunk_start = 1;
775 OUTER:
776 while (1) {
777 my $next_hunk_start = undef;
778 my $i = $hunk_start - 1;
779 my $this = +{
780 TEXT => [],
781 DISPLAY => [],
782 TYPE => 'hunk',
783 OLD => $o_ofs,
784 NEW => $n_ofs,
785 OCNT => 0,
786 NCNT => 0,
787 ADDDEL => 0,
788 POSTCTX => 0,
789 USE => undef,
792 while (++$i < @$text) {
793 my $line = $text->[$i];
794 my $display = $display->[$i];
795 if ($line =~ /^ /) {
796 if ($this->{ADDDEL} &&
797 !defined $next_hunk_start) {
798 # We have seen leading context and
799 # adds/dels and then here is another
800 # context, which is trailing for this
801 # split hunk and leading for the next
802 # one.
803 $next_hunk_start = $i;
805 push @{$this->{TEXT}}, $line;
806 push @{$this->{DISPLAY}}, $display;
807 $this->{OCNT}++;
808 $this->{NCNT}++;
809 if (defined $next_hunk_start) {
810 $this->{POSTCTX}++;
812 next;
815 # add/del
816 if (defined $next_hunk_start) {
817 # We are done with the current hunk and
818 # this is the first real change for the
819 # next split one.
820 $hunk_start = $next_hunk_start;
821 $o_ofs = $this->{OLD} + $this->{OCNT};
822 $n_ofs = $this->{NEW} + $this->{NCNT};
823 $o_ofs -= $this->{POSTCTX};
824 $n_ofs -= $this->{POSTCTX};
825 push @split, $this;
826 redo OUTER;
828 push @{$this->{TEXT}}, $line;
829 push @{$this->{DISPLAY}}, $display;
830 $this->{ADDDEL}++;
831 if ($line =~ /^-/) {
832 $this->{OCNT}++;
834 else {
835 $this->{NCNT}++;
839 push @split, $this;
840 last;
843 for my $hunk (@split) {
844 $o_ofs = $hunk->{OLD};
845 $n_ofs = $hunk->{NEW};
846 my $o_cnt = $hunk->{OCNT};
847 my $n_cnt = $hunk->{NCNT};
849 my $head = ("@@ -$o_ofs" .
850 (($o_cnt != 1) ? ",$o_cnt" : '') .
851 " +$n_ofs" .
852 (($n_cnt != 1) ? ",$n_cnt" : '') .
853 " @@\n");
854 my $display_head = $head;
855 unshift @{$hunk->{TEXT}}, $head;
856 if ($diff_use_color) {
857 $display_head = colored($fraginfo_color, $head);
859 unshift @{$hunk->{DISPLAY}}, $display_head;
861 return @split;
864 sub find_last_o_ctx {
865 my ($it) = @_;
866 my $text = $it->{TEXT};
867 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
868 my $i = @{$text};
869 my $last_o_ctx = $o_ofs + $o_cnt;
870 while (0 < --$i) {
871 my $line = $text->[$i];
872 if ($line =~ /^ /) {
873 $last_o_ctx--;
874 next;
876 last;
878 return $last_o_ctx;
881 sub merge_hunk {
882 my ($prev, $this) = @_;
883 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
884 parse_hunk_header($prev->{TEXT}[0]);
885 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
886 parse_hunk_header($this->{TEXT}[0]);
888 my (@line, $i, $ofs, $o_cnt, $n_cnt);
889 $ofs = $o0_ofs;
890 $o_cnt = $n_cnt = 0;
891 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
892 my $line = $prev->{TEXT}[$i];
893 if ($line =~ /^\+/) {
894 $n_cnt++;
895 push @line, $line;
896 next;
899 last if ($o1_ofs <= $ofs);
901 $o_cnt++;
902 $ofs++;
903 if ($line =~ /^ /) {
904 $n_cnt++;
906 push @line, $line;
909 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
910 my $line = $this->{TEXT}[$i];
911 if ($line =~ /^\+/) {
912 $n_cnt++;
913 push @line, $line;
914 next;
916 $ofs++;
917 $o_cnt++;
918 if ($line =~ /^ /) {
919 $n_cnt++;
921 push @line, $line;
923 my $head = ("@@ -$o0_ofs" .
924 (($o_cnt != 1) ? ",$o_cnt" : '') .
925 " +$n0_ofs" .
926 (($n_cnt != 1) ? ",$n_cnt" : '') .
927 " @@\n");
928 @{$prev->{TEXT}} = ($head, @line);
931 sub coalesce_overlapping_hunks {
932 my (@in) = @_;
933 my @out = ();
935 my ($last_o_ctx, $last_was_dirty);
937 for (grep { $_->{USE} } @in) {
938 if ($_->{TYPE} ne 'hunk') {
939 push @out, $_;
940 next;
942 my $text = $_->{TEXT};
943 my ($o_ofs) = parse_hunk_header($text->[0]);
944 if (defined $last_o_ctx &&
945 $o_ofs <= $last_o_ctx &&
946 !$_->{DIRTY} &&
947 !$last_was_dirty) {
948 merge_hunk($out[-1], $_);
950 else {
951 push @out, $_;
953 $last_o_ctx = find_last_o_ctx($out[-1]);
954 $last_was_dirty = $_->{DIRTY};
956 return @out;
959 sub reassemble_patch {
960 my $head = shift;
961 my @patch;
963 # Include everything in the header except the beginning of the diff.
964 push @patch, (grep { !/^[-+]{3}/ } @$head);
966 # Then include any headers from the hunk lines, which must
967 # come before any actual hunk.
968 while (@_ && $_[0] !~ /^@/) {
969 push @patch, shift;
972 # Then begin the diff.
973 push @patch, grep { /^[-+]{3}/ } @$head;
975 # And then the actual hunks.
976 push @patch, @_;
978 return @patch;
981 sub color_diff {
982 return map {
983 colored((/^@/ ? $fraginfo_color :
984 /^\+/ ? $diff_new_color :
985 /^-/ ? $diff_old_color :
986 $diff_plain_color),
987 $_);
988 } @_;
991 my %edit_hunk_manually_modes = (
992 stage => N__(
993 "If the patch applies cleanly, the edited hunk will immediately be
994 marked for staging."),
995 stash => N__(
996 "If the patch applies cleanly, the edited hunk will immediately be
997 marked for stashing."),
998 reset_head => N__(
999 "If the patch applies cleanly, the edited hunk will immediately be
1000 marked for unstaging."),
1001 reset_nothead => N__(
1002 "If the patch applies cleanly, the edited hunk will immediately be
1003 marked for applying."),
1004 checkout_index => N__(
1005 "If the patch applies cleanly, the edited hunk will immediately be
1006 marked for discarding."),
1007 checkout_head => N__(
1008 "If the patch applies cleanly, the edited hunk will immediately be
1009 marked for discarding."),
1010 checkout_nothead => N__(
1011 "If the patch applies cleanly, the edited hunk will immediately be
1012 marked for applying."),
1015 sub edit_hunk_manually {
1016 my ($oldtext) = @_;
1018 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1019 my $fh;
1020 open $fh, '>', $hunkfile
1021 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1022 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1023 print $fh @$oldtext;
1024 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1025 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1026 my $comment_line_char = Git::get_comment_line_char;
1027 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1029 To remove '%s' lines, make them ' ' lines (context).
1030 To remove '%s' lines, delete them.
1031 Lines starting with %s will be removed.
1033 __($edit_hunk_manually_modes{$patch_mode}),
1034 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1035 __ <<EOF2 ;
1036 If it does not apply cleanly, you will be given an opportunity to
1037 edit again. If all lines of the hunk are removed, then the edit is
1038 aborted and the hunk is left unchanged.
1039 EOF2
1040 close $fh;
1042 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1043 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1045 if ($? != 0) {
1046 return undef;
1049 open $fh, '<', $hunkfile
1050 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1051 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1052 close $fh;
1053 unlink $hunkfile;
1055 # Abort if nothing remains
1056 if (!grep { /\S/ } @newtext) {
1057 return undef;
1060 # Reinsert the first hunk header if the user accidentally deleted it
1061 if ($newtext[0] !~ /^@/) {
1062 unshift @newtext, $oldtext->[0];
1064 return \@newtext;
1067 sub diff_applies {
1068 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1069 map { @{$_->{TEXT}} } @_);
1072 sub _restore_terminal_and_die {
1073 ReadMode 'restore';
1074 print "\n";
1075 exit 1;
1078 sub prompt_single_character {
1079 if ($use_readkey) {
1080 local $SIG{TERM} = \&_restore_terminal_and_die;
1081 local $SIG{INT} = \&_restore_terminal_and_die;
1082 ReadMode 'cbreak';
1083 my $key = ReadKey 0;
1084 ReadMode 'restore';
1085 if ($use_termcap and $key eq "\e") {
1086 while (!defined $term_escapes{$key}) {
1087 my $next = ReadKey 0.5;
1088 last if (!defined $next);
1089 $key .= $next;
1091 $key =~ s/\e/^[/;
1093 print "$key" if defined $key;
1094 print "\n";
1095 return $key;
1096 } else {
1097 return <STDIN>;
1101 sub prompt_yesno {
1102 my ($prompt) = @_;
1103 while (1) {
1104 print colored $prompt_color, $prompt;
1105 my $line = prompt_single_character;
1106 return undef unless defined $line;
1107 return 0 if $line =~ /^n/i;
1108 return 1 if $line =~ /^y/i;
1112 sub edit_hunk_loop {
1113 my ($head, $hunk, $ix) = @_;
1114 my $text = $hunk->[$ix]->{TEXT};
1116 while (1) {
1117 $text = edit_hunk_manually($text);
1118 if (!defined $text) {
1119 return undef;
1121 my $newhunk = {
1122 TEXT => $text,
1123 TYPE => $hunk->[$ix]->{TYPE},
1124 USE => 1,
1125 DIRTY => 1,
1127 if (diff_applies($head,
1128 @{$hunk}[0..$ix-1],
1129 $newhunk,
1130 @{$hunk}[$ix+1..$#{$hunk}])) {
1131 $newhunk->{DISPLAY} = [color_diff(@{$text})];
1132 return $newhunk;
1134 else {
1135 prompt_yesno(
1136 # TRANSLATORS: do not translate [y/n]
1137 # The program will only accept that input
1138 # at this point.
1139 # Consider translating (saying "no" discards!) as
1140 # (saying "n" for "no" discards!) if the translation
1141 # of the word "no" does not start with n.
1142 __('Your edited hunk does not apply. Edit again '
1143 . '(saying "no" discards!) [y/n]? ')
1144 ) or return undef;
1149 my %help_patch_modes = (
1150 stage => N__(
1151 "y - stage this hunk
1152 n - do not stage this hunk
1153 q - quit; do not stage this hunk or any of the remaining ones
1154 a - stage this hunk and all later hunks in the file
1155 d - do not stage this hunk or any of the later hunks in the file"),
1156 stash => N__(
1157 "y - stash this hunk
1158 n - do not stash this hunk
1159 q - quit; do not stash this hunk or any of the remaining ones
1160 a - stash this hunk and all later hunks in the file
1161 d - do not stash this hunk or any of the later hunks in the file"),
1162 reset_head => N__(
1163 "y - unstage this hunk
1164 n - do not unstage this hunk
1165 q - quit; do not unstage this hunk or any of the remaining ones
1166 a - unstage this hunk and all later hunks in the file
1167 d - do not unstage this hunk or any of the later hunks in the file"),
1168 reset_nothead => N__(
1169 "y - apply this hunk to index
1170 n - do not apply this hunk to index
1171 q - quit; do not apply this hunk or any of the remaining ones
1172 a - apply this hunk and all later hunks in the file
1173 d - do not apply this hunk or any of the later hunks in the file"),
1174 checkout_index => N__(
1175 "y - discard this hunk from worktree
1176 n - do not discard this hunk from worktree
1177 q - quit; do not discard this hunk or any of the remaining ones
1178 a - discard this hunk and all later hunks in the file
1179 d - do not discard this hunk or any of the later hunks in the file"),
1180 checkout_head => N__(
1181 "y - discard this hunk from index and worktree
1182 n - do not discard this hunk from index and worktree
1183 q - quit; do not discard this hunk or any of the remaining ones
1184 a - discard this hunk and all later hunks in the file
1185 d - do not discard this hunk or any of the later hunks in the file"),
1186 checkout_nothead => N__(
1187 "y - apply this hunk to index and worktree
1188 n - do not apply this hunk to index and worktree
1189 q - quit; do not apply this hunk or any of the remaining ones
1190 a - apply this hunk and all later hunks in the file
1191 d - do not apply this hunk or any of the later hunks in the file"),
1194 sub help_patch_cmd {
1195 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1196 g - select a hunk to go to
1197 / - search for a hunk matching the given regex
1198 j - leave this hunk undecided, see next undecided hunk
1199 J - leave this hunk undecided, see next hunk
1200 k - leave this hunk undecided, see previous undecided hunk
1201 K - leave this hunk undecided, see previous hunk
1202 s - split the current hunk into smaller hunks
1203 e - manually edit the current hunk
1204 ? - print help
1208 sub apply_patch {
1209 my $cmd = shift;
1210 my $ret = run_git_apply $cmd, @_;
1211 if (!$ret) {
1212 print STDERR @_;
1214 return $ret;
1217 sub apply_patch_for_checkout_commit {
1218 my $reverse = shift;
1219 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1220 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1222 if ($applies_worktree && $applies_index) {
1223 run_git_apply 'apply '.$reverse.' --cached', @_;
1224 run_git_apply 'apply '.$reverse, @_;
1225 return 1;
1226 } elsif (!$applies_index) {
1227 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1228 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1229 return run_git_apply 'apply '.$reverse, @_;
1230 } else {
1231 print colored $error_color, __("Nothing was applied.\n");
1232 return 0;
1234 } else {
1235 print STDERR @_;
1236 return 0;
1240 sub patch_update_cmd {
1241 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1242 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1243 for grep { $_->{UNMERGED} } @all_mods;
1244 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1246 my @mods = grep { !($_->{BINARY}) } @all_mods;
1247 my @them;
1249 if (!@mods) {
1250 if (@all_mods) {
1251 print STDERR __("Only binary files changed.\n");
1252 } else {
1253 print STDERR __("No changes.\n");
1255 return 0;
1257 if ($patch_mode_only) {
1258 @them = @mods;
1260 else {
1261 @them = list_and_choose({ PROMPT => __('Patch update'),
1262 HEADER => $status_head, },
1263 @mods);
1265 for (@them) {
1266 return 0 if patch_update_file($_->{VALUE});
1270 # Generate a one line summary of a hunk.
1271 sub summarize_hunk {
1272 my $rhunk = shift;
1273 my $summary = $rhunk->{TEXT}[0];
1275 # Keep the line numbers, discard extra context.
1276 $summary =~ s/@@(.*?)@@.*/$1 /s;
1277 $summary .= " " x (20 - length $summary);
1279 # Add some user context.
1280 for my $line (@{$rhunk->{TEXT}}) {
1281 if ($line =~ m/^[+-].*\w/) {
1282 $summary .= $line;
1283 last;
1287 chomp $summary;
1288 return substr($summary, 0, 80) . "\n";
1292 # Print a one-line summary of each hunk in the array ref in
1293 # the first argument, starting with the index in the 2nd.
1294 sub display_hunks {
1295 my ($hunks, $i) = @_;
1296 my $ctr = 0;
1297 $i ||= 0;
1298 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1299 my $status = " ";
1300 if (defined $hunks->[$i]{USE}) {
1301 $status = $hunks->[$i]{USE} ? "+" : "-";
1303 printf "%s%2d: %s",
1304 $status,
1305 $i + 1,
1306 summarize_hunk($hunks->[$i]);
1308 return $i;
1311 my %patch_update_prompt_modes = (
1312 stage => {
1313 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1314 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1315 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1317 stash => {
1318 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1319 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1320 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1322 reset_head => {
1323 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1324 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1325 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1327 reset_nothead => {
1328 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1329 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1330 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1332 checkout_index => {
1333 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1334 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1335 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1337 checkout_head => {
1338 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1339 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1340 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1342 checkout_nothead => {
1343 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1344 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1345 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1349 sub patch_update_file {
1350 my $quit = 0;
1351 my ($ix, $num);
1352 my $path = shift;
1353 my ($head, @hunk) = parse_diff($path);
1354 ($head, my $mode, my $deletion) = parse_diff_header($head);
1355 for (@{$head->{DISPLAY}}) {
1356 print;
1359 if (@{$mode->{TEXT}}) {
1360 unshift @hunk, $mode;
1362 if (@{$deletion->{TEXT}}) {
1363 foreach my $hunk (@hunk) {
1364 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1365 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1367 @hunk = ($deletion);
1370 $num = scalar @hunk;
1371 $ix = 0;
1373 while (1) {
1374 my ($prev, $next, $other, $undecided, $i);
1375 $other = '';
1377 if ($num <= $ix) {
1378 $ix = 0;
1380 for ($i = 0; $i < $ix; $i++) {
1381 if (!defined $hunk[$i]{USE}) {
1382 $prev = 1;
1383 $other .= ',k';
1384 last;
1387 if ($ix) {
1388 $other .= ',K';
1390 for ($i = $ix + 1; $i < $num; $i++) {
1391 if (!defined $hunk[$i]{USE}) {
1392 $next = 1;
1393 $other .= ',j';
1394 last;
1397 if ($ix < $num - 1) {
1398 $other .= ',J';
1400 if ($num > 1) {
1401 $other .= ',g';
1403 for ($i = 0; $i < $num; $i++) {
1404 if (!defined $hunk[$i]{USE}) {
1405 $undecided = 1;
1406 last;
1409 last if (!$undecided);
1411 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1412 hunk_splittable($hunk[$ix]{TEXT})) {
1413 $other .= ',s';
1415 if ($hunk[$ix]{TYPE} eq 'hunk') {
1416 $other .= ',e';
1418 for (@{$hunk[$ix]{DISPLAY}}) {
1419 print;
1421 print colored $prompt_color,
1422 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1424 my $line = prompt_single_character;
1425 last unless defined $line;
1426 if ($line) {
1427 if ($line =~ /^y/i) {
1428 $hunk[$ix]{USE} = 1;
1430 elsif ($line =~ /^n/i) {
1431 $hunk[$ix]{USE} = 0;
1433 elsif ($line =~ /^a/i) {
1434 while ($ix < $num) {
1435 if (!defined $hunk[$ix]{USE}) {
1436 $hunk[$ix]{USE} = 1;
1438 $ix++;
1440 next;
1442 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1443 my $response = $1;
1444 my $no = $ix > 10 ? $ix - 10 : 0;
1445 while ($response eq '') {
1446 $no = display_hunks(\@hunk, $no);
1447 if ($no < $num) {
1448 print __("go to which hunk (<ret> to see more)? ");
1449 } else {
1450 print __("go to which hunk? ");
1452 $response = <STDIN>;
1453 if (!defined $response) {
1454 $response = '';
1456 chomp $response;
1458 if ($response !~ /^\s*\d+\s*$/) {
1459 error_msg sprintf(__("Invalid number: '%s'\n"),
1460 $response);
1461 } elsif (0 < $response && $response <= $num) {
1462 $ix = $response - 1;
1463 } else {
1464 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1465 "Sorry, only %d hunks available.\n", $num), $num);
1467 next;
1469 elsif ($line =~ /^d/i) {
1470 while ($ix < $num) {
1471 if (!defined $hunk[$ix]{USE}) {
1472 $hunk[$ix]{USE} = 0;
1474 $ix++;
1476 next;
1478 elsif ($line =~ /^q/i) {
1479 for ($i = 0; $i < $num; $i++) {
1480 if (!defined $hunk[$i]{USE}) {
1481 $hunk[$i]{USE} = 0;
1484 $quit = 1;
1485 last;
1487 elsif ($line =~ m|^/(.*)|) {
1488 my $regex = $1;
1489 if ($1 eq "") {
1490 print colored $prompt_color, __("search for regex? ");
1491 $regex = <STDIN>;
1492 if (defined $regex) {
1493 chomp $regex;
1496 my $search_string;
1497 eval {
1498 $search_string = qr{$regex}m;
1500 if ($@) {
1501 my ($err,$exp) = ($@, $1);
1502 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1503 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1504 next;
1506 my $iy = $ix;
1507 while (1) {
1508 my $text = join ("", @{$hunk[$iy]{TEXT}});
1509 last if ($text =~ $search_string);
1510 $iy++;
1511 $iy = 0 if ($iy >= $num);
1512 if ($ix == $iy) {
1513 error_msg __("No hunk matches the given pattern\n");
1514 last;
1517 $ix = $iy;
1518 next;
1520 elsif ($line =~ /^K/) {
1521 if ($other =~ /K/) {
1522 $ix--;
1524 else {
1525 error_msg __("No previous hunk\n");
1527 next;
1529 elsif ($line =~ /^J/) {
1530 if ($other =~ /J/) {
1531 $ix++;
1533 else {
1534 error_msg __("No next hunk\n");
1536 next;
1538 elsif ($line =~ /^k/) {
1539 if ($other =~ /k/) {
1540 while (1) {
1541 $ix--;
1542 last if (!$ix ||
1543 !defined $hunk[$ix]{USE});
1546 else {
1547 error_msg __("No previous hunk\n");
1549 next;
1551 elsif ($line =~ /^j/) {
1552 if ($other !~ /j/) {
1553 error_msg __("No next hunk\n");
1554 next;
1557 elsif ($other =~ /s/ && $line =~ /^s/) {
1558 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1559 if (1 < @split) {
1560 print colored $header_color, sprintf(
1561 __n("Split into %d hunk.\n",
1562 "Split into %d hunks.\n",
1563 scalar(@split)), scalar(@split));
1565 splice (@hunk, $ix, 1, @split);
1566 $num = scalar @hunk;
1567 next;
1569 elsif ($other =~ /e/ && $line =~ /^e/) {
1570 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1571 if (defined $newhunk) {
1572 splice @hunk, $ix, 1, $newhunk;
1575 else {
1576 help_patch_cmd($other);
1577 next;
1579 # soft increment
1580 while (1) {
1581 $ix++;
1582 last if ($ix >= $num ||
1583 !defined $hunk[$ix]{USE});
1588 @hunk = coalesce_overlapping_hunks(@hunk);
1590 my $n_lofs = 0;
1591 my @result = ();
1592 for (@hunk) {
1593 if ($_->{USE}) {
1594 push @result, @{$_->{TEXT}};
1598 if (@result) {
1599 my @patch = reassemble_patch($head->{TEXT}, @result);
1600 my $apply_routine = $patch_mode_flavour{APPLY};
1601 &$apply_routine(@patch);
1602 refresh();
1605 print "\n";
1606 return $quit;
1609 sub diff_cmd {
1610 my @mods = list_modified('index-only');
1611 @mods = grep { !($_->{BINARY}) } @mods;
1612 return if (!@mods);
1613 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1614 IMMEDIATE => 1,
1615 HEADER => $status_head, },
1616 @mods);
1617 return if (!@them);
1618 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1619 system(qw(git diff -p --cached), $reference, '--',
1620 map { $_->{VALUE} } @them);
1623 sub quit_cmd {
1624 print __("Bye.\n");
1625 exit(0);
1628 sub help_cmd {
1629 # TRANSLATORS: please do not translate the command names
1630 # 'status', 'update', 'revert', etc.
1631 print colored $help_color, __ <<'EOF' ;
1632 status - show paths with changes
1633 update - add working tree state to the staged set of changes
1634 revert - revert staged set of changes back to the HEAD version
1635 patch - pick hunks and update selectively
1636 diff - view diff between HEAD and index
1637 add untracked - add contents of untracked files to the staged set of changes
1641 sub process_args {
1642 return unless @ARGV;
1643 my $arg = shift @ARGV;
1644 if ($arg =~ /--patch(?:=(.*))?/) {
1645 if (defined $1) {
1646 if ($1 eq 'reset') {
1647 $patch_mode = 'reset_head';
1648 $patch_mode_revision = 'HEAD';
1649 $arg = shift @ARGV or die __("missing --");
1650 if ($arg ne '--') {
1651 $patch_mode_revision = $arg;
1652 $patch_mode = ($arg eq 'HEAD' ?
1653 'reset_head' : 'reset_nothead');
1654 $arg = shift @ARGV or die __("missing --");
1656 } elsif ($1 eq 'checkout') {
1657 $arg = shift @ARGV or die __("missing --");
1658 if ($arg eq '--') {
1659 $patch_mode = 'checkout_index';
1660 } else {
1661 $patch_mode_revision = $arg;
1662 $patch_mode = ($arg eq 'HEAD' ?
1663 'checkout_head' : 'checkout_nothead');
1664 $arg = shift @ARGV or die __("missing --");
1666 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1667 $patch_mode = $1;
1668 $arg = shift @ARGV or die __("missing --");
1669 } else {
1670 die sprintf(__("unknown --patch mode: %s"), $1);
1672 } else {
1673 $patch_mode = 'stage';
1674 $arg = shift @ARGV or die __("missing --");
1676 die sprintf(__("invalid argument %s, expecting --"),
1677 $arg) unless $arg eq "--";
1678 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1679 $patch_mode_only = 1;
1681 elsif ($arg ne "--") {
1682 die sprintf(__("invalid argument %s, expecting --"), $arg);
1686 sub main_loop {
1687 my @cmd = ([ 'status', \&status_cmd, ],
1688 [ 'update', \&update_cmd, ],
1689 [ 'revert', \&revert_cmd, ],
1690 [ 'add untracked', \&add_untracked_cmd, ],
1691 [ 'patch', \&patch_update_cmd, ],
1692 [ 'diff', \&diff_cmd, ],
1693 [ 'quit', \&quit_cmd, ],
1694 [ 'help', \&help_cmd, ],
1696 while (1) {
1697 my ($it) = list_and_choose({ PROMPT => __('What now'),
1698 SINGLETON => 1,
1699 LIST_FLAT => 4,
1700 HEADER => __('*** Commands ***'),
1701 ON_EOF => \&quit_cmd,
1702 IMMEDIATE => 1 }, @cmd);
1703 if ($it) {
1704 eval {
1705 $it->[1]->();
1707 if ($@) {
1708 print "$@";
1714 process_args();
1715 refresh();
1716 if ($patch_mode_only) {
1717 patch_update_cmd();
1719 else {
1720 status_cmd();
1721 main_loop();