git-add--interactive.perl 36 KB
Newer Older
1
#!/usr/bin/perl
Junio C Hamano's avatar
Junio C Hamano committed
2

3
use 5.008;
Junio C Hamano's avatar
Junio C Hamano committed
4
use strict;
5
use warnings;
6 7
use Git;

8 9
binmode(STDOUT, ":raw");

10 11
my $repo = Git->repository();

12 13 14 15 16 17 18
my $menu_use_color = $repo->get_colorbool('color.interactive');
my ($prompt_color, $header_color, $help_color) =
	$menu_use_color ? (
		$repo->get_color('color.interactive.prompt', 'bold blue'),
		$repo->get_color('color.interactive.header', 'bold'),
		$repo->get_color('color.interactive.help', 'red bold'),
	) : ();
19 20
my $error_color = ();
if ($menu_use_color) {
21 22
	my $help_color_spec = ($repo->config('color.interactive.help') or
				'red bold');
23 24 25
	$error_color = $repo->get_color('color.interactive.error',
					$help_color_spec);
}
26

27 28 29 30 31
my $diff_use_color = $repo->get_colorbool('color.diff');
my ($fraginfo_color) =
	$diff_use_color ? (
		$repo->get_color('color.diff.frag', 'cyan'),
	) : ();
32 33 34 35 36 37 38 39 40 41 42 43
my ($diff_plain_color) =
	$diff_use_color ? (
		$repo->get_color('color.diff.plain', ''),
	) : ();
my ($diff_old_color) =
	$diff_use_color ? (
		$repo->get_color('color.diff.old', 'red'),
	) : ();
my ($diff_new_color) =
	$diff_use_color ? (
		$repo->get_color('color.diff.new', 'green'),
	) : ();
44

45
my $normal_color = $repo->get_color("", "reset");
46

47 48
my $diff_algorithm = $repo->config('diff.algorithm');

49
my $use_readkey = 0;
50 51 52
my $use_termcap = 0;
my %term_escapes;

53 54
sub ReadMode;
sub ReadKey;
55 56
if ($repo->config_bool("interactive.singlekey")) {
	eval {
57 58
		require Term::ReadKey;
		Term::ReadKey->import;
59 60
		$use_readkey = 1;
	};
61 62 63
	if (!$use_readkey) {
		print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
	}
64 65 66 67 68 69 70 71
	eval {
		require Term::Cap;
		my $termcap = Term::Cap->Tgetent;
		foreach (values %$termcap) {
			$term_escapes{$_} = 1 if /^\e/;
		}
		$use_termcap = 1;
	};
72 73
}

74 75 76 77
sub colored {
	my $color = shift;
	my $string = join("", @_);

78
	if (defined $color) {
79 80 81 82 83 84 85 86 87 88 89
		# Put a color code at the beginning of each line, a reset at the end
		# color after newlines that are not at the end of the string
		$string =~ s/(\n+)(.)/$1$color$2/g;
		# reset before newlines
		$string =~ s/(\n+)/$normal_color$1/g;
		# codes at beginning and end (if necessary):
		$string =~ s/^/$color/;
		$string =~ s/$/$normal_color/ unless $string =~ /\n$/;
	}
	return $string;
}
Junio C Hamano's avatar
Junio C Hamano committed
90

91 92
# command line options
my $patch_mode;
Thomas Rast's avatar
Thomas Rast committed
93
my $patch_mode_revision;
94

95
sub apply_patch;
96
sub apply_patch_for_checkout_commit;
97
sub apply_patch_for_stash;
98 99 100 101 102 103 104 105 106 107

my %patch_modes = (
	'stage' => {
		DIFF => 'diff-files -p',
		APPLY => sub { apply_patch 'apply --cached', @_; },
		APPLY_CHECK => 'apply --cached',
		VERB => 'Stage',
		TARGET => '',
		PARTICIPLE => 'staging',
		FILTER => 'file-only',
108
		IS_REVERSE => 0,
109
	},
110 111 112 113 114 115 116 117
	'stash' => {
		DIFF => 'diff-index -p HEAD',
		APPLY => sub { apply_patch 'apply --cached', @_; },
		APPLY_CHECK => 'apply --cached',
		VERB => 'Stash',
		TARGET => '',
		PARTICIPLE => 'stashing',
		FILTER => undef,
118
		IS_REVERSE => 0,
119
	},
Thomas Rast's avatar
Thomas Rast committed
120 121 122 123 124 125 126 127
	'reset_head' => {
		DIFF => 'diff-index -p --cached',
		APPLY => sub { apply_patch 'apply -R --cached', @_; },
		APPLY_CHECK => 'apply -R --cached',
		VERB => 'Unstage',
		TARGET => '',
		PARTICIPLE => 'unstaging',
		FILTER => 'index-only',
128
		IS_REVERSE => 1,
Thomas Rast's avatar
Thomas Rast committed
129 130 131 132 133 134 135 136 137
	},
	'reset_nothead' => {
		DIFF => 'diff-index -R -p --cached',
		APPLY => sub { apply_patch 'apply --cached', @_; },
		APPLY_CHECK => 'apply --cached',
		VERB => 'Apply',
		TARGET => ' to index',
		PARTICIPLE => 'applying',
		FILTER => 'index-only',
138
		IS_REVERSE => 0,
Thomas Rast's avatar
Thomas Rast committed
139
	},
140 141 142 143 144 145 146 147
	'checkout_index' => {
		DIFF => 'diff-files -p',
		APPLY => sub { apply_patch 'apply -R', @_; },
		APPLY_CHECK => 'apply -R',
		VERB => 'Discard',
		TARGET => ' from worktree',
		PARTICIPLE => 'discarding',
		FILTER => 'file-only',
148
		IS_REVERSE => 1,
149 150 151 152 153 154 155 156 157
	},
	'checkout_head' => {
		DIFF => 'diff-index -p',
		APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
		APPLY_CHECK => 'apply -R',
		VERB => 'Discard',
		TARGET => ' from index and worktree',
		PARTICIPLE => 'discarding',
		FILTER => undef,
158
		IS_REVERSE => 1,
159 160 161 162 163 164 165 166 167
	},
	'checkout_nothead' => {
		DIFF => 'diff-index -R -p',
		APPLY => sub { apply_patch_for_checkout_commit '', @_ },
		APPLY_CHECK => 'apply',
		VERB => 'Apply',
		TARGET => ' to index and worktree',
		PARTICIPLE => 'applying',
		FILTER => undef,
168
		IS_REVERSE => 0,
169
	},
170 171 172
);

