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 --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 for (my $i = 0; $i < @diff; $i++) {
709 if ($diff[$i] =~ /^@@ /) {
710 push @hunk, { TEXT => [], DISPLAY => [],
711 TYPE => 'hunk' };
712 }
713 push @{$hunk[-1]{TEXT}}, $diff[$i];
714 push @{$hunk[-1]{DISPLAY}},
715 (@colored ? $colored[$i] : $diff[$i]);
716 }
717 return @hunk;
718}
719
720sub parse_diff_header {
721 my $src = shift;
722
723 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
724 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
725 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
726
727 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
728 my $dest =
729 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
730 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
731 $head;
732 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
733 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
734 }
735 return ($head, $mode, $deletion);
736}
737
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) =
748 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
749 $o_cnt = 1 unless defined $o_cnt;
750 $n_cnt = 1 unless defined $n_cnt;
751 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
752}
753
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
763sub split_hunk {
764 my ($text, $display) = @_;
765 my @split = ();
766 if (!defined $display) {
767 $display = $text;
768 }
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
773 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
774 my $hunk_start = 1;
775
776 OUTER:
777 while (1) {
778 my $next_hunk_start = undef;
779 my $i = $hunk_start - 1;
780 my $this = +{
781 TEXT => [],
782 DISPLAY => [],
783 TYPE => 'hunk',
784 OLD => $o_ofs,
785 NEW => $n_ofs,
786 OCNT => 0,
787 NCNT => 0,
788 ADDDEL => 0,
789 POSTCTX => 0,
790 USE => undef,
791 };
792
793 while (++$i < @$text) {
794 my $line = $text->[$i];
795 my $display = $display->[$i];
796 if ($line =~ /^\\/) {
797 push @{$this->{TEXT}}, $line;
798 push @{$this->{DISPLAY}}, $display;
799 next;
800 }
801 if ($line =~ /^ /) {
802 if ($this->{ADDDEL} &&
803 !defined $next_hunk_start) {
804 # We have seen leading context and
805 # adds/dels and then here is another
806 # context, which is trailing for this
807 # split hunk and leading for the next
808 # one.
809 $next_hunk_start = $i;
810 }
811 push @{$this->{TEXT}}, $line;
812 push @{$this->{DISPLAY}}, $display;
813 $this->{OCNT}++;
814 $this->{NCNT}++;
815 if (defined $next_hunk_start) {
816 $this->{POSTCTX}++;
817 }
818 next;
819 }
820
821 # add/del
822 if (defined $next_hunk_start) {
823 # We are done with the current hunk and
824 # this is the first real change for the
825 # next split one.
826 $hunk_start = $next_hunk_start;
827 $o_ofs = $this->{OLD} + $this->{OCNT};
828 $n_ofs = $this->{NEW} + $this->{NCNT};
829 $o_ofs -= $this->{POSTCTX};
830 $n_ofs -= $this->{POSTCTX};
831 push @split, $this;
832 redo OUTER;
833 }
834 push @{$this->{TEXT}}, $line;
835 push @{$this->{DISPLAY}}, $display;
836 $this->{ADDDEL}++;
837 if ($line =~ /^-/) {
838 $this->{OCNT}++;
839 }
840 else {
841 $this->{NCNT}++;
842 }
843 }
844
845 push @split, $this;
846 last;
847 }
848
849 for my $hunk (@split) {
850 $o_ofs = $hunk->{OLD};
851 $n_ofs = $hunk->{NEW};
852 my $o_cnt = $hunk->{OCNT};
853 my $n_cnt = $hunk->{NCNT};
854
855 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
856 my $display_head = $head;
857 unshift @{$hunk->{TEXT}}, $head;
858 if ($diff_use_color) {
859 $display_head = colored($fraginfo_color, $head);
860 }
861 unshift @{$hunk->{DISPLAY}}, $display_head;
862 }
863 return @split;
864}
865
866sub find_last_o_ctx {
867 my ($it) = @_;
868 my $text = $it->{TEXT};
869 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
870 my $i = @{$text};
871 my $last_o_ctx = $o_ofs + $o_cnt;
872 while (0 < --$i) {
873 my $line = $text->[$i];
874 if ($line =~ /^ /) {
875 $last_o_ctx--;
876 next;
877 }
878 last;
879 }
880 return $last_o_ctx;
881}
882
883sub merge_hunk {
884 my ($prev, $this) = @_;
885 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
886 parse_hunk_header($prev->{TEXT}[0]);
887 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
888 parse_hunk_header($this->{TEXT}[0]);
889
890 my (@line, $i, $ofs, $o_cnt, $n_cnt);
891 $ofs = $o0_ofs;
892 $o_cnt = $n_cnt = 0;
893 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
894 my $line = $prev->{TEXT}[$i];
895 if ($line =~ /^\+/) {
896 $n_cnt++;
897 push @line, $line;
898 next;
899 } elsif ($line =~ /^\\/) {
900 push @line, $line;
901 next;
902 }
903
904 last if ($o1_ofs <= $ofs);
905
906 $o_cnt++;
907 $ofs++;
908 if ($line =~ /^ /) {
909 $n_cnt++;
910 }
911 push @line, $line;
912 }
913
914 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
915 my $line = $this->{TEXT}[$i];
916 if ($line =~ /^\+/) {
917 $n_cnt++;
918 push @line, $line;
919 next;
920 } elsif ($line =~ /^\\/) {
921 push @line, $line;
922 next;
923 }
924 $ofs++;
925 $o_cnt++;
926 if ($line =~ /^ /) {
927 $n_cnt++;
928 }
929 push @line, $line;
930 }
931 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
932 @{$prev->{TEXT}} = ($head, @line);
933}
934
935sub coalesce_overlapping_hunks {
936 my (@in) = @_;
937 my @out = ();
938
939 my ($last_o_ctx, $last_was_dirty);
940 my $ofs_delta = 0;
941
942 for (@in) {
943 if ($_->{TYPE} ne 'hunk') {
944 push @out, $_;
945 next;
946 }
947 my $text = $_->{TEXT};
948 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
949 parse_hunk_header($text->[0]);
950 unless ($_->{USE}) {
951 $ofs_delta += $o_cnt - $n_cnt;
952 # If this hunk has been edited then subtract
953 # the delta that is due to the edit.
954 if ($_->{OFS_DELTA}) {
955 $ofs_delta -= $_->{OFS_DELTA};
956 }
957 next;
958 }
959 if ($ofs_delta) {
960 $n_ofs += $ofs_delta;
961 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
962 $n_ofs, $n_cnt);
963 }
964 # If this hunk was edited then adjust the offset delta
965 # to reflect the edit.
966 if ($_->{OFS_DELTA}) {
967 $ofs_delta += $_->{OFS_DELTA};
968 }
969 if (defined $last_o_ctx &&
970 $o_ofs <= $last_o_ctx &&
971 !$_->{DIRTY} &&
972 !$last_was_dirty) {
973 merge_hunk($out[-1], $_);
974 }
975 else {
976 push @out, $_;
977 }
978 $last_o_ctx = find_last_o_ctx($out[-1]);
979 $last_was_dirty = $_->{DIRTY};
980 }
981 return @out;
982}
983
984sub reassemble_patch {
985 my $head = shift;
986 my @patch;
987
988 # Include everything in the header except the beginning of the diff.
989 push @patch, (grep { !/^[-+]{3}/ } @$head);
990
991 # Then include any headers from the hunk lines, which must
992 # come before any actual hunk.
993 while (@_ && $_[0] !~ /^@/) {
994 push @patch, shift;
995 }
996
997 # Then begin the diff.
998 push @patch, grep { /^[-+]{3}/ } @$head;
999
1000 # And then the actual hunks.
1001 push @patch, @_;
1002
1003 return @patch;
1004}
1005
1006sub color_diff {
1007 return map {
1008 colored((/^@/ ? $fraginfo_color :
1009 /^\+/ ? $diff_new_color :
1010 /^-/ ? $diff_old_color :
1011 $diff_plain_color),
1012 $_);
1013 } @_;
1014}
1015
1016sub label_hunk_lines {
1017 local $_;
1018 my $hunk = shift;
1019 my $i = 0;
1020 my $labels = [ map { /^[-+]/ ? ++$i : 0 } @{$hunk->{TEXT}} ];
1021 if ($i > 1) {
1022 @{$hunk}{qw(LABELS MAX_LABEL)} = ($labels, $i);
1023 return 1;
1024 }
1025 return 0;
1026}
1027
1028sub select_hunk_lines {
1029 my ($hunk, $selected) = @_;
1030 my ($text, $labels) = @{$hunk}{qw(TEXT LABELS)};
1031 my ($i, $o_cnt, $n_cnt) = (0, 0, 0);
1032 my ($push_eol, @newtext);
1033 # Lines with this mode will become context lines if they are
1034 # not selected
1035 my $context_mode = $patch_mode_flavour{IS_REVERSE} ? '+' : '-';
1036 for $i (1..$#{$text}) {
1037 my $mode = substr($text->[$i], 0, 1);
1038 if ($mode eq '\\') {
1039 push @newtext, $text->[$i] if ($push_eol);
1040 undef $push_eol;
1041 } elsif ($labels->[$i] and $selected->[$labels->[$i]]) {
1042 push @newtext, $text->[$i];
1043 if ($mode eq '+') {
1044 $n_cnt++;
1045 } else {
1046 $o_cnt++;
1047 }
1048 $push_eol = 1;
1049 } elsif ($mode eq ' ' or $mode eq $context_mode) {
1050 push @newtext, ' ' . substr($text->[$i], 1);
1051 $o_cnt++; $n_cnt++;
1052 $push_eol = 1;
1053 } else {
1054 undef $push_eol;
1055 }
1056 }
1057 my ($o_ofs, $orig_o_cnt, $n_ofs, $orig_n_cnt) =
1058 parse_hunk_header($text->[0]);
1059 unshift @newtext, format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1060 my $newhunk = {
1061 TEXT => \@newtext,
1062 DISPLAY => [ color_diff(@newtext) ],
1063 OFS_DELTA => $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt,
1064 TYPE => $hunk->{TYPE},
1065 USE => 1,
1066 };
1067 # If this hunk has previously been edited add the offset delta
1068 # of the old hunk to get the real delta from the original
1069 # hunk.
1070 if ($hunk->{OFS_DELTA}) {
1071 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1072 }
1073 return $newhunk;
1074}
1075
1076sub check_hunk_label {
1077 my ($max_label, $label) = ($_[0]->{MAX_LABEL}, $_[1]);
1078 if ($label < 1 or $label > $max_label) {
1079 error_msg sprintf(__("invalid hunk line '%d'\n"), $label);
1080 return 0;
1081 }
1082 return 1;
1083}
1084
1085sub split_hunk_selection {
1086 local $_;
1087 my @fields = @_;
1088 my @ret;
1089 for (@fields) {
1090 while ($_ ne '') {
1091 if (/^[0-9]-$/) {
1092 push @ret, $_;
1093 last;
1094 } elsif (/^([0-9](?:-[0-9])?)(.*)/) {
1095 push @ret, $1;
1096 $_ = $2;
1097 } else {
1098 error_msg sprintf
1099 __("invalid hunk line '%s'\n"),
1100 substr($_, 0, 1);
1101 return ();
1102 }
1103 }
1104 }
1105 return @ret;
1106}
1107
1108sub parse_hunk_selection {
1109 local $_;
1110 my ($hunk, $line) = @_;
1111 my ($max_label, $invert) = ($hunk->{MAX_LABEL}, undef);
1112 my @selected = (0) x ($max_label + 1);
1113 my @fields = split(/[,\s]+/, $line);
1114 if ($fields[0] =~ /^-(.*)/) {
1115 $invert = 1;
1116 if ($1 ne '') {
1117 $fields[0] = $1;
1118 } else {
1119 shift @fields;
1120 unless (@fields) {
1121 error_msg __("no lines to invert\n");
1122 return undef;
1123 }
1124 }
1125 }
1126 if ($max_label < 10) {
1127 @fields = split_hunk_selection(@fields) or return undef;
1128 }
1129 for (@fields) {
1130 if (my ($lo, $hi) = /^([0-9]+)-([0-9]*)$/) {
1131 if ($hi eq '') {
1132 $hi = $max_label;
1133 }
1134 check_hunk_label($hunk, $lo) or return undef;
1135 check_hunk_label($hunk, $hi) or return undef;
1136 if ($hi < $lo) {
1137 ($lo, $hi) = ($hi, $lo);
1138 }
1139 @selected[$lo..$hi] = (1) x (1 + $hi - $lo);
1140 } elsif (/^([0-9]+)$/) {
1141 check_hunk_label($hunk, $1) or return undef;
1142 $selected[$1] = 1;
1143 } else {
1144 error_msg sprintf(__("invalid hunk line '%s'\n"), $_);
1145 return undef;
1146 }
1147 }
1148 if ($invert) {
1149 @selected = map { !$_ } @selected;
1150 }
1151 return \@selected;
1152}
1153
1154sub display_hunk_lines {
1155 my ($display, $labels, $max_label) =
1156 @{$_[0]}{qw(DISPLAY LABELS MAX_LABEL)};
1157 my $width = int(log($max_label) / log(10)) + 1;
1158 my $padding = ' ' x ($width + 1);
1159 for my $i (0..$#{$display}) {
1160 if ($labels->[$i]) {
1161 printf '%*d %s', $width, $labels->[$i], $display->[$i];
1162 } else {
1163 print $padding . $display->[$i];
1164 }
1165 }
1166}
1167
1168sub select_lines_loop {
1169 my $hunk = shift;
1170 display_hunk_lines($hunk);
1171 my $selection = undef;
1172 until (defined $selection) {
1173 print colored $prompt_color, __("select lines? ");
1174 my $text = <STDIN>;
1175 defined $text and $text =~ /\S/ or return undef;
1176 $selection = parse_hunk_selection($hunk, $text);
1177 }
1178 return select_hunk_lines($hunk, $selection);
1179}
1180
1181my %edit_hunk_manually_modes = (
1182 stage => N__(
1183"If the patch applies cleanly, the edited hunk will immediately be
1184marked for staging."),
1185 stash => N__(
1186"If the patch applies cleanly, the edited hunk will immediately be
1187marked for stashing."),
1188 reset_head => N__(
1189"If the patch applies cleanly, the edited hunk will immediately be
1190marked for unstaging."),
1191 reset_nothead => N__(
1192"If the patch applies cleanly, the edited hunk will immediately be
1193marked for applying."),
1194 checkout_index => N__(
1195"If the patch applies cleanly, the edited hunk will immediately be
1196marked for discarding."),
1197 checkout_head => N__(
1198"If the patch applies cleanly, the edited hunk will immediately be
1199marked for discarding."),
1200 checkout_nothead => N__(
1201"If the patch applies cleanly, the edited hunk will immediately be
1202marked for applying."),
1203);
1204
1205sub recount_edited_hunk {
1206 local $_;
1207 my ($oldtext, $newtext) = @_;
1208 my ($o_cnt, $n_cnt) = (0, 0);
1209 for (@{$newtext}[1..$#{$newtext}]) {
1210 my $mode = substr($_, 0, 1);
1211 if ($mode eq '-') {
1212 $o_cnt++;
1213 } elsif ($mode eq '+') {
1214 $n_cnt++;
1215 } elsif ($mode eq ' ') {
1216 $o_cnt++;
1217 $n_cnt++;
1218 }
1219 }
1220 my ($o_ofs, undef, $n_ofs, undef) =
1221 parse_hunk_header($newtext->[0]);
1222 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1223 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1224 parse_hunk_header($oldtext->[0]);
1225 # Return the change in the number of lines inserted by this hunk
1226 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1227}
1228
1229sub edit_hunk_manually {
1230 my ($oldtext) = @_;
1231
1232 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1233 my $fh;
1234 open $fh, '>', $hunkfile
1235 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1236 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1237 print $fh @$oldtext;
1238 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1239 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1240 my $comment_line_char = Git::get_comment_line_char;
1241 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1242---
1243To remove '%s' lines, make them ' ' lines (context).
1244To remove '%s' lines, delete them.
1245Lines starting with %s will be removed.
1246EOF
1247__($edit_hunk_manually_modes{$patch_mode}),
1248# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1249__ <<EOF2 ;
1250If it does not apply cleanly, you will be given an opportunity to
1251edit again. If all lines of the hunk are removed, then the edit is
1252aborted and the hunk is left unchanged.
1253EOF2
1254 close $fh;
1255
1256 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1257 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1258
1259 if ($? != 0) {
1260 return undef;
1261 }
1262
1263 open $fh, '<', $hunkfile
1264 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1265 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1266 close $fh;
1267 unlink $hunkfile;
1268
1269 # Abort if nothing remains
1270 if (!grep { /\S/ } @newtext) {
1271 return undef;
1272 }
1273
1274 # Reinsert the first hunk header if the user accidentally deleted it
1275 if ($newtext[0] !~ /^@/) {
1276 unshift @newtext, $oldtext->[0];
1277 }
1278 return \@newtext;
1279}
1280
1281sub diff_applies {
1282 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1283 map { @{$_->{TEXT}} } @_);
1284}
1285
1286sub _restore_terminal_and_die {
1287 ReadMode 'restore';
1288 print "\n";
1289 exit 1;
1290}
1291
1292sub prompt_single_character {
1293 if ($use_readkey) {
1294 local $SIG{TERM} = \&_restore_terminal_and_die;
1295 local $SIG{INT} = \&_restore_terminal_and_die;
1296 ReadMode 'cbreak';
1297 my $key = ReadKey 0;
1298 ReadMode 'restore';
1299 if ($use_termcap and $key eq "\e") {
1300 while (!defined $term_escapes{$key}) {
1301 my $next = ReadKey 0.5;
1302 last if (!defined $next);
1303 $key .= $next;
1304 }
1305 $key =~ s/\e/^[/;
1306 }
1307 print "$key" if defined $key;
1308 print "\n";
1309 return $key;
1310 } else {
1311 return <STDIN>;
1312 }
1313}
1314
1315sub prompt_yesno {
1316 my ($prompt) = @_;
1317 while (1) {
1318 print colored $prompt_color, $prompt;
1319 my $line = prompt_single_character;
1320 return undef unless defined $line;
1321 return 0 if $line =~ /^n/i;
1322 return 1 if $line =~ /^y/i;
1323 }
1324}
1325
1326sub edit_hunk_loop {
1327 my ($head, $hunks, $ix) = @_;
1328 my $hunk = $hunks->[$ix];
1329 my $text = $hunk->{TEXT};
1330
1331 while (1) {
1332 my $newtext = edit_hunk_manually($text);
1333 if (!defined $newtext) {
1334 return undef;
1335 }
1336 my $newhunk = {
1337 TEXT => $newtext,
1338 TYPE => $hunk->{TYPE},
1339 USE => 1,
1340 DIRTY => 1,
1341 };
1342 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1343 # If this hunk has already been edited then add the
1344 # offset delta of the previous edit to get the real
1345 # delta from the original unedited hunk.
1346 $hunk->{OFS_DELTA} and
1347 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1348 if (diff_applies($head,
1349 @{$hunks}[0..$ix-1],
1350 $newhunk,
1351 @{$hunks}[$ix+1..$#{$hunks}])) {
1352 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1353 return $newhunk;
1354 }
1355 else {
1356 prompt_yesno(
1357 # TRANSLATORS: do not translate [y/n]
1358 # The program will only accept that input
1359 # at this point.
1360 # Consider translating (saying "no" discards!) as
1361 # (saying "n" for "no" discards!) if the translation
1362 # of the word "no" does not start with n.
1363 __('Your edited hunk does not apply. Edit again '
1364 . '(saying "no" discards!) [y/n]? ')
1365 ) or return undef;
1366 }
1367 }
1368}
1369
1370my %help_patch_modes = (
1371 stage => N__(
1372"y - stage this hunk
1373n - do not stage this hunk
1374q - quit; do not stage this hunk or any of the remaining ones
1375a - stage this hunk and all later hunks in the file
1376d - do not stage this hunk or any of the later hunks in the file"),
1377 stash => N__(
1378"y - stash this hunk
1379n - do not stash this hunk
1380q - quit; do not stash this hunk or any of the remaining ones
1381a - stash this hunk and all later hunks in the file
1382d - do not stash this hunk or any of the later hunks in the file"),
1383 reset_head => N__(
1384"y - unstage this hunk
1385n - do not unstage this hunk
1386q - quit; do not unstage this hunk or any of the remaining ones
1387a - unstage this hunk and all later hunks in the file
1388d - do not unstage this hunk or any of the later hunks in the file"),
1389 reset_nothead => N__(
1390"y - apply this hunk to index
1391n - do not apply this hunk to index
1392q - quit; do not apply this hunk or any of the remaining ones
1393a - apply this hunk and all later hunks in the file
1394d - do not apply this hunk or any of the later hunks in the file"),
1395 checkout_index => N__(
1396"y - discard this hunk from worktree
1397n - do not discard this hunk from worktree
1398q - quit; do not discard this hunk or any of the remaining ones
1399a - discard this hunk and all later hunks in the file
1400d - do not discard this hunk or any of the later hunks in the file"),
1401 checkout_head => N__(
1402"y - discard this hunk from index and worktree
1403n - do not discard this hunk from index and worktree
1404q - quit; do not discard this hunk or any of the remaining ones
1405a - discard this hunk and all later hunks in the file
1406d - do not discard this hunk or any of the later hunks in the file"),
1407 checkout_nothead => N__(
1408"y - apply this hunk to index and worktree
1409n - do not apply this hunk to index and worktree
1410q - quit; do not apply this hunk or any of the remaining ones
1411a - apply this hunk and all later hunks in the file
1412d - do not apply this hunk or any of the later hunks in the file"),
1413);
1414
1415sub help_patch_cmd {
1416 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1417g - select a hunk to go to
1418/ - search for a hunk matching the given regex
1419j - leave this hunk undecided, see next undecided hunk
1420J - leave this hunk undecided, see next hunk
1421k - leave this hunk undecided, see previous undecided hunk
1422K - leave this hunk undecided, see previous hunk
1423l - select hunk lines to use
1424s - split the current hunk into smaller hunks
1425e - manually edit the current hunk
1426? - print help
1427EOF
1428}
1429
1430sub apply_patch {
1431 my $cmd = shift;
1432 my $ret = run_git_apply $cmd, @_;
1433 if (!$ret) {
1434 print STDERR @_;
1435 }
1436 return $ret;
1437}
1438
1439sub apply_patch_for_checkout_commit {
1440 my $reverse = shift;
1441 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1442 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1443
1444 if ($applies_worktree && $applies_index) {
1445 run_git_apply 'apply '.$reverse.' --cached', @_;
1446 run_git_apply 'apply '.$reverse, @_;
1447 return 1;
1448 } elsif (!$applies_index) {
1449 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1450 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1451 return run_git_apply 'apply '.$reverse, @_;
1452 } else {
1453 print colored $error_color, __("Nothing was applied.\n");
1454 return 0;
1455 }
1456 } else {
1457 print STDERR @_;
1458 return 0;
1459 }
1460}
1461
1462sub patch_update_cmd {
1463 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1464 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1465 for grep { $_->{UNMERGED} } @all_mods;
1466 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1467
1468 my @mods = grep { !($_->{BINARY}) } @all_mods;
1469 my @them;
1470
1471 if (!@mods) {
1472 if (@all_mods) {
1473 print STDERR __("Only binary files changed.\n");
1474 } else {
1475 print STDERR __("No changes.\n");
1476 }
1477 return 0;
1478 }
1479 if ($patch_mode_only) {
1480 @them = @mods;
1481 }
1482 else {
1483 @them = list_and_choose({ PROMPT => __('Patch update'),
1484 HEADER => $status_head, },
1485 @mods);
1486 }
1487 for (@them) {
1488 return 0 if patch_update_file($_->{VALUE});
1489 }
1490}
1491
1492# Generate a one line summary of a hunk.
1493sub summarize_hunk {
1494 my $rhunk = shift;
1495 my $summary = $rhunk->{TEXT}[0];
1496
1497 # Keep the line numbers, discard extra context.
1498 $summary =~ s/@@(.*?)@@.*/$1 /s;
1499 $summary .= " " x (20 - length $summary);
1500
1501 # Add some user context.
1502 for my $line (@{$rhunk->{TEXT}}) {
1503 if ($line =~ m/^[+-].*\w/) {
1504 $summary .= $line;
1505 last;
1506 }
1507 }
1508
1509 chomp $summary;
1510 return substr($summary, 0, 80) . "\n";
1511}
1512
1513
1514# Print a one-line summary of each hunk in the array ref in
1515# the first argument, starting with the index in the 2nd.
1516sub display_hunks {
1517 my ($hunks, $i) = @_;
1518 my $ctr = 0;
1519 $i ||= 0;
1520 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1521 my $status = " ";
1522 if (defined $hunks->[$i]{USE}) {
1523 $status = $hunks->[$i]{USE} ? "+" : "-";
1524 }
1525 printf "%s%2d: %s",
1526 $status,
1527 $i + 1,
1528 summarize_hunk($hunks->[$i]);
1529 }
1530 return $i;
1531}
1532
1533my %patch_update_prompt_modes = (
1534 stage => {
1535 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1536 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1537 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1538 },
1539 stash => {
1540 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1541 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1542 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1543 },
1544 reset_head => {
1545 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1546 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1547 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1548 },
1549 reset_nothead => {
1550 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1551 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1552 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1553 },
1554 checkout_index => {
1555 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1556 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1557 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1558 },
1559 checkout_head => {
1560 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1561 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1562 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1563 },
1564 checkout_nothead => {
1565 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1566 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1567 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1568 },
1569);
1570
1571sub patch_update_file {
1572 my $quit = 0;
1573 my ($ix, $num);
1574 my $path = shift;
1575 my ($head, @hunk) = parse_diff($path);
1576 ($head, my $mode, my $deletion) = parse_diff_header($head);
1577 for (@{$head->{DISPLAY}}) {
1578 print;
1579 }
1580
1581 if (@{$mode->{TEXT}}) {
1582 unshift @hunk, $mode;
1583 }
1584 if (@{$deletion->{TEXT}}) {
1585 foreach my $hunk (@hunk) {
1586 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1587 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1588 }
1589 @hunk = ($deletion);
1590 }
1591
1592 $num = scalar @hunk;
1593 $ix = 0;
1594
1595 while (1) {
1596 my ($prev, $next, $other, $undecided, $i);
1597 $other = '';
1598
1599 if ($num <= $ix) {
1600 $ix = 0;
1601 }
1602 for ($i = 0; $i < $ix; $i++) {
1603 if (!defined $hunk[$i]{USE}) {
1604 $prev = 1;
1605 $other .= ',k';
1606 last;
1607 }
1608 }
1609 if ($ix) {
1610 $other .= ',K';
1611 }
1612 for ($i = $ix + 1; $i < $num; $i++) {
1613 if (!defined $hunk[$i]{USE}) {
1614 $next = 1;
1615 $other .= ',j';
1616 last;
1617 }
1618 }
1619 if ($ix < $num - 1) {
1620 $other .= ',J';
1621 }
1622 if ($num > 1) {
1623 $other .= ',g';
1624 }
1625 for ($i = 0; $i < $num; $i++) {
1626 if (!defined $hunk[$i]{USE}) {
1627 $undecided = 1;
1628 last;
1629 }
1630 }
1631 last if (!$undecided);
1632
1633 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1634 hunk_splittable($hunk[$ix]{TEXT})) {
1635 $other .= ',s';
1636 }
1637 if ($hunk[$ix]{TYPE} eq 'hunk') {
1638 $other .= ',e';
1639 }
1640 if (label_hunk_lines($hunk[$ix])) {
1641 $other .= ',l';
1642 }
1643 for (@{$hunk[$ix]{DISPLAY}}) {
1644 print;
1645 }
1646 print colored $prompt_color,
1647 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1648
1649 my $line = prompt_single_character;
1650 last unless defined $line;
1651 if ($line) {
1652 if ($line =~ /^y/i) {
1653 $hunk[$ix]{USE} = 1;
1654 }
1655 elsif ($line =~ /^n/i) {
1656 $hunk[$ix]{USE} = 0;
1657 }
1658 elsif ($line =~ /^a/i) {
1659 while ($ix < $num) {
1660 if (!defined $hunk[$ix]{USE}) {
1661 $hunk[$ix]{USE} = 1;
1662 }
1663 $ix++;
1664 }
1665 next;
1666 }
1667 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1668 my $response = $1;
1669 my $no = $ix > 10 ? $ix - 10 : 0;
1670 while ($response eq '') {
1671 $no = display_hunks(\@hunk, $no);
1672 if ($no < $num) {
1673 print __("go to which hunk (<ret> to see more)? ");
1674 } else {
1675 print __("go to which hunk? ");
1676 }
1677 $response = <STDIN>;
1678 if (!defined $response) {
1679 $response = '';
1680 }
1681 chomp $response;
1682 }
1683 if ($response !~ /^\s*\d+\s*$/) {
1684 error_msg sprintf(__("Invalid number: '%s'\n"),
1685 $response);
1686 } elsif (0 < $response && $response <= $num) {
1687 $ix = $response - 1;
1688 } else {
1689 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1690 "Sorry, only %d hunks available.\n", $num), $num);
1691 }
1692 next;
1693 }
1694 elsif ($line =~ /^d/i) {
1695 while ($ix < $num) {
1696 if (!defined $hunk[$ix]{USE}) {
1697 $hunk[$ix]{USE} = 0;
1698 }
1699 $ix++;
1700 }
1701 next;
1702 }
1703 elsif ($line =~ /^q/i) {
1704 for ($i = 0; $i < $num; $i++) {
1705 if (!defined $hunk[$i]{USE}) {
1706 $hunk[$i]{USE} = 0;
1707 }
1708 }
1709 $quit = 1;
1710 last;
1711 }
1712 elsif ($line =~ m|^/(.*)|) {
1713 my $regex = $1;
1714 if ($1 eq "") {
1715 print colored $prompt_color, __("search for regex? ");
1716 $regex = <STDIN>;
1717 if (defined $regex) {
1718 chomp $regex;
1719 }
1720 }
1721 my $search_string;
1722 eval {
1723 $search_string = qr{$regex}m;
1724 };
1725 if ($@) {
1726 my ($err,$exp) = ($@, $1);
1727 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1728 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1729 next;
1730 }
1731 my $iy = $ix;
1732 while (1) {
1733 my $text = join ("", @{$hunk[$iy]{TEXT}});
1734 last if ($text =~ $search_string);
1735 $iy++;
1736 $iy = 0 if ($iy >= $num);
1737 if ($ix == $iy) {
1738 error_msg __("No hunk matches the given pattern\n");
1739 last;
1740 }
1741 }
1742 $ix = $iy;
1743 next;
1744 }
1745 elsif ($line =~ /^K/) {
1746 if ($other =~ /K/) {
1747 $ix--;
1748 }
1749 else {
1750 error_msg __("No previous hunk\n");
1751 }
1752 next;
1753 }
1754 elsif ($line =~ /^J/) {
1755 if ($other =~ /J/) {
1756 $ix++;
1757 }
1758 else {
1759 error_msg __("No next hunk\n");
1760 }
1761 next;
1762 }
1763 elsif ($line =~ /^k/) {
1764 if ($other =~ /k/) {
1765 while (1) {
1766 $ix--;
1767 last if (!$ix ||
1768 !defined $hunk[$ix]{USE});
1769 }
1770 }
1771 else {
1772 error_msg __("No previous hunk\n");
1773 }
1774 next;
1775 }
1776 elsif ($line =~ /^j/) {
1777 if ($other !~ /j/) {
1778 error_msg __("No next hunk\n");
1779 next;
1780 }
1781 }
1782 elsif ($line =~ /^l/) {
1783 unless ($other =~ /l/) {
1784 error_msg __("Cannot select line by line\n");
1785 next;
1786 }
1787 my $newhunk = select_lines_loop($hunk[$ix]);
1788 if ($newhunk) {
1789 splice @hunk, $ix, 1, $newhunk;
1790 } else {
1791 next;
1792 }
1793 }
1794 elsif ($other =~ /s/ && $line =~ /^s/) {
1795 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1796 if (1 < @split) {
1797 print colored $header_color, sprintf(
1798 __n("Split into %d hunk.\n",
1799 "Split into %d hunks.\n",
1800 scalar(@split)), scalar(@split));
1801 }
1802 splice (@hunk, $ix, 1, @split);
1803 $num = scalar @hunk;
1804 next;
1805 }
1806 elsif ($other =~ /e/ && $line =~ /^e/) {
1807 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1808 if (defined $newhunk) {
1809 splice @hunk, $ix, 1, $newhunk;
1810 }
1811 }
1812 else {
1813 help_patch_cmd($other);
1814 next;
1815 }
1816 # soft increment
1817 while (1) {
1818 $ix++;
1819 last if ($ix >= $num ||
1820 !defined $hunk[$ix]{USE});
1821 }
1822 }
1823 }
1824
1825 @hunk = coalesce_overlapping_hunks(@hunk);
1826
1827 my $n_lofs = 0;
1828 my @result = ();
1829 for (@hunk) {
1830 if ($_->{USE}) {
1831 push @result, @{$_->{TEXT}};
1832 }
1833 }
1834
1835 if (@result) {
1836 my @patch = reassemble_patch($head->{TEXT}, @result);
1837 my $apply_routine = $patch_mode_flavour{APPLY};
1838 &$apply_routine(@patch);
1839 refresh();
1840 }
1841
1842 print "\n";
1843 return $quit;
1844}
1845
1846sub diff_cmd {
1847 my @mods = list_modified('index-only');
1848 @mods = grep { !($_->{BINARY}) } @mods;
1849 return if (!@mods);
1850 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1851 IMMEDIATE => 1,
1852 HEADER => $status_head, },
1853 @mods);
1854 return if (!@them);
1855 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1856 system(qw(git diff -p --cached), $reference, '--',
1857 map { $_->{VALUE} } @them);
1858}
1859
1860sub quit_cmd {
1861 print __("Bye.\n");
1862 exit(0);
1863}
1864
1865sub help_cmd {
1866# TRANSLATORS: please do not translate the command names
1867# 'status', 'update', 'revert', etc.
1868 print colored $help_color, __ <<'EOF' ;
1869status - show paths with changes
1870update - add working tree state to the staged set of changes
1871revert - revert staged set of changes back to the HEAD version
1872patch - pick hunks and update selectively
1873diff - view diff between HEAD and index
1874add untracked - add contents of untracked files to the staged set of changes
1875EOF
1876}
1877
1878sub process_args {
1879 return unless @ARGV;
1880 my $arg = shift @ARGV;
1881 if ($arg =~ /--patch(?:=(.*))?/) {
1882 if (defined $1) {
1883 if ($1 eq 'reset') {
1884 $patch_mode = 'reset_head';
1885 $patch_mode_revision = 'HEAD';
1886 $arg = shift @ARGV or die __("missing --");
1887 if ($arg ne '--') {
1888 $patch_mode_revision = $arg;
1889 $patch_mode = ($arg eq 'HEAD' ?
1890 'reset_head' : 'reset_nothead');
1891 $arg = shift @ARGV or die __("missing --");
1892 }
1893 } elsif ($1 eq 'checkout') {
1894 $arg = shift @ARGV or die __("missing --");
1895 if ($arg eq '--') {
1896 $patch_mode = 'checkout_index';
1897 } else {
1898 $patch_mode_revision = $arg;
1899 $patch_mode = ($arg eq 'HEAD' ?
1900 'checkout_head' : 'checkout_nothead');
1901 $arg = shift @ARGV or die __("missing --");
1902 }
1903 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1904 $patch_mode = $1;
1905 $arg = shift @ARGV or die __("missing --");
1906 } else {
1907 die sprintf(__("unknown --patch mode: %s"), $1);
1908 }
1909 } else {
1910 $patch_mode = 'stage';
1911 $arg = shift @ARGV or die __("missing --");
1912 }
1913 die sprintf(__("invalid argument %s, expecting --"),
1914 $arg) unless $arg eq "--";
1915 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1916 $patch_mode_only = 1;
1917 }
1918 elsif ($arg ne "--") {
1919 die sprintf(__("invalid argument %s, expecting --"), $arg);
1920 }
1921}
1922
1923sub main_loop {
1924 my @cmd = ([ 'status', \&status_cmd, ],
1925 [ 'update', \&update_cmd, ],
1926 [ 'revert', \&revert_cmd, ],
1927 [ 'add untracked', \&add_untracked_cmd, ],
1928 [ 'patch', \&patch_update_cmd, ],
1929 [ 'diff', \&diff_cmd, ],
1930 [ 'quit', \&quit_cmd, ],
1931 [ 'help', \&help_cmd, ],
1932 );
1933 while (1) {
1934 my ($it) = list_and_choose({ PROMPT => __('What now'),
1935 SINGLETON => 1,
1936 LIST_FLAT => 4,
1937 HEADER => __('*** Commands ***'),
1938 ON_EOF => \&quit_cmd,
1939 IMMEDIATE => 1 }, @cmd);
1940 if ($it) {
1941 eval {
1942 $it->[1]->();
1943 };
1944 if ($@) {
1945 print "$@";
1946 }
1947 }
1948 }
1949}
1950
1951process_args();
1952refresh();
1953if ($patch_mode_only) {
1954 patch_update_cmd();
1955}
1956else {
1957 status_cmd();
1958 main_loop();
1959}