]> git.ipfire.org Git - thirdparty/git.git/blame - git-add--interactive.perl
add -p: calculate offset delta for edited patches
[thirdparty/git.git] / git-add--interactive.perl
CommitLineData
3328aced 1#!/usr/bin/perl
5cde71d6 2
d48b2841 3use 5.008;
5cde71d6 4use strict;
3328aced 5use warnings;
1d542a54 6use Git qw(unquote_path);
258e7790 7use Git::I18N;
b4c61ed6 8
8851f480
JH
9binmode(STDOUT, ":raw");
10
b4c61ed6
JH
11my $repo = Git->repository();
12
f87e310d
JK
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 ) : ();
a3019736
TR
20my $error_color = ();
21if ($menu_use_color) {
9aad6cba
SB
22 my $help_color_spec = ($repo->config('color.interactive.help') or
23 'red bold');
a3019736
TR
24 $error_color = $repo->get_color('color.interactive.error',
25 $help_color_spec);
26}
b4c61ed6 27
f87e310d
JK
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 ) : ();
ac083c47
TR
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 ) : ();
b4c61ed6 45
f87e310d 46my $normal_color = $repo->get_color("", "reset");
b4c61ed6 47
2cc0f53b 48my $diff_algorithm = $repo->config('diff.algorithm');
01143847 49my $diff_filter = $repo->config('interactive.difffilter');
2cc0f53b 50
ca6ac7f1 51my $use_readkey = 0;
b5cc0032
TR
52my $use_termcap = 0;
53my %term_escapes;
54
748aa689
TR
55sub ReadMode;
56sub ReadKey;
ca6ac7f1
TR
57if ($repo->config_bool("interactive.singlekey")) {
58 eval {
748aa689
TR
59 require Term::ReadKey;
60 Term::ReadKey->import;
ca6ac7f1
TR
61 $use_readkey = 1;
62 };
b294097b
SR
63 if (!$use_readkey) {
64 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
65 }
b5cc0032
TR
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 };
ca6ac7f1
TR
74}
75
b4c61ed6
JH
76sub colored {
77 my $color = shift;
78 my $string = join("", @_);
79
f87e310d 80 if (defined $color) {
b4c61ed6
JH
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}
5cde71d6 92
b63e9950 93# command line options
c852bd54 94my $patch_mode_only;
b63e9950 95my $patch_mode;
d002ef4d 96my $patch_mode_revision;
b63e9950 97
8f0bef6d 98sub apply_patch;
4f353658 99sub apply_patch_for_checkout_commit;
dda1f2a5 100sub apply_patch_for_stash;
8f0bef6d
TR
101
102my %patch_modes = (
103 'stage' => {
104 DIFF => 'diff-files -p',
105 APPLY => sub { apply_patch 'apply --cached', @_; },
106 APPLY_CHECK => 'apply --cached',
8f0bef6d 107 FILTER => 'file-only',
7b8c7051 108 IS_REVERSE => 0,
8f0bef6d 109 },
dda1f2a5
TR
110 'stash' => {
111 DIFF => 'diff-index -p HEAD',
112 APPLY => sub { apply_patch 'apply --cached', @_; },
113 APPLY_CHECK => 'apply --cached',
dda1f2a5 114 FILTER => undef,
7b8c7051 115 IS_REVERSE => 0,
dda1f2a5 116 },
d002ef4d
TR
117 'reset_head' => {
118 DIFF => 'diff-index -p --cached',
119 APPLY => sub { apply_patch 'apply -R --cached', @_; },
120 APPLY_CHECK => 'apply -R --cached',
d002ef4d 121 FILTER => 'index-only',
7b8c7051 122 IS_REVERSE => 1,
d002ef4d
TR
123 },
124 'reset_nothead' => {
125 DIFF => 'diff-index -R -p --cached',
126 APPLY => sub { apply_patch 'apply --cached', @_; },
127 APPLY_CHECK => 'apply --cached',
d002ef4d 128 FILTER => 'index-only',
7b8c7051 129 IS_REVERSE => 0,
d002ef4d 130 },
4f353658
TR
131 'checkout_index' => {
132 DIFF => 'diff-files -p',
133 APPLY => sub { apply_patch 'apply -R', @_; },
134 APPLY_CHECK => 'apply -R',
4f353658 135 FILTER => 'file-only',
7b8c7051 136 IS_REVERSE => 1,
4f353658
TR
137 },
138 'checkout_head' => {
139 DIFF => 'diff-index -p',
140 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
141 APPLY_CHECK => 'apply -R',
4f353658 142 FILTER => undef,
7b8c7051 143 IS_REVERSE => 1,
4f353658
TR
144 },
145 'checkout_nothead' => {
146 DIFF => 'diff-index -R -p',
147 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
148 APPLY_CHECK => 'apply',
4f353658 149 FILTER => undef,
7b8c7051 150 IS_REVERSE => 0,
4f353658 151 },
8f0bef6d
TR
152);
153
0539d5e6
VA
154$patch_mode = 'stage';
155my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
b63e9950 156
5cde71d6 157sub run_cmd_pipe {
df17e77c 158 if ($^O eq 'MSWin32') {
21e9757e
AR
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 }
5cde71d6
JH
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;
21e9757e 179 open $fh, 'git update-index --refresh |'
5cde71d6
JH
180 or die;
181 while (<$fh>) {
182 ;# ignore 'needs update'
183 }
184 close $fh;
185}
186
187sub list_untracked {
188 map {
189 chomp $_;
8851f480 190 unquote_path($_);
5cde71d6 191 }
4c841684 192 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
5cde71d6
JH
193}
194
258e7790
VA
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'));
5cde71d6 198
18bc7616
JK
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
954312a3
JK
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
5cde71d6 223# Returns list of hashes, contents of each of which are:
5cde71d6
JH
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?
4066bd67 230# UNMERGED: is the path unmerged
5cde71d6
JH
231
232sub list_modified {
233 my ($only) = @_;
234 my (%data, @return);
235 my ($add, $del, $adddel, $file);
236
954312a3 237 my $reference = get_diff_reference($patch_mode_revision);
5cde71d6 238 for (run_cmd_pipe(qw(git diff-index --cached
18bc7616 239 --numstat --summary), $reference,
7288e12c 240 '--', @ARGV)) {
5cde71d6
JH
241 if (($add, $del, $file) =
242 /^([-\d]+) ([-\d]+) (.*)/) {
243 my ($change, $bin);
8851f480 244 $file = unquote_path($file);
5cde71d6 245 if ($add eq '-' && $del eq '-') {
55aa0442 246 $change = __('binary');
5cde71d6
JH
247 $bin = 1;
248 }
249 else {
250 $change = "+$add/-$del";
251 }
252 $data{$file} = {
253 INDEX => $change,
254 BINARY => $bin,
55aa0442 255 FILE => __('nothing'),
5cde71d6
JH
256 }
257 }
258 elsif (($adddel, $file) =
259 /^ (create|delete) mode [0-7]+ (.*)$/) {
8851f480 260 $file = unquote_path($file);
5cde71d6
JH
261 $data{$file}{INDEX_ADDDEL} = $adddel;
262 }
263 }
264
7288e12c 265 for (run_cmd_pipe(qw(git diff-files --numstat --summary --raw --), @ARGV)) {
5cde71d6
JH
266 if (($add, $del, $file) =
267 /^([-\d]+) ([-\d]+) (.*)/) {
8851f480 268 $file = unquote_path($file);
5cde71d6
JH
269 my ($change, $bin);
270 if ($add eq '-' && $del eq '-') {
55aa0442 271 $change = __('binary');
5cde71d6
JH
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]+ (.*)$/) {
8851f480 284 $file = unquote_path($file);
5cde71d6
JH
285 $data{$file}{FILE_ADDDEL} = $adddel;
286 }
4066bd67
JK
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} = +{
55aa0442 291 INDEX => __('unchanged'),
4066bd67
JK
292 BINARY => 0,
293 };
294 }
295 if ($1 eq 'U') {
296 $data{$file}{UNMERGED} = 1;
297 }
298 }
5cde71d6
JH
299 }
300
301 for (sort keys %data) {
302 my $it = $data{$_};
303
304 if ($only) {
305 if ($only eq 'index-only') {
55aa0442 306 next if ($it->{INDEX} eq __('unchanged'));
5cde71d6
JH
307 }
308 if ($only eq 'file-only') {
55aa0442 309 next if ($it->{FILE} eq __('nothing'));
5cde71d6
JH
310 }
311 }
312 push @return, +{
313 VALUE => $_,
5cde71d6
JH
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
14cb5038
WC
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 }
63320989 375 elsif ((ref $print) eq 'HASH') {
14cb5038
WC
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 }
8851f480
JH
403 if (ord($letters[0]) > 127 ||
404 ($soft_limit && $j + 1 > $soft_limit)) {
14cb5038
WC
405 $prefix = undef;
406 $remainder = $ret;
407 }
408 $return[$i] = [$prefix, $remainder];
409 }
410 return @return;
411}
412
63320989
WC
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
7e018be2
WC
420 ($prefix ne '*') && # "all" wildcard
421 ($prefix ne '?'); # prompt help
63320989
WC
422}
423
14cb5038
WC
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;
b4c61ed6
JH
429
430 if (!defined $prefix) {
431 return $remainder;
432 }
433
434 if (!is_valid_prefix($prefix)) {
435 return "$prefix$remainder";
436 }
437
f87e310d 438 if (!$menu_use_color) {
b4c61ed6
JH
439 return "[$prefix]$remainder";
440 }
441
442 return "$prompt_color$prefix$normal_color$remainder";
14cb5038
WC
443}
444
a3019736
TR
445sub error_msg {
446 print STDERR colored $error_color, @_;
447}
448
5cde71d6
JH
449sub list_and_choose {
450 my ($opts, @stuff) = @_;
451 my (@chosen, @return);
a9c4641d
AK
452 if (!@stuff) {
453 return @return;
454 }
5cde71d6 455 my $i;
14cb5038 456 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
5cde71d6
JH
457
458 TOPLOOP:
459 while (1) {
460 my $last_lf = 0;
461
462 if ($opts->{HEADER}) {
463 if (!$opts->{LIST_FLAT}) {
464 print " ";
465 }
b4c61ed6 466 print colored $header_color, "$opts->{HEADER}\n";
5cde71d6
JH
467 }
468 for ($i = 0; $i < @stuff; $i++) {
469 my $chosen = $chosen[$i] ? '*' : ' ';
470 my $print = $stuff[$i];
63320989
WC
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;
5cde71d6
JH
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
b4c61ed6 504 print colored $prompt_color, $opts->{PROMPT};
5cde71d6
JH
505 if ($opts->{SINGLETON}) {
506 print "> ";
507 }
508 else {
509 print ">> ";
510 }
511 my $line = <STDIN>;
c95c0248
JLH
512 if (!$line) {
513 print "\n";
514 $opts->{ON_EOF}->() if $opts->{ON_EOF};
515 last;
516 }
5cde71d6 517 chomp $line;
6a6eb3d0 518 last if $line eq '';
7e018be2
WC
519 if ($line eq '?') {
520 $opts->{SINGLETON} ?
521 singleton_prompt_help_cmd() :
522 prompt_help_cmd();
523 next TOPLOOP;
524 }
5cde71d6
JH
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 }
1e5aaa6d
CM
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);
5cde71d6
JH
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) {
13c58c17 547 error_msg sprintf(__("Huh (%s)?\n"), $choice);
5cde71d6
JH
548 next TOPLOOP;
549 }
550 }
551 if ($opts->{SINGLETON} && $bottom != $top) {
13c58c17 552 error_msg sprintf(__("Huh (%s)?\n"), $choice);
5cde71d6
JH
553 next TOPLOOP;
554 }
555 for ($i = $bottom-1; $i <= $top-1; $i++) {
6a6eb3d0 556 next if (@stuff <= $i || $i < 0);
5cde71d6 557 $chosen[$i] = $choose;
5cde71d6
JH
558 }
559 }
12db334e 560 last if ($opts->{IMMEDIATE} || $line eq '*');
5cde71d6
JH
561 }
562 for ($i = 0; $i < @stuff; $i++) {
563 if ($chosen[$i]) {
564 push @return, $stuff[$i];
565 }
566 }
567 return @return;
568}
569
7e018be2 570sub singleton_prompt_help_cmd {
5fa83264 571 print colored $help_color, __ <<'EOF' ;
7e018be2
WC
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 {
5fa83264 580 print colored $help_color, __ <<'EOF' ;
7e018be2
WC
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
5cde71d6
JH
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 @_;
c4a85c3b
VA
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);
5cde71d6
JH
613 }
614}
615
616sub update_cmd {
617 my @mods = list_modified('file-only');
618 return if (!@mods);
619
258e7790 620 my @update = list_and_choose({ PROMPT => __('Update'),
5cde71d6
JH
621 HEADER => $status_head, },
622 @mods);
623 if (@update) {
a4f7112f 624 system(qw(git update-index --add --remove --),
5cde71d6
JH
625 map { $_->{VALUE} } @update);
626 say_n_paths('updated', @update);
627 }
628 print "\n";
629}
630
631sub revert_cmd {
258e7790 632 my @update = list_and_choose({ PROMPT => __('Revert'),
5cde71d6
JH
633 HEADER => $status_head, },
634 list_modified());
635 if (@update) {
18bc7616
JK
636 if (is_initial_commit()) {
637 system(qw(git rm --cached),
638 map { $_->{VALUE} } @update);
5cde71d6 639 }
18bc7616
JK
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});
13c58c17 655 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
18bc7616 656 }
5cde71d6
JH
657 }
658 }
659 refresh();
660 say_n_paths('reverted', @update);
661 }
662 print "\n";
663}
664
665sub add_untracked_cmd {
258e7790 666 my @add = list_and_choose({ PROMPT => __('Add untracked') },
5cde71d6
JH
667 list_untracked());
668 if (@add) {
669 system(qw(git update-index --add --), @add);
670 say_n_paths('added', @add);
a9c4641d 671 } else {
258e7790 672 print __("No untracked files.\n");
5cde71d6
JH
673 }
674 print "\n";
675}
676
8f0bef6d
TR
677sub run_git_apply {
678 my $cmd = shift;
679 my $fh;
933e44d3 680 open $fh, '| git ' . $cmd . " --recount --allow-overlap";
8f0bef6d
TR
681 print $fh @_;
682 return close $fh;
683}
684
5cde71d6
JH
685sub parse_diff {
686 my ($path) = @_;
8f0bef6d 687 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
2cc0f53b 688 if (defined $diff_algorithm) {
e5c29097 689 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
46e3d17f 690 }
d002ef4d 691 if (defined $patch_mode_revision) {
954312a3 692 push @diff_cmd, get_diff_reference($patch_mode_revision);
d002ef4d 693 }
8f0bef6d 694 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
4af756f3
WC
695 my @colored = ();
696 if ($diff_use_color) {
01143847
JK
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);
5cde71d6 705 }
0392513f 706 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
b4c61ed6 707
4af756f3
WC
708 for (my $i = 0; $i < @diff; $i++) {
709 if ($diff[$i] =~ /^@@ /) {
0392513f
JK
710 push @hunk, { TEXT => [], DISPLAY => [],
711 TYPE => 'hunk' };
b4c61ed6 712 }
4af756f3
WC
713 push @{$hunk[-1]{TEXT}}, $diff[$i];
714 push @{$hunk[-1]{DISPLAY}},
01143847 715 (@colored ? $colored[$i] : $diff[$i]);
b4c61ed6 716 }
4af756f3 717 return @hunk;
b4c61ed6
JH
718}
719
b717a627
JK
720sub parse_diff_header {
721 my $src = shift;
722
0392513f
JK
723 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
724 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
24ab81ae 725 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
b717a627
JK
726
727 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
24ab81ae
JK
728 my $dest =
729 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
730 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
731 $head;
b717a627
JK
732 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
733 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
734 }
24ab81ae 735 return ($head, $mode, $deletion);
b717a627
JK
736}
737
835b2aeb
JH
738sub hunk_splittable {
739 my ($text) = @_;
740
741 my @s = split_hunk($text);
742 return (1 < @s);
743}
744
745sub parse_hunk_header {
746 my ($line) = @_;
747 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
7288ed8e
JLH
748 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
749 $o_cnt = 1 unless defined $o_cnt;
750 $n_cnt = 1 unless defined $n_cnt;
835b2aeb
JH
751 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
752}
753
492e60c8
PW
754sub format_hunk_header {
755 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
756 return ("@@ -$o_ofs" .
757 (($o_cnt != 1) ? ",$o_cnt" : '') .
758 " +$n_ofs" .
759 (($n_cnt != 1) ? ",$n_cnt" : '') .
760 " @@\n");
761}
762
835b2aeb 763sub split_hunk {
4af756f3 764 my ($text, $display) = @_;
835b2aeb 765 my @split = ();
4af756f3
WC
766 if (!defined $display) {
767 $display = $text;
768 }
835b2aeb
JH
769 # If there are context lines in the middle of a hunk,
770 # it can be split, but we would need to take care of
771 # overlaps later.
772
7b40a455 773 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
835b2aeb 774 my $hunk_start = 1;
835b2aeb
JH
775
776 OUTER:
777 while (1) {
778 my $next_hunk_start = undef;
779 my $i = $hunk_start - 1;
780 my $this = +{
781 TEXT => [],
4af756f3 782 DISPLAY => [],
0392513f 783 TYPE => 'hunk',
835b2aeb
JH
784 OLD => $o_ofs,
785 NEW => $n_ofs,
786 OCNT => 0,
787 NCNT => 0,
788 ADDDEL => 0,
789 POSTCTX => 0,
4af756f3 790 USE => undef,
835b2aeb
JH
791 };
792
793 while (++$i < @$text) {
794 my $line = $text->[$i];
4af756f3 795 my $display = $display->[$i];
835b2aeb
JH
796 if ($line =~ /^ /) {
797 if ($this->{ADDDEL} &&
798 !defined $next_hunk_start) {
799 # We have seen leading context and
800 # adds/dels and then here is another
801 # context, which is trailing for this
802 # split hunk and leading for the next
803 # one.
804 $next_hunk_start = $i;
805 }
806 push @{$this->{TEXT}}, $line;
4af756f3 807 push @{$this->{DISPLAY}}, $display;
835b2aeb
JH
808 $this->{OCNT}++;
809 $this->{NCNT}++;
810 if (defined $next_hunk_start) {
811 $this->{POSTCTX}++;
812 }
813 next;
814 }
815
816 # add/del
817 if (defined $next_hunk_start) {
818 # We are done with the current hunk and
819 # this is the first real change for the
820 # next split one.
821 $hunk_start = $next_hunk_start;
822 $o_ofs = $this->{OLD} + $this->{OCNT};
823 $n_ofs = $this->{NEW} + $this->{NCNT};
824 $o_ofs -= $this->{POSTCTX};
825 $n_ofs -= $this->{POSTCTX};
826 push @split, $this;
827 redo OUTER;
828 }
829 push @{$this->{TEXT}}, $line;
4af756f3 830 push @{$this->{DISPLAY}}, $display;
835b2aeb
JH
831 $this->{ADDDEL}++;
832 if ($line =~ /^-/) {
833 $this->{OCNT}++;
834 }
835 else {
836 $this->{NCNT}++;
837 }
838 }
839
840 push @split, $this;
841 last;
842 }
843
844 for my $hunk (@split) {
845 $o_ofs = $hunk->{OLD};
846 $n_ofs = $hunk->{NEW};
7b40a455
JLH
847 my $o_cnt = $hunk->{OCNT};
848 my $n_cnt = $hunk->{NCNT};
835b2aeb 849
492e60c8 850 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
4af756f3 851 my $display_head = $head;
835b2aeb 852 unshift @{$hunk->{TEXT}}, $head;
4af756f3
WC
853 if ($diff_use_color) {
854 $display_head = colored($fraginfo_color, $head);
855 }
856 unshift @{$hunk->{DISPLAY}}, $display_head;
835b2aeb 857 }
4af756f3 858 return @split;
835b2aeb
JH
859}
860
7a26e653
JH
861sub find_last_o_ctx {
862 my ($it) = @_;
863 my $text = $it->{TEXT};
864 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
865 my $i = @{$text};
866 my $last_o_ctx = $o_ofs + $o_cnt;
867 while (0 < --$i) {
868 my $line = $text->[$i];
869 if ($line =~ /^ /) {
870 $last_o_ctx--;
871 next;
872 }
873 last;
874 }
875 return $last_o_ctx;
876}
877
878sub merge_hunk {
879 my ($prev, $this) = @_;
880 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
881 parse_hunk_header($prev->{TEXT}[0]);
882 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
883 parse_hunk_header($this->{TEXT}[0]);
884
885 my (@line, $i, $ofs, $o_cnt, $n_cnt);
886 $ofs = $o0_ofs;
887 $o_cnt = $n_cnt = 0;
888 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
889 my $line = $prev->{TEXT}[$i];
890 if ($line =~ /^\+/) {
891 $n_cnt++;
892 push @line, $line;
893 next;
894 }
895
896 last if ($o1_ofs <= $ofs);
897
898 $o_cnt++;
899 $ofs++;
900 if ($line =~ /^ /) {
901 $n_cnt++;
902 }
903 push @line, $line;
904 }
905
906 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
907 my $line = $this->{TEXT}[$i];
908 if ($line =~ /^\+/) {
909 $n_cnt++;
910 push @line, $line;
911 next;
912 }
913 $ofs++;
914 $o_cnt++;
915 if ($line =~ /^ /) {
916 $n_cnt++;
917 }
918 push @line, $line;
919 }
492e60c8 920 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
7a26e653
JH
921 @{$prev->{TEXT}} = ($head, @line);
922}
923
924sub coalesce_overlapping_hunks {
925 my (@in) = @_;
926 my @out = ();
927
928 my ($last_o_ctx, $last_was_dirty);
fecc6f3a 929 my $ofs_delta = 0;
7a26e653 930
fecc6f3a 931 for (@in) {
3d792161
TR
932 if ($_->{TYPE} ne 'hunk') {
933 push @out, $_;
934 next;
935 }
7a26e653 936 my $text = $_->{TEXT};
fecc6f3a
PW
937 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
938 parse_hunk_header($text->[0]);
939 unless ($_->{USE}) {
940 $ofs_delta += $o_cnt - $n_cnt;
2b8ea7f3
PW
941 # If this hunk has been edited then subtract
942 # the delta that is due to the edit.
943 if ($_->{OFS_DELTA}) {
944 $ofs_delta -= $_->{OFS_DELTA};
945 }
fecc6f3a
PW
946 next;
947 }
948 if ($ofs_delta) {
949 $n_ofs += $ofs_delta;
950 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
951 $n_ofs, $n_cnt);
952 }
2b8ea7f3
PW
953 # If this hunk was edited then adjust the offset delta
954 # to reflect the edit.
955 if ($_->{OFS_DELTA}) {
956 $ofs_delta += $_->{OFS_DELTA};
957 }
7a26e653
JH
958 if (defined $last_o_ctx &&
959 $o_ofs <= $last_o_ctx &&
960 !$_->{DIRTY} &&
961 !$last_was_dirty) {
962 merge_hunk($out[-1], $_);
963 }
964 else {
965 push @out, $_;
966 }
967 $last_o_ctx = find_last_o_ctx($out[-1]);
968 $last_was_dirty = $_->{DIRTY};
969 }
970 return @out;
971}
835b2aeb 972
e1327ed5
JK
973sub reassemble_patch {
974 my $head = shift;
975 my @patch;
976
977 # Include everything in the header except the beginning of the diff.
978 push @patch, (grep { !/^[-+]{3}/ } @$head);
979
980 # Then include any headers from the hunk lines, which must
981 # come before any actual hunk.
982 while (@_ && $_[0] !~ /^@/) {
983 push @patch, shift;
984 }
985
986 # Then begin the diff.
987 push @patch, grep { /^[-+]{3}/ } @$head;
988
989 # And then the actual hunks.
990 push @patch, @_;
991
992 return @patch;
993}
994
ac083c47
TR
995sub color_diff {
996 return map {
997 colored((/^@/ ? $fraginfo_color :
998 /^\+/ ? $diff_new_color :
999 /^-/ ? $diff_old_color :
1000 $diff_plain_color),
1001 $_);
1002 } @_;
1003}
1004
c9d96164
VA
1005my %edit_hunk_manually_modes = (
1006 stage => N__(
1007"If the patch applies cleanly, the edited hunk will immediately be
1008marked for staging."),
1009 stash => N__(
1010"If the patch applies cleanly, the edited hunk will immediately be
1011marked for stashing."),
1012 reset_head => N__(
1013"If the patch applies cleanly, the edited hunk will immediately be
1014marked for unstaging."),
1015 reset_nothead => N__(
1016"If the patch applies cleanly, the edited hunk will immediately be
1017marked for applying."),
1018 checkout_index => N__(
1019"If the patch applies cleanly, the edited hunk will immediately be
0301f1fd 1020marked for discarding."),
c9d96164
VA
1021 checkout_head => N__(
1022"If the patch applies cleanly, the edited hunk will immediately be
1023marked for discarding."),
1024 checkout_nothead => N__(
1025"If the patch applies cleanly, the edited hunk will immediately be
1026marked for applying."),
1027);
1028
2b8ea7f3
PW
1029sub recount_edited_hunk {
1030 local $_;
1031 my ($oldtext, $newtext) = @_;
1032 my ($o_cnt, $n_cnt) = (0, 0);
1033 for (@{$newtext}[1..$#{$newtext}]) {
1034 my $mode = substr($_, 0, 1);
1035 if ($mode eq '-') {
1036 $o_cnt++;
1037 } elsif ($mode eq '+') {
1038 $n_cnt++;
1039 } elsif ($mode eq ' ') {
1040 $o_cnt++;
1041 $n_cnt++;
1042 }
1043 }
1044 my ($o_ofs, undef, $n_ofs, undef) =
1045 parse_hunk_header($newtext->[0]);
1046 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1047 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1048 parse_hunk_header($oldtext->[0]);
1049 # Return the change in the number of lines inserted by this hunk
1050 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1051}
1052
ac083c47
TR
1053sub edit_hunk_manually {
1054 my ($oldtext) = @_;
1055
1056 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1057 my $fh;
1058 open $fh, '>', $hunkfile
13c58c17 1059 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
c9d96164 1060 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
ac083c47 1061 print $fh @$oldtext;
7b8c7051
JDL
1062 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1063 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
c9d96164
VA
1064 my $comment_line_char = Git::get_comment_line_char;
1065 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1066---
1067To remove '%s' lines, make them ' ' lines (context).
1068To remove '%s' lines, delete them.
1069Lines starting with %s will be removed.
ac083c47 1070EOF
c9d96164
VA
1071__($edit_hunk_manually_modes{$patch_mode}),
1072# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1073__ <<EOF2 ;
1074If it does not apply cleanly, you will be given an opportunity to
1075edit again. If all lines of the hunk are removed, then the edit is
1076aborted and the hunk is left unchanged.
1077EOF2
ac083c47
TR
1078 close $fh;
1079
b4479f07 1080 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
ac083c47
TR
1081 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1082
1d398a03
DM
1083 if ($? != 0) {
1084 return undef;
1085 }
1086
ac083c47 1087 open $fh, '<', $hunkfile
13c58c17 1088 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
d85d7ecb 1089 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
ac083c47
TR
1090 close $fh;
1091 unlink $hunkfile;
1092
1093 # Abort if nothing remains
1094 if (!grep { /\S/ } @newtext) {
1095 return undef;
1096 }
1097
1098 # Reinsert the first hunk header if the user accidentally deleted it
1099 if ($newtext[0] !~ /^@/) {
1100 unshift @newtext, $oldtext->[0];
1101 }
1102 return \@newtext;
1103}
1104
1105sub diff_applies {
9dce8323 1106 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
8f0bef6d 1107 map { @{$_->{TEXT}} } @_);
ac083c47
TR
1108}
1109
ca6ac7f1
TR
1110sub _restore_terminal_and_die {
1111 ReadMode 'restore';
1112 print "\n";
1113 exit 1;
1114}
1115
1116sub prompt_single_character {
1117 if ($use_readkey) {
1118 local $SIG{TERM} = \&_restore_terminal_and_die;
1119 local $SIG{INT} = \&_restore_terminal_and_die;
1120 ReadMode 'cbreak';
1121 my $key = ReadKey 0;
1122 ReadMode 'restore';
b5cc0032
TR
1123 if ($use_termcap and $key eq "\e") {
1124 while (!defined $term_escapes{$key}) {
1125 my $next = ReadKey 0.5;
1126 last if (!defined $next);
1127 $key .= $next;
1128 }
1129 $key =~ s/\e/^[/;
1130 }
ca6ac7f1
TR
1131 print "$key" if defined $key;
1132 print "\n";
1133 return $key;
1134 } else {
1135 return <STDIN>;
1136 }
1137}
1138
ac083c47
TR
1139sub prompt_yesno {
1140 my ($prompt) = @_;
1141 while (1) {
1142 print colored $prompt_color, $prompt;
ca6ac7f1 1143 my $line = prompt_single_character;
d5addcf5 1144 return undef unless defined $line;
ac083c47
TR
1145 return 0 if $line =~ /^n/i;
1146 return 1 if $line =~ /^y/i;
1147 }
1148}
1149
1150sub edit_hunk_loop {
2b8ea7f3
PW
1151 my ($head, $hunks, $ix) = @_;
1152 my $hunk = $hunks->[$ix];
1153 my $text = $hunk->{TEXT};
ac083c47
TR
1154
1155 while (1) {
2b8ea7f3
PW
1156 my $newtext = edit_hunk_manually($text);
1157 if (!defined $newtext) {
ac083c47
TR
1158 return undef;
1159 }
0392513f 1160 my $newhunk = {
2b8ea7f3
PW
1161 TEXT => $newtext,
1162 TYPE => $hunk->{TYPE},
7a26e653
JH
1163 USE => 1,
1164 DIRTY => 1,
0392513f 1165 };
2b8ea7f3
PW
1166 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1167 # If this hunk has already been edited then add the
1168 # offset delta of the previous edit to get the real
1169 # delta from the original unedited hunk.
1170 $hunk->{OFS_DELTA} and
1171 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
ac083c47 1172 if (diff_applies($head,
2b8ea7f3 1173 @{$hunks}[0..$ix-1],
ac083c47 1174 $newhunk,
2b8ea7f3
PW
1175 @{$hunks}[$ix+1..$#{$hunks}])) {
1176 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
ac083c47
TR
1177 return $newhunk;
1178 }
1179 else {
1180 prompt_yesno(
258e7790
VA
1181 # TRANSLATORS: do not translate [y/n]
1182 # The program will only accept that input
1183 # at this point.
1184 # Consider translating (saying "no" discards!) as
1185 # (saying "n" for "no" discards!) if the translation
1186 # of the word "no" does not start with n.
1187 __('Your edited hunk does not apply. Edit again '
1188 . '(saying "no" discards!) [y/n]? ')
ac083c47
TR
1189 ) or return undef;
1190 }
1191 }
1192}
1193
186b52c5
VA
1194my %help_patch_modes = (
1195 stage => N__(
1196"y - stage this hunk
1197n - do not stage this hunk
1198q - quit; do not stage this hunk or any of the remaining ones
1199a - stage this hunk and all later hunks in the file
1200d - do not stage this hunk or any of the later hunks in the file"),
1201 stash => N__(
1202"y - stash this hunk
1203n - do not stash this hunk
1204q - quit; do not stash this hunk or any of the remaining ones
1205a - stash this hunk and all later hunks in the file
1206d - do not stash this hunk or any of the later hunks in the file"),
1207 reset_head => N__(
1208"y - unstage this hunk
1209n - do not unstage this hunk
1210q - quit; do not unstage this hunk or any of the remaining ones
1211a - unstage this hunk and all later hunks in the file
1212d - do not unstage this hunk or any of the later hunks in the file"),
1213 reset_nothead => N__(
1214"y - apply this hunk to index
1215n - do not apply this hunk to index
1216q - quit; do not apply this hunk or any of the remaining ones
1217a - apply this hunk and all later hunks in the file
1218d - do not apply this hunk or any of the later hunks in the file"),
1219 checkout_index => N__(
1220"y - discard this hunk from worktree
1221n - do not discard this hunk from worktree
1222q - quit; do not discard this hunk or any of the remaining ones
1223a - discard this hunk and all later hunks in the file
1224d - do not discard this hunk or any of the later hunks in the file"),
1225 checkout_head => N__(
1226"y - discard this hunk from index and worktree
1227n - do not discard this hunk from index and worktree
1228q - quit; do not discard this hunk or any of the remaining ones
1229a - discard this hunk and all later hunks in the file
1230d - do not discard this hunk or any of the later hunks in the file"),
1231 checkout_nothead => N__(
1232"y - apply this hunk to index and worktree
1233n - do not apply this hunk to index and worktree
1234q - quit; do not apply this hunk or any of the remaining ones
1235a - apply this hunk and all later hunks in the file
1236d - do not apply this hunk or any of the later hunks in the file"),
1237);
1238
5cde71d6 1239sub help_patch_cmd {
186b52c5 1240 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
070434d0 1241g - select a hunk to go to
dd971cc9 1242/ - search for a hunk matching the given regex
5cde71d6
JH
1243j - leave this hunk undecided, see next undecided hunk
1244J - leave this hunk undecided, see next hunk
1245k - leave this hunk undecided, see previous undecided hunk
1246K - leave this hunk undecided, see previous hunk
835b2aeb 1247s - split the current hunk into smaller hunks
ac083c47 1248e - manually edit the current hunk
280e50c7 1249? - print help
5cde71d6
JH
1250EOF
1251}
1252
8f0bef6d
TR
1253sub apply_patch {
1254 my $cmd = shift;
9dce8323 1255 my $ret = run_git_apply $cmd, @_;
8f0bef6d
TR
1256 if (!$ret) {
1257 print STDERR @_;
1258 }
1259 return $ret;
1260}
1261
4f353658
TR
1262sub apply_patch_for_checkout_commit {
1263 my $reverse = shift;
9dce8323
JH
1264 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1265 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
4f353658
TR
1266
1267 if ($applies_worktree && $applies_index) {
9dce8323
JH
1268 run_git_apply 'apply '.$reverse.' --cached', @_;
1269 run_git_apply 'apply '.$reverse, @_;
4f353658
TR
1270 return 1;
1271 } elsif (!$applies_index) {
258e7790
VA
1272 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1273 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
9dce8323 1274 return run_git_apply 'apply '.$reverse, @_;
4f353658 1275 } else {
258e7790 1276 print colored $error_color, __("Nothing was applied.\n");
4f353658
TR
1277 return 0;
1278 }
1279 } else {
1280 print STDERR @_;
1281 return 0;
1282 }
1283}
1284
5cde71d6 1285sub patch_update_cmd {
8f0bef6d 1286 my @all_mods = list_modified($patch_mode_flavour{FILTER});
13c58c17 1287 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
4066bd67
JK
1288 for grep { $_->{UNMERGED} } @all_mods;
1289 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1290
9fe7a643 1291 my @mods = grep { !($_->{BINARY}) } @all_mods;
b63e9950 1292 my @them;
5cde71d6 1293
b63e9950 1294 if (!@mods) {
9fe7a643 1295 if (@all_mods) {
258e7790 1296 print STDERR __("Only binary files changed.\n");
9fe7a643 1297 } else {
258e7790 1298 print STDERR __("No changes.\n");
9fe7a643 1299 }
b63e9950
WC
1300 return 0;
1301 }
c852bd54 1302 if ($patch_mode_only) {
b63e9950
WC
1303 @them = @mods;
1304 }
1305 else {
258e7790 1306 @them = list_and_choose({ PROMPT => __('Patch update'),
b63e9950
WC
1307 HEADER => $status_head, },
1308 @mods);
1309 }
12db334e 1310 for (@them) {
9a7a1e03 1311 return 0 if patch_update_file($_->{VALUE});
12db334e 1312 }
a7d9da6c 1313}
5cde71d6 1314
3f6aff68
WP
1315# Generate a one line summary of a hunk.
1316sub summarize_hunk {
1317 my $rhunk = shift;
1318 my $summary = $rhunk->{TEXT}[0];
1319
1320 # Keep the line numbers, discard extra context.
1321 $summary =~ s/@@(.*?)@@.*/$1 /s;
1322 $summary .= " " x (20 - length $summary);
1323
1324 # Add some user context.
1325 for my $line (@{$rhunk->{TEXT}}) {
1326 if ($line =~ m/^[+-].*\w/) {
1327 $summary .= $line;
1328 last;
1329 }
1330 }
1331
1332 chomp $summary;
1333 return substr($summary, 0, 80) . "\n";
1334}
1335
1336
1337# Print a one-line summary of each hunk in the array ref in
41ccfdd9 1338# the first argument, starting with the index in the 2nd.
3f6aff68
WP
1339sub display_hunks {
1340 my ($hunks, $i) = @_;
1341 my $ctr = 0;
1342 $i ||= 0;
1343 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1344 my $status = " ";
1345 if (defined $hunks->[$i]{USE}) {
1346 $status = $hunks->[$i]{USE} ? "+" : "-";
1347 }
1348 printf "%s%2d: %s",
1349 $status,
1350 $i + 1,
1351 summarize_hunk($hunks->[$i]);
1352 }
1353 return $i;
1354}
1355
0539d5e6
VA
1356my %patch_update_prompt_modes = (
1357 stage => {
1358 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1359 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1360 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1361 },
1362 stash => {
1363 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1364 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1365 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1366 },
1367 reset_head => {
1368 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1369 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1370 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1371 },
1372 reset_nothead => {
1373 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1374 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1375 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1376 },
1377 checkout_index => {
1378 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1379 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1380 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1381 },
1382 checkout_head => {
1383 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1384 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1385 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1386 },
1387 checkout_nothead => {
1388 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1389 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1390 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1391 },
1392);
1393
a7d9da6c 1394sub patch_update_file {
9a7a1e03 1395 my $quit = 0;
5cde71d6 1396 my ($ix, $num);
a7d9da6c 1397 my $path = shift;
5cde71d6 1398 my ($head, @hunk) = parse_diff($path);
24ab81ae 1399 ($head, my $mode, my $deletion) = parse_diff_header($head);
4af756f3
WC
1400 for (@{$head->{DISPLAY}}) {
1401 print;
1402 }
ca724686
JK
1403
1404 if (@{$mode->{TEXT}}) {
0392513f 1405 unshift @hunk, $mode;
ca724686 1406 }
8947fdd5
JK
1407 if (@{$deletion->{TEXT}}) {
1408 foreach my $hunk (@hunk) {
1409 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1410 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1411 }
24ab81ae
JK
1412 @hunk = ($deletion);
1413 }
ca724686 1414
5cde71d6
JH
1415 $num = scalar @hunk;
1416 $ix = 0;
1417
1418 while (1) {
835b2aeb 1419 my ($prev, $next, $other, $undecided, $i);
5cde71d6
JH
1420 $other = '';
1421
1422 if ($num <= $ix) {
1423 $ix = 0;
1424 }
835b2aeb 1425 for ($i = 0; $i < $ix; $i++) {
5cde71d6
JH
1426 if (!defined $hunk[$i]{USE}) {
1427 $prev = 1;
57886bc7 1428 $other .= ',k';
5cde71d6
JH
1429 last;
1430 }
1431 }
1432 if ($ix) {
57886bc7 1433 $other .= ',K';
5cde71d6 1434 }
835b2aeb 1435 for ($i = $ix + 1; $i < $num; $i++) {
5cde71d6
JH
1436 if (!defined $hunk[$i]{USE}) {
1437 $next = 1;
57886bc7 1438 $other .= ',j';
5cde71d6
JH
1439 last;
1440 }
1441 }
1442 if ($ix < $num - 1) {
57886bc7 1443 $other .= ',J';
5cde71d6 1444 }
070434d0 1445 if ($num > 1) {
4404b2e3 1446 $other .= ',g';
070434d0 1447 }
835b2aeb 1448 for ($i = 0; $i < $num; $i++) {
5cde71d6
JH
1449 if (!defined $hunk[$i]{USE}) {
1450 $undecided = 1;
1451 last;
1452 }
1453 }
1454 last if (!$undecided);
1455
0392513f
JK
1456 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1457 hunk_splittable($hunk[$ix]{TEXT})) {
57886bc7 1458 $other .= ',s';
835b2aeb 1459 }
0392513f
JK
1460 if ($hunk[$ix]{TYPE} eq 'hunk') {
1461 $other .= ',e';
1462 }
4af756f3
WC
1463 for (@{$hunk[$ix]{DISPLAY}}) {
1464 print;
1465 }
0539d5e6
VA
1466 print colored $prompt_color,
1467 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1468
ca6ac7f1 1469 my $line = prompt_single_character;
a8bec7ab 1470 last unless defined $line;
5cde71d6
JH
1471 if ($line) {
1472 if ($line =~ /^y/i) {
1473 $hunk[$ix]{USE} = 1;
1474 }
1475 elsif ($line =~ /^n/i) {
1476 $hunk[$ix]{USE} = 0;
1477 }
1478 elsif ($line =~ /^a/i) {
1479 while ($ix < $num) {
1480 if (!defined $hunk[$ix]{USE}) {
1481 $hunk[$ix]{USE} = 1;
1482 }
1483 $ix++;
1484 }
1485 next;
1486 }
070434d0
WP
1487 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1488 my $response = $1;
1489 my $no = $ix > 10 ? $ix - 10 : 0;
1490 while ($response eq '') {
070434d0
WP
1491 $no = display_hunks(\@hunk, $no);
1492 if ($no < $num) {
258e7790
VA
1493 print __("go to which hunk (<ret> to see more)? ");
1494 } else {
1495 print __("go to which hunk? ");
070434d0 1496 }
070434d0 1497 $response = <STDIN>;
68c02d7c
TR
1498 if (!defined $response) {
1499 $response = '';
1500 }
070434d0
WP
1501 chomp $response;
1502 }
1503 if ($response !~ /^\s*\d+\s*$/) {
13c58c17
VA
1504 error_msg sprintf(__("Invalid number: '%s'\n"),
1505 $response);
070434d0
WP
1506 } elsif (0 < $response && $response <= $num) {
1507 $ix = $response - 1;
1508 } else {
c4a85c3b
VA
1509 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1510 "Sorry, only %d hunks available.\n", $num), $num);
070434d0
WP
1511 }
1512 next;
1513 }
5cde71d6
JH
1514 elsif ($line =~ /^d/i) {
1515 while ($ix < $num) {
1516 if (!defined $hunk[$ix]{USE}) {
1517 $hunk[$ix]{USE} = 0;
1518 }
1519 $ix++;
1520 }
1521 next;
1522 }
9a7a1e03 1523 elsif ($line =~ /^q/i) {
f5ea3f2b
JH
1524 for ($i = 0; $i < $num; $i++) {
1525 if (!defined $hunk[$i]{USE}) {
1526 $hunk[$i]{USE} = 0;
9a7a1e03 1527 }
9a7a1e03
MM
1528 }
1529 $quit = 1;
f5ea3f2b 1530 last;
9a7a1e03 1531 }
dd971cc9 1532 elsif ($line =~ m|^/(.*)|) {
ca6ac7f1
TR
1533 my $regex = $1;
1534 if ($1 eq "") {
258e7790 1535 print colored $prompt_color, __("search for regex? ");
ca6ac7f1
TR
1536 $regex = <STDIN>;
1537 if (defined $regex) {
1538 chomp $regex;
1539 }
1540 }
dd971cc9
WP
1541 my $search_string;
1542 eval {
ca6ac7f1 1543 $search_string = qr{$regex}m;
dd971cc9
WP
1544 };
1545 if ($@) {
1546 my ($err,$exp) = ($@, $1);
1547 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
13c58c17 1548 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
dd971cc9
WP
1549 next;
1550 }
1551 my $iy = $ix;
1552 while (1) {
1553 my $text = join ("", @{$hunk[$iy]{TEXT}});
1554 last if ($text =~ $search_string);
1555 $iy++;
1556 $iy = 0 if ($iy >= $num);
1557 if ($ix == $iy) {
258e7790 1558 error_msg __("No hunk matches the given pattern\n");
dd971cc9
WP
1559 last;
1560 }
1561 }
1562 $ix = $iy;
1563 next;
1564 }
ace30ba8
WP
1565 elsif ($line =~ /^K/) {
1566 if ($other =~ /K/) {
1567 $ix--;
1568 }
1569 else {
258e7790 1570 error_msg __("No previous hunk\n");
ace30ba8 1571 }
5cde71d6
JH
1572 next;
1573 }
ace30ba8
WP
1574 elsif ($line =~ /^J/) {
1575 if ($other =~ /J/) {
1576 $ix++;
1577 }
1578 else {
258e7790 1579 error_msg __("No next hunk\n");
ace30ba8 1580 }
5cde71d6
JH
1581 next;
1582 }
ace30ba8
WP
1583 elsif ($line =~ /^k/) {
1584 if ($other =~ /k/) {
1585 while (1) {
1586 $ix--;
1587 last if (!$ix ||
1588 !defined $hunk[$ix]{USE});
1589 }
1590 }
1591 else {
258e7790 1592 error_msg __("No previous hunk\n");
5cde71d6
JH
1593 }
1594 next;
1595 }
ace30ba8
WP
1596 elsif ($line =~ /^j/) {
1597 if ($other !~ /j/) {
258e7790 1598 error_msg __("No next hunk\n");
ace30ba8 1599 next;
5cde71d6 1600 }
5cde71d6 1601 }
835b2aeb 1602 elsif ($other =~ /s/ && $line =~ /^s/) {
4af756f3 1603 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
835b2aeb 1604 if (1 < @split) {
c4a85c3b
VA
1605 print colored $header_color, sprintf(
1606 __n("Split into %d hunk.\n",
1607 "Split into %d hunks.\n",
1608 scalar(@split)), scalar(@split));
835b2aeb 1609 }
4af756f3 1610 splice (@hunk, $ix, 1, @split);
835b2aeb
JH
1611 $num = scalar @hunk;
1612 next;
1613 }
0392513f 1614 elsif ($other =~ /e/ && $line =~ /^e/) {
ac083c47
TR
1615 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1616 if (defined $newhunk) {
1617 splice @hunk, $ix, 1, $newhunk;
1618 }
1619 }
5cde71d6
JH
1620 else {
1621 help_patch_cmd($other);
1622 next;
1623 }
1624 # soft increment
1625 while (1) {
1626 $ix++;
1627 last if ($ix >= $num ||
1628 !defined $hunk[$ix]{USE});
1629 }
1630 }
1631 }
1632
7a26e653
JH
1633 @hunk = coalesce_overlapping_hunks(@hunk);
1634
7b40a455 1635 my $n_lofs = 0;
5cde71d6
JH
1636 my @result = ();
1637 for (@hunk) {
8cbd4310
TR
1638 if ($_->{USE}) {
1639 push @result, @{$_->{TEXT}};
5cde71d6
JH
1640 }
1641 }
1642
1643 if (@result) {
e1327ed5 1644 my @patch = reassemble_patch($head->{TEXT}, @result);
8f0bef6d
TR
1645 my $apply_routine = $patch_mode_flavour{APPLY};
1646 &$apply_routine(@patch);
5cde71d6
JH
1647 refresh();
1648 }
1649
1650 print "\n";
9a7a1e03 1651 return $quit;
5cde71d6
JH
1652}
1653
1654sub diff_cmd {
1655 my @mods = list_modified('index-only');
1656 @mods = grep { !($_->{BINARY}) } @mods;
1657 return if (!@mods);
258e7790 1658 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
5cde71d6
JH
1659 IMMEDIATE => 1,
1660 HEADER => $status_head, },
1661 @mods);
1662 return if (!@them);
258e7790 1663 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
18bc7616
JK
1664 system(qw(git diff -p --cached), $reference, '--',
1665 map { $_->{VALUE} } @them);
5cde71d6
JH
1666}
1667
1668sub quit_cmd {
258e7790 1669 print __("Bye.\n");
5cde71d6
JH
1670 exit(0);
1671}
1672
1673sub help_cmd {
5fa83264
VA
1674# TRANSLATORS: please do not translate the command names
1675# 'status', 'update', 'revert', etc.
1676 print colored $help_color, __ <<'EOF' ;
5cde71d6
JH
1677status - show paths with changes
1678update - add working tree state to the staged set of changes
1679revert - revert staged set of changes back to the HEAD version
1680patch - pick hunks and update selectively
e519eccd 1681diff - view diff between HEAD and index
5cde71d6
JH
1682add untracked - add contents of untracked files to the staged set of changes
1683EOF
1684}
1685
b63e9950
WC
1686sub process_args {
1687 return unless @ARGV;
1688 my $arg = shift @ARGV;
d002ef4d
TR
1689 if ($arg =~ /--patch(?:=(.*))?/) {
1690 if (defined $1) {
1691 if ($1 eq 'reset') {
1692 $patch_mode = 'reset_head';
1693 $patch_mode_revision = 'HEAD';
258e7790 1694 $arg = shift @ARGV or die __("missing --");
d002ef4d
TR
1695 if ($arg ne '--') {
1696 $patch_mode_revision = $arg;
1697 $patch_mode = ($arg eq 'HEAD' ?
1698 'reset_head' : 'reset_nothead');
258e7790 1699 $arg = shift @ARGV or die __("missing --");
d002ef4d 1700 }
4f353658 1701 } elsif ($1 eq 'checkout') {
258e7790 1702 $arg = shift @ARGV or die __("missing --");
4f353658
TR
1703 if ($arg eq '--') {
1704 $patch_mode = 'checkout_index';
1705 } else {
1706 $patch_mode_revision = $arg;
1707 $patch_mode = ($arg eq 'HEAD' ?
1708 'checkout_head' : 'checkout_nothead');
258e7790 1709 $arg = shift @ARGV or die __("missing --");
4f353658 1710 }
dda1f2a5
TR
1711 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1712 $patch_mode = $1;
258e7790 1713 $arg = shift @ARGV or die __("missing --");
d002ef4d 1714 } else {
13c58c17 1715 die sprintf(__("unknown --patch mode: %s"), $1);
d002ef4d
TR
1716 }
1717 } else {
1718 $patch_mode = 'stage';
258e7790 1719 $arg = shift @ARGV or die __("missing --");
d002ef4d 1720 }
13c58c17
VA
1721 die sprintf(__("invalid argument %s, expecting --"),
1722 $arg) unless $arg eq "--";
d002ef4d 1723 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
c852bd54 1724 $patch_mode_only = 1;
b63e9950
WC
1725 }
1726 elsif ($arg ne "--") {
13c58c17 1727 die sprintf(__("invalid argument %s, expecting --"), $arg);
b63e9950
WC
1728 }
1729}
1730
5cde71d6
JH
1731sub main_loop {
1732 my @cmd = ([ 'status', \&status_cmd, ],
1733 [ 'update', \&update_cmd, ],
1734 [ 'revert', \&revert_cmd, ],
1735 [ 'add untracked', \&add_untracked_cmd, ],
1736 [ 'patch', \&patch_update_cmd, ],
1737 [ 'diff', \&diff_cmd, ],
1738 [ 'quit', \&quit_cmd, ],
1739 [ 'help', \&help_cmd, ],
1740 );
1741 while (1) {
258e7790 1742 my ($it) = list_and_choose({ PROMPT => __('What now'),
5cde71d6
JH
1743 SINGLETON => 1,
1744 LIST_FLAT => 4,
258e7790 1745 HEADER => __('*** Commands ***'),
c95c0248 1746 ON_EOF => \&quit_cmd,
5cde71d6
JH
1747 IMMEDIATE => 1 }, @cmd);
1748 if ($it) {
1749 eval {
1750 $it->[1]->();
1751 };
1752 if ($@) {
1753 print "$@";
1754 }
1755 }
1756 }
1757}
1758
b63e9950 1759process_args();
5cde71d6 1760refresh();
c852bd54 1761if ($patch_mode_only) {
b63e9950
WC
1762 patch_update_cmd();
1763}
1764else {
1765 status_cmd();
1766 main_loop();
1767}