my %patch_mode_flavour = %{$patch_modes{stage}};
173

Junio C Hamano's avatar
Junio C Hamano committed
174
sub run_cmd_pipe {
175
	if ($^O eq 'MSWin32') {
176 177 178 179 180 181 182 183 184
		my @invalid = grep {m/[":*]/} @_;
		die "$^O does not support: @invalid\n" if @invalid;
		my @args = map { m/ /o ? "\"$_\"": $_ } @_;
		return qx{@args};
	} else {
		my $fh = undef;
		open($fh, '-|', @_) or die;
		return <$fh>;
	}
Junio C Hamano's avatar
Junio C Hamano committed
185 186 187 188 189 190 191 192 193
}

my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));

if (!defined $GIT_DIR) {
	exit(1); # rev-parse would have already said "not a git repo"
}
chomp($GIT_DIR);

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
my %cquote_map = (
 "b" => chr(8),
 "t" => chr(9),
 "n" => chr(10),
 "v" => chr(11),
 "f" => chr(12),
 "r" => chr(13),
 "\\" => "\\",
 "\042" => "\042",
);

sub unquote_path {
	local ($_) = @_;
	my ($retval, $remainder);
	if (!/^\042(.*)\042$/) {
		return $_;
	}
	($_, $retval) = ($1, "");
	while (/^([^\\]*)\\(.*)$/) {
		$remainder = $2;
		$retval .= $1;
		for ($remainder) {
			if (/^([0-3][0-7][0-7])(.*)$/) {
				$retval .= chr(oct($1));
				$_ = $2;
				last;
			}
			if (/^([\\\042btnvfr])(.*)$/) {
				$retval .= $cquote_map{$1};
				$_ = $2;
				last;
			}
			# This is malformed -- just return it as-is for now.
			return $_[0];
		}
		$_ = $remainder;
	}
	$retval .= $_;
	return $retval;
}

Junio C Hamano's avatar
Junio C Hamano committed
235 236
sub refresh {
	my $fh;
237
	open $fh, 'git update-index --refresh |'
Junio C Hamano's avatar
Junio C Hamano committed
238 239 240 241 242 243 244 245 246 247
	    or die;
	while (<$fh>) {
		;# ignore 'needs update'
	}
	close $fh;
}

sub list_untracked {
	map {
		chomp $_;
248
		unquote_path($_);
Junio C Hamano's avatar
Junio C Hamano committed
249
	}
250
	run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
Junio C Hamano's avatar
Junio C Hamano committed
251 252 253 254 255
}

my $status_fmt = '%12s %12s %s';
my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path');

256 257 258 259 260 261 262 263 264 265 266 267 268
{
	my $initial;
	sub is_initial_commit {
		$initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
			unless defined $initial;
		return $initial;
	}
}

sub get_empty_tree {
	return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
}

269 270 271 272 273 274 275 276 277 278 279
sub get_diff_reference {
	my $ref = shift;
	if (defined $ref and $ref ne 'HEAD') {
		return $ref;
	} elsif (is_initial_commit()) {
		return get_empty_tree();
	} else {
		return 'HEAD';
	}
}

Junio C Hamano's avatar
Junio C Hamano committed
280 281 282 283 284 285 286
# Returns list of hashes, contents of each of which are:
# VALUE:	pathname
# BINARY:	is a binary path
# INDEX:	is index different from HEAD?
# FILE:		is file different from index?
# INDEX_ADDDEL:	is it add/delete between HEAD and index?
# FILE_ADDDEL:	is it add/delete between index and file?
287
# UNMERGED:	is the path unmerged
Junio C Hamano's avatar
Junio C Hamano committed
288 289 290 291 292

sub list_modified {
	my ($only) = @_;
	my (%data, @return);
	my ($add, $del, $adddel, $file);
293 294 295 296
	my @tracked = ();

	if (@ARGV) {
		@tracked = map {
297 298
			chomp $_;
			unquote_path($_);
299
		} run_cmd_pipe(qw(git ls-files --), @ARGV);
300 301
		return if (!@tracked);
	}
Junio C Hamano's avatar
Junio C Hamano committed
302

303
	my $reference = get_diff_reference($patch_mode_revision);
Junio C Hamano's avatar
Junio C Hamano committed
304
	for (run_cmd_pipe(qw(git diff-index --cached
305 306
			     --numstat --summary), $reference,
			     '--', @tracked)) {
Junio C Hamano's avatar
Junio C Hamano committed
307 308 309
		if (($add, $del, $file) =
		    /^([-\d]+)	([-\d]+)	(.*)/) {
			my ($change, $bin);
310
			$file = unquote_path($file);
Junio C Hamano's avatar
Junio C Hamano committed
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
			if ($add eq '-' && $del eq '-') {
				$change = 'binary';
				$bin = 1;
			}
			else {
				$change = "+$add/-$del";
			}
			$data{$file} = {
				INDEX => $change,
				BINARY => $bin,
				FILE => 'nothing',
			}
		}
		elsif (($adddel, $file) =
		       /^ (create|delete) mode [0-7]+ (.*)$/) {
326
			$file = unquote_path($file);
Junio C Hamano's avatar
Junio C Hamano committed
327 328 329 330
			$data{$file}{INDEX_ADDDEL} = $adddel;
		}
	}

331
	for (run_cmd_pipe(qw(git diff-files --numstat --summary --raw --), @tracked)) {
Junio C Hamano's avatar
Junio C Hamano committed
332 333
		if (($add, $del, $file) =
		    /^([-\d]+)	([-\d]+)	(.*)/) {
334
			$file = unquote_path($file);
Junio C Hamano's avatar
Junio C Hamano committed
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
			my ($change, $bin);
			if ($add eq '-' && $del eq '-') {
				$change = 'binary';
				$bin = 1;
			}
			else {
				$change = "+$add/-$del";
			}
			$data{$file}{FILE} = $change;
			if ($bin) {
				$data{$file}{BINARY} = 1;
			}
		}
		elsif (($adddel, $file) =
		       /^ (create|delete) mode [0-7]+ (.*)$/) {
350
			$file = unquote_path($file);
Junio C Hamano's avatar
Junio C Hamano committed
351 352
			$data{$file}{FILE_ADDDEL} = $adddel;
		}
353 354 355 356 357 358 359 360 361 362 363 364
		elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.)	(.*)$/) {
			$file = unquote_path($2);
			if (!exists $data{$file}) {
				$data{$file} = +{
					INDEX => 'unchanged',
					BINARY => 0,
				};
			}
			if ($1 eq 'U') {
				$data{$file}{UNMERGED} = 1;
			}
		}
