1#!/usr/bin/perl
2
3use 5.008;
4use strict;
5use warnings;
6use Git qw(unquote_path);
7use Git::I18N;
8
9binmode(STDOUT, ":raw");
10
11my $repo = Git->repository();
12
13my $menu_use_color = $repo->get_colorbool('color.interactive');
14my ($prompt_color, $header_color, $help_color) =
15 $menu_use_color ? (
16 $repo->get_color('color.interactive.prompt', 'bold blue'),
17 $repo->get_color('color.interactive.header', 'bold'),
18 $repo->get_color('color.interactive.help', 'red bold'),
19 ) : ();
20my $error_color = ();
21if ($menu_use_color) {
22 my $help_color_spec = ($repo->config('color.interactive.help') or
23 'red bold');
24 $error_color = $repo->get_color('color.interactive.error',
25 $help_color_spec);
26}
27
28my $diff_use_color = $repo->get_colorbool('color.diff');
29my ($fraginfo_color) =
30 $diff_use_color ? (
31 $repo->get_color('color.diff.frag', 'cyan'),
32 ) : ();
33my ($diff_plain_color) =
34 $diff_use_color ? (
35 $repo->get_color('color.diff.plain', ''),
36 ) : ();
37my ($diff_old_color) =
38 $diff_use_color ? (
39 $repo->get_color('color.diff.old', 'red'),
40 ) : ();
41my ($diff_new_color) =
42 $diff_use_color ? (
43 $repo->get_color('color.diff.new', 'green'),
44 ) : ();
45
46my $normal_color = $repo->get_color("", "reset");
47
48my $diff_algorithm = $repo->config('diff.algorithm');
49my $diff_filter = $repo->config('interactive.difffilter');
50
51my $use_readkey = 0;
52my $use_termcap = 0;
53my %term_escapes;
54
55sub ReadMode;
56sub ReadKey;
57if ($repo->config_bool("interactive.singlekey")) {
58 eval {
59 require Term::ReadKey;
60 Term::ReadKey->import;
61 $use_readkey = 1;
62 };
63 if (!$use_readkey) {
64 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
65 }
66 eval {
67 require Term::Cap;
68 my $termcap = Term::Cap->Tgetent;
69 foreach (values %$termcap) {
70 $term_escapes{$_} = 1 if /^\e/;
71 }
72 $use_termcap = 1;
73 };
74}
75
76sub colored {
77 my $color = shift;
78 my $string = join("", @_);
79
80 if (defined $color) {
81 # Put a color code at the beginning of each line, a reset at the end
82 # color after newlines that are not at the end of the string
83 $string =~ s/(\n+)(.)/$1$color$2/g;
84 # reset before newlines
85 $string =~ s/(\n+)/$normal_color$1/g;
86 # codes at beginning and end (if necessary):
87 $string =~ s/^/$color/;
88 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
89 }
90 return $string;
91}
92
93# command line options
94my $patch_mode_only;
95my $patch_mode;
96my $patch_mode_revision;
97
98sub apply_patch;
99sub apply_patch_for_checkout_commit;
100sub apply_patch_for_stash;
101
102my %patch_modes = (
103 'stage' => {
104 DIFF => 'diff-files -p',
105 APPLY => sub { apply_patch 'apply --cached', @_; },
106 APPLY_CHECK => 'apply --cached',
107 FILTER => 'file-only',
108 IS_REVERSE => 0,
109 },
110 'stash' => {
111 DIFF => 'diff-index -p HEAD',
112 APPLY => sub { apply_patch 'apply --cached', @_; },
113 APPLY_CHECK => 'apply --cached',
114 FILTER => undef,
115 IS_REVERSE => 0,
116 },
117 'reset_head' => {
118 DIFF => 'diff-index -p --cached',
119 APPLY => sub { apply_patch 'apply -R --cached', @_; },
120 APPLY_CHECK => 'apply -R --cached',
121 FILTER => 'index-only',
122 IS_REVERSE => 1,
123 },
124 'reset_nothead' => {
125 DIFF => 'diff-index -R -p --cached',
126 APPLY => sub { apply_patch 'apply --cached', @_; },
127 APPLY_CHECK => 'apply --cached',
128 FILTER => 'index-only',
129 IS_REVERSE => 0,
130 },
131 'checkout_index' => {
132 DIFF => 'diff-files -p',
133 APPLY => sub { apply_patch 'apply -R', @_; },
134 APPLY_CHECK => 'apply -R',
135 FILTER => 'file-only',
136 IS_REVERSE => 1,
137 },
138 'checkout_head' => {
139 DIFF => 'diff-index -p',
140 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
141 APPLY_CHECK => 'apply -R',
142 FILTER => undef,
143 IS_REVERSE => 1,
144 },
145 'checkout_nothead' => {
146 DIFF => 'diff-index -R -p',
147 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
148 APPLY_CHECK => 'apply',
149 FILTER => undef,
150 IS_REVERSE => 0,
151 },
152);
153
154$patch_mode = 'stage';
155my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
156
157sub run_cmd_pipe {
158 if ($^O eq 'MSWin32') {
159 my @invalid = grep {m/[":*]/} @_;
160 die "$^O does not support: @invalid\n" if @invalid;
161 my @args = map { m/ /o ? "\"$_\"": $_ } @_;
162 return qx{@args};
163 } else {
164 my $fh = undef;
165 open($fh, '-|', @_) or die;
166 return <$fh>;
167 }
168}
169
170my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
171
172if (!defined $GIT_DIR) {
173 exit(1); # rev-parse would have already said "not a git repo"
174}
175chomp($GIT_DIR);
176
177sub refresh {
178 my $fh;
179 open $fh, 'git update-index --refresh |'
180 or die;
181 while (<$fh>) {
182 ;# ignore 'needs update'
183 }
184 close $fh;
185}
186
187sub list_untracked {
188 map {
189 chomp $_;
190 unquote_path($_);
191 }
192 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
193}
194
195# TRANSLATORS: you can adjust this to align "git add -i" status menu
196my $status_fmt = __('%12s %12s %s');
197my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
198
199{
200 my $initial;
201 sub is_initial_commit {
202 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
203 unless defined $initial;
204 return $initial;
205 }
206}
207
208sub get_empty_tree {
209 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
210}
211
212sub get_diff_reference {
213 my $ref = shift;
214 if (defined $ref and $ref ne 'HEAD') {
215 return $ref;
216 } elsif (is_initial_commit()) {
217 return get_empty_tree();
218 } else {
219 return 'HEAD';
220 }
221}
222
223# Returns list of hashes, contents of each of which are:
224# VALUE: pathname
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
231
232sub list_modified {
233 my ($only) = @_;
234 my (%data, @return);
235 my ($add, $del, $adddel, $file);
236
237 my $reference = get_diff_reference($patch_mode_revision);
238 for (run_cmd_pipe(qw(git diff-index --cached
239 --numstat --summary), $reference,
240 '--', @ARGV)) {
241 if (($add, $del, $file) =
242 /^([-\d]+) ([-\d]+) (.*)/) {
243 my ($change, $bin);
244 $file = unquote_path($file);
245 if ($add eq '-' && $del eq '-') {
246 $change = __('binary');
247 $bin = 1;
248 }
249 else {
250 $change = "+$add/-$del";
251 }
252 $data{$file} = {
253 INDEX => $change,
254 BINARY => $bin,
255 FILE => __('nothing'),
256 }
257 }
258 elsif (($adddel, $file) =
259 /^ (create|delete) mode [0-7]+ (.*)$/) {
260 $file = unquote_path($file);
261 $data{$file}{INDEX_ADDDEL} = $adddel;
262 }
263 }
264
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);
269 my ($change, $bin);
270 if ($add eq '-' && $del eq '-') {
271 $change = __('binary');
272 $bin = 1;
273 }
274 else {
275 $change = "+$add/-$del";
276 }
277 $data{$file}{FILE} = $change;
278 if ($bin) {
279 $data{$file}{BINARY} = 1;
280 }
281 }
282 elsif (($adddel, $file) =
283 /^ (create|delete) mode [0-7]+ (.*)$/) {
284 $file = unquote_path($file);
285 $data{$file}{FILE_ADDDEL} = $adddel;
286 }
287 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
288 $file = unquote_path($2);
289 if (!exists $data{$file}) {
290 $data{$file} = +{
291 INDEX => __('unchanged'),
292 BINARY => 0,
293 };
294 }
295 if ($1 eq 'U') {
296 $data{$file}{UNMERGED} = 1;
297 }
298 }
299 }
300
301 for (sort keys %data) {
302 my $it = $data{$_};
303
304 if ($only) {
305 if ($only eq 'index-only') {
306 next if ($it->{INDEX} eq __('unchanged'));
307 }
308 if ($only eq 'file-only') {
309 next if ($it->{FILE} eq __('nothing'));
310 }
311 }
312 push @return, +{
313 VALUE => $_,
314 %$it,
315 };
316 }
317 return @return;
318}
319
320sub find_unique {
321 my ($string, @stuff) = @_;
322 my $found = undef;
323 for (my $i = 0; $i < @stuff; $i++) {
324 my $it = $stuff[$i];
325 my $hit = undef;
326 if (ref $it) {
327 if ((ref $it) eq 'ARRAY') {
328 $it = $it->[0];
329 }
330 else {
331 $it = $it->{VALUE};
332 }
333 }
334 eval {
335 if ($it =~ /^$string/) {
336 $hit = 1;
337 };
338 };
339 if (defined $hit && defined $found) {
340 return undef;
341 }
342 if ($hit) {
343 $found = $i + 1;
344 }
345 }
346 return $found;
347}
348
349# inserts string into trie and updates count for each character
350sub update_trie {
351 my ($trie, $string) = @_;
352 foreach (split //, $string) {
353 $trie = $trie->{$_} ||= {COUNT => 0};
354 $trie->{COUNT}++;
355 }
356}
357
358# returns an array of tuples (prefix, remainder)
359sub find_unique_prefixes {
360 my @stuff = @_;
361 my @return = ();
362
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
366 my $soft_limit = 0;
367 my $hard_limit = 3;
368
369 # build a trie modelling all possible options
370 my %trie;
371 foreach my $print (@stuff) {
372 if ((ref $print) eq 'ARRAY') {
373 $print = $print->[0];
374 }
375 elsif ((ref $print) eq 'HASH') {
376 $print = $print->{VALUE};
377 }
378 update_trie(\%trie, $print);
379 push @return, $print;
380 }
381
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;
386 my %search = %trie;
387 my ($prefix, $remainder);
388 my $j;
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;
394 last;
395 }
396 else {
397 my $prefix = substr $ret, 0, $j;
398 return ()
399 if ($hard_limit && $j + 1 > $hard_limit);
400 }
401 %search = %{$search{$letter}};
402 }
403 if (ord($letters[0]) > 127 ||
404 ($soft_limit && $j + 1 > $soft_limit)) {
405 $prefix = undef;
406 $remainder = $ret;
407 }
408 $return[$i] = [$prefix, $remainder];
409 }
410 return @return;
411}
412
413# filters out prefixes which have special meaning to list_and_choose()
414sub is_valid_prefix {
415 my $prefix = shift;
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
422}
423
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)
426sub highlight_prefix {
427 my $prefix = shift;
428 my $remainder = shift;
429
430 if (!defined $prefix) {
431 return $remainder;
432 }
433
434 if (!is_valid_prefix($prefix)) {
435 return "$prefix$remainder";
436 }
437
438 if (!$menu_use_color) {
439 return "[$prefix]$remainder";
440 }
441
442 return "$prompt_color$prefix$normal_color$remainder";
443}
444
445sub error_msg {
446 print STDERR colored $error_color, @_;
447}
448
449sub list_and_choose {
450 my ($opts, @stuff) = @_;
451 my (@chosen, @return);
452 if (!@stuff) {
453 return @return;
454 }
455 my $i;
456 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
457
458 TOPLOOP:
459 while (1) {
460 my $last_lf = 0;
461
462 if ($opts->{HEADER}) {
463 if (!$opts->{LIST_FLAT}) {
464 print " ";
465 }
466 print colored $header_color, "$opts->{HEADER}\n";
467 }
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]})
473 if @prefixes;
474 if ($ref eq 'ARRAY') {
475 $print = $highlighted || $print->[0];
476 }
477 elsif ($ref eq 'HASH') {
478 my $value = $highlighted || $print->{VALUE};
479 $print = sprintf($status_fmt,
480 $print->{INDEX},
481 $print->{FILE},
482 $value);
483 }
484 else {
485 $print = $highlighted || $print;
486 }
487 printf("%s%2d: %s", $chosen, $i+1, $print);
488 if (($opts->{LIST_FLAT}) &&
489 (($i + 1) % ($opts->{LIST_FLAT}))) {
490 print "\t";
491 $last_lf = 0;
492 }
493 else {
494 print "\n";
495 $last_lf = 1;
496 }
497 }
498 if (!$last_lf) {
499 print "\n";
500 }
501
502 return if ($opts->{LIST_ONLY});
503
504 print colored $prompt_color, $opts->{PROMPT};
505 if ($opts->{SINGLETON}) {
506 print "> ";
507 }
508 else {
509 print ">> ";
510 }
511 my $line = <STDIN>;
512 if (!$line) {
513 print "\n";
514 $opts->{ON_EOF}->() if $opts->{ON_EOF};
515 last;
516 }
517 chomp $line;
518 last if $line eq '';
519 if ($line eq '?') {
520 $opts->{SINGLETON} ?
521 singleton_prompt_help_cmd() :
522 prompt_help_cmd();
523 next TOPLOOP;
524 }
525 for my $choice (split(/[\s,]+/, $line)) {
526 my $choose = 1;
527 my ($bottom, $top);
528
529 # Input that begins with '-'; unchoose
530 if ($choice =~ s/^-//) {
531 $choose = 0;
532 }
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);
536 }
537 elsif ($choice =~ /^\d+$/) {
538 $bottom = $top = $choice;
539 }
540 elsif ($choice eq '*') {
541 $bottom = 1;
542 $top = 1 + @stuff;
543 }
544 else {
545 $bottom = $top = find_unique($choice, @stuff);
546 if (!defined $bottom) {
547 error_msg sprintf(__("Huh (%s)?\n"), $choice);
548 next TOPLOOP;
549 }
550 }
551 if ($opts->{SINGLETON} && $bottom != $top) {
552 error_msg sprintf(__("Huh (%s)?\n"), $choice);
553 next TOPLOOP;
554 }
555 for ($i = $bottom-1; $i <= $top-1; $i++) {
556 next if (@stuff <= $i || $i < 0);
557 $chosen[$i] = $choose;
558 }
559 }
560 last if ($opts->{IMMEDIATE} || $line eq '*');
561 }
562 for ($i = 0; $i < @stuff; $i++) {
563 if ($chosen[$i]) {
564 push @return, $stuff[$i];
565 }
566 }
567 return @return;
568}
569
570sub singleton_prompt_help_cmd {
571 print colored $help_color, __ <<'EOF' ;
572Prompt help:
5731 - select a numbered item
574foo - select item based on unique prefix
575 - (empty) select nothing
576EOF
577}
578
579sub prompt_help_cmd {
580 print colored $help_color, __ <<'EOF' ;
581Prompt help:
5821 - select a single item
5833-5 - select a range of items
5842-3,6-9 - select multiple ranges
585foo - select item based on unique prefix
586-... - unselect specified items
587* - choose all items
588 - (empty) finish selecting
589EOF
590}
591
592sub status_cmd {
593 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
594 list_modified());
595 print "\n";
596}
597
598sub say_n_paths {
599 my $did = shift @_;
600 my $cnt = scalar @_;
601 if ($did eq 'added') {
602 printf(__n("added %d path\n", "added %d paths\n",
603 $cnt), $cnt);
604 } elsif ($did eq 'updated') {
605 printf(__n("updated %d path\n", "updated %d paths\n",
606 $cnt), $cnt);
607 } elsif ($did eq 'reverted') {
608 printf(__n("reverted %d path\n", "reverted %d paths\n",
609 $cnt), $cnt);
610 } else {
611 printf(__n("touched %d path\n", "touched %d paths\n",
612 $cnt), $cnt);
613 }
614}
615
616sub update_cmd {
617 my @mods = list_modified('file-only');
618 return if (!@mods);
619
620 my @update = list_and_choose({ PROMPT => __('Update'),
621 HEADER => $status_head, },
622 @mods);
623 if (@update) {
624 system(qw(git update-index --add --remove --),
625 map { $_->{VALUE} } @update);
626 say_n_paths('updated', @update);
627 }
628 print "\n";
629}
630
631sub revert_cmd {
632 my @update = list_and_choose({ PROMPT => __('Revert'),
633 HEADER => $status_head, },
634 list_modified());
635 if (@update) {
636 if (is_initial_commit()) {
637 system(qw(git rm --cached),
638 map { $_->{VALUE} } @update);
639 }
640 else {
641 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
642 map { $_->{VALUE} } @update);
643 my $fh;
644 open $fh, '| git update-index --index-info'
645 or die;
646 for (@lines) {
647 print $fh $_;
648 }
649 close($fh);
650 for (@update) {
651 if ($_->{INDEX_ADDDEL} &&
652 $_->{INDEX_ADDDEL} eq 'create') {
653 system(qw(git update-index --force-remove --),
654 $_->{VALUE});
655 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
656 }
657 }
658 }
659 refresh();
660 say_n_paths('reverted', @update);
661 }
662 print "\n";
663}
664
665sub add_untracked_cmd {
666 my @add = list_and_choose({ PROMPT => __('Add untracked') },
667 list_untracked());
668 if (@add) {
669 system(qw(git update-index --add --), @add);
670 say_n_paths('added', @add);
671 } else {
672 print __("No untracked files.\n");
673 }
674 print "\n";
675}
676
677sub run_git_apply {
678 my $cmd = shift;
679 my $fh;
680 open $fh, '| git ' . $cmd . " --allow-overlap";
681 print $fh @_;
682 return close $fh;
683}
684
685sub parse_diff {
686 my ($path) = @_;
687 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
688 if (defined $diff_algorithm) {
689 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
690 }
691 if (defined $patch_mode_revision) {
692 push @diff_cmd, get_diff_reference($patch_mode_revision);
693 }
694 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
695 my @colored = ();
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");
702 }
703
704 @colored = run_cmd_pipe(@display_cmd);
705 }
706 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
707
708 if (@colored && @colored != @diff) {
709 print STDERR
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";
713 exit 1;
714 }
715
716 for (my $i = 0; $i < @diff; $i++) {
717 if ($diff[$i] =~ /^@@ /) {
718 push @hunk, { TEXT => [], DISPLAY => [],
719 TYPE => 'hunk' };
720 }
721 push @{$hunk[-1]{TEXT}}, $diff[$i];
722 push @{$hunk[-1]{DISPLAY}},
723 (@colored ? $colored[$i] : $diff[$i]);
724 }
725 return @hunk;
726}
727
728sub parse_diff_header {
729 my $src = shift;
730
731 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
732 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
733 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
734
735 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
736 my $dest =
737 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
738 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
739 $head;
740 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
741 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
742 }
743 return ($head, $mode, $deletion);
744}
745
746sub hunk_splittable {
747 my ($text) = @_;
748
749 my @s = split_hunk($text);
750 return (1 < @s);
751}
752
753sub parse_hunk_header {
754 my ($line) = @_;
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);
760}
761
762sub format_hunk_header {
763 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
764 return ("@@ -$o_ofs" .
765 (($o_cnt != 1) ? ",$o_cnt" : '') .
766 " +$n_ofs" .
767 (($n_cnt != 1) ? ",$n_cnt" : '') .
768 " @@\n");
769}
770
771sub split_hunk {
772 my ($text, $display) = @_;
773 my @split = ();
774 if (!defined $display) {
775 $display = $text;
776 }
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
779 # overlaps later.
780
781 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
782 my $hunk_start = 1;
783
784 OUTER:
785 while (1) {
786 my $next_hunk_start = undef;
787 my $i = $hunk_start - 1;
788 my $this = +{
789 TEXT => [],
790 DISPLAY => [],
791 TYPE => 'hunk',
792 OLD => $o_ofs,
793 NEW => $n_ofs,
794 OCNT => 0,
795 NCNT => 0,
796 ADDDEL => 0,
797 POSTCTX => 0,
798 USE => undef,
799 };
800
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;
807 next;
808 }
809 if ($line =~ /^ /) {
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
816 # one.
817 $next_hunk_start = $i;
818 }
819 push @{$this->{TEXT}}, $line;
820 push @{$this->{DISPLAY}}, $display;
821 $this->{OCNT}++;
822 $this->{NCNT}++;
823 if (defined $next_hunk_start) {
824 $this->{POSTCTX}++;
825 }
826 next;
827 }
828
829 # add/del
830 if (defined $next_hunk_start) {
831 # We are done with the current hunk and
832 # this is the first real change for the
833 # next split one.
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};
839 push @split, $this;
840 redo OUTER;
841 }
842 push @{$this->{TEXT}}, $line;
843 push @{$this->{DISPLAY}}, $display;
844 $this->{ADDDEL}++;
845 if ($line =~ /^-/) {
846 $this->{OCNT}++;
847 }
848 else {
849 $this->{NCNT}++;
850 }
851 }
852
853 push @split, $this;
854 last;
855 }
856
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};
862
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);
868 }
869 unshift @{$hunk->{DISPLAY}}, $display_head;
870 }
871 return @split;
872}
873
874sub find_last_o_ctx {
875 my ($it) = @_;
876 my $text = $it->{TEXT};
877 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
878 my $i = @{$text};
879 my $last_o_ctx = $o_ofs + $o_cnt;
880 while (0 < --$i) {
881 my $line = $text->[$i];
882 if ($line =~ /^ /) {
883 $last_o_ctx--;
884 next;
885 }
886 last;
887 }
888 return $last_o_ctx;
889}
890
891sub merge_hunk {
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]);
897
898 my (@line, $i, $ofs, $o_cnt, $n_cnt);
899 $ofs = $o0_ofs;
900 $o_cnt = $n_cnt = 0;
901 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
902 my $line = $prev->{TEXT}[$i];
903 if ($line =~ /^\+/) {
904 $n_cnt++;
905 push @line, $line;
906 next;
907 } elsif ($line =~ /^\\/) {
908 push @line, $line;
909 next;
910 }
911
912 last if ($o1_ofs <= $ofs);
913
914 $o_cnt++;
915 $ofs++;
916 if ($line =~ /^ /) {
917 $n_cnt++;
918 }
919 push @line, $line;
920 }
921
922 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
923 my $line = $this->{TEXT}[$i];
924 if ($line =~ /^\+/) {
925 $n_cnt++;
926 push @line, $line;
927 next;
928 } elsif ($line =~ /^\\/) {
929 push @line, $line;
930 next;
931 }
932 $ofs++;
933 $o_cnt++;
934 if ($line =~ /^ /) {
935 $n_cnt++;
936 }
937 push @line, $line;
938 }
939 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
940 @{$prev->{TEXT}} = ($head, @line);
941}
942
943sub coalesce_overlapping_hunks {
944 my (@in) = @_;
945 my @out = ();
946
947 my ($last_o_ctx, $last_was_dirty);
948 my $ofs_delta = 0;
949
950 for (@in) {
951 if ($_->{TYPE} ne 'hunk') {
952 push @out, $_;
953 next;
954 }
955 my $text = $_->{TEXT};
956 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
957 parse_hunk_header($text->[0]);
958 unless ($_->{USE}) {
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};
964 }
965 next;
966 }
967 if ($ofs_delta) {
968 $n_ofs += $ofs_delta;
969 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
970 $n_ofs, $n_cnt);
971 }
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};
976 }
977 if (defined $last_o_ctx &&
978 $o_ofs <= $last_o_ctx &&
979 !$_->{DIRTY} &&
980 !$last_was_dirty) {
981 merge_hunk($out[-1], $_);
982 }
983 else {
984 push @out, $_;
985 }
986 $last_o_ctx = find_last_o_ctx($out[-1]);
987 $last_was_dirty = $_->{DIRTY};
988 }
989 return @out;
990}
991
992sub reassemble_patch {
993 my $head = shift;
994 my @patch;
995
996 # Include everything in the header except the beginning of the diff.
997 push @patch, (grep { !/^[-+]{3}/ } @$head);
998
999 # Then include any headers from the hunk lines, which must
1000 # come before any actual hunk.
1001 while (@_ && $_[0] !~ /^@/) {
1002 push @patch, shift;
1003 }
1004
1005 # Then begin the diff.
1006 push @patch, grep { /^[-+]{3}/ } @$head;
1007
1008 # And then the actual hunks.
1009 push @patch, @_;
1010
1011 return @patch;
1012}
1013
1014sub color_diff {
1015 return map {
1016 colored((/^@/ ? $fraginfo_color :
1017 /^\+/ ? $diff_new_color :
1018 /^-/ ? $diff_old_color :
1019 $diff_plain_color),
1020 $_);
1021 } @_;
1022}
1023
1024sub label_hunk_lines {
1025 local $_;
1026 my $hunk = shift;
1027 my $i = 0;
1028 my $labels = [ map { /^[-+]/ ? ++$i : 0 } @{$hunk->{TEXT}} ];
1029 if ($i > 1) {
1030 @{$hunk}{qw(LABELS MAX_LABEL)} = ($labels, $i);
1031 return 1;
1032 }
1033 return 0;
1034}
1035
1036sub select_hunk_lines {
1037 my ($hunk, $selected) = @_;
1038 my ($text, $labels) = @{$hunk}{qw(TEXT LABELS)};
1039 my ($i, $o_cnt, $n_cnt) = (0, 0, 0);
1040 my ($push_eol, @newtext);
1041 # Lines with this mode will become context lines if they are
1042 # not selected
1043 my $context_mode = $patch_mode_flavour{IS_REVERSE} ? '+' : '-';
1044 for $i (1..$#{$text}) {
1045 my $mode = substr($text->[$i], 0, 1);
1046 if ($mode eq '\\') {
1047 push @newtext, $text->[$i] if ($push_eol);
1048 undef $push_eol;
1049 } elsif ($labels->[$i] and $selected->[$labels->[$i]]) {
1050 push @newtext, $text->[$i];
1051 if ($mode eq '+') {
1052 $n_cnt++;
1053 } else {
1054 $o_cnt++;
1055 }
1056 $push_eol = 1;
1057 } elsif ($mode eq ' ' or $mode eq $context_mode) {
1058 push @newtext, ' ' . substr($text->[$i], 1);
1059 $o_cnt++; $n_cnt++;
1060 $push_eol = 1;
1061 } else {
1062 undef $push_eol;
1063 }
1064 }
1065 my ($o_ofs, $orig_o_cnt, $n_ofs, $orig_n_cnt) =
1066 parse_hunk_header($text->[0]);
1067 unshift @newtext, format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1068 my $newhunk = {
1069 TEXT => \@newtext,
1070 DISPLAY => [ color_diff(@newtext) ],
1071 OFS_DELTA => $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt,
1072 TYPE => $hunk->{TYPE},
1073 USE => 1,
1074 };
1075 # If this hunk has previously been edited add the offset delta
1076 # of the old hunk to get the real delta from the original
1077 # hunk.
1078 if ($hunk->{OFS_DELTA}) {
1079 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1080 }
1081 return $newhunk;
1082}
1083
1084sub check_hunk_label {
1085 my ($max_label, $label) = ($_[0]->{MAX_LABEL}, $_[1]);
1086 if ($label < 1 or $label > $max_label) {
1087 error_msg sprintf(__("invalid hunk line '%d'\n"), $label);
1088 return 0;
1089 }
1090 return 1;
1091}
1092
1093sub split_hunk_selection {
1094 local $_;
1095 my @fields = @_;
1096 my @ret;
1097 for (@fields) {
1098 while ($_ ne '') {
1099 if (/^[0-9]-$/) {
1100 push @ret, $_;
1101 last;
1102 } elsif (/^([0-9](?:-[0-9])?)(.*)/) {
1103 push @ret, $1;
1104 $_ = $2;
1105 } else {
1106 error_msg sprintf
1107 __("invalid hunk line '%s'\n"),
1108 substr($_, 0, 1);
1109 return ();
1110 }
1111 }
1112 }
1113 return @ret;
1114}
1115
1116sub parse_hunk_selection {
1117 local $_;
1118 my ($hunk, $line) = @_;
1119 my ($max_label, $invert) = ($hunk->{MAX_LABEL}, undef);
1120 my @selected = (0) x ($max_label + 1);
1121 my @fields = split(/[,\s]+/, $line);
1122 if ($fields[0] =~ /^-(.*)/) {
1123 $invert = 1;
1124 if ($1 ne '') {
1125 $fields[0] = $1;
1126 } else {
1127 shift @fields;
1128 unless (@fields) {
1129 error_msg __("no lines to invert\n");
1130 return undef;
1131 }
1132 }
1133 }
1134 if ($max_label < 10) {
1135 @fields = split_hunk_selection(@fields) or return undef;
1136 }
1137 for (@fields) {
1138 if (my ($lo, $hi) = /^([0-9]+)-([0-9]*)$/) {
1139 if ($hi eq '') {
1140 $hi = $max_label;
1141 }
1142 check_hunk_label($hunk, $lo) or return undef;
1143 check_hunk_label($hunk, $hi) or return undef;
1144 if ($hi < $lo) {
1145 ($lo, $hi) = ($hi, $lo);
1146 }
1147 @selected[$lo..$hi] = (1) x (1 + $hi - $lo);
1148 } elsif (/^([0-9]+)$/) {
1149 check_hunk_label($hunk, $1) or return undef;
1150 $selected[$1] = 1;
1151 } else {
1152 error_msg sprintf(__("invalid hunk line '%s'\n"), $_);
1153 return undef;
1154 }
1155 }
1156 if ($invert) {
1157 @selected = map { !$_ } @selected;
1158 }
1159 return \@selected;
1160}
1161
1162sub display_hunk_lines {
1163 my ($display, $labels, $max_label) =
1164 @{$_[0]}{qw(DISPLAY LABELS MAX_LABEL)};
1165 my $width = int(log($max_label) / log(10)) + 1;
1166 my $padding = ' ' x ($width + 1);
1167 for my $i (0..$#{$display}) {
1168 if ($labels->[$i]) {
1169 printf '%*d %s', $width, $labels->[$i], $display->[$i];
1170 } else {
1171 print $padding . $display->[$i];
1172 }
1173 }
1174}
1175
1176sub select_lines_loop {
1177 my $hunk = shift;
1178 display_hunk_lines($hunk);
1179 my $selection = undef;
1180 until (defined $selection) {
1181 print colored $prompt_color, __("select lines? ");
1182 my $text = <STDIN>;
1183 defined $text and $text =~ /\S/ or return undef;
1184 $selection = parse_hunk_selection($hunk, $text);
1185 }
1186 return select_hunk_lines($hunk, $selection);
1187}
1188
1189my %edit_hunk_manually_modes = (
1190 stage => N__(
1191"If the patch applies cleanly, the edited hunk will immediately be
1192marked for staging."),
1193 stash => N__(
1194"If the patch applies cleanly, the edited hunk will immediately be
1195marked for stashing."),
1196 reset_head => N__(
1197"If the patch applies cleanly, the edited hunk will immediately be
1198marked for unstaging."),
1199 reset_nothead => N__(
1200"If the patch applies cleanly, the edited hunk will immediately be
1201marked for applying."),
1202 checkout_index => N__(
1203"If the patch applies cleanly, the edited hunk will immediately be
1204marked for discarding."),
1205 checkout_head => N__(
1206"If the patch applies cleanly, the edited hunk will immediately be
1207marked for discarding."),
1208 checkout_nothead => N__(
1209"If the patch applies cleanly, the edited hunk will immediately be
1210marked for applying."),
1211);
1212
1213sub recount_edited_hunk {
1214 local $_;
1215 my ($oldtext, $newtext) = @_;
1216 my ($o_cnt, $n_cnt) = (0, 0);
1217 for (@{$newtext}[1..$#{$newtext}]) {
1218 my $mode = substr($_, 0, 1);
1219 if ($mode eq '-') {
1220 $o_cnt++;
1221 } elsif ($mode eq '+') {
1222 $n_cnt++;
1223 } elsif ($mode eq ' ') {
1224 $o_cnt++;
1225 $n_cnt++;
1226 }
1227 }
1228 my ($o_ofs, undef, $n_ofs, undef) =
1229 parse_hunk_header($newtext->[0]);
1230 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1231 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1232 parse_hunk_header($oldtext->[0]);
1233 # Return the change in the number of lines inserted by this hunk
1234 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1235}
1236
1237sub edit_hunk_manually {
1238 my ($oldtext) = @_;
1239
1240 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1241 my $fh;
1242 open $fh, '>', $hunkfile
1243 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1244 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1245 print $fh @$oldtext;
1246 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1247 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1248 my $comment_line_char = Git::get_comment_line_char;
1249 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1250---
1251To remove '%s' lines, make them ' ' lines (context).
1252To remove '%s' lines, delete them.
1253Lines starting with %s will be removed.
1254EOF
1255__($edit_hunk_manually_modes{$patch_mode}),
1256# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1257__ <<EOF2 ;
1258If it does not apply cleanly, you will be given an opportunity to
1259edit again. If all lines of the hunk are removed, then the edit is
1260aborted and the hunk is left unchanged.
1261EOF2
1262 close $fh;
1263
1264 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1265 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1266
1267 if ($? != 0) {
1268 return undef;
1269 }
1270
1271 open $fh, '<', $hunkfile
1272 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1273 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1274 close $fh;
1275 unlink $hunkfile;
1276
1277 # Abort if nothing remains
1278 if (!grep { /\S/ } @newtext) {
1279 return undef;
1280 }
1281
1282 # Reinsert the first hunk header if the user accidentally deleted it
1283 if ($newtext[0] !~ /^@/) {
1284 unshift @newtext, $oldtext->[0];
1285 }
1286 return \@newtext;
1287}
1288
1289sub diff_applies {
1290 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1291 map { @{$_->{TEXT}} } @_);
1292}
1293
1294sub _restore_terminal_and_die {
1295 ReadMode 'restore';
1296 print "\n";
1297 exit 1;
1298}
1299
1300sub prompt_single_character {
1301 if ($use_readkey) {
1302 local $SIG{TERM} = \&_restore_terminal_and_die;
1303 local $SIG{INT} = \&_restore_terminal_and_die;
1304 ReadMode 'cbreak';
1305 my $key = ReadKey 0;
1306 ReadMode 'restore';
1307 if ($use_termcap and $key eq "\e") {
1308 while (!defined $term_escapes{$key}) {
1309 my $next = ReadKey 0.5;
1310 last if (!defined $next);
1311 $key .= $next;
1312 }
1313 $key =~ s/\e/^[/;
1314 }
1315 print "$key" if defined $key;
1316 print "\n";
1317 return $key;
1318 } else {
1319 return <STDIN>;
1320 }
1321}
1322
1323sub prompt_yesno {
1324 my ($prompt) = @_;
1325 while (1) {
1326 print colored $prompt_color, $prompt;
1327 my $line = prompt_single_character;
1328 return undef unless defined $line;
1329 return 0 if $line =~ /^n/i;
1330 return 1 if $line =~ /^y/i;
1331 }
1332}
1333
1334sub edit_hunk_loop {
1335 my ($head, $hunks, $ix) = @_;
1336 my $hunk = $hunks->[$ix];
1337 my $text = $hunk->{TEXT};
1338
1339 while (1) {
1340 my $newtext = edit_hunk_manually($text);
1341 if (!defined $newtext) {
1342 return undef;
1343 }
1344 my $newhunk = {
1345 TEXT => $newtext,
1346 TYPE => $hunk->{TYPE},
1347 USE => 1,
1348 DIRTY => 1,
1349 };
1350 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1351 # If this hunk has already been edited then add the
1352 # offset delta of the previous edit to get the real
1353 # delta from the original unedited hunk.
1354 $hunk->{OFS_DELTA} and
1355 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1356 if (diff_applies($head,
1357 @{$hunks}[0..$ix-1],
1358 $newhunk,
1359 @{$hunks}[$ix+1..$#{$hunks}])) {
1360 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1361 return $newhunk;
1362 }
1363 else {
1364 prompt_yesno(
1365 # TRANSLATORS: do not translate [y/n]
1366 # The program will only accept that input
1367 # at this point.
1368 # Consider translating (saying "no" discards!) as
1369 # (saying "n" for "no" discards!) if the translation
1370 # of the word "no" does not start with n.
1371 __('Your edited hunk does not apply. Edit again '
1372 . '(saying "no" discards!) [y/n]? ')
1373 ) or return undef;
1374 }
1375 }
1376}
1377
1378my %help_patch_modes = (
1379 stage => N__(
1380"y - stage this hunk
1381n - do not stage this hunk
1382q - quit; do not stage this hunk or any of the remaining ones
1383a - stage this hunk and all later hunks in the file
1384d - do not stage this hunk or any of the later hunks in the file"),
1385 stash => N__(
1386"y - stash this hunk
1387n - do not stash this hunk
1388q - quit; do not stash this hunk or any of the remaining ones
1389a - stash this hunk and all later hunks in the file
1390d - do not stash this hunk or any of the later hunks in the file"),
1391 reset_head => N__(
1392"y - unstage this hunk
1393n - do not unstage this hunk
1394q - quit; do not unstage this hunk or any of the remaining ones
1395a - unstage this hunk and all later hunks in the file
1396d - do not unstage this hunk or any of the later hunks in the file"),
1397 reset_nothead => N__(
1398"y - apply this hunk to index
1399n - do not apply this hunk to index
1400q - quit; do not apply this hunk or any of the remaining ones
1401a - apply this hunk and all later hunks in the file
1402d - do not apply this hunk or any of the later hunks in the file"),
1403 checkout_index => N__(
1404"y - discard this hunk from worktree
1405n - do not discard this hunk from worktree
1406q - quit; do not discard this hunk or any of the remaining ones
1407a - discard this hunk and all later hunks in the file
1408d - do not discard this hunk or any of the later hunks in the file"),
1409 checkout_head => N__(
1410"y - discard this hunk from index and worktree
1411n - do not discard this hunk from index and worktree
1412q - quit; do not discard this hunk or any of the remaining ones
1413a - discard this hunk and all later hunks in the file
1414d - do not discard this hunk or any of the later hunks in the file"),
1415 checkout_nothead => N__(
1416"y - apply this hunk to index and worktree
1417n - do not apply this hunk to index and worktree
1418q - quit; do not apply this hunk or any of the remaining ones
1419a - apply this hunk and all later hunks in the file
1420d - do not apply this hunk or any of the later hunks in the file"),
1421);
1422
1423sub help_patch_cmd {
1424 local $_;
1425 my $other = $_[0] . ",?";
1426 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1427 map { "$_\n" } grep {
1428 my $c = quotemeta(substr($_, 0, 1));
1429 $other =~ /,$c/
1430 } split "\n", __ <<EOF ;
1431g - select a hunk to go to
1432/ - search for a hunk matching the given regex
1433j - leave this hunk undecided, see next undecided hunk
1434J - leave this hunk undecided, see next hunk
1435k - leave this hunk undecided, see previous undecided hunk
1436K - leave this hunk undecided, see previous hunk
1437l - select hunk lines to use
1438s - split the current hunk into smaller hunks
1439e - manually edit the current hunk
1440? - print help
1441EOF
1442}
1443
1444sub apply_patch {
1445 my $cmd = shift;
1446 my $ret = run_git_apply $cmd, @_;
1447 if (!$ret) {
1448 print STDERR @_;
1449 }
1450 return $ret;
1451}
1452
1453sub apply_patch_for_checkout_commit {
1454 my $reverse = shift;
1455 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1456 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1457
1458 if ($applies_worktree && $applies_index) {
1459 run_git_apply 'apply '.$reverse.' --cached', @_;
1460 run_git_apply 'apply '.$reverse, @_;
1461 return 1;
1462 } elsif (!$applies_index) {
1463 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1464 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1465 return run_git_apply 'apply '.$reverse, @_;
1466 } else {
1467 print colored $error_color, __("Nothing was applied.\n");
1468 return 0;
1469 }
1470 } else {
1471 print STDERR @_;
1472 return 0;
1473 }
1474}
1475
1476sub patch_update_cmd {
1477 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1478 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1479 for grep { $_->{UNMERGED} } @all_mods;
1480 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1481
1482 my @mods = grep { !($_->{BINARY}) } @all_mods;
1483 my @them;
1484
1485 if (!@mods) {
1486 if (@all_mods) {
1487 print STDERR __("Only binary files changed.\n");
1488 } else {
1489 print STDERR __("No changes.\n");
1490 }
1491 return 0;
1492 }
1493 if ($patch_mode_only) {
1494 @them = @mods;
1495 }
1496 else {
1497 @them = list_and_choose({ PROMPT => __('Patch update'),
1498 HEADER => $status_head, },
1499 @mods);
1500 }
1501 for (@them) {
1502 return 0 if patch_update_file($_->{VALUE});
1503 }
1504}
1505
1506# Generate a one line summary of a hunk.
1507sub summarize_hunk {
1508 my $rhunk = shift;
1509 my $summary = $rhunk->{TEXT}[0];
1510
1511 # Keep the line numbers, discard extra context.
1512 $summary =~ s/@@(.*?)@@.*/$1 /s;
1513 $summary .= " " x (20 - length $summary);
1514
1515 # Add some user context.
1516 for my $line (@{$rhunk->{TEXT}}) {
1517 if ($line =~ m/^[+-].*\w/) {
1518 $summary .= $line;
1519 last;
1520 }
1521 }
1522
1523 chomp $summary;
1524 return substr($summary, 0, 80) . "\n";
1525}
1526
1527
1528# Print a one-line summary of each hunk in the array ref in
1529# the first argument, starting with the index in the 2nd.
1530sub display_hunks {
1531 my ($hunks, $i) = @_;
1532 my $ctr = 0;
1533 $i ||= 0;
1534 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1535 my $status = " ";
1536 if (defined $hunks->[$i]{USE}) {
1537 $status = $hunks->[$i]{USE} ? "+" : "-";
1538 }
1539 printf "%s%2d: %s",
1540 $status,
1541 $i + 1,
1542 summarize_hunk($hunks->[$i]);
1543 }
1544 return $i;
1545}
1546
1547my %patch_update_prompt_modes = (
1548 stage => {
1549 mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1550 deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1551 hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1552 },
1553 stash => {
1554 mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1555 deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1556 hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1557 },
1558 reset_head => {
1559 mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1560 deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1561 hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1562 },
1563 reset_nothead => {
1564 mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1565 deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1566 hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1567 },
1568 checkout_index => {
1569 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1570 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1571 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1572 },
1573 checkout_head => {
1574 mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1575 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1576 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1577 },
1578 checkout_nothead => {
1579 mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1580 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1581 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1582 },
1583);
1584
1585sub patch_update_file {
1586 my $quit = 0;
1587 my ($ix, $num);
1588 my $path = shift;
1589 my ($head, @hunk) = parse_diff($path);
1590 ($head, my $mode, my $deletion) = parse_diff_header($head);
1591 for (@{$head->{DISPLAY}}) {
1592 print;
1593 }
1594
1595 if (@{$mode->{TEXT}}) {
1596 unshift @hunk, $mode;
1597 }
1598 if (@{$deletion->{TEXT}}) {
1599 foreach my $hunk (@hunk) {
1600 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1601 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1602 }
1603 @hunk = ($deletion);
1604 }
1605
1606 $num = scalar @hunk;
1607 $ix = 0;
1608
1609 while (1) {
1610 my ($prev, $next, $other, $undecided, $i);
1611 $other = '';
1612
1613 if ($num <= $ix) {
1614 $ix = 0;
1615 }
1616 for ($i = 0; $i < $ix; $i++) {
1617 if (!defined $hunk[$i]{USE}) {
1618 $prev = 1;
1619 $other .= ',k';
1620 last;
1621 }
1622 }
1623 if ($ix) {
1624 $other .= ',K';
1625 }
1626 for ($i = $ix + 1; $i < $num; $i++) {
1627 if (!defined $hunk[$i]{USE}) {
1628 $next = 1;
1629 $other .= ',j';
1630 last;
1631 }
1632 }
1633 if ($ix < $num - 1) {
1634 $other .= ',J';
1635 }
1636 if ($num > 1) {
1637 $other .= ',g,/';
1638 }
1639 for ($i = 0; $i < $num; $i++) {
1640 if (!defined $hunk[$i]{USE}) {
1641 $undecided = 1;
1642 last;
1643 }
1644 }
1645 last if (!$undecided);
1646
1647 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1648 hunk_splittable($hunk[$ix]{TEXT})) {
1649 $other .= ',s';
1650 }
1651 if ($hunk[$ix]{TYPE} eq 'hunk') {
1652 $other .= ',e';
1653 }
1654 if (label_hunk_lines($hunk[$ix])) {
1655 $other .= ',l';
1656 }
1657 for (@{$hunk[$ix]{DISPLAY}}) {
1658 print;
1659 }
1660 print colored $prompt_color,
1661 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1662
1663 my $line = prompt_single_character;
1664 last unless defined $line;
1665 if ($line) {
1666 if ($line =~ /^y/i) {
1667 $hunk[$ix]{USE} = 1;
1668 }
1669 elsif ($line =~ /^n/i) {
1670 $hunk[$ix]{USE} = 0;
1671 }
1672 elsif ($line =~ /^a/i) {
1673 while ($ix < $num) {
1674 if (!defined $hunk[$ix]{USE}) {
1675 $hunk[$ix]{USE} = 1;
1676 }
1677 $ix++;
1678 }
1679 next;
1680 }
1681 elsif ($line =~ /^g(.*)/) {
1682 my $response = $1;
1683 unless ($other =~ /g/) {
1684 error_msg __("No other hunks to goto\n");
1685 next;
1686 }
1687 my $no = $ix > 10 ? $ix - 10 : 0;
1688 while ($response eq '') {
1689 $no = display_hunks(\@hunk, $no);
1690 if ($no < $num) {
1691 print __("go to which hunk (<ret> to see more)? ");
1692 } else {
1693 print __("go to which hunk? ");
1694 }
1695 $response = <STDIN>;
1696 if (!defined $response) {
1697 $response = '';
1698 }
1699 chomp $response;
1700 }
1701 if ($response !~ /^\s*\d+\s*$/) {
1702 error_msg sprintf(__("Invalid number: '%s'\n"),
1703 $response);
1704 } elsif (0 < $response && $response <= $num) {
1705 $ix = $response - 1;
1706 } else {
1707 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1708 "Sorry, only %d hunks available.\n", $num), $num);
1709 }
1710 next;
1711 }
1712 elsif ($line =~ /^d/i) {
1713 while ($ix < $num) {
1714 if (!defined $hunk[$ix]{USE}) {
1715 $hunk[$ix]{USE} = 0;
1716 }
1717 $ix++;
1718 }
1719 next;
1720 }
1721 elsif ($line =~ /^q/i) {
1722 for ($i = 0; $i < $num; $i++) {
1723 if (!defined $hunk[$i]{USE}) {
1724 $hunk[$i]{USE} = 0;
1725 }
1726 }
1727 $quit = 1;
1728 last;
1729 }
1730 elsif ($line =~ m|^/(.*)|) {
1731 my $regex = $1;
1732 unless ($other =~ m|/|) {
1733 error_msg __("No other hunks to search\n");
1734 next;
1735 }
1736 if ($1 eq "") {
1737 print colored $prompt_color, __("search for regex? ");
1738 $regex = <STDIN>;
1739 if (defined $regex) {
1740 chomp $regex;
1741 }
1742 }
1743 my $search_string;
1744 eval {
1745 $search_string = qr{$regex}m;
1746 };
1747 if ($@) {
1748 my ($err,$exp) = ($@, $1);
1749 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1750 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1751 next;
1752 }
1753 my $iy = $ix;
1754 while (1) {
1755 my $text = join ("", @{$hunk[$iy]{TEXT}});
1756 last if ($text =~ $search_string);
1757 $iy++;
1758 $iy = 0 if ($iy >= $num);
1759 if ($ix == $iy) {
1760 error_msg __("No hunk matches the given pattern\n");
1761 last;
1762 }
1763 }
1764 $ix = $iy;
1765 next;
1766 }
1767 elsif ($line =~ /^K/) {
1768 if ($other =~ /K/) {
1769 $ix--;
1770 }
1771 else {
1772 error_msg __("No previous hunk\n");
1773 }
1774 next;
1775 }
1776 elsif ($line =~ /^J/) {
1777 if ($other =~ /J/) {
1778 $ix++;
1779 }
1780 else {
1781 error_msg __("No next hunk\n");
1782 }
1783 next;
1784 }
1785 elsif ($line =~ /^k/) {
1786 if ($other =~ /k/) {
1787 while (1) {
1788 $ix--;
1789 last if (!$ix ||
1790 !defined $hunk[$ix]{USE});
1791 }
1792 }
1793 else {
1794 error_msg __("No previous hunk\n");
1795 }
1796 next;
1797 }
1798 elsif ($line =~ /^j/) {
1799 if ($other !~ /j/) {
1800 error_msg __("No next hunk\n");
1801 next;
1802 }
1803 }
1804 elsif ($line =~ /^l/) {
1805 unless ($other =~ /l/) {
1806 error_msg __("Cannot select line by line\n");
1807 next;
1808 }
1809 my $newhunk = select_lines_loop($hunk[$ix]);
1810 if ($newhunk) {
1811 splice @hunk, $ix, 1, $newhunk;
1812 } else {
1813 next;
1814 }
1815 }
1816 elsif ($line =~ /^s/) {
1817 unless ($other =~ /s/) {
1818 error_msg __("Sorry, cannot split this hunk\n");
1819 next;
1820 }
1821 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1822 if (1 < @split) {
1823 print colored $header_color, sprintf(
1824 __n("Split into %d hunk.\n",
1825 "Split into %d hunks.\n",
1826 scalar(@split)), scalar(@split));
1827 }
1828 splice (@hunk, $ix, 1, @split);
1829 $num = scalar @hunk;
1830 next;
1831 }
1832 elsif ($line =~ /^e/) {
1833 unless ($other =~ /e/) {
1834 error_msg __("Sorry, cannot edit this hunk\n");
1835 next;
1836 }
1837 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1838 if (defined $newhunk) {
1839 splice @hunk, $ix, 1, $newhunk;
1840 }
1841 }
1842 else {
1843 help_patch_cmd($other);
1844 next;
1845 }
1846 # soft increment
1847 while (1) {
1848 $ix++;
1849 last if ($ix >= $num ||
1850 !defined $hunk[$ix]{USE});
1851 }
1852 }
1853 }
1854
1855 @hunk = coalesce_overlapping_hunks(@hunk);
1856
1857 my $n_lofs = 0;
1858 my @result = ();
1859 for (@hunk) {
1860 if ($_->{USE}) {
1861 push @result, @{$_->{TEXT}};
1862 }
1863 }
1864
1865 if (@result) {
1866 my @patch = reassemble_patch($head->{TEXT}, @result);
1867 my $apply_routine = $patch_mode_flavour{APPLY};
1868 &$apply_routine(@patch);
1869 refresh();
1870 }
1871
1872 print "\n";
1873 return $quit;
1874}
1875
1876sub diff_cmd {
1877 my @mods = list_modified('index-only');
1878 @mods = grep { !($_->{BINARY}) } @mods;
1879 return if (!@mods);
1880 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1881 IMMEDIATE => 1,
1882 HEADER => $status_head, },
1883 @mods);
1884 return if (!@them);
1885 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1886 system(qw(git diff -p --cached), $reference, '--',
1887 map { $_->{VALUE} } @them);
1888}
1889
1890sub quit_cmd {
1891 print __("Bye.\n");
1892 exit(0);
1893}
1894
1895sub help_cmd {
1896# TRANSLATORS: please do not translate the command names
1897# 'status', 'update', 'revert', etc.
1898 print colored $help_color, __ <<'EOF' ;
1899status - show paths with changes
1900update - add working tree state to the staged set of changes
1901revert - revert staged set of changes back to the HEAD version
1902patch - pick hunks and update selectively
1903diff - view diff between HEAD and index
1904add untracked - add contents of untracked files to the staged set of changes
1905EOF
1906}
1907
1908sub process_args {
1909 return unless @ARGV;
1910 my $arg = shift @ARGV;
1911 if ($arg =~ /--patch(?:=(.*))?/) {
1912 if (defined $1) {
1913 if ($1 eq 'reset') {
1914 $patch_mode = 'reset_head';
1915 $patch_mode_revision = 'HEAD';
1916 $arg = shift @ARGV or die __("missing --");
1917 if ($arg ne '--') {
1918 $patch_mode_revision = $arg;
1919 $patch_mode = ($arg eq 'HEAD' ?
1920 'reset_head' : 'reset_nothead');
1921 $arg = shift @ARGV or die __("missing --");
1922 }
1923 } elsif ($1 eq 'checkout') {
1924 $arg = shift @ARGV or die __("missing --");
1925 if ($arg eq '--') {
1926 $patch_mode = 'checkout_index';
1927 } else {
1928 $patch_mode_revision = $arg;
1929 $patch_mode = ($arg eq 'HEAD' ?
1930 'checkout_head' : 'checkout_nothead');
1931 $arg = shift @ARGV or die __("missing --");
1932 }
1933 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1934 $patch_mode = $1;
1935 $arg = shift @ARGV or die __("missing --");
1936 } else {
1937 die sprintf(__("unknown --patch mode: %s"), $1);
1938 }
1939 } else {
1940 $patch_mode = 'stage';
1941 $arg = shift @ARGV or die __("missing --");
1942 }
1943 die sprintf(__("invalid argument %s, expecting --"),
1944 $arg) unless $arg eq "--";
1945 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1946 $patch_mode_only = 1;
1947 }
1948 elsif ($arg ne "--") {
1949 die sprintf(__("invalid argument %s, expecting --"), $arg);
1950 }
1951}
1952
1953sub main_loop {
1954 my @cmd = ([ 'status', \&status_cmd, ],
1955 [ 'update', \&update_cmd, ],
1956 [ 'revert', \&revert_cmd, ],
1957 [ 'add untracked', \&add_untracked_cmd, ],
1958 [ 'patch', \&patch_update_cmd, ],
1959 [ 'diff', \&diff_cmd, ],
1960 [ 'quit', \&quit_cmd, ],
1961 [ 'help', \&help_cmd, ],
1962 );
1963 while (1) {
1964 my ($it) = list_and_choose({ PROMPT => __('What now'),
1965 SINGLETON => 1,
1966 LIST_FLAT => 4,
1967 HEADER => __('*** Commands ***'),
1968 ON_EOF => \&quit_cmd,
1969 IMMEDIATE => 1 }, @cmd);
1970 if ($it) {
1971 eval {
1972 $it->[1]->();
1973 };
1974 if ($@) {
1975 print "$@";
1976 }
1977 }
1978 }
1979}
1980
1981process_args();
1982refresh();
1983if ($patch_mode_only) {
1984 patch_update_cmd();
1985}
1986else {
1987 status_cmd();
1988 main_loop();
1989}