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