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