pax_global_header00006660000000000000000000000064137574056660014535gustar00rootroot0000000000000052 comment=206e282c1713b31293094b534d94088210711294 git-autofixup-0.003001/000077500000000000000000000000001375740566600145655ustar00rootroot00000000000000git-autofixup-0.003001/.gitignore000066400000000000000000000000521375740566600165520ustar00rootroot00000000000000*.bak MYMETA.* App-Git-Autofixup-*.tar.gz git-autofixup-0.003001/Changes000066400000000000000000000042111375740566600160560ustar00rootroot00000000000000# 0.003001 - Fix bug where the index would be left out-of-sync with `HEAD` after autofixing unstaged hunks due to a temporary index being used. If you're running v0.003000 and hit this, `git restore --staged` can be used to read the new `HEAD`'s tree into the index. Thanks to Johannes Altmanninger for finding and fixing this. # 0.003000 The most important change to the interface is that now, if there are any hunks staged in the index, only those hunks will be considered for assigning to fixup commits. A temporary git index is used to make any created fixup commits, so any staged hunks that don't get assigned will remain staged. Thanks to Jonas Bernoulli and Max Odnoletkov for their help with this. - Add --gitopt to allow working around git settings issues - Add --exit-code option, which gives more granular status about what subset of hunks were assigned to commits Bug fixes: - Fix diff commands so that the diff.noprefix, diff.mnemonicPrefix, and diff.external settings don't result in us getting unexpected input. Thanks to Paolo Giarrusso and Ryan Campbell for help with this. - Fix bug where multiple hunks assigned to the same commit would result in copies of the same fixup commit, resulting in "patch does not apply" errors. Thanks to Johannes Altmanninger for identifying and fixing this. # 0.002007 - Fix hunk parsing for files with multiple hunks (broken in 0.002006) - Create fixup commits in a consistent order # 0.002006 - Improve docs for the --strict option - Fix hunk parsing for files without a newline at EOF # 0.002005 - Fix running from repo subdirectories - Fix docs for invoking as "git autofixup" # 0.002004 - Fix angle brackets in POD # 0.002003 - Check git version when running tests # 0.002002 - Fix tests when git user.name and email aren't configured. # 0.002001 - Make compatible with perl 5.8.4. Previously 5.8.9 was required. # 0.002000 - Better descriptions of hunk handling now printed to stdout. - Use --strict=1 behaviour as a fallback when --strict=0. - Fix blamed line number and left side output for runs of added lines # 0.001002 - Make compatible with perl5.8 - --help now shows the manpage git-autofixup-0.003001/LICENSE000066400000000000000000000215231375740566600155750ustar00rootroot00000000000000This software is Copyright (c) 2017 by Jordan Torbiak. This is free software, licensed under: The Artistic License 2.0 (GPL Compatible) The Artistic License 2.0 Copyright (c) 2000-2006, The Perl Foundation. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble This license establishes the terms under which a given free software Package may be copied, modified, distributed, and/or redistributed. The intent is that the Copyright Holder maintains some artistic control over the development of that Package while still keeping the Package available as open source and free software. You are always permitted to make arrangements wholly outside of this license directly with the Copyright Holder of a given Package. If the terms of this license do not permit the full use that you propose to make of the Package, you should contact the Copyright Holder and seek a different licensing arrangement. Definitions "Copyright Holder" means the individual(s) or organization(s) named in the copyright notice for the entire Package. "Contributor" means any party that has contributed code or other material to the Package, in accordance with the Copyright Holder's procedures. "You" and "your" means any person who would like to copy, distribute, or modify the Package. "Package" means the collection of files distributed by the Copyright Holder, and derivatives of that collection and/or of those files. A given Package may consist of either the Standard Version, or a Modified Version. "Distribute" means providing a copy of the Package or making it accessible to anyone else, or in the case of a company or organization, to others outside of your company or organization. "Distributor Fee" means any fee that you charge for Distributing this Package or providing support for this Package to another party. It does not mean licensing fees. "Standard Version" refers to the Package if it has not been modified, or has been modified only in ways explicitly requested by the Copyright Holder. "Modified Version" means the Package, if it has been changed, and such changes were not explicitly requested by the Copyright Holder. "Original License" means this Artistic License as Distributed with the Standard Version of the Package, in its current version or as it may be modified by The Perl Foundation in the future. "Source" form means the source code, documentation source, and configuration files for the Package. "Compiled" form means the compiled bytecode, object code, binary, or any other form resulting from mechanical transformation or translation of the Source form. Permission for Use and Modification Without Distribution (1) You are permitted to use the Standard Version and create and use Modified Versions for any purpose without restriction, provided that you do not Distribute the Modified Version. Permissions for Redistribution of the Standard Version (2) You may Distribute verbatim copies of the Source form of the Standard Version of this Package in any medium without restriction, either gratis or for a Distributor Fee, provided that you duplicate all of the original copyright notices and associated disclaimers. At your discretion, such verbatim copies may or may not include a Compiled form of the Package. (3) You may apply any bug fixes, portability changes, and other modifications made available from the Copyright Holder. The resulting Package will still be considered the Standard Version, and as such will be subject to the Original License. Distribution of Modified Versions of the Package as Source (4) You may Distribute your Modified Version as Source (either gratis or for a Distributor Fee, and with or without a Compiled form of the Modified Version) provided that you clearly document how it differs from the Standard Version, including, but not limited to, documenting any non-standard features, executables, or modules, and provided that you do at least ONE of the following: (a) make the Modified Version available to the Copyright Holder of the Standard Version, under the Original License, so that the Copyright Holder may include your modifications in the Standard Version. (b) ensure that installation of your Modified Version does not prevent the user installing or running the Standard Version. In addition, the Modified Version must bear a name that is different from the name of the Standard Version. (c) allow anyone who receives a copy of the Modified Version to make the Source form of the Modified Version available to others under (i) the Original License or (ii) a license that permits the licensee to freely copy, modify and redistribute the Modified Version using the same licensing terms that apply to the copy that the licensee received, and requires that the Source form of the Modified Version, and of any works derived from it, be made freely available in that license fees are prohibited but Distributor Fees are allowed. Distribution of Compiled Forms of the Standard Version or Modified Versions without the Source (5) You may Distribute Compiled forms of the Standard Version without the Source, provided that you include complete instructions on how to get the Source of the Standard Version. Such instructions must be valid at the time of your distribution. If these instructions, at any time while you are carrying out such distribution, become invalid, you must provide new instructions on demand or cease further distribution. If you provide valid instructions or cease distribution within thirty days after you become aware that the instructions are invalid, then you do not forfeit any of your rights under this license. (6) You may Distribute a Modified Version in Compiled form without the Source, provided that you comply with Section 4 with respect to the Source of the Modified Version. Aggregating or Linking the Package (7) You may aggregate the Package (either the Standard Version or Modified Version) with other packages and Distribute the resulting aggregation provided that you do not charge a licensing fee for the Package. Distributor Fees are permitted, and licensing fees for other components in the aggregation are permitted. The terms of this license apply to the use and Distribution of the Standard or Modified Versions as included in the aggregation. (8) You are permitted to link Modified and Standard Versions with other works, to embed the Package in a larger work of your own, or to build stand-alone binary or bytecode versions of applications that include the Package, and Distribute the result without restriction, provided the result does not expose a direct interface to the Package. Items That are Not Considered Part of a Modified Version (9) Works (including, but not limited to, modules and scripts) that merely extend or make use of the Package, do not, by themselves, cause the Package to be a Modified Version. In addition, such works are not considered parts of the Package itself, and are not subject to the terms of this license. General Provisions (10) Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License. By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license. (11) If your Modified Version has been derived from a Modified Version made by someone other than you, you are nevertheless required to ensure that your Modified Version complies with the requirements of this license. (12) This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder. (13) This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement, then this Artistic License to you shall terminate on the date that such litigation is filed. (14) Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. git-autofixup-0.003001/MANIFEST000066400000000000000000000001721375740566600157160ustar00rootroot00000000000000Changes git-autofixup lib/App/Git/Autofixup.pm LICENSE Makefile.PL MANIFEST This list of files README.pod t/autofixup.t git-autofixup-0.003001/MANIFEST.SKIP000066400000000000000000000022011375740566600164560ustar00rootroot00000000000000#!start included /usr/share/perl5/core_perl/ExtUtils/MANIFEST.SKIP # Avoid version control files. \bRCS\b \bCVS\b \bSCCS\b ,v$ \B\.svn\b \B\.git\b \B\.gitignore\b \b_darcs\b \B\.cvsignore$ # Avoid VMS specific MakeMaker generated files \bDescrip.MMS$ \bDESCRIP.MMS$ \bdescrip.mms$ # Avoid Makemaker generated and utility files. \bMANIFEST\.bak \bMakefile$ \bblib/ \bMakeMaker-\d \bpm_to_blib\.ts$ \bpm_to_blib$ \bblibdirs\.ts$ # 6.18 through 6.25 generated this \b_eumm/ # 7.05_05 and above # Avoid Module::Build generated and utility files. \bBuild$ \b_build/ \bBuild.bat$ \bBuild.COM$ \bBUILD.COM$ \bbuild.com$ # and Module::Build::Tiny generated files \b_build_params$ # Avoid temp and backup files. ~$ \.old$ \#$ \b\.# \.bak$ \.tmp$ \.# \.rej$ \..*\.sw.?$ # Avoid OS-specific files/dirs # Mac OSX metadata \B\.DS_Store # Mac OSX SMB mount metadata files \B\._ # Avoid Devel::Cover and Devel::CoverX::Covered files. \bcover_db\b \bcovered\b # Avoid prove files \B\.prove$ # Avoid MYMETA files ^MYMETA\. #!end included /usr/share/perl5/core_perl/ExtUtils/MANIFEST.SKIP \.tgz$ \.tar\.gz$ MANIFEST\.SKIP ^xt \.sw[pmno]$ git-autofixup-0.003001/Makefile.PL000066400000000000000000000015361375740566600165440ustar00rootroot00000000000000use 5.008004; use ExtUtils::MakeMaker; WriteMakefile( NAME => 'App::Git::Autofixup', VERSION_FROM => 'lib/App/Git/Autofixup.pm', ABSTRACT_FROM => 'lib/App/Git/Autofixup.pm', AUTHOR => 'Jordan Torbiak', LICENSE => 'artistic_2', MIN_PERL_VERSION => '5.008004', EXE_FILES => ['git-autofixup'], (eval { ExtUtils::MakeMaker->VERSION(6.46) } ? (META_MERGE => { 'meta-spec' => { version => 2 }, resources => { repository => { type => 'git', url => 'https://github.com/torbiak/git-autofixup.git', web => 'https://github.com/torbiak/git-autofixup', }, bugtracker => { web => 'https://github.com/torbiak/git-autofixup/issues' }, }}) : () ), ); git-autofixup-0.003001/README.pod000066400000000000000000000136221375740566600162320ustar00rootroot00000000000000=pod =head1 NAME App::Git::Autofixup - create fixup commits for topic branches =head1 SYNOPSIS git-autofixup [] =head1 DESCRIPTION F parses hunks of changes in the working directory out of C output and uses C to assign those hunks to commits in CrevisionE..HEAD>, which will typically represent a topic branch, and then creates fixup commits to be used with C. 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 CrevisionE> if the current branch has a tracking branch. See C for other ways to specify revisions. If any changes have been staged to the index using C, then F will only consider staged hunks when trying to create fixup commits. A temporary index is used to create any resulting commits. 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. COMMIT |LINE|HEAD |WORKING DIRECTORY 99f370af| 1|first line | first line | | |+added line a1eadbe2| 2|second line | second line But if that second line were instead blamed on an upstream commit (denoted by C<^>), the hunk would be added to a fixup commit for C<99f370af>: 99f370af| 1|first line | first line | | |+added line ^ | 2|second line | second line Output similar to this example can be generated by setting verbosity to 2 or greater by using the verbosity option multiple times, eg. C, and can be helpful in determining how a hunk will be handled. F is not to be used mindlessly. Always inspect the created fixup commits to ensure hunks have been assigned correctly, especially when used on a working directory that has been changed with a mix of fixups and new work. =head1 OPTIONS =over =item -h Show usage. =item --help Show manpage. =item --version Show version. =item -v, --verbose Increase verbosity. Can be used up to two times. =item -c N, --context N Change the number of context lines C uses around hunks. Default: 3. This can change how hunks are assigned to fixup commits, especially with C<--strict 0>. =item -s N, --strict N Set how strict F is about assigning hunks to fixup commits. Default: 0. Strictness levels are described under DESCRIPTION. =item -g ARG, --gitopt ARG Specify option for git. Can be used multiple times. Useful for testing, to override config options that break git-autofixup, or to override global diff options to tweak what git-autofixup considers a hunk. Note ARG won't be wordsplit, so to give multiple arguments, such as for setting a config option like C<-c diff.algorithm>, this option must be used multiple times: C<-g -c -g diff.algorithm=patience>. =item -e, --exit-code Use more detailed exit codes: =over =item 0: All hunks have been assigned. =item 1: Only some hunks have been assigned. =item 2: No hunks have been assigned. =item 3: There was nothing to be assigned. =item 255: Unexpected error occurred. =back =back =head1 INSTALLATION If cpan is available, run C. Otherwise, copy F to a directory in C and ensure it has execute permissions. It can then be invoked as either C or C, since git searches C for appropriately named binaries. Git is distributed with Perl 5 for platforms not expected to already have it installed, but installing modules with cpan requires other tools that might not be available, such as make. This script has no dependencies outside of the standard library, so it is hoped that it works on any platform that Git does without much trouble. Requires a git supporting C: 1.7.4 or later. =head1 BUGS/LIMITATIONS If a topic branch adds some lines in one commit and subsequently removes some of them in another, a hunk in the working directory that re-adds those lines will be assigned to fixup the first commit, and during rebasing they'll be removed again by the later commit. Not tested in F on Windows. Run it from Git Bash, Cygwin, or a similar Unix emulation environment. =head1 ACKNOWLEDGEMENTS F was inspired by a description of L in the L. While I was working on it I found L, by oktal3700, which was helpful to examine. =head1 COPYRIGHT AND LICENSE Copyright (C) 2017, Jordan Torbiak. This program is free software; you can redistribute it and/or modify it under the terms of the Artistic License v2.0. =cut git-autofixup-0.003001/git-autofixup000077500000000000000000000555521375740566600173340ustar00rootroot00000000000000#!/usr/bin/perl package main; use 5.008004; use strict; use warnings FATAL => 'all'; use Carp qw(croak); use Pod::Usage; use Getopt::Long qw(:config bundling); use File::Temp; use File::Spec (); our $VERSION = 0.003001; # X.YYYZZZ my $VERBOSE; my @GIT_OPTIONS; # Strictness levels. my ($CONTEXT, $ADJACENT, $SURROUNDED) = (0..10); my $usage =<<'END'; usage: git-autofixup [] -h show usage --help show manpage --version show version -v, --verbose increase verbosity (use up to 2 times) -c N, --context N set number of diff context lines (default 3) -e, --exit-code use more detailed exit codes (see --help) -s N, --strict N set strictness (default 0) Assign a hunk to fixup a topic branch commit if: 0: either only one topic branch commit is blamed in the hunk context or blocks of added lines are adjacent to exactly one topic branch commit. Removing upstream lines is allowed for this level. 1: blocks of added lines are adjacent to exactly one topic branch commit 2: blocks of added lines are surrounded by exactly one topic branch commit Regardless of strictness level, removed lines are correlated with the commit they're blamed on, and all the blocks of changed lines in a hunk must be correlated with the same topic branch commit in order to be assigned to it. See the --help for more details. -g ARG, --gitopt ARG Specify option for git. Can be used multiple times. END # Parse hunks out of `git diff` output. Return an array of hunk hashrefs. sub parse_hunks { my $fh = shift; my ($file_a, $file_b); my @hunks; my $line; while ($line = <$fh>) { if ($line =~ /^--- (.*)/) { $file_a = $1; } elsif ($line =~ /^\+\+\+ (.*)/) { $file_b = $1; } elsif ($line =~ /^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@/) { my $header = $line; for ($file_a, $file_b) { s#^[abiwco]/##; } next if $file_a ne $file_b; # Ignore creations and deletions. my $lines = []; while (1) { $line = <$fh>; if (!defined($line) || $line =~ /^[^ +\\-]/) { last; } push @{$lines}, $line; } push(@hunks, { file => $file_a, start => $1, count => defined($2) ? $2 : 1, header => $header, lines => $lines, }); # The next line after a hunk could be a header for the next commit # or hunk. redo if defined $line; } } return @hunks; } sub git_cmd { return ('git', @GIT_OPTIONS, @_); } sub summary_for_commits { my $rev = shift; my %commits; for (qx(git log --no-merges --format=%H:%s $rev..)) { chomp; my ($sha, $msg) = split ':', $_, 2; $commits{$sha} = $msg; } return \%commits; } # Return targets of fixup!/squash! commits. sub sha_aliases { my $summary_for = shift; my %aliases; my @targets = keys(%{$summary_for}); for my $sha (@targets) { my $summary = $summary_for->{$sha}; next if $summary !~ /^(?:fixup|squash)! (.*)/; my $prefix = $1; if ($prefix =~ /^(?:(?:fixup|squash)! ){2}/) { die "fixup commits for fixup commits aren't supported: $sha"; } my @matches = grep {startswith($summary_for->{$_}, $prefix)} @targets; if (@matches > 1) { die "ambiguous fixup commit target: multiple commit summaries start with: $prefix\n"; } elsif (@matches == 0) { die "no fixup target in topic branch: $sha\n"; } elsif (@matches == 1) { $aliases{$sha} = $matches[0]; } } return \%aliases; } sub fixup_sha { my $args = shift; my $hunk = $args->{hunk}; my $blame = $args->{blame}; my $summary_for = $args->{summary_for}; my $strict = $args->{strict}; if (grep {!defined} ($hunk, $blame, $summary_for, $strict)) { croak 'missing argument'; } my @targets; if ($args->{strict} == $CONTEXT) { @targets = fixup_targets_from_all_context($args); my @topic_targets = grep {defined $summary_for->{$_}} @targets; if (@topic_targets > 1) { # The context assignment is ambiguous, but an adjacency assignment # might not be. @targets = fixup_targets_from_adjacent_context($args); } } else { @targets = fixup_targets_from_adjacent_context($args); } my $upstream_is_blamed = grep {!defined $summary_for->{$_}} @targets; my @topic_targets = grep {defined $summary_for->{$_}} @targets; if ($strict && $upstream_is_blamed) { $VERBOSE && print hunk_desc($hunk), " changes lines blamed on upstream\n"; return; } elsif (@topic_targets > 1) { $VERBOSE && print hunk_desc($hunk), " has multiple targets\n"; return; } elsif (@topic_targets == 0) { $VERBOSE && print hunk_desc($hunk), " has no targets\n"; return; } return $topic_targets[0]; } sub hunk_desc { my $hunk = shift; return join " ", ( $hunk->{file}, $hunk->{header} =~ /(@@[^@]*@@)/, ); } sub fixup_targets_from_all_context { my $args = shift; my ($hunk, $blame, $summary_for) = @{$args}{qw(hunk blame summary_for)}; croak 'missing argument' if grep {!defined} ($hunk, $blame, $summary_for); my @targets = uniq(map {$_->{sha}} values(%{$blame})); return wantarray ? @targets : \@targets; } sub uniq { my %seen; return grep {!$seen{$_}++} @_; } sub fixup_targets_from_adjacent_context { my $args = shift; my $hunk = $args->{hunk}; my $blame = $args->{blame}; my $summary_for = $args->{summary_for}; my $strict = $args->{strict}; if (grep {!defined} ($hunk, $blame, $summary_for, $strict)) { croak 'missing argument'; } my $blame_indexes = blame_indexes($hunk); my %blamed; my $diff = $hunk->{lines}; for (my $di = 0; $di < @{$diff}; $di++) { # diff index my $bi = $blame_indexes->[$di]; my $line = $diff->[$di]; if (startswith($line, '-')) { my $sha = $blame->{$bi}{sha}; $blamed{$sha} = 1; } elsif (startswith($line, '+')) { my @lines; if ($di > 0 && defined $blame->{$bi-1}) { push @lines, $bi-1; } if (defined $blame->{$bi}) { push @lines, $bi; } my @adjacent_shas = uniq(map {$_->{sha}} @{$blame}{@lines}); my @target_shas = grep {defined $summary_for->{$_}} @adjacent_shas; # Note that lines at the beginning or end of a file can be # "surrounded" by a single line. my $is_surrounded = @target_shas > 0 && @target_shas == @adjacent_shas && $target_shas[0] eq $target_shas[-1]; my $is_adjacent = @target_shas == 1; if ($is_surrounded || ($strict < $SURROUNDED && $is_adjacent)) { $blamed{$target_shas[0]} = 1; } while ($di < @$diff-1 && startswith($diff->[$di+1], '+')) { $di++; } } } my @targets = keys %blamed; return wantarray ? @targets : \@targets; } sub startswith { my ($haystack, $needle) = @_; return index($haystack, $needle, 0) == 0; } # Map lines in a hunk's diff to the corresponding `git blame HEAD` output. sub blame_indexes { my $hunk = shift; my @indexes; my $bi = $hunk->{start}; for (my $di = 0; $di < @{$hunk->{lines}}; $di++) { push @indexes, $bi; my $first = substr($hunk->{lines}[$di], 0, 1); if ($first eq '-' or $first eq ' ') { $bi++; } # Don't increment $bi for added lines. } return \@indexes; } sub print_hunk_blamediff { my $args = shift; my $fh = $args->{fh}; my $hunk = $args->{hunk}; my $summary_for = $args->{summary_for}; my $blame = $args->{blame}; my $blame_indexes = $args->{blame_indexes}; if (grep {!defined} ($fh, $hunk, $summary_for, $blame, $blame_indexes)) { croak 'missing argument'; } my $format = "%-8.8s|%4.4s|%-30.30s|%-30.30s\n"; for (my $i = 0; $i < @{$hunk->{lines}}; $i++) { my $line_r = $hunk->{lines}[$i]; my $bi = $blame_indexes->[$i]; my $sha = defined $blame->{$bi} ? $blame->{$bi}{sha} : undef; my $display_sha = defined($sha) ? $sha : q{}; my $display_bi = $bi; if (startswith($line_r, '+')) { $display_sha = q{}; # For added lines. $display_bi = q{}; } if (defined($sha) && !defined($summary_for->{$sha})) { # For lines from before the given upstream revision. $display_sha = '^'; } my $line_l = ''; if (defined $blame->{$bi} && !startswith($line_r, '+')) { $line_l = $blame->{$bi}{text}; } for ($line_l, $line_r) { # For the table to line up, tabs need to be converted to a string of fixed width. s/\t/^I/g; # Remove trailing newlines and carriage returns. If more trailing # whitespace is removed, that's fine. $_ = rtrim($_); } printf {$fh} $format, $display_sha, $display_bi, $line_l, $line_r; } print {$fh} "\n"; return; } sub rtrim { my $s = shift; $s =~ s/\s+\z//; return $s; } sub blame { my ($hunk, $alias_for) = @_; if ($hunk->{count} == 0) { return {}; } my @cmd = git_cmd( 'blame', '--porcelain', '-L' => "$hunk->{start},+$hunk->{count}", 'HEAD', '--', "$hunk->{file}"); my %blame; my ($sha, $line_num); open(my $fh, '-|', @cmd) or die "git blame: $!\n"; while (my $line = <$fh>) { if ($line =~ /^([0-9a-f]{40}) \d+ (\d+)/) { ($sha, $line_num) = ($1, $2); } if (startswith($line, "\t")) { if (defined $alias_for->{$sha}) { $sha = $alias_for->{$sha}; } $blame{$line_num} = {sha => $sha, text => substr($line, 1)}; } } close($fh) or die "git blame: non-zero exit code"; return \%blame; } sub diff_hunks { my $num_context_lines = shift; my @cmd = git_cmd(qw(-c diff.noprefix=false diff --no-ext-diff --ignore-submodules), "-U$num_context_lines"); if (is_index_dirty()) { push @cmd, "--cached"; } open(my $fh, '-|', @cmd) or die $!; my @hunks = parse_hunks($fh, keep_lines => 1); close($fh) or die "git diff: non-zero exit code"; return wantarray ? @hunks : \@hunks; } sub commit_fixup { my ($sha, $hunks) = @_; open my $fh, '|-', git_cmd(qw(apply --unidiff-zero --cached -)) or die "git apply: $!\n"; for my $hunk (@{$hunks}) { print({$fh} "--- a/$hunk->{file}\n", "+++ b/$hunk->{file}\n", $hunk->{header}, @{$hunk->{lines}}, ); } close $fh or die "git apply: non-zero exit code\n"; system(git_cmd('commit', "--fixup=$sha")) == 0 or die "git commit: $!\n"; return; } sub is_index_dirty { return system(git_cmd(qw(diff-index --cached HEAD --quiet))) != 0; } sub fixup_hunks_by_sha { my $args = shift; my $hunks = $args->{hunks}; my $blame_for = $args->{blame_for}; my $summary_for = $args->{summary_for}; my $strict = $args->{strict}; if (grep {!defined} ($hunks, $blame_for, $summary_for, $strict)) { croak 'missing argument'; } my %hunks_for; for my $hunk (@{$hunks}) { my $blame = $blame_for->{$hunk}; my $sha = fixup_sha({ hunk => $hunk, blame => $blame, summary_for => $summary_for, strict => $strict, }); if ($sha && $VERBOSE) { printf "%s fixes %s %s\n", hunk_desc($hunk), substr($sha, 0, 8), $summary_for->{$sha}; } if ($VERBOSE > 1) { print_hunk_blamediff({ fh => *STDOUT, hunk => $hunk, summary_for => $summary_for, blame => $blame, blame_indexes => blame_indexes($hunk) }); } next if !$sha; push @{$hunks_for{$sha}}, $hunk; } return \%hunks_for; } # Return SHAs in some consistent order. # # Currently they're ordered by how early their assigned hunks appear in the # diff output. This assumes $hunks is in the order it was parsed from the diff. # This ordering seems nice since it'd be similar to the order a human would # make commits in if they were working their way down the diff. sub ordered_shas { my $hunks = shift; my $sha_for = shift; my @ordered = (); for my $hunk (@{$hunks}) { if (defined $sha_for->{$hunk}) { push @ordered, $sha_for->{$hunk}; } } return uniq(@ordered); } # Reverse the sha->hunks hashef and return a hunk->sha hashref. sub sha_for_hunk_map { my $hunks_for = shift; my %sha_for; for my $sha (keys %{$hunks_for}) { for my $hunk (@{$hunks_for->{$sha}}) { if (defined $sha_for{$hunk}) { die "multiple SHAs for hunk"; # This should never happen. } $sha_for{$hunk} = $sha; } } return \%sha_for; } sub exit_code { my ($hunks, $hunks_for) = @_; my $hunk_count = scalar @{$hunks}; my $assigned_hunk_count = 0; for (values %{$hunks_for}) { $assigned_hunk_count += @{$_}; } my $rc; if ($hunk_count == 0) { $rc = 3; # no hunks to assign } elsif ($assigned_hunk_count == 0) { $rc = 2; # hunks exist, but none assigned } elsif ($assigned_hunk_count < $hunk_count) { $rc = 1; # not all hunks assigned } elsif ($hunk_count == $assigned_hunk_count) { $rc = 0; # all hunks assigned } else { die "unexpected conditions when choosing exit code"; } return $rc; } # 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 $tempfile = File::Temp->new( TEMPLATE => 'git-autofixup_index.XXXXXX', DIR => File::Spec->tmpdir()); $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"; return $tempfile; } sub main { $VERBOSE = 0; my $help; my $man; my $show_version; my $strict = $CONTEXT; my $num_context_lines = 3; my $dryrun; my $use_detailed_exit_codes; GetOptions( 'h' => \$help, 'help' => \$man, 'version' => \$show_version, 'verbose|v+' => \$VERBOSE, 'strict|s=i' => \$strict, 'context|c=i' => \$num_context_lines, 'dryrun|n' => \$dryrun, 'gitopt|g=s' => \@GIT_OPTIONS, 'exit-code' => \$use_detailed_exit_codes, ) or return 1; if ($help) { print $usage; return 0; } if ($show_version) { print "$VERSION\n"; return 0; } if ($man) { 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"; if ($num_context_lines < 0) { die "invalid number of context lines: $num_context_lines\n"; } if ($strict < 0) { die "invalid strictness level: $strict\n"; } elsif ($strict > 0 && $num_context_lines == 0) { die "strict hunk assignment requires context\n"; } my $toplevel = qx(git rev-parse --show-toplevel); chomp $toplevel; $? == 0 or die "Can't get repo toplevel dir\n"; chdir $toplevel or die $!; my $hunks = diff_hunks($num_context_lines); my $summary_for = summary_for_commits($upstream); my $alias_for = sha_aliases($summary_for); my %blame_for = map {$_ => blame($_, $alias_for)} @{$hunks}; my $hunks_for = fixup_hunks_by_sha({ hunks => $hunks, blame_for => \%blame_for, summary_for => $summary_for, strict => $strict, }); my @ordered_shas = ordered_shas($hunks, sha_for_hunk_map($hunks_for)); if ($dryrun) { if ($use_detailed_exit_codes) { return exit_code($hunks, $hunks_for); } return 0; } 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(); } for my $sha (@ordered_shas) { my $fixup_hunks = $hunks_for->{$sha}; commit_fixup($sha, $fixup_hunks); } if ($use_detailed_exit_codes) { return exit_code($hunks, $hunks_for); } return 0; } if (!caller()) { exit main(); } 1; __END__ =pod =head1 NAME App::Git::Autofixup - create fixup commits for topic branches =head1 SYNOPSIS git-autofixup [] =head1 DESCRIPTION F parses hunks of changes in the working directory out of C output and uses C to assign those hunks to commits in CrevisionE..HEAD>, which will typically represent a topic branch, and then creates fixup commits to be used with C. 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 CrevisionE> if the current branch has a tracking branch. See C for other ways to specify revisions. If any changes have been staged to the index using C, then F will only consider staged hunks when trying to create fixup commits. A temporary index is used to create any resulting commits. 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. COMMIT |LINE|HEAD |WORKING DIRECTORY 99f370af| 1|first line | first line | | |+added line a1eadbe2| 2|second line | second line But if that second line were instead blamed on an upstream commit (denoted by C<^>), the hunk would be added to a fixup commit for C<99f370af>: 99f370af| 1|first line | first line | | |+added line ^ | 2|second line | second line Output similar to this example can be generated by setting verbosity to 2 or greater by using the verbosity option multiple times, eg. C, and can be helpful in determining how a hunk will be handled. F is not to be used mindlessly. Always inspect the created fixup commits to ensure hunks have been assigned correctly, especially when used on a working directory that has been changed with a mix of fixups and new work. =head1 OPTIONS =over =item -h Show usage. =item --help Show manpage. =item --version Show version. =item -v, --verbose Increase verbosity. Can be used up to two times. =item -c N, --context N Change the number of context lines C uses around hunks. Default: 3. This can change how hunks are assigned to fixup commits, especially with C<--strict 0>. =item -s N, --strict N Set how strict F is about assigning hunks to fixup commits. Default: 0. Strictness levels are described under DESCRIPTION. =item -g ARG, --gitopt ARG Specify option for git. Can be used multiple times. Useful for testing, to override config options that break git-autofixup, or to override global diff options to tweak what git-autofixup considers a hunk. Note ARG won't be wordsplit, so to give multiple arguments, such as for setting a config option like C<-c diff.algorithm>, this option must be used multiple times: C<-g -c -g diff.algorithm=patience>. =item -e, --exit-code Use more detailed exit codes: =over =item 0: All hunks have been assigned. =item 1: Only some hunks have been assigned. =item 2: No hunks have been assigned. =item 3: There was nothing to be assigned. =item 255: Unexpected error occurred. =back =back =head1 INSTALLATION If cpan is available, run C. Otherwise, copy F to a directory in C and ensure it has execute permissions. It can then be invoked as either C or C, since git searches C for appropriately named binaries. Git is distributed with Perl 5 for platforms not expected to already have it installed, but installing modules with cpan requires other tools that might not be available, such as make. This script has no dependencies outside of the standard library, so it is hoped that it works on any platform that Git does without much trouble. Requires a git supporting C: 1.7.4 or later. =head1 BUGS/LIMITATIONS If a topic branch adds some lines in one commit and subsequently removes some of them in another, a hunk in the working directory that re-adds those lines will be assigned to fixup the first commit, and during rebasing they'll be removed again by the later commit. Not tested in F on Windows. Run it from Git Bash, Cygwin, or a similar Unix emulation environment. =head1 ACKNOWLEDGEMENTS F was inspired by a description of L in the L. While I was working on it I found L, by oktal3700, which was helpful to examine. =head1 COPYRIGHT AND LICENSE Copyright (C) 2017, Jordan Torbiak. This program is free software; you can redistribute it and/or modify it under the terms of the Artistic License v2.0. =cut git-autofixup-0.003001/lib/000077500000000000000000000000001375740566600153335ustar00rootroot00000000000000git-autofixup-0.003001/lib/App/000077500000000000000000000000001375740566600160535ustar00rootroot00000000000000git-autofixup-0.003001/lib/App/Git/000077500000000000000000000000001375740566600165765ustar00rootroot00000000000000git-autofixup-0.003001/lib/App/Git/Autofixup.pm000066400000000000000000000005241375740566600211210ustar00rootroot00000000000000package App::Git::Autofixup; use strict; use warnings FATAL => 'all'; our $VERSION = 0.003001; =head1 NAME App::Git::Autofixup - create fixup commits for topic branches =head1 DESCRIPTION This is a stub module, see F for details of the app. =head1 AUTHOR Jordan Torbiak =head1 LICENSE Artistic License 2.0 =cut 1; git-autofixup-0.003001/t/000077500000000000000000000000001375740566600150305ustar00rootroot00000000000000git-autofixup-0.003001/t/autofixup.t000077500000000000000000000425271375740566600172560ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings FATAL => 'all'; use Carp qw(croak); use Cwd; use English qw(-no_match_vars); use File::Temp qw(tempdir); use Test::More; if ($OSNAME eq 'MSWin32') { plan skip_all => 'Run from Cygwin or Git Bash on Windows' } elsif (!has_git()) { plan skip_all => 'git version 1.7.4+ required' } else { plan tests => 40; } require './git-autofixup'; $ENV{GIT_AUTHOR_NAME} = 'A U Thor'; $ENV{GIT_AUTHOR_EMAIL} = 'author@example.com'; $ENV{GIT_COMMITTER_NAME} = 'C O Mitter'; $ENV{GIT_COMMITTER_EMAIL} = 'committer@example.com'; sub has_git { my $stdout = qx{git --version}; return if $? != 0; my ($x, $y, $z) = $stdout =~ /(\d+)\.(\d+)(?:\.(\d+))?/; defined $x or die "unexpected output from git: $stdout"; $z = defined $z ? $z : 0; my $cmp = $x <=> 1 || $y <=> 7 || $z <=> 4; return $cmp >= 0; } sub test_autofixup_strict { my $params = shift; my $strict_levels = $params->{strict} or croak "strictness levels not given"; delete $params->{strict}; my $autofixup_opts = $params->{autofixup_opts} || []; if (grep /^(--strict|-s)/, @{$autofixup_opts}) { croak "strict option already given"; } my $name = $params->{name} || croak "name not given"; for my $strict (@{$strict_levels}) { $params->{name} = "$name, strict=$strict"; $params->{autofixup_opts} = ['-s' => $strict, @{$autofixup_opts}]; test_autofixup($params); } } # test_autofixup initializes a git repo in a tempdir, creates given "upstream" # and "topic" commits, applies changes to the working directory, runs # autofixup, and compares the git log of the fixup commits to an expected log. # # The upstream_commits and topic_commits arguments are heterogeneous lists of # sub and hash refs. Hash refs are interpreted as being maps of filenames to # contents to be written. If more flexibility is needed a subref can be given # to manipulate the working directory. # # Arguments given in a hashref: # upstream_commits: sub or hash refs that must not be fixed up # topic_commits: sub or hash refs representing commits that can be fixed up # unstaged: sub or hash ref of working directory changes # staged: sub or hash ref of index changes # log_want: expected log output # autofixup_opts: command-line options to pass thru to autofixup sub test_autofixup { my ($args) = shift; my $name = defined($args->{name}) ? $args->{name} : croak "no test name given"; my $upstream_commits = $args->{upstream_commits} || []; my $topic_commits = $args->{topic_commits} || []; my $unstaged = defined($args->{unstaged}) ? $args->{unstaged} : croak "no unstaged changes given"; my $staged = $args->{staged}; my $log_want = defined($args->{log_want}) ? $args->{log_want} : croak "wanted log output not given"; my $staged_want = $args->{staged_want}; my $unstaged_want = $args->{unstaged_want}; my $exit_code_want = $args->{exit_code}; my $autofixup_opts = $args->{autofixup_opts} || []; push @{$autofixup_opts}, '--exit-code'; if (!$upstream_commits && !$topic_commits) { croak "no upstream or topic commits given"; } if (exists $args->{strict}) { croak "strict key given; use test_autofixup_strict instead"; } my $exit_code_got; my $log_got; my $staged_got; my $unstaged_got; my $orig_dir = getcwd(); my $dir = File::Temp::tempdir(CLEANUP => 1); chdir $dir or die "$!"; eval { init_repo(); my $i = 0; for my $commit (@{$upstream_commits}) { apply_change($commit); commit_if_dirty("commit$i"); $i++; } my $upstream_rev = get_revision_sha(); for my $commit (@{$topic_commits}) { apply_change($commit); commit_if_dirty("commit$i"); $i++; } my $pre_fixup_rev = get_revision_sha(); if (defined($staged)) { apply_change($staged); # We're at the repo root, so using -A will change everything even # in pre-v2 versions of git. See git commit 808d3d717e8. run("git add -A"); } apply_change($unstaged); run("git --no-pager log --format='%h %s' ${upstream_rev}.."); $exit_code_got = autofixup(@{$autofixup_opts}, $upstream_rev); $log_got = git_log(${pre_fixup_rev}); $staged_got = diff('--cached'); if (defined($unstaged_want)) { $unstaged_got = diff('HEAD'); } }; my $err = $@; chdir $orig_dir or die "$!"; if ($err) { diag($err); fail($name); return; } my $failed = 0; if ($log_got ne $log_want) { diag("log_got=<', $filename) or croak "$!"; print {$fh} $contents or croak "$!"; close $fh or croak "$!"; } sub git_log { my $revision = shift; my $log = qx{git -c diff.noprefix=false log -p --format=%s ${revision}..}; if ($? != 0) { croak "git log: $?\n"; } return $log; } sub diff { my $revision = shift; my $diff = qx{git -c diff.noprefix=false diff ${revision}}; if ($? != 0) { croak "git diff $?\n"; } return $diff; } sub get_revision_sha { my $dir = shift; my $revision = qx{git rev-parse HEAD}; $? == 0 or croak "git rev-parse: $?"; chomp $revision; return $revision; } sub autofixup { local @ARGV = @_; print "# git-autofixup ", join(' ', @ARGV), "\n"; return main(); } test_autofixup_strict({ name => "single-line change gets autofixed", strict => [0..2], topic_commits => [{a => "a1\n"}], unstaged => {a => "a2\n"}, exit_code => 0, log_want => <<'EOF' fixup! commit0 diff --git a/a b/a index da0f8ed..c1827f0 100644 --- a/a +++ b/a @@ -1 +1 @@ -a1 +a2 EOF }); test_autofixup_strict({ name => "adjacent change gets autofixed", strict => [0..1], upstream_commits => [{a => "a3\n"}], topic_commits => [{a => "a1\na3\n"}], unstaged => {a => "a1\na2\na3\n"}, exit_code => 0, log_want => <<'EOF' fixup! commit1 diff --git a/a b/a index 76642d4..2cdcdb0 100644 --- a/a +++ b/a @@ -1,2 +1,3 @@ a1 +a2 a3 EOF }); test_autofixup({ name => "adjacent change doesn't get autofixed if strict=2", upstream_commits => [{a => "a3\n"}], topic_commits => [{a => "a1\na3\n"}], unstaged => {a => "a1\na2\na3\n"}, log_want => '', autofixup_opts => ['-s2'], exit_code => 2, }); test_autofixup({ name => 'fixups are created for additions surrounded by topic commit lines when strict=2', topic_commits => [{a => "a1\na3\n", b => "b1\n", c => "c2\n"}], unstaged => {a => "a1\na2\na3\n", b => "b1\nb2\n", c => "c1\nc2\n"}, autofixup_opts => ['-s2'], exit_code => 0, log_want => <<'EOF' fixup! commit0 diff --git a/a b/a index 76642d4..2cdcdb0 100644 --- a/a +++ b/a @@ -1,2 +1,3 @@ a1 +a2 a3 diff --git a/b b/b index c9c6af7..9b89cd5 100644 --- a/b +++ b/b @@ -1 +1,2 @@ b1 +b2 diff --git a/c b/c index 16f9ec0..d0aaf97 100644 --- a/c +++ b/c @@ -1 +1,2 @@ +c1 c2 EOF }); test_autofixup_strict({ name => "removed file doesn't get autofixed", strict => [0..2], topic_commits => [sub { write_file(a => "a1\n"); }], unstaged => sub { unlink 'a'; }, exit_code => 3, log_want => '', }); test_autofixup_strict({ name => "re-added file doesn't get autofixed", strict => [0..2], topic_commits => [ sub { write_file(a => "a1\n"); }, sub { unlink 'a'; }, ], unstaged => sub { write_file(a => "a1a\n"); }, exit_code => 3, log_want => '', }); test_autofixup_strict({ name => "re-added line gets autofixed into the commit blamed for the adjacent context", # During rebase the line will just get removed again by the next commit. # --strict can be used to avoid creating a fixup in this case, where the # added line is adjacent to only one of a topic commit's blamed lines, # but not if it's surrounded by them. It seems possible to avoid # potentially confusing situations like this by parsing the diffs of the # topic commits and tracking changes in files' line numbers, but it's # doubtful that it would be worth it. strict => [0..2], topic_commits => [ {a => "a1\na2\n"}, {a => "a1\n"}, ], exit_code => 0, unstaged => {a => "a1\na2\n"}, log_want => <<'EOF' fixup! commit0 diff --git a/a b/a index da0f8ed..0016606 100644 --- a/a +++ b/a @@ -1 +1,2 @@ a1 +a2 EOF }); test_autofixup_strict({ name => "removed lines get autofixed", strict => [0..2], topic_commits => [{a => "a1\n", b => "b1\nb2\n"}], unstaged => {a => "", b => "b2\n"}, exit_code => 0, log_want => <<'EOF' fixup! commit0 diff --git a/a b/a index da0f8ed..e69de29 100644 --- a/a +++ b/a @@ -1 +0,0 @@ -a1 diff --git a/b b/b index 9b89cd5..e6bfff5 100644 --- a/b +++ b/b @@ -1,2 +1 @@ -b1 b2 EOF }); test_autofixup_strict({ name => 'no fixups are created for upstream commits', strict => [0..2], upstream_commits => [{a => "a1\n"}], unstaged => {a => "a1a\n"}, exit_code => 2, log_want => '', }); test_autofixup({ name => 'fixups are created for hunks changing lines blamed by upstream if strict=0', # This depends on the number of context lines kept when creating diffs. git # keeps 3 by default. upstream_commits => [{a => "a1\na2\na3\n"}], topic_commits => [{a => "a1\na2a\na3a\n"}], unstaged => {a => "a1b\na2b\na3b\n"}, exit_code => 0, log_want => <<'EOF' fixup! commit1 diff --git a/a b/a index 125d560..cc1aa32 100644 --- a/a +++ b/a @@ -1,3 +1,3 @@ -a1 -a2a -a3a +a1b +a2b +a3b EOF }); test_autofixup_strict({ name => 'no fixups are created for hunks changing lines blamed by upstream if strict > 0', # This depends on the number of context lines kept when creating diffs. git # keeps 3 by default. strict => [1..2], upstream_commits => [{a => "a1\na2\na3\n"}], topic_commits => [{a => "a1\na2a\na3a\n"}], unstaged => {a => "a1b\na2b\na3b\n"}, exit_code => 2, log_want => '', }); test_autofixup_strict({ name => "hunks blamed on a fixup! commit are assigned to that fixup's target", strict => [0..2], topic_commits => [ {a => "a1\n"}, sub { write_file(a => "a2\n"); run(qw(git commit -a --fixup=HEAD)); }, ], exit_code => 0, unstaged => {a => "a2\na3\n"}, log_want => <<'EOF' fixup! commit0 diff --git a/a b/a index c1827f0..b792f74 100644 --- a/a +++ b/a @@ -1 +1,2 @@ a2 +a3 EOF }); test_autofixup({ name => "removed line gets autofixed when context=0", topic_commits => [{a => "a1\na2\n"}], unstaged => {a => "a1\n"}, autofixup_opts => ['-c' => 0], exit_code => 0, log_want => <<'EOF' fixup! commit0 diff --git a/a b/a index 0016606..da0f8ed 100644 --- a/a +++ b/a @@ -1,2 +1 @@ a1 -a2 EOF }); test_autofixup({ name => "added line is ignored when context=0", topic_commits => [{a => "a1\n"}], unstaged => {a => "a1\na2\n"}, autofixup_opts => ['-c' => 0], exit_code => 2, log_want => '', }); test_autofixup({ name => "ADJACENCY assignment is used as a fallback for multiple context targets", topic_commits => [ {a => "a1\n"}, {a => "a1\na2\n"}, ], exit_code => 0, unstaged => {a => "a1\na2a\n"}, log_want => <<'EOF' fixup! commit1 diff --git a/a b/a index 0016606..a0ef52c 100644 --- a/a +++ b/a @@ -1,2 +1,2 @@ a1 -a2 +a2a EOF }); test_autofixup({ name => "Works when run in a subdir of the repo root", topic_commits => [ sub { mkdir 'sub' or die $!; chdir 'sub' or die $!; write_file("a", "a1\n"); } ], exit_code => 0, unstaged => {'a' => "a1\na2\n"}, log_want => <<'EOF' fixup! commit0 diff --git a/sub/a b/sub/a index da0f8ed..0016606 100644 --- a/sub/a +++ b/sub/a @@ -1 +1,2 @@ a1 +a2 EOF }); test_autofixup({ name => "file without newline at EOF gets autofixed", topic_commits => [{a => "a1\na2"}], unstaged => {'a' => "a1\na2\n"}, exit_code => 0, log_want => <<'EOF' fixup! commit0 diff --git a/a b/a index c928c51..0016606 100644 --- a/a +++ b/a @@ -1,2 +1,2 @@ a1 -a2 \ No newline at end of file +a2 EOF }); test_autofixup({ name => "multiple hunks in the same file get autofixed", topic_commits => [ {a => "a1.0\na2\na3\na4\na5\na6\na7\na8\na9.0\n"}, {a => "a1.0\na2\na3\na4\na5\na6\na7\na8\na9.1\n"}, {a => "a1.2\na2\na3\na4\na5\na6\na7\na8\na9.1\n"}, ], unstaged => {'a' => "a1.3\na2\na3\na4\na5\na6\na7\na8\na9.3\n"}, exit_code => 0, log_want => <<'EOF' fixup! commit1 diff --git a/a b/a index d9f44da..5b9ebcd 100644 --- a/a +++ b/a @@ -6,4 +6,4 @@ a5 a6 a7 a8 -a9.1 +a9.3 fixup! commit2 diff --git a/a b/a index 50de7e8..d9f44da 100644 --- a/a +++ b/a @@ -1,4 +1,4 @@ -a1.2 +a1.3 a2 a3 a4 EOF }); test_autofixup({ name => "single-line change gets autofixed when mnemonic prefixes are enabled", topic_commits => [{a => "a1\n"}], unstaged => {a => "a2\n"}, autofixup_opts => ['-g', '-c', '-g', 'diff.mnemonicPrefix=true'], exit_code => 0, log_want => <<'EOF' fixup! commit0 diff --git a/a b/a index da0f8ed..c1827f0 100644 --- a/a +++ b/a @@ -1 +1 @@ -a1 +a2 EOF }); test_autofixup({ name => "single-line change gets autofixed when diff.external is set", topic_commits => [{a => "a1\n"}], unstaged => {a => "a2\n"}, autofixup_opts => ['-g', '-c', '-g', 'diff.external=vimdiff'], exit_code => 0, log_want => <<'EOF' fixup! commit0 diff --git a/a b/a index da0f8ed..c1827f0 100644 --- a/a +++ b/a @@ -1 +1 @@ -a1 +a2 EOF }); test_autofixup({ name => 'exit code is 1 when some hunks are assigned', upstream_commits => [{a => "a1\n"}], topic_commits => [{b => "b1\n"}], unstaged => {a => "a1a\n", b => "b2\n"}, exit_code => 1, log_want => <<'EOF' fixup! commit1 diff --git a/b b/b index c9c6af7..e6bfff5 100644 --- a/b +++ b/b @@ -1 +1 @@ -b1 +b2 EOF }); test_autofixup({ name => "multiple hunks to the same commit get autofixed", topic_commits => [ {a => "a1.0\na2\na3\na4\na5\na6\na7\na8\na9.0\n"}, {b => "b1.0\n"}, ], unstaged => {'a' => "a1.1\na2\na3\na4\na5\na6\na7\na8\na9.1\n", b => "b1.1\n"}, exit_code => 0, log_want => q{fixup! commit1 diff --git a/b b/b index 253a619..6419a9e 100644 --- a/b +++ b/b @@ -1 +1 @@ -b1.0 +b1.1 fixup! commit0 diff --git a/a b/a index 5d11004..0054137 100644 --- a/a +++ b/a @@ -1,4 +1,4 @@ -a1.0 +a1.1 a2 a3 a4 @@ -6,4 +6,4 @@ a5 a6 a7 a8 -a9.0 +a9.1 }}); test_autofixup({ name => "only staged hunks get autofixed", topic_commits => [{a => "a1\n", b => "b1\n"}], staged => {a => "a2\n"}, unstaged => {b => "b2\n"}, exit_code => 0, log_want => <<'EOF' fixup! commit0 diff --git a/a b/a index da0f8ed..c1827f0 100644 --- a/a +++ b/a @@ -1 +1 @@ -a1 +a2 EOF , unstaged_want => <<'EOF' diff --git a/b b/b index c9c6af7..e6bfff5 100644 --- a/b +++ b/b @@ -1 +1 @@ -b1 +b2 EOF }); test_autofixup({ name => "staged hunks that aren't autofixed remain in index", upstream_commits => [{b => "b1\n"}], topic_commits => [{a => "a1\n", , c => "c1\n"}], staged => {a => "a2\n", b => "b2\n"}, unstaged => {c => "c2\n"}, exit_code => 1, log_want => <<'EOF' fixup! commit1 diff --git a/a b/a index da0f8ed..c1827f0 100644 --- a/a +++ b/a @@ -1 +1 @@ -a1 +a2 EOF , unstaged_want => <<'EOF' diff --git a/b b/b index c9c6af7..e6bfff5 100644 --- a/b +++ b/b @@ -1 +1 @@ -b1 +b2 diff --git a/c b/c index ae93045..16f9ec0 100644 --- a/c +++ b/c @@ -1 +1 @@ -c1 +c2 EOF , staged_want => <<'EOF' diff --git a/b b/b index c9c6af7..e6bfff5 100644 --- a/b +++ b/b @@ -1 +1 @@ -b1 +b2 EOF }); git-autofixup-0.003001/xt/000077500000000000000000000000001375740566600152205ustar00rootroot00000000000000git-autofixup-0.003001/xt/pod.t000066400000000000000000000002531375740566600161670ustar00rootroot00000000000000use strict; use warnings FATAL => 'all'; use Test::More; eval "use Test::Pod 1.00"; plan skip_all => "Test::Pod 1.00 required for testing POD" if $@; all_pod_files_ok(); git-autofixup-0.003001/xt/precommit.t000066400000000000000000000006261375740566600174100ustar00rootroot00000000000000use strict; use warnings FATAL => 'all'; use Test::More tests => 2; use File::Temp; use App::Git::Autofixup; require './git-autofixup'; our $VERSION; is($VERSION, $App::Git::Autofixup::VERSION, "versions agree"); my $tmp = File::Temp->new(); my $tmp_name = $tmp->filename(); system("perldoc -u git-autofixup >$tmp_name"); system("diff -u $tmp_name README.pod"); ok($? == 0, 'README.pod is up-to-date');