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