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) =
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'),
21 if ($menu_use_color) {
22 my $help_color_spec = ($repo->config('color.interactive.help') or
24 $error_color = $repo->get_color('color.interactive.error',
28 my $diff_use_color = $repo->get_colorbool('color.diff');
29 my ($fraginfo_color) =
31 $repo->get_color('color.diff.frag', 'cyan'),
33 my ($diff_plain_color) =
35 $repo->get_color('color.diff.plain', ''),
37 my ($diff_old_color) =
39 $repo->get_color('color.diff.old', 'red'),
41 my ($diff_new_color) =
43 $repo->get_color('color.diff.new', 'green'),
46 my $normal_color = $repo->get_color("", "reset");
48 my $diff_algorithm = $repo->config('diff.algorithm');
49 my $diff_compaction_heuristic = $repo->config_bool('diff.compactionheuristic');
50 my $diff_filter = $repo->config('interactive.difffilter');
58 if ($repo->config_bool("interactive.singlekey")) {
60 require Term::ReadKey;
61 Term::ReadKey->import;
65 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
69 my $termcap = Term::Cap->Tgetent;
70 foreach (values %$termcap) {
71 $term_escapes{$_} = 1 if /^\e/;
79 my $string = join("", @_);
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$/;
94 # command line options
97 my $patch_mode_revision;
100 sub apply_patch_for_checkout_commit;
101 sub apply_patch_for_stash;
105 DIFF => 'diff-files -p',
106 APPLY => sub { apply_patch 'apply --cached', @_; },
107 APPLY_CHECK => 'apply --cached',
108 FILTER => 'file-only',
112 DIFF => 'diff-index -p HEAD',
113 APPLY => sub { apply_patch 'apply --cached', @_; },
114 APPLY_CHECK => 'apply --cached',
119 DIFF => 'diff-index -p --cached',
120 APPLY => sub { apply_patch 'apply -R --cached', @_; },
121 APPLY_CHECK => 'apply -R --cached',
122 FILTER => 'index-only',
126 DIFF => 'diff-index -R -p --cached',
127 APPLY => sub { apply_patch 'apply --cached', @_; },
128 APPLY_CHECK => 'apply --cached',
129 FILTER => 'index-only',
132 'checkout_index' => {
133 DIFF => 'diff-files -p',
134 APPLY => sub { apply_patch 'apply -R', @_; },
135 APPLY_CHECK => 'apply -R',
136 FILTER => 'file-only',
140 DIFF => 'diff-index -p',
141 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
142 APPLY_CHECK => 'apply -R',
146 'checkout_nothead' => {
147 DIFF => 'diff-index -R -p',
148 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
149 APPLY_CHECK => 'apply',
155 $patch_mode = 'stage';
156 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
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 ? "\"$_\"": $_ } @_;
166 open($fh, '-|', @_) or die;
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"
191 my ($retval, $remainder);
192 if (!/^\042(.*)\042$/) {
195 ($_, $retval) = ($1, "");
196 while (/^([^\\]*)\\(.*)$/) {
200 if (/^([0-3][0-7][0-7])(.*)$/) {
201 $retval .= chr(oct($1));
205 if (/^([\\\042btnvfr])(.*)$/) {
206 $retval .= $cquote_map{$1};
210 # This is malformed -- just return it as-is for now.
221 open $fh, 'git update-index --refresh |'
224 ;# ignore 'needs update'
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'));
243 sub is_initial_commit {
244 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
245 unless defined $initial;
251 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
254 sub get_diff_reference {
256 if (defined $ref and $ref ne 'HEAD') {
258 } elsif (is_initial_commit()) {
259 return get_empty_tree();
265 # Returns list of hashes, contents of each of which are:
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
277 my ($add, $del, $adddel, $file);
284 } run_cmd_pipe(qw(git ls-files --), @ARGV);
285 return if (!@tracked);
288 my $reference = get_diff_reference($patch_mode_revision);
289 for (run_cmd_pipe(qw(git diff-index --cached
290 --numstat --summary), $reference,
292 if (($add, $del, $file) =
293 /^([-\d]+) ([-\d]+) (.*)/) {
295 $file = unquote_path($file);
296 if ($add eq '-' && $del eq '-') {
297 $change = __('binary');
301 $change = "+$add/-$del";
306 FILE => __('nothing'),
309 elsif (($adddel, $file) =
310 /^ (create|delete) mode [0-7]+ (.*)$/) {
311 $file = unquote_path($file);
312 $data{$file}{INDEX_ADDDEL} = $adddel;
316 for (run_cmd_pipe(qw(git diff-files --numstat --summary --raw --), @tracked)) {
317 if (($add, $del, $file) =
318 /^([-\d]+) ([-\d]+) (.*)/) {
319 $file = unquote_path($file);
321 if ($add eq '-' && $del eq '-') {
322 $change = __('binary');
326 $change = "+$add/-$del";
328 $data{$file}{FILE} = $change;
330 $data{$file}{BINARY} = 1;
333 elsif (($adddel, $file) =
334 /^ (create|delete) mode [0-7]+ (.*)$/) {
335 $file = unquote_path($file);
336 $data{$file}{FILE_ADDDEL} = $adddel;
338 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
339 $file = unquote_path($2);
340 if (!exists $data{$file}) {
342 INDEX => __('unchanged'),
347 $data{$file}{UNMERGED} = 1;
352 for (sort keys %data) {
356 if ($only eq 'index-only') {
357 next if ($it->{INDEX} eq __('unchanged'));
359 if ($only eq 'file-only') {
360 next if ($it->{FILE} eq __('nothing'));
372 my ($string, @stuff) = @_;
374 for (my $i = 0; $i < @stuff; $i++) {
378 if ((ref $it) eq 'ARRAY') {
386 if ($it =~ /^$string/) {
390 if (defined $hit && defined $found) {
400 # inserts string into trie and updates count for each character
402 my ($trie, $string) = @_;
403 foreach (split //, $string) {
404 $trie = $trie->{$_} ||= {COUNT => 0};
409 # returns an array of tuples (prefix, remainder)
410 sub find_unique_prefixes {
414 # any single prefix exceeding the soft limit is omitted
415 # if any prefix exceeds the hard limit all are omitted
416 # 0 indicates no limit
420 # build a trie modelling all possible options
422 foreach my $print (@stuff) {
423 if ((ref $print) eq 'ARRAY') {
424 $print = $print->[0];
426 elsif ((ref $print) eq 'HASH') {
427 $print = $print->{VALUE};
429 update_trie(\%trie, $print);
430 push @return, $print;
433 # use the trie to find the unique prefixes
434 for (my $i = 0; $i < @return; $i++) {
435 my $ret = $return[$i];
436 my @letters = split //, $ret;
438 my ($prefix, $remainder);
440 for ($j = 0; $j < @letters; $j++) {
441 my $letter = $letters[$j];
442 if ($search{$letter}{COUNT} == 1) {
443 $prefix = substr $ret, 0, $j + 1;
444 $remainder = substr $ret, $j + 1;
448 my $prefix = substr $ret, 0, $j;
450 if ($hard_limit && $j + 1 > $hard_limit);
452 %search = %{$search{$letter}};
454 if (ord($letters[0]) > 127 ||
455 ($soft_limit && $j + 1 > $soft_limit)) {
459 $return[$i] = [$prefix, $remainder];
464 # filters out prefixes which have special meaning to list_and_choose()
465 sub is_valid_prefix {
467 return (defined $prefix) &&
468 !($prefix =~ /[\s,]/) && # separators
469 !($prefix =~ /^-/) && # deselection
470 !($prefix =~ /^\d+/) && # selection
471 ($prefix ne '*') && # "all" wildcard
472 ($prefix ne '?'); # prompt help
475 # given a prefix/remainder tuple return a string with the prefix highlighted
476 # for now use square brackets; later might use ANSI colors (underline, bold)
477 sub highlight_prefix {
479 my $remainder = shift;
481 if (!defined $prefix) {
485 if (!is_valid_prefix($prefix)) {
486 return "$prefix$remainder";
489 if (!$menu_use_color) {
490 return "[$prefix]$remainder";
493 return "$prompt_color$prefix$normal_color$remainder";
497 print STDERR colored $error_color, @_;
500 sub list_and_choose {
501 my ($opts, @stuff) = @_;
502 my (@chosen, @return);
507 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
513 if ($opts->{HEADER}) {
514 if (!$opts->{LIST_FLAT}) {
517 print colored $header_color, "$opts->{HEADER}\n";
519 for ($i = 0; $i < @stuff; $i++) {
520 my $chosen = $chosen[$i] ? '*' : ' ';
521 my $print = $stuff[$i];
522 my $ref = ref $print;
523 my $highlighted = highlight_prefix(@{$prefixes[$i]})
525 if ($ref eq 'ARRAY') {
526 $print = $highlighted || $print->[0];
528 elsif ($ref eq 'HASH') {
529 my $value = $highlighted || $print->{VALUE};
530 $print = sprintf($status_fmt,
536 $print = $highlighted || $print;
538 printf("%s%2d: %s", $chosen, $i+1, $print);
539 if (($opts->{LIST_FLAT}) &&
540 (($i + 1) % ($opts->{LIST_FLAT}))) {
553 return if ($opts->{LIST_ONLY});
555 print colored $prompt_color, $opts->{PROMPT};
556 if ($opts->{SINGLETON}) {
565 $opts->{ON_EOF}->() if $opts->{ON_EOF};
572 singleton_prompt_help_cmd() :
576 for my $choice (split(/[\s,]+/, $line)) {
580 # Input that begins with '-'; unchoose
581 if ($choice =~ s/^-//) {
584 # A range can be specified like 5-7 or 5-.
585 if ($choice =~ /^(\d+)-(\d*)$/) {
586 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
588 elsif ($choice =~ /^\d+$/) {
589 $bottom = $top = $choice;
591 elsif ($choice eq '*') {
596 $bottom = $top = find_unique($choice, @stuff);
597 if (!defined $bottom) {
598 error_msg sprintf(__("Huh (%s)?\n"), $choice);
602 if ($opts->{SINGLETON} && $bottom != $top) {
603 error_msg sprintf(__("Huh (%s)?\n"), $choice);
606 for ($i = $bottom-1; $i <= $top-1; $i++) {
607 next if (@stuff <= $i || $i < 0);
608 $chosen[$i] = $choose;
611 last if ($opts->{IMMEDIATE} || $line eq '*');
613 for ($i = 0; $i < @stuff; $i++) {
615 push @return, $stuff[$i];
621 sub singleton_prompt_help_cmd {
622 print colored $help_color, __ <<'EOF' ;
624 1 - select a numbered item
625 foo - select item based on unique prefix
626 - (empty) select nothing
630 sub prompt_help_cmd {
631 print colored $help_color, __ <<'EOF' ;
633 1 - select a single item
634 3-5 - select a range of items
635 2-3,6-9 - select multiple ranges
636 foo - select item based on unique prefix
637 -... - unselect specified items
639 - (empty) finish selecting
644 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
652 if ($did eq 'added') {
653 printf(__n("added %d path\n", "added %d paths\n",
655 } elsif ($did eq 'updated') {
656 printf(__n("updated %d path\n", "updated %d paths\n",
658 } elsif ($did eq 'reverted') {
659 printf(__n("reverted %d path\n", "reverted %d paths\n",
662 printf(__n("touched %d path\n", "touched %d paths\n",
668 my @mods = list_modified('file-only');
671 my @update = list_and_choose({ PROMPT => __('Update'),
672 HEADER => $status_head, },
675 system(qw(git update-index --add --remove --),
676 map { $_->{VALUE} } @update);
677 say_n_paths('updated', @update);
683 my @update = list_and_choose({ PROMPT => __('Revert'),
684 HEADER => $status_head, },
687 if (is_initial_commit()) {
688 system(qw(git rm --cached),
689 map { $_->{VALUE} } @update);
692 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
693 map { $_->{VALUE} } @update);
695 open $fh, '| git update-index --index-info'
702 if ($_->{INDEX_ADDDEL} &&
703 $_->{INDEX_ADDDEL} eq 'create') {
704 system(qw(git update-index --force-remove --),
706 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
711 say_n_paths('reverted', @update);
716 sub add_untracked_cmd {
717 my @add = list_and_choose({ PROMPT => __('Add untracked') },
720 system(qw(git update-index --add --), @add);
721 say_n_paths('added', @add);
723 print __("No untracked files.\n");
731 open $fh, '| git ' . $cmd . " --recount --allow-overlap";
738 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
739 if (defined $diff_algorithm) {
740 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
742 if ($diff_compaction_heuristic) {
743 splice @diff_cmd, 1, 0, "--compaction-heuristic";
745 if (defined $patch_mode_revision) {
746 push @diff_cmd, get_diff_reference($patch_mode_revision);
748 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
750 if ($diff_use_color) {
751 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
752 if (defined $diff_filter) {
753 # quotemeta is overkill, but sufficient for shell-quoting
754 my $diff = join(' ', map { quotemeta } @display_cmd);
755 @display_cmd = ("$diff | $diff_filter");
758 @colored = run_cmd_pipe(@display_cmd);
760 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
762 for (my $i = 0; $i < @diff; $i++) {
763 if ($diff[$i] =~ /^@@ /) {
764 push @hunk, { TEXT => [], DISPLAY => [],
767 push @{$hunk[-1]{TEXT}}, $diff[$i];
768 push @{$hunk[-1]{DISPLAY}},
769 (@colored ? $colored[$i] : $diff[$i]);
774 sub parse_diff_header {
777 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
778 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
779 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
781 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
783 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
784 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
786 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
787 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
789 return ($head, $mode, $deletion);
792 sub hunk_splittable {
795 my @s = split_hunk($text);
799 sub parse_hunk_header {
801 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
802 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
803 $o_cnt = 1 unless defined $o_cnt;
804 $n_cnt = 1 unless defined $n_cnt;
805 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
809 my ($text, $display) = @_;
811 if (!defined $display) {
814 # If there are context lines in the middle of a hunk,
815 # it can be split, but we would need to take care of
818 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
823 my $next_hunk_start = undef;
824 my $i = $hunk_start - 1;
838 while (++$i < @$text) {
839 my $line = $text->[$i];
840 my $display = $display->[$i];
842 if ($this->{ADDDEL} &&
843 !defined $next_hunk_start) {
844 # We have seen leading context and
845 # adds/dels and then here is another
846 # context, which is trailing for this
847 # split hunk and leading for the next
849 $next_hunk_start = $i;
851 push @{$this->{TEXT}}, $line;
852 push @{$this->{DISPLAY}}, $display;
855 if (defined $next_hunk_start) {
862 if (defined $next_hunk_start) {
863 # We are done with the current hunk and
864 # this is the first real change for the
866 $hunk_start = $next_hunk_start;
867 $o_ofs = $this->{OLD} + $this->{OCNT};
868 $n_ofs = $this->{NEW} + $this->{NCNT};
869 $o_ofs -= $this->{POSTCTX};
870 $n_ofs -= $this->{POSTCTX};
874 push @{$this->{TEXT}}, $line;
875 push @{$this->{DISPLAY}}, $display;
889 for my $hunk (@split) {
890 $o_ofs = $hunk->{OLD};
891 $n_ofs = $hunk->{NEW};
892 my $o_cnt = $hunk->{OCNT};
893 my $n_cnt = $hunk->{NCNT};
895 my $head = ("@@ -$o_ofs" .
896 (($o_cnt != 1) ? ",$o_cnt" : '') .
898 (($n_cnt != 1) ? ",$n_cnt" : '') .
900 my $display_head = $head;
901 unshift @{$hunk->{TEXT}}, $head;
902 if ($diff_use_color) {
903 $display_head = colored($fraginfo_color, $head);
905 unshift @{$hunk->{DISPLAY}}, $display_head;
910 sub find_last_o_ctx {
912 my $text = $it->{TEXT};
913 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
915 my $last_o_ctx = $o_ofs + $o_cnt;
917 my $line = $text->[$i];
928 my ($prev, $this) = @_;
929 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
930 parse_hunk_header($prev->{TEXT}[0]);
931 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
932 parse_hunk_header($this->{TEXT}[0]);
934 my (@line, $i, $ofs, $o_cnt, $n_cnt);
937 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
938 my $line = $prev->{TEXT}[$i];
939 if ($line =~ /^\+/) {
945 last if ($o1_ofs <= $ofs);
955 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
956 my $line = $this->{TEXT}[$i];
957 if ($line =~ /^\+/) {
969 my $head = ("@@ -$o0_ofs" .
970 (($o_cnt != 1) ? ",$o_cnt" : '') .
972 (($n_cnt != 1) ? ",$n_cnt" : '') .
974 @{$prev->{TEXT}} = ($head, @line);
977 sub coalesce_overlapping_hunks {
981 my ($last_o_ctx, $last_was_dirty);
983 for (grep { $_->{USE} } @in) {
984 if ($_->{TYPE} ne 'hunk') {
988 my $text = $_->{TEXT};
989 my ($o_ofs) = parse_hunk_header($text->[0]);
990 if (defined $last_o_ctx &&
991 $o_ofs <= $last_o_ctx &&
994 merge_hunk($out[-1], $_);
999 $last_o_ctx = find_last_o_ctx($out[-1]);
1000 $last_was_dirty = $_->{DIRTY};
1005 sub reassemble_patch {
1009 # Include everything in the header except the beginning of the diff.
1010 push @patch, (grep { !/^[-+]{3}/ } @$head);
1012 # Then include any headers from the hunk lines, which must
1013 # come before any actual hunk.
1014 while (@_ && $_[0] !~ /^@/) {
1018 # Then begin the diff.
1019 push @patch, grep { /^[-+]{3}/ } @$head;
1021 # And then the actual hunks.
1029 colored((/^@/ ? $fraginfo_color :
1030 /^\+/ ? $diff_new_color :
1031 /^-/ ? $diff_old_color :
1037 my %edit_hunk_manually_modes = (
1039 "If the patch applies cleanly, the edited hunk will immediately be
1040 marked for staging."),
1042 "If the patch applies cleanly, the edited hunk will immediately be
1043 marked for stashing."),
1045 "If the patch applies cleanly, the edited hunk will immediately be
1046 marked for unstaging."),
1047 reset_nothead => N__(
1048 "If the patch applies cleanly, the edited hunk will immediately be
1049 marked for applying."),
1050 checkout_index => N__(
1051 "If the patch applies cleanly, the edited hunk will immediately be
1052 marked for discarding"),
1053 checkout_head => N__(
1054 "If the patch applies cleanly, the edited hunk will immediately be
1055 marked for discarding."),
1056 checkout_nothead => N__(
1057 "If the patch applies cleanly, the edited hunk will immediately be
1058 marked for applying."),
1061 sub edit_hunk_manually {
1064 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1066 open $fh, '>', $hunkfile
1067 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1068 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1069 print $fh @$oldtext;
1070 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1071 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1072 my $comment_line_char = Git::get_comment_line_char;
1073 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1075 To remove '%s' lines, make them ' ' lines (context).
1076 To remove '%s' lines, delete them.
1077 Lines starting with %s will be removed.
1079 __($edit_hunk_manually_modes{$patch_mode}),
1080 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1082 If it does not apply cleanly, you will be given an opportunity to
1083 edit again. If all lines of the hunk are removed, then the edit is
1084 aborted and the hunk is left unchanged.
1088 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1089 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1095 open $fh, '<', $hunkfile
1096 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1097 my @newtext = grep { !/^$comment_line_char/ } <$fh>;
1101 # Abort if nothing remains
1102 if (!grep { /\S/ } @newtext) {
1106 # Reinsert the first hunk header if the user accidentally deleted it
1107 if ($newtext[0] !~ /^@/) {
1108 unshift @newtext, $oldtext->[0];
1114 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1115 map { @{$_->{TEXT}} } @_);
1118 sub _restore_terminal_and_die {
1124 sub prompt_single_character {
1126 local $SIG{TERM} = \&_restore_terminal_and_die;
1127 local $SIG{INT} = \&_restore_terminal_and_die;
1129 my $key = ReadKey 0;
1131 if ($use_termcap and $key eq "\e") {
1132 while (!defined $term_escapes{$key}) {
1133 my $next = ReadKey 0.5;
1134 last if (!defined $next);
1139 print "$key" if defined $key;
1150 print colored $prompt_color, $prompt;
1151 my $line = prompt_single_character;
1152 return 0 if $line =~ /^n/i;
1153 return 1 if $line =~ /^y/i;
1157 sub edit_hunk_loop {
1158 my ($head, $hunk, $ix) = @_;
1159 my $text = $hunk->[$ix]->{TEXT};
1162 $text = edit_hunk_manually($text);
1163 if (!defined $text) {
1168 TYPE => $hunk->[$ix]->{TYPE},
1172 if (diff_applies($head,
1175 @{$hunk}[$ix+1..$#{$hunk}])) {
1176 $newhunk->{DISPLAY} = [color_diff(@{$text})];
1181 # TRANSLATORS: do not translate [y/n]
1182 # The program will only accept that input
1184 # Consider translating (saying "no" discards!) as
1185 # (saying "n" for "no" discards!) if the translation
1186 # of the word "no" does not start with n.
1187 __('Your edited hunk does not apply. Edit again '
1188 . '(saying "no" discards!) [y/n]? ')
1194 my %help_patch_modes = (
1196 "y - stage this hunk
1197 n - do not stage this hunk
1198 q - quit; do not stage this hunk or any of the remaining ones
1199 a - stage this hunk and all later hunks in the file
1200 d - do not stage this hunk or any of the later hunks in the file"),
1202 "y - stash this hunk
1203 n - do not stash this hunk
1204 q - quit; do not stash this hunk or any of the remaining ones
1205 a - stash this hunk and all later hunks in the file
1206 d - do not stash this hunk or any of the later hunks in the file"),
1208 "y - unstage this hunk
1209 n - do not unstage this hunk
1210 q - quit; do not unstage this hunk or any of the remaining ones
1211 a - unstage this hunk and all later hunks in the file
1212 d - do not unstage this hunk or any of the later hunks in the file"),
1213 reset_nothead => N__(
1214 "y - apply this hunk to index
1215 n - do not apply this hunk to index
1216 q - quit; do not apply this hunk or any of the remaining ones
1217 a - apply this hunk and all later hunks in the file
1218 d - do not apply this hunk or any of the later hunks in the file"),
1219 checkout_index => N__(
1220 "y - discard this hunk from worktree
1221 n - do not discard this hunk from worktree
1222 q - quit; do not discard this hunk or any of the remaining ones
1223 a - discard this hunk and all later hunks in the file
1224 d - do not discard this hunk or any of the later hunks in the file"),
1225 checkout_head => N__(
1226 "y - discard this hunk from index and worktree
1227 n - do not discard this hunk from index and worktree
1228 q - quit; do not discard this hunk or any of the remaining ones
1229 a - discard this hunk and all later hunks in the file
1230 d - do not discard this hunk or any of the later hunks in the file"),
1231 checkout_nothead => N__(
1232 "y - apply this hunk to index and worktree
1233 n - do not apply this hunk to index and worktree
1234 q - quit; do not apply this hunk or any of the remaining ones
1235 a - apply this hunk and all later hunks in the file
1236 d - do not apply this hunk or any of the later hunks in the file"),
1239 sub help_patch_cmd {
1240 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1241 g - select a hunk to go to
1242 / - search for a hunk matching the given regex
1243 j - leave this hunk undecided, see next undecided hunk
1244 J - leave this hunk undecided, see next hunk
1245 k - leave this hunk undecided, see previous undecided hunk
1246 K - leave this hunk undecided, see previous hunk
1247 s - split the current hunk into smaller hunks
1248 e - manually edit the current hunk
1255 my $ret = run_git_apply $cmd, @_;
1262 sub apply_patch_for_checkout_commit {
1263 my $reverse = shift;
1264 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1265 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1267 if ($applies_worktree && $applies_index) {
1268 run_git_apply 'apply '.$reverse.' --cached', @_;
1269 run_git_apply 'apply '.$reverse, @_;
1271 } elsif (!$applies_index) {
1272 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1273 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1274 return run_git_apply 'apply '.$reverse, @_;
1276 print colored $error_color, __("Nothing was applied.\n");
1285 sub patch_update_cmd {
1286 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1287 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1288 for grep { $_->{UNMERGED} } @all_mods;
1289 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1291 my @mods = grep { !($_->{BINARY}) } @all_mods;
1296 print STDERR __("Only binary files changed.\n");
1298 print STDERR __("No changes.\n");
1306 @them = list_and_choose({ PROMPT => __('Patch update'),
1307 HEADER => $status_head, },
1311 return 0 if patch_update_file($_->{VALUE});
1315 # Generate a one line summary of a hunk.
1316 sub summarize_hunk {
1318 my $summary = $rhunk->{TEXT}[0];
1320 # Keep the line numbers, discard extra context.
1321 $summary =~ s/@@(.*?)@@.*/$1 /s;
1322 $summary .= " " x (20 - length $summary);
1324 # Add some user context.
1325 for my $line (@{$rhunk->{TEXT}}) {
1326 if ($line =~ m/^[+-].*\w/) {
1333 return substr($summary, 0, 80) . "\n";
1337 # Print a one-line summary of each hunk in the array ref in
1338 # the first argument, starting with the index in the 2nd.
1340 my ($hunks, $i) = @_;
1343 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1345 if (defined $hunks->[$i]{USE}) {
1346 $status = $hunks->[$i]{USE} ? "+" : "-";
1351 summarize_hunk($hunks->[$i]);
1356 my %patch_update_prompt_modes = (
1358 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1359 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1360 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1363 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1364 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1365 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1368 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1369 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1370 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1373 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1374 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1375 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1378 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1379 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1380 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1383 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1384 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1385 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1387 checkout_nothead => {
1388 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1389 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1390 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1394 sub patch_update_file {
1398 my ($head, @hunk) = parse_diff($path);
1399 ($head, my $mode, my $deletion) = parse_diff_header($head);
1400 for (@{$head->{DISPLAY}}) {
1404 if (@{$mode->{TEXT}}) {
1405 unshift @hunk, $mode;
1407 if (@{$deletion->{TEXT}}) {
1408 foreach my $hunk (@hunk) {
1409 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1410 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1412 @hunk = ($deletion);
1415 $num = scalar @hunk;
1419 my ($prev, $next, $other, $undecided, $i);
1425 for ($i = 0; $i < $ix; $i++) {
1426 if (!defined $hunk[$i]{USE}) {
1435 for ($i = $ix + 1; $i < $num; $i++) {
1436 if (!defined $hunk[$i]{USE}) {
1442 if ($ix < $num - 1) {
1448 for ($i = 0; $i < $num; $i++) {
1449 if (!defined $hunk[$i]{USE}) {
1454 last if (!$undecided);
1456 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1457 hunk_splittable($hunk[$ix]{TEXT})) {
1460 if ($hunk[$ix]{TYPE} eq 'hunk') {
1463 for (@{$hunk[$ix]{DISPLAY}}) {
1466 print colored $prompt_color,
1467 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1469 my $line = prompt_single_character;
1470 last unless defined $line;
1472 if ($line =~ /^y/i) {
1473 $hunk[$ix]{USE} = 1;
1475 elsif ($line =~ /^n/i) {
1476 $hunk[$ix]{USE} = 0;
1478 elsif ($line =~ /^a/i) {
1479 while ($ix < $num) {
1480 if (!defined $hunk[$ix]{USE}) {
1481 $hunk[$ix]{USE} = 1;
1487 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1489 my $no = $ix > 10 ? $ix - 10 : 0;
1490 while ($response eq '') {
1491 $no = display_hunks(\@hunk, $no);
1493 print __("go to which hunk (<ret> to see more)? ");
1495 print __("go to which hunk? ");
1497 $response = <STDIN>;
1498 if (!defined $response) {
1503 if ($response !~ /^\s*\d+\s*$/) {
1504 error_msg sprintf(__("Invalid number: '%s'\n"),
1506 } elsif (0 < $response && $response <= $num) {
1507 $ix = $response - 1;
1509 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1510 "Sorry, only %d hunks available.\n", $num), $num);
1514 elsif ($line =~ /^d/i) {
1515 while ($ix < $num) {
1516 if (!defined $hunk[$ix]{USE}) {
1517 $hunk[$ix]{USE} = 0;
1523 elsif ($line =~ /^q/i) {
1524 for ($i = 0; $i < $num; $i++) {
1525 if (!defined $hunk[$i]{USE}) {
1532 elsif ($line =~ m|^/(.*)|) {
1535 print colored $prompt_color, __("search for regex? ");
1537 if (defined $regex) {
1543 $search_string = qr{$regex}m;
1546 my ($err,$exp) = ($@, $1);
1547 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1548 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1553 my $text = join ("", @{$hunk[$iy]{TEXT}});
1554 last if ($text =~ $search_string);
1556 $iy = 0 if ($iy >= $num);
1558 error_msg __("No hunk matches the given pattern\n");
1565 elsif ($line =~ /^K/) {
1566 if ($other =~ /K/) {
1570 error_msg __("No previous hunk\n");
1574 elsif ($line =~ /^J/) {
1575 if ($other =~ /J/) {
1579 error_msg __("No next hunk\n");
1583 elsif ($line =~ /^k/) {
1584 if ($other =~ /k/) {
1588 !defined $hunk[$ix]{USE});
1592 error_msg __("No previous hunk\n");
1596 elsif ($line =~ /^j/) {
1597 if ($other !~ /j/) {
1598 error_msg __("No next hunk\n");
1602 elsif ($other =~ /s/ && $line =~ /^s/) {
1603 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1605 print colored $header_color, sprintf(
1606 __n("Split into %d hunk.\n",
1607 "Split into %d hunks.\n",
1608 scalar(@split)), scalar(@split));
1610 splice (@hunk, $ix, 1, @split);
1611 $num = scalar @hunk;
1614 elsif ($other =~ /e/ && $line =~ /^e/) {
1615 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1616 if (defined $newhunk) {
1617 splice @hunk, $ix, 1, $newhunk;
1621 help_patch_cmd($other);
1627 last if ($ix >= $num ||
1628 !defined $hunk[$ix]{USE});
1633 @hunk = coalesce_overlapping_hunks(@hunk);
1639 push @result, @{$_->{TEXT}};
1644 my @patch = reassemble_patch($head->{TEXT}, @result);
1645 my $apply_routine = $patch_mode_flavour{APPLY};
1646 &$apply_routine(@patch);
1655 my @mods = list_modified('index-only');
1656 @mods = grep { !($_->{BINARY}) } @mods;
1658 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1660 HEADER => $status_head, },
1663 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1664 system(qw(git diff -p --cached), $reference, '--',
1665 map { $_->{VALUE} } @them);
1674 # TRANSLATORS: please do not translate the command names
1675 # 'status', 'update', 'revert', etc.
1676 print colored $help_color, __ <<'EOF' ;
1677 status - show paths with changes
1678 update - add working tree state to the staged set of changes
1679 revert - revert staged set of changes back to the HEAD version
1680 patch - pick hunks and update selectively
1681 diff - view diff between HEAD and index
1682 add untracked - add contents of untracked files to the staged set of changes
1687 return unless @ARGV;
1688 my $arg = shift @ARGV;
1689 if ($arg =~ /--patch(?:=(.*))?/) {
1691 if ($1 eq 'reset') {
1692 $patch_mode = 'reset_head';
1693 $patch_mode_revision = 'HEAD';
1694 $arg = shift @ARGV or die __("missing --");
1696 $patch_mode_revision = $arg;
1697 $patch_mode = ($arg eq 'HEAD' ?
1698 'reset_head' : 'reset_nothead');
1699 $arg = shift @ARGV or die __("missing --");
1701 } elsif ($1 eq 'checkout') {
1702 $arg = shift @ARGV or die __("missing --");
1704 $patch_mode = 'checkout_index';
1706 $patch_mode_revision = $arg;
1707 $patch_mode = ($arg eq 'HEAD' ?
1708 'checkout_head' : 'checkout_nothead');
1709 $arg = shift @ARGV or die __("missing --");
1711 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1713 $arg = shift @ARGV or die __("missing --");
1715 die sprintf(__("unknown --patch mode: %s"), $1);
1718 $patch_mode = 'stage';
1719 $arg = shift @ARGV or die __("missing --");
1721 die sprintf(__("invalid argument %s, expecting --"),
1722 $arg) unless $arg eq "--";
1723 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1726 elsif ($arg ne "--") {
1727 die sprintf(__("invalid argument %s, expecting --"), $arg);
1732 my @cmd = ([ 'status', \&status_cmd, ],
1733 [ 'update', \&update_cmd, ],
1734 [ 'revert', \&revert_cmd, ],
1735 [ 'add untracked', \&add_untracked_cmd, ],
1736 [ 'patch', \&patch_update_cmd, ],
1737 [ 'diff', \&diff_cmd, ],
1738 [ 'quit', \&quit_cmd, ],
1739 [ 'help', \&help_cmd, ],
1742 my ($it) = list_and_choose({ PROMPT => __('What now'),
1745 HEADER => __('*** Commands ***'),
1746 ON_EOF => \&quit_cmd,
1747 IMMEDIATE => 1 }, @cmd);