Junio C Hamano's avatar
Junio C Hamano committed
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
	}

	for (sort keys %data) {
		my $it = $data{$_};

		if ($only) {
			if ($only eq 'index-only') {
				next if ($it->{INDEX} eq 'unchanged');
			}
			if ($only eq 'file-only') {
				next if ($it->{FILE} eq 'nothing');
			}
		}
		push @return, +{
			VALUE => $_,
			%$it,
		};
	}
	return @return;
}

sub find_unique {
	my ($string, @stuff) = @_;
	my $found = undef;
	for (my $i = 0; $i < @stuff; $i++) {
		my $it = $stuff[$i];
		my $hit = undef;
		if (ref $it) {
			if ((ref $it) eq 'ARRAY') {
				$it = $it->[0];
			}
			else {
				$it = $it->{VALUE};
			}
		}
		eval {
			if ($it =~ /^$string/) {
				$hit = 1;
			};
		};
		if (defined $hit && defined $found) {
			return undef;
		}
		if ($hit) {
			$found = $i + 1;
		}
	}
	return $found;
}

415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
# inserts string into trie and updates count for each character
sub update_trie {
	my ($trie, $string) = @_;
	foreach (split //, $string) {
		$trie = $trie->{$_} ||= {COUNT => 0};
		$trie->{COUNT}++;
	}
}

# returns an array of tuples (prefix, remainder)
sub find_unique_prefixes {
	my @stuff = @_;
	my @return = ();

	# any single prefix exceeding the soft limit is omitted
	# if any prefix exceeds the hard limit all are omitted
	# 0 indicates no limit
	my $soft_limit = 0;
	my $hard_limit = 3;

	# build a trie modelling all possible options
	my %trie;
	foreach my $print (@stuff) {
		if ((ref $print) eq 'ARRAY') {
			$print = $print->[0];
		}
441
		elsif ((ref $print) eq 'HASH') {
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
			$print = $print->{VALUE};
		}
		update_trie(\%trie, $print);
		push @return, $print;
	}

	# use the trie to find the unique prefixes
	for (my $i = 0; $i < @return; $i++) {
		my $ret = $return[$i];
		my @letters = split //, $ret;
		my %search = %trie;
		my ($prefix, $remainder);
		my $j;
		for ($j = 0; $j < @letters; $j++) {
			my $letter = $letters[$j];
			if ($search{$letter}{COUNT} == 1) {
				$prefix = substr $ret, 0, $j + 1;
				$remainder = substr $ret, $j + 1;
				last;
			}
			else {
				my $prefix = substr $ret, 0, $j;
				return ()
				    if ($hard_limit && $j + 1 > $hard_limit);
			}
			%search = %{$search{$letter}};
		}
469 470
		if (ord($letters[0]) > 127 ||
		    ($soft_limit && $j + 1 > $soft_limit)) {
471 472 473 474 475 476 477 478
			$prefix = undef;
			$remainder = $ret;
		}
		$return[$i] = [$prefix, $remainder];
	}
	return @return;
}

479 480 481 482 483 484 485
# filters out prefixes which have special meaning to list_and_choose()
sub is_valid_prefix {
	my $prefix = shift;
	return (defined $prefix) &&
	    !($prefix =~ /[\s,]/) && # separators
	    !($prefix =~ /^-/) &&    # deselection
	    !($prefix =~ /^\d+/) &&  # selection
486 487
	    ($prefix ne '*') &&      # "all" wildcard
	    ($prefix ne '?');        # prompt help
488 489
}

490 491 492 493 494
# given a prefix/remainder tuple return a string with the prefix highlighted
# for now use square brackets; later might use ANSI colors (underline, bold)
sub highlight_prefix {
	my $prefix = shift;
	my $remainder = shift;
495 496 497 498 499 500 501 502 503

	if (!defined $prefix) {
		return $remainder;
	}

	if (!is_valid_prefix($prefix)) {
		return "$prefix$remainder";
	}

504
	if (!$menu_use_color) {
505 506 507 508
		return "[$prefix]$remainder";
	}

	return "$prompt_color$prefix$normal_color$remainder";
509 510
}

511 512 513 514
sub error_msg {
	print STDERR colored $error_color, @_;
}

