Skip to content

Commit

Permalink
Choose upstream revisions automatically
Browse files Browse the repository at this point in the history
If an upstream revision isn't given on the command-line, try to find
suitable ones automatically. This only works if the current branch has a
tracking branch.

Writing tests for this feature required refactoring the existing test
helpers significantly, and it made sense to put things in different
files and use packages other than main to make it obvious what's being
called. The stub module has to use the package that the module is
published as on CPAN, and we want to put the version in the script
(git-autofixup) as well and compare the two, so the script needs to be a
different package, which will only be used internally.

Closes #27
  • Loading branch information
torbiak committed Sep 9, 2023
1 parent 6b0e81f commit 48e153a
Show file tree
Hide file tree
Showing 10 changed files with 851 additions and 308 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
MYMETA.*
App-Git-Autofixup-*.tar.gz
Makefile
/todo
3 changes: 3 additions & 0 deletions MANIFEST
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ Makefile.PL
MANIFEST This list of files
README.pod
t/autofixup.t
t/implicit_upstream.t
t/repo.pl
t/util.pl
3 changes: 3 additions & 0 deletions MANIFEST.SKIP
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,6 @@
MANIFEST\.SKIP
^xt
\.sw[pmno]$
^\.github
^release$
^todo$
6 changes: 2 additions & 4 deletions README.pod
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ App::Git::Autofixup - create fixup commits for topic branches

=head1 SYNOPSIS

git-autofixup [<options>] <revision>
git-autofixup [<options>] [<revision>]

=head1 DESCRIPTION

F<git-autofixup> parses hunks of changes in the working directory out of C<git diff> output and uses C<git blame> to assign those hunks to commits in C<E<lt>revisionE<gt>..HEAD>, which will typically represent a topic branch, and then creates fixup commits to be used with C<git rebase --interactive --autosquash>. It is assumed that hunks near changes that were previously committed to the topic branch are related.

C<@{upstream}> or C<@{u}> is likely a convenient value to use for C<E<lt>revisionE<gt>> if the current branch has a tracking branch. See C<git help revisions> for other ways to specify revisions.
F<git-autofixup> parses hunks of changes in the working directory out of C<git diff> output and uses C<git blame> to assign those hunks to commits in C<E<lt>revisionE<gt>..HEAD>, which will typically represent a topic branch, and then creates fixup commits to be used with C<git rebase --interactive --autosquash>. It is assumed that hunks near changes that were previously committed to the topic branch are related. If no revision is given and the current branch has a tracking branch, then C<@{upstream}> is used to find reasonable fork-points or merge-bases to use as upstream cutoffs. See C<git help revisions> for info about how to specify revisions.

If any changes have been staged to the index using C<git add>, then F<git-autofixup> will only consider staged hunks when trying to create fixup commits. A temporary index is used to create any resulting commits.

Expand Down
99 changes: 82 additions & 17 deletions git-autofixup
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/perl
package main;
package Autofixup;
use 5.008004;
use strict;
use warnings FATAL => 'all';
Expand All @@ -20,7 +20,7 @@ my @GIT_OPTIONS;
my ($CONTEXT, $ADJACENT, $SURROUNDED) = (0..10);

my $usage =<<'END';
usage: git-autofixup [<options>] <revision>
usage: git-autofixup [<options>] [<revision>]
-h show usage
--help show manpage
Expand Down Expand Up @@ -119,10 +119,67 @@ sub git_cmd {
return ('git', @GIT_OPTIONS, @_);
}

# With a linear git history there'll be a single merge base that's easy to
# refer to with @{upstream}, but during an interactive rebase we need to get
# the "current" branch from the rebase metadata.
#
# Unusual cases:
#
# While there can be multiple merge bases if there have been criss-cross
# merges, there'll still be a single fork point unless the relevant reflog
# entries have already been garbage-collected.
#
# When multiple upstreams are configured via `branch.<name>.merge` in git's
# config the most correct approach is probably to find the fork-point for each
# merge value and return those. But it seems unlikely that someone is doing
# octopus merges and using git-autofixup, so we're not handling that specially
# currently.
sub find_merge_bases {
my $upstream = '@{upstream}';

# If an interactive rebase is in progress, derive the upstream from the
# rebase meatadata.
my $gitdir = qx(git rev-parse --git-dir) or die "git rev-parse: $!\n";
chomp $gitdir;
if (-e "$gitdir/rebase-merge") {
my $branch = slurp("$gitdir/rebase-merge/head-name");
chomp $branch;
$branch =~ s#^refs/heads/##;
$upstream = "$branch\@{upstream}";
}

# `git merge-base` will fail if there's no tracking branch. In that case
# redirect stderr and communicate failure by returning an empty list. Also,
# with the --fork-point option, no merge bases are returned if the relevant
# reflog entries have been GC'd, so fall back to normal merge-bases.
my @merge_bases;
for (qx(git merge-base --all --fork-point $upstream HEAD 2>/dev/null)) {
chomp;
push @merge_bases, $_;
}
if (!@merge_bases) {
for (qx(git merge-base --all $upstream HEAD 2>/dev/null)) {
chomp;
push @merge_bases, $_;
}
}

return wantarray ? @merge_bases : \@merge_bases;
}

