doc: describe git svn init --ignore-refs
[git/git-svn.git] / git-add--interactive.perl
blob79d675b5b02ee73a025f86acc6cc29e530ee8db6
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_filter = $repo->config('interactive.difffilter');
51 my $use_readkey = 0;
52 my $use_termcap = 0;
53 my %term_escapes;
55 sub ReadMode;
56 sub ReadKey;
57 if ($repo->config_bool("interactive.singlekey")) {
58 eval {
59 require Term::ReadKey;
60 Term::ReadKey->import;
61 $use_readkey = 1;
63 if (!$use_readkey) {
64 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
66 eval {
67 require Term::Cap;
68 my $termcap = Term::Cap->Tgetent;
69 foreach (values %$termcap) {
70 $term_escapes{$_} = 1 if /^\e/;
72 $use_termcap = 1;
76 sub colored {
77 my $color = shift;
78 my $string = join("", @_);
80 if (defined $color) {
81 # Put a color code at the beginning of each line, a reset at the end
82 # color after newlines that are not at the end of the string
83 $string =~ s/(\n+)(.)/$1$color$2/g;
84 # reset before newlines
85 $string =~ s/(\n+)/$normal_color$1/g;
86 # codes at beginning and end (if necessary):
87 $string =~ s/^/$color/;
88 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
90 return $string;
93 # command line options
94 my $patch_mode_only;
95 my $patch_mode;
96 my $patch_mode_revision;
98 sub apply_patch;
99 sub apply_patch_for_checkout_commit;
100 sub apply_patch_for_stash;
102 my %patch_modes = (
103 'stage' => {
104 DIFF => 'diff-files -p',
105 APPLY => sub { apply_patch 'apply --cached', @_; },
106 APPLY_CHECK => 'apply --cached',
107 FILTER => 'file-only',
108 IS_REVERSE => 0,
110 'stash' => {
111 DIFF => 'diff-index -p HEAD',
112 APPLY => sub { apply_patch 'apply --cached', @_; },
113 APPLY_CHECK => 'apply --cached',
114 FILTER => undef,
115 IS_REVERSE => 0,
117 'reset_head' => {
118 DIFF => 'diff-index -p --cached',
119 APPLY => sub { apply_patch 'apply -R --cached', @_; },
120 APPLY_CHECK => 'apply -R --cached',
121 FILTER => 'index-only',
122 IS_REVERSE => 1,
124 'reset_nothead' => {
125 DIFF => 'diff-index -R -p --cached',
126 APPLY => sub { apply_patch 'apply --cached', @_; },
127 APPLY_CHECK => 'apply --cached',
128 FILTER => 'index-only',
129 IS_REVERSE => 0,
131 'checkout_index' => {
132 DIFF => 'diff-files -p',
133 APPLY => sub { apply_patch 'apply -R', @_; },
134 APPLY_CHECK => 'apply -R',
135 FILTER => 'file-only',
136 IS_REVERSE => 1,
138 'checkout_head' => {
139 DIFF => 'diff-index -p',
140 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
141 APPLY_CHECK => 'apply -R',
142 FILTER => undef,
143 IS_REVERSE => 1,
145 'checkout_nothead' => {
146 DIFF => 'diff-index -R -p',
147 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
148 APPLY_CHECK => 'apply',
149 FILTER => undef,
150 IS_REVERSE => 0,
154 $patch_mode = 'stage';
155 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
157 sub run_cmd_pipe {
158 if ($^O eq 'MSWin32') {
159 my @invalid = grep {m/[":*]/} @_;
160 die "$^O does not support: @invalid\n" if @invalid;
161 my @args = map { m/ /o ? "\"$_\"": $_ } @_;
162 return qx{@args};
163 } else {
164 my $fh = undef;
165 open($fh, '-|', @_) or die;
166 return <$fh>;
170 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
172 if (!defined $GIT_DIR) {
173 exit(1); # rev-parse would have already said "not a git repo"
175 chomp($GIT_DIR);
177 my %cquote_map = (
178 "b" => chr(8),
179 "t" => chr(9),
180 "n" => chr(10),
181 "v" => chr(11),
182 "f" => chr(12),
183 "r" => chr(13),
184 "\\" => "\\",
185 "\042" => "\042",
188 sub unquote_path {
189 local ($_) = @_;
190 my ($retval, $remainder);
191 if (!/^\042(.*)\042$/) {
192 return $_;
194 ($_, $retval) = ($1, "");
195 while (/^([^\\]*)\\(.*)$/) {
196 $remainder = $2;
197 $retval .= $1;
198 for ($remainder) {
199 if (/^([0-3][0-7][0-7])(.*)$/) {
200 $retval .= chr(oct($1));
201 $_ = $2;
202 last;
204 if (/^([\\\042btnvfr])(.*)$/) {
205 $retval .= $cquote_map{$1};
206 $_ = $2;
207 last;
209 # This is malformed -- just return it as-is for now.
210 return $_[0];
212 $_ = $remainder;
214 $retval .= $_;
215 return $retval;
218 sub refresh {
219 my $fh;
220 open $fh, 'git update-index --refresh |'
221 or die;
222 while (<$fh>) {
223 ;# ignore 'needs update'
225 close $fh;
228 sub list_untracked {
229 map {
230 chomp $_;
231 unquote_path($_);
233 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
236 # TRANSLATORS: you can adjust this to align "git add -i" status menu
237 my $status_fmt = __('%12s %12s %s');
238 my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
241 my $initial;
242 sub is_initial_commit {
243 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
244 unless defined $initial;
245 return $initial;
249 sub get_empty_tree {
250 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
253 sub get_diff_reference {
254 my $ref = shift;
255 if (defined $ref and $ref ne 'HEAD') {
256 return $ref;
257 } elsif (is_initial_commit()) {
258 return get_empty_tree();
259 } else {
260 return 'HEAD';
264 # Returns list of hashes, contents of each of which are:
265 # VALUE: pathname
266 # BINARY: is a binary path
267 # INDEX: is index different from HEAD?
268 # FILE: is file different from index?
269 # INDEX_ADDDEL: is it add/delete between HEAD and index?
270 # FILE_ADDDEL: is it add/delete between index and file?
271 # UNMERGED: is the path unmerged
273 sub list_modified {
274 my ($only) = @_;
275 my (%data, @return);
276 my ($add, $del, $adddel, $file);
278 my $reference = get_diff_reference($patch_mode_revision);
279 for (run_cmd_pipe(qw(git diff-index --cached
280 --numstat --summary), $reference,
281 '--', @ARGV)) {
282 if (($add, $del, $file) =
283 /^([-\d]+) ([-\d]+) (.*)/) {
284 my ($change, $bin);
285 $file = unquote_path($file);
286 if ($add eq '-' && $del eq '-') {
287 $change = __('binary');
288 $bin = 1;
290 else {
291 $change = "+$add/-$del";
293 $data{$file} = {
294 INDEX => $change,
295 BINARY => $bin,
296 FILE => __('nothing'),
299 elsif (($adddel, $file) =
300 /^ (create|delete) mode [0-7]+ (.*)$/) {
301 $file = unquote_path($file);
302 $data{$file}{INDEX_ADDDEL} = $adddel;
306 for (run_cmd_pipe(qw(git diff-files --numstat --summary --raw --), @ARGV)) {
307 if (($add, $del, $file) =
308 /^([-\d]+) ([-\d]+) (.*)/) {
309 $file = unquote_path($file);
310 my ($change, $bin);
311 if ($add eq '-' && $del eq '-') {
312 $change = __('binary');
313 $bin = 1;
315 else {
316 $change = "+$add/-$del";
318 $data{$file}{FILE} = $change;
319 if ($bin) {
320 $data{$file}{BINARY} = 1;
323 elsif (($adddel, $file) =
324 /^ (create|delete) mode [0-7]+ (.*)$/) {
325 $file = unquote_path($file);
326 $data{$file}{FILE_ADDDEL} = $adddel;
328 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
329 $file = unquote_path($2);
330 if (!exists $data{$file}) {
331 $data{$file} = +{
332 INDEX => __('unchanged'),
333 BINARY => 0,
336 if ($1 eq 'U') {
337 $data{$file}{UNMERGED} = 1;
342 for (sort keys %data) {
343 my $it = $data{$_};
345 if ($only) {
346 if ($only eq 'index-only') {
347 next if ($it->{INDEX} eq __('unchanged'));
349 if ($only eq 'file-only') {
350 next if ($it->{FILE} eq __('nothing'));
353 push @return, +{
354 VALUE => $_,
355 %$it,
358 return @return;
361 sub find_unique {
362 my ($string, @stuff) = @_;
363 my $found = undef;
364 for (my $i = 0; $i < @stuff; $i++) {
365 my $it = $stuff[$i];
366 my $hit = undef;
367 if (ref $it) {
368 if ((ref $it) eq 'ARRAY') {
369 $it = $it->[0];
371 else {
372 $it = $it->{VALUE};
375 eval {
376 if ($it =~ /^$string/) {
377 $hit = 1;
380 if (defined $hit && defined $found) {
381 return undef;
383 if ($hit) {
384 $found = $i + 1;
387 return $found;
390 # inserts string into trie and updates count for each character
391 sub update_trie {
392 my ($trie, $string) = @_;
393 foreach (split //, $string) {
394 $trie = $trie->{$_} ||= {COUNT => 0};
395 $trie->{COUNT}++;
399 # returns an array of tuples (prefix, remainder)
400 sub find_unique_prefixes {
401 my @stuff = @_;
402 my @return = ();
404 # any single prefix exceeding the soft limit is omitted
405 # if any prefix exceeds the hard limit all are omitted
406 # 0 indicates no limit
407 my $soft_limit = 0;
408 my $hard_limit = 3;
410 # build a trie modelling all possible options
411 my %trie;
412 foreach my $print (@stuff) {
413 if ((ref $print) eq 'ARRAY') {
414 $print = $print->[0];
416 elsif ((ref $print) eq 'HASH') {
417 $print = $print->{VALUE};
419 update_trie(\%trie, $print);
420 push @return, $print;
423 # use the trie to find the unique prefixes
424 for (my $i = 0; $i < @return; $i++) {
425 my $ret = $return[$i];
426 my @letters = split //, $ret;
427 my %search = %trie;
428 my ($prefix, $remainder);
429 my $j;
430 for ($j = 0; $j < @letters; $j++) {
431 my $letter = $letters[$j];
432 if ($search{$letter}{COUNT} == 1) {
433 $prefix = substr $ret, 0, $j + 1;
434 $remainder = substr $ret, $j + 1;
435 last;
437 else {
438 my $prefix = substr $ret, 0, $j;
439 return ()
440 if ($hard_limit && $j + 1 > $hard_limit);
442 %search = %{$search{$letter}};
444 if (ord($letters[0]) > 127 ||
445 ($soft_limit && $j + 1 > $soft_limit)) {
446 $prefix = undef;
447 $remainder = $ret;
449 $return[$i] = [$prefix, $remainder];
451 return @return;
454 # filters out prefixes which have special meaning to list_and_choose()
455 sub is_valid_prefix {
456 my $prefix = shift;
457 return (defined $prefix) &&
458 !($prefix =~ /[\s,]/) && # separators
459 !($prefix =~ /^-/) && # deselection
460 !($prefix =~ /^\d+/) && # selection
461 ($prefix ne '*') && # "all" wildcard
462 ($prefix ne '?'); # prompt help
465 # given a prefix/remainder tuple return a string with the prefix highlighted
466 # for now use square brackets; later might use ANSI colors (underline, bold)
467 sub highlight_prefix {
468 my $prefix = shift;
469 my $remainder = shift;
471 if (!defined $prefix) {
472 return $remainder;
475 if (!is_valid_prefix($prefix)) {
476 return "$prefix$remainder";
479 if (!$menu_use_color) {
480 return "[$prefix]$remainder";
483 return "$prompt_color$prefix$normal_color$remainder";
486 sub error_msg {
487 print STDERR colored $error_color, @_;
490 sub list_and_choose {
491 my ($opts, @stuff) = @_;
492 my (@chosen, @return);
493 if (!@stuff) {
494 return @return;
496 my $i;
497 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
499 TOPLOOP:
500 while (1) {
501 my $last_lf = 0;
503 if ($opts->{HEADER}) {
504 if (!$opts->{LIST_FLAT}) {
505 print " ";
507 print colored $header_color, "$opts->{HEADER}\n";
509 for ($i = 0; $i < @stuff; $i++) {
510 my $chosen = $chosen[$i] ? '*' : ' ';
511 my $print = $stuff[$i];
512 my $ref = ref $print;
513 my $highlighted = highlight_prefix(@{$prefixes[$i]})
514 if @prefixes;
515 if ($ref eq 'ARRAY') {
516 $print = $highlighted || $print->[0];
518 elsif ($ref eq 'HASH') {
519 my $value = $highlighted || $print->{VALUE};
520 $print = sprintf($status_fmt,
521 $print->{INDEX},
522 $print->{FILE},
523 $value);
525 else {
526 $print = $highlighted || $print;
528 printf("%s%2d: %s", $chosen, $i+1, $print);
529 if (($opts->{LIST_FLAT}) &&
530 (($i + 1) % ($opts->{LIST_FLAT}))) {
531 print "\t";
532 $last_lf = 0;
534 else {
535 print "\n";
536 $last_lf = 1;
539 if (!$last_lf) {
540 print "\n";
543 return if ($opts->{LIST_ONLY});
545 print colored $prompt_color, $opts->{PROMPT};
546 if ($opts->{SINGLETON}) {
547 print "> ";
549 else {
550 print ">> ";
552 my $line = <STDIN>;
553 if (!$line) {
554 print "\n";
555 $opts->{ON_EOF}->() if $opts->{ON_EOF};
556 last;
558 chomp $line;
559 last if $line eq '';
560 if ($line eq '?') {
561 $opts->{SINGLETON} ?
562 singleton_prompt_help_cmd() :
563 prompt_help_cmd();
564 next TOPLOOP;
566 for my $choice (split(/[\s,]+/, $line)) {
567 my $choose = 1;
568 my ($bottom, $top);
570 # Input that begins with '-'; unchoose
571 if ($choice =~ s/^-//) {
572 $choose = 0;
574 # A range can be specified like 5-7 or 5-.
575 if ($choice =~ /^(\d+)-(\d*)$/) {
576 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
578 elsif ($choice =~ /^\d+$/) {
579 $bottom = $top = $choice;
581 elsif ($choice eq '*') {
582 $bottom = 1;
583 $top = 1 + @stuff;
585 else {
586 $bottom = $top = find_unique($choice, @stuff);
587 if (!defined $bottom) {
588 error_msg sprintf(__("Huh (%s)?\n"), $choice);
589 next TOPLOOP;
592 if ($opts->{SINGLETON} && $bottom != $top) {
593 error_msg sprintf(__("Huh (%s)?\n"), $choice);
594 next TOPLOOP;
596 for ($i = $bottom-1; $i <= $top-1; $i++) {
597 next if (@stuff <= $i || $i < 0);
598 $chosen[$i] = $choose;
601 last if ($opts->{IMMEDIATE} || $line eq '*');
603 for ($i = 0; $i < @stuff; $i++) {
604 if ($chosen[$i]) {
605 push @return, $stuff[$i];
608 return @return;
611 sub singleton_prompt_help_cmd {
612 print colored $help_color, __ <<'EOF' ;
613 Prompt help:
614 1 - select a numbered item
615 foo - select item based on unique prefix
616 - (empty) select nothing
620 sub prompt_help_cmd {
621 print colored $help_color, __ <<'EOF' ;
622 Prompt help:
623 1 - select a single item
624 3-5 - select a range of items
625 2-3,6-9 - select multiple ranges
626 foo - select item based on unique prefix
627 -... - unselect specified items
628 * - choose all items
629 - (empty) finish selecting
633 sub status_cmd {
634 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
635 list_modified());
636 print "\n";
639 sub say_n_paths {
640 my $did = shift @_;
641 my $cnt = scalar @_;
642 if ($did eq 'added') {
643 printf(__n("added %d path\n", "added %d paths\n",
644 $cnt), $cnt);
645 } elsif ($did eq 'updated') {
646 printf(__n("updated %d path\n", "updated %d paths\n",
647 $cnt), $cnt);
648 } elsif ($did eq 'reverted') {
649 printf(__n("reverted %d path\n", "reverted %d paths\n",
650 $cnt), $cnt);
651 } else {
652 printf(__n("touched %d path\n", "touched %d paths\n",
653 $cnt), $cnt);
657 sub update_cmd {
658 my @mods = list_modified('file-only');
659 return if (!@mods);
661 my @update = list_and_choose({ PROMPT => __('Update'),
662 HEADER => $status_head, },
663 @mods);
664 if (@update) {
665 system(qw(git update-index --add --remove --),
666 map { $_->{VALUE} } @update);
667 say_n_paths('updated', @update);
669 print "\n";
672 sub revert_cmd {
673 my @update = list_and_choose({ PROMPT => __('Revert'),
674 HEADER => $status_head, },
675 list_modified());
676 if (@update) {
677 if (is_initial_commit()) {
678 system(qw(git rm --cached),
679 map { $_->{VALUE} } @update);
681 else {
682 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
683 map { $_->{VALUE} } @update);
684 my $fh;
685 open $fh, '| git update-index --index-info'
686 or die;
687 for (@lines) {
688 print $fh $_;
690 close($fh);
691 for (@update) {
692 if ($_->{INDEX_ADDDEL} &&
693 $_->{INDEX_ADDDEL} eq 'create') {
694 system(qw(git update-index --force-remove --),
695 $_->{VALUE});
696 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
700 refresh();
701 say_n_paths('reverted', @update);
703 print "\n";
706 sub add_untracked_cmd {
707 my @add = list_and_choose({ PROMPT => __('Add untracked') },
708 list_untracked());
709 if (@add) {
710 system(qw(git update-index --add --), @add);
711 say_n_paths('added', @add);
712 } else {
713 print __("No untracked files.\n");
715 print "\n";
718 sub run_git_apply {
719 my $cmd = shift;
720 my $fh;
721 open $fh, '| git ' . $cmd . " --recount --allow-overlap";
722 print $fh @_;
723 return close $fh;
726 sub parse_diff {
727 my ($path) = @_;
728 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
729 if (defined $diff_algorithm) {
730 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
732 if (defined $patch_mode_revision) {
733 push @diff_cmd, get_diff_reference($patch_mode_revision);
735 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
736 my @colored = ();
737 if ($diff_use_color) {
738 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
739 if (defined $diff_filter) {
740 # quotemeta is overkill, but sufficient for shell-quoting
741 my $diff = join(' ', map { quotemeta } @display_cmd);
742 @display_cmd = ("$diff | $diff_filter");
745 @colored = run_cmd_pipe(@display_cmd);
747 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
749 for (my $i = 0; $i < @diff; $i++) {
750 if ($diff[$i] =~ /^@@ /) {
751 push @hunk, { TEXT => [], DISPLAY => [],
752 TYPE => 'hunk' };
754 push @{$hunk[-1]{TEXT}}, $diff[$i];
755 push @{$hunk[-1]{DISPLAY}},
756 (@colored ? $colored[$i] : $diff[$i]);
758 return @hunk;
761 sub parse_diff_header {
762 my $src = shift;
764 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
765 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
766 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
768 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
769 my $dest =
770 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
771 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
772 $head;
773 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
774 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
776 return ($head, $mode, $deletion);
779 sub hunk_splittable {
780 my ($text) = @_;
782 my @s = split_hunk($text);
783 return (1 < @s);
786 sub parse_hunk_header {
787 my ($line) = @_;
788 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
789 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
790 $o_cnt = 1 unless defined $o_cnt;
791 $n_cnt = 1 unless defined $n_cnt;
792 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
795 sub split_hunk {
796 my ($text, $display) = @_;
797 my @split = ();
798 if (!defined $display) {
799 $display = $text;
801 # If there are context lines in the middle of a hunk,
802 # it can be split, but we would need to take care of
803 # overlaps later.
805 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
806 my $hunk_start = 1;
808 OUTER:
809 while (1) {
810 my $next_hunk_start = undef;
811 my $i = $hunk_start - 1;
812 my $this = +{
813 TEXT => [],
814 DISPLAY => [],
815 TYPE => 'hunk',
816 OLD => $o_ofs,
817 NEW => $n_ofs,
818 OCNT => 0,
819 NCNT => 0,
820 ADDDEL => 0,
821 POSTCTX => 0,
822 USE => undef,
825 while (++$i < @$text) {
826 my $line = $text->[$i];
827 my $display = $display->[$i];
828 if ($line =~ /^ /) {
829 if ($this->{ADDDEL} &&
830 !defined $next_hunk_start) {
831 # We have seen leading context and
832 # adds/dels and then here is another
833 # context, which is trailing for this
834 # split hunk and leading for the next
835 # one.
836 $next_hunk_start = $i;
838 push @{$this->{TEXT}}, $line;
839 push @{$this->{DISPLAY}}, $display;
840 $this->{OCNT}++;
841 $this->{NCNT}++;
842 if (defined $next_hunk_start) {
843 $this->{POSTCTX}++;
845 next;
848 # add/del
849 if (defined $next_hunk_start) {
850 # We are done with the current hunk and
851 # this is the first real change for the
852 # next split one.
853 $hunk_start = $next_hunk_start;
854 $o_ofs = $this->{OLD} + $this->{OCNT};
855 $n_ofs = $this->{NEW} + $this->{NCNT};
856 $o_ofs -= $this->{POSTCTX};
857 $n_ofs -= $this->{POSTCTX};
858 push @split, $this;
859 redo OUTER;
861 push @{$this->{TEXT}}, $line;
862 push @{$this->{DISPLAY}}, $display;
863 $this->{ADDDEL}++;
864 if ($line =~ /^-/) {
865 $this->{OCNT}++;
867 else {
868 $this->{NCNT}++;
872 push @split, $this;
873 last;
876 for my $hunk (@split) {
877 $o_ofs = $hunk->{OLD};
878 $n_ofs = $hunk->{NEW};
879 my $o_cnt = $hunk->{OCNT};
880 my $n_cnt = $hunk->{NCNT};
882 my $head = ("@@ -$o_ofs" .
883 (($o_cnt != 1) ? ",$o_cnt" : '') .
884 " +$n_ofs" .
885 (($n_cnt != 1) ? ",$n_cnt" : '') .
886 " @@\n");
887 my $display_head = $head;
888 unshift @{$hunk->{TEXT}}, $head;
889 if ($diff_use_color) {
890 $display_head = colored($fraginfo_color, $head);
892 unshift @{$hunk->{DISPLAY}}, $display_head;
894 return @split;
897 sub find_last_o_ctx {
898 my ($it) = @_;
899 my $text = $it->{TEXT};
900 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
901 my $i = @{$text};
902 my $last_o_ctx = $o_ofs + $o_cnt;
903 while (0 < --$i) {
904 my $line = $text->[$i];
905 if ($line =~ /^ /) {
906 $last_o_ctx--;
907 next;
909 last;
911 return $last_o_ctx;
914 sub merge_hunk {
915 my ($prev, $this) = @_;
916 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
917 parse_hunk_header($prev->{TEXT}[0]);
918 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
919 parse_hunk_header($this->{TEXT}[0]);
921 my (@line, $i, $ofs, $o_cnt, $n_cnt);
922 $ofs = $o0_ofs;
923 $o_cnt = $n_cnt = 0;
924 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
925 my $line = $prev->{TEXT}[$i];
926 if ($line =~ /^\+/) {
927 $n_cnt++;
928 push @line, $line;
929 next;
932 last if ($o1_ofs <= $ofs);
934 $o_cnt++;
935 $ofs++;
936 if ($line =~ /^ /) {
937 $n_cnt++;
939 push @line, $line;
942 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
943 my $line = $this->{TEXT}[$i];
944 if ($line =~ /^\+/) {
945 $n_cnt++;
946 push @line, $line;
947 next;
949 $ofs++;
950 $o_cnt++;
951 if ($line =~ /^ /) {
952 $n_cnt++;
954 push @line, $line;
956 my $head = ("@@ -$o0_ofs" .
957 (($o_cnt != 1) ? ",$o_cnt" : '') .
958 " +$n0_ofs" .
959 (($n_cnt != 1) ? ",$n_cnt" : '') .
960 " @@\n");
961 @{$prev->{TEXT}} = ($head, @line);
964 sub coalesce_overlapping_hunks {
965 my (@in) = @_;
966 my @out = ();
968 my ($last_o_ctx, $last_was_dirty);
970 for (grep { $_->{USE} } @in) {
971 if ($_->{TYPE} ne 'hunk') {
972 push @out, $_;
973 next;
975 my $text = $_->{TEXT};
976 my ($o_ofs) = parse_hunk_header($text->[0]);
977 if (defined $last_o_ctx &&
978 $o_ofs <= $last_o_ctx &&
979 !$_->{DIRTY} &&
980 !$last_was_dirty) {
981 merge_hunk($out[-1], $_);
983 else {
984 push @out, $_;
986 $last_o_ctx = find_last_o_ctx($out[-1]);
987 $last_was_dirty = $_->{DIRTY};
989 return @out;
992 sub reassemble_patch {
993 my $head = shift;
994 my @patch;
996 # Include everything in the header except the beginning of the diff.
997 push @patch, (grep { !/^[-+]{3}/ } @$head);
999 # Then include any headers from the hunk lines, which must
1000 # come before any actual hunk.
1001 while (@_ && $_[0] !~ /^@/) {
1002 push @patch, shift;
1005 # Then begin the diff.
1006 push @patch, grep { /^[-+]{3}/ } @$head;
1008 # And then the actual hunks.
1009 push @patch, @_;
1011 return @patch;
1014 sub color_diff {
1015 return map {
1016 colored((/^@/ ? $fraginfo_color :
1017 /^\+/ ? $diff_new_color :
1018 /^-/ ? $diff_old_color :
1019 $diff_plain_color),
1020 $_);
1021 } @_;
1024 my %edit_hunk_manually_modes = (
1025 stage => N__(
1026 "If the patch applies cleanly, the edited hunk will immediately be
1027 marked for staging."),
1028 stash => N__(
1029 "If the patch applies cleanly, the edited hunk will immediately be
1030 marked for stashing."),
1031 reset_head => N__(
1032 "If the patch applies cleanly, the edited hunk will immediately be
1033 marked for unstaging."),
1034 reset_nothead => N__(
1035 "If the patch applies cleanly, the edited hunk will immediately be
1036 marked for applying."),
1037 checkout_index => N__(
1038 "If the patch applies cleanly, the edited hunk will immediately be
1039 marked for discarding."),
1040 checkout_head => N__(
1041 "If the patch applies cleanly, the edited hunk will immediately be
1042 marked for discarding."),
1043 checkout_nothead => N__(
1044 "If the patch applies cleanly, the edited hunk will immediately be
1045 marked for applying."),
1048 sub edit_hunk_manually {
1049 my ($oldtext) = @_;
1051 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1052 my $fh;
1053 open $fh, '>', $hunkfile
1054 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1055 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1056 print $fh @$oldtext;
1057 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1058 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1059 my $comment_line_char = Git::get_comment_line_char;
1060 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1062 To remove '%s' lines, make them ' ' lines (context).
1063 To remove '%s' lines, delete them.
1064 Lines starting with %s will be removed.
1066 __($edit_hunk_manually_modes{$patch_mode}),
1067 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1068 __ <<EOF2 ;
1069 If it does not apply cleanly, you will be given an opportunity to
1070 edit again. If all lines of the hunk are removed, then the edit is
1071 aborted and the hunk is left unchanged.
1072 EOF2
1073 close $fh;
1075 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1076 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1078 if ($? != 0) {
1079 return undef;
1082 open $fh, '<', $hunkfile
1083 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1084 my @newtext = grep { !/^$comment_line_char/ } <$fh>;
1085 close $fh;
1086 unlink $hunkfile;
1088 # Abort if nothing remains
1089 if (!grep { /\S/ } @newtext) {
1090 return undef;
1093 # Reinsert the first hunk header if the user accidentally deleted it
1094 if ($newtext[0] !~ /^@/) {
1095 unshift @newtext, $oldtext->[0];
1097 return \@newtext;
1100 sub diff_applies {
1101 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1102 map { @{$_->{TEXT}} } @_);
1105 sub _restore_terminal_and_die {
1106 ReadMode 'restore';
1107 print "\n";
1108 exit 1;
1111 sub prompt_single_character {
1112 if ($use_readkey) {
1113 local $SIG{TERM} = \&_restore_terminal_and_die;
1114 local $SIG{INT} = \&_restore_terminal_and_die;
1115 ReadMode 'cbreak';
1116 my $key = ReadKey 0;
1117 ReadMode 'restore';
1118 if ($use_termcap and $key eq "\e") {
1119 while (!defined $term_escapes{$key}) {
1120 my $next = ReadKey 0.5;
1121 last if (!defined $next);
1122 $key .= $next;
1124 $key =~ s/\e/^[/;
1126 print "$key" if defined $key;
1127 print "\n";
1128 return $key;
1129 } else {
1130 return <STDIN>;
1134 sub prompt_yesno {
1135 my ($prompt) = @_;
1136 while (1) {
1137 print colored $prompt_color, $prompt;
1138 my $line = prompt_single_character;
1139 return 0 if $line =~ /^n/i;
1140 return 1 if $line =~ /^y/i;
1144 sub edit_hunk_loop {
1145 my ($head, $hunk, $ix) = @_;
1146 my $text = $hunk->[$ix]->{TEXT};
1148 while (1) {
1149 $text = edit_hunk_manually($text);
1150 if (!defined $text) {
1151 return undef;
1153 my $newhunk = {
1154 TEXT => $text,
1155 TYPE => $hunk->[$ix]->{TYPE},
1156 USE => 1,
1157 DIRTY => 1,
1159 if (diff_applies($head,
1160 @{$hunk}[0..$ix-1],
1161 $newhunk,
1162 @{$hunk}[$ix+1..$#{$hunk}])) {
1163 $newhunk->{DISPLAY} = [color_diff(@{$text})];
1164 return $newhunk;
1166 else {
1167 prompt_yesno(
1168 # TRANSLATORS: do not translate [y/n]
1169 # The program will only accept that input
1170 # at this point.
1171 # Consider translating (saying "no" discards!) as
1172 # (saying "n" for "no" discards!) if the translation
1173 # of the word "no" does not start with n.
1174 __('Your edited hunk does not apply. Edit again '
1175 . '(saying "no" discards!) [y/n]? ')
1176 ) or return undef;
1181 my %help_patch_modes = (
1182 stage => N__(
1183 "y - stage this hunk
1184 n - do not stage this hunk
1185 q - quit; do not stage this hunk or any of the remaining ones
1186 a - stage this hunk and all later hunks in the file
1187 d - do not stage this hunk or any of the later hunks in the file"),
1188 stash => N__(
1189 "y - stash this hunk
1190 n - do not stash this hunk
1191 q - quit; do not stash this hunk or any of the remaining ones
1192 a - stash this hunk and all later hunks in the file
1193 d - do not stash this hunk or any of the later hunks in the file"),
1194 reset_head => N__(
1195 "y - unstage this hunk
1196 n - do not unstage this hunk
1197 q - quit; do not unstage this hunk or any of the remaining ones
1198 a - unstage this hunk and all later hunks in the file
1199 d - do not unstage this hunk or any of the later hunks in the file"),
1200 reset_nothead => N__(
1201 "y - apply this hunk to index
1202 n - do not apply this hunk to index
1203 q - quit; do not apply this hunk or any of the remaining ones
1204 a - apply this hunk and all later hunks in the file
1205 d - do not apply this hunk or any of the later hunks in the file"),
1206 checkout_index => N__(
1207 "y - discard this hunk from worktree
1208 n - do not discard this hunk from worktree
1209 q - quit; do not discard this hunk or any of the remaining ones
1210 a - discard this hunk and all later hunks in the file
1211 d - do not discard this hunk or any of the later hunks in the file"),
1212 checkout_head => N__(
1213 "y - discard this hunk from index and worktree
1214 n - do not discard this hunk from index and worktree
1215 q - quit; do not discard this hunk or any of the remaining ones
1216 a - discard this hunk and all later hunks in the file
1217 d - do not discard this hunk or any of the later hunks in the file"),
1218 checkout_nothead => N__(
1219 "y - apply this hunk to index and worktree
1220 n - do not apply this hunk to index and worktree
1221 q - quit; do not apply this hunk or any of the remaining ones
1222 a - apply this hunk and all later hunks in the file
1223 d - do not apply this hunk or any of the later hunks in the file"),
1226 sub help_patch_cmd {
1227 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1228 g - select a hunk to go to
1229 / - search for a hunk matching the given regex
1230 j - leave this hunk undecided, see next undecided hunk
1231 J - leave this hunk undecided, see next hunk
1232 k - leave this hunk undecided, see previous undecided hunk
1233 K - leave this hunk undecided, see previous hunk
1234 s - split the current hunk into smaller hunks
1235 e - manually edit the current hunk
1236 ? - print help
1240 sub apply_patch {
1241 my $cmd = shift;
1242 my $ret = run_git_apply $cmd, @_;
1243 if (!$ret) {
1244 print STDERR @_;
1246 return $ret;
1249 sub apply_patch_for_checkout_commit {
1250 my $reverse = shift;
1251 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1252 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1254 if ($applies_worktree && $applies_index) {
1255 run_git_apply 'apply '.$reverse.' --cached', @_;
1256 run_git_apply 'apply '.$reverse, @_;
1257 return 1;
1258 } elsif (!$applies_index) {
1259 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1260 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1261 return run_git_apply 'apply '.$reverse, @_;
1262 } else {
1263 print colored $error_color, __("Nothing was applied.\n");
1264 return 0;
1266 } else {
1267 print STDERR @_;
1268 return 0;
1272 sub patch_update_cmd {
1273 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1274 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1275 for grep { $_->{UNMERGED} } @all_mods;
1276 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1278 my @mods = grep { !($_->{BINARY}) } @all_mods;
1279 my @them;
1281 if (!@mods) {
1282 if (@all_mods) {
1283 print STDERR __("Only binary files changed.\n");
1284 } else {
1285 print STDERR __("No changes.\n");
1287 return 0;
1289 if ($patch_mode_only) {
1290 @them = @mods;
1292 else {
1293 @them = list_and_choose({ PROMPT => __('Patch update'),
1294 HEADER => $status_head, },
1295 @mods);
1297 for (@them) {
1298 return 0 if patch_update_file($_->{VALUE});
1302 # Generate a one line summary of a hunk.
1303 sub summarize_hunk {
1304 my $rhunk = shift;
1305 my $summary = $rhunk->{TEXT}[0];
1307 # Keep the line numbers, discard extra context.
1308 $summary =~ s/@@(.*?)@@.*/$1 /s;
1309 $summary .= " " x (20 - length $summary);
1311 # Add some user context.
1312 for my $line (@{$rhunk->{TEXT}}) {
1313 if ($line =~ m/^[+-].*\w/) {
1314 $summary .= $line;
1315 last;
1319 chomp $summary;
1320 return substr($summary, 0, 80) . "\n";
1324 # Print a one-line summary of each hunk in the array ref in
1325 # the first argument, starting with the index in the 2nd.
1326 sub display_hunks {
1327 my ($hunks, $i) = @_;
1328 my $ctr = 0;
1329 $i ||= 0;
1330 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1331 my $status = " ";
1332 if (defined $hunks->[$i]{USE}) {
1333 $status = $hunks->[$i]{USE} ? "+" : "-";
1335 printf "%s%2d: %s",
1336 $status,
1337 $i + 1,
1338 summarize_hunk($hunks->[$i]);
1340 return $i;
1343 my %patch_update_prompt_modes = (
1344 stage => {
1345 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1346 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1347 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1349 stash => {
1350 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1351 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1352 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1354 reset_head => {
1355 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1356 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1357 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1359 reset_nothead => {
1360 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1361 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1362 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1364 checkout_index => {
1365 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1366 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1367 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1369 checkout_head => {
1370 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1371 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1372 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1374 checkout_nothead => {
1375 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1376 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1377 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1381 sub patch_update_file {
1382 my $quit = 0;
1383 my ($ix, $num);
1384 my $path = shift;
1385 my ($head, @hunk) = parse_diff($path);
1386 ($head, my $mode, my $deletion) = parse_diff_header($head);
1387 for (@{$head->{DISPLAY}}) {
1388 print;
1391 if (@{$mode->{TEXT}}) {
1392 unshift @hunk, $mode;
1394 if (@{$deletion->{TEXT}}) {
1395 foreach my $hunk (@hunk) {
1396 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1397 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1399 @hunk = ($deletion);
1402 $num = scalar @hunk;
1403 $ix = 0;
1405 while (1) {
1406 my ($prev, $next, $other, $undecided, $i);
1407 $other = '';
1409 if ($num <= $ix) {
1410 $ix = 0;
1412 for ($i = 0; $i < $ix; $i++) {
1413 if (!defined $hunk[$i]{USE}) {
1414 $prev = 1;
1415 $other .= ',k';
1416 last;
1419 if ($ix) {
1420 $other .= ',K';
1422 for ($i = $ix + 1; $i < $num; $i++) {
1423 if (!defined $hunk[$i]{USE}) {
1424 $next = 1;
1425 $other .= ',j';
1426 last;
1429 if ($ix < $num - 1) {
1430 $other .= ',J';
1432 if ($num > 1) {
1433 $other .= ',g';
1435 for ($i = 0; $i < $num; $i++) {
1436 if (!defined $hunk[$i]{USE}) {
1437 $undecided = 1;
1438 last;
1441 last if (!$undecided);
1443 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1444 hunk_splittable($hunk[$ix]{TEXT})) {
1445 $other .= ',s';
1447 if ($hunk[$ix]{TYPE} eq 'hunk') {
1448 $other .= ',e';
1450 for (@{$hunk[$ix]{DISPLAY}}) {
1451 print;
1453 print colored $prompt_color,
1454 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1456 my $line = prompt_single_character;
1457 last unless defined $line;
1458 if ($line) {
1459 if ($line =~ /^y/i) {
1460 $hunk[$ix]{USE} = 1;
1462 elsif ($line =~ /^n/i) {
1463 $hunk[$ix]{USE} = 0;
1465 elsif ($line =~ /^a/i) {
1466 while ($ix < $num) {
1467 if (!defined $hunk[$ix]{USE}) {
1468 $hunk[$ix]{USE} = 1;
1470 $ix++;
1472 next;
1474 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1475 my $response = $1;
1476 my $no = $ix > 10 ? $ix - 10 : 0;
1477 while ($response eq '') {
1478 $no = display_hunks(\@hunk, $no);
1479 if ($no < $num) {
1480 print __("go to which hunk (<ret> to see more)? ");
1481 } else {
1482 print __("go to which hunk? ");
1484 $response = <STDIN>;
1485 if (!defined $response) {
1486 $response = '';
1488 chomp $response;
1490 if ($response !~ /^\s*\d+\s*$/) {
1491 error_msg sprintf(__("Invalid number: '%s'\n"),
1492 $response);
1493 } elsif (0 < $response && $response <= $num) {
1494 $ix = $response - 1;
1495 } else {
1496 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1497 "Sorry, only %d hunks available.\n", $num), $num);
1499 next;
1501 elsif ($line =~ /^d/i) {
1502 while ($ix < $num) {
1503 if (!defined $hunk[$ix]{USE}) {
1504 $hunk[$ix]{USE} = 0;
1506 $ix++;
1508 next;
1510 elsif ($line =~ /^q/i) {
1511 for ($i = 0; $i < $num; $i++) {
1512 if (!defined $hunk[$i]{USE}) {
1513 $hunk[$i]{USE} = 0;
1516 $quit = 1;
1517 last;
1519 elsif ($line =~ m|^/(.*)|) {
1520 my $regex = $1;
1521 if ($1 eq "") {
1522 print colored $prompt_color, __("search for regex? ");
1523 $regex = <STDIN>;
1524 if (defined $regex) {
1525 chomp $regex;
1528 my $search_string;
1529 eval {
1530 $search_string = qr{$regex}m;
1532 if ($@) {
1533 my ($err,$exp) = ($@, $1);
1534 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1535 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1536 next;
1538 my $iy = $ix;
1539 while (1) {
1540 my $text = join ("", @{$hunk[$iy]{TEXT}});
1541 last if ($text =~ $search_string);
1542 $iy++;
1543 $iy = 0 if ($iy >= $num);
1544 if ($ix == $iy) {
1545 error_msg __("No hunk matches the given pattern\n");
1546 last;
1549 $ix = $iy;
1550 next;
1552 elsif ($line =~ /^K/) {
1553 if ($other =~ /K/) {
1554 $ix--;
1556 else {
1557 error_msg __("No previous hunk\n");
1559 next;
1561 elsif ($line =~ /^J/) {
1562 if ($other =~ /J/) {
1563 $ix++;
1565 else {
1566 error_msg __("No next hunk\n");
1568 next;
1570 elsif ($line =~ /^k/) {
1571 if ($other =~ /k/) {
1572 while (1) {
1573 $ix--;
1574 last if (!$ix ||
1575 !defined $hunk[$ix]{USE});
1578 else {
1579 error_msg __("No previous hunk\n");
1581 next;
1583 elsif ($line =~ /^j/) {
1584 if ($other !~ /j/) {
1585 error_msg __("No next hunk\n");
1586 next;
1589 elsif ($other =~ /s/ && $line =~ /^s/) {
1590 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1591 if (1 < @split) {
1592 print colored $header_color, sprintf(
1593 __n("Split into %d hunk.\n",
1594 "Split into %d hunks.\n",
1595 scalar(@split)), scalar(@split));
1597 splice (@hunk, $ix, 1, @split);
1598 $num = scalar @hunk;
1599 next;
1601 elsif ($other =~ /e/ && $line =~ /^e/) {
1602 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1603 if (defined $newhunk) {
1604 splice @hunk, $ix, 1, $newhunk;
1607 else {
1608 help_patch_cmd($other);
1609 next;
1611 # soft increment
1612 while (1) {
1613 $ix++;
1614 last if ($ix >= $num ||
1615 !defined $hunk[$ix]{USE});
1620 @hunk = coalesce_overlapping_hunks(@hunk);
1622 my $n_lofs = 0;
1623 my @result = ();
1624 for (@hunk) {
1625 if ($_->{USE}) {
1626 push @result, @{$_->{TEXT}};
1630 if (@result) {
1631 my @patch = reassemble_patch($head->{TEXT}, @result);
1632 my $apply_routine = $patch_mode_flavour{APPLY};
1633 &$apply_routine(@patch);
1634 refresh();
1637 print "\n";
1638 return $quit;
1641 sub diff_cmd {
1642 my @mods = list_modified('index-only');
1643 @mods = grep { !($_->{BINARY}) } @mods;
1644 return if (!@mods);
1645 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1646 IMMEDIATE => 1,
1647 HEADER => $status_head, },
1648 @mods);
1649 return if (!@them);
1650 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1651 system(qw(git diff -p --cached), $reference, '--',
1652 map { $_->{VALUE} } @them);
1655 sub quit_cmd {
1656 print __("Bye.\n");
1657 exit(0);
1660 sub help_cmd {
1661 # TRANSLATORS: please do not translate the command names
1662 # 'status', 'update', 'revert', etc.
1663 print colored $help_color, __ <<'EOF' ;
1664 status - show paths with changes
1665 update - add working tree state to the staged set of changes
1666 revert - revert staged set of changes back to the HEAD version
1667 patch - pick hunks and update selectively
1668 diff - view diff between HEAD and index
1669 add untracked - add contents of untracked files to the staged set of changes
1673 sub process_args {
1674 return unless @ARGV;
1675 my $arg = shift @ARGV;
1676 if ($arg =~ /--patch(?:=(.*))?/) {
1677 if (defined $1) {
1678 if ($1 eq 'reset') {
1679 $patch_mode = 'reset_head';
1680 $patch_mode_revision = 'HEAD';
1681 $arg = shift @ARGV or die __("missing --");
1682 if ($arg ne '--') {
1683 $patch_mode_revision = $arg;
1684 $patch_mode = ($arg eq 'HEAD' ?
1685 'reset_head' : 'reset_nothead');
1686 $arg = shift @ARGV or die __("missing --");
1688 } elsif ($1 eq 'checkout') {
1689 $arg = shift @ARGV or die __("missing --");
1690 if ($arg eq '--') {
1691 $patch_mode = 'checkout_index';
1692 } else {
1693 $patch_mode_revision = $arg;
1694 $patch_mode = ($arg eq 'HEAD' ?
1695 'checkout_head' : 'checkout_nothead');
1696 $arg = shift @ARGV or die __("missing --");
1698 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1699 $patch_mode = $1;
1700 $arg = shift @ARGV or die __("missing --");
1701 } else {
1702 die sprintf(__("unknown --patch mode: %s"), $1);
1704 } else {
1705 $patch_mode = 'stage';
1706 $arg = shift @ARGV or die __("missing --");
1708 die sprintf(__("invalid argument %s, expecting --"),
1709 $arg) unless $arg eq "--";
1710 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1711 $patch_mode_only = 1;
1713 elsif ($arg ne "--") {
1714 die sprintf(__("invalid argument %s, expecting --"), $arg);
1718 sub main_loop {
1719 my @cmd = ([ 'status', \&status_cmd, ],
1720 [ 'update', \&update_cmd, ],
1721 [ 'revert', \&revert_cmd, ],
1722 [ 'add untracked', \&add_untracked_cmd, ],
1723 [ 'patch', \&patch_update_cmd, ],
1724 [ 'diff', \&diff_cmd, ],
1725 [ 'quit', \&quit_cmd, ],
1726 [ 'help', \&help_cmd, ],
1728 while (1) {
1729 my ($it) = list_and_choose({ PROMPT => __('What now'),
1730 SINGLETON => 1,
1731 LIST_FLAT => 4,
1732 HEADER => __('*** Commands ***'),
1733 ON_EOF => \&quit_cmd,
1734 IMMEDIATE => 1 }, @cmd);
1735 if ($it) {
1736 eval {
1737 $it->[1]->();
1739 if ($@) {
1740 print "$@";
1746 process_args();
1747 refresh();
1748 if ($patch_mode_only) {
1749 patch_update_cmd();
1751 else {
1752 status_cmd();
1753 main_loop();