Junio C Hamano's avatar
Junio C Hamano committed
515 516 517 518
sub list_and_choose {
	my ($opts, @stuff) = @_;
	my (@chosen, @return);
	my $i;
519
	my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
Junio C Hamano's avatar
Junio C Hamano committed
520 521 522 523 524 525 526 527 528

      TOPLOOP:
	while (1) {
		my $last_lf = 0;

		if ($opts->{HEADER}) {
			if (!$opts->{LIST_FLAT}) {
				print "     ";
			}
529
			print colored $header_color, "$opts->{HEADER}\n";
Junio C Hamano's avatar
Junio C Hamano committed
530 531 532 533
		}
		for ($i = 0; $i < @stuff; $i++) {
			my $chosen = $chosen[$i] ? '*' : ' ';
			my $print = $stuff[$i];
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
			my $ref = ref $print;
			my $highlighted = highlight_prefix(@{$prefixes[$i]})
			    if @prefixes;
			if ($ref eq 'ARRAY') {
				$print = $highlighted || $print->[0];
			}
			elsif ($ref eq 'HASH') {
				my $value = $highlighted || $print->{VALUE};
				$print = sprintf($status_fmt,
				    $print->{INDEX},
				    $print->{FILE},
				    $value);
			}
			else {
				$print = $highlighted || $print;
Junio C Hamano's avatar
Junio C Hamano committed
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566
			}
			printf("%s%2d: %s", $chosen, $i+1, $print);
			if (($opts->{LIST_FLAT}) &&
			    (($i + 1) % ($opts->{LIST_FLAT}))) {
				print "\t";
				$last_lf = 0;
			}
			else {
				print "\n";
				$last_lf = 1;
			}
		}
		if (!$last_lf) {
			print "\n";
		}

		return if ($opts->{LIST_ONLY});

567
		print colored $prompt_color, $opts->{PROMPT};
Junio C Hamano's avatar
Junio C Hamano committed
568 569 570 571 572 573 574
		if ($opts->{SINGLETON}) {
			print "> ";
		}
		else {
			print ">> ";
		}
		my $line = <STDIN>;
575 576 577 578 579
		if (!$line) {
			print "\n";
			$opts->{ON_EOF}->() if $opts->{ON_EOF};
			last;
		}
Junio C Hamano's avatar
Junio C Hamano committed
580
		chomp $line;
581
		last if $line eq '';
582 583 584 585 586 587
		if ($line eq '?') {
			$opts->{SINGLETON} ?
			    singleton_prompt_help_cmd() :
			    prompt_help_cmd();
			next TOPLOOP;
		}
Junio C Hamano's avatar
Junio C Hamano committed
588 589 590 591 592 593 594 595
		for my $choice (split(/[\s,]+/, $line)) {
			my $choose = 1;
			my ($bottom, $top);

			# Input that begins with '-'; unchoose
			if ($choice =~ s/^-//) {
				$choose = 0;
			}
596 597 598
			# A range can be specified like 5-7 or 5-.
			if ($choice =~ /^(\d+)-(\d*)$/) {
				($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
Junio C Hamano's avatar
Junio C Hamano committed
599 600 601 602 603 604 605 606 607 608 609
			}
			elsif ($choice =~ /^\d+$/) {
				$bottom = $top = $choice;
			}
			elsif ($choice eq '*') {
				$bottom = 1;
				$top = 1 + @stuff;
			}
			else {
				$bottom = $top = find_unique($choice, @stuff);
				if (!defined $bottom) {
610
					error_msg "Huh ($choice)?\n";
Junio C Hamano's avatar
Junio C Hamano committed
611 612 613 614
					next TOPLOOP;
				}
			}
			if ($opts->{SINGLETON} && $bottom != $top) {
615
				error_msg "Huh ($choice)?\n";
Junio C Hamano's avatar
Junio C Hamano committed
616 617 618
				next TOPLOOP;
			}
			for ($i = $bottom-1; $i <= $top-1; $i++) {
619
				next if (@stuff <= $i || $i < 0);
Junio C Hamano's avatar
Junio C Hamano committed
620 621 622
				$chosen[$i] = $choose;
			}
		}
623
		last if ($opts->{IMMEDIATE} || $line eq '*');
Junio C Hamano's avatar
Junio C Hamano committed
624 625 626 627 628 629 630 631 632
	}
	for ($i = 0; $i < @stuff; $i++) {
		if ($chosen[$i]) {
			push @return, $stuff[$i];
		}
	}
	return @return;
}

633
sub singleton_prompt_help_cmd {
634
	print colored $help_color, <<\EOF ;
635 636 637 638 639 640 641 642
Prompt help:
1          - select a numbered item
foo        - select item based on unique prefix
           - (empty) select nothing
EOF
}

sub prompt_help_cmd {
643
	print colored $help_color, <<\EOF ;
644 645 646 647 648 649 650 651 652 653 654
Prompt help:
1          - select a single item
3-5        - select a range of items
2-3,6-9    - select multiple ranges
foo        - select item based on unique prefix
-...       - unselect specified items
*          - choose all items
           - (empty) finish selecting
EOF
}

Junio C Hamano's avatar
Junio C Hamano committed
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680
sub status_cmd {
	list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
			list_modified());
	print "\n";
}

sub say_n_paths {
	my $did = shift @_;
	my $cnt = scalar @_;
	print "$did ";
	if (1 < $cnt) {
		print "$cnt paths\n";
	}
	else {
		print "one path\n";
	}
}

sub update_cmd {
	my @mods = list_modified('file-only');
	return if (!@mods);

	my @update = list_and_choose({ PROMPT => 'Update',
				       HEADER => $status_head, },
				     @mods);
	if (@update) {
681
		system(qw(git update-index --add --remove --),
Junio C Hamano's avatar
Junio C Hamano committed
682 683 684 685 686 687 688 689 690 691 692
		       map { $_->{VALUE} } @update);
		say_n_paths('updated', @update);
	}
	print "\n";
}

sub revert_cmd {
	my @update = list_and_choose({ PROMPT => 'Revert',
				       HEADER => $status_head, },
				     list_modified());
	if (@update) {
693 694 695
		if (is_initial_commit()) {
			system(qw(git rm --cached),
				map { $_->{VALUE} } @update);
Junio C Hamano's avatar
Junio C Hamano committed
696
		}
697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713
		else {
			my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
						 map { $_->{VALUE} } @update);
			my $fh;
			open $fh, '| git update-index --index-info'
			    or die;
			for (@lines) {
				print $fh $_;
			}
			close($fh);
			for (@update) {
				if ($_->{INDEX_ADDDEL} &&
				    $_->{INDEX_ADDDEL} eq 'create') {
					system(qw(git update-index --force-remove --),
					       $_->{VALUE});
					print "note: $_->{VALUE} is untracked now.\n";
				}
Junio C Hamano's avatar
Junio C Hamano committed
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731
			}
		}
		refresh();
		say_n_paths('reverted', @update);
	}
	print "\n";
}

sub add_untracked_cmd {
	my @add = list_and_choose({ PROMPT => 'Add untracked' },
				  list_untracked());
	if (@add) {
		system(qw(git update-index --add --), @add);
		say_n_paths('added', @add);
	}
	print "\n";
}

732 733 734
sub run_git_apply {
	my $cmd = shift;
	my $fh;
735
	open $fh, '| git ' . $cmd . " --recount --allow-overlap";
736 737 738 739
	print $fh @_;
	return close $fh;
}

