gitcommitgit-amend

amending a commit after it has been pushed


In my most recent commit to my repository that has already been pushed, I noticed I misspelled a single word in a file and now would like to change it. However, I do not want to create a whole new commit. Is there a way to amend my latest commit and push that into my repository without having to reset, remove, and push?

In the past I did:

git reset HEAD~1 --soft
git push -f
# correct the spelling
git add .
git commit -m "bug10 fix"

Is there a way to correct the spelling of the file, amend my latest commit and push without having to reset, force remove, then re-commit?

I tried:

# fixed the file
git add .
git commit --amend --no-edit
git push
 ! [rejected]        Nov21 -> Nov21 (non-fast-forward)

Solution

  • It's impossible to change any commit. That includes before it's pushed. The reason this is important to know—the reason you need to know that git commit --amend is a lie—is that what git commit --amend does locally, can be done here when pushing a commit to another Git repository.

    The (tiny) lie with git commit --amend is that it didn't change a commit at all. It just seemed to do that. What it really did was make a new and improved replacement commit.

    You can do this any time you like. You just need to know that each commit is numbered—every commit has a unique number, which is its hash ID—and that Git finds commits in a sort of backwards (perhaps even backassward) fashion, by having a branch name or other name locate the most recent commit for you, and then working backwards from there.

    Imagine a simple chain of commits, ending with your most recent commit whose hash is some big ugly random-looking hash ID that we'll just call H. Here's how Git sees—and finds—these commits:

    ... <-F <-G <-H   <--your-branch
    

    Your branch name "points to" (contains the hash ID of) commit H. Commit H itself contains the hash ID of earlier commit G. (Commit H also contains a full snapshot of every file, though we won't look at this at all here.)

    Commit G, being a commit, is numbered by some big ugly random-looking hash ID, and has a snapshot and metadata and hence points backwards to still-earlier commit F. That's a commit so it's numbered and has the same kind of stuff and points backwards yet again, and so on.

    Now, if you have a tiny typo in commit H and run git commit --amend, what Git does is to make a new commit. The new commit is exactly like H except in two ways:

    Commit H is still there in your repository. It's just no longer used. H still points back to G, but so does new commit I:

                H   ???
               /
    ... <-F <-G <-I   <--your-branch
    

    Git updates your branch name to point to new commit I. This effectively "kicks H off the branch", because Git finds commits by starting at the end and working backwards.

    Now, if you ran git push, what you did was send commit H to another Git repository, and then have them update one of their branch names—probably the same name you're using in your repository—to point to the now-shared commit:

    ...--F--G--H   <--their-branch
    

    If you make a new commit I that's a new-and-improved replacement for H and just send it to them with a regular git push, they'll take the commit and put it into their repository temporarily:

              H   <--their-branch
             /
    ...--F--G
             \
              I
    

    and then you have your Git ask their Git to set their branch name to point to I too.

    The problem comes in here, because when you do that, they check: If I update my branch name to point to I, do I lose any commits off the end? And of course they do: they lose commit H off the end of their branch. That's what you want them to do—that's what your Git did with your git commit --amend, after all—but they don't know that. All they know at this point is that they do have H and you're asking them to drop it now.

    You can use:

    git push --force
    

    or:

    git push --force-with-lease
    

    to send them a command, instead of a polite request, that tells them that they definitely should make their name point to I (regular --force), or that you think their name points to H now and if so they should make it point to I instead (--force-with-lease). If they will take such commands from you—most hosting sites, like GitHub and Bitbucket and GitLab have fancy configurations they add atop Git to control these things—then this lets you replace H with I after all.

    Why you might use --force-with-lease

    Note that if the hosting site is taking git push requests from other people, they might by now have:

              H--J--K   <--their-branch
             /
    ...--F--G
             \
              I
    

    and hence your command that they set their branch to point to I will drop not just your commit H, but also these other two additional commits J-K, off the end of their branch.

    Using git push --force-with-lease sends them your new commit I but also sends them the hash ID H that you believe they have as their final commit on their branch. If your belief is wrong, your conditional command—that they must change their name to point to I instead—gets a rejection that says your Git is now out of date, and you need to run git fetch first.

    If this can't happen, you don't need --force-with-lease. Very old Git versions don't have --force-with-lease either, but all modern ones do and it's usually wisest to keep the safety check in place even if it's not necessary, since "habits" may become "bad habits" over time.