git-add--interactive.perlon commit Allow the test suite to pass in a directory whose name contains spaces (567c53d)
   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 { !/^\Q$comment_line_char\E/ } <$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 undef unless defined $line;
1103                return 0 if $line =~ /^n/i;
1104                return 1 if $line =~ /^y/i;
1105        }
1106}
1107
1108sub edit_hunk_loop {
1109        my ($head, $hunk, $ix) = @_;
1110        my $text = $hunk->[$ix]->{TEXT};
1111
1112        while (1) {
1113                $text = edit_hunk_manually($text);
1114                if (!defined $text) {
1115                        return undef;
1116                }
1117                my $newhunk = {
1118                        TEXT => $text,
1119                        TYPE => $hunk->[$ix]->{TYPE},
1120                        USE => 1,
1121                        DIRTY => 1,
1122                };
1123                if (diff_applies($head,
1124                                 @{$hunk}[0..$ix-1],
1125                                 $newhunk,
1126                                 @{$hunk}[$ix+1..$#{$hunk}])) {
1127                        $newhunk->{DISPLAY} = [color_diff(@{$text})];
1128                        return $newhunk;
1129                }
1130                else {
1131                        prompt_yesno(
1132                                # TRANSLATORS: do not translate [y/n]
1133                                # The program will only accept that input
1134                                # at this point.
1135                                # Consider translating (saying "no" discards!) as
1136                                # (saying "n" for "no" discards!) if the translation
1137                                # of the word "no" does not start with n.
1138                                __('Your edited hunk does not apply. Edit again '
1139                                   . '(saying "no" discards!) [y/n]? ')
1140                                ) or return undef;
1141                }
1142        }
1143}
1144
1145my %help_patch_modes = (
1146        stage => N__(
1147"y - stage this hunk
1148n - do not stage this hunk
1149q - quit; do not stage this hunk or any of the remaining ones
1150a - stage this hunk and all later hunks in the file
1151d - do not stage this hunk or any of the later hunks in the file"),
1152        stash => N__(
1153"y - stash this hunk
1154n - do not stash this hunk
1155q - quit; do not stash this hunk or any of the remaining ones
1156a - stash this hunk and all later hunks in the file
1157d - do not stash this hunk or any of the later hunks in the file"),
1158        reset_head => N__(
1159"y - unstage this hunk
1160n - do not unstage this hunk
1161q - quit; do not unstage this hunk or any of the remaining ones
1162a - unstage this hunk and all later hunks in the file
1163d - do not unstage this hunk or any of the later hunks in the file"),
1164        reset_nothead => N__(
1165"y - apply this hunk to index
1166n - do not apply this hunk to index
1167q - quit; do not apply this hunk or any of the remaining ones
1168a - apply this hunk and all later hunks in the file
1169d - do not apply this hunk or any of the later hunks in the file"),
1170        checkout_index => N__(
1171"y - discard this hunk from worktree
1172n - do not discard this hunk from worktree
1173q - quit; do not discard this hunk or any of the remaining ones
1174a - discard this hunk and all later hunks in the file
1175d - do not discard this hunk or any of the later hunks in the file"),
1176        checkout_head => N__(
1177"y - discard this hunk from index and worktree
1178n - do not discard this hunk from index and worktree
1179q - quit; do not discard this hunk or any of the remaining ones
1180a - discard this hunk and all later hunks in the file
1181d - do not discard this hunk or any of the later hunks in the file"),
1182        checkout_nothead => N__(
1183"y - apply this hunk to index and worktree
1184n - do not apply this hunk to index and worktree
1185q - quit; do not apply this hunk or any of the remaining ones
1186a - apply this hunk and all later hunks in the file
1187d - do not apply this hunk or any of the later hunks in the file"),
1188);
1189
1190sub help_patch_cmd {
1191        print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1192g - select a hunk to go to
1193/ - search for a hunk matching the given regex
1194j - leave this hunk undecided, see next undecided hunk
1195J - leave this hunk undecided, see next hunk
1196k - leave this hunk undecided, see previous undecided hunk
1197K - leave this hunk undecided, see previous hunk
1198s - split the current hunk into smaller hunks
1199e - manually edit the current hunk
1200? - print help
1201EOF
1202}
1203
1204sub apply_patch {
1205        my $cmd = shift;
1206        my $ret = run_git_apply $cmd, @_;
1207        if (!$ret) {
1208                print STDERR @_;
1209        }
1210        return $ret;
1211}
1212
1213sub apply_patch_for_checkout_commit {
1214        my $reverse = shift;
1215        my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1216        my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1217
1218        if ($applies_worktree && $applies_index) {
1219                run_git_apply 'apply '.$reverse.' --cached', @_;
1220                run_git_apply 'apply '.$reverse, @_;
1221                return 1;
1222        } elsif (!$applies_index) {
1223                print colored $error_color, __("The selected hunks do not apply to the index!\n");
1224                if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1225                        return run_git_apply 'apply '.$reverse, @_;
1226                } else {
1227                        print colored $error_color, __("Nothing was applied.\n");
1228                        return 0;
1229                }
1230        } else {
1231                print STDERR @_;
1232                return 0;
1233        }
1234}
1235
1236sub patch_update_cmd {
1237        my @all_mods = list_modified($patch_mode_flavour{FILTER});
1238        error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1239                for grep { $_->{UNMERGED} } @all_mods;
1240        @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1241
1242        my @mods = grep { !($_->{BINARY}) } @all_mods;
1243        my @them;
1244
1245        if (!@mods) {
1246                if (@all_mods) {
1247                        print STDERR __("Only binary files changed.\n");
1248                } else {
1249                        print STDERR __("No changes.\n");
1250                }
1251                return 0;
1252        }
1253        if ($patch_mode_only) {
1254                @them = @mods;
1255        }
1256        else {
1257                @them = list_and_choose({ PROMPT => __('Patch update'),
1258                                          HEADER => $status_head, },
1259                                        @mods);
1260        }
1261        for (@them) {
1262                return 0 if patch_update_file($_->{VALUE});
1263        }
1264}
1265
1266# Generate a one line summary of a hunk.
1267sub summarize_hunk {
1268        my $rhunk = shift;
1269        my $summary = $rhunk->{TEXT}[0];
1270
1271        # Keep the line numbers, discard extra context.
1272        $summary =~ s/@@(.*?)@@.*/$1 /s;
1273        $summary .= " " x (20 - length $summary);
1274
1275        # Add some user context.
1276        for my $line (@{$rhunk->{TEXT}}) {
1277                if ($line =~ m/^[+-].*\w/) {
1278                        $summary .= $line;
1279                        last;
1280                }
1281        }
1282
1283        chomp $summary;
1284        return substr($summary, 0, 80) . "\n";
1285}
1286
1287
1288# Print a one-line summary of each hunk in the array ref in
1289# the first argument, starting with the index in the 2nd.
1290sub display_hunks {
1291        my ($hunks, $i) = @_;
1292        my $ctr = 0;
1293        $i ||= 0;
1294        for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1295                my $status = " ";
1296                if (defined $hunks->[$i]{USE}) {
1297                        $status = $hunks->[$i]{USE} ? "+" : "-";
1298                }
1299                printf "%s%2d: %s",
1300                        $status,
1301                        $i + 1,
1302                        summarize_hunk($hunks->[$i]);
1303        }
1304        return $i;
1305}
1306
1307my %patch_update_prompt_modes = (
1308        stage => {
1309                mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1310                deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1311                hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1312        },
1313        stash => {
1314                mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1315                deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1316                hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1317        },
1318        reset_head => {
1319                mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1320                deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1321                hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1322        },
1323        reset_nothead => {
1324                mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1325                deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1326                hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1327        },
1328        checkout_index => {
1329                mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1330                deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1331                hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1332        },
1333        checkout_head => {
1334                mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1335                deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1336                hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1337        },
1338        checkout_nothead => {
1339                mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1340                deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1341                hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1342        },
1343);
1344
1345sub patch_update_file {
1346        my $quit = 0;
1347        my ($ix, $num);
1348        my $path = shift;
1349        my ($head, @hunk) = parse_diff($path);
1350        ($head, my $mode, my $deletion) = parse_diff_header($head);
1351        for (@{$head->{DISPLAY}}) {
1352                print;
1353        }
1354
1355        if (@{$mode->{TEXT}}) {
1356                unshift @hunk, $mode;
1357        }
1358        if (@{$deletion->{TEXT}}) {
1359                foreach my $hunk (@hunk) {
1360                        push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1361                        push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1362                }
1363                @hunk = ($deletion);
1364        }
1365
1366        $num = scalar @hunk;
1367        $ix = 0;
1368
1369        while (1) {
1370                my ($prev, $next, $other, $undecided, $i);
1371                $other = '';
1372
1373                if ($num <= $ix) {
1374                        $ix = 0;
1375                }
1376                for ($i = 0; $i < $ix; $i++) {
1377                        if (!defined $hunk[$i]{USE}) {
1378                                $prev = 1;
1379                                $other .= ',k';
1380                                last;
1381                        }
1382                }
1383                if ($ix) {
1384                        $other .= ',K';
1385                }
1386                for ($i = $ix + 1; $i < $num; $i++) {
1387                        if (!defined $hunk[$i]{USE}) {
1388                                $next = 1;
1389                                $other .= ',j';
1390                                last;
1391                        }
1392                }
1393                if ($ix < $num - 1) {
1394                        $other .= ',J';
1395                }
1396                if ($num > 1) {
1397                        $other .= ',g';
1398                }
1399                for ($i = 0; $i < $num; $i++) {
1400                        if (!defined $hunk[$i]{USE}) {
1401                                $undecided = 1;
1402                                last;
1403                        }
1404                }
1405                last if (!$undecided);
1406
1407                if ($hunk[$ix]{TYPE} eq 'hunk' &&
1408                    hunk_splittable($hunk[$ix]{TEXT})) {
1409                        $other .= ',s';
1410                }
1411                if ($hunk[$ix]{TYPE} eq 'hunk') {
1412                        $other .= ',e';
1413                }
1414                for (@{$hunk[$ix]{DISPLAY}}) {
1415                        print;
1416                }
1417                print colored $prompt_color,
1418                        sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1419
1420                my $line = prompt_single_character;
1421                last unless defined $line;
1422                if ($line) {
1423                        if ($line =~ /^y/i) {
1424                                $hunk[$ix]{USE} = 1;
1425                        }
1426                        elsif ($line =~ /^n/i) {
1427                                $hunk[$ix]{USE} = 0;
1428                        }
1429                        elsif ($line =~ /^a/i) {
1430                                while ($ix < $num) {
1431                                        if (!defined $hunk[$ix]{USE}) {
1432                                                $hunk[$ix]{USE} = 1;
1433                                        }
1434                                        $ix++;
1435                                }
1436                                next;
1437                        }
1438                        elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1439                                my $response = $1;
1440                                my $no = $ix > 10 ? $ix - 10 : 0;
1441                                while ($response eq '') {
1442                                        $no = display_hunks(\@hunk, $no);
1443                                        if ($no < $num) {
1444                                                print __("go to which hunk (<ret> to see more)? ");
1445                                        } else {
1446                                                print __("go to which hunk? ");
1447                                        }
1448                                        $response = <STDIN>;
1449                                        if (!defined $response) {
1450                                                $response = '';
1451                                        }
1452                                        chomp $response;
1453                                }
1454                                if ($response !~ /^\s*\d+\s*$/) {
1455                                        error_msg sprintf(__("Invalid number: '%s'\n"),
1456                                                             $response);
1457                                } elsif (0 < $response && $response <= $num) {
1458                                        $ix = $response - 1;
1459                                } else {
1460                                        error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1461                                                              "Sorry, only %d hunks available.\n", $num), $num);
1462                                }
1463                                next;
1464                        }
1465                        elsif ($line =~ /^d/i) {
1466                                while ($ix < $num) {
1467                                        if (!defined $hunk[$ix]{USE}) {
1468                                                $hunk[$ix]{USE} = 0;
1469                                        }
1470                                        $ix++;
1471                                }
1472                                next;
1473                        }
1474                        elsif ($line =~ /^q/i) {
1475                                for ($i = 0; $i < $num; $i++) {
1476                                        if (!defined $hunk[$i]{USE}) {
1477                                                $hunk[$i]{USE} = 0;
1478                                        }
1479                                }
1480                                $quit = 1;
1481                                last;
1482                        }
1483                        elsif ($line =~ m|^/(.*)|) {
1484                                my $regex = $1;
1485                                if ($1 eq "") {
1486                                        print colored $prompt_color, __("search for regex? ");
1487                                        $regex = <STDIN>;
1488                                        if (defined $regex) {
1489                                                chomp $regex;
1490                                        }
1491                                }
1492                                my $search_string;
1493                                eval {
1494                                        $search_string = qr{$regex}m;
1495                                };
1496                                if ($@) {
1497                                        my ($err,$exp) = ($@, $1);
1498                                        $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1499                                        error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1500                                        next;
1501                                }
1502                                my $iy = $ix;
1503                                while (1) {
1504                                        my $text = join ("", @{$hunk[$iy]{TEXT}});
1505                                        last if ($text =~ $search_string);
1506                                        $iy++;
1507                                        $iy = 0 if ($iy >= $num);
1508                                        if ($ix == $iy) {
1509                                                error_msg __("No hunk matches the given pattern\n");
1510                                                last;
1511                                        }
1512                                }
1513                                $ix = $iy;
1514                                next;
1515                        }
1516                        elsif ($line =~ /^K/) {
1517                                if ($other =~ /K/) {
1518                                        $ix--;
1519                                }
1520                                else {
1521                                        error_msg __("No previous hunk\n");
1522                                }
1523                                next;
1524                        }
1525                        elsif ($line =~ /^J/) {
1526                                if ($other =~ /J/) {
1527                                        $ix++;
1528                                }
1529                                else {
1530                                        error_msg __("No next hunk\n");
1531                                }
1532                                next;
1533                        }
1534                        elsif ($line =~ /^k/) {
1535                                if ($other =~ /k/) {
1536                                        while (1) {
1537                                                $ix--;
1538                                                last if (!$ix ||
1539                                                         !defined $hunk[$ix]{USE});
1540                                        }
1541                                }
1542                                else {
1543                                        error_msg __("No previous hunk\n");
1544                                }
1545                                next;
1546                        }
1547                        elsif ($line =~ /^j/) {
1548                                if ($other !~ /j/) {
1549                                        error_msg __("No next hunk\n");
1550                                        next;
1551                                }
1552                        }
1553                        elsif ($other =~ /s/ && $line =~ /^s/) {
1554                                my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1555                                if (1 < @split) {
1556                                        print colored $header_color, sprintf(
1557                                                __n("Split into %d hunk.\n",
1558                                                    "Split into %d hunks.\n",
1559                                                    scalar(@split)), scalar(@split));
1560                                }
1561                                splice (@hunk, $ix, 1, @split);
1562                                $num = scalar @hunk;
1563                                next;
1564                        }
1565                        elsif ($other =~ /e/ && $line =~ /^e/) {
1566                                my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1567                                if (defined $newhunk) {
1568                                        splice @hunk, $ix, 1, $newhunk;
1569                                }
1570                        }
1571                        else {
1572                                help_patch_cmd($other);
1573                                next;
1574                        }
1575                        # soft increment
1576                        while (1) {
1577                                $ix++;
1578                                last if ($ix >= $num ||
1579                                         !defined $hunk[$ix]{USE});
1580                        }
1581                }
1582        }
1583
1584        @hunk = coalesce_overlapping_hunks(@hunk);
1585
1586        my $n_lofs = 0;
1587        my @result = ();
1588        for (@hunk) {
1589                if ($_->{USE}) {
1590                        push @result, @{$_->{TEXT}};
1591                }
1592        }
1593
1594        if (@result) {
1595                my @patch = reassemble_patch($head->{TEXT}, @result);
1596                my $apply_routine = $patch_mode_flavour{APPLY};
1597                &$apply_routine(@patch);
1598                refresh();
1599        }
1600
1601        print "\n";
1602        return $quit;
1603}
1604
1605sub diff_cmd {
1606        my @mods = list_modified('index-only');
1607        @mods = grep { !($_->{BINARY}) } @mods;
1608        return if (!@mods);
1609        my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1610                                     IMMEDIATE => 1,
1611                                     HEADER => $status_head, },
1612                                   @mods);
1613        return if (!@them);
1614        my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1615        system(qw(git diff -p --cached), $reference, '--',
1616                map { $_->{VALUE} } @them);
1617}
1618
1619sub quit_cmd {
1620        print __("Bye.\n");
1621        exit(0);
1622}
1623
1624sub help_cmd {
1625# TRANSLATORS: please do not translate the command names
1626# 'status', 'update', 'revert', etc.
1627        print colored $help_color, __ <<'EOF' ;
1628status        - show paths with changes
1629update        - add working tree state to the staged set of changes
1630revert        - revert staged set of changes back to the HEAD version
1631patch         - pick hunks and update selectively
1632diff          - view diff between HEAD and index
1633add untracked - add contents of untracked files to the staged set of changes
1634EOF
1635}
1636
1637sub process_args {
1638        return unless @ARGV;
1639        my $arg = shift @ARGV;
1640        if ($arg =~ /--patch(?:=(.*))?/) {
1641                if (defined $1) {
1642                        if ($1 eq 'reset') {
1643                                $patch_mode = 'reset_head';
1644                                $patch_mode_revision = 'HEAD';
1645                                $arg = shift @ARGV or die __("missing --");
1646                                if ($arg ne '--') {
1647                                        $patch_mode_revision = $arg;
1648                                        $patch_mode = ($arg eq 'HEAD' ?
1649                                                       'reset_head' : 'reset_nothead');
1650                                        $arg = shift @ARGV or die __("missing --");
1651                                }
1652                        } elsif ($1 eq 'checkout') {
1653                                $arg = shift @ARGV or die __("missing --");
1654                                if ($arg eq '--') {
1655                                        $patch_mode = 'checkout_index';
1656                                } else {
1657                                        $patch_mode_revision = $arg;
1658                                        $patch_mode = ($arg eq 'HEAD' ?
1659                                                       'checkout_head' : 'checkout_nothead');
1660                                        $arg = shift @ARGV or die __("missing --");
1661                                }
1662                        } elsif ($1 eq 'stage' or $1 eq 'stash') {
1663                                $patch_mode = $1;
1664                                $arg = shift @ARGV or die __("missing --");
1665                        } else {
1666                                die sprintf(__("unknown --patch mode: %s"), $1);
1667                        }
1668                } else {
1669                        $patch_mode = 'stage';
1670                        $arg = shift @ARGV or die __("missing --");
1671                }
1672                die sprintf(__("invalid argument %s, expecting --"),
1673                               $arg) unless $arg eq "--";
1674                %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1675                $patch_mode_only = 1;
1676        }
1677        elsif ($arg ne "--") {
1678                die sprintf(__("invalid argument %s, expecting --"), $arg);
1679        }
1680}
1681
1682sub main_loop {
1683        my @cmd = ([ 'status', \&status_cmd, ],
1684                   [ 'update', \&update_cmd, ],
1685                   [ 'revert', \&revert_cmd, ],
1686                   [ 'add untracked', \&add_untracked_cmd, ],
1687                   [ 'patch', \&patch_update_cmd, ],
1688                   [ 'diff', \&diff_cmd, ],
1689                   [ 'quit', \&quit_cmd, ],
1690                   [ 'help', \&help_cmd, ],
1691        );
1692        while (1) {
1693                my ($it) = list_and_choose({ PROMPT => __('What now'),
1694                                             SINGLETON => 1,
1695                                             LIST_FLAT => 4,
1696                                             HEADER => __('*** Commands ***'),
1697                                             ON_EOF => \&quit_cmd,
1698                                             IMMEDIATE => 1 }, @cmd);
1699                if ($it) {
1700                        eval {
1701                                $it->[1]->();
1702                        };
1703                        if ($@) {
1704                                print "$@";
1705                        }
1706                }
1707        }
1708}
1709
1710process_args();
1711refresh();
1712if ($patch_mode_only) {
1713        patch_update_cmd();
1714}
1715else {
1716        status_cmd();
1717        main_loop();
1718}