8bc0626f0e29cc8a4ea3408352b5c867c34b0834
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 parse_hunk_selection {
1086 local $_;
1087 my ($hunk, $line) = @_;
1088 my ($max_label, $invert) = ($hunk->{MAX_LABEL}, undef);
1089 my @selected = (0) x ($max_label + 1);
1090 my @fields = split(/[,\s]+/, $line);
1091 if ($fields[0] =~ /^-(.*)/) {
1092 $invert = 1;
1093 if ($1 ne '') {
1094 $fields[0] = $1;
1095 } else {
1096 shift @fields;
1097 unless (@fields) {
1098 error_msg __("no lines to invert\n");
1099 return undef;
1100 }
1101 }
1102 }
1103 for (@fields) {
1104 if (my ($lo, $hi) = /^([0-9]+)-([0-9]*)$/) {
1105 if ($hi eq '') {
1106 $hi = $max_label;
1107 }
1108 check_hunk_label($hunk, $lo) or return undef;
1109 check_hunk_label($hunk, $hi) or return undef;
1110 if ($hi < $lo) {
1111 ($lo, $hi) = ($hi, $lo);
1112 }
1113 @selected[$lo..$hi] = (1) x (1 + $hi - $lo);
1114 } elsif (/^([0-9]+)$/) {
1115 check_hunk_label($hunk, $1) or return undef;
1116 $selected[$1] = 1;
1117 } else {
1118 error_msg sprintf(__("invalid hunk line '%s'\n"), $_);
1119 return undef;
1120 }
1121 }
1122 if ($invert) {
1123 @selected = map { !$_ } @selected;
1124 }
1125 return \@selected;
1126}
1127
1128sub display_hunk_lines {
1129 my ($display, $labels, $max_label) =
1130 @{$_[0]}{qw(DISPLAY LABELS MAX_LABEL)};
1131 my $width = int(log($max_label) / log(10)) + 1;
1132 my $padding = ' ' x ($width + 1);
1133 for my $i (0..$#{$display}) {
1134 if ($labels->[$i]) {
1135 printf '%*d %s', $width, $labels->[$i], $display->[$i];
1136 } else {
1137 print $padding . $display->[$i];
1138 }
1139 }
1140}
1141
1142sub select_lines_loop {
1143 my $hunk = shift;
1144 display_hunk_lines($hunk);
1145 my $selection = undef;
1146 until (defined $selection) {
1147 print colored $prompt_color, __("select lines? ");
1148 my $text = <STDIN>;
1149 defined $text and $text =~ /\S/ or return undef;
1150 $selection = parse_hunk_selection($hunk, $text);
1151 }
1152 return select_hunk_lines($hunk, $selection);
1153}
1154
1155my %edit_hunk_manually_modes = (
1156 stage => N__(
1157"If the patch applies cleanly, the edited hunk will immediately be
1158marked for staging."),
1159 stash => N__(
1160"If the patch applies cleanly, the edited hunk will immediately be
1161marked for stashing."),
1162 reset_head => N__(
1163"If the patch applies cleanly, the edited hunk will immediately be
1164marked for unstaging."),
1165 reset_nothead => N__(
1166"If the patch applies cleanly, the edited hunk will immediately be
1167marked for applying."),
1168 checkout_index => N__(
1169"If the patch applies cleanly, the edited hunk will immediately be
1170marked for discarding."),
1171 checkout_head => N__(
1172"If the patch applies cleanly, the edited hunk will immediately be
1173marked for discarding."),
1174 checkout_nothead => N__(
1175"If the patch applies cleanly, the edited hunk will immediately be
1176marked for applying."),
1177);
1178
1179sub recount_edited_hunk {
1180 local $_;
1181 my ($oldtext, $newtext) = @_;
1182 my ($o_cnt, $n_cnt) = (0, 0);
1183 for (@{$newtext}[1..$#{$newtext}]) {
1184 my $mode = substr($_, 0, 1);
1185 if ($mode eq '-') {
1186 $o_cnt++;
1187 } elsif ($mode eq '+') {
1188 $n_cnt++;
1189 } elsif ($mode eq ' ') {
1190 $o_cnt++;
1191 $n_cnt++;
1192 }
1193 }
1194 my ($o_ofs, undef, $n_ofs, undef) =
1195 parse_hunk_header($newtext->[0]);
1196 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1197 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1198 parse_hunk_header($oldtext->[0]);
1199 # Return the change in the number of lines inserted by this hunk
1200 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1201}
1202
1203sub edit_hunk_manually {
1204 my ($oldtext) = @_;
1205
1206 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1207 my $fh;
1208 open $fh, '>', $hunkfile
1209 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1210 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1211 print $fh @$oldtext;
1212 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1213 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1214 my $comment_line_char = Git::get_comment_line_char;
1215 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1216---
1217To remove '%s' lines, make them ' ' lines (context).
1218To remove '%s' lines, delete them.
1219Lines starting with %s will be removed.
1220EOF
1221__($edit_hunk_manually_modes{$patch_mode}),
1222# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1223__ <<EOF2 ;
1224If it does not apply cleanly, you will be given an opportunity to
1225edit again. If all lines of the hunk are removed, then the edit is
1226aborted and the hunk is left unchanged.
1227EOF2
1228 close $fh;
1229
1230 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1231 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1232
1233 if ($? != 0) {
1234 return undef;
1235 }
1236
1237 open $fh, '<', $hunkfile
1238 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1239 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1240 close $fh;
1241 unlink $hunkfile;
1242
1243 # Abort if nothing remains
1244 if (!grep { /\S/ } @newtext) {
1245 return undef;
1246 }
1247
1248 # Reinsert the first hunk header if the user accidentally deleted it
1249 if ($newtext[0] !~ /^@/) {
1250 unshift @newtext, $oldtext->[0];
1251 }
1252 return \@newtext;
1253}
1254
1255sub diff_applies {
1256 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1257 map { @{$_->{TEXT}} } @_);
1258}
1259
1260sub _restore_terminal_and_die {
1261 ReadMode 'restore';
1262 print "\n";
1263 exit 1;
1264}
1265
1266sub prompt_single_character {
1267 if ($use_readkey) {
1268 local $SIG{TERM} = \&_restore_terminal_and_die;
1269 local $SIG{INT} = \&_restore_terminal_and_die;
1270 ReadMode 'cbreak';
1271 my $key = ReadKey 0;
1272 ReadMode 'restore';
1273 if ($use_termcap and $key eq "\e") {
1274 while (!defined $term_escapes{$key}) {
1275 my $next = ReadKey 0.5;
1276 last if (!defined $next);
1277 $key .= $next;
1278 }
1279 $key =~ s/\e/^[/;
1280 }
1281 print "$key" if defined $key;
1282 print "\n";
1283 return $key;
1284 } else {
1285 return <STDIN>;
1286 }
1287}
1288
1289sub prompt_yesno {
1290 my ($prompt) = @_;
1291 while (1) {
1292 print colored $prompt_color, $prompt;
1293 my $line = prompt_single_character;
1294 return undef unless defined $line;
1295 return 0 if $line =~ /^n/i;
1296 return 1 if $line =~ /^y/i;
1297 }
1298}
1299
1300sub edit_hunk_loop {
1301 my ($head, $hunks, $ix) = @_;
1302 my $hunk = $hunks->[$ix];
1303 my $text = $hunk->{TEXT};
1304
1305 while (1) {
1306 my $newtext = edit_hunk_manually($text);
1307 if (!defined $newtext) {
1308 return undef;
1309 }
1310 my $newhunk = {
1311 TEXT => $newtext,
1312 TYPE => $hunk->{TYPE},
1313 USE => 1,
1314 DIRTY => 1,
1315 };
1316 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1317 # If this hunk has already been edited then add the
1318 # offset delta of the previous edit to get the real
1319 # delta from the original unedited hunk.
1320 $hunk->{OFS_DELTA} and
1321 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1322 if (diff_applies($head,
1323 @{$hunks}[0..$ix-1],
1324 $newhunk,
1325 @{$hunks}[$ix+1..$#{$hunks}])) {
1326 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1327 return $newhunk;
1328 }
1329 else {
1330 prompt_yesno(
1331 # TRANSLATORS: do not translate [y/n]
1332 # The program will only accept that input
1333 # at this point.
1334 # Consider translating (saying "no" discards!) as
1335 # (saying "n" for "no" discards!) if the translation
1336 # of the word "no" does not start with n.
1337 __('Your edited hunk does not apply. Edit again '
1338 . '(saying "no" discards!) [y/n]? ')
1339 ) or return undef;
1340 }
1341 }
1342}
1343
1344my %help_patch_modes = (
1345 stage => N__(
1346"y - stage this hunk
1347n - do not stage this hunk
1348q - quit; do not stage this hunk or any of the remaining ones
1349a - stage this hunk and all later hunks in the file
1350d - do not stage this hunk or any of the later hunks in the file"),
1351 stash => N__(
1352"y - stash this hunk
1353n - do not stash this hunk
1354q - quit; do not stash this hunk or any of the remaining ones
1355a - stash this hunk and all later hunks in the file
1356d - do not stash this hunk or any of the later hunks in the file"),
1357 reset_head => N__(
1358"y - unstage this hunk
1359n - do not unstage this hunk
1360q - quit; do not unstage this hunk or any of the remaining ones
1361a - unstage this hunk and all later hunks in the file
1362d - do not unstage this hunk or any of the later hunks in the file"),
1363 reset_nothead => N__(
1364"y - apply this hunk to index
1365n - do not apply this hunk to index
1366q - quit; do not apply this hunk or any of the remaining ones
1367a - apply this hunk and all later hunks in the file
1368d - do not apply this hunk or any of the later hunks in the file"),
1369 checkout_index => N__(
1370"y - discard this hunk from worktree
1371n - do not discard this hunk from worktree
1372q - quit; do not discard this hunk or any of the remaining ones
1373a - discard this hunk and all later hunks in the file
1374d - do not discard this hunk or any of the later hunks in the file"),
1375 checkout_head => N__(
1376"y - discard this hunk from index and worktree
1377n - do not discard this hunk from index and worktree
1378q - quit; do not discard this hunk or any of the remaining ones
1379a - discard this hunk and all later hunks in the file
1380d - do not discard this hunk or any of the later hunks in the file"),
1381 checkout_nothead => N__(
1382"y - apply this hunk to index and worktree
1383n - do not apply this hunk to index and worktree
1384q - quit; do not apply this hunk or any of the remaining ones
1385a - apply this hunk and all later hunks in the file
1386d - do not apply this hunk or any of the later hunks in the file"),
1387);
1388
1389sub help_patch_cmd {
1390 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1391g - select a hunk to go to
1392/ - search for a hunk matching the given regex
1393j - leave this hunk undecided, see next undecided hunk
1394J - leave this hunk undecided, see next hunk
1395k - leave this hunk undecided, see previous undecided hunk
1396K - leave this hunk undecided, see previous hunk
1397l - select hunk lines to use
1398s - split the current hunk into smaller hunks
1399e - manually edit the current hunk
1400? - print help
1401EOF
1402}
1403
1404sub apply_patch {
1405 my $cmd = shift;
1406 my $ret = run_git_apply $cmd, @_;
1407 if (!$ret) {
1408 print STDERR @_;
1409 }
1410 return $ret;
1411}
1412
1413sub apply_patch_for_checkout_commit {
1414 my $reverse = shift;
1415 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1416 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1417
1418 if ($applies_worktree && $applies_index) {
1419 run_git_apply 'apply '.$reverse.' --cached', @_;
1420 run_git_apply 'apply '.$reverse, @_;
1421 return 1;
1422 } elsif (!$applies_index) {
1423 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1424 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1425 return run_git_apply 'apply '.$reverse, @_;
1426 } else {
1427 print colored $error_color, __("Nothing was applied.\n");
1428 return 0;
1429 }
1430 } else {
1431 print STDERR @_;
1432 return 0;
1433 }
1434}
1435
1436sub patch_update_cmd {
1437 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1438 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1439 for grep { $_->{UNMERGED} } @all_mods;
1440 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1441
1442 my @mods = grep { !($_->{BINARY}) } @all_mods;
1443 my @them;
1444
1445 if (!@mods) {
1446 if (@all_mods) {
1447 print STDERR __("Only binary files changed.\n");
1448 } else {
1449 print STDERR __("No changes.\n");
1450 }
1451 return 0;
1452 }
1453 if ($patch_mode_only) {
1454 @them = @mods;
1455 }
1456 else {
1457 @them = list_and_choose({ PROMPT => __('Patch update'),
1458 HEADER => $status_head, },
1459 @mods);
1460 }
1461 for (@them) {
1462 return 0 if patch_update_file($_->{VALUE});
1463 }
1464}
1465
1466# Generate a one line summary of a hunk.
1467sub summarize_hunk {
1468 my $rhunk = shift;
1469 my $summary = $rhunk->{TEXT}[0];
1470
1471 # Keep the line numbers, discard extra context.
1472 $summary =~ s/@@(.*?)@@.*/$1 /s;
1473 $summary .= " " x (20 - length $summary);
1474
1475 # Add some user context.
1476 for my $line (@{$rhunk->{TEXT}}) {
1477 if ($line =~ m/^[+-].*\w/) {
1478 $summary .= $line;
1479 last;
1480 }
1481 }
1482
1483 chomp $summary;
1484 return substr($summary, 0, 80) . "\n";
1485}
1486
1487
1488# Print a one-line summary of each hunk in the array ref in
1489# the first argument, starting with the index in the 2nd.
1490sub display_hunks {
1491 my ($hunks, $i) = @_;
1492 my $ctr = 0;
1493 $i ||= 0;
1494 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1495 my $status = " ";
1496 if (defined $hunks->[$i]{USE}) {
1497 $status = $hunks->[$i]{USE} ? "+" : "-";
1498 }
1499 printf "%s%2d: %s",
1500 $status,
1501 $i + 1,
1502 summarize_hunk($hunks->[$i]);
1503 }
1504 return $i;
1505}
1506
1507my %patch_update_prompt_modes = (
1508 stage => {
1509 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1510 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1511 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1512 },
1513 stash => {
1514 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1515 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1516 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1517 },
1518 reset_head => {
1519 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1520 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1521 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1522 },
1523 reset_nothead => {
1524 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1525 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1526 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1527 },
1528 checkout_index => {
1529 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1530 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1531 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1532 },
1533 checkout_head => {
1534 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1535 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1536 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1537 },
1538 checkout_nothead => {
1539 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1540 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1541 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1542 },
1543);
1544
1545sub patch_update_file {
1546 my $quit = 0;
1547 my ($ix, $num);
1548 my $path = shift;
1549 my ($head, @hunk) = parse_diff($path);
1550 ($head, my $mode, my $deletion) = parse_diff_header($head);
1551 for (@{$head->{DISPLAY}}) {
1552 print;
1553 }
1554
1555 if (@{$mode->{TEXT}}) {
1556 unshift @hunk, $mode;
1557 }
1558 if (@{$deletion->{TEXT}}) {
1559 foreach my $hunk (@hunk) {
1560 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1561 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1562 }
1563 @hunk = ($deletion);
1564 }
1565
1566 $num = scalar @hunk;
1567 $ix = 0;
1568
1569 while (1) {
1570 my ($prev, $next, $other, $undecided, $i);
1571 $other = '';
1572
1573 if ($num <= $ix) {
1574 $ix = 0;
1575 }
1576 for ($i = 0; $i < $ix; $i++) {
1577 if (!defined $hunk[$i]{USE}) {
1578 $prev = 1;
1579 $other .= ',k';
1580 last;
1581 }
1582 }
1583 if ($ix) {
1584 $other .= ',K';
1585 }
1586 for ($i = $ix + 1; $i < $num; $i++) {
1587 if (!defined $hunk[$i]{USE}) {
1588 $next = 1;
1589 $other .= ',j';
1590 last;
1591 }
1592 }
1593 if ($ix < $num - 1) {
1594 $other .= ',J';
1595 }
1596 if ($num > 1) {
1597 $other .= ',g';
1598 }
1599 for ($i = 0; $i < $num; $i++) {
1600 if (!defined $hunk[$i]{USE}) {
1601 $undecided = 1;
1602 last;
1603 }
1604 }
1605 last if (!$undecided);
1606
1607 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1608 hunk_splittable($hunk[$ix]{TEXT})) {
1609 $other .= ',s';
1610 }
1611 if ($hunk[$ix]{TYPE} eq 'hunk') {
1612 $other .= ',e';
1613 }
1614 if (label_hunk_lines($hunk[$ix])) {
1615 $other .= ',l';
1616 }
1617 for (@{$hunk[$ix]{DISPLAY}}) {
1618 print;
1619 }
1620 print colored $prompt_color,
1621 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1622
1623 my $line = prompt_single_character;
1624 last unless defined $line;
1625 if ($line) {
1626 if ($line =~ /^y/i) {
1627 $hunk[$ix]{USE} = 1;
1628 }
1629 elsif ($line =~ /^n/i) {
1630 $hunk[$ix]{USE} = 0;
1631 }
1632 elsif ($line =~ /^a/i) {
1633 while ($ix < $num) {
1634 if (!defined $hunk[$ix]{USE}) {
1635 $hunk[$ix]{USE} = 1;
1636 }
1637 $ix++;
1638 }
1639 next;
1640 }
1641 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1642 my $response = $1;
1643 my $no = $ix > 10 ? $ix - 10 : 0;
1644 while ($response eq '') {
1645 $no = display_hunks(\@hunk, $no);
1646 if ($no < $num) {
1647 print __("go to which hunk (<ret> to see more)? ");
1648 } else {
1649 print __("go to which hunk? ");
1650 }
1651 $response = <STDIN>;
1652 if (!defined $response) {
1653 $response = '';
1654 }
1655 chomp $response;
1656 }
1657 if ($response !~ /^\s*\d+\s*$/) {
1658 error_msg sprintf(__("Invalid number: '%s'\n"),
1659 $response);
1660 } elsif (0 < $response && $response <= $num) {
1661 $ix = $response - 1;
1662 } else {
1663 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1664 "Sorry, only %d hunks available.\n", $num), $num);
1665 }
1666 next;
1667 }
1668 elsif ($line =~ /^d/i) {
1669 while ($ix < $num) {
1670 if (!defined $hunk[$ix]{USE}) {
1671 $hunk[$ix]{USE} = 0;
1672 }
1673 $ix++;
1674 }
1675 next;
1676 }
1677 elsif ($line =~ /^q/i) {
1678 for ($i = 0; $i < $num; $i++) {
1679 if (!defined $hunk[$i]{USE}) {
1680 $hunk[$i]{USE} = 0;
1681 }
1682 }
1683 $quit = 1;
1684 last;
1685 }
1686 elsif ($line =~ m|^/(.*)|) {
1687 my $regex = $1;
1688 if ($1 eq "") {
1689 print colored $prompt_color, __("search for regex? ");
1690 $regex = <STDIN>;
1691 if (defined $regex) {
1692 chomp $regex;
1693 }
1694 }
1695 my $search_string;
1696 eval {
1697 $search_string = qr{$regex}m;
1698 };
1699 if ($@) {
1700 my ($err,$exp) = ($@, $1);
1701 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1702 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1703 next;
1704 }
1705 my $iy = $ix;
1706 while (1) {
1707 my $text = join ("", @{$hunk[$iy]{TEXT}});
1708 last if ($text =~ $search_string);
1709 $iy++;
1710 $iy = 0 if ($iy >= $num);
1711 if ($ix == $iy) {
1712 error_msg __("No hunk matches the given pattern\n");
1713 last;
1714 }
1715 }
1716 $ix = $iy;
1717 next;
1718 }
1719 elsif ($line =~ /^K/) {
1720 if ($other =~ /K/) {
1721 $ix--;
1722 }
1723 else {
1724 error_msg __("No previous hunk\n");
1725 }
1726 next;
1727 }
1728 elsif ($line =~ /^J/) {
1729 if ($other =~ /J/) {
1730 $ix++;
1731 }
1732 else {
1733 error_msg __("No next hunk\n");
1734 }
1735 next;
1736 }
1737 elsif ($line =~ /^k/) {
1738 if ($other =~ /k/) {
1739 while (1) {
1740 $ix--;
1741 last if (!$ix ||
1742 !defined $hunk[$ix]{USE});
1743 }
1744 }
1745 else {
1746 error_msg __("No previous hunk\n");
1747 }
1748 next;
1749 }
1750 elsif ($line =~ /^j/) {
1751 if ($other !~ /j/) {
1752 error_msg __("No next hunk\n");
1753 next;
1754 }
1755 }
1756 elsif ($line =~ /^l/) {
1757 unless ($other =~ /l/) {
1758 error_msg __("Cannot select line by line\n");
1759 next;
1760 }
1761 my $newhunk = select_lines_loop($hunk[$ix]);
1762 if ($newhunk) {
1763 splice @hunk, $ix, 1, $newhunk;
1764 } else {
1765 next;
1766 }
1767 }
1768 elsif ($other =~ /s/ && $line =~ /^s/) {
1769 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1770 if (1 < @split) {
1771 print colored $header_color, sprintf(
1772 __n("Split into %d hunk.\n",
1773 "Split into %d hunks.\n",
1774 scalar(@split)), scalar(@split));
1775 }
1776 splice (@hunk, $ix, 1, @split);
1777 $num = scalar @hunk;
1778 next;
1779 }
1780 elsif ($other =~ /e/ && $line =~ /^e/) {
1781 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1782 if (defined $newhunk) {
1783 splice @hunk, $ix, 1, $newhunk;
1784 }
1785 }
1786 else {
1787 help_patch_cmd($other);
1788 next;
1789 }
1790 # soft increment
1791 while (1) {
1792 $ix++;
1793 last if ($ix >= $num ||
1794 !defined $hunk[$ix]{USE});
1795 }
1796 }
1797 }
1798
1799 @hunk = coalesce_overlapping_hunks(@hunk);
1800
1801 my $n_lofs = 0;
1802 my @result = ();
1803 for (@hunk) {
1804 if ($_->{USE}) {
1805 push @result, @{$_->{TEXT}};
1806 }
1807 }
1808
1809 if (@result) {
1810 my @patch = reassemble_patch($head->{TEXT}, @result);
1811 my $apply_routine = $patch_mode_flavour{APPLY};
1812 &$apply_routine(@patch);
1813 refresh();
1814 }
1815
1816 print "\n";
1817 return $quit;
1818}
1819
1820sub diff_cmd {
1821 my @mods = list_modified('index-only');
1822 @mods = grep { !($_->{BINARY}) } @mods;
1823 return if (!@mods);
1824 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1825 IMMEDIATE => 1,
1826 HEADER => $status_head, },
1827 @mods);
1828 return if (!@them);
1829 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1830 system(qw(git diff -p --cached), $reference, '--',
1831 map { $_->{VALUE} } @them);
1832}
1833
1834sub quit_cmd {
1835 print __("Bye.\n");
1836 exit(0);
1837}
1838
1839sub help_cmd {
1840# TRANSLATORS: please do not translate the command names
1841# 'status', 'update', 'revert', etc.
1842 print colored $help_color, __ <<'EOF' ;
1843status - show paths with changes
1844update - add working tree state to the staged set of changes
1845revert - revert staged set of changes back to the HEAD version
1846patch - pick hunks and update selectively
1847diff - view diff between HEAD and index
1848add untracked - add contents of untracked files to the staged set of changes
1849EOF
1850}
1851
1852sub process_args {
1853 return unless @ARGV;
1854 my $arg = shift @ARGV;
1855 if ($arg =~ /--patch(?:=(.*))?/) {
1856 if (defined $1) {
1857 if ($1 eq 'reset') {
1858 $patch_mode = 'reset_head';
1859 $patch_mode_revision = 'HEAD';
1860 $arg = shift @ARGV or die __("missing --");
1861 if ($arg ne '--') {
1862 $patch_mode_revision = $arg;
1863 $patch_mode = ($arg eq 'HEAD' ?
1864 'reset_head' : 'reset_nothead');
1865 $arg = shift @ARGV or die __("missing --");
1866 }
1867 } elsif ($1 eq 'checkout') {
1868 $arg = shift @ARGV or die __("missing --");
1869 if ($arg eq '--') {
1870 $patch_mode = 'checkout_index';
1871 } else {
1872 $patch_mode_revision = $arg;
1873 $patch_mode = ($arg eq 'HEAD' ?
1874 'checkout_head' : 'checkout_nothead');
1875 $arg = shift @ARGV or die __("missing --");
1876 }
1877 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1878 $patch_mode = $1;
1879 $arg = shift @ARGV or die __("missing --");
1880 } else {
1881 die sprintf(__("unknown --patch mode: %s"), $1);
1882 }
1883 } else {
1884 $patch_mode = 'stage';
1885 $arg = shift @ARGV or die __("missing --");
1886 }
1887 die sprintf(__("invalid argument %s, expecting --"),
1888 $arg) unless $arg eq "--";
1889 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1890 $patch_mode_only = 1;
1891 }
1892 elsif ($arg ne "--") {
1893 die sprintf(__("invalid argument %s, expecting --"), $arg);
1894 }
1895}
1896
1897sub main_loop {
1898 my @cmd = ([ 'status', \&status_cmd, ],
1899 [ 'update', \&update_cmd, ],
1900 [ 'revert', \&revert_cmd, ],
1901 [ 'add untracked', \&add_untracked_cmd, ],
1902 [ 'patch', \&patch_update_cmd, ],
1903 [ 'diff', \&diff_cmd, ],
1904 [ 'quit', \&quit_cmd, ],
1905 [ 'help', \&help_cmd, ],
1906 );
1907 while (1) {
1908 my ($it) = list_and_choose({ PROMPT => __('What now'),
1909 SINGLETON => 1,
1910 LIST_FLAT => 4,
1911 HEADER => __('*** Commands ***'),
1912 ON_EOF => \&quit_cmd,
1913 IMMEDIATE => 1 }, @cmd);
1914 if ($it) {
1915 eval {
1916 $it->[1]->();
1917 };
1918 if ($@) {
1919 print "$@";
1920 }
1921 }
1922 }
1923}
1924
1925process_args();
1926refresh();
1927if ($patch_mode_only) {
1928 patch_update_cmd();
1929}
1930else {
1931 status_cmd();
1932 main_loop();
1933}