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;
181 close $fh || die "Cannot close @_ ($!)";
186 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
188 if (!defined $GIT_DIR) {
189 exit(1); # rev-parse would have already said "not a git repo"
195 open $fh, 'git update-index --refresh |'
198 ;# ignore 'needs update'
208 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
211 # TRANSLATORS: you can adjust this to align "git add -i" status menu
212 my $status_fmt = __('%12s %12s %s');
213 my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
217 sub is_initial_commit {
218 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
219 unless defined $initial;
227 return $empty_tree if defined $empty_tree;
229 ($empty_tree) = run_cmd_pipe(qw(git hash-object -t tree /dev/null));
235 sub get_diff_reference {
237 if (defined $ref and $ref ne 'HEAD') {
239 } elsif (is_initial_commit()) {
240 return get_empty_tree();
246 # Returns list of hashes, contents of each of which are:
248 # BINARY: is a binary path
249 # INDEX: is index different from HEAD?
250 # FILE: is file different from index?
251 # INDEX_ADDDEL: is it add/delete between HEAD and index?
252 # FILE_ADDDEL: is it add/delete between index and file?
253 # UNMERGED: is the path unmerged
258 my ($add, $del, $adddel, $file);
260 my $reference = get_diff_reference($patch_mode_revision);
261 for (run_cmd_pipe(qw(git diff-index --cached
262 --numstat --summary), $reference,
264 if (($add, $del, $file) =
265 /^([-\d]+) ([-\d]+) (.*)/) {
267 $file = unquote_path($file);
268 if ($add eq '-' && $del eq '-') {
269 $change = __('binary');
273 $change = "+$add/-$del";
278 FILE => __('nothing'),
281 elsif (($adddel, $file) =
282 /^ (create|delete) mode [0-7]+ (.*)$/) {
283 $file = unquote_path($file);
284 $data{$file}{INDEX_ADDDEL} = $adddel;
288 for (run_cmd_pipe(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) {
289 if (($add, $del, $file) =
290 /^([-\d]+) ([-\d]+) (.*)/) {
291 $file = unquote_path($file);
293 if ($add eq '-' && $del eq '-') {
294 $change = __('binary');
298 $change = "+$add/-$del";
300 $data{$file}{FILE} = $change;
302 $data{$file}{BINARY} = 1;
305 elsif (($adddel, $file) =
306 /^ (create|delete) mode [0-7]+ (.*)$/) {
307 $file = unquote_path($file);
308 $data{$file}{FILE_ADDDEL} = $adddel;
310 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
311 $file = unquote_path($2);
312 if (!exists $data{$file}) {
314 INDEX => __('unchanged'),
319 $data{$file}{UNMERGED} = 1;
324 for (sort keys %data) {
328 if ($only eq 'index-only') {
329 next if ($it->{INDEX} eq __('unchanged'));
331 if ($only eq 'file-only') {
332 next if ($it->{FILE} eq __('nothing'));
344 my ($string, @stuff) = @_;
346 for (my $i = 0; $i < @stuff; $i++) {
350 if ((ref $it) eq 'ARRAY') {
358 if ($it =~ /^$string/) {
362 if (defined $hit && defined $found) {
372 # inserts string into trie and updates count for each character
374 my ($trie, $string) = @_;
375 foreach (split //, $string) {
376 $trie = $trie->{$_} ||= {COUNT => 0};
381 # returns an array of tuples (prefix, remainder)
382 sub find_unique_prefixes {
386 # any single prefix exceeding the soft limit is omitted
387 # if any prefix exceeds the hard limit all are omitted
388 # 0 indicates no limit
392 # build a trie modelling all possible options
394 foreach my $print (@stuff) {
395 if ((ref $print) eq 'ARRAY') {
396 $print = $print->[0];
398 elsif ((ref $print) eq 'HASH') {
399 $print = $print->{VALUE};
401 update_trie(\%trie, $print);
402 push @return, $print;
405 # use the trie to find the unique prefixes
406 for (my $i = 0; $i < @return; $i++) {
407 my $ret = $return[$i];
408 my @letters = split //, $ret;
410 my ($prefix, $remainder);
412 for ($j = 0; $j < @letters; $j++) {
413 my $letter = $letters[$j];
414 if ($search{$letter}{COUNT} == 1) {
415 $prefix = substr $ret, 0, $j + 1;
416 $remainder = substr $ret, $j + 1;
420 my $prefix = substr $ret, 0, $j;
422 if ($hard_limit && $j + 1 > $hard_limit);
424 %search = %{$search{$letter}};
426 if (ord($letters[0]) > 127 ||
427 ($soft_limit && $j + 1 > $soft_limit)) {
431 $return[$i] = [$prefix, $remainder];
436 # filters out prefixes which have special meaning to list_and_choose()
437 sub is_valid_prefix {
439 return (defined $prefix) &&
440 !($prefix =~ /[\s,]/) && # separators
441 !($prefix =~ /^-/) && # deselection
442 !($prefix =~ /^\d+/) && # selection
443 ($prefix ne '*') && # "all" wildcard
444 ($prefix ne '?'); # prompt help
447 # given a prefix/remainder tuple return a string with the prefix highlighted
448 # for now use square brackets; later might use ANSI colors (underline, bold)
449 sub highlight_prefix {
451 my $remainder = shift;
453 if (!defined $prefix) {
457 if (!is_valid_prefix($prefix)) {
458 return "$prefix$remainder";
461 if (!$menu_use_color) {
462 return "[$prefix]$remainder";
465 return "$prompt_color$prefix$normal_color$remainder";
469 print STDERR colored $error_color, @_;
472 sub list_and_choose {
473 my ($opts, @stuff) = @_;
474 my (@chosen, @return);
479 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
485 if ($opts->{HEADER}) {
486 if (!$opts->{LIST_FLAT}) {
489 print colored $header_color, "$opts->{HEADER}\n";
491 for ($i = 0; $i < @stuff; $i++) {
492 my $chosen = $chosen[$i] ? '*' : ' ';
493 my $print = $stuff[$i];
494 my $ref = ref $print;
495 my $highlighted = highlight_prefix(@{$prefixes[$i]})
497 if ($ref eq 'ARRAY') {
498 $print = $highlighted || $print->[0];
500 elsif ($ref eq 'HASH') {
501 my $value = $highlighted || $print->{VALUE};
502 $print = sprintf($status_fmt,
508 $print = $highlighted || $print;
510 printf("%s%2d: %s", $chosen, $i+1, $print);
511 if (($opts->{LIST_FLAT}) &&
512 (($i + 1) % ($opts->{LIST_FLAT}))) {
525 return if ($opts->{LIST_ONLY});
527 print colored $prompt_color, $opts->{PROMPT};
528 if ($opts->{SINGLETON}) {
537 $opts->{ON_EOF}->() if $opts->{ON_EOF};
544 singleton_prompt_help_cmd() :
548 for my $choice (split(/[\s,]+/, $line)) {
552 # Input that begins with '-'; unchoose
553 if ($choice =~ s/^-//) {
556 # A range can be specified like 5-7 or 5-.
557 if ($choice =~ /^(\d+)-(\d*)$/) {
558 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
560 elsif ($choice =~ /^\d+$/) {
561 $bottom = $top = $choice;
563 elsif ($choice eq '*') {
568 $bottom = $top = find_unique($choice, @stuff);
569 if (!defined $bottom) {
570 error_msg sprintf(__("Huh (%s)?\n"), $choice);
574 if ($opts->{SINGLETON} && $bottom != $top) {
575 error_msg sprintf(__("Huh (%s)?\n"), $choice);
578 for ($i = $bottom-1; $i <= $top-1; $i++) {
579 next if (@stuff <= $i || $i < 0);
580 $chosen[$i] = $choose;
583 last if ($opts->{IMMEDIATE} || $line eq '*');
585 for ($i = 0; $i < @stuff; $i++) {
587 push @return, $stuff[$i];
593 sub singleton_prompt_help_cmd {
594 print colored $help_color, __ <<'EOF' ;
596 1 - select a numbered item
597 foo - select item based on unique prefix
598 - (empty) select nothing
602 sub prompt_help_cmd {
603 print colored $help_color, __ <<'EOF' ;
605 1 - select a single item
606 3-5 - select a range of items
607 2-3,6-9 - select multiple ranges
608 foo - select item based on unique prefix
609 -... - unselect specified items
611 - (empty) finish selecting
616 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
624 if ($did eq 'added') {
625 printf(__n("added %d path\n", "added %d paths\n",
627 } elsif ($did eq 'updated') {
628 printf(__n("updated %d path\n", "updated %d paths\n",
630 } elsif ($did eq 'reverted') {
631 printf(__n("reverted %d path\n", "reverted %d paths\n",
634 printf(__n("touched %d path\n", "touched %d paths\n",
640 my @mods = list_modified('file-only');
643 my @update = list_and_choose({ PROMPT => __('Update'),
644 HEADER => $status_head, },
647 system(qw(git update-index --add --remove --),
648 map { $_->{VALUE} } @update);
649 say_n_paths('updated', @update);
655 my @update = list_and_choose({ PROMPT => __('Revert'),
656 HEADER => $status_head, },
659 if (is_initial_commit()) {
660 system(qw(git rm --cached),
661 map { $_->{VALUE} } @update);
664 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
665 map { $_->{VALUE} } @update);
667 open $fh, '| git update-index --index-info'
674 if ($_->{INDEX_ADDDEL} &&
675 $_->{INDEX_ADDDEL} eq 'create') {
676 system(qw(git update-index --force-remove --),
678 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
683 say_n_paths('reverted', @update);
688 sub add_untracked_cmd {
689 my @add = list_and_choose({ PROMPT => __('Add untracked') },
692 system(qw(git update-index --add --), @add);
693 say_n_paths('added', @add);
695 print __("No untracked files.\n");
703 open $fh, '| git ' . $cmd . " --allow-overlap";
710 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
711 if (defined $diff_algorithm) {
712 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
714 if (defined $patch_mode_revision) {
715 push @diff_cmd, get_diff_reference($patch_mode_revision);
717 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
719 if ($diff_use_color) {
720 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
721 if (defined $diff_filter) {
722 # quotemeta is overkill, but sufficient for shell-quoting
723 my $diff = join(' ', map { quotemeta } @display_cmd);
724 @display_cmd = ("$diff | $diff_filter");
727 @colored = run_cmd_pipe(@display_cmd);
729 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
731 if (@colored && @colored != @diff) {
733 "fatal: mismatched output from interactive.diffFilter\n",
734 "hint: Your filter must maintain a one-to-one correspondence\n",
735 "hint: between its input and output lines.\n";
739 for (my $i = 0; $i < @diff; $i++) {
740 if ($diff[$i] =~ /^@@ /) {
741 push @hunk, { TEXT => [], DISPLAY => [],
744 push @{$hunk[-1]{TEXT}}, $diff[$i];
745 push @{$hunk[-1]{DISPLAY}},
746 (@colored ? $colored[$i] : $diff[$i]);
751 sub parse_diff_header {
754 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
755 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
756 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
757 my $addition = { TEXT => [], DISPLAY => [], TYPE => 'addition' };
759 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
761 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
762 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
763 $src->{TEXT}->[$i] =~ /^new file/ ? $addition :
765 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
766 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
768 return ($head, $mode, $deletion, $addition);
771 sub hunk_splittable {
774 my @s = split_hunk($text);
778 sub parse_hunk_header {
780 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
781 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
782 $o_cnt = 1 unless defined $o_cnt;
783 $n_cnt = 1 unless defined $n_cnt;
784 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
787 sub format_hunk_header {
788 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
789 return ("@@ -$o_ofs" .
790 (($o_cnt != 1) ? ",$o_cnt" : '') .
792 (($n_cnt != 1) ? ",$n_cnt" : '') .
797 my ($text, $display) = @_;
799 if (!defined $display) {
802 # If there are context lines in the middle of a hunk,
803 # it can be split, but we would need to take care of
806 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
811 my $next_hunk_start = undef;
812 my $i = $hunk_start - 1;
826 while (++$i < @$text) {
827 my $line = $text->[$i];
828 my $display = $display->[$i];
829 if ($line =~ /^\\/) {
830 push @{$this->{TEXT}}, $line;
831 push @{$this->{DISPLAY}}, $display;
835 if ($this->{ADDDEL} &&
836 !defined $next_hunk_start) {
837 # We have seen leading context and
838 # adds/dels and then here is another
839 # context, which is trailing for this
840 # split hunk and leading for the next
842 $next_hunk_start = $i;
844 push @{$this->{TEXT}}, $line;
845 push @{$this->{DISPLAY}}, $display;
848 if (defined $next_hunk_start) {
855 if (defined $next_hunk_start) {
856 # We are done with the current hunk and
857 # this is the first real change for the
859 $hunk_start = $next_hunk_start;
860 $o_ofs = $this->{OLD} + $this->{OCNT};
861 $n_ofs = $this->{NEW} + $this->{NCNT};
862 $o_ofs -= $this->{POSTCTX};
863 $n_ofs -= $this->{POSTCTX};
867 push @{$this->{TEXT}}, $line;
868 push @{$this->{DISPLAY}}, $display;
882 for my $hunk (@split) {
883 $o_ofs = $hunk->{OLD};
884 $n_ofs = $hunk->{NEW};
885 my $o_cnt = $hunk->{OCNT};
886 my $n_cnt = $hunk->{NCNT};
888 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
889 my $display_head = $head;
890 unshift @{$hunk->{TEXT}}, $head;
891 if ($diff_use_color) {
892 $display_head = colored($fraginfo_color, $head);
894 unshift @{$hunk->{DISPLAY}}, $display_head;
899 sub find_last_o_ctx {
901 my $text = $it->{TEXT};
902 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
904 my $last_o_ctx = $o_ofs + $o_cnt;
906 my $line = $text->[$i];
917 my ($prev, $this) = @_;
918 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
919 parse_hunk_header($prev->{TEXT}[0]);
920 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
921 parse_hunk_header($this->{TEXT}[0]);
923 my (@line, $i, $ofs, $o_cnt, $n_cnt);
926 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
927 my $line = $prev->{TEXT}[$i];
928 if ($line =~ /^\+/) {
932 } elsif ($line =~ /^\\/) {
937 last if ($o1_ofs <= $ofs);
947 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
948 my $line = $this->{TEXT}[$i];
949 if ($line =~ /^\+/) {
953 } elsif ($line =~ /^\\/) {
964 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
965 @{$prev->{TEXT}} = ($head, @line);
968 sub coalesce_overlapping_hunks {
972 my ($last_o_ctx, $last_was_dirty);
976 if ($_->{TYPE} ne 'hunk') {
980 my $text = $_->{TEXT};
981 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
982 parse_hunk_header($text->[0]);
984 $ofs_delta += $o_cnt - $n_cnt;
985 # If this hunk has been edited then subtract
986 # the delta that is due to the edit.
987 if ($_->{OFS_DELTA}) {
988 $ofs_delta -= $_->{OFS_DELTA};
993 if ($patch_mode_flavour{IS_REVERSE}) {
994 $o_ofs -= $ofs_delta;
996 $n_ofs += $ofs_delta;
998 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
1001 # If this hunk was edited then adjust the offset delta
1002 # to reflect the edit.
1003 if ($_->{OFS_DELTA}) {
1004 $ofs_delta += $_->{OFS_DELTA};
1006 if (defined $last_o_ctx &&
1007 $o_ofs <= $last_o_ctx &&
1010 merge_hunk($out[-1], $_);
1015 $last_o_ctx = find_last_o_ctx($out[-1]);
1016 $last_was_dirty = $_->{DIRTY};
1021 sub reassemble_patch {
1025 # Include everything in the header except the beginning of the diff.
1026 push @patch, (grep { !/^[-+]{3}/ } @$head);
1028 # Then include any headers from the hunk lines, which must
1029 # come before any actual hunk.
1030 while (@_ && $_[0] !~ /^@/) {
1034 # Then begin the diff.
1035 push @patch, grep { /^[-+]{3}/ } @$head;
1037 # And then the actual hunks.
1045 colored((/^@/ ? $fraginfo_color :
1046 /^\+/ ? $diff_new_color :
1047 /^-/ ? $diff_old_color :
1053 my %edit_hunk_manually_modes = (
1055 "If the patch applies cleanly, the edited hunk will immediately be
1056 marked for staging."),
1058 "If the patch applies cleanly, the edited hunk will immediately be
1059 marked for stashing."),
1061 "If the patch applies cleanly, the edited hunk will immediately be
1062 marked for unstaging."),
1063 reset_nothead => N__(
1064 "If the patch applies cleanly, the edited hunk will immediately be
1065 marked for applying."),
1066 checkout_index => N__(
1067 "If the patch applies cleanly, the edited hunk will immediately be
1068 marked for discarding."),
1069 checkout_head => N__(
1070 "If the patch applies cleanly, the edited hunk will immediately be
1071 marked for discarding."),
1072 checkout_nothead => N__(
1073 "If the patch applies cleanly, the edited hunk will immediately be
1074 marked for applying."),
1075 worktree_head => N__(
1076 "If the patch applies cleanly, the edited hunk will immediately be
1077 marked for discarding."),
1078 worktree_nothead => N__(
1079 "If the patch applies cleanly, the edited hunk will immediately be
1080 marked for applying."),
1083 sub recount_edited_hunk {
1085 my ($oldtext, $newtext) = @_;
1086 my ($o_cnt, $n_cnt) = (0, 0);
1087 for (@{$newtext}[1..$#{$newtext}]) {
1088 my $mode = substr($_, 0, 1);
1091 } elsif ($mode eq '+') {
1093 } elsif ($mode eq ' ' or $mode eq "\n") {
1098 my ($o_ofs, undef, $n_ofs, undef) =
1099 parse_hunk_header($newtext->[0]);
1100 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1101 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1102 parse_hunk_header($oldtext->[0]);
1103 # Return the change in the number of lines inserted by this hunk
1104 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1107 sub edit_hunk_manually {
1110 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1112 open $fh, '>', $hunkfile
1113 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1114 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1115 print $fh @$oldtext;
1116 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1117 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1118 my $comment_line_char = Git::get_comment_line_char;
1119 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1121 To remove '%s' lines, make them ' ' lines (context).
1122 To remove '%s' lines, delete them.
1123 Lines starting with %s will be removed.
1125 __($edit_hunk_manually_modes{$patch_mode}),
1126 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1128 If it does not apply cleanly, you will be given an opportunity to
1129 edit again. If all lines of the hunk are removed, then the edit is
1130 aborted and the hunk is left unchanged.
1134 chomp(my ($editor) = run_cmd_pipe(qw(git var GIT_EDITOR)));
1135 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1141 open $fh, '<', $hunkfile
1142 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1143 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1147 # Abort if nothing remains
1148 if (!grep { /\S/ } @newtext) {
1152 # Reinsert the first hunk header if the user accidentally deleted it
1153 if ($newtext[0] !~ /^@/) {
1154 unshift @newtext, $oldtext->[0];
1160 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1161 map { @{$_->{TEXT}} } @_);
1164 sub _restore_terminal_and_die {
1170 sub prompt_single_character {
1172 local $SIG{TERM} = \&_restore_terminal_and_die;
1173 local $SIG{INT} = \&_restore_terminal_and_die;
1175 my $key = ReadKey 0;
1177 if ($use_termcap and $key eq "\e") {
1178 while (!defined $term_escapes{$key}) {
1179 my $next = ReadKey 0.5;
1180 last if (!defined $next);
1185 print "$key" if defined $key;
1196 print colored $prompt_color, $prompt;
1197 my $line = prompt_single_character;
1198 return undef unless defined $line;
1199 return 0 if $line =~ /^n/i;
1200 return 1 if $line =~ /^y/i;
1204 sub edit_hunk_loop {
1205 my ($head, $hunks, $ix) = @_;
1206 my $hunk = $hunks->[$ix];
1207 my $text = $hunk->{TEXT};
1210 my $newtext = edit_hunk_manually($text);
1211 if (!defined $newtext) {
1216 TYPE => $hunk->{TYPE},
1220 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1221 # If this hunk has already been edited then add the
1222 # offset delta of the previous edit to get the real
1223 # delta from the original unedited hunk.
1224 $hunk->{OFS_DELTA} and
1225 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1226 if (diff_applies($head,
1227 @{$hunks}[0..$ix-1],
1229 @{$hunks}[$ix+1..$#{$hunks}])) {
1230 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1235 # TRANSLATORS: do not translate [y/n]
1236 # The program will only accept that input
1238 # Consider translating (saying "no" discards!) as
1239 # (saying "n" for "no" discards!) if the translation
1240 # of the word "no" does not start with n.
1241 __('Your edited hunk does not apply. Edit again '
1242 . '(saying "no" discards!) [y/n]? ')
1248 my %help_patch_modes = (
1250 "y - stage this hunk
1251 n - do not stage this hunk
1252 q - quit; do not stage this hunk or any of the remaining ones
1253 a - stage this hunk and all later hunks in the file
1254 d - do not stage this hunk or any of the later hunks in the file"),
1256 "y - stash this hunk
1257 n - do not stash this hunk
1258 q - quit; do not stash this hunk or any of the remaining ones
1259 a - stash this hunk and all later hunks in the file
1260 d - do not stash this hunk or any of the later hunks in the file"),
1262 "y - unstage this hunk
1263 n - do not unstage this hunk
1264 q - quit; do not unstage this hunk or any of the remaining ones
1265 a - unstage this hunk and all later hunks in the file
1266 d - do not unstage this hunk or any of the later hunks in the file"),
1267 reset_nothead => N__(
1268 "y - apply this hunk to index
1269 n - do not apply this hunk to index
1270 q - quit; do not apply this hunk or any of the remaining ones
1271 a - apply this hunk and all later hunks in the file
1272 d - do not apply this hunk or any of the later hunks in the file"),
1273 checkout_index => N__(
1274 "y - discard this hunk from worktree
1275 n - do not discard this hunk from worktree
1276 q - quit; do not discard this hunk or any of the remaining ones
1277 a - discard this hunk and all later hunks in the file
1278 d - do not discard this hunk or any of the later hunks in the file"),
1279 checkout_head => N__(
1280 "y - discard this hunk from index and worktree
1281 n - do not discard this hunk from index and worktree
1282 q - quit; do not discard this hunk or any of the remaining ones
1283 a - discard this hunk and all later hunks in the file
1284 d - do not discard this hunk or any of the later hunks in the file"),
1285 checkout_nothead => N__(
1286 "y - apply this hunk to index and worktree
1287 n - do not apply this hunk to index and worktree
1288 q - quit; do not apply this hunk or any of the remaining ones
1289 a - apply this hunk and all later hunks in the file
1290 d - do not apply this hunk or any of the later hunks in the file"),
1291 worktree_head => N__(
1292 "y - discard this hunk from worktree
1293 n - do not discard this hunk from worktree
1294 q - quit; do not discard this hunk or any of the remaining ones
1295 a - discard this hunk and all later hunks in the file
1296 d - do not discard this hunk or any of the later hunks in the file"),
1297 worktree_nothead => N__(
1298 "y - apply this hunk to worktree
1299 n - do not apply this hunk to worktree
1300 q - quit; do not apply this hunk or any of the remaining ones
1301 a - apply this hunk and all later hunks in the file
1302 d - do not apply this hunk or any of the later hunks in the file"),
1305 sub help_patch_cmd {
1307 my $other = $_[0] . ",?";
1308 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1309 map { "$_\n" } grep {
1310 my $c = quotemeta(substr($_, 0, 1));
1312 } split "\n", __ <<EOF ;
1313 g - select a hunk to go to
1314 / - search for a hunk matching the given regex
1315 j - leave this hunk undecided, see next undecided hunk
1316 J - leave this hunk undecided, see next hunk
1317 k - leave this hunk undecided, see previous undecided hunk
1318 K - leave this hunk undecided, see previous hunk
1319 s - split the current hunk into smaller hunks
1320 e - manually edit the current hunk
1327 my $ret = run_git_apply $cmd, @_;
1334 sub apply_patch_for_checkout_commit {
1335 my $reverse = shift;
1336 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1337 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1339 if ($applies_worktree && $applies_index) {
1340 run_git_apply 'apply '.$reverse.' --cached', @_;
1341 run_git_apply 'apply '.$reverse, @_;
1343 } elsif (!$applies_index) {
1344 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1345 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1346 return run_git_apply 'apply '.$reverse, @_;
1348 print colored $error_color, __("Nothing was applied.\n");
1357 sub patch_update_cmd {
1358 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1359 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1360 for grep { $_->{UNMERGED} } @all_mods;
1361 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1363 my @mods = grep { !($_->{BINARY}) } @all_mods;
1368 print STDERR __("Only binary files changed.\n");
1370 print STDERR __("No changes.\n");
1374 if ($patch_mode_only) {
1378 @them = list_and_choose({ PROMPT => __('Patch update'),
1379 HEADER => $status_head, },
1383 return 0 if patch_update_file($_->{VALUE});
1387 # Generate a one line summary of a hunk.
1388 sub summarize_hunk {
1390 my $summary = $rhunk->{TEXT}[0];
1392 # Keep the line numbers, discard extra context.
1393 $summary =~ s/@@(.*?)@@.*/$1 /s;
1394 $summary .= " " x (20 - length $summary);
1396 # Add some user context.
1397 for my $line (@{$rhunk->{TEXT}}) {
1398 if ($line =~ m/^[+-].*\w/) {
1405 return substr($summary, 0, 80) . "\n";
1409 # Print a one-line summary of each hunk in the array ref in
1410 # the first argument, starting with the index in the 2nd.
1412 my ($hunks, $i) = @_;
1415 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1417 if (defined $hunks->[$i]{USE}) {
1418 $status = $hunks->[$i]{USE} ? "+" : "-";
1423 summarize_hunk($hunks->[$i]);
1428 my %patch_update_prompt_modes = (
1430 mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1431 deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1432 addition => N__("Stage addition [y,n,q,a,d%s,?]? "),
1433 hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1436 mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1437 deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1438 addition => N__("Stash addition [y,n,q,a,d%s,?]? "),
1439 hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1442 mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1443 deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1444 addition => N__("Unstage addition [y,n,q,a,d%s,?]? "),
1445 hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1448 mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1449 deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1450 addition => N__("Apply addition to index [y,n,q,a,d%s,?]? "),
1451 hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1454 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1455 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1456 addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "),
1457 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1460 mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1461 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1462 addition => N__("Discard addition from index and worktree [y,n,q,a,d%s,?]? "),
1463 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1465 checkout_nothead => {
1466 mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1467 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1468 addition => N__("Apply addition to index and worktree [y,n,q,a,d%s,?]? "),
1469 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1472 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1473 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1474 addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "),
1475 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1477 worktree_nothead => {
1478 mode => N__("Apply mode change to worktree [y,n,q,a,d%s,?]? "),
1479 deletion => N__("Apply deletion to worktree [y,n,q,a,d%s,?]? "),
1480 addition => N__("Apply addition to worktree [y,n,q,a,d%s,?]? "),
1481 hunk => N__("Apply this hunk to worktree [y,n,q,a,d%s,?]? "),
1485 sub patch_update_file {
1489 my ($head, @hunk) = parse_diff($path);
1490 ($head, my $mode, my $deletion, my $addition) = parse_diff_header($head);
1491 for (@{$head->{DISPLAY}}) {
1495 if (@{$mode->{TEXT}}) {
1496 unshift @hunk, $mode;
1498 if (@{$deletion->{TEXT}}) {
1499 foreach my $hunk (@hunk) {
1500 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1501 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1503 @hunk = ($deletion);
1504 } elsif (@{$addition->{TEXT}}) {
1505 foreach my $hunk (@hunk) {
1506 push @{$addition->{TEXT}}, @{$hunk->{TEXT}};
1507 push @{$addition->{DISPLAY}}, @{$hunk->{DISPLAY}};
1509 @hunk = ($addition);
1512 $num = scalar @hunk;
1516 my ($prev, $next, $other, $undecided, $i);
1522 for ($i = 0; $i < $ix; $i++) {
1523 if (!defined $hunk[$i]{USE}) {
1532 for ($i = $ix + 1; $i < $num; $i++) {
1533 if (!defined $hunk[$i]{USE}) {
1539 if ($ix < $num - 1) {
1545 for ($i = 0; $i < $num; $i++) {
1546 if (!defined $hunk[$i]{USE}) {
1551 last if (!$undecided);
1553 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1554 hunk_splittable($hunk[$ix]{TEXT})) {
1557 if ($hunk[$ix]{TYPE} eq 'hunk') {
1560 for (@{$hunk[$ix]{DISPLAY}}) {
1563 print colored $prompt_color, "(", ($ix+1), "/$num) ",
1564 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1566 my $line = prompt_single_character;
1567 last unless defined $line;
1569 if ($line =~ /^y/i) {
1570 $hunk[$ix]{USE} = 1;
1572 elsif ($line =~ /^n/i) {
1573 $hunk[$ix]{USE} = 0;
1575 elsif ($line =~ /^a/i) {
1576 while ($ix < $num) {
1577 if (!defined $hunk[$ix]{USE}) {
1578 $hunk[$ix]{USE} = 1;
1584 elsif ($line =~ /^g(.*)/) {
1586 unless ($other =~ /g/) {
1587 error_msg __("No other hunks to goto\n");
1590 my $no = $ix > 10 ? $ix - 10 : 0;
1591 while ($response eq '') {
1592 $no = display_hunks(\@hunk, $no);
1594 print __("go to which hunk (<ret> to see more)? ");
1596 print __("go to which hunk? ");
1598 $response = <STDIN>;
1599 if (!defined $response) {
1604 if ($response !~ /^\s*\d+\s*$/) {
1605 error_msg sprintf(__("Invalid number: '%s'\n"),
1607 } elsif (0 < $response && $response <= $num) {
1608 $ix = $response - 1;
1610 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1611 "Sorry, only %d hunks available.\n", $num), $num);
1615 elsif ($line =~ /^d/i) {
1616 while ($ix < $num) {
1617 if (!defined $hunk[$ix]{USE}) {
1618 $hunk[$ix]{USE} = 0;
1624 elsif ($line =~ /^q/i) {
1625 for ($i = 0; $i < $num; $i++) {
1626 if (!defined $hunk[$i]{USE}) {
1633 elsif ($line =~ m|^/(.*)|) {
1635 unless ($other =~ m|/|) {
1636 error_msg __("No other hunks to search\n");
1640 print colored $prompt_color, __("search for regex? ");
1642 if (defined $regex) {
1648 $search_string = qr{$regex}m;
1651 my ($err,$exp) = ($@, $1);
1652 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1653 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1658 my $text = join ("", @{$hunk[$iy]{TEXT}});
1659 last if ($text =~ $search_string);
1661 $iy = 0 if ($iy >= $num);
1663 error_msg __("No hunk matches the given pattern\n");
1670 elsif ($line =~ /^K/) {
1671 if ($other =~ /K/) {
1675 error_msg __("No previous hunk\n");
1679 elsif ($line =~ /^J/) {
1680 if ($other =~ /J/) {
1684 error_msg __("No next hunk\n");
1688 elsif ($line =~ /^k/) {
1689 if ($other =~ /k/) {
1693 !defined $hunk[$ix]{USE});
1697 error_msg __("No previous hunk\n");
1701 elsif ($line =~ /^j/) {
1702 if ($other !~ /j/) {
1703 error_msg __("No next hunk\n");
1707 elsif ($line =~ /^s/) {
1708 unless ($other =~ /s/) {
1709 error_msg __("Sorry, cannot split this hunk\n");
1712 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1714 print colored $header_color, sprintf(
1715 __n("Split into %d hunk.\n",
1716 "Split into %d hunks.\n",
1717 scalar(@split)), scalar(@split));
1719 splice (@hunk, $ix, 1, @split);
1720 $num = scalar @hunk;
1723 elsif ($line =~ /^e/) {
1724 unless ($other =~ /e/) {
1725 error_msg __("Sorry, cannot edit this hunk\n");
1728 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1729 if (defined $newhunk) {
1730 splice @hunk, $ix, 1, $newhunk;
1734 help_patch_cmd($other);
1740 last if ($ix >= $num ||
1741 !defined $hunk[$ix]{USE});
1746 @hunk = coalesce_overlapping_hunks(@hunk);
1752 push @result, @{$_->{TEXT}};
1757 my @patch = reassemble_patch($head->{TEXT}, @result);
1758 my $apply_routine = $patch_mode_flavour{APPLY};
1759 &$apply_routine(@patch);
1768 my @mods = list_modified('index-only');
1769 @mods = grep { !($_->{BINARY}) } @mods;
1771 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1773 HEADER => $status_head, },
1776 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1777 system(qw(git diff -p --cached), $reference, '--',
1778 map { $_->{VALUE} } @them);
1787 # TRANSLATORS: please do not translate the command names
1788 # 'status', 'update', 'revert', etc.
1789 print colored $help_color, __ <<'EOF' ;
1790 status - show paths with changes
1791 update - add working tree state to the staged set of changes
1792 revert - revert staged set of changes back to the HEAD version
1793 patch - pick hunks and update selectively
1794 diff - view diff between HEAD and index
1795 add untracked - add contents of untracked files to the staged set of changes
1800 return unless @ARGV;
1801 my $arg = shift @ARGV;
1802 if ($arg =~ /--patch(?:=(.*))?/) {
1804 if ($1 eq 'reset') {
1805 $patch_mode = 'reset_head';
1806 $patch_mode_revision = 'HEAD';
1807 $arg = shift @ARGV or die __("missing --");
1809 $patch_mode_revision = $arg;
1810 $patch_mode = ($arg eq 'HEAD' ?
1811 'reset_head' : 'reset_nothead');
1812 $arg = shift @ARGV or die __("missing --");
1814 } elsif ($1 eq 'checkout') {
1815 $arg = shift @ARGV or die __("missing --");
1817 $patch_mode = 'checkout_index';
1819 $patch_mode_revision = $arg;
1820 $patch_mode = ($arg eq 'HEAD' ?
1821 'checkout_head' : 'checkout_nothead');
1822 $arg = shift @ARGV or die __("missing --");
1824 } elsif ($1 eq 'worktree') {
1825 $arg = shift @ARGV or die __("missing --");
1827 $patch_mode = 'checkout_index';
1829 $patch_mode_revision = $arg;
1830 $patch_mode = ($arg eq 'HEAD' ?
1831 'worktree_head' : 'worktree_nothead');
1832 $arg = shift @ARGV or die __("missing --");
1834 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1836 $arg = shift @ARGV or die __("missing --");
1838 die sprintf(__("unknown --patch mode: %s"), $1);
1841 $patch_mode = 'stage';
1842 $arg = shift @ARGV or die __("missing --");
1844 die sprintf(__("invalid argument %s, expecting --"),
1845 $arg) unless $arg eq "--";
1846 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1847 $patch_mode_only = 1;
1849 elsif ($arg ne "--") {
1850 die sprintf(__("invalid argument %s, expecting --"), $arg);
1855 my @cmd = ([ 'status', \&status_cmd, ],
1856 [ 'update', \&update_cmd, ],
1857 [ 'revert', \&revert_cmd, ],
1858 [ 'add untracked', \&add_untracked_cmd, ],
1859 [ 'patch', \&patch_update_cmd, ],
1860 [ 'diff', \&diff_cmd, ],
1861 [ 'quit', \&quit_cmd, ],
1862 [ 'help', \&help_cmd, ],
1865 my ($it) = list_and_choose({ PROMPT => __('What now'),
1868 HEADER => __('*** Commands ***'),
1869 ON_EOF => \&quit_cmd,
1870 IMMEDIATE => 1 }, @cmd);
1884 if ($patch_mode_only) {