sub slurp {
my $filename = shift;
open my $fh, '<', $filename or die "slurp $filename: $!";
local $/;
my $content = readline $fh;
return $content;
}

sub summary_for_commits {
my $rev = shift;
my @upstreams = @_;
my %commits;
for (qx(git log --no-merges --format=%H:%s $rev..)) {
my $negative = join(" ", map {"^$_"} @upstreams);
for (qx(git log --no-merges --format=%H:%s HEAD $negative)) {
chomp;
my ($sha, $msg) = split ':', $_, 2;
$commits{$sha} = $msg;
Expand Down Expand Up @@ -524,13 +581,14 @@ sub create_temp_index {
# commit doesn't have any parents, resulting in blame only searching back as
# far back as the upstream commit.
sub create_grafts_file {
my $upstream = shift;
my @upstreams = @_;
my $grafts_file = File::Temp->new(
TEMPLATE => 'git-autofixup_grafts.XXXXXX',
DIR => File::Spec->tmpdir());
my $merge_base = qx(git merge-base $upstream HEAD) or return "/dev/null";
open(my $fh, '>', $grafts_file) or die "Can't open $grafts_file: $!\n";
print $fh $merge_base, "\n";
for (@upstreams) {
print $fh $_, "\n";
}
close($fh) or die "Error closing grafts file: $!\n";
return $grafts_file;
}
Expand Down Expand Up @@ -568,10 +626,19 @@ sub main {
pod2usage(-exitval => 0, -verbose => 2);
}

@ARGV == 1 or die "No upstream commit given.\n";
my $upstream = shift @ARGV;
qx(git rev-parse --verify ${upstream}^{commit});
$? == 0 or die "Can't resolve given commit.\n";
# "upstream" revisions as 40 byte SHA1 hex hashes.
my @upstreams = ();
if (@ARGV == 1) {
my $raw_upstream = shift @ARGV;
my $upstream = qx(git rev-parse --verify --end-of-options ${raw_upstream}^{commit})
or die "Can't resolve given commit.\n";
push @upstreams, $upstream;
} else {
@upstreams = find_merge_bases();
if (!@upstreams) {
die "Can't find tracking branch. Please specify a revision.\n";
}
}

if ($num_context_lines < 0) {
die "invalid number of context lines: $num_context_lines\n";
Expand All @@ -590,9 +657,9 @@ sub main {
chdir $toplevel or die $!;

my $hunks = diff_hunks($num_context_lines);
my $summary_for = summary_for_commits($upstream);
my $summary_for = summary_for_commits(@upstreams);
my $alias_for = sha_aliases($summary_for);
my $grafts_file = create_grafts_file($upstream);
my $grafts_file = create_grafts_file(@upstreams);
my %blame_for = map {$_ => blame($_, $alias_for, $grafts_file)} @{$hunks};
my $hunks_for = fixup_hunks_by_sha({
hunks => $hunks,
Expand Down Expand Up @@ -641,13 +708,11 @@ App::Git::Autofixup - create fixup commits for topic branches
=head1 SYNOPSIS
git-autofixup [<options>] <revision>
git-autofixup [<options>] [<revision>]
=head1 DESCRIPTION
F<git-autofixup> parses hunks of changes in the working directory out of C<git diff> output and uses C<git blame> to assign those hunks to commits in C<E<lt>revisionE<gt>..HEAD>, which will typically represent a topic branch, and then creates fixup commits to be used with C<git rebase --interactive --autosquash>. It is assumed that hunks near changes that were previously committed to the topic branch are related.
C<@{upstream}> or C<@{u}> is likely a convenient value to use for C<E<lt>revisionE<gt>> if the current branch has a tracking branch. See C<git help revisions> for other ways to specify revisions.
F<git-autofixup> parses hunks of changes in the working directory out of C<git diff> output and uses C<git blame> to assign those hunks to commits in C<E<lt>revisionE<gt>..HEAD>, which will typically represent a topic branch, and then creates fixup commits to be used with C<git rebase --interactive --autosquash>. It is assumed that hunks near changes that were previously committed to the topic branch are related. If no revision is given and the current branch has a tracking branch, then C<@{upstream}> is used to find reasonable fork-points or merge-bases to use as upstream cutoffs. See C<git help revisions> for info about how to specify revisions.
If any changes have been staged to the index using C<git add>, then F<git-autofixup> will only consider staged hunks when trying to create fixup commits. A temporary index is used to create any resulting commits.
Expand Down
Loading

0 comments on commit 48e153a

Please sign in to comment.