Git 2.13.7
[git.git] / git-add--interactive.perl
blobdaef1c14fb099261a2ee49900c1434def9d7f415
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 { !/^\Q$comment_line_char\E/ } <$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 undef unless defined $line;
1103 return 0 if $line =~ /^n/i;
1104 return 1 if $line =~ /^y/i;
1108 sub edit_hunk_loop {
1109 my ($head, $hunk, $ix) = @_;
1110 my $text = $hunk->[$ix]->{TEXT};
1112 while (1) {
1113 $text = edit_hunk_manually($text);
1114 if (!defined $text) {
1115 return undef;
1117 my $newhunk = {
1118 TEXT => $text,
1119 TYPE => $hunk->[$ix]->{TYPE},
1120 USE => 1,
1121 DIRTY => 1,
1123 if (diff_applies($head,
1124 @{$hunk}[0..$ix-1],
1125 $newhunk,
1126 @{$hunk}[$ix+1..$#{$hunk}])) {
1127 $newhunk->{DISPLAY} = [color_diff(@{$text})];
1128 return $newhunk;
1130 else {
1131 prompt_yesno(
1132 # TRANSLATORS: do not translate [y/n]
1133 # The program will only accept that input
1134 # at this point.
1135 # Consider translating (saying "no" discards!) as
1136 # (saying "n" for "no" discards!) if the translation
1137 # of the word "no" does not start with n.
1138 __('Your edited hunk does not apply. Edit again '
1139 . '(saying "no" discards!) [y/n]? ')
1140 ) or return undef;
1145 my %help_patch_modes = (
1146 stage => N__(
1147 "y - stage this hunk
1148 n - do not stage this hunk
1149 q - quit; do not stage this hunk or any of the remaining ones
1150 a - stage this hunk and all later hunks in the file
1151 d - do not stage this hunk or any of the later hunks in the file"),
1152 stash => N__(
1153 "y - stash this hunk
1154 n - do not stash this hunk
1155 q - quit; do not stash this hunk or any of the remaining ones
1156 a - stash this hunk and all later hunks in the file
1157 d - do not stash this hunk or any of the later hunks in the file"),
1158 reset_head => N__(
1159 "y - unstage this hunk
1160 n - do not unstage this hunk
1161 q - quit; do not unstage this hunk or any of the remaining ones
1162 a - unstage this hunk and all later hunks in the file
1163 d - do not unstage this hunk or any of the later hunks in the file"),
1164 reset_nothead => N__(
1165 "y - apply this hunk to index
1166 n - do not apply this hunk to index
1167 q - quit; do not apply this hunk or any of the remaining ones
1168 a - apply this hunk and all later hunks in the file
1169 d - do not apply this hunk or any of the later hunks in the file"),
1170 checkout_index => N__(
1171 "y - discard this hunk from worktree
1172 n - do not discard this hunk from worktree
1173 q - quit; do not discard this hunk or any of the remaining ones
1174 a - discard this hunk and all later hunks in the file
1175 d - do not discard this hunk or any of the later hunks in the file"),
1176 checkout_head => N__(
1177 "y - discard this hunk from index and worktree
1178 n - do not discard this hunk from index and worktree
1179 q - quit; do not discard this hunk or any of the remaining ones
1180 a - discard this hunk and all later hunks in the file
1181 d - do not discard this hunk or any of the later hunks in the file"),
1182 checkout_nothead => N__(
1183 "y - apply this hunk to index and worktree
1184 n - do not apply this hunk to index and worktree
1185 q - quit; do not apply this hunk or any of the remaining ones
1186 a - apply this hunk and all later hunks in the file
1187 d - do not apply this hunk or any of the later hunks in the file"),
1190 sub help_patch_cmd {
1191 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1192 g - select a hunk to go to
1193 / - search for a hunk matching the given regex
1194 j - leave this hunk undecided, see next undecided hunk
1195 J - leave this hunk undecided, see next hunk
1196 k - leave this hunk undecided, see previous undecided hunk
1197 K - leave this hunk undecided, see previous hunk
1198 s - split the current hunk into smaller hunks
1199 e - manually edit the current hunk
1200 ? - print help
1204 sub apply_patch {
1205 my $cmd = shift;
1206 my $ret = run_git_apply $cmd, @_;
1207 if (!$ret) {
1208 print STDERR @_;
1210 return $ret;
1213 sub apply_patch_for_checkout_commit {
1214 my $reverse = shift;
1215 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1216 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1218 if ($applies_worktree && $applies_index) {
1219 run_git_apply 'apply '.$reverse.' --cached', @_;
1220 run_git_apply 'apply '.$reverse, @_;
1221 return 1;
1222 } elsif (!$applies_index) {
1223 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1224 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1225 return run_git_apply 'apply '.$reverse, @_;
1226 } else {
1227 print colored $error_color, __("Nothing was applied.\n");
1228 return 0;
1230 } else {
1231 print STDERR @_;
1232 return 0;
1236 sub patch_update_cmd {
1237 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1238 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1239 for grep { $_->{UNMERGED} } @all_mods;
1240 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1242 my @mods = grep { !($_->{BINARY}) } @all_mods;
1243 my @them;
1245 if (!@mods) {
1246 if (@all_mods) {
1247 print STDERR __("Only binary files changed.\n");
1248 } else {
1249 print STDERR __("No changes.\n");
1251 return 0;
1253 if ($patch_mode_only) {
1254 @them = @mods;
1256 else {
1257 @them = list_and_choose({ PROMPT => __('Patch update'),
1258 HEADER => $status_head, },
1259 @mods);
1261 for (@them) {
1262 return 0 if patch_update_file($_->{VALUE});
1266 # Generate a one line summary of a hunk.
1267 sub summarize_hunk {
1268 my $rhunk = shift;
1269 my $summary = $rhunk->{TEXT}[0];
1271 # Keep the line numbers, discard extra context.
1272 $summary =~ s/@@(.*?)@@.*/$1 /s;
1273 $summary .= " " x (20 - length $summary);
1275 # Add some user context.
1276 for my $line (@{$rhunk->{TEXT}}) {
1277 if ($line =~ m/^[+-].*\w/) {
1278 $summary .= $line;
1279 last;
1283 chomp $summary;
1284 return substr($summary, 0, 80) . "\n";
1288 # Print a one-line summary of each hunk in the array ref in
1289 # the first argument, starting with the index in the 2nd.
1290 sub display_hunks {
1291 my ($hunks, $i) = @_;
1292 my $ctr = 0;
1293 $i ||= 0;
1294 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1295 my $status = " ";
1296 if (defined $hunks->[$i]{USE}) {
1297 $status = $hunks->[$i]{USE} ? "+" : "-";
1299 printf "%s%2d: %s",
1300 $status,
1301 $i + 1,
1302 summarize_hunk($hunks->[$i]);
1304 return $i;
1307 my %patch_update_prompt_modes = (
1308 stage => {
1309 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1310 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1311 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1313 stash => {
1314 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1315 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1316 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1318 reset_head => {
1319 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1320 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1321 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1323 reset_nothead => {
1324 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1325 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1326 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1328 checkout_index => {
1329 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1330 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1331 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1333 checkout_head => {
1334 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1335 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1336 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1338 checkout_nothead => {
1339 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1340 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1341 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1345 sub patch_update_file {
1346 my $quit = 0;
1347 my ($ix, $num);
1348 my $path = shift;
1349 my ($head, @hunk) = parse_diff($path);
1350 ($head, my $mode, my $deletion) = parse_diff_header($head);
1351 for (@{$head->{DISPLAY}}) {
1352 print;
1355 if (@{$mode->{TEXT}}) {
1356 unshift @hunk, $mode;
1358 if (@{$deletion->{TEXT}}) {
1359 foreach my $hunk (@hunk) {
1360 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1361 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1363 @hunk = ($deletion);
1366 $num = scalar @hunk;
1367 $ix = 0;
1369 while (1) {
1370 my ($prev, $next, $other, $undecided, $i);
1371 $other = '';
1373 if ($num <= $ix) {
1374 $ix = 0;
1376 for ($i = 0; $i < $ix; $i++) {
1377 if (!defined $hunk[$i]{USE}) {
1378 $prev = 1;
1379 $other .= ',k';
1380 last;
1383 if ($ix) {
1384 $other .= ',K';
1386 for ($i = $ix + 1; $i < $num; $i++) {
1387 if (!defined $hunk[$i]{USE}) {
1388 $next = 1;
1389 $other .= ',j';
1390 last;
1393 if ($ix < $num - 1) {
1394 $other .= ',J';
1396 if ($num > 1) {
1397 $other .= ',g';
1399 for ($i = 0; $i < $num; $i++) {
1400 if (!defined $hunk[$i]{USE}) {
1401 $undecided = 1;
1402 last;
1405 last if (!$undecided);
1407 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1408 hunk_splittable($hunk[$ix]{TEXT})) {
1409 $other .= ',s';
1411 if ($hunk[$ix]{TYPE} eq 'hunk') {
1412 $other .= ',e';
1414 for (@{$hunk[$ix]{DISPLAY}}) {
1415 print;
1417 print colored $prompt_color,
1418 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1420 my $line = prompt_single_character;
1421 last unless defined $line;
1422 if ($line) {
1423 if ($line =~ /^y/i) {
1424 $hunk[$ix]{USE} = 1;
1426 elsif ($line =~ /^n/i) {
1427 $hunk[$ix]{USE} = 0;
1429 elsif ($line =~ /^a/i) {
1430 while ($ix < $num) {
1431 if (!defined $hunk[$ix]{USE}) {
1432 $hunk[$ix]{USE} = 1;
1434 $ix++;
1436 next;
1438 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1439 my $response = $1;
1440 my $no = $ix > 10 ? $ix - 10 : 0;
1441 while ($response eq '') {
1442 $no = display_hunks(\@hunk, $no);
1443 if ($no < $num) {
1444 print __("go to which hunk (<ret> to see more)? ");
1445 } else {
1446 print __("go to which hunk? ");
1448 $response = <STDIN>;
1449 if (!defined $response) {
1450 $response = '';
1452 chomp $response;
1454 if ($response !~ /^\s*\d+\s*$/) {
1455 error_msg sprintf(__("Invalid number: '%s'\n"),
1456 $response);
1457 } elsif (0 < $response && $response <= $num) {
1458 $ix = $response - 1;
1459 } else {
1460 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1461 "Sorry, only %d hunks available.\n", $num), $num);
1463 next;
1465 elsif ($line =~ /^d/i) {
1466 while ($ix < $num) {
1467 if (!defined $hunk[$ix]{USE}) {
1468 $hunk[$ix]{USE} = 0;
1470 $ix++;
1472 next;
1474 elsif ($line =~ /^q/i) {
1475 for ($i = 0; $i < $num; $i++) {
1476 if (!defined $hunk[$i]{USE}) {
1477 $hunk[$i]{USE} = 0;
1480 $quit = 1;
1481 last;
1483 elsif ($line =~ m|^/(.*)|) {
1484 my $regex = $1;
1485 if ($1 eq "") {
1486 print colored $prompt_color, __("search for regex? ");
1487 $regex = <STDIN>;
1488 if (defined $regex) {
1489 chomp $regex;
1492 my $search_string;
1493 eval {
1494 $search_string = qr{$regex}m;
1496 if ($@) {
1497 my ($err,$exp) = ($@, $1);
1498 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1499 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1500 next;
1502 my $iy = $ix;
1503 while (1) {
1504 my $text = join ("", @{$hunk[$iy]{TEXT}});
1505 last if ($text =~ $search_string);
1506 $iy++;
1507 $iy = 0 if ($iy >= $num);
1508 if ($ix == $iy) {
1509 error_msg __("No hunk matches the given pattern\n");
1510 last;
1513 $ix = $iy;
1514 next;
1516 elsif ($line =~ /^K/) {
1517 if ($other =~ /K/) {
1518 $ix--;
1520 else {
1521 error_msg __("No previous hunk\n");
1523 next;
1525 elsif ($line =~ /^J/) {
1526 if ($other =~ /J/) {
1527 $ix++;
1529 else {
1530 error_msg __("No next hunk\n");
1532 next;
1534 elsif ($line =~ /^k/) {
1535 if ($other =~ /k/) {
1536 while (1) {
1537 $ix--;
1538 last if (!$ix ||
1539 !defined $hunk[$ix]{USE});
1542 else {
1543 error_msg __("No previous hunk\n");
1545 next;
1547 elsif ($line =~ /^j/) {
1548 if ($other !~ /j/) {
1549 error_msg __("No next hunk\n");
1550 next;
1553 elsif ($other =~ /s/ && $line =~ /^s/) {
1554 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1555 if (1 < @split) {
1556 print colored $header_color, sprintf(
1557 __n("Split into %d hunk.\n",
1558 "Split into %d hunks.\n",
1559 scalar(@split)), scalar(@split));
1561 splice (@hunk, $ix, 1, @split);
1562 $num = scalar @hunk;
1563 next;
1565 elsif ($other =~ /e/ && $line =~ /^e/) {
1566 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1567 if (defined $newhunk) {
1568 splice @hunk, $ix, 1, $newhunk;
1571 else {
1572 help_patch_cmd($other);
1573 next;
1575 # soft increment
1576 while (1) {
1577 $ix++;
1578 last if ($ix >= $num ||
1579 !defined $hunk[$ix]{USE});
1584 @hunk = coalesce_overlapping_hunks(@hunk);
1586 my $n_lofs = 0;
1587 my @result = ();
1588 for (@hunk) {
1589 if ($_->{USE}) {
1590 push @result, @{$_->{TEXT}};
1594 if (@result) {
1595 my @patch = reassemble_patch($head->{TEXT}, @result);
1596 my $apply_routine = $patch_mode_flavour{APPLY};
1597 &$apply_routine(@patch);
1598 refresh();
1601 print "\n";
1602 return $quit;
1605 sub diff_cmd {
1606 my @mods = list_modified('index-only');
1607 @mods = grep { !($_->{BINARY}) } @mods;
1608 return if (!@mods);
1609 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1610 IMMEDIATE => 1,
1611 HEADER => $status_head, },
1612 @mods);
1613 return if (!@them);
1614 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1615 system(qw(git diff -p --cached), $reference, '--',
1616 map { $_->{VALUE} } @them);
1619 sub quit_cmd {
1620 print __("Bye.\n");
1621 exit(0);
1624 sub help_cmd {
1625 # TRANSLATORS: please do not translate the command names
1626 # 'status', 'update', 'revert', etc.
1627 print colored $help_color, __ <<'EOF' ;
1628 status - show paths with changes
1629 update - add working tree state to the staged set of changes
1630 revert - revert staged set of changes back to the HEAD version
1631 patch - pick hunks and update selectively
1632 diff - view diff between HEAD and index
1633 add untracked - add contents of untracked files to the staged set of changes
1637 sub process_args {
1638 return unless @ARGV;
1639 my $arg = shift @ARGV;
1640 if ($arg =~ /--patch(?:=(.*))?/) {
1641 if (defined $1) {
1642 if ($1 eq 'reset') {
1643 $patch_mode = 'reset_head';
1644 $patch_mode_revision = 'HEAD';
1645 $arg = shift @ARGV or die __("missing --");
1646 if ($arg ne '--') {
1647 $patch_mode_revision = $arg;
1648 $patch_mode = ($arg eq 'HEAD' ?
1649 'reset_head' : 'reset_nothead');
1650 $arg = shift @ARGV or die __("missing --");
1652 } elsif ($1 eq 'checkout') {
1653 $arg = shift @ARGV or die __("missing --");
1654 if ($arg eq '--') {
1655 $patch_mode = 'checkout_index';
1656 } else {
1657 $patch_mode_revision = $arg;
1658 $patch_mode = ($arg eq 'HEAD' ?
1659 'checkout_head' : 'checkout_nothead');
1660 $arg = shift @ARGV or die __("missing --");
1662 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1663 $patch_mode = $1;
1664 $arg = shift @ARGV or die __("missing --");
1665 } else {
1666 die sprintf(__("unknown --patch mode: %s"), $1);
1668 } else {
1669 $patch_mode = 'stage';
1670 $arg = shift @ARGV or die __("missing --");
1672 die sprintf(__("invalid argument %s, expecting --"),
1673 $arg) unless $arg eq "--";
1674 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1675 $patch_mode_only = 1;
1677 elsif ($arg ne "--") {
1678 die sprintf(__("invalid argument %s, expecting --"), $arg);
1682 sub main_loop {
1683 my @cmd = ([ 'status', \&status_cmd, ],
1684 [ 'update', \&update_cmd, ],
1685 [ 'revert', \&revert_cmd, ],
1686 [ 'add untracked', \&add_untracked_cmd, ],
1687 [ 'patch', \&patch_update_cmd, ],
1688 [ 'diff', \&diff_cmd, ],
1689 [ 'quit', \&quit_cmd, ],
1690 [ 'help', \&help_cmd, ],
1692 while (1) {
1693 my ($it) = list_and_choose({ PROMPT => __('What now'),
1694 SINGLETON => 1,
1695 LIST_FLAT => 4,
1696 HEADER => __('*** Commands ***'),
1697 ON_EOF => \&quit_cmd,
1698 IMMEDIATE => 1 }, @cmd);
1699 if ($it) {
1700 eval {
1701 $it->[1]->();
1703 if ($@) {
1704 print "$@";
1710 process_args();
1711 refresh();
1712 if ($patch_mode_only) {
1713 patch_update_cmd();
1715 else {
1716 status_cmd();
1717 main_loop();