diff --git a/Changes b/Changes index 39888d4..e055629 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,6 @@ +# 0.004008 +- Allow to specify paths on the command line to limit fixup commits to these files only + # 0.004007 - Use the `--no-prefix` option for calls to `git diff` so that git-autofixup works regardless the configured values of the relatively new diff.srcPrefix and diff.dstPrefix variables, added in Git 2.45.0, as well as the older diff.noPrefix and diff.mnemonicPrefix. diff --git a/README.pod b/README.pod index a6e6a74..78ee976 100644 --- a/README.pod +++ b/README.pod @@ -6,7 +6,7 @@ App::Git::Autofixup - create fixup commits for topic branches =head1 SYNOPSIS - git-autofixup [] [] + git-autofixup [] [] [--] [...] =head1 DESCRIPTION @@ -14,6 +14,8 @@ F parses hunks of changes in the working directory out of C, then F will only consider staged hunks when trying to create fixup commits. A temporary index is used to create any resulting commits. +If one or more paths are specified, only changes to those files will be considered. The C<--> separator between the revision and paths is optional. + By default a hunk will be included in a fixup commit if all the lines in the hunk's context blamed on topic branch commits refer to the same commit, so there's no ambiguity about which commit the hunk corresponds to. If there is ambiguity the assignment behaviour used under C<--strict 1> will be used to attempt to resolve it. If C<--strict 1> is given the same topic branch commit must be blamed for every removed line and at least one of the lines adjacent to each added line, and added lines must not be adjacent to lines blamed on other topic branch commits. All the same restrictions apply when C<--strict 2> is given, but each added line must be surrounded by lines blamed on the same topic branch commit. For example, the added line in the hunk below is adjacent to lines committed by commits C<99f370af> and C. If these are both topic branch commits then it's ambiguous which commit the added line is fixing up and the hunk will be ignored. diff --git a/git-autofixup b/git-autofixup old mode 100755 new mode 100644 index 2c6f964..3a223eb --- a/git-autofixup +++ b/git-autofixup @@ -11,7 +11,7 @@ use File::Temp; use Getopt::Long qw(:config bundling); use IPC::Open3; -our $VERSION = 0.004007; +our $VERSION = 0.004008; my $VERBOSE; my @GIT_OPTIONS; @@ -24,7 +24,7 @@ my @STRICTNESS_LEVELS = ( ); my $usage =<<'END'; -usage: git-autofixup [] [] +usage: git-autofixup [] [] [--] [...] -h show usage --help show manpage @@ -475,10 +475,14 @@ sub blame { sub diff_hunks { my $num_context_lines = shift; + my @files = @_; my @cmd = git_cmd(qw(diff --no-prefix --no-ext-diff --ignore-submodules), "-U$num_context_lines"); if (is_index_dirty()) { push @cmd, "--cached"; } + if (@files) { + push @cmd, '--', @files; + } open(my $fh, '-|', @cmd) or die "run git diff: $!"; my @hunks = parse_hunks($fh, keep_lines => 1); close($fh) or die "git diff: " . child_error_desc($?) . "\n"; @@ -671,6 +675,22 @@ sub main { my $dryrun; my $use_detailed_exit_codes; + my $raw_upstream; + my @files = (); + + my $found_option_terminator = 0; + my @new_argv = (); + for my $arg (@ARGV) { + if ($found_option_terminator) { + push @files, $arg; + } elsif ($arg eq '--') { + $found_option_terminator = 1; + } else { + push @new_argv, $arg; + } + } + @ARGV = @new_argv; + GetOptions( 'h' => \$help, 'help' => \$man, @@ -682,6 +702,11 @@ sub main { 'gitopt|g=s' => \@GIT_OPTIONS, 'exit-code' => \$use_detailed_exit_codes, ) or return 1; + + if (@ARGV) { + $raw_upstream = shift @ARGV; + @files = (@ARGV, @files); + } if ($help) { print $usage; return 0; @@ -712,8 +737,8 @@ EOF # "upstream" revisions as 40 byte SHA1 hex hashes. my @upstreams = (); - if (@ARGV == 1) { - my $raw_upstream = shift @ARGV; + + if ($raw_upstream) { my $upstream = rev_parse("${raw_upstream}^{commit}"); push @upstreams, $upstream; } else { @@ -737,7 +762,7 @@ EOF my $toplevel = toplevel_dir(); chdir $toplevel or die "cd to toplevel: $!\n"; - my $hunks = diff_hunks($num_context_lines); + my $hunks = diff_hunks($num_context_lines, @files); my $summary_for = summary_for_commits(@upstreams); my $alias_for = sha_aliases($summary_for); my $grafts_file = create_grafts_file(@upstreams); @@ -789,7 +814,7 @@ App::Git::Autofixup - create fixup commits for topic branches =head1 SYNOPSIS - git-autofixup [] [] + git-autofixup [] [] [--] [...] =head1 DESCRIPTION @@ -797,6 +822,8 @@ F parses hunks of changes in the working directory out of C, then F will only consider staged hunks when trying to create fixup commits. A temporary index is used to create any resulting commits. +If one or more paths are specified, only changes to those files will be considered. The C<--> separator between the revision and paths is optional. + By default a hunk will be included in a fixup commit if all the lines in the hunk's context blamed on topic branch commits refer to the same commit, so there's no ambiguity about which commit the hunk corresponds to. If there is ambiguity the assignment behaviour used under C<--strict 1> will be used to attempt to resolve it. If C<--strict 1> is given the same topic branch commit must be blamed for every removed line and at least one of the lines adjacent to each added line, and added lines must not be adjacent to lines blamed on other topic branch commits. All the same restrictions apply when C<--strict 2> is given, but each added line must be surrounded by lines blamed on the same topic branch commit. For example, the added line in the hunk below is adjacent to lines committed by commits C<99f370af> and C. If these are both topic branch commits then it's ambiguous which commit the added line is fixing up and the hunk will be ignored. diff --git a/lib/App/Git/Autofixup.pm b/lib/App/Git/Autofixup.pm index cd79895..1610601 100644 --- a/lib/App/Git/Autofixup.pm +++ b/lib/App/Git/Autofixup.pm @@ -2,7 +2,7 @@ package App::Git::Autofixup; use strict; use warnings FATAL => 'all'; -our $VERSION = 0.004007; +our $VERSION = 0.004008; =head1 NAME diff --git a/t/autofixup.t b/t/autofixup.t index ab72819..e9f26c1 100755 --- a/t/autofixup.t +++ b/t/autofixup.t @@ -9,7 +9,7 @@ require './t/test.pl'; require './t/util.pl'; Util::check_test_deps(); -plan tests => 44; +plan tests => 49; Test::autofixup_strict( name => "single-line change gets autofixed", @@ -615,3 +615,114 @@ index da0f8ed..c1827f0 100644 EOF ); } + +Test::autofixup( + name => 'file filtering with -- separator (single file)', + topic_commits => [{a => "a1\n", b => "b1\n"}], + unstaged => {a => "a2\n", b => "b2\n"}, + autofixup_args => ['--', 'a'], + exit_code => 0, + log_want => <<'EOF' +fixup! commit0 + +diff --git a a +index da0f8ed..c1827f0 100644 +--- a ++++ a +@@ -1 +1 @@ +-a1 ++a2 +EOF +); + +Test::autofixup( + name => 'file filtering with -- separator (multiple files)', + topic_commits => [{a => "a1\n", b => "b1\n", c => "c1\n"}], + unstaged => {a => "a2\n", b => "b2\n", c => "c2\n"}, + autofixup_args => ['--', 'a', 'b'], + exit_code => 0, + log_want => <<'EOF' +fixup! commit0 + +diff --git a a +index da0f8ed..c1827f0 100644 +--- a ++++ a +@@ -1 +1 @@ +-a1 ++a2 +diff --git b b +index c9c6af7..e6bfff5 100644 +--- b ++++ b +@@ -1 +1 @@ +-b1 ++b2 +EOF +); + +Test::autofixup( + name => 'file filtering excludes unspecified files', + topic_commits => [{a => "a1\n", b => "b1\n"}], + unstaged => {a => "a2\n", b => "b2\n"}, + autofixup_args => ['--', 'a'], + exit_code => 0, + log_want => <<'EOF', +fixup! commit0 + +diff --git a a +index da0f8ed..c1827f0 100644 +--- a ++++ a +@@ -1 +1 @@ +-a1 ++a2 +EOF + unstaged_want => <<'EOF' +diff --git b b +index c9c6af7..e6bfff5 100644 +--- b ++++ b +@@ -1 +1 @@ +-b1 ++b2 +EOF +); + +Test::autofixup( + name => 'file filtering without the optional -- separator', + topic_commits => [{a => "a1\n", b => "b1\n"}], + unstaged => {a => "a2\n", b => "b2\n"}, + autofixup_args => ['a'], + exit_code => 0, + log_want => <<'EOF' +fixup! commit0 + +diff --git a a +index da0f8ed..c1827f0 100644 +--- a ++++ a +@@ -1 +1 @@ +-a1 ++a2 +EOF +); + +Test::autofixup( + name => 'file filtering with -- at start (no revision)', + topic_commits => [{a => "a1\n", b => "b1\n"}], + unstaged => {a => "a2\n", b => "b2\n"}, + autofixup_args => ['--', 'a'], + exit_code => 0, + log_want => <<'EXPECTED' +fixup! commit0 + +diff --git a a +index da0f8ed..c1827f0 100644 +--- a ++++ a +@@ -1 +1 @@ +-a1 ++a2 +EXPECTED +); diff --git a/t/filepath_selection.t b/t/filepath_selection.t new file mode 100644 index 0000000..df7b92e --- /dev/null +++ b/t/filepath_selection.t @@ -0,0 +1,228 @@ +#!/usr/bin/perl +use strict; +use warnings FATAL => 'all'; + +use English qw(-no_match_vars); +use Test::More; + +require './t/test.pl'; +require './t/util.pl'; +require './t/repo.pl'; +require './git-autofixup'; + +Util::check_test_deps(); +plan tests => 4; + +# Test Case 1: -- present with --strict 2 and an explicit revision +# Command line: git-autofixup --strict 2 -- a +{ + my $name = '-- present, --strict 2, with explicit revision'; + + my $wants = { + fixup_log => <<'EOF', +fixup! commit1 + +diff --git a a +index 76642d4..2cdcdb0 100644 +--- a ++++ a +@@ -1,2 +1,3 @@ + a1 ++a2 + a3 +EOF + staged => '', + unstaged => <<'EOF', # File b changes should remain unstaged +diff --git b b +index c9c6af7..9b89cd5 100644 +--- b ++++ b +@@ -1 +1,2 @@ + b1 ++b2 +EOF + }; + + eval { + my $repo = Repo->new(); + + $repo->create_commits({README => "upstream\n"}); + my $upstream = $repo->current_commit_sha(); + + $repo->create_commits({a => "a1\na3\n", b => "b1\n"}); + my $topic = $repo->current_commit_sha(); + + $repo->write_change({a => "a1\na2\na3\n", b => "b1\nb2\n"}); + + my $exit_code = $repo->autofixup('--exit-code', '-s', '2', $upstream, '--', 'a'); + my $ok = Util::exit_code_ok(want => 0, got => $exit_code); + $ok &&= Util::repo_state_ok($repo, $topic, $wants); + + ok($ok, $name); + }; + if ($@) { + diag($@); + fail($name); + } +} + +# Test Case 2: -- present with --strict 2 without an explicit revision +# Command line: git-autofixup --strict 2 -- a +{ + my $name = '-- present, --strict 2, no explicit revision'; + + my $wants = { + fixup_log => <<'EOF', +fixup! commit1 + +diff --git a a +index 76642d4..2cdcdb0 100644 +--- a ++++ a +@@ -1,2 +1,3 @@ + a1 ++a2 + a3 +EOF + staged => '', + unstaged => <<'EOF', # File b changes should remain unstaged +diff --git b b +index c9c6af7..9b89cd5 100644 +--- b ++++ b +@@ -1 +1,2 @@ + b1 ++b2 +EOF + }; + + eval { + my $repo = Repo->new(); + + $repo->create_commits({README => "upstream\n"}); + my $upstream = $repo->current_commit_sha(); + + $repo->switch_to_downstream_branch('topic'); + $repo->create_commits({a => "a1\na3\n", b => "b1\n"}); + my $topic = $repo->current_commit_sha(); + + $repo->write_change({a => "a1\na2\na3\n", b => "b1\nb2\n"}); + + # No explicit revision - should find it via tracking branch + my $exit_code = $repo->autofixup('--exit-code', '-s', '2', '--', 'a'); + my $ok = Util::exit_code_ok(want => 0, got => $exit_code); + $ok &&= Util::repo_state_ok($repo, $topic, $wants); + + ok($ok, $name); + }; + if ($@) { + diag($@); + fail($name); + } +} + +# Test Case 3: no -- with --strict 2 and an explicit revision +# Command line: git-autofixup --strict 2 +{ + my $name = 'no --, --strict 2, with explicit revision'; + + my $wants = { + fixup_log => <<'EOF', +fixup! commit1 + +diff --git a a +index 76642d4..2cdcdb0 100644 +--- a ++++ a +@@ -1,2 +1,3 @@ + a1 ++a2 + a3 +diff --git b b +index c9c6af7..9b89cd5 100644 +--- b ++++ b +@@ -1 +1,2 @@ + b1 ++b2 +EOF + staged => '', + unstaged => '', + }; + + eval { + my $repo = Repo->new(); + + $repo->create_commits({README => "upstream\n"}); + my $upstream = $repo->current_commit_sha(); + + $repo->create_commits({a => "a1\na3\n", b => "b1\n"}); + my $topic = $repo->current_commit_sha(); + + $repo->write_change({a => "a1\na2\na3\n", b => "b1\nb2\n"}); + + my $exit_code = $repo->autofixup('--exit-code', '-s', '2', $upstream); + my $ok = Util::exit_code_ok(want => 0, got => $exit_code); + $ok &&= Util::repo_state_ok($repo, $topic, $wants); + + ok($ok, $name); + }; + if ($@) { + diag($@); + fail($name); + } +} + +# Test Case 4: no -- with --strict 2 without an explicit revision +# Command line: git-autofixup --strict 2 +{ + my $name = 'no --, --strict 2, no explicit revision'; + + my $wants = { + fixup_log => <<'EOF', +fixup! commit1 + +diff --git a a +index 76642d4..2cdcdb0 100644 +--- a ++++ a +@@ -1,2 +1,3 @@ + a1 ++a2 + a3 +diff --git b b +index c9c6af7..9b89cd5 100644 +--- b ++++ b +@@ -1 +1,2 @@ + b1 ++b2 +EOF + staged => '', + unstaged => '', + }; + + eval { + my $repo = Repo->new(); + + $repo->create_commits({README => "upstream\n"}); + my $upstream = $repo->current_commit_sha(); + + $repo->switch_to_downstream_branch('topic'); + $repo->create_commits({a => "a1\na3\n", b => "b1\n"}); + my $topic = $repo->current_commit_sha(); + + $repo->write_change({a => "a1\na2\na3\n", b => "b1\nb2\n"}); + + # No explicit revision - should find it via tracking branch + my $exit_code = $repo->autofixup('--exit-code', '-s', '2'); + my $ok = Util::exit_code_ok(want => 0, got => $exit_code); + $ok &&= Util::repo_state_ok($repo, $topic, $wants); + + ok($ok, $name); + }; + if ($@) { + diag($@); + fail($name); + } +} diff --git a/t/test.pl b/t/test.pl index 614cee4..2ad34cc 100644 --- a/t/test.pl +++ b/t/test.pl @@ -42,7 +42,8 @@ sub autofixup_strict { # log_want: expected log output for new fixup commited # staged_want: expected log output for the staging area # unstaged_want: expected diff output for the working tree -# autofixup_opts: command-line options to pass thru to autofixup +# autofixup_opts: command-line options to pass thru to autofixup (before revision) +# autofixup_args: additional arguments to pass after revision (i.e. file paths) # git_config: hashref of git config key/value pairs # # The upstream_commits and topic_commits arguments are heterogeneous lists of @@ -64,6 +65,7 @@ sub autofixup { my $unstaged_want = $args{unstaged_want}; my $exit_code_want = $args{exit_code}; my $autofixup_opts = $args{autofixup_opts} || []; + my $autofixup_args = $args{autofixup_args} || []; push @{$autofixup_opts}, '--exit-code'; my $git_config = $args{git_config} || {}; if (!$upstream_commits && !$topic_commits) { @@ -99,7 +101,7 @@ sub autofixup { $repo->write_change($unstaged); Util::run('git', '--no-pager', 'log', "--format='%h %s'", "${upstream_rev}.."); - my $exit_code_got = $repo->autofixup(@{$autofixup_opts}, $upstream_rev); + my $exit_code_got = $repo->autofixup(@{$autofixup_opts}, $upstream_rev, @{$autofixup_args}); my $ok = Util::exit_code_ok(want => $exit_code_want, got => $exit_code_got); my $wants = {