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