Junio C Hamano's avatar
Junio C Hamano committed
740 741
sub parse_diff {
	my ($path) = @_;
742
	my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
743
	if (defined $diff_algorithm) {
744
		splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
745
	}
Thomas Rast's avatar
Thomas Rast committed
746
	if (defined $patch_mode_revision) {
747
		push @diff_cmd, get_diff_reference($patch_mode_revision);
Thomas Rast's avatar
Thomas Rast committed
748
	}
749
	my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
750 751
	my @colored = ();
	if ($diff_use_color) {
752
		@colored = run_cmd_pipe("git", @diff_cmd, qw(--color --), $path);
Junio C Hamano's avatar
Junio C Hamano committed
753
	}
754
	my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
755

756 757
	for (my $i = 0; $i < @diff; $i++) {
		if ($diff[$i] =~ /^@@ /) {
758 759
			push @hunk, { TEXT => [], DISPLAY => [],
				TYPE => 'hunk' };
760
		}
761 762 763
		push @{$hunk[-1]{TEXT}}, $diff[$i];
		push @{$hunk[-1]{DISPLAY}},
			($diff_use_color ? $colored[$i] : $diff[$i]);
764
	}
765
	return @hunk;
766 767
}

768 769 770
sub parse_diff_header {
	my $src = shift;

771 772
	my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
	my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
773
	my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
774 775

	for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
776 777 778 779
		my $dest =
		   $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
		   $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
		   $head;
780 781 782
		push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
		push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
	}
783
	return ($head, $mode, $deletion);
784 785
}

786 787 788 789 790 791 792 793 794 795
sub hunk_splittable {
	my ($text) = @_;

	my @s = split_hunk($text);
	return (1 < @s);
}

sub parse_hunk_header {
	my ($line) = @_;
	my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
796 797 798
	    $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
	$o_cnt = 1 unless defined $o_cnt;
	$n_cnt = 1 unless defined $n_cnt;
799 800 801 802
	return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
}

sub split_hunk {
803
	my ($text, $display) = @_;
804
	my @split = ();
805 806 807
	if (!defined $display) {
		$display = $text;
	}
808 809 810 811
	# If there are context lines in the middle of a hunk,
	# it can be split, but we would need to take care of
	# overlaps later.

812
	my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
813 814 815 816 817 818 819 820
	my $hunk_start = 1;

      OUTER:
	while (1) {
		my $next_hunk_start = undef;
		my $i = $hunk_start - 1;
		my $this = +{
			TEXT => [],
821
			DISPLAY => [],
822
			TYPE => 'hunk',
823 824 825 826 827 828
			OLD => $o_ofs,
			NEW => $n_ofs,
			OCNT => 0,
			NCNT => 0,
			ADDDEL => 0,
			POSTCTX => 0,
829
			USE => undef,
830 831 832 833
		};

		while (++$i < @$text) {
			my $line = $text->[$i];
834
			my $display = $display->[$i];
835 836 837 838 839 840 841 842 843 844 845
			if ($line =~ /^ /) {
				if ($this->{ADDDEL} &&
				    !defined $next_hunk_start) {
					# We have seen leading context and
					# adds/dels and then here is another
					# context, which is trailing for this
					# split hunk and leading for the next
					# one.
					$next_hunk_start = $i;
				}
				push @{$this->{TEXT}}, $line;
846
				push @{$this->{DISPLAY}}, $display;
847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868
				$this->{OCNT}++;
				$this->{NCNT}++;
				if (defined $next_hunk_start) {
					$this->{POSTCTX}++;
				}
				next;
			}

			# add/del
			if (defined $next_hunk_start) {
				# We are done with the current hunk and
				# this is the first real change for the
				# next split one.
				$hunk_start = $next_hunk_start;
				$o_ofs = $this->{OLD} + $this->{OCNT};
				$n_ofs = $this->{NEW} + $this->{NCNT};
				$o_ofs -= $this->{POSTCTX};
				$n_ofs -= $this->{POSTCTX};
				push @split, $this;
				redo OUTER;
			}
			push @{$this->{TEXT}}, $line;
869
			push @{$this->{DISPLAY}}, $display;
870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
			$this->{ADDDEL}++;
			if ($line =~ /^-/) {
				$this->{OCNT}++;
			}
			else {
				$this->{NCNT}++;
			}
		}

		push @split, $this;
		last;
	}

	for my $hunk (@split) {
		$o_ofs = $hunk->{OLD};
		$n_ofs = $hunk->{NEW};
886 887
		my $o_cnt = $hunk->{OCNT};
		my $n_cnt = $hunk->{NCNT};
888 889 890 891 892 893

		my $head = ("@@ -$o_ofs" .
			    (($o_cnt != 1) ? ",$o_cnt" : '') .
			    " +$n_ofs" .
			    (($n_cnt != 1) ? ",$n_cnt" : '') .
			    " @@\n");
894
		my $display_head = $head;
895
		unshift @{$hunk->{TEXT}}, $head;
896 897 898 899
		if ($diff_use_color) {
			$display_head = colored($fraginfo_color, $head);
		}
		unshift @{$hunk->{DISPLAY}}, $display_head;
900
	}
901
	return @split;
902 903
}

904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977
sub find_last_o_ctx {
	my ($it) = @_;
	my $text = $it->{TEXT};
	my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
	my $i = @{$text};
	my $last_o_ctx = $o_ofs + $o_cnt;
	while (0 < --$i) {
		my $line = $text->[$i];
		if ($line =~ /^ /) {
			$last_o_ctx--;
			next;
		}
		last;
	}
	return $last_o_ctx;
}

