Git::unquote_path(): handle '\a'
[git/git-svn.git] / git-add--interactive.perl
blob7ea3591edc056d4405b1f18800ffd2d0b351f93c
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_indent_heuristic = $repo->config_bool('diff.indentheuristic');
50 my $diff_filter = $repo->config('interactive.difffilter');
52 my $use_readkey = 0;
53 my $use_termcap = 0;
54 my %term_escapes;
56 sub ReadMode;
57 sub ReadKey;
58 if ($repo->config_bool("interactive.singlekey")) {
59 eval {
60 require Term::ReadKey;
61 Term::ReadKey->import;
62 $use_readkey = 1;
64 if (!$use_readkey) {
65 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
67 eval {
68 require Term::Cap;
69 my $termcap = Term::Cap->Tgetent;
70 foreach (values %$termcap) {
71 $term_escapes{$_} = 1 if /^\e/;
73 $use_termcap = 1;
77 sub colored {
78 my $color = shift;
79 my $string = join("", @_);
81 if (defined $color) {
82 # Put a color code at the beginning of each line, a reset at the end
83 # color after newlines that are not at the end of the string
84 $string =~ s/(\n+)(.)/$1$color$2/g;
85 # reset before newlines
86 $string =~ s/(\n+)/$normal_color$1/g;
87 # codes at beginning and end (if necessary):
88 $string =~ s/^/$color/;
89 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
91 return $string;
94 # command line options
95 my $patch_mode_only;
96 my $patch_mode;
97 my $patch_mode_revision;
99 sub apply_patch;
100 sub apply_patch_for_checkout_commit;
101 sub apply_patch_for_stash;
103 my %patch_modes = (
104 'stage' => {
105 DIFF => 'diff-files -p',
106 APPLY => sub { apply_patch 'apply --cached', @_; },
107 APPLY_CHECK => 'apply --cached',
108 FILTER => 'file-only',
109 IS_REVERSE => 0,
111 'stash' => {
112 DIFF => 'diff-index -p HEAD',
113 APPLY => sub { apply_patch 'apply --cached', @_; },
114 APPLY_CHECK => 'apply --cached',
115 FILTER => undef,
116 IS_REVERSE => 0,
118 'reset_head' => {
119 DIFF => 'diff-index -p --cached',
120 APPLY => sub { apply_patch 'apply -R --cached', @_; },
121 APPLY_CHECK => 'apply -R --cached',
122 FILTER => 'index-only',
123 IS_REVERSE => 1,
125 'reset_nothead' => {
126 DIFF => 'diff-index -R -p --cached',
127 APPLY => sub { apply_patch 'apply --cached', @_; },
128 APPLY_CHECK => 'apply --cached',
129 FILTER => 'index-only',
130 IS_REVERSE => 0,
132 'checkout_index' => {
133 DIFF => 'diff-files -p',
134 APPLY => sub { apply_patch 'apply -R', @_; },
135 APPLY_CHECK => 'apply -R',
136 FILTER => 'file-only',
137 IS_REVERSE => 1,
139 'checkout_head' => {
140 DIFF => 'diff-index -p',
141 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
142 APPLY_CHECK => 'apply -R',
143 FILTER => undef,
144 IS_REVERSE => 1,
146 'checkout_nothead' => {
147 DIFF => 'diff-index -R -p',
148 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
149 APPLY_CHECK => 'apply',
150 FILTER => undef,
151 IS_REVERSE => 0,
155 $patch_mode = 'stage';
156 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
158 sub run_cmd_pipe {
159 if ($^O eq 'MSWin32') {
160 my @invalid = grep {m/[":*]/} @_;
161 die "$^O does not support: @invalid\n" if @invalid;
162 my @args = map { m/ /o ? "\"$_\"": $_ } @_;
163 return qx{@args};
164 } else {
165 my $fh = undef;
166 open($fh, '-|', @_) or die;
167 return <$fh>;
171 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
173 if (!defined $GIT_DIR) {
174 exit(1); # rev-parse would have already said "not a git repo"
176 chomp($GIT_DIR);
178 sub refresh {
179 my $fh;
180 open $fh, 'git update-index --refresh |'
181 or die;
182 while (<$fh>) {
183 ;# ignore 'needs update'
185 close $fh;
188 sub list_untracked {
189 map {
190 chomp $_;
191 unquote_path($_);
193 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
196 # TRANSLATORS: you can adjust this to align "git add -i" status menu
197 my $status_fmt = __('%12s %12s %s');
198 my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
201 my $initial;
202 sub is_initial_commit {
203 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
204 unless defined $initial;
205 return $initial;
209 sub get_empty_tree {
210 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
213 sub get_diff_reference {
214 my $ref = shift;
215 if (defined $ref and $ref ne 'HEAD') {
216 return $ref;
217 } elsif (is_initial_commit()) {
218 return get_empty_tree();
219 } else {
220 return 'HEAD';
224 # Returns list of hashes, contents of each of which are:
225 # VALUE: pathname
226 # BINARY: is a binary path
227 # INDEX: is index different from HEAD?
228 # FILE: is file different from index?
229 # INDEX_ADDDEL: is it add/delete between HEAD and index?
230 # FILE_ADDDEL: is it add/delete between index and file?
231 # UNMERGED: is the path unmerged
233 sub list_modified {
234 my ($only) = @_;
235 my (%data, @return);
236 my ($add, $del, $adddel, $file);
238 my $reference = get_diff_reference($patch_mode_revision);
239 for (run_cmd_pipe(qw(git diff-index --cached
240 --numstat --summary), $reference,
241 '--', @ARGV)) {
242 if (($add, $del, $file) =
243 /^([-\d]+) ([-\d]+) (.*)/) {
244 my ($change, $bin);
245 $file = unquote_path($file);
246 if ($add eq '-' && $del eq '-') {
247 $change = __('binary');
248 $bin = 1;
250 else {
251 $change = "+$add/-$del";
253 $data{$file} = {
254 INDEX => $change,
255 BINARY => $bin,
256 FILE => __('nothing'),
259 elsif (($adddel, $file) =
260 /^ (create|delete) mode [0-7]+ (.*)$/) {
261 $file = unquote_path($file);
262 $data{$file}{INDEX_ADDDEL} = $adddel;
266 for (run_cmd_pipe(qw(git diff-files --numstat --summary --raw --), @ARGV)) {
267 if (($add, $del, $file) =
268 /^([-\d]+) ([-\d]+) (.*)/) {
269 $file = unquote_path($file);
270 my ($change, $bin);
271 if ($add eq '-' && $del eq '-') {
272 $change = __('binary');
273 $bin = 1;
275 else {
276 $change = "+$add/-$del";
278 $data{$file}{FILE} = $change;
279 if ($bin) {
280 $data{$file}{BINARY} = 1;
283 elsif (($adddel, $file) =
284 /^ (create|delete) mode [0-7]+ (.*)$/) {
285 $file = unquote_path($file);
286 $data{$file}{FILE_ADDDEL} = $adddel;
288 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
289 $file = unquote_path($2);
290 if (!exists $data{$file}) {
291 $data{$file} = +{
292 INDEX => __('unchanged'),
293 BINARY => 0,
296 if ($1 eq 'U') {
297 $data{$file}{UNMERGED} = 1;
302 for (sort keys %data) {
303 my $it = $data{$_};
305 if ($only) {
306 if ($only eq 'index-only') {
307 next if ($it->{INDEX} eq __('unchanged'));
309 if ($only eq 'file-only') {
310 next if ($it->{FILE} eq __('nothing'));
313 push @return, +{
314 VALUE => $_,
315 %$it,
318 return @return;
321 sub find_unique {
322 my ($string, @stuff) = @_;
323 my $found = undef;
324 for (my $i = 0; $i < @stuff; $i++) {
325 my $it = $stuff[$i];
326 my $hit = undef;
327 if (ref $it) {
328 if ((ref $it) eq 'ARRAY') {
329 $it = $it->[0];
331 else {
332 $it = $it->{VALUE};
335 eval {
336 if ($it =~ /^$string/) {
337 $hit = 1;
340 if (defined $hit && defined $found) {
341 return undef;
343 if ($hit) {
344 $found = $i + 1;
347 return $found;
350 # inserts string into trie and updates count for each character
351 sub update_trie {
352 my ($trie, $string) = @_;
353 foreach (split //, $string) {
354 $trie = $trie->{$_} ||= {COUNT => 0};
355 $trie->{COUNT}++;
359 # returns an array of tuples (prefix, remainder)
360 sub find_unique_prefixes {
361 my @stuff = @_;
362 my @return = ();
364 # any single prefix exceeding the soft limit is omitted
365 # if any prefix exceeds the hard limit all are omitted
366 # 0 indicates no limit
367 my $soft_limit = 0;
368 my $hard_limit = 3;
370 # build a trie modelling all possible options
371 my %trie;
372 foreach my $print (@stuff) {
373 if ((ref $print) eq 'ARRAY') {
374 $print = $print->[0];
376 elsif ((ref $print) eq 'HASH') {
377 $print = $print->{VALUE};
379 update_trie(\%trie, $print);
380 push @return, $print;
383 # use the trie to find the unique prefixes
384 for (my $i = 0; $i < @return; $i++) {
385 my $ret = $return[$i];
386 my @letters = split //, $ret;
387 my %search = %trie;
388 my ($prefix, $remainder);
389 my $j;
390 for ($j = 0; $j < @letters; $j++) {
391 my $letter = $letters[$j];
392 if ($search{$letter}{COUNT} == 1) {
393 $prefix = substr $ret, 0, $j + 1;
394 $remainder = substr $ret, $j + 1;
395 last;
397 else {
398 my $prefix = substr $ret, 0, $j;
399 return ()
400 if ($hard_limit && $j + 1 > $hard_limit);
402 %search = %{$search{$letter}};
404 if (ord($letters[0]) > 127 ||
405 ($soft_limit && $j + 1 > $soft_limit)) {
406 $prefix = undef;
407 $remainder = $ret;
409 $return[$i] = [$prefix, $remainder];
411 return @return;
414 # filters out prefixes which have special meaning to list_and_choose()
415 sub is_valid_prefix {
416 my $prefix = shift;
417 return (defined $prefix) &&
418 !($prefix =~ /[\s,]/) && # separators
419 !($prefix =~ /^-/) && # deselection
420 !($prefix =~ /^\d+/) && # selection
421 ($prefix ne '*') && # "all" wildcard
422 ($prefix ne '?'); # prompt help
425 # given a prefix/remainder tuple return a string with the prefix highlighted
426 # for now use square brackets; later might use ANSI colors (underline, bold)
427 sub highlight_prefix {
428 my $prefix = shift;
429 my $remainder = shift;
431 if (!defined $prefix) {
432 return $remainder;
435 if (!is_valid_prefix($prefix)) {
436 return "$prefix$remainder";
439 if (!$menu_use_color) {
440 return "[$prefix]$remainder";
443 return "$prompt_color$prefix$normal_color$remainder";
446 sub error_msg {
447 print STDERR colored $error_color, @_;
450 sub list_and_choose {
451 my ($opts, @stuff) = @_;
452 my (@chosen, @return);
453 if (!@stuff) {
454 return @return;
456 my $i;
457 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
459 TOPLOOP:
460 while (1) {
461 my $last_lf = 0;
463 if ($opts->{HEADER}) {
464 if (!$opts->{LIST_FLAT}) {
465 print " ";
467 print colored $header_color, "$opts->{HEADER}\n";
469 for ($i = 0; $i < @stuff; $i++) {
470 my $chosen = $chosen[$i] ? '*' : ' ';
471 my $print = $stuff[$i];
472 my $ref = ref $print;
473 my $highlighted = highlight_prefix(@{$prefixes[$i]})
474 if @prefixes;
475 if ($ref eq 'ARRAY') {
476 $print = $highlighted || $print->[0];
478 elsif ($ref eq 'HASH') {
479 my $value = $highlighted || $print->{VALUE};
480 $print = sprintf($status_fmt,
481 $print->{INDEX},
482 $print->{FILE},
483 $value);
485 else {
486 $print = $highlighted || $print;
488 printf("%s%2d: %s", $chosen, $i+1, $print);
489 if (($opts->{LIST_FLAT}) &&
490 (($i + 1) % ($opts->{LIST_FLAT}))) {
491 print "\t";
492 $last_lf = 0;
494 else {
495 print "\n";
496 $last_lf = 1;
499 if (!$last_lf) {
500 print "\n";
503 return if ($opts->{LIST_ONLY});
505 print colored $prompt_color, $opts->{PROMPT};
506 if ($opts->{SINGLETON}) {
507 print "> ";
509 else {
510 print ">> ";
512 my $line = <STDIN>;
513 if (!$line) {
514 print "\n";
515 $opts->{ON_EOF}->() if $opts->{ON_EOF};
516 last;
518 chomp $line;
519 last if $line eq '';
520 if ($line eq '?') {
521 $opts->{SINGLETON} ?
522 singleton_prompt_help_cmd() :
523 prompt_help_cmd();
524 next TOPLOOP;
526 for my $choice (split(/[\s,]+/, $line)) {
527 my $choose = 1;
528 my ($bottom, $top);
530 # Input that begins with '-'; unchoose
531 if ($choice =~ s/^-//) {
532 $choose = 0;
534 # A range can be specified like 5-7 or 5-.
535 if ($choice =~ /^(\d+)-(\d*)$/) {
536 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
538 elsif ($choice =~ /^\d+$/) {
539 $bottom = $top = $choice;
541 elsif ($choice eq '*') {
542 $bottom = 1;
543 $top = 1 + @stuff;
545 else {
546 $bottom = $top = find_unique($choice, @stuff);
547 if (!defined $bottom) {
548 error_msg sprintf(__("Huh (%s)?\n"), $choice);
549 next TOPLOOP;
552 if ($opts->{SINGLETON} && $bottom != $top) {
553 error_msg sprintf(__("Huh (%s)?\n"), $choice);
554 next TOPLOOP;
556 for ($i = $bottom-1; $i <= $top-1; $i++) {
557 next if (@stuff <= $i || $i < 0);
558 $chosen[$i] = $choose;
561 last if ($opts->{IMMEDIATE} || $line eq '*');
563 for ($i = 0; $i < @stuff; $i++) {
564 if ($chosen[$i]) {
565 push @return, $stuff[$i];
568 return @return;
571 sub singleton_prompt_help_cmd {
572 print colored $help_color, __ <<'EOF' ;
573 Prompt help:
574 1 - select a numbered item
575 foo - select item based on unique prefix
576 - (empty) select nothing
580 sub prompt_help_cmd {
581 print colored $help_color, __ <<'EOF' ;
582 Prompt help:
583 1 - select a single item
584 3-5 - select a range of items
585 2-3,6-9 - select multiple ranges
586 foo - select item based on unique prefix
587 -... - unselect specified items
588 * - choose all items
589 - (empty) finish selecting
593 sub status_cmd {
594 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
595 list_modified());
596 print "\n";
599 sub say_n_paths {
600 my $did = shift @_;
601 my $cnt = scalar @_;
602 if ($did eq 'added') {
603 printf(__n("added %d path\n", "added %d paths\n",
604 $cnt), $cnt);
605 } elsif ($did eq 'updated') {
606 printf(__n("updated %d path\n", "updated %d paths\n",
607 $cnt), $cnt);
608 } elsif ($did eq 'reverted') {
609 printf(__n("reverted %d path\n", "reverted %d paths\n",
610 $cnt), $cnt);
611 } else {
612 printf(__n("touched %d path\n", "touched %d paths\n",
613 $cnt), $cnt);
617 sub update_cmd {
618 my @mods = list_modified('file-only');
619 return if (!@mods);
621 my @update = list_and_choose({ PROMPT => __('Update'),
622 HEADER => $status_head, },
623 @mods);
624 if (@update) {
625 system(qw(git update-index --add --remove --),
626 map { $_->{VALUE} } @update);
627 say_n_paths('updated', @update);
629 print "\n";
632 sub revert_cmd {
633 my @update = list_and_choose({ PROMPT => __('Revert'),
634 HEADER => $status_head, },
635 list_modified());
636 if (@update) {
637 if (is_initial_commit()) {
638 system(qw(git rm --cached),
639 map { $_->{VALUE} } @update);
641 else {
642 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
643 map { $_->{VALUE} } @update);
644 my $fh;
645 open $fh, '| git update-index --index-info'
646 or die;
647 for (@lines) {
648 print $fh $_;
650 close($fh);
651 for (@update) {
652 if ($_->{INDEX_ADDDEL} &&
653 $_->{INDEX_ADDDEL} eq 'create') {
654 system(qw(git update-index --force-remove --),
655 $_->{VALUE});
656 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
660 refresh();
661 say_n_paths('reverted', @update);
663 print "\n";
666 sub add_untracked_cmd {
667 my @add = list_and_choose({ PROMPT => __('Add untracked') },
668 list_untracked());
669 if (@add) {
670 system(qw(git update-index --add --), @add);
671 say_n_paths('added', @add);
672 } else {
673 print __("No untracked files.\n");
675 print "\n";
678 sub run_git_apply {
679 my $cmd = shift;
680 my $fh;
681 open $fh, '| git ' . $cmd . " --recount --allow-overlap";
682 print $fh @_;
683 return close $fh;
686 sub parse_diff {
687 my ($path) = @_;
688 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
689 if (defined $diff_algorithm) {
690 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
692 if ($diff_indent_heuristic) {
693 splice @diff_cmd, 1, 0, "--indent-heuristic";
695 if (defined $patch_mode_revision) {
696 push @diff_cmd, get_diff_reference($patch_mode_revision);
698 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
699 my @colored = ();
700 if ($diff_use_color) {
701 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
702 if (defined $diff_filter) {
703 # quotemeta is overkill, but sufficient for shell-quoting
704 my $diff = join(' ', map { quotemeta } @display_cmd);
705 @display_cmd = ("$diff | $diff_filter");
708 @colored = run_cmd_pipe(@display_cmd);
710 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
712 for (my $i = 0; $i < @diff; $i++) {
713 if ($diff[$i] =~ /^@@ /) {
714 push @hunk, { TEXT => [], DISPLAY => [],
715 TYPE => 'hunk' };
717 push @{$hunk[-1]{TEXT}}, $diff[$i];
718 push @{$hunk[-1]{DISPLAY}},
719 (@colored ? $colored[$i] : $diff[$i]);
721 return @hunk;
724 sub parse_diff_header {
725 my $src = shift;
727 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
728 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
729 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
731 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
732 my $dest =
733 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
734 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
735 $head;
736 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
737 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
739 return ($head, $mode, $deletion);
742 sub hunk_splittable {
743 my ($text) = @_;
745 my @s = split_hunk($text);
746 return (1 < @s);
749 sub parse_hunk_header {
750 my ($line) = @_;
751 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
752 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
753 $o_cnt = 1 unless defined $o_cnt;
754 $n_cnt = 1 unless defined $n_cnt;
755 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
758 sub split_hunk {
759 my ($text, $display) = @_;
760 my @split = ();
761 if (!defined $display) {
762 $display = $text;
764 # If there are context lines in the middle of a hunk,
765 # it can be split, but we would need to take care of
766 # overlaps later.
768 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
769 my $hunk_start = 1;
771 OUTER:
772 while (1) {
773 my $next_hunk_start = undef;
774 my $i = $hunk_start - 1;
775 my $this = +{
776 TEXT => [],
777 DISPLAY => [],
778 TYPE => 'hunk',
779 OLD => $o_ofs,
780 NEW => $n_ofs,
781 OCNT => 0,
782 NCNT => 0,
783 ADDDEL => 0,
784 POSTCTX => 0,
785 USE => undef,
788 while (++$i < @$text) {
789 my $line = $text->[$i];
790 my $display = $display->[$i];
791 if ($line =~ /^ /) {
792 if ($this->{ADDDEL} &&
793 !defined $next_hunk_start) {
794 # We have seen leading context and
795 # adds/dels and then here is another
796 # context, which is trailing for this
797 # split hunk and leading for the next
798 # one.
799 $next_hunk_start = $i;
801 push @{$this->{TEXT}}, $line;
802 push @{$this->{DISPLAY}}, $display;
803 $this->{OCNT}++;
804 $this->{NCNT}++;
805 if (defined $next_hunk_start) {
806 $this->{POSTCTX}++;
808 next;
811 # add/del
812 if (defined $next_hunk_start) {
813 # We are done with the current hunk and
814 # this is the first real change for the
815 # next split one.
816 $hunk_start = $next_hunk_start;
817 $o_ofs = $this->{OLD} + $this->{OCNT};
818 $n_ofs = $this->{NEW} + $this->{NCNT};
819 $o_ofs -= $this->{POSTCTX};
820 $n_ofs -= $this->{POSTCTX};
821 push @split, $this;
822 redo OUTER;
824 push @{$this->{TEXT}}, $line;
825 push @{$this->{DISPLAY}}, $display;
826 $this->{ADDDEL}++;
827 if ($line =~ /^-/) {
828 $this->{OCNT}++;
830 else {
831 $this->{NCNT}++;
835 push @split, $this;
836 last;
839 for my $hunk (@split) {
840 $o_ofs = $hunk->{OLD};
841 $n_ofs = $hunk->{NEW};
842 my $o_cnt = $hunk->{OCNT};
843 my $n_cnt = $hunk->{NCNT};
845 my $head = ("@@ -$o_ofs" .
846 (($o_cnt != 1) ? ",$o_cnt" : '') .
847 " +$n_ofs" .
848 (($n_cnt != 1) ? ",$n_cnt" : '') .
849 " @@\n");
850 my $display_head = $head;
851 unshift @{$hunk->{TEXT}}, $head;
852 if ($diff_use_color) {
853 $display_head = colored($fraginfo_color, $head);
855 unshift @{$hunk->{DISPLAY}}, $display_head;
857 return @split;
860 sub find_last_o_ctx {
861 my ($it) = @_;
862 my $text = $it->{TEXT};
863 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
864 my $i = @{$text};
865 my $last_o_ctx = $o_ofs + $o_cnt;
866 while (0 < --$i) {
867 my $line = $text->[$i];
868 if ($line =~ /^ /) {
869 $last_o_ctx--;
870 next;
872 last;
874 return $last_o_ctx;
877 sub merge_hunk {
878 my ($prev, $this) = @_;
879 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
880 parse_hunk_header($prev->{TEXT}[0]);
881 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
882 parse_hunk_header($this->{TEXT}[0]);
884 my (@line, $i, $ofs, $o_cnt, $n_cnt);
885 $ofs = $o0_ofs;
886 $o_cnt = $n_cnt = 0;
887 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
888 my $line = $prev->{TEXT}[$i];
889 if ($line =~ /^\+/) {
890 $n_cnt++;
891 push @line, $line;
892 next;
895 last if ($o1_ofs <= $ofs);
897 $o_cnt++;
898 $ofs++;
899 if ($line =~ /^ /) {
900 $n_cnt++;
902 push @line, $line;
905 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
906 my $line = $this->{TEXT}[$i];
907 if ($line =~ /^\+/) {
908 $n_cnt++;
909 push @line, $line;
910 next;
912 $ofs++;
913 $o_cnt++;
914 if ($line =~ /^ /) {
915 $n_cnt++;
917 push @line, $line;
919 my $head = ("@@ -$o0_ofs" .
920 (($o_cnt != 1) ? ",$o_cnt" : '') .
921 " +$n0_ofs" .
922 (($n_cnt != 1) ? ",$n_cnt" : '') .
923 " @@\n");
924 @{$prev->{TEXT}} = ($head, @line);
927 sub coalesce_overlapping_hunks {
928 my (@in) = @_;
929 my @out = ();
931 my ($last_o_ctx, $last_was_dirty);
933 for (grep { $_->{USE} } @in) {
934 if ($_->{TYPE} ne 'hunk') {
935 push @out, $_;
936 next;
938 my $text = $_->{TEXT};
939 my ($o_ofs) = parse_hunk_header($text->[0]);
940 if (defined $last_o_ctx &&
941 $o_ofs <= $last_o_ctx &&
942 !$_->{DIRTY} &&
943 !$last_was_dirty) {
944 merge_hunk($out[-1], $_);
946 else {
947 push @out, $_;
949 $last_o_ctx = find_last_o_ctx($out[-1]);
950 $last_was_dirty = $_->{DIRTY};
952 return @out;
955 sub reassemble_patch {
956 my $head = shift;
957 my @patch;
959 # Include everything in the header except the beginning of the diff.
960 push @patch, (grep { !/^[-+]{3}/ } @$head);
962 # Then include any headers from the hunk lines, which must
963 # come before any actual hunk.
964 while (@_ && $_[0] !~ /^@/) {
965 push @patch, shift;
968 # Then begin the diff.
969 push @patch, grep { /^[-+]{3}/ } @$head;
971 # And then the actual hunks.
972 push @patch, @_;
974 return @patch;
977 sub color_diff {
978 return map {
979 colored((/^@/ ? $fraginfo_color :
980 /^\+/ ? $diff_new_color :
981 /^-/ ? $diff_old_color :
982 $diff_plain_color),
983 $_);
984 } @_;
987 my %edit_hunk_manually_modes = (
988 stage => N__(
989 "If the patch applies cleanly, the edited hunk will immediately be
990 marked for staging."),
991 stash => N__(
992 "If the patch applies cleanly, the edited hunk will immediately be
993 marked for stashing."),
994 reset_head => N__(
995 "If the patch applies cleanly, the edited hunk will immediately be
996 marked for unstaging."),
997 reset_nothead => N__(
998 "If the patch applies cleanly, the edited hunk will immediately be
999 marked for applying."),
1000 checkout_index => N__(
1001 "If the patch applies cleanly, the edited hunk will immediately be
1002 marked for discarding."),
1003 checkout_head => N__(
1004 "If the patch applies cleanly, the edited hunk will immediately be
1005 marked for discarding."),
1006 checkout_nothead => N__(
1007 "If the patch applies cleanly, the edited hunk will immediately be
1008 marked for applying."),
1011 sub edit_hunk_manually {
1012 my ($oldtext) = @_;
1014 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1015 my $fh;
1016 open $fh, '>', $hunkfile
1017 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1018 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1019 print $fh @$oldtext;
1020 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1021 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1022 my $comment_line_char = Git::get_comment_line_char;
1023 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1025 To remove '%s' lines, make them ' ' lines (context).
1026 To remove '%s' lines, delete them.
1027 Lines starting with %s will be removed.
1029 __($edit_hunk_manually_modes{$patch_mode}),
1030 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1031 __ <<EOF2 ;
1032 If it does not apply cleanly, you will be given an opportunity to
1033 edit again. If all lines of the hunk are removed, then the edit is
1034 aborted and the hunk is left unchanged.
1035 EOF2
1036 close $fh;
1038 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1039 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1041 if ($? != 0) {
1042 return undef;
1045 open $fh, '<', $hunkfile
1046 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1047 my @newtext = grep { !/^$comment_line_char/ } <$fh>;
1048 close $fh;
1049 unlink $hunkfile;
1051 # Abort if nothing remains
1052 if (!grep { /\S/ } @newtext) {
1053 return undef;
1056 # Reinsert the first hunk header if the user accidentally deleted it
1057 if ($newtext[0] !~ /^@/) {
1058 unshift @newtext, $oldtext->[0];
1060 return \@newtext;
1063 sub diff_applies {
1064 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1065 map { @{$_->{TEXT}} } @_);
1068 sub _restore_terminal_and_die {
1069 ReadMode 'restore';
1070 print "\n";
1071 exit 1;
1074 sub prompt_single_character {
1075 if ($use_readkey) {
1076 local $SIG{TERM} = \&_restore_terminal_and_die;
1077 local $SIG{INT} = \&_restore_terminal_and_die;
1078 ReadMode 'cbreak';
1079 my $key = ReadKey 0;
1080 ReadMode 'restore';
1081 if ($use_termcap and $key eq "\e") {
1082 while (!defined $term_escapes{$key}) {
1083 my $next = ReadKey 0.5;
1084 last if (!defined $next);
1085 $key .= $next;
1087 $key =~ s/\e/^[/;
1089 print "$key" if defined $key;
1090 print "\n";
1091 return $key;
1092 } else {
1093 return <STDIN>;
1097 sub prompt_yesno {
1098 my ($prompt) = @_;
1099 while (1) {
1100 print colored $prompt_color, $prompt;
1101 my $line = prompt_single_character;
1102 return 0 if $line =~ /^n/i;
1103 return 1 if $line =~ /^y/i;
1107 sub edit_hunk_loop {
1108 my ($head, $hunk, $ix) = @_;
1109 my $text = $hunk->[$ix]->{TEXT};
1111 while (1) {
1112 $text = edit_hunk_manually($text);
1113 if (!defined $text) {
1114 return undef;
1116 my $newhunk = {
1117 TEXT => $text,
1118 TYPE => $hunk->[$ix]->{TYPE},
1119 USE => 1,
1120 DIRTY => 1,
1122 if (diff_applies($head,
1123 @{$hunk}[0..$ix-1],
1124 $newhunk,
1125 @{$hunk}[$ix+1..$#{$hunk}])) {
1126 $newhunk->{DISPLAY} = [color_diff(@{$text})];
1127 return $newhunk;
1129 else {
1130 prompt_yesno(
1131 # TRANSLATORS: do not translate [y/n]
1132 # The program will only accept that input
1133 # at this point.
1134 # Consider translating (saying "no" discards!) as
1135 # (saying "n" for "no" discards!) if the translation
1136 # of the word "no" does not start with n.
1137 __('Your edited hunk does not apply. Edit again '
1138 . '(saying "no" discards!) [y/n]? ')
1139 ) or return undef;
1144 my %help_patch_modes = (
1145 stage => N__(
1146 "y - stage this hunk
1147 n - do not stage this hunk
1148 q - quit; do not stage this hunk or any of the remaining ones
1149 a - stage this hunk and all later hunks in the file
1150 d - do not stage this hunk or any of the later hunks in the file"),
1151 stash => N__(
1152 "y - stash this hunk
1153 n - do not stash this hunk
1154 q - quit; do not stash this hunk or any of the remaining ones
1155 a - stash this hunk and all later hunks in the file
1156 d - do not stash this hunk or any of the later hunks in the file"),
1157 reset_head => N__(
1158 "y - unstage this hunk
1159 n - do not unstage this hunk
1160 q - quit; do not unstage this hunk or any of the remaining ones
1161 a - unstage this hunk and all later hunks in the file
1162 d - do not unstage this hunk or any of the later hunks in the file"),
1163 reset_nothead => N__(
1164 "y - apply this hunk to index
1165 n - do not apply this hunk to index
1166 q - quit; do not apply this hunk or any of the remaining ones
1167 a - apply this hunk and all later hunks in the file
1168 d - do not apply this hunk or any of the later hunks in the file"),
1169 checkout_index => N__(
1170 "y - discard this hunk from worktree
1171 n - do not discard this hunk from worktree
1172 q - quit; do not discard this hunk or any of the remaining ones
1173 a - discard this hunk and all later hunks in the file
1174 d - do not discard this hunk or any of the later hunks in the file"),
1175 checkout_head => N__(
1176 "y - discard this hunk from index and worktree
1177 n - do not discard this hunk from index and worktree
1178 q - quit; do not discard this hunk or any of the remaining ones
1179 a - discard this hunk and all later hunks in the file
1180 d - do not discard this hunk or any of the later hunks in the file"),
1181 checkout_nothead => N__(
1182 "y - apply this hunk to index and worktree
1183 n - do not apply this hunk to index and worktree
1184 q - quit; do not apply this hunk or any of the remaining ones
1185 a - apply this hunk and all later hunks in the file
1186 d - do not apply this hunk or any of the later hunks in the file"),
1189 sub help_patch_cmd {
1190 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1191 g - select a hunk to go to
1192 / - search for a hunk matching the given regex
1193 j - leave this hunk undecided, see next undecided hunk
1194 J - leave this hunk undecided, see next hunk
1195 k - leave this hunk undecided, see previous undecided hunk
1196 K - leave this hunk undecided, see previous hunk
1197 s - split the current hunk into smaller hunks
1198 e - manually edit the current hunk
1199 ? - print help
1203 sub apply_patch {
1204 my $cmd = shift;
1205 my $ret = run_git_apply $cmd, @_;
1206 if (!$ret) {
1207 print STDERR @_;
1209 return $ret;
1212 sub apply_patch_for_checkout_commit {
1213 my $reverse = shift;
1214 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1215 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1217 if ($applies_worktree && $applies_index) {
1218 run_git_apply 'apply '.$reverse.' --cached', @_;
1219 run_git_apply 'apply '.$reverse, @_;
1220 return 1;
1221 } elsif (!$applies_index) {
1222 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1223 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1224 return run_git_apply 'apply '.$reverse, @_;
1225 } else {
1226 print colored $error_color, __("Nothing was applied.\n");
1227 return 0;
1229 } else {
1230 print STDERR @_;
1231 return 0;
1235 sub patch_update_cmd {
1236 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1237 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1238 for grep { $_->{UNMERGED} } @all_mods;
1239 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1241 my @mods = grep { !($_->{BINARY}) } @all_mods;
1242 my @them;
1244 if (!@mods) {
1245 if (@all_mods) {
1246 print STDERR __("Only binary files changed.\n");
1247 } else {
1248 print STDERR __("No changes.\n");
1250 return 0;
1252 if ($patch_mode_only) {
1253 @them = @mods;
1255 else {
1256 @them = list_and_choose({ PROMPT => __('Patch update'),
1257 HEADER => $status_head, },
1258 @mods);
1260 for (@them) {
1261 return 0 if patch_update_file($_->{VALUE});
1265 # Generate a one line summary of a hunk.
1266 sub summarize_hunk {
1267 my $rhunk = shift;
1268 my $summary = $rhunk->{TEXT}[0];
1270 # Keep the line numbers, discard extra context.
1271 $summary =~ s/@@(.*?)@@.*/$1 /s;
1272 $summary .= " " x (20 - length $summary);
1274 # Add some user context.
1275 for my $line (@{$rhunk->{TEXT}}) {
1276 if ($line =~ m/^[+-].*\w/) {
1277 $summary .= $line;
1278 last;
1282 chomp $summary;
1283 return substr($summary, 0, 80) . "\n";
1287 # Print a one-line summary of each hunk in the array ref in
1288 # the first argument, starting with the index in the 2nd.
1289 sub display_hunks {
1290 my ($hunks, $i) = @_;
1291 my $ctr = 0;
1292 $i ||= 0;
1293 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1294 my $status = " ";
1295 if (defined $hunks->[$i]{USE}) {
1296 $status = $hunks->[$i]{USE} ? "+" : "-";
1298 printf "%s%2d: %s",
1299 $status,
1300 $i + 1,
1301 summarize_hunk($hunks->[$i]);
1303 return $i;
1306 my %patch_update_prompt_modes = (
1307 stage => {
1308 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1309 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1310 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1312 stash => {
1313 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1314 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1315 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1317 reset_head => {
1318 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1319 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1320 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1322 reset_nothead => {
1323 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1324 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1325 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1327 checkout_index => {
1328 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1329 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1330 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1332 checkout_head => {
1333 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1334 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1335 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1337 checkout_nothead => {
1338 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1339 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1340 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1344 sub patch_update_file {
1345 my $quit = 0;
1346 my ($ix, $num);
1347 my $path = shift;
1348 my ($head, @hunk) = parse_diff($path);
1349 ($head, my $mode, my $deletion) = parse_diff_header($head);
1350 for (@{$head->{DISPLAY}}) {
1351 print;
1354 if (@{$mode->{TEXT}}) {
1355 unshift @hunk, $mode;
1357 if (@{$deletion->{TEXT}}) {
1358 foreach my $hunk (@hunk) {
1359 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1360 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1362 @hunk = ($deletion);
1365 $num = scalar @hunk;
1366 $ix = 0;
1368 while (1) {
1369 my ($prev, $next, $other, $undecided, $i);
1370 $other = '';
1372 if ($num <= $ix) {
1373 $ix = 0;
1375 for ($i = 0; $i < $ix; $i++) {
1376 if (!defined $hunk[$i]{USE}) {
1377 $prev = 1;
1378 $other .= ',k';
1379 last;
1382 if ($ix) {
1383 $other .= ',K';
1385 for ($i = $ix + 1; $i < $num; $i++) {
1386 if (!defined $hunk[$i]{USE}) {
1387 $next = 1;
1388 $other .= ',j';
1389 last;
1392 if ($ix < $num - 1) {
1393 $other .= ',J';
1395 if ($num > 1) {
1396 $other .= ',g';
1398 for ($i = 0; $i < $num; $i++) {
1399 if (!defined $hunk[$i]{USE}) {
1400 $undecided = 1;
1401 last;
1404 last if (!$undecided);
1406 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1407 hunk_splittable($hunk[$ix]{TEXT})) {
1408 $other .= ',s';
1410 if ($hunk[$ix]{TYPE} eq 'hunk') {
1411 $other .= ',e';
1413 for (@{$hunk[$ix]{DISPLAY}}) {
1414 print;
1416 print colored $prompt_color,
1417 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1419 my $line = prompt_single_character;
1420 last unless defined $line;
1421 if ($line) {
1422 if ($line =~ /^y/i) {
1423 $hunk[$ix]{USE} = 1;
1425 elsif ($line =~ /^n/i) {
1426 $hunk[$ix]{USE} = 0;
1428 elsif ($line =~ /^a/i) {
1429 while ($ix < $num) {
1430 if (!defined $hunk[$ix]{USE}) {
1431 $hunk[$ix]{USE} = 1;
1433 $ix++;
1435 next;
1437 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1438 my $response = $1;
1439 my $no = $ix > 10 ? $ix - 10 : 0;
1440 while ($response eq '') {
1441 $no = display_hunks(\@hunk, $no);
1442 if ($no < $num) {
1443 print __("go to which hunk (<ret> to see more)? ");
1444 } else {
1445 print __("go to which hunk? ");
1447 $response = <STDIN>;
1448 if (!defined $response) {
1449 $response = '';
1451 chomp $response;
1453 if ($response !~ /^\s*\d+\s*$/) {
1454 error_msg sprintf(__("Invalid number: '%s'\n"),
1455 $response);
1456 } elsif (0 < $response && $response <= $num) {
1457 $ix = $response - 1;
1458 } else {
1459 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1460 "Sorry, only %d hunks available.\n", $num), $num);
1462 next;
1464 elsif ($line =~ /^d/i) {
1465 while ($ix < $num) {
1466 if (!defined $hunk[$ix]{USE}) {
1467 $hunk[$ix]{USE} = 0;
1469 $ix++;
1471 next;
1473 elsif ($line =~ /^q/i) {
1474 for ($i = 0; $i < $num; $i++) {
1475 if (!defined $hunk[$i]{USE}) {
1476 $hunk[$i]{USE} = 0;
1479 $quit = 1;
1480 last;
1482 elsif ($line =~ m|^/(.*)|) {
1483 my $regex = $1;
1484 if ($1 eq "") {
1485 print colored $prompt_color, __("search for regex? ");
1486 $regex = <STDIN>;
1487 if (defined $regex) {
1488 chomp $regex;
1491 my $search_string;
1492 eval {
1493 $search_string = qr{$regex}m;
1495 if ($@) {
1496 my ($err,$exp) = ($@, $1);
1497 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1498 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1499 next;
1501 my $iy = $ix;
1502 while (1) {
1503 my $text = join ("", @{$hunk[$iy]{TEXT}});
1504 last if ($text =~ $search_string);
1505 $iy++;
1506 $iy = 0 if ($iy >= $num);
1507 if ($ix == $iy) {
1508 error_msg __("No hunk matches the given pattern\n");
1509 last;
1512 $ix = $iy;
1513 next;
1515 elsif ($line =~ /^K/) {
1516 if ($other =~ /K/) {
1517 $ix--;
1519 else {
1520 error_msg __("No previous hunk\n");
1522 next;
1524 elsif ($line =~ /^J/) {
1525 if ($other =~ /J/) {
1526 $ix++;
1528 else {
1529 error_msg __("No next hunk\n");
1531 next;
1533 elsif ($line =~ /^k/) {
1534 if ($other =~ /k/) {
1535 while (1) {
1536 $ix--;
1537 last if (!$ix ||
1538 !defined $hunk[$ix]{USE});
1541 else {
1542 error_msg __("No previous hunk\n");
1544 next;
1546 elsif ($line =~ /^j/) {
1547 if ($other !~ /j/) {
1548 error_msg __("No next hunk\n");
1549 next;
1552 elsif ($other =~ /s/ && $line =~ /^s/) {
1553 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1554 if (1 < @split) {
1555 print colored $header_color, sprintf(
1556 __n("Split into %d hunk.\n",
1557 "Split into %d hunks.\n",
1558 scalar(@split)), scalar(@split));
1560 splice (@hunk, $ix, 1, @split);
1561 $num = scalar @hunk;
1562 next;
1564 elsif ($other =~ /e/ && $line =~ /^e/) {
1565 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1566 if (defined $newhunk) {
1567 splice @hunk, $ix, 1, $newhunk;
1570 else {
1571 help_patch_cmd($other);
1572 next;
1574 # soft increment
1575 while (1) {
1576 $ix++;
1577 last if ($ix >= $num ||
1578 !defined $hunk[$ix]{USE});
1583 @hunk = coalesce_overlapping_hunks(@hunk);
1585 my $n_lofs = 0;
1586 my @result = ();
1587 for (@hunk) {
1588 if ($_->{USE}) {
1589 push @result, @{$_->{TEXT}};
1593 if (@result) {
1594 my @patch = reassemble_patch($head->{TEXT}, @result);
1595 my $apply_routine = $patch_mode_flavour{APPLY};
1596 &$apply_routine(@patch);
1597 refresh();
1600 print "\n";
1601 return $quit;
1604 sub diff_cmd {
1605 my @mods = list_modified('index-only');
1606 @mods = grep { !($_->{BINARY}) } @mods;
1607 return if (!@mods);
1608 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1609 IMMEDIATE => 1,
1610 HEADER => $status_head, },
1611 @mods);
1612 return if (!@them);
1613 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1614 system(qw(git diff -p --cached), $reference, '--',
1615 map { $_->{VALUE} } @them);
1618 sub quit_cmd {
1619 print __("Bye.\n");
1620 exit(0);
1623 sub help_cmd {
1624 # TRANSLATORS: please do not translate the command names
1625 # 'status', 'update', 'revert', etc.
1626 print colored $help_color, __ <<'EOF' ;
1627 status - show paths with changes
1628 update - add working tree state to the staged set of changes
1629 revert - revert staged set of changes back to the HEAD version
1630 patch - pick hunks and update selectively
1631 diff - view diff between HEAD and index
1632 add untracked - add contents of untracked files to the staged set of changes
1636 sub process_args {
1637 return unless @ARGV;
1638 my $arg = shift @ARGV;
1639 if ($arg =~ /--patch(?:=(.*))?/) {
1640 if (defined $1) {
1641 if ($1 eq 'reset') {
1642 $patch_mode = 'reset_head';
1643 $patch_mode_revision = 'HEAD';
1644 $arg = shift @ARGV or die __("missing --");
1645 if ($arg ne '--') {
1646 $patch_mode_revision = $arg;
1647 $patch_mode = ($arg eq 'HEAD' ?
1648 'reset_head' : 'reset_nothead');
1649 $arg = shift @ARGV or die __("missing --");
1651 } elsif ($1 eq 'checkout') {
1652 $arg = shift @ARGV or die __("missing --");
1653 if ($arg eq '--') {
1654 $patch_mode = 'checkout_index';
1655 } else {
1656 $patch_mode_revision = $arg;
1657 $patch_mode = ($arg eq 'HEAD' ?
1658 'checkout_head' : 'checkout_nothead');
1659 $arg = shift @ARGV or die __("missing --");
1661 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1662 $patch_mode = $1;
1663 $arg = shift @ARGV or die __("missing --");
1664 } else {
1665 die sprintf(__("unknown --patch mode: %s"), $1);
1667 } else {
1668 $patch_mode = 'stage';
1669 $arg = shift @ARGV or die __("missing --");
1671 die sprintf(__("invalid argument %s, expecting --"),
1672 $arg) unless $arg eq "--";
1673 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1674 $patch_mode_only = 1;
1676 elsif ($arg ne "--") {
1677 die sprintf(__("invalid argument %s, expecting --"), $arg);
1681 sub main_loop {
1682 my @cmd = ([ 'status', \&status_cmd, ],
1683 [ 'update', \&update_cmd, ],
1684 [ 'revert', \&revert_cmd, ],
1685 [ 'add untracked', \&add_untracked_cmd, ],
1686 [ 'patch', \&patch_update_cmd, ],
1687 [ 'diff', \&diff_cmd, ],
1688 [ 'quit', \&quit_cmd, ],
1689 [ 'help', \&help_cmd, ],
1691 while (1) {
1692 my ($it) = list_and_choose({ PROMPT => __('What now'),
1693 SINGLETON => 1,
1694 LIST_FLAT => 4,
1695 HEADER => __('*** Commands ***'),
1696 ON_EOF => \&quit_cmd,
1697 IMMEDIATE => 1 }, @cmd);
1698 if ($it) {
1699 eval {
1700 $it->[1]->();
1702 if ($@) {
1703 print "$@";
1709 process_args();
1710 refresh();
1711 if ($patch_mode_only) {
1712 patch_update_cmd();
1714 else {
1715 status_cmd();
1716 main_loop();