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',
153 DIFF => 'diff-index -p',
154 APPLY => sub { apply_patch 'apply -R', @_ },
155 APPLY_CHECK => 'apply -R',
159 'worktree_nothead' => {
160 DIFF => 'diff-index -R -p',
161 APPLY => sub { apply_patch 'apply', @_ },
162 APPLY_CHECK => 'apply',
168 $patch_mode = 'stage';
169 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
172 if ($^O eq 'MSWin32') {
173 my @invalid = grep {m/[":*]/} @_;
174 die "$^O does not support: @invalid\n" if @invalid;
175 my @args = map { m/ /o ? "\"$_\"": $_ } @_;
179 open($fh, '-|', @_) or die;
184 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
186 if (!defined $GIT_DIR) {
187 exit(1); # rev-parse would have already said "not a git repo"
193 open $fh, 'git update-index --refresh |'
196 ;# ignore 'needs update'
206 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
209 # TRANSLATORS: you can adjust this to align "git add -i" status menu
210 my $status_fmt = __('%12s %12s %s');
211 my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
215 sub is_initial_commit {
216 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
217 unless defined $initial;
225 return $empty_tree if defined $empty_tree;
227 $empty_tree = run_cmd_pipe(qw(git hash-object -t tree /dev/null));
233 sub get_diff_reference {
235 if (defined $ref and $ref ne 'HEAD') {
237 } elsif (is_initial_commit()) {
238 return get_empty_tree();
244 # Returns list of hashes, contents of each of which are:
246 # BINARY: is a binary path
247 # INDEX: is index different from HEAD?
248 # FILE: is file different from index?
249 # INDEX_ADDDEL: is it add/delete between HEAD and index?
250 # FILE_ADDDEL: is it add/delete between index and file?
251 # UNMERGED: is the path unmerged
256 my ($add, $del, $adddel, $file);
258 my $reference = get_diff_reference($patch_mode_revision);
259 for (run_cmd_pipe(qw(git diff-index --cached
260 --numstat --summary), $reference,
262 if (($add, $del, $file) =
263 /^([-\d]+) ([-\d]+) (.*)/) {
265 $file = unquote_path($file);
266 if ($add eq '-' && $del eq '-') {
267 $change = __('binary');
271 $change = "+$add/-$del";
276 FILE => __('nothing'),
279 elsif (($adddel, $file) =
280 /^ (create|delete) mode [0-7]+ (.*)$/) {
281 $file = unquote_path($file);
282 $data{$file}{INDEX_ADDDEL} = $adddel;
286 for (run_cmd_pipe(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) {
287 if (($add, $del, $file) =
288 /^([-\d]+) ([-\d]+) (.*)/) {
289 $file = unquote_path($file);
291 if ($add eq '-' && $del eq '-') {
292 $change = __('binary');
296 $change = "+$add/-$del";
298 $data{$file}{FILE} = $change;
300 $data{$file}{BINARY} = 1;
303 elsif (($adddel, $file) =
304 /^ (create|delete) mode [0-7]+ (.*)$/) {
305 $file = unquote_path($file);
306 $data{$file}{FILE_ADDDEL} = $adddel;
308 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
309 $file = unquote_path($2);
310 if (!exists $data{$file}) {
312 INDEX => __('unchanged'),
317 $data{$file}{UNMERGED} = 1;
322 for (sort keys %data) {
326 if ($only eq 'index-only') {
327 next if ($it->{INDEX} eq __('unchanged'));
329 if ($only eq 'file-only') {
330 next if ($it->{FILE} eq __('nothing'));
342 my ($string, @stuff) = @_;
344 for (my $i = 0; $i < @stuff; $i++) {
348 if ((ref $it) eq 'ARRAY') {
356 if ($it =~ /^$string/) {
360 if (defined $hit && defined $found) {
370 # inserts string into trie and updates count for each character
372 my ($trie, $string) = @_;
373 foreach (split //, $string) {
374 $trie = $trie->{$_} ||= {COUNT => 0};
379 # returns an array of tuples (prefix, remainder)
380 sub find_unique_prefixes {
384 # any single prefix exceeding the soft limit is omitted
385 # if any prefix exceeds the hard limit all are omitted
386 # 0 indicates no limit
390 # build a trie modelling all possible options
392 foreach my $print (@stuff) {
393 if ((ref $print) eq 'ARRAY') {
394 $print = $print->[0];
396 elsif ((ref $print) eq 'HASH') {
397 $print = $print->{VALUE};
399 update_trie(\%trie, $print);
400 push @return, $print;
403 # use the trie to find the unique prefixes
404 for (my $i = 0; $i < @return; $i++) {
405 my $ret = $return[$i];
406 my @letters = split //, $ret;
408 my ($prefix, $remainder);
410 for ($j = 0; $j < @letters; $j++) {
411 my $letter = $letters[$j];
412 if ($search{$letter}{COUNT} == 1) {
413 $prefix = substr $ret, 0, $j + 1;
414 $remainder = substr $ret, $j + 1;
418 my $prefix = substr $ret, 0, $j;
420 if ($hard_limit && $j + 1 > $hard_limit);
422 %search = %{$search{$letter}};
424 if (ord($letters[0]) > 127 ||
425 ($soft_limit && $j + 1 > $soft_limit)) {
429 $return[$i] = [$prefix, $remainder];
434 # filters out prefixes which have special meaning to list_and_choose()
435 sub is_valid_prefix {
437 return (defined $prefix) &&
438 !($prefix =~ /[\s,]/) && # separators
439 !($prefix =~ /^-/) && # deselection
440 !($prefix =~ /^\d+/) && # selection
441 ($prefix ne '*') && # "all" wildcard
442 ($prefix ne '?'); # prompt help
445 # given a prefix/remainder tuple return a string with the prefix highlighted
446 # for now use square brackets; later might use ANSI colors (underline, bold)
447 sub highlight_prefix {
449 my $remainder = shift;
451 if (!defined $prefix) {
455 if (!is_valid_prefix($prefix)) {
456 return "$prefix$remainder";
459 if (!$menu_use_color) {
460 return "[$prefix]$remainder";
463 return "$prompt_color$prefix$normal_color$remainder";
467 print STDERR colored $error_color, @_;
470 sub list_and_choose {
471 my ($opts, @stuff) = @_;
472 my (@chosen, @return);
477 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
483 if ($opts->{HEADER}) {
484 if (!$opts->{LIST_FLAT}) {
487 print colored $header_color, "$opts->{HEADER}\n";
489 for ($i = 0; $i < @stuff; $i++) {
490 my $chosen = $chosen[$i] ? '*' : ' ';
491 my $print = $stuff[$i];
492 my $ref = ref $print;
493 my $highlighted = highlight_prefix(@{$prefixes[$i]})
495 if ($ref eq 'ARRAY') {
496 $print = $highlighted || $print->[0];
498 elsif ($ref eq 'HASH') {
499 my $value = $highlighted || $print->{VALUE};
500 $print = sprintf($status_fmt,
506 $print = $highlighted || $print;
508 printf("%s%2d: %s", $chosen, $i+1, $print);
509 if (($opts->{LIST_FLAT}) &&
510 (($i + 1) % ($opts->{LIST_FLAT}))) {
523 return if ($opts->{LIST_ONLY});
525 print colored $prompt_color, $opts->{PROMPT};
526 if ($opts->{SINGLETON}) {
535 $opts->{ON_EOF}->() if $opts->{ON_EOF};
542 singleton_prompt_help_cmd() :
546 for my $choice (split(/[\s,]+/, $line)) {
550 # Input that begins with '-'; unchoose
551 if ($choice =~ s/^-//) {
554 # A range can be specified like 5-7 or 5-.
555 if ($choice =~ /^(\d+)-(\d*)$/) {
556 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
558 elsif ($choice =~ /^\d+$/) {
559 $bottom = $top = $choice;
561 elsif ($choice eq '*') {
566 $bottom = $top = find_unique($choice, @stuff);
567 if (!defined $bottom) {
568 error_msg sprintf(__("Huh (%s)?\n"), $choice);
572 if ($opts->{SINGLETON} && $bottom != $top) {
573 error_msg sprintf(__("Huh (%s)?\n"), $choice);
576 for ($i = $bottom-1; $i <= $top-1; $i++) {
577 next if (@stuff <= $i || $i < 0);
578 $chosen[$i] = $choose;
581 last if ($opts->{IMMEDIATE} || $line eq '*');
583 for ($i = 0; $i < @stuff; $i++) {
585 push @return, $stuff[$i];
591 sub singleton_prompt_help_cmd {
592 print colored $help_color, __ <<'EOF' ;
594 1 - select a numbered item
595 foo - select item based on unique prefix
596 - (empty) select nothing
600 sub prompt_help_cmd {
601 print colored $help_color, __ <<'EOF' ;
603 1 - select a single item
604 3-5 - select a range of items
605 2-3,6-9 - select multiple ranges
606 foo - select item based on unique prefix
607 -... - unselect specified items
609 - (empty) finish selecting
614 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
622 if ($did eq 'added') {
623 printf(__n("added %d path\n", "added %d paths\n",
625 } elsif ($did eq 'updated') {
626 printf(__n("updated %d path\n", "updated %d paths\n",
628 } elsif ($did eq 'reverted') {
629 printf(__n("reverted %d path\n", "reverted %d paths\n",
632 printf(__n("touched %d path\n", "touched %d paths\n",
638 my @mods = list_modified('file-only');
641 my @update = list_and_choose({ PROMPT => __('Update'),
642 HEADER => $status_head, },
645 system(qw(git update-index --add --remove --),
646 map { $_->{VALUE} } @update);
647 say_n_paths('updated', @update);
653 my @update = list_and_choose({ PROMPT => __('Revert'),
654 HEADER => $status_head, },
657 if (is_initial_commit()) {
658 system(qw(git rm --cached),
659 map { $_->{VALUE} } @update);
662 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
663 map { $_->{VALUE} } @update);
665 open $fh, '| git update-index --index-info'
672 if ($_->{INDEX_ADDDEL} &&
673 $_->{INDEX_ADDDEL} eq 'create') {
674 system(qw(git update-index --force-remove --),
676 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
681 say_n_paths('reverted', @update);
686 sub add_untracked_cmd {
687 my @add = list_and_choose({ PROMPT => __('Add untracked') },
690 system(qw(git update-index --add --), @add);
691 say_n_paths('added', @add);
693 print __("No untracked files.\n");
701 open $fh, '| git ' . $cmd . " --allow-overlap";
708 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
709 if (defined $diff_algorithm) {
710 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
712 if (defined $patch_mode_revision) {
713 push @diff_cmd, get_diff_reference($patch_mode_revision);
715 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
717 if ($diff_use_color) {
718 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
719 if (defined $diff_filter) {
720 # quotemeta is overkill, but sufficient for shell-quoting
721 my $diff = join(' ', map { quotemeta } @display_cmd);
722 @display_cmd = ("$diff | $diff_filter");
725 @colored = run_cmd_pipe(@display_cmd);
727 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
729 if (@colored && @colored != @diff) {
731 "fatal: mismatched output from interactive.diffFilter\n",
732 "hint: Your filter must maintain a one-to-one correspondence\n",
733 "hint: between its input and output lines.\n";
737 for (my $i = 0; $i < @diff; $i++) {
738 if ($diff[$i] =~ /^@@ /) {
739 push @hunk, { TEXT => [], DISPLAY => [],
742 push @{$hunk[-1]{TEXT}}, $diff[$i];
743 push @{$hunk[-1]{DISPLAY}},
744 (@colored ? $colored[$i] : $diff[$i]);
749 sub parse_diff_header {
752 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
753 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
754 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
756 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
758 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
759 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
761 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
762 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
764 return ($head, $mode, $deletion);
767 sub hunk_splittable {
770 my @s = split_hunk($text);
774 sub parse_hunk_header {
776 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
777 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
778 $o_cnt = 1 unless defined $o_cnt;
779 $n_cnt = 1 unless defined $n_cnt;
780 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
783 sub format_hunk_header {
784 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
785 return ("@@ -$o_ofs" .
786 (($o_cnt != 1) ? ",$o_cnt" : '') .
788 (($n_cnt != 1) ? ",$n_cnt" : '') .
793 my ($text, $display) = @_;
795 if (!defined $display) {
798 # If there are context lines in the middle of a hunk,
799 # it can be split, but we would need to take care of
802 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
807 my $next_hunk_start = undef;
808 my $i = $hunk_start - 1;
822 while (++$i < @$text) {
823 my $line = $text->[$i];
824 my $display = $display->[$i];
825 if ($line =~ /^\\/) {
826 push @{$this->{TEXT}}, $line;
827 push @{$this->{DISPLAY}}, $display;
831 if ($this->{ADDDEL} &&
832 !defined $next_hunk_start) {
833 # We have seen leading context and
834 # adds/dels and then here is another
835 # context, which is trailing for this
836 # split hunk and leading for the next
838 $next_hunk_start = $i;
840 push @{$this->{TEXT}}, $line;
841 push @{$this->{DISPLAY}}, $display;
844 if (defined $next_hunk_start) {
851 if (defined $next_hunk_start) {
852 # We are done with the current hunk and
853 # this is the first real change for the
855 $hunk_start = $next_hunk_start;
856 $o_ofs = $this->{OLD} + $this->{OCNT};
857 $n_ofs = $this->{NEW} + $this->{NCNT};
858 $o_ofs -= $this->{POSTCTX};
859 $n_ofs -= $this->{POSTCTX};
863 push @{$this->{TEXT}}, $line;
864 push @{$this->{DISPLAY}}, $display;
878 for my $hunk (@split) {
879 $o_ofs = $hunk->{OLD};
880 $n_ofs = $hunk->{NEW};
881 my $o_cnt = $hunk->{OCNT};
882 my $n_cnt = $hunk->{NCNT};
884 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
885 my $display_head = $head;
886 unshift @{$hunk->{TEXT}}, $head;
887 if ($diff_use_color) {
888 $display_head = colored($fraginfo_color, $head);
890 unshift @{$hunk->{DISPLAY}}, $display_head;
895 sub find_last_o_ctx {
897 my $text = $it->{TEXT};
898 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
900 my $last_o_ctx = $o_ofs + $o_cnt;
902 my $line = $text->[$i];
913 my ($prev, $this) = @_;
914 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
915 parse_hunk_header($prev->{TEXT}[0]);
916 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
917 parse_hunk_header($this->{TEXT}[0]);
919 my (@line, $i, $ofs, $o_cnt, $n_cnt);
922 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
923 my $line = $prev->{TEXT}[$i];
924 if ($line =~ /^\+/) {
928 } elsif ($line =~ /^\\/) {
933 last if ($o1_ofs <= $ofs);
943 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
944 my $line = $this->{TEXT}[$i];
945 if ($line =~ /^\+/) {
949 } elsif ($line =~ /^\\/) {
960 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
961 @{$prev->{TEXT}} = ($head, @line);
964 sub coalesce_overlapping_hunks {
968 my ($last_o_ctx, $last_was_dirty);
972 if ($_->{TYPE} ne 'hunk') {
976 my $text = $_->{TEXT};
977 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
978 parse_hunk_header($text->[0]);
980 $ofs_delta += $o_cnt - $n_cnt;
981 # If this hunk has been edited then subtract
982 # the delta that is due to the edit.
983 if ($_->{OFS_DELTA}) {
984 $ofs_delta -= $_->{OFS_DELTA};
989 if ($patch_mode_flavour{IS_REVERSE}) {
990 $o_ofs -= $ofs_delta;
992 $n_ofs += $ofs_delta;
994 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
997 # If this hunk was edited then adjust the offset delta
998 # to reflect the edit.
999 if ($_->{OFS_DELTA}) {
1000 $ofs_delta += $_->{OFS_DELTA};
1002 if (defined $last_o_ctx &&
1003 $o_ofs <= $last_o_ctx &&
1006 merge_hunk($out[-1], $_);
1011 $last_o_ctx = find_last_o_ctx($out[-1]);
1012 $last_was_dirty = $_->{DIRTY};
1017 sub reassemble_patch {
1021 # Include everything in the header except the beginning of the diff.
1022 push @patch, (grep { !/^[-+]{3}/ } @$head);
1024 # Then include any headers from the hunk lines, which must
1025 # come before any actual hunk.
1026 while (@_ && $_[0] !~ /^@/) {
1030 # Then begin the diff.
1031 push @patch, grep { /^[-+]{3}/ } @$head;
1033 # And then the actual hunks.
1041 colored((/^@/ ? $fraginfo_color :
1042 /^\+/ ? $diff_new_color :
1043 /^-/ ? $diff_old_color :
1049 my %edit_hunk_manually_modes = (
1051 "If the patch applies cleanly, the edited hunk will immediately be
1052 marked for staging."),
1054 "If the patch applies cleanly, the edited hunk will immediately be
1055 marked for stashing."),
1057 "If the patch applies cleanly, the edited hunk will immediately be
1058 marked for unstaging."),
1059 reset_nothead => N__(
1060 "If the patch applies cleanly, the edited hunk will immediately be
1061 marked for applying."),
1062 checkout_index => N__(
1063 "If the patch applies cleanly, the edited hunk will immediately be
1064 marked for discarding."),
1065 checkout_head => N__(
1066 "If the patch applies cleanly, the edited hunk will immediately be
1067 marked for discarding."),
1068 checkout_nothead => N__(
1069 "If the patch applies cleanly, the edited hunk will immediately be
1070 marked for applying."),
1071 worktree_head => N__(
1072 "If the patch applies cleanly, the edited hunk will immediately be
1073 marked for discarding."),
1074 worktree_nothead => N__(
1075 "If the patch applies cleanly, the edited hunk will immediately be
1076 marked for applying."),
1079 sub recount_edited_hunk {
1081 my ($oldtext, $newtext) = @_;
1082 my ($o_cnt, $n_cnt) = (0, 0);
1083 for (@{$newtext}[1..$#{$newtext}]) {
1084 my $mode = substr($_, 0, 1);
1087 } elsif ($mode eq '+') {
1089 } elsif ($mode eq ' ' or $mode eq "\n") {
1094 my ($o_ofs, undef, $n_ofs, undef) =
1095 parse_hunk_header($newtext->[0]);
1096 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1097 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1098 parse_hunk_header($oldtext->[0]);
1099 # Return the change in the number of lines inserted by this hunk
1100 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1103 sub edit_hunk_manually {
1106 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1108 open $fh, '>', $hunkfile
1109 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1110 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1111 print $fh @$oldtext;
1112 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1113 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1114 my $comment_line_char = Git::get_comment_line_char;
1115 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1117 To remove '%s' lines, make them ' ' lines (context).
1118 To remove '%s' lines, delete them.
1119 Lines starting with %s will be removed.
1121 __($edit_hunk_manually_modes{$patch_mode}),
1122 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1124 If it does not apply cleanly, you will be given an opportunity to
1125 edit again. If all lines of the hunk are removed, then the edit is
1126 aborted and the hunk is left unchanged.
1130 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1131 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1137 open $fh, '<', $hunkfile
1138 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1139 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1143 # Abort if nothing remains
1144 if (!grep { /\S/ } @newtext) {
1148 # Reinsert the first hunk header if the user accidentally deleted it
1149 if ($newtext[0] !~ /^@/) {
1150 unshift @newtext, $oldtext->[0];
1156 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1157 map { @{$_->{TEXT}} } @_);
1160 sub _restore_terminal_and_die {
1166 sub prompt_single_character {
1168 local $SIG{TERM} = \&_restore_terminal_and_die;
1169 local $SIG{INT} = \&_restore_terminal_and_die;
1171 my $key = ReadKey 0;
1173 if ($use_termcap and $key eq "\e") {
1174 while (!defined $term_escapes{$key}) {
1175 my $next = ReadKey 0.5;
1176 last if (!defined $next);
1181 print "$key" if defined $key;
1192 print colored $prompt_color, $prompt;
1193 my $line = prompt_single_character;
1194 return undef unless defined $line;
1195 return 0 if $line =~ /^n/i;
1196 return 1 if $line =~ /^y/i;
1200 sub edit_hunk_loop {
1201 my ($head, $hunks, $ix) = @_;
1202 my $hunk = $hunks->[$ix];
1203 my $text = $hunk->{TEXT};
1206 my $newtext = edit_hunk_manually($text);
1207 if (!defined $newtext) {
1212 TYPE => $hunk->{TYPE},
1216 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1217 # If this hunk has already been edited then add the
1218 # offset delta of the previous edit to get the real
1219 # delta from the original unedited hunk.
1220 $hunk->{OFS_DELTA} and
1221 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1222 if (diff_applies($head,
1223 @{$hunks}[0..$ix-1],
1225 @{$hunks}[$ix+1..$#{$hunks}])) {
1226 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1231 # TRANSLATORS: do not translate [y/n]
1232 # The program will only accept that input
1234 # Consider translating (saying "no" discards!) as
1235 # (saying "n" for "no" discards!) if the translation
1236 # of the word "no" does not start with n.
1237 __('Your edited hunk does not apply. Edit again '
1238 . '(saying "no" discards!) [y/n]? ')
1244 my %help_patch_modes = (
1246 "y - stage this hunk
1247 n - do not stage this hunk
1248 q - quit; do not stage this hunk or any of the remaining ones
1249 a - stage this hunk and all later hunks in the file
1250 d - do not stage this hunk or any of the later hunks in the file"),
1252 "y - stash this hunk
1253 n - do not stash this hunk
1254 q - quit; do not stash this hunk or any of the remaining ones
1255 a - stash this hunk and all later hunks in the file
1256 d - do not stash this hunk or any of the later hunks in the file"),
1258 "y - unstage this hunk
1259 n - do not unstage this hunk
1260 q - quit; do not unstage this hunk or any of the remaining ones
1261 a - unstage this hunk and all later hunks in the file
1262 d - do not unstage this hunk or any of the later hunks in the file"),
1263 reset_nothead => N__(
1264 "y - apply this hunk to index
1265 n - do not apply this hunk to index
1266 q - quit; do not apply this hunk or any of the remaining ones
1267 a - apply this hunk and all later hunks in the file
1268 d - do not apply this hunk or any of the later hunks in the file"),
1269 checkout_index => N__(
1270 "y - discard this hunk from worktree
1271 n - do not discard this hunk from worktree
1272 q - quit; do not discard this hunk or any of the remaining ones
1273 a - discard this hunk and all later hunks in the file
1274 d - do not discard this hunk or any of the later hunks in the file"),
1275 checkout_head => N__(
1276 "y - discard this hunk from index and worktree
1277 n - do not discard this hunk from index and worktree
1278 q - quit; do not discard this hunk or any of the remaining ones
1279 a - discard this hunk and all later hunks in the file
1280 d - do not discard this hunk or any of the later hunks in the file"),
1281 checkout_nothead => N__(
1282 "y - apply this hunk to index and worktree
1283 n - do not apply this hunk to index and worktree
1284 q - quit; do not apply this hunk or any of the remaining ones
1285 a - apply this hunk and all later hunks in the file
1286 d - do not apply this hunk or any of the later hunks in the file"),
1287 worktree_head => N__(
1288 "y - discard this hunk from worktree
1289 n - do not discard this hunk from worktree
1290 q - quit; do not discard this hunk or any of the remaining ones
1291 a - discard this hunk and all later hunks in the file
1292 d - do not discard this hunk or any of the later hunks in the file"),
1293 worktree_nothead => N__(
1294 "y - apply this hunk to worktree
1295 n - do not apply this hunk to worktree
1296 q - quit; do not apply this hunk or any of the remaining ones
1297 a - apply this hunk and all later hunks in the file
1298 d - do not apply this hunk or any of the later hunks in the file"),
1301 sub help_patch_cmd {
1303 my $other = $_[0] . ",?";
1304 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1305 map { "$_\n" } grep {
1306 my $c = quotemeta(substr($_, 0, 1));
1308 } split "\n", __ <<EOF ;
1309 g - select a hunk to go to
1310 / - search for a hunk matching the given regex
1311 j - leave this hunk undecided, see next undecided hunk
1312 J - leave this hunk undecided, see next hunk
1313 k - leave this hunk undecided, see previous undecided hunk
1314 K - leave this hunk undecided, see previous hunk
1315 s - split the current hunk into smaller hunks
1316 e - manually edit the current hunk
1323 my $ret = run_git_apply $cmd, @_;
1330 sub apply_patch_for_checkout_commit {
1331 my $reverse = shift;
1332 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1333 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1335 if ($applies_worktree && $applies_index) {
1336 run_git_apply 'apply '.$reverse.' --cached', @_;
1337 run_git_apply 'apply '.$reverse, @_;
1339 } elsif (!$applies_index) {
1340 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1341 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1342 return run_git_apply 'apply '.$reverse, @_;
1344 print colored $error_color, __("Nothing was applied.\n");
1353 sub patch_update_cmd {
1354 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1355 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1356 for grep { $_->{UNMERGED} } @all_mods;
1357 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1359 my @mods = grep { !($_->{BINARY}) } @all_mods;
1364 print STDERR __("Only binary files changed.\n");
1366 print STDERR __("No changes.\n");
1370 if ($patch_mode_only) {
1374 @them = list_and_choose({ PROMPT => __('Patch update'),
1375 HEADER => $status_head, },
1379 return 0 if patch_update_file($_->{VALUE});
1383 # Generate a one line summary of a hunk.
1384 sub summarize_hunk {
1386 my $summary = $rhunk->{TEXT}[0];
1388 # Keep the line numbers, discard extra context.
1389 $summary =~ s/@@(.*?)@@.*/$1 /s;
1390 $summary .= " " x (20 - length $summary);
1392 # Add some user context.
1393 for my $line (@{$rhunk->{TEXT}}) {
1394 if ($line =~ m/^[+-].*\w/) {
1401 return substr($summary, 0, 80) . "\n";
1405 # Print a one-line summary of each hunk in the array ref in
1406 # the first argument, starting with the index in the 2nd.
1408 my ($hunks, $i) = @_;
1411 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1413 if (defined $hunks->[$i]{USE}) {
1414 $status = $hunks->[$i]{USE} ? "+" : "-";
1419 summarize_hunk($hunks->[$i]);
1424 my %patch_update_prompt_modes = (
1426 mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1427 deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1428 hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1431 mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1432 deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1433 hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1436 mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1437 deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1438 hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1441 mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1442 deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1443 hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1446 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1447 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1448 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1451 mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1452 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1453 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1455 checkout_nothead => {
1456 mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1457 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1458 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1461 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1462 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1463 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1465 worktree_nothead => {
1466 mode => N__("Apply mode change to worktree [y,n,q,a,d%s,?]? "),
1467 deletion => N__("Apply deletion to worktree [y,n,q,a,d%s,?]? "),
1468 hunk => N__("Apply this hunk to worktree [y,n,q,a,d%s,?]? "),
1472 sub patch_update_file {
1476 my ($head, @hunk) = parse_diff($path);
1477 ($head, my $mode, my $deletion) = parse_diff_header($head);
1478 for (@{$head->{DISPLAY}}) {
1482 if (@{$mode->{TEXT}}) {
1483 unshift @hunk, $mode;
1485 if (@{$deletion->{TEXT}}) {
1486 foreach my $hunk (@hunk) {
1487 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1488 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1490 @hunk = ($deletion);
1493 $num = scalar @hunk;
1497 my ($prev, $next, $other, $undecided, $i);
1503 for ($i = 0; $i < $ix; $i++) {
1504 if (!defined $hunk[$i]{USE}) {
1513 for ($i = $ix + 1; $i < $num; $i++) {
1514 if (!defined $hunk[$i]{USE}) {
1520 if ($ix < $num - 1) {
1526 for ($i = 0; $i < $num; $i++) {
1527 if (!defined $hunk[$i]{USE}) {
1532 last if (!$undecided);
1534 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1535 hunk_splittable($hunk[$ix]{TEXT})) {
1538 if ($hunk[$ix]{TYPE} eq 'hunk') {
1541 for (@{$hunk[$ix]{DISPLAY}}) {
1544 print colored $prompt_color,
1545 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1547 my $line = prompt_single_character;
1548 last unless defined $line;
1550 if ($line =~ /^y/i) {
1551 $hunk[$ix]{USE} = 1;
1553 elsif ($line =~ /^n/i) {
1554 $hunk[$ix]{USE} = 0;
1556 elsif ($line =~ /^a/i) {
1557 while ($ix < $num) {
1558 if (!defined $hunk[$ix]{USE}) {
1559 $hunk[$ix]{USE} = 1;
1565 elsif ($line =~ /^g(.*)/) {
1567 unless ($other =~ /g/) {
1568 error_msg __("No other hunks to goto\n");
1571 my $no = $ix > 10 ? $ix - 10 : 0;
1572 while ($response eq '') {
1573 $no = display_hunks(\@hunk, $no);
1575 print __("go to which hunk (<ret> to see more)? ");
1577 print __("go to which hunk? ");
1579 $response = <STDIN>;
1580 if (!defined $response) {
1585 if ($response !~ /^\s*\d+\s*$/) {
1586 error_msg sprintf(__("Invalid number: '%s'\n"),
1588 } elsif (0 < $response && $response <= $num) {
1589 $ix = $response - 1;
1591 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1592 "Sorry, only %d hunks available.\n", $num), $num);
1596 elsif ($line =~ /^d/i) {
1597 while ($ix < $num) {
1598 if (!defined $hunk[$ix]{USE}) {
1599 $hunk[$ix]{USE} = 0;
1605 elsif ($line =~ /^q/i) {
1606 for ($i = 0; $i < $num; $i++) {
1607 if (!defined $hunk[$i]{USE}) {
1614 elsif ($line =~ m|^/(.*)|) {
1616 unless ($other =~ m|/|) {
1617 error_msg __("No other hunks to search\n");
1621 print colored $prompt_color, __("search for regex? ");
1623 if (defined $regex) {
1629 $search_string = qr{$regex}m;
1632 my ($err,$exp) = ($@, $1);
1633 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1634 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1639 my $text = join ("", @{$hunk[$iy]{TEXT}});
1640 last if ($text =~ $search_string);
1642 $iy = 0 if ($iy >= $num);
1644 error_msg __("No hunk matches the given pattern\n");
1651 elsif ($line =~ /^K/) {
1652 if ($other =~ /K/) {
1656 error_msg __("No previous hunk\n");
1660 elsif ($line =~ /^J/) {
1661 if ($other =~ /J/) {
1665 error_msg __("No next hunk\n");
1669 elsif ($line =~ /^k/) {
1670 if ($other =~ /k/) {
1674 !defined $hunk[$ix]{USE});
1678 error_msg __("No previous hunk\n");
1682 elsif ($line =~ /^j/) {
1683 if ($other !~ /j/) {
1684 error_msg __("No next hunk\n");
1688 elsif ($line =~ /^s/) {
1689 unless ($other =~ /s/) {
1690 error_msg __("Sorry, cannot split this hunk\n");
1693 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1695 print colored $header_color, sprintf(
1696 __n("Split into %d hunk.\n",
1697 "Split into %d hunks.\n",
1698 scalar(@split)), scalar(@split));
1700 splice (@hunk, $ix, 1, @split);
1701 $num = scalar @hunk;
1704 elsif ($line =~ /^e/) {
1705 unless ($other =~ /e/) {
1706 error_msg __("Sorry, cannot edit this hunk\n");
1709 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1710 if (defined $newhunk) {
1711 splice @hunk, $ix, 1, $newhunk;
1715 help_patch_cmd($other);
1721 last if ($ix >= $num ||
1722 !defined $hunk[$ix]{USE});
1727 @hunk = coalesce_overlapping_hunks(@hunk);
1733 push @result, @{$_->{TEXT}};
1738 my @patch = reassemble_patch($head->{TEXT}, @result);
1739 my $apply_routine = $patch_mode_flavour{APPLY};
1740 &$apply_routine(@patch);
1749 my @mods = list_modified('index-only');
1750 @mods = grep { !($_->{BINARY}) } @mods;
1752 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1754 HEADER => $status_head, },
1757 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1758 system(qw(git diff -p --cached), $reference, '--',
1759 map { $_->{VALUE} } @them);
1768 # TRANSLATORS: please do not translate the command names
1769 # 'status', 'update', 'revert', etc.
1770 print colored $help_color, __ <<'EOF' ;
1771 status - show paths with changes
1772 update - add working tree state to the staged set of changes
1773 revert - revert staged set of changes back to the HEAD version
1774 patch - pick hunks and update selectively
1775 diff - view diff between HEAD and index
1776 add untracked - add contents of untracked files to the staged set of changes
1781 return unless @ARGV;
1782 my $arg = shift @ARGV;
1783 if ($arg =~ /--patch(?:=(.*))?/) {
1785 if ($1 eq 'reset') {
1786 $patch_mode = 'reset_head';
1787 $patch_mode_revision = 'HEAD';
1788 $arg = shift @ARGV or die __("missing --");
1790 $patch_mode_revision = $arg;
1791 $patch_mode = ($arg eq 'HEAD' ?
1792 'reset_head' : 'reset_nothead');
1793 $arg = shift @ARGV or die __("missing --");
1795 } elsif ($1 eq 'checkout') {
1796 $arg = shift @ARGV or die __("missing --");
1798 $patch_mode = 'checkout_index';
1800 $patch_mode_revision = $arg;
1801 $patch_mode = ($arg eq 'HEAD' ?
1802 'checkout_head' : 'checkout_nothead');
1803 $arg = shift @ARGV or die __("missing --");
1805 } elsif ($1 eq 'worktree') {
1806 $arg = shift @ARGV or die __("missing --");
1808 $patch_mode = 'checkout_index';
1810 $patch_mode_revision = $arg;
1811 $patch_mode = ($arg eq 'HEAD' ?
1812 'worktree_head' : 'worktree_nothead');
1813 $arg = shift @ARGV or die __("missing --");
1815 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1817 $arg = shift @ARGV or die __("missing --");
1819 die sprintf(__("unknown --patch mode: %s"), $1);
1822 $patch_mode = 'stage';
1823 $arg = shift @ARGV or die __("missing --");
1825 die sprintf(__("invalid argument %s, expecting --"),
1826 $arg) unless $arg eq "--";
1827 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1828 $patch_mode_only = 1;
1830 elsif ($arg ne "--") {
1831 die sprintf(__("invalid argument %s, expecting --"), $arg);
1836 my @cmd = ([ 'status', \&status_cmd, ],
1837 [ 'update', \&update_cmd, ],
1838 [ 'revert', \&revert_cmd, ],
1839 [ 'add untracked', \&add_untracked_cmd, ],
1840 [ 'patch', \&patch_update_cmd, ],
1841 [ 'diff', \&diff_cmd, ],
1842 [ 'quit', \&quit_cmd, ],
1843 [ 'help', \&help_cmd, ],
1846 my ($it) = list_and_choose({ PROMPT => __('What now'),
1849 HEADER => __('*** Commands ***'),
1850 ON_EOF => \&quit_cmd,
1851 IMMEDIATE => 1 }, @cmd);
1865 if ($patch_mode_only) {