sub merge_hunk {
	my ($prev, $this) = @_;
	my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
	    parse_hunk_header($prev->{TEXT}[0]);
	my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
	    parse_hunk_header($this->{TEXT}[0]);

	my (@line, $i, $ofs, $o_cnt, $n_cnt);
	$ofs = $o0_ofs;
	$o_cnt = $n_cnt = 0;
	for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
		my $line = $prev->{TEXT}[$i];
		if ($line =~ /^\+/) {
			$n_cnt++;
			push @line, $line;
			next;
		}

		last if ($o1_ofs <= $ofs);

		$o_cnt++;
		$ofs++;
		if ($line =~ /^ /) {
			$n_cnt++;
		}
		push @line, $line;
	}

	for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
		my $line = $this->{TEXT}[$i];
		if ($line =~ /^\+/) {
			$n_cnt++;
			push @line, $line;
			next;
		}
		$ofs++;
		$o_cnt++;
		if ($line =~ /^ /) {
			$n_cnt++;
		}
		push @line, $line;
	}
	my $head = ("@@ -$o0_ofs" .
		    (($o_cnt != 1) ? ",$o_cnt" : '') .
		    " +$n0_ofs" .
		    (($n_cnt != 1) ? ",$n_cnt" : '') .
		    " @@\n");
	@{$prev->{TEXT}} = ($head, @line);
}

sub coalesce_overlapping_hunks {
	my (@in) = @_;
	my @out = ();

	my ($last_o_ctx, $last_was_dirty);

	for (grep { $_->{USE} } @in) {
978 979 980 981
		if ($_->{TYPE} ne 'hunk') {
			push @out, $_;
			next;
		}
982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997
		my $text = $_->{TEXT};
		my ($o_ofs) = parse_hunk_header($text->[0]);
		if (defined $last_o_ctx &&
		    $o_ofs <= $last_o_ctx &&
		    !$_->{DIRTY} &&
		    !$last_was_dirty) {
			merge_hunk($out[-1], $_);
		}
		else {
			push @out, $_;
		}
		$last_o_ctx = find_last_o_ctx($out[-1]);
		$last_was_dirty = $_->{DIRTY};
	}
	return @out;
}
998

999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
sub reassemble_patch {
	my $head = shift;
	my @patch;

	# Include everything in the header except the beginning of the diff.
	push @patch, (grep { !/^[-+]{3}/ } @$head);

	# Then include any headers from the hunk lines, which must
	# come before any actual hunk.
	while (@_ && $_[0] !~ /^@/) {
		push @patch, shift;
	}

	# Then begin the diff.
	push @patch, grep { /^[-+]{3}/ } @$head;

	# And then the actual hunks.
	push @patch, @_;

	return @patch;
}

1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
sub color_diff {
	return map {
		colored((/^@/  ? $fraginfo_color :
			 /^\+/ ? $diff_new_color :
			 /^-/  ? $diff_old_color :
			 $diff_plain_color),
			$_);
	} @_;
}

sub edit_hunk_manually {
	my ($oldtext) = @_;

	my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
	my $fh;
	open $fh, '>', $hunkfile
		or die "failed to open hunk edit file for writing: " . $!;
	print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n";
	print $fh @$oldtext;
1040
	my $participle = $patch_mode_flavour{PARTICIPLE};
1041 1042
	my $is_reverse = $patch_mode_flavour{IS_REVERSE};
	my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1043 1044
	print $fh <<EOF;
# ---
1045 1046
# To remove '$remove_minus' lines, make them ' ' lines (context).
# To remove '$remove_plus' lines, delete them.
1047 1048 1049
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
1050
# marked for $participle. If it does not apply cleanly, you will be given
1051 1052 1053 1054 1055
# an opportunity to edit again. If all lines of the hunk are removed,
# then the edit is aborted and the hunk is left unchanged.
EOF
	close $fh;

1056
	chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1057 1058
	system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);

1059 1060 1061 1062
	if ($? != 0) {
		return undef;
	}

1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
	open $fh, '<', $hunkfile
		or die "failed to open hunk edit file for reading: " . $!;
	my @newtext = grep { !/^#/ } <$fh>;
	close $fh;
	unlink $hunkfile;

	# Abort if nothing remains
	if (!grep { /\S/ } @newtext) {
		return undef;
	}

	# Reinsert the first hunk header if the user accidentally deleted it
	if ($newtext[0] !~ /^@/) {
		unshift @newtext, $oldtext->[0];
	}
	return \@newtext;
}

sub diff_applies {
1082
	return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1083
			     map { @{$_->{TEXT}} } @_);
1084 1085
}

1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098
sub _restore_terminal_and_die {
	ReadMode 'restore';
	print "\n";
	exit 1;
}

