Skip to content

Commit

Permalink
Speed up creation of temporary index when autofixing staged changes
Browse files Browse the repository at this point in the history
Instead of using "git read-tree" to create an entirely new index,
start by copying the old index file, and then subtracting the changes
made since HEAD.

This greatly improves performance on a monorepo of 30k files,
where my .git/index file is around 5MB. A run of git-autofixup with
a single staged line and some 100 commits in the topic branch goes
from 6 seconds to merely 0.5.

Take care to support the case where a user has set their own
GIT_INDEX_FILE. This is a bit awkward because we apply it's default
value ($GIT_DIR/index) in the callee but I couldn't make it work in
the caller.

In a previous attempt I tried to remove the temporary index, see
krobelus@optimize-staged-changes
but that approach made it hard to reliably preserve user data in
failure scenarios - a little copying is much safer.
  • Loading branch information
krobelus authored and torbiak committed Aug 18, 2023
1 parent f6fc16e commit e2583f1
Showing 1 changed file with 20 additions and 5 deletions.
25 changes: 20 additions & 5 deletions git-autofixup
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use warnings FATAL => 'all';
use Carp qw(croak);
use Pod::Usage;
use Getopt::Long qw(:config bundling);
use File::Copy;
use File::Temp;
use File::Spec ();

Expand Down Expand Up @@ -469,14 +470,27 @@ sub exit_code {
# Create a temporary index so we can craft commits with already-staged hunks.
# Return a File::Temp object so the caller has control over its lifetime.
sub create_temp_index {
my $old_index = shift;
my $tempfile = File::Temp->new(
TEMPLATE => 'git-autofixup_index.XXXXXX',
DIR => File::Spec->tmpdir());
# The index ought to be equivalent to HEAD. The fastest way to create it
# is to start with the current index, and subtract the changes since HEAD.
if (not defined($old_index)) {
my $gitdir = qx(git rev-parse --git-dir) or die "git rev-parse: $!\n";
chomp $gitdir;
$old_index = "$gitdir/index";
}
copy($old_index, $tempfile->filename()) or die "Can't copy Git index '$old_index' to '$tempfile': $!\n";

$ENV{GIT_INDEX_FILE} = $tempfile->filename();
# A blank index makes it look like we're deleting everything, so read
# HEAD's tree into it.
qx(git read-tree HEAD^{tree});
$? == 0 or die "Can't read HEAD's tree into temp index.\n";

# Remove any staged changes from the new index - we want to turn them into fixup commits.
my $index_changes = qx(git diff-index --patch --no-ext-diff --ignore-submodules --cached HEAD);
open my $fh, '|-', git_cmd(qw(apply --cached --whitespace=nowarn --reverse -)) or die "git apply: $!\n";
print $fh $index_changes;
close $fh or die "git apply: non-zero exit code\n";

return $tempfile;
}

Expand Down Expand Up @@ -569,10 +583,11 @@ sub main {
return 0;
}

my $old_index = $ENV{GIT_INDEX_FILE};
local $ENV{GIT_INDEX_FILE}; # Throw away changes between main() calls.
if (is_index_dirty()) {
# Limit the tempfile's lifetime to the execution of main().
my $tempfile = create_temp_index();
my $tempfile = create_temp_index($old_index);
}

for my $sha (@ordered_shas) {
Expand Down

0 comments on commit e2583f1

Please sign in to comment.