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