6 use Git qw(unquote_path);
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_filter = $repo->config('interactive.difffilter');
57 if ($repo->config_bool("interactive.singlekey")) {
59 require Term::ReadKey;
60 Term::ReadKey->import;
64 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
68 my $termcap = Term::Cap->Tgetent;
69 foreach (values %$termcap) {
70 $term_escapes{$_} = 1 if /^\e/;
78 my $string = join("", @_);
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$/;
93 # command line options
96 my $patch_mode_revision;
99 sub apply_patch_for_checkout_commit;
100 sub apply_patch_for_stash;
104 DIFF => 'diff-files -p',
105 APPLY => sub { apply_patch 'apply --cached', @_; },
106 APPLY_CHECK => 'apply --cached',
107 FILTER => 'file-only',
111 DIFF => 'diff-index -p HEAD',
112 APPLY => sub { apply_patch 'apply --cached', @_; },
113 APPLY_CHECK => 'apply --cached',
118 DIFF => 'diff-index -p --cached',
119 APPLY => sub { apply_patch 'apply -R --cached', @_; },
120 APPLY_CHECK => 'apply -R --cached',
121 FILTER => 'index-only',
125 DIFF => 'diff-index -R -p --cached',
126 APPLY => sub { apply_patch 'apply --cached', @_; },
127 APPLY_CHECK => 'apply --cached',
128 FILTER => 'index-only',
131 'checkout_index' => {
132 DIFF => 'diff-files -p',
133 APPLY => sub { apply_patch 'apply -R', @_; },
134 APPLY_CHECK => 'apply -R',
135 FILTER => 'file-only',
139 DIFF => 'diff-index -p',
140 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
141 APPLY_CHECK => 'apply -R',
145 'checkout_nothead' => {
146 DIFF => 'diff-index -R -p',
147 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
148 APPLY_CHECK => 'apply',
154 $patch_mode = 'stage';
155 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
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 ? "\"$_\"": $_ } @_;
165 open($fh, '-|', @_) or die;
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"
179 open $fh, 'git update-index --refresh |'
182 ;# ignore 'needs update'
192 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
195 # TRANSLATORS: you can adjust this to align "git add -i" status menu
196 my $status_fmt = __('%12s %12s %s');
197 my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
201 sub is_initial_commit {
202 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
203 unless defined $initial;
209 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
212 sub get_diff_reference {
214 if (defined $ref and $ref ne 'HEAD') {
216 } elsif (is_initial_commit()) {
217 return get_empty_tree();
223 # Returns list of hashes, contents of each of which are:
225 # BINARY: is a binary path
226 # INDEX: is index different from HEAD?
227 # FILE: is file different from index?
228 # INDEX_ADDDEL: is it add/delete between HEAD and index?
229 # FILE_ADDDEL: is it add/delete between index and file?
230 # UNMERGED: is the path unmerged
235 my ($add, $del, $adddel, $file);
237 my $reference = get_diff_reference($patch_mode_revision);
238 for (run_cmd_pipe(qw(git diff-index --cached
239 --numstat --summary), $reference,
241 if (($add, $del, $file) =
242 /^([-\d]+) ([-\d]+) (.*)/) {
244 $file = unquote_path($file);
245 if ($add eq '-' && $del eq '-') {
246 $change = __('binary');
250 $change = "+$add/-$del";
255 FILE => __('nothing'),
258 elsif (($adddel, $file) =
259 /^ (create|delete) mode [0-7]+ (.*)$/) {
260 $file = unquote_path($file);
261 $data{$file}{INDEX_ADDDEL} = $adddel;
265 for (run_cmd_pipe(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) {
266 if (($add, $del, $file) =
267 /^([-\d]+) ([-\d]+) (.*)/) {
268 $file = unquote_path($file);
270 if ($add eq '-' && $del eq '-') {
271 $change = __('binary');
275 $change = "+$add/-$del";
277 $data{$file}{FILE} = $change;
279 $data{$file}{BINARY} = 1;
282 elsif (($adddel, $file) =
283 /^ (create|delete) mode [0-7]+ (.*)$/) {
284 $file = unquote_path($file);
285 $data{$file}{FILE_ADDDEL} = $adddel;
287 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
288 $file = unquote_path($2);
289 if (!exists $data{$file}) {
291 INDEX => __('unchanged'),
296 $data{$file}{UNMERGED} = 1;
301 for (sort keys %data) {
305 if ($only eq 'index-only') {
306 next if ($it->{INDEX} eq __('unchanged'));
308 if ($only eq 'file-only') {
309 next if ($it->{FILE} eq __('nothing'));
321 my ($string, @stuff) = @_;
323 for (my $i = 0; $i < @stuff; $i++) {
327 if ((ref $it) eq 'ARRAY') {
335 if ($it =~ /^$string/) {
339 if (defined $hit && defined $found) {
349 # inserts string into trie and updates count for each character
351 my ($trie, $string) = @_;
352 foreach (split //, $string) {
353 $trie = $trie->{$_} ||= {COUNT => 0};
358 # returns an array of tuples (prefix, remainder)
359 sub find_unique_prefixes {
363 # any single prefix exceeding the soft limit is omitted
364 # if any prefix exceeds the hard limit all are omitted
365 # 0 indicates no limit
369 # build a trie modelling all possible options
371 foreach my $print (@stuff) {
372 if ((ref $print) eq 'ARRAY') {
373 $print = $print->[0];
375 elsif ((ref $print) eq 'HASH') {
376 $print = $print->{VALUE};
378 update_trie(\%trie, $print);
379 push @return, $print;
382 # use the trie to find the unique prefixes
383 for (my $i = 0; $i < @return; $i++) {
384 my $ret = $return[$i];
385 my @letters = split //, $ret;
387 my ($prefix, $remainder);
389 for ($j = 0; $j < @letters; $j++) {
390 my $letter = $letters[$j];
391 if ($search{$letter}{COUNT} == 1) {
392 $prefix = substr $ret, 0, $j + 1;
393 $remainder = substr $ret, $j + 1;
397 my $prefix = substr $ret, 0, $j;
399 if ($hard_limit && $j + 1 > $hard_limit);
401 %search = %{$search{$letter}};
403 if (ord($letters[0]) > 127 ||
404 ($soft_limit && $j + 1 > $soft_limit)) {
408 $return[$i] = [$prefix, $remainder];
413 # filters out prefixes which have special meaning to list_and_choose()
414 sub is_valid_prefix {
416 return (defined $prefix) &&
417 !($prefix =~ /[\s,]/) && # separators
418 !($prefix =~ /^-/) && # deselection
419 !($prefix =~ /^\d+/) && # selection
420 ($prefix ne '*') && # "all" wildcard
421 ($prefix ne '?'); # prompt help
424 # given a prefix/remainder tuple return a string with the prefix highlighted
425 # for now use square brackets; later might use ANSI colors (underline, bold)
426 sub highlight_prefix {
428 my $remainder = shift;
430 if (!defined $prefix) {
434 if (!is_valid_prefix($prefix)) {
435 return "$prefix$remainder";
438 if (!$menu_use_color) {
439 return "[$prefix]$remainder";
442 return "$prompt_color$prefix$normal_color$remainder";
446 print STDERR colored $error_color, @_;
449 sub list_and_choose {
450 my ($opts, @stuff) = @_;
451 my (@chosen, @return);
456 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
462 if ($opts->{HEADER}) {
463 if (!$opts->{LIST_FLAT}) {
466 print colored $header_color, "$opts->{HEADER}\n";
468 for ($i = 0; $i < @stuff; $i++) {
469 my $chosen = $chosen[$i] ? '*' : ' ';
470 my $print = $stuff[$i];
471 my $ref = ref $print;
472 my $highlighted = highlight_prefix(@{$prefixes[$i]})
474 if ($ref eq 'ARRAY') {
475 $print = $highlighted || $print->[0];
477 elsif ($ref eq 'HASH') {
478 my $value = $highlighted || $print->{VALUE};
479 $print = sprintf($status_fmt,
485 $print = $highlighted || $print;
487 printf("%s%2d: %s", $chosen, $i+1, $print);
488 if (($opts->{LIST_FLAT}) &&
489 (($i + 1) % ($opts->{LIST_FLAT}))) {
502 return if ($opts->{LIST_ONLY});
504 print colored $prompt_color, $opts->{PROMPT};
505 if ($opts->{SINGLETON}) {
514 $opts->{ON_EOF}->() if $opts->{ON_EOF};
521 singleton_prompt_help_cmd() :
525 for my $choice (split(/[\s,]+/, $line)) {
529 # Input that begins with '-'; unchoose
530 if ($choice =~ s/^-//) {
533 # A range can be specified like 5-7 or 5-.
534 if ($choice =~ /^(\d+)-(\d*)$/) {
535 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
537 elsif ($choice =~ /^\d+$/) {
538 $bottom = $top = $choice;
540 elsif ($choice eq '*') {
545 $bottom = $top = find_unique($choice, @stuff);
546 if (!defined $bottom) {
547 error_msg sprintf(__("Huh (%s)?\n"), $choice);
551 if ($opts->{SINGLETON} && $bottom != $top) {
552 error_msg sprintf(__("Huh (%s)?\n"), $choice);
555 for ($i = $bottom-1; $i <= $top-1; $i++) {
556 next if (@stuff <= $i || $i < 0);
557 $chosen[$i] = $choose;
560 last if ($opts->{IMMEDIATE} || $line eq '*');
562 for ($i = 0; $i < @stuff; $i++) {
564 push @return, $stuff[$i];
570 sub singleton_prompt_help_cmd {
571 print colored $help_color, __ <<'EOF' ;
573 1 - select a numbered item
574 foo - select item based on unique prefix
575 - (empty) select nothing
579 sub prompt_help_cmd {
580 print colored $help_color, __ <<'EOF' ;
582 1 - select a single item
583 3-5 - select a range of items
584 2-3,6-9 - select multiple ranges
585 foo - select item based on unique prefix
586 -... - unselect specified items
588 - (empty) finish selecting
593 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
601 if ($did eq 'added') {
602 printf(__n("added %d path\n", "added %d paths\n",
604 } elsif ($did eq 'updated') {
605 printf(__n("updated %d path\n", "updated %d paths\n",
607 } elsif ($did eq 'reverted') {
608 printf(__n("reverted %d path\n", "reverted %d paths\n",
611 printf(__n("touched %d path\n", "touched %d paths\n",
617 my @mods = list_modified('file-only');
620 my @update = list_and_choose({ PROMPT => __('Update'),
621 HEADER => $status_head, },
624 system(qw(git update-index --add --remove --),
625 map { $_->{VALUE} } @update);
626 say_n_paths('updated', @update);
632 my @update = list_and_choose({ PROMPT => __('Revert'),
633 HEADER => $status_head, },
636 if (is_initial_commit()) {
637 system(qw(git rm --cached),
638 map { $_->{VALUE} } @update);
641 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
642 map { $_->{VALUE} } @update);
644 open $fh, '| git update-index --index-info'
651 if ($_->{INDEX_ADDDEL} &&
652 $_->{INDEX_ADDDEL} eq 'create') {
653 system(qw(git update-index --force-remove --),
655 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
660 say_n_paths('reverted', @update);
665 sub add_untracked_cmd {
666 my @add = list_and_choose({ PROMPT => __('Add untracked') },
669 system(qw(git update-index --add --), @add);
670 say_n_paths('added', @add);
672 print __("No untracked files.\n");
680 open $fh, '| git ' . $cmd . " --allow-overlap";
687 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
688 if (defined $diff_algorithm) {
689 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
691 if (defined $patch_mode_revision) {
692 push @diff_cmd, get_diff_reference($patch_mode_revision);
694 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
696 if ($diff_use_color) {
697 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
698 if (defined $diff_filter) {
699 # quotemeta is overkill, but sufficient for shell-quoting
700 my $diff = join(' ', map { quotemeta } @display_cmd);
701 @display_cmd = ("$diff | $diff_filter");
704 @colored = run_cmd_pipe(@display_cmd);
706 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
708 if (@colored && @colored != @diff) {
710 "fatal: mismatched output from interactive.diffFilter\n",
711 "hint: Your filter must maintain a one-to-one correspondence\n",
712 "hint: between its input and output lines.\n";
716 for (my $i = 0; $i < @diff; $i++) {
717 if ($diff[$i] =~ /^@@ /) {
718 push @hunk, { TEXT => [], DISPLAY => [],
721 push @{$hunk[-1]{TEXT}}, $diff[$i];
722 push @{$hunk[-1]{DISPLAY}},
723 (@colored ? $colored[$i] : $diff[$i]);
728 sub parse_diff_header {
731 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
732 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
733 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
735 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
737 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
738 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
740 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
741 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
743 return ($head, $mode, $deletion);
746 sub hunk_splittable {
749 my @s = split_hunk($text);
753 sub parse_hunk_header {
755 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
756 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
757 $o_cnt = 1 unless defined $o_cnt;
758 $n_cnt = 1 unless defined $n_cnt;
759 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
762 sub format_hunk_header {
763 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
764 return ("@@ -$o_ofs" .
765 (($o_cnt != 1) ? ",$o_cnt" : '') .
767 (($n_cnt != 1) ? ",$n_cnt" : '') .
772 my ($text, $display) = @_;
774 if (!defined $display) {
777 # If there are context lines in the middle of a hunk,
778 # it can be split, but we would need to take care of
781 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
786 my $next_hunk_start = undef;
787 my $i = $hunk_start - 1;
801 while (++$i < @$text) {
802 my $line = $text->[$i];
803 my $display = $display->[$i];
804 if ($line =~ /^\\/) {
805 push @{$this->{TEXT}}, $line;
806 push @{$this->{DISPLAY}}, $display;
810 if ($this->{ADDDEL} &&
811 !defined $next_hunk_start) {
812 # We have seen leading context and
813 # adds/dels and then here is another
814 # context, which is trailing for this
815 # split hunk and leading for the next
817 $next_hunk_start = $i;
819 push @{$this->{TEXT}}, $line;
820 push @{$this->{DISPLAY}}, $display;
823 if (defined $next_hunk_start) {
830 if (defined $next_hunk_start) {
831 # We are done with the current hunk and
832 # this is the first real change for the
834 $hunk_start = $next_hunk_start;
835 $o_ofs = $this->{OLD} + $this->{OCNT};
836 $n_ofs = $this->{NEW} + $this->{NCNT};
837 $o_ofs -= $this->{POSTCTX};
838 $n_ofs -= $this->{POSTCTX};
842 push @{$this->{TEXT}}, $line;
843 push @{$this->{DISPLAY}}, $display;
857 for my $hunk (@split) {
858 $o_ofs = $hunk->{OLD};
859 $n_ofs = $hunk->{NEW};
860 my $o_cnt = $hunk->{OCNT};
861 my $n_cnt = $hunk->{NCNT};
863 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
864 my $display_head = $head;
865 unshift @{$hunk->{TEXT}}, $head;
866 if ($diff_use_color) {
867 $display_head = colored($fraginfo_color, $head);
869 unshift @{$hunk->{DISPLAY}}, $display_head;
874 sub find_last_o_ctx {
876 my $text = $it->{TEXT};
877 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
879 my $last_o_ctx = $o_ofs + $o_cnt;
881 my $line = $text->[$i];
892 my ($prev, $this) = @_;
893 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
894 parse_hunk_header($prev->{TEXT}[0]);
895 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
896 parse_hunk_header($this->{TEXT}[0]);
898 my (@line, $i, $ofs, $o_cnt, $n_cnt);
901 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
902 my $line = $prev->{TEXT}[$i];
903 if ($line =~ /^\+/) {
907 } elsif ($line =~ /^\\/) {
912 last if ($o1_ofs <= $ofs);
922 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
923 my $line = $this->{TEXT}[$i];
924 if ($line =~ /^\+/) {
928 } elsif ($line =~ /^\\/) {
939 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
940 @{$prev->{TEXT}} = ($head, @line);
943 sub coalesce_overlapping_hunks {
947 my ($last_o_ctx, $last_was_dirty);
951 if ($_->{TYPE} ne 'hunk') {
955 my $text = $_->{TEXT};
956 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
957 parse_hunk_header($text->[0]);
959 $ofs_delta += $o_cnt - $n_cnt;
960 # If this hunk has been edited then subtract
961 # the delta that is due to the edit.
962 if ($_->{OFS_DELTA}) {
963 $ofs_delta -= $_->{OFS_DELTA};
968 $n_ofs += $ofs_delta;
969 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
972 # If this hunk was edited then adjust the offset delta
973 # to reflect the edit.
974 if ($_->{OFS_DELTA}) {
975 $ofs_delta += $_->{OFS_DELTA};
977 if (defined $last_o_ctx &&
978 $o_ofs <= $last_o_ctx &&
981 merge_hunk($out[-1], $_);
986 $last_o_ctx = find_last_o_ctx($out[-1]);
987 $last_was_dirty = $_->{DIRTY};
992 sub reassemble_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] !~ /^@/) {
1005 # Then begin the diff.
1006 push @patch, grep { /^[-+]{3}/ } @$head;
1008 # And then the actual hunks.
1016 colored((/^@/ ? $fraginfo_color :
1017 /^\+/ ? $diff_new_color :
1018 /^-/ ? $diff_old_color :
1024 my %edit_hunk_manually_modes = (
1026 "If the patch applies cleanly, the edited hunk will immediately be
1027 marked for staging."),
1029 "If the patch applies cleanly, the edited hunk will immediately be
1030 marked for stashing."),
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 recount_edited_hunk {
1050 my ($oldtext, $newtext) = @_;
1051 my ($o_cnt, $n_cnt) = (0, 0);
1052 for (@{$newtext}[1..$#{$newtext}]) {
1053 my $mode = substr($_, 0, 1);
1056 } elsif ($mode eq '+') {
1058 } elsif ($mode eq ' ') {
1063 my ($o_ofs, undef, $n_ofs, undef) =
1064 parse_hunk_header($newtext->[0]);
1065 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1066 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1067 parse_hunk_header($oldtext->[0]);
1068 # Return the change in the number of lines inserted by this hunk
1069 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1072 sub edit_hunk_manually {
1075 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1077 open $fh, '>', $hunkfile
1078 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1079 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1080 print $fh @$oldtext;
1081 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1082 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1083 my $comment_line_char = Git::get_comment_line_char;
1084 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1086 To remove '%s' lines, make them ' ' lines (context).
1087 To remove '%s' lines, delete them.
1088 Lines starting with %s will be removed.
1090 __($edit_hunk_manually_modes{$patch_mode}),
1091 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1093 If it does not apply cleanly, you will be given an opportunity to
1094 edit again. If all lines of the hunk are removed, then the edit is
1095 aborted and the hunk is left unchanged.
1099 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1100 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1106 open $fh, '<', $hunkfile
1107 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1108 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1112 # Abort if nothing remains
1113 if (!grep { /\S/ } @newtext) {
1117 # Reinsert the first hunk header if the user accidentally deleted it
1118 if ($newtext[0] !~ /^@/) {
1119 unshift @newtext, $oldtext->[0];
1125 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1126 map { @{$_->{TEXT}} } @_);
1129 sub _restore_terminal_and_die {
1135 sub prompt_single_character {
1137 local $SIG{TERM} = \&_restore_terminal_and_die;
1138 local $SIG{INT} = \&_restore_terminal_and_die;
1140 my $key = ReadKey 0;
1142 if ($use_termcap and $key eq "\e") {
1143 while (!defined $term_escapes{$key}) {
1144 my $next = ReadKey 0.5;
1145 last if (!defined $next);
1150 print "$key" if defined $key;
1161 print colored $prompt_color, $prompt;
1162 my $line = prompt_single_character;
1163 return undef unless defined $line;
1164 return 0 if $line =~ /^n/i;
1165 return 1 if $line =~ /^y/i;
1169 sub edit_hunk_loop {
1170 my ($head, $hunks, $ix) = @_;
1171 my $hunk = $hunks->[$ix];
1172 my $text = $hunk->{TEXT};
1175 my $newtext = edit_hunk_manually($text);
1176 if (!defined $newtext) {
1181 TYPE => $hunk->{TYPE},
1185 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1186 # If this hunk has already been edited then add the
1187 # offset delta of the previous edit to get the real
1188 # delta from the original unedited hunk.
1189 $hunk->{OFS_DELTA} and
1190 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1191 if (diff_applies($head,
1192 @{$hunks}[0..$ix-1],
1194 @{$hunks}[$ix+1..$#{$hunks}])) {
1195 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1200 # TRANSLATORS: do not translate [y/n]
1201 # The program will only accept that input
1203 # Consider translating (saying "no" discards!) as
1204 # (saying "n" for "no" discards!) if the translation
1205 # of the word "no" does not start with n.
1206 __('Your edited hunk does not apply. Edit again '
1207 . '(saying "no" discards!) [y/n]? ')
1213 my %help_patch_modes = (
1215 "y - stage this hunk
1216 n - do not stage this hunk
1217 q - quit; do not stage this hunk or any of the remaining ones
1218 a - stage this hunk and all later hunks in the file
1219 d - do not stage this hunk or any of the later hunks in the file"),
1221 "y - stash this hunk
1222 n - do not stash this hunk
1223 q - quit; do not stash this hunk or any of the remaining ones
1224 a - stash this hunk and all later hunks in the file
1225 d - do not stash this hunk or any of the later hunks in the file"),
1227 "y - unstage this hunk
1228 n - do not unstage this hunk
1229 q - quit; do not unstage this hunk or any of the remaining ones
1230 a - unstage this hunk and all later hunks in the file
1231 d - do not unstage this hunk or any of the later hunks in the file"),
1232 reset_nothead => N__(
1233 "y - apply this hunk to index
1234 n - do not apply this hunk to index
1235 q - quit; do not apply this hunk or any of the remaining ones
1236 a - apply this hunk and all later hunks in the file
1237 d - do not apply this hunk or any of the later hunks in the file"),
1238 checkout_index => N__(
1239 "y - discard this hunk from worktree
1240 n - do not discard this hunk from worktree
1241 q - quit; do not discard this hunk or any of the remaining ones
1242 a - discard this hunk and all later hunks in the file
1243 d - do not discard this hunk or any of the later hunks in the file"),
1244 checkout_head => N__(
1245 "y - discard this hunk from index and worktree
1246 n - do not discard this hunk from index and worktree
1247 q - quit; do not discard this hunk or any of the remaining ones
1248 a - discard this hunk and all later hunks in the file
1249 d - do not discard this hunk or any of the later hunks in the file"),
1250 checkout_nothead => N__(
1251 "y - apply this hunk to index and worktree
1252 n - do not apply this hunk to index and worktree
1253 q - quit; do not apply this hunk or any of the remaining ones
1254 a - apply this hunk and all later hunks in the file
1255 d - do not apply this hunk or any of the later hunks in the file"),
1258 sub help_patch_cmd {
1260 my $other = $_[0] . ",?";
1261 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1262 map { "$_\n" } grep {
1263 my $c = quotemeta(substr($_, 0, 1));
1265 } split "\n", __ <<EOF ;
1266 g - select a hunk to go to
1267 / - search for a hunk matching the given regex
1268 j - leave this hunk undecided, see next undecided hunk
1269 J - leave this hunk undecided, see next hunk
1270 k - leave this hunk undecided, see previous undecided hunk
1271 K - leave this hunk undecided, see previous hunk
1272 s - split the current hunk into smaller hunks
1273 e - manually edit the current hunk
1280 my $ret = run_git_apply $cmd, @_;
1287 sub apply_patch_for_checkout_commit {
1288 my $reverse = shift;
1289 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1290 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1292 if ($applies_worktree && $applies_index) {
1293 run_git_apply 'apply '.$reverse.' --cached', @_;
1294 run_git_apply 'apply '.$reverse, @_;
1296 } elsif (!$applies_index) {
1297 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1298 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1299 return run_git_apply 'apply '.$reverse, @_;
1301 print colored $error_color, __("Nothing was applied.\n");
1310 sub patch_update_cmd {
1311 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1312 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1313 for grep { $_->{UNMERGED} } @all_mods;
1314 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1316 my @mods = grep { !($_->{BINARY}) } @all_mods;
1321 print STDERR __("Only binary files changed.\n");
1323 print STDERR __("No changes.\n");
1327 if ($patch_mode_only) {
1331 @them = list_and_choose({ PROMPT => __('Patch update'),
1332 HEADER => $status_head, },
1336 return 0 if patch_update_file($_->{VALUE});
1340 # Generate a one line summary of a hunk.
1341 sub summarize_hunk {
1343 my $summary = $rhunk->{TEXT}[0];
1345 # Keep the line numbers, discard extra context.
1346 $summary =~ s/@@(.*?)@@.*/$1 /s;
1347 $summary .= " " x (20 - length $summary);
1349 # Add some user context.
1350 for my $line (@{$rhunk->{TEXT}}) {
1351 if ($line =~ m/^[+-].*\w/) {
1358 return substr($summary, 0, 80) . "\n";
1362 # Print a one-line summary of each hunk in the array ref in
1363 # the first argument, starting with the index in the 2nd.
1365 my ($hunks, $i) = @_;
1368 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1370 if (defined $hunks->[$i]{USE}) {
1371 $status = $hunks->[$i]{USE} ? "+" : "-";
1376 summarize_hunk($hunks->[$i]);
1381 my %patch_update_prompt_modes = (
1383 mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1384 deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1385 hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1388 mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1389 deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1390 hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1393 mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1394 deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1395 hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1398 mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1399 deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1400 hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1403 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1404 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1405 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1408 mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1409 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1410 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1412 checkout_nothead => {
1413 mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1414 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1415 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1419 sub patch_update_file {
1423 my ($head, @hunk) = parse_diff($path);
1424 ($head, my $mode, my $deletion) = parse_diff_header($head);
1425 for (@{$head->{DISPLAY}}) {
1429 if (@{$mode->{TEXT}}) {
1430 unshift @hunk, $mode;
1432 if (@{$deletion->{TEXT}}) {
1433 foreach my $hunk (@hunk) {
1434 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1435 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1437 @hunk = ($deletion);
1440 $num = scalar @hunk;
1444 my ($prev, $next, $other, $undecided, $i);
1450 for ($i = 0; $i < $ix; $i++) {
1451 if (!defined $hunk[$i]{USE}) {
1460 for ($i = $ix + 1; $i < $num; $i++) {
1461 if (!defined $hunk[$i]{USE}) {
1467 if ($ix < $num - 1) {
1473 for ($i = 0; $i < $num; $i++) {
1474 if (!defined $hunk[$i]{USE}) {
1479 last if (!$undecided);
1481 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1482 hunk_splittable($hunk[$ix]{TEXT})) {
1485 if ($hunk[$ix]{TYPE} eq 'hunk') {
1488 for (@{$hunk[$ix]{DISPLAY}}) {
1491 print colored $prompt_color,
1492 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1494 my $line = prompt_single_character;
1495 last unless defined $line;
1497 if ($line =~ /^y/i) {
1498 $hunk[$ix]{USE} = 1;
1500 elsif ($line =~ /^n/i) {
1501 $hunk[$ix]{USE} = 0;
1503 elsif ($line =~ /^a/i) {
1504 while ($ix < $num) {
1505 if (!defined $hunk[$ix]{USE}) {
1506 $hunk[$ix]{USE} = 1;
1512 elsif ($line =~ /^g(.*)/) {
1514 unless ($other =~ /g/) {
1515 error_msg __("No other hunks to goto\n");
1518 my $no = $ix > 10 ? $ix - 10 : 0;
1519 while ($response eq '') {
1520 $no = display_hunks(\@hunk, $no);
1522 print __("go to which hunk (<ret> to see more)? ");
1524 print __("go to which hunk? ");
1526 $response = <STDIN>;
1527 if (!defined $response) {
1532 if ($response !~ /^\s*\d+\s*$/) {
1533 error_msg sprintf(__("Invalid number: '%s'\n"),
1535 } elsif (0 < $response && $response <= $num) {
1536 $ix = $response - 1;
1538 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1539 "Sorry, only %d hunks available.\n", $num), $num);
1543 elsif ($line =~ /^d/i) {
1544 while ($ix < $num) {
1545 if (!defined $hunk[$ix]{USE}) {
1546 $hunk[$ix]{USE} = 0;
1552 elsif ($line =~ /^q/i) {
1553 for ($i = 0; $i < $num; $i++) {
1554 if (!defined $hunk[$i]{USE}) {
1561 elsif ($line =~ m|^/(.*)|) {
1563 unless ($other =~ m|/|) {
1564 error_msg __("No other hunks to search\n");
1568 print colored $prompt_color, __("search for regex? ");
1570 if (defined $regex) {
1576 $search_string = qr{$regex}m;
1579 my ($err,$exp) = ($@, $1);
1580 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1581 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1586 my $text = join ("", @{$hunk[$iy]{TEXT}});
1587 last if ($text =~ $search_string);
1589 $iy = 0 if ($iy >= $num);
1591 error_msg __("No hunk matches the given pattern\n");
1598 elsif ($line =~ /^K/) {
1599 if ($other =~ /K/) {
1603 error_msg __("No previous hunk\n");
1607 elsif ($line =~ /^J/) {
1608 if ($other =~ /J/) {
1612 error_msg __("No next hunk\n");
1616 elsif ($line =~ /^k/) {
1617 if ($other =~ /k/) {
1621 !defined $hunk[$ix]{USE});
1625 error_msg __("No previous hunk\n");
1629 elsif ($line =~ /^j/) {
1630 if ($other !~ /j/) {
1631 error_msg __("No next hunk\n");
1635 elsif ($line =~ /^s/) {
1636 unless ($other =~ /s/) {
1637 error_msg __("Sorry, cannot split this hunk\n");
1640 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1642 print colored $header_color, sprintf(
1643 __n("Split into %d hunk.\n",
1644 "Split into %d hunks.\n",
1645 scalar(@split)), scalar(@split));
1647 splice (@hunk, $ix, 1, @split);
1648 $num = scalar @hunk;
1651 elsif ($line =~ /^e/) {
1652 unless ($other =~ /e/) {
1653 error_msg __("Sorry, cannot edit this hunk\n");
1656 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1657 if (defined $newhunk) {
1658 splice @hunk, $ix, 1, $newhunk;
1662 help_patch_cmd($other);
1668 last if ($ix >= $num ||
1669 !defined $hunk[$ix]{USE});
1674 @hunk = coalesce_overlapping_hunks(@hunk);
1680 push @result, @{$_->{TEXT}};
1685 my @patch = reassemble_patch($head->{TEXT}, @result);
1686 my $apply_routine = $patch_mode_flavour{APPLY};
1687 &$apply_routine(@patch);
1696 my @mods = list_modified('index-only');
1697 @mods = grep { !($_->{BINARY}) } @mods;
1699 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1701 HEADER => $status_head, },
1704 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1705 system(qw(git diff -p --cached), $reference, '--',
1706 map { $_->{VALUE} } @them);
1715 # TRANSLATORS: please do not translate the command names
1716 # 'status', 'update', 'revert', etc.
1717 print colored $help_color, __ <<'EOF' ;
1718 status - show paths with changes
1719 update - add working tree state to the staged set of changes
1720 revert - revert staged set of changes back to the HEAD version
1721 patch - pick hunks and update selectively
1722 diff - view diff between HEAD and index
1723 add untracked - add contents of untracked files to the staged set of changes
1728 return unless @ARGV;
1729 my $arg = shift @ARGV;
1730 if ($arg =~ /--patch(?:=(.*))?/) {
1732 if ($1 eq 'reset') {
1733 $patch_mode = 'reset_head';
1734 $patch_mode_revision = 'HEAD';
1735 $arg = shift @ARGV or die __("missing --");
1737 $patch_mode_revision = $arg;
1738 $patch_mode = ($arg eq 'HEAD' ?
1739 'reset_head' : 'reset_nothead');
1740 $arg = shift @ARGV or die __("missing --");
1742 } elsif ($1 eq 'checkout') {
1743 $arg = shift @ARGV or die __("missing --");
1745 $patch_mode = 'checkout_index';
1747 $patch_mode_revision = $arg;
1748 $patch_mode = ($arg eq 'HEAD' ?
1749 'checkout_head' : 'checkout_nothead');
1750 $arg = shift @ARGV or die __("missing --");
1752 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1754 $arg = shift @ARGV or die __("missing --");
1756 die sprintf(__("unknown --patch mode: %s"), $1);
1759 $patch_mode = 'stage';
1760 $arg = shift @ARGV or die __("missing --");
1762 die sprintf(__("invalid argument %s, expecting --"),
1763 $arg) unless $arg eq "--";
1764 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1765 $patch_mode_only = 1;
1767 elsif ($arg ne "--") {
1768 die sprintf(__("invalid argument %s, expecting --"), $arg);
1773 my @cmd = ([ 'status', \&status_cmd, ],
1774 [ 'update', \&update_cmd, ],
1775 [ 'revert', \&revert_cmd, ],
1776 [ 'add untracked', \&add_untracked_cmd, ],
1777 [ 'patch', \&patch_update_cmd, ],
1778 [ 'diff', \&diff_cmd, ],
1779 [ 'quit', \&quit_cmd, ],
1780 [ 'help', \&help_cmd, ],
1783 my ($it) = list_and_choose({ PROMPT => __('What now'),
1786 HEADER => __('*** Commands ***'),
1787 ON_EOF => \&quit_cmd,
1788 IMMEDIATE => 1 }, @cmd);
1802 if ($patch_mode_only) {