sub prompt_single_character {
	if ($use_readkey) {
		local $SIG{TERM} = \&_restore_terminal_and_die;
		local $SIG{INT} = \&_restore_terminal_and_die;
		ReadMode 'cbreak';
		my $key = ReadKey 0;
		ReadMode 'restore';
1099 1100 1101 1102 1103 1104 1105 1106
		if ($use_termcap and $key eq "\e") {
			while (!defined $term_escapes{$key}) {
				my $next = ReadKey 0.5;
				last if (!defined $next);
				$key .= $next;
			}
			$key =~ s/\e/^[/;
		}
1107 1108 1109 1110 1111 1112 1113 1114
		print "$key" if defined $key;
		print "\n";
		return $key;
	} else {
		return <STDIN>;
	}
}

1115 1116 1117 1118
sub prompt_yesno {
	my ($prompt) = @_;
	while (1) {
		print colored $prompt_color, $prompt;
1119
		my $line = prompt_single_character;
1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133
		return 0 if $line =~ /^n/i;
		return 1 if $line =~ /^y/i;
	}
}

sub edit_hunk_loop {
	my ($head, $hunk, $ix) = @_;
	my $text = $hunk->[$ix]->{TEXT};

	while (1) {
		$text = edit_hunk_manually($text);
		if (!defined $text) {
			return undef;
		}
1134 1135 1136
		my $newhunk = {
			TEXT => $text,
			TYPE => $hunk->[$ix]->{TYPE},
1137 1138
			USE => 1,
			DIRTY => 1,
1139
		};
1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155
		if (diff_applies($head,
				 @{$hunk}[0..$ix-1],
				 $newhunk,
				 @{$hunk}[$ix+1..$#{$hunk}])) {
			$newhunk->{DISPLAY} = [color_diff(@{$text})];
			return $newhunk;
		}
		else {
			prompt_yesno(
				'Your edited hunk does not apply. Edit again '
				. '(saying "no" discards!) [y/n]? '
				) or return undef;
		}
	}
}

Junio C Hamano's avatar
Junio C Hamano committed
1156
sub help_patch_cmd {
1157 1158 1159 1160 1161
	my $verb = lc $patch_mode_flavour{VERB};
	my $target = $patch_mode_flavour{TARGET};
	print colored $help_color, <<EOF ;
y - $verb this hunk$target
n - do not $verb this hunk$target
1162
q - quit; do not $verb this hunk or any of the remaining ones
1163
a - $verb this hunk and all later hunks in the file
1164
d - do not $verb this hunk or any of the later hunks in the file
1165
g - select a hunk to go to
1166
/ - search for a hunk matching the given regex
Junio C Hamano's avatar
Junio C Hamano committed
1167 1168 1169 1170
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
1171
s - split the current hunk into smaller hunks
1172
e - manually edit the current hunk
1173
? - print help
Junio C Hamano's avatar
Junio C Hamano committed
1174 1175 1176
EOF
}

1177 1178
sub apply_patch {
	my $cmd = shift;
1179
	my $ret = run_git_apply $cmd, @_;
1180 1181 1182 1183 1184 1185
	if (!$ret) {
		print STDERR @_;
	}
	return $ret;
}

1186 1187
sub apply_patch_for_checkout_commit {
	my $reverse = shift;
1188 1189
	my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
	my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1190 1191

	if ($applies_worktree && $applies_index) {
1192 1193
		run_git_apply 'apply '.$reverse.' --cached', @_;
		run_git_apply 'apply '.$reverse, @_;
1194 1195 1196 1197
		return 1;
	} elsif (!$applies_index) {
		print colored $error_color, "The selected hunks do not apply to the index!\n";
		if (prompt_yesno "Apply them to the worktree anyway? ") {
1198
			return run_git_apply 'apply '.$reverse, @_;
1199 1200 1201 1202 1203 1204 1205 1206 1207 1208
		} else {
			print colored $error_color, "Nothing was applied.\n";
			return 0;
		}
	} else {
		print STDERR @_;
		return 0;
	}
}

Junio C Hamano's avatar
Junio C Hamano committed
1209
sub patch_update_cmd {
1210
	my @all_mods = list_modified($patch_mode_flavour{FILTER});
1211 1212 1213 1214
	error_msg "ignoring unmerged: $_->{VALUE}\n"
		for grep { $_->{UNMERGED} } @all_mods;
	@all_mods = grep { !$_->{UNMERGED} } @all_mods;

1215
	my @mods = grep { !($_->{BINARY}) } @all_mods;
1216
	my @them;
Junio C Hamano's avatar
Junio C Hamano committed
1217

1218
	if (!@mods) {
1219 1220 1221 1222 1223
		if (@all_mods) {
			print STDERR "Only binary files changed.\n";
		} else {
			print STDERR "No changes.\n";
		}
1224 1225 1226 1227 1228 1229 1230 1231 1232 1233
		return 0;
	}
	if ($patch_mode) {
		@them = @mods;
	}
	else {
		@them = list_and_choose({ PROMPT => 'Patch update',
					  HEADER => $status_head, },
					@mods);
	}
1234
	for (@them) {
1235
		return 0 if patch_update_file($_->{VALUE});
1236
	}
Wincent Colaiuta's avatar
Wincent Colaiuta committed
1237
}
Junio C Hamano's avatar
Junio C Hamano committed
1238

1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261
# Generate a one line summary of a hunk.
sub summarize_hunk {
	my $rhunk = shift;
	my $summary = $rhunk->{TEXT}[0];

	# Keep the line numbers, discard extra context.
	$summary =~ s/@@(.*?)@@.*/$1 /s;
	$summary .= " " x (20 - length $summary);

	# Add some user context.
	for my $line (@{$rhunk->{TEXT}}) {
		if ($line =~ m/^[+-].*\w/) {
			$summary .= $line;
			last;
		}
	}

	chomp $summary;
	return substr($summary, 0, 80) . "\n";
}


# Print a one-line summary of each hunk in the array ref in
1262
# the first argument, starting with the index in the 2nd.
1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279
sub display_hunks {
	my ($hunks, $i) = @_;
	my $ctr = 0;
	$i ||= 0;
	for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
		my $status = " ";
		if (defined $hunks->[$i]{USE}) {
			$status = $hunks->[$i]{USE} ? "+" : "-";
		}
		printf "%s%2d: %s",
			$status,
			$i + 1,
			summarize_hunk($hunks->[$i]);
	}
	return $i;
}

Wincent Colaiuta's avatar
Wincent Colaiuta committed
1280
sub patch_update_file {
1281
	my $quit = 0;
Junio C Hamano's avatar
Junio C Hamano committed
1282
	my ($ix, $num);
Wincent Colaiuta's avatar
Wincent Colaiuta committed
1283
	my $path = shift;
Junio C Hamano's avatar
Junio C Hamano committed
1284
	my ($head, @hunk) = parse_diff($path);
1285
	($head, my $mode, my $deletion) = parse_diff_header($head);
1286 1287 1288
	for (@{$head->{DISPLAY}}) {
		print;
	}
1289 1290

	if (@{$mode->{TEXT}}) {
1291
		unshift @hunk, $mode;
1292
	}
1293 1294 1295 1296 1297
	if (@{$deletion->{TEXT}}) {
		foreach my $hunk (@hunk) {
			push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
			push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
		}
1298 1299
		@hunk = ($deletion);
	}
1300

Junio C Hamano's avatar
Junio C Hamano committed
1301 1302 1303 1304
	$num = scalar @hunk;
	$ix = 0;

	while (1) {
1305
		my ($prev, $next, $other, $undecided, $i);
Junio C Hamano's avatar
Junio C Hamano committed
1306 1307 1308 1309 1310
		$other = '';

		if ($num <= $ix) {
			$ix = 0;
		}
1311
		for ($i = 0; $i < $ix; $i++) {
Junio C Hamano's avatar
Junio C Hamano committed
1312 1313
			if (!defined $hunk[$i]{USE}) {
				$prev = 1;
1314
				$other .= ',k';
Junio C Hamano's avatar
Junio C Hamano committed
1315 1316 1317 1318
				last;
			}
		}
		if ($ix) {
1319
			$other .= ',K';
Junio C Hamano's avatar
Junio C Hamano committed
1320
		}
1321
		for ($i = $ix + 1; $i < $num; $i++) {
Junio C Hamano's avatar
Junio C Hamano committed
1322 1323
			if (!defined $hunk[$i]{USE}) {
				$next = 1;
1324
				$other .= ',j';
Junio C Hamano's avatar
Junio C Hamano committed
1325 1326 1327 1328
				last;
			}
		}
		if ($ix < $num - 1) {
1329
			$other .= ',J';
Junio C Hamano's avatar
Junio C Hamano committed
1330
		}
1331
		if ($num > 1) {
1332
			$other .= ',g';
1333
		}
1334
		for ($i = 0; $i < $num; $i++) {
Junio C Hamano's avatar
Junio C Hamano committed
1335 1336 1337 1338 1339 1340 1341
			if (!defined $hunk[$i]{USE}) {
				$undecided = 1;
				last;
			}
		}
		last if (!$undecided);

1342 1343
		if ($hunk[$ix]{TYPE} eq 'hunk' &&
		    hunk_splittable($hunk[$ix]{TEXT})) {
1344
			$other .= ',s';
1345
		}
1346 1347 1348
		if ($hunk[$ix]{TYPE} eq 'hunk') {
			$other .= ',e';
		}
1349 1350 1351
		for (@{$hunk[$ix]{DISPLAY}}) {
			print;
		}
1352
		print colored $prompt_color, $patch_mode_flavour{VERB},
1353 1354 1355
		  ($hunk[$ix]{TYPE} eq 'mode' ? ' mode change' :
		   $hunk[$ix]{TYPE} eq 'deletion' ? ' deletion' :
		   ' this hunk'),
1356
		  $patch_mode_flavour{TARGET},
1357
		  " [y,n,q,a,d,/$other,?]? ";
1358
		my $line = prompt_single_character;
Junio C Hamano's avatar
Junio C Hamano committed
1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374
		if ($line) {
			if ($line =~ /^y/i) {
				$hunk[$ix]{USE} = 1;
			}
			elsif ($line =~ /^n/i) {
				$hunk[$ix]{USE} = 0;
			}
			elsif ($line =~ /^a/i) {
				while ($ix < $num) {
					if (!defined $hunk[$ix]{USE}) {
						$hunk[$ix]{USE} = 1;
					}
					$ix++;
				}
				next;
			}
1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385
			elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
				my $response = $1;
				my $no = $ix > 10 ? $ix - 10 : 0;
				while ($response eq '') {
					my $extra = "";
					$no = display_hunks(\@hunk, $no);
					if ($no < $num) {
						$extra = " (<ret> to see more)";
					}
					print "go to which hunk$extra? ";
					$response = <STDIN>;
1386 1387 1388
					if (!defined $response) {
						$response = '';
					}
1389 1390 1391
					chomp $response;
				}
				if ($response !~ /^\s*\d+\s*$/) {
1392
					error_msg "Invalid number: '$response'\n";
1393 1394 1395
				} elsif (0 < $response && $response <= $num) {
					$ix = $response - 1;
				} else {
1396
					error_msg "Sorry, only $num hunks available.\n";
1397 1398 1399
				}
				next;
			}
Junio C Hamano's avatar
Junio C Hamano committed
1400 1401 1402 1403 1404 1405 1406 1407 1408
			elsif ($line =~ /^d/i) {
				while ($ix < $num) {
					if (!defined $hunk[$ix]{USE}) {
						$hunk[$ix]{USE} = 0;
					}
					$ix++;
				}
				next;
			}
1409
			elsif ($line =~ /^q/i) {
1410 1411 1412
				for ($i = 0; $i < $num; $i++) {
					if (!defined $hunk[$i]{USE}) {
						$hunk[$i]{USE} = 0;
1413 1414 1415
					}
				}
				$quit = 1;
1416
				last;
1417
			}
1418
			elsif ($line =~ m|^/(.*)|) {
1419 1420 1421 1422 1423 1424 1425 1426
				my $regex = $1;
				if ($1 eq "") {
					print colored $prompt_color, "search for regex? ";
					$regex = <STDIN>;
					if (defined $regex) {
						chomp $regex;
					}
				}
1427 1428
				my $search_string;
				eval {
1429
					$search_string = qr{$regex}m;
1430 1431 1432 1433
				};
				if ($@) {
					my ($err,$exp) = ($@, $1);
					$err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1434
					error_msg "Malformed search regexp $exp: $err\n";
1435 1436 1437 1438 1439 1440 1441 1442 1443
					next;
				}
				my $iy = $ix;
				while (1) {
					my $text = join ("", @{$hunk[$iy]{TEXT}});
					last if ($text =~ $search_string);
					$iy++;
					$iy = 0 if ($iy >= $num);
					if ($ix == $iy) {
1444
						error_msg "No hunk matches the given pattern\n";
1445 1446 1447 1448 1449 1450
						last;
					}
				}
				$ix = $iy;
				next;
			}
1451 1452 1453 1454 1455
			elsif ($line =~ /^K/) {
				if ($other =~ /K/) {
					$ix--;
				}
				else {
1456
					error_msg "No previous hunk\n";
1457
				}
Junio C Hamano's avatar
Junio C Hamano committed
1458 1459
				next;
			}
1460 1461 1462 1463 1464
			elsif ($line =~ /^J/) {
				if ($other =~ /J/) {
					$ix++;
				}
				else {
1465
					error_msg "No next hunk\n";
1466
				}
Junio C Hamano's avatar
Junio C Hamano committed
1467 1468
				next;
			}
1469 1470 1471 1472 1473 1474 1475 1476 1477
			elsif ($line =~ /^k/) {
				if ($other =~ /k/) {
					while (1) {
						$ix--;
						last if (!$ix ||
							 !defined $hunk[$ix]{USE});
					}
				}
				else {
1478
					error_msg "No previous hunk\n";
Junio C Hamano's avatar
Junio C Hamano committed
1479 1480 1481
				}
				next;
			}
1482 1483
			elsif ($line =~ /^j/) {
				if ($other !~ /j/) {
1484
					error_msg "No next hunk\n";
1485
					next;
Junio C Hamano's avatar
Junio C Hamano committed
1486 1487
				}
			}
1488
			elsif ($other =~ /s/ && $line =~ /^s/) {
1489
				my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});