175

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:

Every now and then, I notice a bug in my code while working on an (unrelated) feature. A quick git blame then reveals that the bug has been introduced a few commits ago (I commit quite a lot, so usually it's not the most recent commit which introduced the bug). At this point, I usually do this:

git stash                      # temporarily put my work aside
git rebase -i <bad_commit>~1   # rebase one step before the bad commit
                               # mark broken commit for editing
vim <affected_sources>         # fix the bug
git add <affected_sources>     # stage fixes
git commit -C <bad_commit>     # commit fixes using same log message as before
git rebase --continue          # base all later changes onto this

However, this happens so often that the above sequence is getting annoying. Especially the 'interactive rebase' is boring. Is there any shortcut to the above sequence, which lets me amend an arbitrary commit in the past with the staged changes? I'm perfectly aware that this changes the history, but I'm doing mistakes so often that I'd really love to have something like

vim <affected_sources>             # fix bug
git add -p <affected_sources>      # Mark my 'fixup' hungs for staging
git fixup <bad_commit>             # amend the specified commit with staged changes,
                                   # rebase any successors of bad commit on rewritten 
                                   # commit.

Maybe a smart script which can rewrite commits using plumbing tools or so?

Community
  • 1
  • 1
Frerich Raabe
  • 90,689
  • 19
  • 115
  • 207
  • What do you mean by "reorders" the commits? If you're changing history then all the commits since the changed commits _have_ to be different, but the accepted answer to the linked question doesn't re-order the commits in any meaningful sense. – CB Bailey Jun 23 '10 at 16:38
  • 2
    @Charles: I meant reordering as in: if I notice that HEAD~5 is the broken commit, the following the accepted answer in the linked question will make HEAD (the tip of the branch) the fixed commit. However, I'd like HEAD~5 to be the fixed commit - which is what you get when using an interactive rebase and editing a single commit for fixing. – Frerich Raabe Jun 24 '10 at 00:04
  • Yes, but then the rebase command will re-checkout master and rebase all the subsequent commits onto the fixed commit. Isn't this how you're driving the `rebase -i` ? – CB Bailey Jun 24 '10 at 06:28
  • Actually, there's a potential issue with that answer, I think it should be `rebase --onto tmp bad-commit master`. As written it will try to apply the bad commit to the fixed commit state. – CB Bailey Jun 24 '10 at 06:32
  • Here's another tool for automating the fixup/rebase process: http://stackoverflow.com/a/24656286/1058622 – Mika Eloranta Jul 23 '14 at 07:49
  • I found this helpful https://github.com/keis/git-fixup – hakunin Aug 23 '16 at 05:23
  • 1
    Related: https://stackoverflow.com/questions/1186535/how-do-i-modify-a-specific-commit – Ciro Santilli OurBigBook.com Oct 27 '22 at 10:14

14 Answers14

254

UPDATED ANSWER

A while ago, a new --fixup argument was added to git commit which can be used to construct a commit with a log message suitable for git rebase --interactive --autosquash. So the simplest way to fixup a past commit is now:

$ git add ...                           # Stage a fix
$ git commit --fixup=a0b1c2d3           # Perform the commit to fix broken a0b1c2d3
$ git rebase -i --autosquash a0b1c2d3~1 # Now merge fixup commit into broken commit

ORIGINAL ANSWER

Here's a little Python script I wrote a while ago which implements this git fixup logic I hoped for in my original question. The script assumes that you staged some changes and then applies those changes to the given commit.

NOTE: This script is Windows-specific; it looks for git.exe and sets the GIT_EDITOR environment variable using set. Adjust this as needed for other operating systems.

Using this script I can implement precisely the 'fix broken sources, stage fixes, run git fixup ' workflow I asked for:

#!/usr/bin/env python
from subprocess import call
import sys

# Taken from http://stackoverflow.com/questions/377017/test-if-executable-exists-in python
def which(program):
    import os
    def is_exe(fpath):
        return os.path.exists(fpath) and os.access(fpath, os.X_OK)

    fpath, fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file

    return None

if len(sys.argv) != 2:
    print "Usage: git fixup <commit>"
    sys.exit(1)

git = which("git.exe")
if not git:
    print "git-fixup: failed to locate git executable"
    sys.exit(2)

broken_commit = sys.argv[1]
if call([git, "rev-parse", "--verify", "--quiet", broken_commit]) != 0:
    print "git-fixup: %s is not a valid commit" % broken_commit
    sys.exit(3)

if call([git, "diff", "--staged", "--quiet"]) == 0:
    print "git-fixup: cannot fixup past commit; no fix staged."
    sys.exit(4)

if call([git, "diff", "--quiet"]) != 0:
    print "git-fixup: cannot fixup past commit; working directory must be clean."
    sys.exit(5)

call([git, "commit", "--fixup=" + broken_commit])
call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i", "--autosquash", broken_commit + "~1"], shell=True)
Jacob Ford
  • 4,553
  • 5
  • 27
  • 42
Frerich Raabe
  • 90,689
  • 19
  • 115
  • 207
  • 3
    you could use `git stash` and `git stash pop` around your rebase to no longer require a clean working directory – Tobias Kienzler Oct 01 '10 at 11:49
  • @TobiasKienzler: About using `git stash` and `git stash pop`: you're right, but unfortunately `git stash` is *much* slower on Windows than it is on Linux or OS/X. Since my working directory is usually clean, I omitted this step to not slow down the command. – Frerich Raabe Aug 08 '12 at 06:40
  • I can confirm that, especially when working on a network share :-/ – Tobias Kienzler Aug 08 '12 at 10:28
  • 1
    Nice. I accidentally did `git rebase -i --fixup`, and it rebased from the fixed-up commit as starting point, so the sha argument wasn't needed in my case. – cthulhu Sep 25 '12 at 12:35
  • Wouldn't the `broken_commit + "~1"` part fail if for HEAD~ is used, for example? For reasons for this failure, look at my answer below. Otherwise, you've got my +1. – Deiwin Dec 02 '12 at 17:41
  • 1
    For people using the --autosquash often, it may be useful to set it to be the default behavior: `git config --global rebase.autosquash true` – azerty Feb 21 '17 at 20:26
  • Doesnt the rebase mean that I can't do this to commits which are public? If the commits are public how should do this? – Anton James Jul 12 '18 at 16:43
  • It's possible to reference to the commit with *HEAD* like in `git commit --fixup HEAD` – Tim Jan 29 '19 at 12:56
  • Here's a [git alias](https://stackoverflow.com/a/65305169/65732) based on the updated part of this answer. – sepehr Dec 15 '20 at 11:43
  • 1
    @TobiasKienzler Regarding using `git stash` and `git stash pop` around the rebase: there is now a flag for this `--autostash` [[see git docs](https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autostash)] – gMale May 06 '22 at 02:51
46

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...

Kris Jenkins
  • 4,083
  • 30
  • 40
  • 10
    Obviously you can change HEAD~5 to HEAD~n to go back further. You won't want to meddle with any history you've pushed upstream, so I usually type 'git rebase -i origin/master' to ensure that I'm only changing unpushed history. – Kris Jenkins Sep 29 '10 at 19:58
  • 4
    This is much like what I always did; FWIW, you might be interested in the `--autosquash` switch for `git rebase`, which automatically reorders the steps in the editor for you. See my response for a script which takes advantage of this to implement a `git fixup` command. – Frerich Raabe Sep 30 '10 at 08:11
  • I didn't know you could just re-order commit hashes, nice! – Aaron Franke May 16 '18 at 02:56
  • That is great! Only to make sure all the rebase work is done is separate feature branch. And not to mess with common branch like master. – Jay Modi Aug 03 '18 at 13:17
  • This! In combination of vim commands like `dd` (for yanking line) and `p`/`P` (for pasting), it is a great balance between control and ease imo. For example, in the case above I'd type `ddkP` then go to insert mode `i`, change `pick` to `f`, go out of insert mode by pressing esc. Then saving with `:wq` and enter. – Knogobert Sep 24 '20 at 08:54
  • I did something like this, made the change, made a fixup commit, ran `git rebase -i HEAD~5`, rearranged the commits and replaced `pick` with `fixup`. All this was in the feature branch, pushed the feature branch on remote feature, and created a PR. There was a conflict (the change I made) - resolved it and merged the PR, now all the commits after the fixup are twice in my git log - https://github.com/Exter-dg/activities/commits/main, fixup commit was for `Add delete item functionality`. Where did I go wrong? – Parth Kapadia Feb 23 '22 at 12:10
31

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.

dschlyter
  • 455
  • 5
  • 7
  • This is pretty helpful. For me the most common usecase is to fixup the changes onto the previous commit, so `git fixup HEAD` is what I created an alias for. I could also use amend for that I suppose. – grasshopper May 05 '16 at 07:12
  • Thank you! I also most often use it on the last commit, but I have another alias for a quick amend. `amend = commit --amend --reuse-message=HEAD` Then you can just type `git amend` or `git amend -a`and skip the editor for the commit message. – dschlyter Sep 09 '16 at 12:02
  • 4
    The problem with amend is that I don't remember how to spell it. I always have to think, is it ammend or amend and that's not good. – grasshopper Sep 09 '16 at 12:25
20

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.

Sérgio
  • 6,966
  • 1
  • 48
  • 53
7

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.

Community
  • 1
  • 1
Deiwin
  • 420
  • 5
  • 21
6

What really bothered me about the fixup workflow was that I had to figure out myself which commit I wanted to squash the change into every time. I created a "git fixup" command that helps with this.

This command creates fixup commits, with the added magic that it uses git-deps to automatically find the relevant commit, so the workflow often comes down to:

# discover and fix typo in a previously committed change
git add -p # stage only typo fix
git fixup

# at some later point squash all the fixup commits that came up
git rebase --autosquash master

This only works if the staged changes can be unambiguously attributed to a particular commit on the working tree (between master and HEAD). I find that is the case very often for the type of small changes I use this for, e.g. typos in comments or names of newly introduced (or renamed) methods. If this is not the case, it will at least display a list of candidate commits.

I use this a lot in my daily workflow, to quickly integrate small changes to previously changed lines into commits on my working branch. The script is not as beautiful as it could be, and it's written in zsh, but it has been doing the job for me well enough for a good while now that I never felt the need to rewrite it:

https://github.com/Valodim/git-fixup

Valodim
  • 61
  • 1
  • 3
5

I'd recommend https://github.com/tummychow/git-absorb:

Elevator Pitch

You have a feature branch with a few commits. Your teammate reviewed the branch and pointed out a few bugs. You have fixes for the bugs, but you don't want to shove them all into an opaque commit that says fixes, because you believe in atomic commits. Instead of manually finding commit SHAs for git commit --fixup, or running a manual interactive rebase, do this:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • or: git rebase -i --autosquash master

git absorb will automatically identify which commits are safe to modify, and which indexed changes belong to each of those commits. It will then write fixup! commits for each of those changes. You can check its output manually if you don't trust it, and then fold the fixups into your feature branch with git's built-in autosquash functionality.

Petski
  • 341
  • 3
  • 4
5

Here's git alias based on the accepted answer that works like:

git fixup          # fixup staged & unstaged changes into the last commit
git fixup ac1dc0d3 # fixup staged & unstaged changes into the given commit

Update your ~/.gitconfig file & add this alias:

[alias]
    fixup = "!git add . && git commit --fixup=${1:-$(git rev-parse HEAD)} && GIT_EDITOR=true git rebase --interactive --autosquash ${1:-$(git rev-parse HEAD~2)}~1"
sepehr
  • 17,110
  • 7
  • 81
  • 119
  • For anyone else wondering about the 1:- syntax [this explains it](https://unix.stackexchange.com/questions/338146/bash-defining-variables-with-var-number-default). TL;DR: use the numbered argument or use what comes after the hyphen. – gMale May 06 '22 at 02:23
4

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.

joeytwiddle
  • 29,306
  • 13
  • 121
  • 110
  • Indeed, this is exactly what I did in my 'original answer' Python script answer from September 30th 2010 (note how at the bottom of the script, it says `call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...`). – Frerich Raabe Nov 07 '16 at 11:03
2

You can create a fixup for a particular file by using this alias.

[alias]
...
# fixup for a file, using the commit where it was last modified
fixup-file = "!sh -c '\
        [ $(git diff          --numstat $1 | wc -l) -eq 1 ] && git add $1 && \
        [ $(git diff --cached --numstat $1 | wc -l) -eq 1 ] || (echo No changes staged. ; exit 1) && \
        COMMIT=$(git log -n 1 --pretty=format:"%H" $1) && \
            git commit --fixup=$COMMIT && \
            git rebase -i --autosquash $COMMIT~1' -"

If you have made some changes in myfile.txt but you don't want to put them in a new commit, git fixup-file myfile.txt will create a fixup! for the commit where myfile.txt was last modified, and then it will rebase --autosquash.

Alvaro
  • 21
  • 3
2

commit --fixup and rebase --autosquash are great, but they don't do enough. When I have a sequence of commits A-B-C and I write some more changes in my working tree which belong in one or more of those existing commits, I have to manually look at the history, decide which changes belong in which commits, stage them and create the fixup! commits. But git already has access to enough information to be able to do all that for me, so I've written a Perl script which does just that.

For each hunk in git diff the script uses git blame to find the commit that last touched the relevant lines, and calls git commit --fixup to write the appropriate fixup! commits, essentially doing the same thing I was doing manually before.

If you find it useful, please feel free to improve and iterate on it and maybe one day we'll get such a feature in git proper. I'd love to see a tool that can understand how a merge conflict should be resolved when it has been introduced by an interactive rebase.

Oktalist
  • 14,336
  • 3
  • 43
  • 63
  • I also had dreams about automation: git should just try to put it as far back in history as it can, without the patch breaking. But your method is probably more sane. Great to see you've attempted it. I will try it out! (Of course there are times when the fixup patch appears elsewhere in the file, and only the developer knows which commit it belongs with. Or perhaps a new test in the test suite could help the machine to work out where the fix should go.) – joeytwiddle Nov 07 '16 at 10:39
2

Given

$ git log --oneline
123123 Add foo
234234 Fix biz
123113 Remove fong
123123 Modify bar
123143 Add bar

You can create a fixup commit for Modify bar using

git commit --fixup ':/bar'

It creates a fixup commit for the last commit containing the substring bar . I always forget the exact syntax for this and its pretty hard to find since everyone apparently know all their commits by SHAs

Then just run rebase -i --autosquash ... at your convenience to actually do the fixup.

NB: This uses some kind of regular expression, so (, ) and other special characters might need quoting.

CervEd
  • 3,306
  • 28
  • 25
1

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).

joeytwiddle
  • 29,306
  • 13
  • 121
  • 110
0

I'm not aware of an automated way, but here's a solution that might by easier to human-botize:

git stash
# write the patch
git add -p <file>
git commit -m"whatever"   # message doesn't matter, will be replaced via 'fixup'
git rebase -i <bad-commit-id>~1
# now cut&paste the "whatever" line from the bottom to the second line
# (i.e. below <bad-commit>) and change its 'pick' into 'fixup'
# -> the fix commit will be merged into the <bad-commit> without changing the
# commit message
git stash pop
Tobias Kienzler
  • 25,759
  • 22
  • 127
  • 221