git-add--interactive.perlon commit general UI improvements (05293f9)
   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                        if ($patch_mode_flavour{IS_REVERSE}) {
 990                                $o_ofs -= $ofs_delta;
 991                        } else {
 992                                $n_ofs += $ofs_delta;
 993                        }
 994                        $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
 995                                                             $n_ofs, $n_cnt);
 996                }
 997                # If this hunk was edited then adjust the offset delta
 998                # to reflect the edit.
 999                if ($_->{OFS_DELTA}) {
1000                        $ofs_delta += $_->{OFS_DELTA};
1001                }
1002                if (defined $last_o_ctx &&
1003                    $o_ofs <= $last_o_ctx &&
1004                    !$_->{DIRTY} &&
1005                    !$last_was_dirty) {
1006                        merge_hunk($out[-1], $_);
1007                }
1008                else {
1009                        push @out, $_;
1010                }
1011                $last_o_ctx = find_last_o_ctx($out[-1]);
1012                $last_was_dirty = $_->{DIRTY};
1013        }
1014        return @out;
1015}
1016
1017sub reassemble_patch {
1018        my $head = shift;
1019        my @patch;
1020
1021        # Include everything in the header except the beginning of the diff.
1022        push @patch, (grep { !/^[-+]{3}/ } @$head);
1023
1024        # Then include any headers from the hunk lines, which must
1025        # come before any actual hunk.
1026        while (@_ && $_[0] !~ /^@/) {
1027                push @patch, shift;
1028        }
1029
1030        # Then begin the diff.
1031        push @patch, grep { /^[-+]{3}/ } @$head;
1032
1033        # And then the actual hunks.
1034        push @patch, @_;
1035
1036        return @patch;
1037}
1038
1039sub color_diff {
1040        return map {
1041                colored((/^@/  ? $fraginfo_color :
1042                         /^\+/ ? $diff_new_color :
1043                         /^-/  ? $diff_old_color :
1044                         $diff_plain_color),
1045                        $_);
1046        } @_;
1047}
1048
1049my %edit_hunk_manually_modes = (
1050        stage => N__(
1051"If the patch applies cleanly, the edited hunk will immediately be
1052marked for staging."),
1053        stash => N__(
1054"If the patch applies cleanly, the edited hunk will immediately be
1055marked for stashing."),
1056        reset_head => N__(
1057"If the patch applies cleanly, the edited hunk will immediately be
1058marked for unstaging."),
1059        reset_nothead => N__(
1060"If the patch applies cleanly, the edited hunk will immediately be
1061marked for applying."),
1062        checkout_index => N__(
1063"If the patch applies cleanly, the edited hunk will immediately be
1064marked for discarding."),
1065        checkout_head => N__(
1066"If the patch applies cleanly, the edited hunk will immediately be
1067marked for discarding."),
1068        checkout_nothead => N__(
1069"If the patch applies cleanly, the edited hunk will immediately be
1070marked for applying."),
1071        worktree_head => N__(
1072"If the patch applies cleanly, the edited hunk will immediately be
1073marked for discarding."),
1074        worktree_nothead => N__(
1075"If the patch applies cleanly, the edited hunk will immediately be
1076marked for applying."),
1077);
1078
1079sub recount_edited_hunk {
1080        local $_;
1081        my ($oldtext, $newtext) = @_;
1082        my ($o_cnt, $n_cnt) = (0, 0);
1083        for (@{$newtext}[1..$#{$newtext}]) {
1084                my $mode = substr($_, 0, 1);
1085                if ($mode eq '-') {
1086                        $o_cnt++;
1087                } elsif ($mode eq '+') {
1088                        $n_cnt++;
1089                } elsif ($mode eq ' ' or $mode eq "\n") {
1090                        $o_cnt++;
1091                        $n_cnt++;
1092                }
1093        }
1094        my ($o_ofs, undef, $n_ofs, undef) =
1095                                        parse_hunk_header($newtext->[0]);
1096        $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1097        my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1098                                        parse_hunk_header($oldtext->[0]);
1099        # Return the change in the number of lines inserted by this hunk
1100        return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1101}
1102
1103sub edit_hunk_manually {
1104        my ($oldtext) = @_;
1105
1106        my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1107        my $fh;
1108        open $fh, '>', $hunkfile
1109                or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1110        print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1111        print $fh @$oldtext;
1112        my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1113        my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1114        my $comment_line_char = Git::get_comment_line_char;
1115        print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1116---
1117To remove '%s' lines, make them ' ' lines (context).
1118To remove '%s' lines, delete them.
1119Lines starting with %s will be removed.
1120EOF
1121__($edit_hunk_manually_modes{$patch_mode}),
1122# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1123__ <<EOF2 ;
1124If it does not apply cleanly, you will be given an opportunity to
1125edit again.  If all lines of the hunk are removed, then the edit is
1126aborted and the hunk is left unchanged.
1127EOF2
1128        close $fh;
1129
1130        chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1131        system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1132
1133        if ($? != 0) {
1134                return undef;
1135        }
1136
1137        open $fh, '<', $hunkfile
1138                or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1139        my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1140        close $fh;
1141        unlink $hunkfile;
1142
1143        # Abort if nothing remains
1144        if (!grep { /\S/ } @newtext) {
1145                return undef;
1146        }
1147
1148        # Reinsert the first hunk header if the user accidentally deleted it
1149        if ($newtext[0] !~ /^@/) {
1150                unshift @newtext, $oldtext->[0];
1151        }
1152        return \@newtext;
1153}
1154
1155sub diff_applies {
1156        return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1157                             map { @{$_->{TEXT}} } @_);
1158}
1159
1160sub _restore_terminal_and_die {
1161        ReadMode 'restore';
1162        print "\n";
1163        exit 1;
1164}
1165
1166sub prompt_single_character {
1167        if ($use_readkey) {
1168                local $SIG{TERM} = \&_restore_terminal_and_die;
1169                local $SIG{INT} = \&_restore_terminal_and_die;
1170                ReadMode 'cbreak';
1171                my $key = ReadKey 0;
1172                ReadMode 'restore';
1173                if ($use_termcap and $key eq "\e") {
1174                        while (!defined $term_escapes{$key}) {
1175                                my $next = ReadKey 0.5;
1176                                last if (!defined $next);
1177                                $key .= $next;
1178                        }
1179                        $key =~ s/\e/^[/;
1180                }
1181                print "$key" if defined $key;
1182                print "\n";
1183                return $key;
1184        } else {
1185                return <STDIN>;
1186        }
1187}
1188
1189sub prompt_yesno {
1190        my ($prompt) = @_;
1191        while (1) {
1192                print colored $prompt_color, $prompt;
1193                my $line = prompt_single_character;
1194                return undef unless defined $line;
1195                return 0 if $line =~ /^n/i;
1196                return 1 if $line =~ /^y/i;
1197        }
1198}
1199
1200sub edit_hunk_loop {
1201        my ($head, $hunks, $ix) = @_;
1202        my $hunk = $hunks->[$ix];
1203        my $text = $hunk->{TEXT};
1204
1205        while (1) {
1206                my $newtext = edit_hunk_manually($text);
1207                if (!defined $newtext) {
1208                        return undef;
1209                }
1210                my $newhunk = {
1211                        TEXT => $newtext,
1212                        TYPE => $hunk->{TYPE},
1213                        USE => 1,
1214                        DIRTY => 1,
1215                };
1216                $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1217                # If this hunk has already been edited then add the
1218                # offset delta of the previous edit to get the real
1219                # delta from the original unedited hunk.
1220                $hunk->{OFS_DELTA} and
1221                                $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1222                if (diff_applies($head,
1223                                 @{$hunks}[0..$ix-1],
1224                                 $newhunk,
1225                                 @{$hunks}[$ix+1..$#{$hunks}])) {
1226                        $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1227                        return $newhunk;
1228                }
1229                else {
1230                        prompt_yesno(
1231                                # TRANSLATORS: do not translate [y/n]
1232                                # The program will only accept that input
1233                                # at this point.
1234                                # Consider translating (saying "no" discards!) as
1235                                # (saying "n" for "no" discards!) if the translation
1236                                # of the word "no" does not start with n.
1237                                __('Your edited hunk does not apply. Edit again '
1238                                   . '(saying "no" discards!) [y/n]? ')
1239                                ) or return undef;
1240                }
1241        }
1242}
1243
1244my %help_patch_modes = (
1245        stage => N__(
1246"y - stage this hunk
1247n - do not stage this hunk
1248q - quit; do not stage this hunk or any of the remaining ones
1249a - stage this hunk and all later hunks in the file
1250d - do not stage this hunk or any of the later hunks in the file"),
1251        stash => N__(
1252"y - stash this hunk
1253n - do not stash this hunk
1254q - quit; do not stash this hunk or any of the remaining ones
1255a - stash this hunk and all later hunks in the file
1256d - do not stash this hunk or any of the later hunks in the file"),
1257        reset_head => N__(
1258"y - unstage this hunk
1259n - do not unstage this hunk
1260q - quit; do not unstage this hunk or any of the remaining ones
1261a - unstage this hunk and all later hunks in the file
1262d - do not unstage this hunk or any of the later hunks in the file"),
1263        reset_nothead => N__(
1264"y - apply this hunk to index
1265n - do not apply this hunk to index
1266q - quit; do not apply this hunk or any of the remaining ones
1267a - apply this hunk and all later hunks in the file
1268d - do not apply this hunk or any of the later hunks in the file"),
1269        checkout_index => N__(
1270"y - discard this hunk from worktree
1271n - do not discard this hunk from worktree
1272q - quit; do not discard this hunk or any of the remaining ones
1273a - discard this hunk and all later hunks in the file
1274d - do not discard this hunk or any of the later hunks in the file"),
1275        checkout_head => N__(
1276"y - discard this hunk from index and worktree
1277n - do not discard this hunk from index and worktree
1278q - quit; do not discard this hunk or any of the remaining ones
1279a - discard this hunk and all later hunks in the file
1280d - do not discard this hunk or any of the later hunks in the file"),
1281        checkout_nothead => N__(
1282"y - apply this hunk to index and worktree
1283n - do not apply this hunk to index and worktree
1284q - quit; do not apply this hunk or any of the remaining ones
1285a - apply this hunk and all later hunks in the file
1286d - do not apply this hunk or any of the later hunks in the file"),
1287        worktree_head => N__(
1288"y - discard this hunk from worktree
1289n - do not discard this hunk from worktree
1290q - quit; do not discard this hunk or any of the remaining ones
1291a - discard this hunk and all later hunks in the file
1292d - do not discard this hunk or any of the later hunks in the file"),
1293        worktree_nothead => N__(
1294"y - apply this hunk to worktree
1295n - do not apply this hunk to worktree
1296q - quit; do not apply this hunk or any of the remaining ones
1297a - apply this hunk and all later hunks in the file
1298d - do not apply this hunk or any of the later hunks in the file"),
1299);
1300
1301sub help_patch_cmd {
1302        local $_;
1303        my $other = $_[0] . ",?";
1304        print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1305                map { "$_\n" } grep {
1306                        my $c = quotemeta(substr($_, 0, 1));
1307                        $other =~ /,$c/
1308                } split "\n", __ <<EOF ;
1309g - select a hunk to go to
1310/ - search for a hunk matching the given regex
1311j - leave this hunk undecided, see next undecided hunk
1312J - leave this hunk undecided, see next hunk
1313k - leave this hunk undecided, see previous undecided hunk
1314K - leave this hunk undecided, see previous hunk
1315s - split the current hunk into smaller hunks
1316e - manually edit the current hunk
1317? - print help
1318EOF
1319}
1320
1321sub apply_patch {
1322        my $cmd = shift;
1323        my $ret = run_git_apply $cmd, @_;
1324        if (!$ret) {
1325                print STDERR @_;
1326        }
1327        return $ret;
1328}
1329
1330sub apply_patch_for_checkout_commit {
1331        my $reverse = shift;
1332        my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1333        my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1334
1335        if ($applies_worktree && $applies_index) {
1336                run_git_apply 'apply '.$reverse.' --cached', @_;
1337                run_git_apply 'apply '.$reverse, @_;
1338                return 1;
1339        } elsif (!$applies_index) {
1340                print colored $error_color, __("The selected hunks do not apply to the index!\n");
1341                if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1342                        return run_git_apply 'apply '.$reverse, @_;
1343                } else {
1344                        print colored $error_color, __("Nothing was applied.\n");
1345                        return 0;
1346                }
1347        } else {
1348                print STDERR @_;
1349                return 0;
1350        }
1351}
1352
1353sub patch_update_cmd {
1354        my @all_mods = list_modified($patch_mode_flavour{FILTER});
1355        error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1356                for grep { $_->{UNMERGED} } @all_mods;
1357        @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1358
1359        my @mods = grep { !($_->{BINARY}) } @all_mods;
1360        my @them;
1361
1362        if (!@mods) {
1363                if (@all_mods) {
1364                        print STDERR __("Only binary files changed.\n");
1365                } else {
1366                        print STDERR __("No changes.\n");
1367                }
1368                return 0;
1369        }
1370        if ($patch_mode_only) {
1371                @them = @mods;
1372        }
1373        else {
1374                @them = list_and_choose({ PROMPT => __('Patch update'),
1375                                          HEADER => $status_head, },
1376                                        @mods);
1377        }
1378        for (@them) {
1379                return 0 if patch_update_file($_->{VALUE});
1380        }
1381}
1382
1383# Generate a one line summary of a hunk.
1384sub summarize_hunk {
1385        my $rhunk = shift;
1386        my $summary = $rhunk->{TEXT}[0];
1387
1388        # Keep the line numbers, discard extra context.
1389        $summary =~ s/@@(.*?)@@.*/$1 /s;
1390        $summary .= " " x (20 - length $summary);
1391
1392        # Add some user context.
1393        for my $line (@{$rhunk->{TEXT}}) {
1394                if ($line =~ m/^[+-].*\w/) {
1395                        $summary .= $line;
1396                        last;
1397                }
1398        }
1399
1400        chomp $summary;
1401        return substr($summary, 0, 80) . "\n";
1402}
1403
1404
1405# Print a one-line summary of each hunk in the array ref in
1406# the first argument, starting with the index in the 2nd.
1407sub display_hunks {
1408        my ($hunks, $i) = @_;
1409        my $ctr = 0;
1410        $i ||= 0;
1411        for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1412                my $status = " ";
1413                if (defined $hunks->[$i]{USE}) {
1414                        $status = $hunks->[$i]{USE} ? "+" : "-";
1415                }
1416                printf "%s%2d: %s",
1417                        $status,
1418                        $i + 1,
1419                        summarize_hunk($hunks->[$i]);
1420        }
1421        return $i;
1422}
1423
1424my %patch_update_prompt_modes = (
1425        stage => {
1426                mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1427                deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1428                hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1429        },
1430        stash => {
1431                mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1432                deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1433                hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1434        },
1435        reset_head => {
1436                mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1437                deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1438                hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1439        },
1440        reset_nothead => {
1441                mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1442                deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1443                hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1444        },
1445        checkout_index => {
1446                mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1447                deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1448                hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1449        },
1450        checkout_head => {
1451                mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1452                deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1453                hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1454        },
1455        checkout_nothead => {
1456                mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1457                deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1458                hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1459        },
1460        worktree_head => {
1461                mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1462                deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1463                hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1464        },
1465        worktree_nothead => {
1466                mode => N__("Apply mode change to worktree [y,n,q,a,d%s,?]? "),
1467                deletion => N__("Apply deletion to worktree [y,n,q,a,d%s,?]? "),
1468                hunk => N__("Apply this hunk to worktree [y,n,q,a,d%s,?]? "),
1469        },
1470);
1471
1472sub patch_update_file {
1473        my $quit = 0;
1474        my ($ix, $num);
1475        my $path = shift;
1476        my ($head, @hunk) = parse_diff($path);
1477        ($head, my $mode, my $deletion) = parse_diff_header($head);
1478        for (@{$head->{DISPLAY}}) {
1479                print;
1480        }
1481
1482        if (@{$mode->{TEXT}}) {
1483                unshift @hunk, $mode;
1484        }
1485        if (@{$deletion->{TEXT}}) {
1486                foreach my $hunk (@hunk) {
1487                        push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1488                        push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1489                }
1490                @hunk = ($deletion);
1491        }
1492
1493        $num = scalar @hunk;
1494        $ix = 0;
1495
1496        while (1) {
1497                my ($prev, $next, $other, $undecided, $i);
1498                $other = '';
1499
1500                if ($num <= $ix) {
1501                        $ix = 0;
1502                }
1503                for ($i = 0; $i < $ix; $i++) {
1504                        if (!defined $hunk[$i]{USE}) {
1505                                $prev = 1;
1506                                $other .= ',k';
1507                                last;
1508                        }
1509                }
1510                if ($ix) {
1511                        $other .= ',K';
1512                }
1513                for ($i = $ix + 1; $i < $num; $i++) {
1514                        if (!defined $hunk[$i]{USE}) {
1515                                $next = 1;
1516                                $other .= ',j';
1517                                last;
1518                        }
1519                }
1520                if ($ix < $num - 1) {
1521                        $other .= ',J';
1522                }
1523                if ($num > 1) {
1524                        $other .= ',g,/';
1525                }
1526                for ($i = 0; $i < $num; $i++) {
1527                        if (!defined $hunk[$i]{USE}) {
1528                                $undecided = 1;
1529                                last;
1530                        }
1531                }
1532                last if (!$undecided);
1533
1534                if ($hunk[$ix]{TYPE} eq 'hunk' &&
1535                    hunk_splittable($hunk[$ix]{TEXT})) {
1536                        $other .= ',s';
1537                }
1538                if ($hunk[$ix]{TYPE} eq 'hunk') {
1539                        $other .= ',e';
1540                }
1541                for (@{$hunk[$ix]{DISPLAY}}) {
1542                        print;
1543                }
1544                print colored $prompt_color,
1545                        sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1546
1547                my $line = prompt_single_character;
1548                last unless defined $line;
1549                if ($line) {
1550                        if ($line =~ /^y/i) {
1551                                $hunk[$ix]{USE} = 1;
1552                        }
1553                        elsif ($line =~ /^n/i) {
1554                                $hunk[$ix]{USE} = 0;
1555                        }
1556                        elsif ($line =~ /^a/i) {
1557                                while ($ix < $num) {
1558                                        if (!defined $hunk[$ix]{USE}) {
1559                                                $hunk[$ix]{USE} = 1;
1560                                        }
1561                                        $ix++;
1562                                }
1563                                next;
1564                        }
1565                        elsif ($line =~ /^g(.*)/) {
1566                                my $response = $1;
1567                                unless ($other =~ /g/) {
1568                                        error_msg __("No other hunks to goto\n");
1569                                        next;
1570                                }
1571                                my $no = $ix > 10 ? $ix - 10 : 0;
1572                                while ($response eq '') {
1573                                        $no = display_hunks(\@hunk, $no);
1574                                        if ($no < $num) {
1575                                                print __("go to which hunk (<ret> to see more)? ");
1576                                        } else {
1577                                                print __("go to which hunk? ");
1578                                        }
1579                                        $response = <STDIN>;
1580                                        if (!defined $response) {
1581                                                $response = '';
1582                                        }
1583                                        chomp $response;
1584                                }
1585                                if ($response !~ /^\s*\d+\s*$/) {
1586                                        error_msg sprintf(__("Invalid number: '%s'\n"),
1587                                                             $response);
1588                                } elsif (0 < $response && $response <= $num) {
1589                                        $ix = $response - 1;
1590                                } else {
1591                                        error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1592                                                              "Sorry, only %d hunks available.\n", $num), $num);
1593                                }
1594                                next;
1595                        }
1596                        elsif ($line =~ /^d/i) {
1597                                while ($ix < $num) {
1598                                        if (!defined $hunk[$ix]{USE}) {
1599                                                $hunk[$ix]{USE} = 0;
1600                                        }
1601                                        $ix++;
1602                                }
1603                                next;
1604                        }
1605                        elsif ($line =~ /^q/i) {
1606                                for ($i = 0; $i < $num; $i++) {
1607                                        if (!defined $hunk[$i]{USE}) {
1608                                                $hunk[$i]{USE} = 0;
1609                                        }
1610                                }
1611                                $quit = 1;
1612                                last;
1613                        }
1614                        elsif ($line =~ m|^/(.*)|) {
1615                                my $regex = $1;
1616                                unless ($other =~ m|/|) {
1617                                        error_msg __("No other hunks to search\n");
1618                                        next;
1619                                }
1620                                if ($regex eq "") {
1621                                        print colored $prompt_color, __("search for regex? ");
1622                                        $regex = <STDIN>;
1623                                        if (defined $regex) {
1624                                                chomp $regex;
1625                                        }
1626                                }
1627                                my $search_string;
1628                                eval {
1629                                        $search_string = qr{$regex}m;
1630                                };
1631                                if ($@) {
1632                                        my ($err,$exp) = ($@, $1);
1633                                        $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1634                                        error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1635                                        next;
1636                                }
1637                                my $iy = $ix;
1638                                while (1) {
1639                                        my $text = join ("", @{$hunk[$iy]{TEXT}});
1640                                        last if ($text =~ $search_string);
1641                                        $iy++;
1642                                        $iy = 0 if ($iy >= $num);
1643                                        if ($ix == $iy) {
1644                                                error_msg __("No hunk matches the given pattern\n");
1645                                                last;
1646                                        }
1647                                }
1648                                $ix = $iy;
1649                                next;
1650                        }
1651                        elsif ($line =~ /^K/) {
1652                                if ($other =~ /K/) {
1653                                        $ix--;
1654                                }
1655                                else {
1656                                        error_msg __("No previous hunk\n");
1657                                }
1658                                next;
1659                        }
1660                        elsif ($line =~ /^J/) {
1661                                if ($other =~ /J/) {
1662                                        $ix++;
1663                                }
1664                                else {
1665                                        error_msg __("No next hunk\n");
1666                                }
1667                                next;
1668                        }
1669                        elsif ($line =~ /^k/) {
1670                                if ($other =~ /k/) {
1671                                        while (1) {
1672                                                $ix--;
1673                                                last if (!$ix ||
1674                                                         !defined $hunk[$ix]{USE});
1675                                        }
1676                                }
1677                                else {
1678                                        error_msg __("No previous hunk\n");
1679                                }
1680                                next;
1681                        }
1682                        elsif ($line =~ /^j/) {
1683                                if ($other !~ /j/) {
1684                                        error_msg __("No next hunk\n");
1685                                        next;
1686                                }
1687                        }
1688                        elsif ($line =~ /^s/) {
1689                                unless ($other =~ /s/) {
1690                                        error_msg __("Sorry, cannot split this hunk\n");
1691                                        next;
1692                                }
1693                                my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1694                                if (1 < @split) {
1695                                        print colored $header_color, sprintf(
1696                                                __n("Split into %d hunk.\n",
1697                                                    "Split into %d hunks.\n",
1698                                                    scalar(@split)), scalar(@split));
1699                                }
1700                                splice (@hunk, $ix, 1, @split);
1701                                $num = scalar @hunk;
1702                                next;
1703                        }
1704                        elsif ($line =~ /^e/) {
1705                                unless ($other =~ /e/) {
1706                                        error_msg __("Sorry, cannot edit this hunk\n");
1707                                        next;
1708                                }
1709                                my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1710                                if (defined $newhunk) {
1711                                        splice @hunk, $ix, 1, $newhunk;
1712                                }
1713                        }
1714                        else {
1715                                help_patch_cmd($other);
1716                                next;
1717                        }
1718                        # soft increment
1719                        while (1) {
1720                                $ix++;
1721                                last if ($ix >= $num ||
1722                                         !defined $hunk[$ix]{USE});
1723                        }
1724                }
1725        }
1726
1727        @hunk = coalesce_overlapping_hunks(@hunk);
1728
1729        my $n_lofs = 0;
1730        my @result = ();
1731        for (@hunk) {
1732                if ($_->{USE}) {
1733                        push @result, @{$_->{TEXT}};
1734                }
1735        }
1736
1737        if (@result) {
1738                my @patch = reassemble_patch($head->{TEXT}, @result);
1739                my $apply_routine = $patch_mode_flavour{APPLY};
1740                &$apply_routine(@patch);
1741                refresh();
1742        }
1743
1744        print "\n";
1745        return $quit;
1746}
1747
1748sub diff_cmd {
1749        my @mods = list_modified('index-only');
1750        @mods = grep { !($_->{BINARY}) } @mods;
1751        return if (!@mods);
1752        my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1753                                     IMMEDIATE => 1,
1754                                     HEADER => $status_head, },
1755                                   @mods);
1756        return if (!@them);
1757        my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1758        system(qw(git diff -p --cached), $reference, '--',
1759                map { $_->{VALUE} } @them);
1760}
1761
1762sub quit_cmd {
1763        print __("Bye.\n");
1764        exit(0);
1765}
1766
1767sub help_cmd {
1768# TRANSLATORS: please do not translate the command names
1769# 'status', 'update', 'revert', etc.
1770        print colored $help_color, __ <<'EOF' ;
1771status        - show paths with changes
1772update        - add working tree state to the staged set of changes
1773revert        - revert staged set of changes back to the HEAD version
1774patch         - pick hunks and update selectively
1775diff          - view diff between HEAD and index
1776add untracked - add contents of untracked files to the staged set of changes
1777EOF
1778}
1779
1780sub process_args {
1781        return unless @ARGV;
1782        my $arg = shift @ARGV;
1783        if ($arg =~ /--patch(?:=(.*))?/) {
1784                if (defined $1) {
1785                        if ($1 eq 'reset') {
1786                                $patch_mode = 'reset_head';
1787                                $patch_mode_revision = 'HEAD';
1788                                $arg = shift @ARGV or die __("missing --");
1789                                if ($arg ne '--') {
1790                                        $patch_mode_revision = $arg;
1791                                        $patch_mode = ($arg eq 'HEAD' ?
1792                                                       'reset_head' : 'reset_nothead');
1793                                        $arg = shift @ARGV or die __("missing --");
1794                                }
1795                        } elsif ($1 eq 'checkout') {
1796                                $arg = shift @ARGV or die __("missing --");
1797                                if ($arg eq '--') {
1798                                        $patch_mode = 'checkout_index';
1799                                } else {
1800                                        $patch_mode_revision = $arg;
1801                                        $patch_mode = ($arg eq 'HEAD' ?
1802                                                       'checkout_head' : 'checkout_nothead');
1803                                        $arg = shift @ARGV or die __("missing --");
1804                                }
1805                        } elsif ($1 eq 'worktree') {
1806                                $arg = shift @ARGV or die __("missing --");
1807                                if ($arg eq '--') {
1808                                        $patch_mode = 'checkout_index';
1809                                } else {
1810                                        $patch_mode_revision = $arg;
1811                                        $patch_mode = ($arg eq 'HEAD' ?
1812                                                       'worktree_head' : 'worktree_nothead');
1813                                        $arg = shift @ARGV or die __("missing --");
1814                                }
1815                        } elsif ($1 eq 'stage' or $1 eq 'stash') {
1816                                $patch_mode = $1;
1817                                $arg = shift @ARGV or die __("missing --");
1818                        } else {
1819                                die sprintf(__("unknown --patch mode: %s"), $1);
1820                        }
1821                } else {
1822                        $patch_mode = 'stage';
1823                        $arg = shift @ARGV or die __("missing --");
1824                }
1825                die sprintf(__("invalid argument %s, expecting --"),
1826                               $arg) unless $arg eq "--";
1827                %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1828                $patch_mode_only = 1;
1829        }
1830        elsif ($arg ne "--") {
1831                die sprintf(__("invalid argument %s, expecting --"), $arg);
1832        }
1833}
1834
1835sub main_loop {
1836        my @cmd = ([ 'status', \&status_cmd, ],
1837                   [ 'update', \&update_cmd, ],
1838                   [ 'revert', \&revert_cmd, ],
1839                   [ 'add untracked', \&add_untracked_cmd, ],
1840                   [ 'patch', \&patch_update_cmd, ],
1841                   [ 'diff', \&diff_cmd, ],
1842                   [ 'quit', \&quit_cmd, ],
1843                   [ 'help', \&help_cmd, ],
1844        );
1845        while (1) {
1846                my ($it) = list_and_choose({ PROMPT => __('What now'),
1847                                             SINGLETON => 1,
1848                                             LIST_FLAT => 4,
1849                                             HEADER => __('*** Commands ***'),
1850                                             ON_EOF => \&quit_cmd,
1851                                             IMMEDIATE => 1 }, @cmd);
1852                if ($it) {
1853                        eval {
1854                                $it->[1]->();
1855                        };
1856                        if ($@) {
1857                                print "$@";
1858                        }
1859                }
1860        }
1861}
1862
1863process_args();
1864refresh();
1865if ($patch_mode_only) {
1866        patch_update_cmd();
1867}
1868else {
1869        status_cmd();
1870        main_loop();
1871}