I just read amending a single file in a past commit in git but unfortunately the accepted solution \'reorders\' the commits, which is not what I want. So here\'s my question
What I do is:
git add ... # Add the fix. git commit # Committed, but in the wrong place. git rebase -i HEAD~5 # Examine the last 5 commits for rebasing.
Your editor will open with a list of the last 5 commits, ready to be meddled with. Change:
pick 08e833c Good change 1. pick 9134ac9 Good change 2. pick 5adda55 Bad change! pick 400bce4 Good change 3. pick 2bc82n1 Fix of bad change.
...to:
pick 08e833c Good change 1. pick 9134ac9 Good change 2. pick 5adda55 Bad change! f 2bc82n1 Fix of bad change. # Move up, and change 'pick' to 'f' for 'fixup'. pick 400bce4 Good change 3.
Save & exit your editor, and the fix will be squished back into the commit it belongs with.
After you've done that a few times, you'll do it in seconds in your sleep. Interactive rebasing is the feature that really sold me on git. It's incredibly useful for this and more...
To fixup one commit :
git commit --fixup a0b1c2d3 .
git rebase --autosquash -i HEAD~2
where a0b1c2d3 is commit that you want fixup and where 2 is the number of commits +1 pasted that you want to change.
Note: git rebase --autosquash without -i doesn't worked but with -i worked, which is strange.
A bit late to the party, but here is a solution that works as the author imagined.
Add this to your .gitconfig:
[alias]
fixup = "!sh -c '(git diff-files --quiet || (echo Unstaged changes, please commit or stash with --keep-index; exit 1)) && COMMIT=$(git rev-parse $1) && git commit --fixup=$COMMIT && git rebase -i --autosquash $COMMIT~1' -"
Example usage:
git add -p
git fixup HEAD~5
However if you have unstaged changes, you must stash them before the rebase.
git add -p
git stash --keep-index
git fixup HEAD~5
git stash pop
You could modify the alias to stash automatically, instead of giving a warning. However, if the fixup does not apply cleanly you will need pop the stash manually after fixing the conflicts. Doing both the saving and popping manually seems more consistent and less confusing.
UPDATE: A cleaner version of the script can now be found here: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup.
I've been looking for something similar. This Python script seems too complicated, though, therefore I've hammered together my own solution:
First, my git aliases look like that (borrowed from here):
[alias]
fixup = !sh -c 'git commit --fixup=$1' -
squash = !sh -c 'git commit --squash=$1' -
ri = rebase --interactive --autosquash
Now the bash function becomes quite simple:
function gf {
if [ $# -eq 1 ]
then
if [[ "$1" == HEAD* ]]
then
git add -A; git fixup $1; git ri $1~2
else
git add -A; git fixup $1; git ri $1~1
fi
else
echo "Usage: gf <commit-ref> "
fi
}
This code first stages all current changes(you can remove this part, if you wish to stage the files yourself). Then creates the fixup(squash can also be used, if that's what you need) commit. After that it starts an interactive rebase with the --autosquash
flag on the parent of the commit you give as the argument. That will open your configured text editor, so you could verify that everything is as you expect and simply closing the editor will finish the process.
The if [[ "$1" == HEAD* ]]
part (borrowed from here) is used, because if you use, for example, HEAD~2 as your commit(the commit you want to fix current changes up with) reference then the HEAD will be displaced after the fixup commit has been created and you would need to use HEAD~3 to refer to the same commit.
I wrote a little shell function called gcf
to perform the fixup commit and the rebase automatically:
$ git add -p
... select hunks for the patch with y/n ...
$ gcf <earlier_commit_id>
That commits the fixup and does the rebase. Done! You can get back to coding.
For example, you can patch the second commit before the latest with: gcf HEAD~~
Here is the function. You can paste it into your ~/.bashrc
git_commit_immediate_fixup() {
local commit_to_amend="$1"
if [ -z "$commit_to_amend" ]; then
echo "You must provide a commit to fixup!"; return 1
fi
# Get a static commit ref in case the commit is something relative like HEAD~
commit_to_amend="$(git rev-parse "${commit_to_amend}")" || return 2
#echo ">> Committing"
git commit --no-verify --fixup "${commit_to_amend}" || return 3
#echo ">> Performing rebase"
EDITOR=true git rebase --interactive --autosquash --autostash \
--rebase-merges --no-fork-point "${commit_to_amend}~"
}
alias gcf='git_commit_immediate_fixup'
It uses --autostash
to stash and pop any uncommitted changes if necessary.
--autosquash
requires an --interactive
rebase, but we avoid the interaction by using a dummy EDITOR
.
--no-fork-point
protects commits from being silently dropped in rare situations (when you have forked off a new branch, and someone has already rebased past commits).
You can avoid the interactive stage by using a "null" editor:
$ EDITOR=true git rebase --autosquash -i ...
This will use /bin/true
as the editor, instead of /usr/bin/vim
. It always accepts whatever git suggests, without prompting.