Merge branch 'mb/reword-autocomplete-message' into maint
[alt-git.git] / git-add--interactive.perl
blobc55c612455205a045bbcae134fa6eeb2c2a3a5bb
1 #!/usr/bin/perl
3 use 5.008;
4 use strict;
5 use warnings;
6 use Git;
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 my %cquote_map = (
179 "b" => chr(8),
180 "t" => chr(9),
181 "n" => chr(10),
182 "v" => chr(11),
183 "f" => chr(12),
184 "r" => chr(13),
185 "\\" => "\\",
186 "\042" => "\042",
189 sub unquote_path {
190 local ($_) = @_;
191 my ($retval, $remainder);
192 if (!/^\042(.*)\042$/) {
193 return $_;
195 ($_, $retval) = ($1, "");
196 while (/^([^\\]*)\\(.*)$/) {
197 $remainder = $2;
198 $retval .= $1;
199 for ($remainder) {
200 if (/^([0-3][0-7][0-7])(.*)$/) {
201 $retval .= chr(oct($1));
202 $_ = $2;
203 last;
205 if (/^([\\\042btnvfr])(.*)$/) {
206 $retval .= $cquote_map{$1};
207 $_ = $2;
208 last;
210 # This is malformed -- just return it as-is for now.
211 return $_[0];
213 $_ = $remainder;
215 $retval .= $_;
216 return $retval;
219 sub refresh {
220 my $fh;
221 open $fh, 'git update-index --refresh |'
222 or die;
223 while (<$fh>) {
224 ;# ignore 'needs update'
226 close $fh;
229 sub list_untracked {
230 map {
231 chomp $_;
232 unquote_path($_);
234 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
237 # TRANSLATORS: you can adjust this to align "git add -i" status menu
238 my $status_fmt = __('%12s %12s %s');
239 my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
242 my $initial;
243 sub is_initial_commit {
244 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
245 unless defined $initial;
246 return $initial;
250 sub get_empty_tree {
251 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
254 sub get_diff_reference {
255 my $ref = shift;
256 if (defined $ref and $ref ne 'HEAD') {
257 return $ref;
258 } elsif (is_initial_commit()) {
259 return get_empty_tree();
260 } else {
261 return 'HEAD';
265 # Returns list of hashes, contents of each of which are:
266 # VALUE: pathname
267 # BINARY: is a binary path
268 # INDEX: is index different from HEAD?
269 # FILE: is file different from index?
270 # INDEX_ADDDEL: is it add/delete between HEAD and index?
271 # FILE_ADDDEL: is it add/delete between index and file?
272 # UNMERGED: is the path unmerged
274 sub list_modified {
275 my ($only) = @_;
276 my (%data, @return);
277 my ($add, $del, $adddel, $file);
279 my $reference = get_diff_reference($patch_mode_revision);
280 for (run_cmd_pipe(qw(git diff-index --cached
281 --numstat --summary), $reference,
282 '--', @ARGV)) {
283 if (($add, $del, $file) =
284 /^([-\d]+) ([-\d]+) (.*)/) {
285 my ($change, $bin);
286 $file = unquote_path($file);
287 if ($add eq '-' && $del eq '-') {
288 $change = __('binary');
289 $bin = 1;
291 else {
292 $change = "+$add/-$del";
294 $data{$file} = {
295 INDEX => $change,
296 BINARY => $bin,
297 FILE => __('nothing'),
300 elsif (($adddel, $file) =
301 /^ (create|delete) mode [0-7]+ (.*)$/) {
302 $file = unquote_path($file);
303 $data{$file}{INDEX_ADDDEL} = $adddel;
307 for (run_cmd_pipe(qw(git diff-files --numstat --summary --raw --), @ARGV)) {
308 if (($add, $del, $file) =
309 /^([-\d]+) ([-\d]+) (.*)/) {
310 $file = unquote_path($file);
311 my ($change, $bin);
312 if ($add eq '-' && $del eq '-') {
313 $change = __('binary');
314 $bin = 1;
316 else {
317 $change = "+$add/-$del";
319 $data{$file}{FILE} = $change;
320 if ($bin) {
321 $data{$file}{BINARY} = 1;
324 elsif (($adddel, $file) =
325 /^ (create|delete) mode [0-7]+ (.*)$/) {
326 $file = unquote_path($file);
327 $data{$file}{FILE_ADDDEL} = $adddel;
329 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
330 $file = unquote_path($2);
331 if (!exists $data{$file}) {
332 $data{$file} = +{
333 INDEX => __('unchanged'),
334 BINARY => 0,
337 if ($1 eq 'U') {
338 $data{$file}{UNMERGED} = 1;
343 for (sort keys %data) {
344 my $it = $data{$_};
346 if ($only) {
347 if ($only eq 'index-only') {
348 next if ($it->{INDEX} eq __('unchanged'));
350 if ($only eq 'file-only') {
351 next if ($it->{FILE} eq __('nothing'));
354 push @return, +{
355 VALUE => $_,
356 %$it,
359 return @return;
362 sub find_unique {
363 my ($string, @stuff) = @_;
364 my $found = undef;
365 for (my $i = 0; $i < @stuff; $i++) {
366 my $it = $stuff[$i];
367 my $hit = undef;
368 if (ref $it) {
369 if ((ref $it) eq 'ARRAY') {
370 $it = $it->[0];
372 else {
373 $it = $it->{VALUE};
376 eval {
377 if ($it =~ /^$string/) {
378 $hit = 1;
381 if (defined $hit && defined $found) {
382 return undef;
384 if ($hit) {
385 $found = $i + 1;
388 return $found;
391 # inserts string into trie and updates count for each character
392 sub update_trie {
393 my ($trie, $string) = @_;
394 foreach (split //, $string) {
395 $trie = $trie->{$_} ||= {COUNT => 0};
396 $trie->{COUNT}++;
400 # returns an array of tuples (prefix, remainder)
401 sub find_unique_prefixes {
402 my @stuff = @_;
403 my @return = ();
405 # any single prefix exceeding the soft limit is omitted
406 # if any prefix exceeds the hard limit all are omitted
407 # 0 indicates no limit
408 my $soft_limit = 0;
409 my $hard_limit = 3;
411 # build a trie modelling all possible options
412 my %trie;
413 foreach my $print (@stuff) {
414 if ((ref $print) eq 'ARRAY') {
415 $print = $print->[0];
417 elsif ((ref $print) eq 'HASH') {
418 $print = $print->{VALUE};
420 update_trie(\%trie, $print);
421 push @return, $print;
424 # use the trie to find the unique prefixes
425 for (my $i = 0; $i < @return; $i++) {
426 my $ret = $return[$i];
427 my @letters = split //, $ret;
428 my %search = %trie;
429 my ($prefix, $remainder);
430 my $j;
431 for ($j = 0; $j < @letters; $j++) {
432 my $letter = $letters[$j];
433 if ($search{$letter}{COUNT} == 1) {
434 $prefix = substr $ret, 0, $j + 1;
435 $remainder = substr $ret, $j + 1;
436 last;
438 else {
439 my $prefix = substr $ret, 0, $j;
440 return ()
441 if ($hard_limit && $j + 1 > $hard_limit);
443 %search = %{$search{$letter}};
445 if (ord($letters[0]) > 127 ||
446 ($soft_limit && $j + 1 > $soft_limit)) {
447 $prefix = undef;
448 $remainder = $ret;
450 $return[$i] = [$prefix, $remainder];
452 return @return;
455 # filters out prefixes which have special meaning to list_and_choose()
456 sub is_valid_prefix {
457 my $prefix = shift;
458 return (defined $prefix) &&
459 !($prefix =~ /[\s,]/) && # separators
460 !($prefix =~ /^-/) && # deselection
461 !($prefix =~ /^\d+/) && # selection
462 ($prefix ne '*') && # "all" wildcard
463 ($prefix ne '?'); # prompt help
466 # given a prefix/remainder tuple return a string with the prefix highlighted
467 # for now use square brackets; later might use ANSI colors (underline, bold)
468 sub highlight_prefix {
469 my $prefix = shift;
470 my $remainder = shift;
472 if (!defined $prefix) {
473 return $remainder;
476 if (!is_valid_prefix($prefix)) {
477 return "$prefix$remainder";
480 if (!$menu_use_color) {
481 return "[$prefix]$remainder";
484 return "$prompt_color$prefix$normal_color$remainder";
487 sub error_msg {
488 print STDERR colored $error_color, @_;
491 sub list_and_choose {
492 my ($opts, @stuff) = @_;
493 my (@chosen, @return);
494 if (!@stuff) {
495 return @return;
497 my $i;
498 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
500 TOPLOOP:
501 while (1) {
502 my $last_lf = 0;
504 if ($opts->{HEADER}) {
505 if (!$opts->{LIST_FLAT}) {
506 print " ";
508 print colored $header_color, "$opts->{HEADER}\n";
510 for ($i = 0; $i < @stuff; $i++) {
511 my $chosen = $chosen[$i] ? '*' : ' ';
512 my $print = $stuff[$i];
513 my $ref = ref $print;
514 my $highlighted = highlight_prefix(@{$prefixes[$i]})
515 if @prefixes;
516 if ($ref eq 'ARRAY') {
517 $print = $highlighted || $print->[0];
519 elsif ($ref eq 'HASH') {
520 my $value = $highlighted || $print->{VALUE};
521 $print = sprintf($status_fmt,
522 $print->{INDEX},
523 $print->{FILE},
524 $value);
526 else {
527 $print = $highlighted || $print;
529 printf("%s%2d: %s", $chosen, $i+1, $print);
530 if (($opts->{LIST_FLAT}) &&
531 (($i + 1) % ($opts->{LIST_FLAT}))) {
532 print "\t";
533 $last_lf = 0;
535 else {
536 print "\n";
537 $last_lf = 1;
540 if (!$last_lf) {
541 print "\n";
544 return if ($opts->{LIST_ONLY});
546 print colored $prompt_color, $opts->{PROMPT};
547 if ($opts->{SINGLETON}) {
548 print "> ";
550 else {
551 print ">> ";
553 my $line = <STDIN>;
554 if (!$line) {
555 print "\n";
556 $opts->{ON_EOF}->() if $opts->{ON_EOF};
557 last;
559 chomp $line;
560 last if $line eq '';
561 if ($line eq '?') {
562 $opts->{SINGLETON} ?
563 singleton_prompt_help_cmd() :
564 prompt_help_cmd();
565 next TOPLOOP;
567 for my $choice (split(/[\s,]+/, $line)) {
568 my $choose = 1;
569 my ($bottom, $top);
571 # Input that begins with '-'; unchoose
572 if ($choice =~ s/^-//) {
573 $choose = 0;
575 # A range can be specified like 5-7 or 5-.
576 if ($choice =~ /^(\d+)-(\d*)$/) {
577 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
579 elsif ($choice =~ /^\d+$/) {
580 $bottom = $top = $choice;
582 elsif ($choice eq '*') {
583 $bottom = 1;
584 $top = 1 + @stuff;
586 else {
587 $bottom = $top = find_unique($choice, @stuff);
588 if (!defined $bottom) {
589 error_msg sprintf(__("Huh (%s)?\n"), $choice);
590 next TOPLOOP;
593 if ($opts->{SINGLETON} && $bottom != $top) {
594 error_msg sprintf(__("Huh (%s)?\n"), $choice);
595 next TOPLOOP;
597 for ($i = $bottom-1; $i <= $top-1; $i++) {
598 next if (@stuff <= $i || $i < 0);
599 $chosen[$i] = $choose;
602 last if ($opts->{IMMEDIATE} || $line eq '*');
604 for ($i = 0; $i < @stuff; $i++) {
605 if ($chosen[$i]) {
606 push @return, $stuff[$i];
609 return @return;
612 sub singleton_prompt_help_cmd {
613 print colored $help_color, __ <<'EOF' ;
614 Prompt help:
615 1 - select a numbered item
616 foo - select item based on unique prefix
617 - (empty) select nothing
621 sub prompt_help_cmd {
622 print colored $help_color, __ <<'EOF' ;
623 Prompt help:
624 1 - select a single item
625 3-5 - select a range of items
626 2-3,6-9 - select multiple ranges
627 foo - select item based on unique prefix
628 -... - unselect specified items
629 * - choose all items
630 - (empty) finish selecting
634 sub status_cmd {
635 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
636 list_modified());
637 print "\n";
640 sub say_n_paths {
641 my $did = shift @_;
642 my $cnt = scalar @_;
643 if ($did eq 'added') {
644 printf(__n("added %d path\n", "added %d paths\n",
645 $cnt), $cnt);
646 } elsif ($did eq 'updated') {
647 printf(__n("updated %d path\n", "updated %d paths\n",
648 $cnt), $cnt);
649 } elsif ($did eq 'reverted') {
650 printf(__n("reverted %d path\n", "reverted %d paths\n",
651 $cnt), $cnt);
652 } else {
653 printf(__n("touched %d path\n", "touched %d paths\n",
654 $cnt), $cnt);
658 sub update_cmd {
659 my @mods = list_modified('file-only');
660 return if (!@mods);
662 my @update = list_and_choose({ PROMPT => __('Update'),
663 HEADER => $status_head, },
664 @mods);
665 if (@update) {
666 system(qw(git update-index --add --remove --),
667 map { $_->{VALUE} } @update);
668 say_n_paths('updated', @update);
670 print "\n";
673 sub revert_cmd {
674 my @update = list_and_choose({ PROMPT => __('Revert'),
675 HEADER => $status_head, },
676 list_modified());
677 if (@update) {
678 if (is_initial_commit()) {
679 system(qw(git rm --cached),
680 map { $_->{VALUE} } @update);
682 else {
683 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
684 map { $_->{VALUE} } @update);
685 my $fh;
686 open $fh, '| git update-index --index-info'
687 or die;
688 for (@lines) {
689 print $fh $_;
691 close($fh);
692 for (@update) {
693 if ($_->{INDEX_ADDDEL} &&
694 $_->{INDEX_ADDDEL} eq 'create') {
695 system(qw(git update-index --force-remove --),
696 $_->{VALUE});
697 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
701 refresh();
702 say_n_paths('reverted', @update);
704 print "\n";
707 sub add_untracked_cmd {
708 my @add = list_and_choose({ PROMPT => __('Add untracked') },
709 list_untracked());
710 if (@add) {
711 system(qw(git update-index --add --), @add);
712 say_n_paths('added', @add);
713 } else {
714 print __("No untracked files.\n");
716 print "\n";
719 sub run_git_apply {
720 my $cmd = shift;
721 my $fh;
722 open $fh, '| git ' . $cmd . " --recount --allow-overlap";
723 print $fh @_;
724 return close $fh;
727 sub parse_diff {
728 my ($path) = @_;
729 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
730 if (defined $diff_algorithm) {
731 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
733 if ($diff_indent_heuristic) {
734 splice @diff_cmd, 1, 0, "--indent-heuristic";
736 if (defined $patch_mode_revision) {
737 push @diff_cmd, get_diff_reference($patch_mode_revision);
739 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
740 my @colored = ();
741 if ($diff_use_color) {
742 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
743 if (defined $diff_filter) {
744 # quotemeta is overkill, but sufficient for shell-quoting
745 my $diff = join(' ', map { quotemeta } @display_cmd);
746 @display_cmd = ("$diff | $diff_filter");
749 @colored = run_cmd_pipe(@display_cmd);
751 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
753 for (my $i = 0; $i < @diff; $i++) {
754 if ($diff[$i] =~ /^@@ /) {
755 push @hunk, { TEXT => [], DISPLAY => [],
756 TYPE => 'hunk' };
758 push @{$hunk[-1]{TEXT}}, $diff[$i];
759 push @{$hunk[-1]{DISPLAY}},
760 (@colored ? $colored[$i] : $diff[$i]);
762 return @hunk;
765 sub parse_diff_header {
766 my $src = shift;
768 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
769 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
770 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
772 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
773 my $dest =
774 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
775 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
776 $head;
777 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
778 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
780 return ($head, $mode, $deletion);
783 sub hunk_splittable {
784 my ($text) = @_;
786 my @s = split_hunk($text);
787 return (1 < @s);
790 sub parse_hunk_header {
791 my ($line) = @_;
792 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
793 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
794 $o_cnt = 1 unless defined $o_cnt;
795 $n_cnt = 1 unless defined $n_cnt;
796 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
799 sub split_hunk {
800 my ($text, $display) = @_;
801 my @split = ();
802 if (!defined $display) {
803 $display = $text;
805 # If there are context lines in the middle of a hunk,
806 # it can be split, but we would need to take care of
807 # overlaps later.
809 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
810 my $hunk_start = 1;
812 OUTER:
813 while (1) {
814 my $next_hunk_start = undef;
815 my $i = $hunk_start - 1;
816 my $this = +{
817 TEXT => [],
818 DISPLAY => [],
819 TYPE => 'hunk',
820 OLD => $o_ofs,
821 NEW => $n_ofs,
822 OCNT => 0,
823 NCNT => 0,
824 ADDDEL => 0,
825 POSTCTX => 0,
826 USE => undef,
829 while (++$i < @$text) {
830 my $line = $text->[$i];
831 my $display = $display->[$i];
832 if ($line =~ /^ /) {
833 if ($this->{ADDDEL} &&
834 !defined $next_hunk_start) {
835 # We have seen leading context and
836 # adds/dels and then here is another
837 # context, which is trailing for this
838 # split hunk and leading for the next
839 # one.
840 $next_hunk_start = $i;
842 push @{$this->{TEXT}}, $line;
843 push @{$this->{DISPLAY}}, $display;
844 $this->{OCNT}++;
845 $this->{NCNT}++;
846 if (defined $next_hunk_start) {
847 $this->{POSTCTX}++;
849 next;
852 # add/del
853 if (defined $next_hunk_start) {
854 # We are done with the current hunk and
855 # this is the first real change for the
856 # next split one.
857 $hunk_start = $next_hunk_start;
858 $o_ofs = $this->{OLD} + $this->{OCNT};
859 $n_ofs = $this->{NEW} + $this->{NCNT};
860 $o_ofs -= $this->{POSTCTX};
861 $n_ofs -= $this->{POSTCTX};
862 push @split, $this;
863 redo OUTER;
865 push @{$this->{TEXT}}, $line;
866 push @{$this->{DISPLAY}}, $display;
867 $this->{ADDDEL}++;
868 if ($line =~ /^-/) {
869 $this->{OCNT}++;
871 else {
872 $this->{NCNT}++;
876 push @split, $this;
877 last;
880 for my $hunk (@split) {
881 $o_ofs = $hunk->{OLD};
882 $n_ofs = $hunk->{NEW};
883 my $o_cnt = $hunk->{OCNT};
884 my $n_cnt = $hunk->{NCNT};
886 my $head = ("@@ -$o_ofs" .
887 (($o_cnt != 1) ? ",$o_cnt" : '') .
888 " +$n_ofs" .
889 (($n_cnt != 1) ? ",$n_cnt" : '') .
890 " @@\n");
891 my $display_head = $head;
892 unshift @{$hunk->{TEXT}}, $head;
893 if ($diff_use_color) {
894 $display_head = colored($fraginfo_color, $head);
896 unshift @{$hunk->{DISPLAY}}, $display_head;
898 return @split;
901 sub find_last_o_ctx {
902 my ($it) = @_;
903 my $text = $it->{TEXT};
904 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
905 my $i = @{$text};
906 my $last_o_ctx = $o_ofs + $o_cnt;
907 while (0 < --$i) {
908 my $line = $text->[$i];
909 if ($line =~ /^ /) {
910 $last_o_ctx--;
911 next;
913 last;
915 return $last_o_ctx;
918 sub merge_hunk {
919 my ($prev, $this) = @_;
920 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
921 parse_hunk_header($prev->{TEXT}[0]);
922 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
923 parse_hunk_header($this->{TEXT}[0]);
925 my (@line, $i, $ofs, $o_cnt, $n_cnt);
926 $ofs = $o0_ofs;
927 $o_cnt = $n_cnt = 0;
928 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
929 my $line = $prev->{TEXT}[$i];
930 if ($line =~ /^\+/) {
931 $n_cnt++;
932 push @line, $line;
933 next;
936 last if ($o1_ofs <= $ofs);
938 $o_cnt++;
939 $ofs++;
940 if ($line =~ /^ /) {
941 $n_cnt++;
943 push @line, $line;
946 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
947 my $line = $this->{TEXT}[$i];
948 if ($line =~ /^\+/) {
949 $n_cnt++;
950 push @line, $line;
951 next;
953 $ofs++;
954 $o_cnt++;
955 if ($line =~ /^ /) {
956 $n_cnt++;
958 push @line, $line;
960 my $head = ("@@ -$o0_ofs" .
961 (($o_cnt != 1) ? ",$o_cnt" : '') .
962 " +$n0_ofs" .
963 (($n_cnt != 1) ? ",$n_cnt" : '') .
964 " @@\n");
965 @{$prev->{TEXT}} = ($head, @line);
968 sub coalesce_overlapping_hunks {
969 my (@in) = @_;
970 my @out = ();
972 my ($last_o_ctx, $last_was_dirty);
974 for (grep { $_->{USE} } @in) {
975 if ($_->{TYPE} ne 'hunk') {
976 push @out, $_;
977 next;
979 my $text = $_->{TEXT};
980 my ($o_ofs) = parse_hunk_header($text->[0]);
981 if (defined $last_o_ctx &&
982 $o_ofs <= $last_o_ctx &&
983 !$_->{DIRTY} &&
984 !$last_was_dirty) {
985 merge_hunk($out[-1], $_);
987 else {
988 push @out, $_;
990 $last_o_ctx = find_last_o_ctx($out[-1]);
991 $last_was_dirty = $_->{DIRTY};
993 return @out;
996 sub reassemble_patch {
997 my $head = shift;
998 my @patch;
1000 # Include everything in the header except the beginning of the diff.
1001 push @patch, (grep { !/^[-+]{3}/ } @$head);
1003 # Then include any headers from the hunk lines, which must
1004 # come before any actual hunk.
1005 while (@_ && $_[0] !~ /^@/) {
1006 push @patch, shift;
1009 # Then begin the diff.
1010 push @patch, grep { /^[-+]{3}/ } @$head;
1012 # And then the actual hunks.
1013 push @patch, @_;
1015 return @patch;
1018 sub color_diff {
1019 return map {
1020 colored((/^@/ ? $fraginfo_color :
1021 /^\+/ ? $diff_new_color :
1022 /^-/ ? $diff_old_color :
1023 $diff_plain_color),
1024 $_);
1025 } @_;
1028 my %edit_hunk_manually_modes = (
1029 stage => N__(
1030 "If the patch applies cleanly, the edited hunk will immediately be
1031 marked for staging."),
1032 stash => N__(
1033 "If the patch applies cleanly, the edited hunk will immediately be
1034 marked for stashing."),
1035 reset_head => N__(
1036 "If the patch applies cleanly, the edited hunk will immediately be
1037 marked for unstaging."),
1038 reset_nothead => N__(
1039 "If the patch applies cleanly, the edited hunk will immediately be
1040 marked for applying."),
1041 checkout_index => N__(
1042 "If the patch applies cleanly, the edited hunk will immediately be
1043 marked for discarding."),
1044 checkout_head => N__(
1045 "If the patch applies cleanly, the edited hunk will immediately be
1046 marked for discarding."),
1047 checkout_nothead => N__(
1048 "If the patch applies cleanly, the edited hunk will immediately be
1049 marked for applying."),
1052 sub edit_hunk_manually {
1053 my ($oldtext) = @_;
1055 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1056 my $fh;
1057 open $fh, '>', $hunkfile
1058 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1059 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1060 print $fh @$oldtext;
1061 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1062 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1063 my $comment_line_char = Git::get_comment_line_char;
1064 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1066 To remove '%s' lines, make them ' ' lines (context).
1067 To remove '%s' lines, delete them.
1068 Lines starting with %s will be removed.
1070 __($edit_hunk_manually_modes{$patch_mode}),
1071 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1072 __ <<EOF2 ;
1073 If it does not apply cleanly, you will be given an opportunity to
1074 edit again. If all lines of the hunk are removed, then the edit is
1075 aborted and the hunk is left unchanged.
1076 EOF2
1077 close $fh;
1079 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1080 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1082 if ($? != 0) {
1083 return undef;
1086 open $fh, '<', $hunkfile
1087 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1088 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1089 close $fh;
1090 unlink $hunkfile;
1092 # Abort if nothing remains
1093 if (!grep { /\S/ } @newtext) {
1094 return undef;
1097 # Reinsert the first hunk header if the user accidentally deleted it
1098 if ($newtext[0] !~ /^@/) {
1099 unshift @newtext, $oldtext->[0];
1101 return \@newtext;
1104 sub diff_applies {
1105 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1106 map { @{$_->{TEXT}} } @_);
1109 sub _restore_terminal_and_die {
1110 ReadMode 'restore';
1111 print "\n";
1112 exit 1;
1115 sub prompt_single_character {
1116 if ($use_readkey) {
1117 local $SIG{TERM} = \&_restore_terminal_and_die;
1118 local $SIG{INT} = \&_restore_terminal_and_die;
1119 ReadMode 'cbreak';
1120 my $key = ReadKey 0;
1121 ReadMode 'restore';
1122 if ($use_termcap and $key eq "\e") {
1123 while (!defined $term_escapes{$key}) {
1124 my $next = ReadKey 0.5;
1125 last if (!defined $next);
1126 $key .= $next;
1128 $key =~ s/\e/^[/;
1130 print "$key" if defined $key;
1131 print "\n";
1132 return $key;
1133 } else {
1134 return <STDIN>;
1138 sub prompt_yesno {
1139 my ($prompt) = @_;
1140 while (1) {
1141 print colored $prompt_color, $prompt;
1142 my $line = prompt_single_character;
1143 return undef unless defined $line;
1144 return 0 if $line =~ /^n/i;
1145 return 1 if $line =~ /^y/i;
1149 sub edit_hunk_loop {
1150 my ($head, $hunk, $ix) = @_;
1151 my $text = $hunk->[$ix]->{TEXT};
1153 while (1) {
1154 $text = edit_hunk_manually($text);
1155 if (!defined $text) {
1156 return undef;
1158 my $newhunk = {
1159 TEXT => $text,
1160 TYPE => $hunk->[$ix]->{TYPE},
1161 USE => 1,
1162 DIRTY => 1,
1164 if (diff_applies($head,
1165 @{$hunk}[0..$ix-1],
1166 $newhunk,
1167 @{$hunk}[$ix+1..$#{$hunk}])) {
1168 $newhunk->{DISPLAY} = [color_diff(@{$text})];
1169 return $newhunk;
1171 else {
1172 prompt_yesno(
1173 # TRANSLATORS: do not translate [y/n]
1174 # The program will only accept that input
1175 # at this point.
1176 # Consider translating (saying "no" discards!) as
1177 # (saying "n" for "no" discards!) if the translation
1178 # of the word "no" does not start with n.
1179 __('Your edited hunk does not apply. Edit again '
1180 . '(saying "no" discards!) [y/n]? ')
1181 ) or return undef;
1186 my %help_patch_modes = (
1187 stage => N__(
1188 "y - stage this hunk
1189 n - do not stage this hunk
1190 q - quit; do not stage this hunk or any of the remaining ones
1191 a - stage this hunk and all later hunks in the file
1192 d - do not stage this hunk or any of the later hunks in the file"),
1193 stash => N__(
1194 "y - stash this hunk
1195 n - do not stash this hunk
1196 q - quit; do not stash this hunk or any of the remaining ones
1197 a - stash this hunk and all later hunks in the file
1198 d - do not stash this hunk or any of the later hunks in the file"),
1199 reset_head => N__(
1200 "y - unstage this hunk
1201 n - do not unstage this hunk
1202 q - quit; do not unstage this hunk or any of the remaining ones
1203 a - unstage this hunk and all later hunks in the file
1204 d - do not unstage this hunk or any of the later hunks in the file"),
1205 reset_nothead => N__(
1206 "y - apply this hunk to index
1207 n - do not apply this hunk to index
1208 q - quit; do not apply this hunk or any of the remaining ones
1209 a - apply this hunk and all later hunks in the file
1210 d - do not apply this hunk or any of the later hunks in the file"),
1211 checkout_index => N__(
1212 "y - discard this hunk from worktree
1213 n - do not discard this hunk from worktree
1214 q - quit; do not discard this hunk or any of the remaining ones
1215 a - discard this hunk and all later hunks in the file
1216 d - do not discard this hunk or any of the later hunks in the file"),
1217 checkout_head => N__(
1218 "y - discard this hunk from index and worktree
1219 n - do not discard this hunk from index and worktree
1220 q - quit; do not discard this hunk or any of the remaining ones
1221 a - discard this hunk and all later hunks in the file
1222 d - do not discard this hunk or any of the later hunks in the file"),
1223 checkout_nothead => N__(
1224 "y - apply this hunk to index and worktree
1225 n - do not apply this hunk to index and worktree
1226 q - quit; do not apply this hunk or any of the remaining ones
1227 a - apply this hunk and all later hunks in the file
1228 d - do not apply this hunk or any of the later hunks in the file"),
1231 sub help_patch_cmd {
1232 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1233 g - select a hunk to go to
1234 / - search for a hunk matching the given regex
1235 j - leave this hunk undecided, see next undecided hunk
1236 J - leave this hunk undecided, see next hunk
1237 k - leave this hunk undecided, see previous undecided hunk
1238 K - leave this hunk undecided, see previous hunk
1239 s - split the current hunk into smaller hunks
1240 e - manually edit the current hunk
1241 ? - print help
1245 sub apply_patch {
1246 my $cmd = shift;
1247 my $ret = run_git_apply $cmd, @_;
1248 if (!$ret) {
1249 print STDERR @_;
1251 return $ret;
1254 sub apply_patch_for_checkout_commit {
1255 my $reverse = shift;
1256 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1257 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1259 if ($applies_worktree && $applies_index) {
1260 run_git_apply 'apply '.$reverse.' --cached', @_;
1261 run_git_apply 'apply '.$reverse, @_;
1262 return 1;
1263 } elsif (!$applies_index) {
1264 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1265 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1266 return run_git_apply 'apply '.$reverse, @_;
1267 } else {
1268 print colored $error_color, __("Nothing was applied.\n");
1269 return 0;
1271 } else {
1272 print STDERR @_;
1273 return 0;
1277 sub patch_update_cmd {
1278 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1279 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1280 for grep { $_->{UNMERGED} } @all_mods;
1281 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1283 my @mods = grep { !($_->{BINARY}) } @all_mods;
1284 my @them;
1286 if (!@mods) {
1287 if (@all_mods) {
1288 print STDERR __("Only binary files changed.\n");
1289 } else {
1290 print STDERR __("No changes.\n");
1292 return 0;
1294 if ($patch_mode_only) {
1295 @them = @mods;
1297 else {
1298 @them = list_and_choose({ PROMPT => __('Patch update'),
1299 HEADER => $status_head, },
1300 @mods);
1302 for (@them) {
1303 return 0 if patch_update_file($_->{VALUE});
1307 # Generate a one line summary of a hunk.
1308 sub summarize_hunk {
1309 my $rhunk = shift;
1310 my $summary = $rhunk->{TEXT}[0];
1312 # Keep the line numbers, discard extra context.
1313 $summary =~ s/@@(.*?)@@.*/$1 /s;
1314 $summary .= " " x (20 - length $summary);
1316 # Add some user context.
1317 for my $line (@{$rhunk->{TEXT}}) {
1318 if ($line =~ m/^[+-].*\w/) {
1319 $summary .= $line;
1320 last;
1324 chomp $summary;
1325 return substr($summary, 0, 80) . "\n";
1329 # Print a one-line summary of each hunk in the array ref in
1330 # the first argument, starting with the index in the 2nd.
1331 sub display_hunks {
1332 my ($hunks, $i) = @_;
1333 my $ctr = 0;
1334 $i ||= 0;
1335 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1336 my $status = " ";
1337 if (defined $hunks->[$i]{USE}) {
1338 $status = $hunks->[$i]{USE} ? "+" : "-";
1340 printf "%s%2d: %s",
1341 $status,
1342 $i + 1,
1343 summarize_hunk($hunks->[$i]);
1345 return $i;
1348 my %patch_update_prompt_modes = (
1349 stage => {
1350 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1351 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1352 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1354 stash => {
1355 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1356 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1357 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1359 reset_head => {
1360 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1361 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1362 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1364 reset_nothead => {
1365 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1366 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1367 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1369 checkout_index => {
1370 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1371 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1372 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1374 checkout_head => {
1375 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1376 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1377 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1379 checkout_nothead => {
1380 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1381 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1382 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1386 sub patch_update_file {
1387 my $quit = 0;
1388 my ($ix, $num);
1389 my $path = shift;
1390 my ($head, @hunk) = parse_diff($path);
1391 ($head, my $mode, my $deletion) = parse_diff_header($head);
1392 for (@{$head->{DISPLAY}}) {
1393 print;
1396 if (@{$mode->{TEXT}}) {
1397 unshift @hunk, $mode;
1399 if (@{$deletion->{TEXT}}) {
1400 foreach my $hunk (@hunk) {
1401 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1402 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1404 @hunk = ($deletion);
1407 $num = scalar @hunk;
1408 $ix = 0;
1410 while (1) {
1411 my ($prev, $next, $other, $undecided, $i);
1412 $other = '';
1414 if ($num <= $ix) {
1415 $ix = 0;
1417 for ($i = 0; $i < $ix; $i++) {
1418 if (!defined $hunk[$i]{USE}) {
1419 $prev = 1;
1420 $other .= ',k';
1421 last;
1424 if ($ix) {
1425 $other .= ',K';
1427 for ($i = $ix + 1; $i < $num; $i++) {
1428 if (!defined $hunk[$i]{USE}) {
1429 $next = 1;
1430 $other .= ',j';
1431 last;
1434 if ($ix < $num - 1) {
1435 $other .= ',J';
1437 if ($num > 1) {
1438 $other .= ',g';
1440 for ($i = 0; $i < $num; $i++) {
1441 if (!defined $hunk[$i]{USE}) {
1442 $undecided = 1;
1443 last;
1446 last if (!$undecided);
1448 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1449 hunk_splittable($hunk[$ix]{TEXT})) {
1450 $other .= ',s';
1452 if ($hunk[$ix]{TYPE} eq 'hunk') {
1453 $other .= ',e';
1455 for (@{$hunk[$ix]{DISPLAY}}) {
1456 print;
1458 print colored $prompt_color,
1459 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1461 my $line = prompt_single_character;
1462 last unless defined $line;
1463 if ($line) {
1464 if ($line =~ /^y/i) {
1465 $hunk[$ix]{USE} = 1;
1467 elsif ($line =~ /^n/i) {
1468 $hunk[$ix]{USE} = 0;
1470 elsif ($line =~ /^a/i) {
1471 while ($ix < $num) {
1472 if (!defined $hunk[$ix]{USE}) {
1473 $hunk[$ix]{USE} = 1;
1475 $ix++;
1477 next;
1479 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1480 my $response = $1;
1481 my $no = $ix > 10 ? $ix - 10 : 0;
1482 while ($response eq '') {
1483 $no = display_hunks(\@hunk, $no);
1484 if ($no < $num) {
1485 print __("go to which hunk (<ret> to see more)? ");
1486 } else {
1487 print __("go to which hunk? ");
1489 $response = <STDIN>;
1490 if (!defined $response) {
1491 $response = '';
1493 chomp $response;
1495 if ($response !~ /^\s*\d+\s*$/) {
1496 error_msg sprintf(__("Invalid number: '%s'\n"),
1497 $response);
1498 } elsif (0 < $response && $response <= $num) {
1499 $ix = $response - 1;
1500 } else {
1501 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1502 "Sorry, only %d hunks available.\n", $num), $num);
1504 next;
1506 elsif ($line =~ /^d/i) {
1507 while ($ix < $num) {
1508 if (!defined $hunk[$ix]{USE}) {
1509 $hunk[$ix]{USE} = 0;
1511 $ix++;
1513 next;
1515 elsif ($line =~ /^q/i) {
1516 for ($i = 0; $i < $num; $i++) {
1517 if (!defined $hunk[$i]{USE}) {
1518 $hunk[$i]{USE} = 0;
1521 $quit = 1;
1522 last;
1524 elsif ($line =~ m|^/(.*)|) {
1525 my $regex = $1;
1526 if ($1 eq "") {
1527 print colored $prompt_color, __("search for regex? ");
1528 $regex = <STDIN>;
1529 if (defined $regex) {
1530 chomp $regex;
1533 my $search_string;
1534 eval {
1535 $search_string = qr{$regex}m;
1537 if ($@) {
1538 my ($err,$exp) = ($@, $1);
1539 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1540 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1541 next;
1543 my $iy = $ix;
1544 while (1) {
1545 my $text = join ("", @{$hunk[$iy]{TEXT}});
1546 last if ($text =~ $search_string);
1547 $iy++;
1548 $iy = 0 if ($iy >= $num);
1549 if ($ix == $iy) {
1550 error_msg __("No hunk matches the given pattern\n");
1551 last;
1554 $ix = $iy;
1555 next;
1557 elsif ($line =~ /^K/) {
1558 if ($other =~ /K/) {
1559 $ix--;
1561 else {
1562 error_msg __("No previous hunk\n");
1564 next;
1566 elsif ($line =~ /^J/) {
1567 if ($other =~ /J/) {
1568 $ix++;
1570 else {
1571 error_msg __("No next hunk\n");
1573 next;
1575 elsif ($line =~ /^k/) {
1576 if ($other =~ /k/) {
1577 while (1) {
1578 $ix--;
1579 last if (!$ix ||
1580 !defined $hunk[$ix]{USE});
1583 else {
1584 error_msg __("No previous hunk\n");
1586 next;
1588 elsif ($line =~ /^j/) {
1589 if ($other !~ /j/) {
1590 error_msg __("No next hunk\n");
1591 next;
1594 elsif ($other =~ /s/ && $line =~ /^s/) {
1595 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1596 if (1 < @split) {
1597 print colored $header_color, sprintf(
1598 __n("Split into %d hunk.\n",
1599 "Split into %d hunks.\n",
1600 scalar(@split)), scalar(@split));
1602 splice (@hunk, $ix, 1, @split);
1603 $num = scalar @hunk;
1604 next;
1606 elsif ($other =~ /e/ && $line =~ /^e/) {
1607 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1608 if (defined $newhunk) {
1609 splice @hunk, $ix, 1, $newhunk;
1612 else {
1613 help_patch_cmd($other);
1614 next;
1616 # soft increment
1617 while (1) {
1618 $ix++;
1619 last if ($ix >= $num ||
1620 !defined $hunk[$ix]{USE});
1625 @hunk = coalesce_overlapping_hunks(@hunk);
1627 my $n_lofs = 0;
1628 my @result = ();
1629 for (@hunk) {
1630 if ($_->{USE}) {
1631 push @result, @{$_->{TEXT}};
1635 if (@result) {
1636 my @patch = reassemble_patch($head->{TEXT}, @result);
1637 my $apply_routine = $patch_mode_flavour{APPLY};
1638 &$apply_routine(@patch);
1639 refresh();
1642 print "\n";
1643 return $quit;
1646 sub diff_cmd {
1647 my @mods = list_modified('index-only');
1648 @mods = grep { !($_->{BINARY}) } @mods;
1649 return if (!@mods);
1650 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1651 IMMEDIATE => 1,
1652 HEADER => $status_head, },
1653 @mods);
1654 return if (!@them);
1655 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1656 system(qw(git diff -p --cached), $reference, '--',
1657 map { $_->{VALUE} } @them);
1660 sub quit_cmd {
1661 print __("Bye.\n");
1662 exit(0);
1665 sub help_cmd {
1666 # TRANSLATORS: please do not translate the command names
1667 # 'status', 'update', 'revert', etc.
1668 print colored $help_color, __ <<'EOF' ;
1669 status - show paths with changes
1670 update - add working tree state to the staged set of changes
1671 revert - revert staged set of changes back to the HEAD version
1672 patch - pick hunks and update selectively
1673 diff - view diff between HEAD and index
1674 add untracked - add contents of untracked files to the staged set of changes
1678 sub process_args {
1679 return unless @ARGV;
1680 my $arg = shift @ARGV;
1681 if ($arg =~ /--patch(?:=(.*))?/) {
1682 if (defined $1) {
1683 if ($1 eq 'reset') {
1684 $patch_mode = 'reset_head';
1685 $patch_mode_revision = 'HEAD';
1686 $arg = shift @ARGV or die __("missing --");
1687 if ($arg ne '--') {
1688 $patch_mode_revision = $arg;
1689 $patch_mode = ($arg eq 'HEAD' ?
1690 'reset_head' : 'reset_nothead');
1691 $arg = shift @ARGV or die __("missing --");
1693 } elsif ($1 eq 'checkout') {
1694 $arg = shift @ARGV or die __("missing --");
1695 if ($arg eq '--') {
1696 $patch_mode = 'checkout_index';
1697 } else {
1698 $patch_mode_revision = $arg;
1699 $patch_mode = ($arg eq 'HEAD' ?
1700 'checkout_head' : 'checkout_nothead');
1701 $arg = shift @ARGV or die __("missing --");
1703 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1704 $patch_mode = $1;
1705 $arg = shift @ARGV or die __("missing --");
1706 } else {
1707 die sprintf(__("unknown --patch mode: %s"), $1);
1709 } else {
1710 $patch_mode = 'stage';
1711 $arg = shift @ARGV or die __("missing --");
1713 die sprintf(__("invalid argument %s, expecting --"),
1714 $arg) unless $arg eq "--";
1715 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1716 $patch_mode_only = 1;
1718 elsif ($arg ne "--") {
1719 die sprintf(__("invalid argument %s, expecting --"), $arg);
1723 sub main_loop {
1724 my @cmd = ([ 'status', \&status_cmd, ],
1725 [ 'update', \&update_cmd, ],
1726 [ 'revert', \&revert_cmd, ],
1727 [ 'add untracked', \&add_untracked_cmd, ],
1728 [ 'patch', \&patch_update_cmd, ],
1729 [ 'diff', \&diff_cmd, ],
1730 [ 'quit', \&quit_cmd, ],
1731 [ 'help', \&help_cmd, ],
1733 while (1) {
1734 my ($it) = list_and_choose({ PROMPT => __('What now'),
1735 SINGLETON => 1,
1736 LIST_FLAT => 4,
1737 HEADER => __('*** Commands ***'),
1738 ON_EOF => \&quit_cmd,
1739 IMMEDIATE => 1 }, @cmd);
1740 if ($it) {
1741 eval {
1742 $it->[1]->();
1744 if ($@) {
1745 print "$@";
1751 process_args();
1752 refresh();
1753 if ($patch_mode_only) {
1754 patch_update_cmd();
1756 else {
1757 status_cmd();
1758 main_loop();