Amending commits

You’re working on your feature branch. You’ve just committed some new changes and oh no! You’ve committed some debug statements by accident. From this point you’ve got two options:

Remove them and create a new commit

Remove them and change the previous commit using --amend

This post explores how git commit --amend operates and how you can leverage it to simplify your quick fixes and keep your commit history clean.

The Golden Rule of Amending

It must be stated before venturing deeper, that amending a commit will rewrite Git history. This is important to know as it will cause many issues for those who are also making changes to your branch. Rewriting history should only be done on commits that have not yet been pushed to a shared remote repository.

If you’ve already pushed the commit, stop and consider alternatives, or ensure you coordinate very carefully with your team. Messing with shared history can lead to significant merge conflicts and headaches for everyone involved.

There are ways of recovering from such situations found further in the “RECOVERING FROM UPSTREAM REBASE” section in git-rebase(1) but the simplest way of recovery is to not have done so in the first place.

This warning is not to scare you, or lead you away from amending commits. But, to warn of the implications when running such commands on published branches.

Example

You’re working on a new feature and you’ve just made a commit. You’re about to move on to the next task when you quickly scan your changes and spot a small oversight - perhaps a stray debug statement or a typo in a comment.

Consider a file user_auth.ts where you’ve implemented a login function:

function login(username: string, password: string): boolean {
// 👇 Oops, debug line!
console.debug(`Attempting login for ${username}`);
return username === "admin" && password === "secret";
}

The changes have already been committed:

$ git add user_auth.ts
$ git commit -m "feat(auth): implement login functionality"
[main abc1234] feat(auth): implement login functionality
1 file changed, 4 insertions(+)
create mode 100644 user_auth.ts

Upon noticing the debug log statement, console.debug(), one’s first instinct may be to:

Open user_auth.ts and remove the debug line.

Stage the change.

Create a new commit with the fix.

Walking through this, you edit user_auth.ts to remove the debug log statement:

function login(username: string, password: string): boolean {
return username === "admin" && password === "secret";
}

Then stage and commit the correction:

$ git add user_auth.ts
$ git commit -m "fix(auth): remove debug statement from login function"
[main def5678] fix(auth): remove debug statement from login function
1 file changed, 1 deletion(-)

Looking at the commit history after, there are now two commits:

$ git log --oneline
def5678 (HEAD -> main) fix(auth): remove debug statement from login function
abc1234 feat(auth): implement login functionality

This works, but it clutters the commit history with small, corrective commits that don’t add much narrative value. For a tiny fix likes this, especially on a commit that hasn’t been shared yet, there’s a much cleaner way.

Let’s rewind to the point just after you made the initial commit (abc1234) and notice the debug line. Instead of creating a new commit, let’s try amend the previous one instead.

Just as before, edit user_auth.ts to remove the debug log statement:

function login(username: string, password: string): boolean {
return username === "admin" && password === "secret";
}

Next, stage the change:

$ git add user_auth.ts

Now, instead of a new git commit, which makes a brand new commit. Let’s use git commit --amend, which will rewrite the previous commit we made. Since the original commit message (“feat(auth): implement login functionality”) is still perfectly fine, let’s add the --no-edit flag to keep it unchanged. If the message was no longer accurate, omit --no-edit and your configured text editor will open and allow changes to be made.

$ git commit --amend --no-edit
[main xyz7890] feat(auth): implement login functionality
Date: Tue Mar 4 10:30:00 2025 +1000
1 file changed, 3 insertions(+)
create mode 100644 user_auth.ts

Take special interest into how Git reports the commit as if it’s redoing the original one, but with the new latest changes included.

Let’s take a look at the history now:

$ git log --oneline
xyz7890 (HEAD -> main) feat(auth): implement login functionality

The Git history is much cleaner. The commit abc1234 has been replaced with a new commit xyz7890 which includes both the original login functionality and the removal of the debug statement, all under the original commit message.

Amending commits is ideal for:

Correcting typos in the last commit message.

Adding files you forgot to include in the last commit.

Making small modifications to files that were part of the last commit.

It’s important to remember the golden rule mentioned earlier: only amend commits that have not yet been pushed to a shared repository. Amending rewrites history, and doing so on shared branches can cause problems for other collaborators. For local, unshared work, it’s a fantastic tool for keeping commit history tidy and meaningful.

Amending Commits

I mentioned earlier that when amending a commit, we’re not editing the previous commit but rather replacing it. What does this actually mean?

The --amend argument is a rough equivalent to:

$ git reset --soft HEAD^
$ git add <desired items>
$ git commit -c ORIG_HEAD

In the above example, the current index is reset to HEAD^ (the previous commit). Git then prepares the commits content by taking the changes you’ve staged (e.g. with git add). Finally, a new commit is made using the information from the original commit via the [-c <commit> | --reedit-message=<commit>] argument.

By default, git commit --amend will amend any changes that are currently staged. If you want to be specific with which files you want, you can use the [-i | --include] or [-o | --only] arguments.

The [-c <commit> | --reedit-message=<commit>] argument will open the configured text editor as if committing normally but will prefill the editor with all the information from the previous commit^1.

