Everyday Git: Amending commits
This post is part of Everyday Git, a series that explores Git features from the command line, with real-world usage in mind.
Introduction
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
Approach 1: Creating a new commit
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.
Approach 2: Amending the commit
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 commit’s 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.If you want to use the current staged files and an additional file:
$ git commit --amend -i user_auth.ts
or, if you only want specific files:
$ git commit --amend -o user_auth.ts
The [-c <commit> | --reedit-message=<commit>]
argument will open the configured text editor as if commiting normally but will prefill the editor with all the information from the previous commit1.
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 Git’s 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 commitxyz7890
.HEAD@{1}
is the state before that amend: the original commitabc1234
.
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 toabc1234
. - 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 ❤
Footnotes
1: Take an existing man git-commit(1)