Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
How can I git checkout the previous HEAD?
After switching to a different branch, git checkout -
can move me back to the branch I came from. This is handy for times when I wonder "wait, what was that last branch again?"
But this does not work for everything situation when you switch from commit to another. It works for branches, but when you do git pull
and receive some new commits, git checkout
won't switch you back to the previous one.
Is there a command for "switch back to the previous commit I was on" that works in every situation that changes HEAD?
1 answer
There are many ways to get the previous states of HEAD
, and which one to use will depend on each situation.
Git keeps reference logs (also called "reflogs"), that "record when the tips of branches and other references were updated in the local repository".
To provide a simple example, let's start an empty repository and add some commits to it (for the sake of brevity, some outputs were ommited):
$ mkdir project
$ cd project/
$ git init
# create first and second commits
$ echo first >> file.txt && git add . && git commit -m"first"
$ echo second >> file.txt && git commit -am"second"
# see commit history
$ git log --oneline
899ed6c (HEAD -> master) second
d142056 first
Now we can run git reflog
to see the reference logs:
$ git reflog
899ed6c (HEAD -> master) HEAD@{0}: commit: second
d142056 HEAD@{1}: commit (initial): first
Basically, this is the history of how HEAD
has changed (most recent first). We can also note the notation HEAD@{N}
, that specifies the Nth prior value of HEAD
.
Therefore, we can use this in any command that expects a commit/reference, such as git log HEAD@{1}
, git diff HEAD@{1}
, git checkout HEAD@{1}
and so on. HEAD@{N}
will use the reflogs to search for the commit that corresponds to the Nth prior value of HEAD
, and all will work as if you provided the commit hash/branch name directly.
Therefore,
HEAD@{0}
is the same asHEAD
.
The reflogs also record when you switch to another branch:
# create a branch and switch to it
$ git checkout -b somebranch # or "git switch -c somebranch" for Git version >= 2.23.0
$ git reflog
899ed6c (HEAD -> somebranch, master) HEAD@{0}: checkout: moving from master to somebranch
899ed6c (HEAD -> somebranch, master) HEAD@{1}: commit: second
d142056 HEAD@{2}: commit (initial): first
# add a commit to the branch
$ echo branch >> file.txt && git commit -am"branch"
$ git reflog
d0e4769 (HEAD -> somebranch) HEAD@{0}: commit: branch
899ed6c (master) HEAD@{1}: checkout: moving from master to somebranch
899ed6c (master) HEAD@{2}: commit: second
d142056 HEAD@{3}: commit (initial): first
$ git log --oneline
d0e4769 (HEAD -> somebranch) branch
899ed6c second
d142056 first
Note how it records the branch switching from master
to somebranch
. It's also noteworthy that all the HEAD@{N}
references had also changed (ex: HEAD@{0}
was pointing to commit 899ed6c
, now it's d0e4769
). The reflogs numbers are updated everytime there's a new record in it, with {0}
being the most recent one.
Just to add one more example, let's say someone else updated the remote repository and I want to get those updates:
$ git checkout master # or "git switch master" for Git version >= 2.23.0
$ git pull origin master # get updates from the remote
$ git reflog
0ff62c8 (HEAD -> master, origin/master) HEAD@{0}: pull origin master: Fast-forward
899ed6c HEAD@{1}: checkout: moving from somebranch to master
d0e4769 (origin/somebranch, somebranch) HEAD@{2}: commit: branch
899ed6c HEAD@{3}: checkout: moving from master to somebranch
899ed6c HEAD@{4}: commit: second
d142056 HEAD@{5}: commit (initial): first
PS: I skipped the parts where I configured the remote and added commit
0ff62c8
to it.
git pull
also updates HEAD
, hence this change is recorded in the reflogs as well. As you can see, the reflog is a general way to know how HEAD
has changed in a particular repository.
But git pull
also created two additional references:
$ git log FETCH_HEAD --oneline -1
0ff62c8 (HEAD -> master, origin/master) another
$ git log ORIG_HEAD --oneline -1
899ed6c second
Those are described in the documentation:
-
FETCH_HEAD
: records the branch which you fetched from a remote repository with your lastgit fetch
invocation. Note:git pull
callsgit fetch
, that's why this was created. -
ORIG_HEAD
: is created by commands that move yourHEAD
in a drastic way (git am
,git merge
,git rebase
,git reset
), to record the position ofHEAD
before their operation, so that you can easily change the tip of the branch back to the state before you ran them. Note: after fetching,git pull
also does a merge or a rebase (depending on the configuration or command line options), that's why this was created.
So FETCH_HEAD
refers to the remote branch that was fetched from the remote repository (in this case, origin/master
, which corresponds to commit 0ff62c8
), and ORIG_HEAD
points to where HEAD
was before merging it to 0ff62c8
. We can see those in the commit history:
$ git log --oneline
0ff62c8 (HEAD -> master, origin/master) another
899ed6c second
d142056 first
In the same documentation you'll also find some other special references, such as MERGE_HEAD
, that records the commit(s) which you are merging into your branch. Usually it exists only during the merge, and it's deleted after it finishes (so you can check it when there's a conflict, as it's deleted only after you solve or abort it).
Those might be helpful if you want more details about the status of HEAD
during a specific operation. But as general solution, you can always check the reflogs.
The documentation also describes some other ways to get previous states of HEAD
in a particular point in time, such as HEAD@{yesterday}
, HEAD@{5.minutes.ago}
or HEAD@{2024-08-21T08:30:00}
. You can also use spaces, but in this case the reference must be quoted: git log "HEAD@{5 minutes ago}"
works, git log HEAD@{5 minutes ago}
doesn't.
And remind that this notation works not only with HEAD
, but with any other branch. So you can use things like master@{yesterday}
(where the master branch was pointing to yesterday) or somebranch@{2}
(the second previous value of somebranch
).
And if you omit the reference, it'll refer to the current branch. So if your current branch is master
, then @{yesterday}
will be equivalent to master@{yesterday}
. But note that it might not be the same as HEAD@{yesterday}
, because HEAD
could be pointing to a different branch at that time.
As a curiosity, @
is a shortcut for HEAD
.
Beware: if you use negative numbers, it's a totally different thing.
@{-N}
means "the Nth branch/commit checked out before the current one". And you can't put a reference before it, soHEAD@{-1}
doesn't work. Negative numbers always refer to commits that were checked out before. As a curiosity, thegit checkout -
command you mentioned is a shortcut forgit checkout @{-1}
(since version 1.6.2).
Finally, it's important to remember that the reflog is stored locally, and it's not shared with the remote repositories when you fetch, pull or push.
Even if you clone the same remote repository in another folder of the same machine, the new clone will have its own separate reflog (which means that HEAD@{N}
might not be the same in every clone).
And if you're using git-worktree, each worktree will also have its own separate reflog.
2 comment threads