Once pushed, the old commit will no longer be part of the branch history essentially erasing its existence. If you were the original person to push this commit, there is still a way to recover it via your local reflog. I talk more about this in the “Local Recovery” section below.

If you had previously pushed the original commit (abc1234), amending it locally creates a divergence. Your local branch now has the new amended commit (xyz7890), while the remote branch still has the old one. Running git status might show that your branch has diverged from the remote: it’s ahead by one commit (your new amended one) and also appears ‘behind’ by one commit (the old one you replaced locally, which is still on the remote).

Below is a diagram illustrating this divergence. Imagine you pushed your initial commit, abc1234, before you noticed the stray console.debug(). At that point, both your local and the remote main branch pointed to the same commit:

(origin/main)
|
v
... -- [abc1234] feat(auth): implement login functionality <- (HEAD -> main)

Now, you use git commit --amend to fix the mistake. This creates a brand new commit, xyz7890, and moves your local main branch to point to it. The remote repository is unaware of this change and still points to the original, now-outdated commit.

(HEAD -> main)
|
v
-- [xyz7890] feat(auth): implement login functionality
/
... ----
\
-- [abc1234] feat(auth): implement login functionality
^
|
(origin/main)

As far as your local is concerned, this is correct. The original commit a8d4a8d that’s on the remote is no longer the tip of your local branch. Remember, when we amended the commit, we essentially removed the old one, and created a whole new one in its place. The commit hashes are not the same. To combat this, we have to force push the changes we made to the remote - overwriting the commit we removed locally. This can be done by adding the [-f | --force] flag when pushing, git push -f.

Again, I must reiterate that force pushing changes without regard to others working on the branch will, in most cases, cause many headaches them. The hard and fast rule is this:

For branches that are being worked on collaboratively, create a new commit.

For branches that are the basis of other branches that people are working on. Also, create a new commit.

For branches totally local or isolated from others modifications, force push with caution.

Local Recovery

So, you’ve amended a commit, and perhaps you’ve realised you made a mistake during the amend, or you actually preferred the original commit. You might be wondering: if amending replaces the old commit, is it gone forever? The good news is, locally, Git often keeps a ‘ghost’ of that old commit for a while, and you can usually get it back thanks to Gits reflog.

Think of git reflog as your personal Git diary. While git log shows the public, sharable history of your project, git reflog records almost every move where HEAD (your current position) changes in your local repository. This typically includes commits, amends, resets, merges, and even branch switches. It’s your safety net for when things go sideways locally.

When you amend a commit (say, abc1234 becomes xyz7890 as in our earlier example), the original commit abc1234 is no longer in your current branch’s direct history. However, the reflog remembers that HEAD used to point to abc1234 right before it was moved to xyz7890.

If you run git reflog after the amend, you might see something like this:

$ git reflog
xyz7890 (HEAD -> main) HEAD@{0}: commit (amend): feat(auth): implement login functionality
abc1234 HEAD@{1}: commit: feat(auth): implement login functionality
# ... older actions ...

And would you look at that!

HEAD@{0} is your most recent action: the amended commit xyz7890.

HEAD@{1} is the state before that amend: the original commit abc1234.

The reflog shows the commit SHA (abc1234) and a pointer (HEAD@{1}) to where HEAD was. This pointer is the key to recovery.

If you want to go back to the state before the amend, you can use git reset --hard with the reflog pointer:

# Be careful: `git reset --hard` will discard any uncommitted changes in your
# working directory and staging area.
# Ensure any current work you want to keep is committed or stashed.

$ git reset --hard HEAD@{1}
HEAD is now at abc1234 feat(auth): implement login functionality

After running this command:

The main branch (or whatever branch you’re on) will now be pointing to abc1234.

The working directory and staging area will be updated to match the state of the commit abc1234.

The amended commit xyz7890 is now “orphaned” (no branch points to it directly), but it too will still be in the reflog for a while if you need to revert again.

You can verify this state by checking git log --oneline:

$ git log --oneline
abc1234 (HEAD -> main) feat(auth): implement login functionality
# ... commits before abc1234 ...

It’s as if the amendment never happened (locally, at least).

While incredibly useful, there’s two main things to remember with the reflog:

It’s strictly local: The reflog is not shared when you push to or fetch from remote repositories. Each collaborator has their own private reflog. This recovery technique is for your local repository only.

It’s not forever: Reflog entries don’t live indefinitely. Git has a system for expiring old entries - typically after 30 days for unreachable commits like an original pre-amend commit, and 90 days for reachable ones. Git will periodically prune these old entries to save space. So, it’s a great short-to-medium-term recovery tool, but not a permanent archive of every misstep.

Conclusion

Using git commit --amend is a powerful way to keep your local commit history clean and meaningful. This, in addition to git reflog, gives you the ability to amend with confidence knowing you have a way to undo those changes locally if needed.

Thanks for reading,
- Brook ❤

Read this post in its original formatting at https://brookjeynes.dev/posts/everyday-git-amending-commits/

Footnotes

1: Take an existing object, and reuse the log message and the authorship information (including the timestamp) when creating the commit. - man git-commit(1)