I think I know why you're getting confused (well, it's because "git can be confusing" :-) but I mean more specifically).
First, let me mention that pull
is just fetch
followed by merge
.1 The fetch
step does not affect your work at all (this is kind of the point of separating out fetch
from everything else, fetch
gets you other peoples' work but does not touch anything of your own). It's the merge
step that affects your own work. (It's not that important here, but it's another part of git that I found confusing at first.)
Let's take a look at revision IDs, specifically those big ugly 40-character things like e59f6c2d348d465e3147b11098126d3965686098
. You will see these all over the place, often in the shortened form you quoted above: f8e8370 HEAD@{2}: ...
These things—they are SHA-1 hashes, so they're called "sha-1s"—are what git uses internally to identify commits. Well, they identify everything in the repository, not just commits, but the point is, these are the "true names" of the commits. They're ugly for humans to use, but these numbers always work, and in fact, most of the time you give git a more human-friendly name, git just immediately turns it into a sha-1. It happens when you use the name HEAD
, and it usually happens when you use the name branch1
too.
(There's one huge exception to the "name turns into sha-1" rule, which is when you're doing git checkout
. Here a branch name is kept as a branch name. You can give checkout
a sha-1 instead, which gets you what git calls a "detached HEAD"; with a branch name your head doesn't get guillotined off, and instead git puts you "on the branch". But other commands, including git reset
and git merge
, don't care so much. Merge does save the branch name to put in the commit message, but the merge action uses the sha-1.)
There's only one sha-1 for any commit, by definition. That sha-1 is the "true name" of the commit. There can be many other names for the commit—we can call it Joe-Bob if we want: seriously, you can assign names to any commit2—but ultimately git needs the sha-1.
Let's take another look at that reflog output you quoted (or a bit of it):
f8e8370 HEAD@{0}: reset: moving to HEAD~1
ce5fceb HEAD@{1}: pull origin master: Merge made by the 'recursive' strategy.
f8e8370 HEAD@{2}: commit: commit at 11:35
This uses shortened 7-character sha-1s rather than the full ones, but clearly the first and third are the same (and if we looked at the remaining 33 characters they'd match too). They also have alternative, more-human-friendly names: HEAD@{0}
and HEAD@{2}
.
What that means is that HEAD@{2}
is just a user-friendly name for commit f8e8370...
, and so is HEAD@{0}
. There's a lot more to this @{text goes here}
suffix stuff, but for the moment, just note that there is a name (HEAD
), the at-sign @
, and then curly brackets around the digits. The name tells git which reflog to use—the one for HEAD
, in this case; there's one for each branch too—and the number tells git how far back to look in the reflog. So HEAD@{0}
is "what HEAD resolved to 0 times ago", i.e., go back zero times to an earlier version of HEAD
. That means use the value it has right now, f8e8370...
. Meanwhile HEAD@{2}
is "what HEAD resolved to, before the last two changes". Two changes ago, HEAD
resolved to the same commit sha-1 as it does now.
Now, you also mentioned HEAD~1
when you quoted this bit:
git reset --hard HEAD~1
While this looks a lot like HEAD@{1}
, it's very different. It uses the tilde ~
character instead of at-sign and curly brackets. To describe how this really works, we have to get into the "commit graph". I won't cover this completely here, but the general idea is that this counts back some number of "parent commits". So git finds the SHA-1 for HEAD
right now, then steps back to a parent commit, one time.
(To see all the rules for this, use git help revisions
or look at the gitrevisions
page here.)
Finally, let's look at git reset
. This command can do a lot of stuff,3 but the way you're using it, git reset --hard <revision>
, does just one big thing, at least conceptually: it resolves the <revision>
argument to a commit sha-1, and then it re-sets your current branch, index, and work-directory to that commit.
You can spell the commit ID however you like. HEAD@{number}
resolves to a sha-1. HEAD~number
resolves to a sha-1. The gitrevisions
page gives you all the other ways to specify a sha-1, including a shortened sha-1 like f8e8370
(you can cut and paste these from git log
or git reflog
output, for instance).
To undo a real merge (but not a "fast-forward merge", which technically isn't a merge at all), the name HEAD~1
will in fact always work. This is because a real merge creates a merge commit that has two parents. The "first parent" is the previous tip of the current branch (the second parent is the commit that was merged-in) and the tilde syntax always uses first-parents. "First parent" is an important concept in commit graphs since it identifies what most people mostly think of as "the main line of development".
The name HEAD@{1}
will also always work, if you use it soon enough, so that there are no additional changes to HEAD
. This is because the most recent change was the merge that git pull
did. The value HEAD
had just before that was the value HEAD
had before git pull
did its merge. This version works even if the merge was a fast-forward!
The big problem with the HEAD@{n}
syntax is that if you've made any other changes to HEAD
, you have to keep bumping the number up: maybe you need the value two-changes-ago, or three-changes-ago.
(The remaining problem occurs when the merge that git pull
does fails. In this case, HEAD
is not yet changed and what you need to undo the merge is to run git merge --abort
.)
The "raw SHA-1" method always works too, but you have to find (and then cut and paste) the SHA-1 (usually not that big a deal, really).
1You can change this: you can have pull
do a fetch
followed by a rebase
instead. Rebasing is probably more appropriate for most people, but it's not the default.
2In fact, this is what a tag is, in git: a name for a commit. (Git has two kinds of tags, with the "annotated" tag providing additional stuff, not just a name, and the lightweight tag providing just a name.)
3Git usually crams two to five separate commands into one git do-a-thing
CLI command, usually because they're related internally, even if not so much externally. For instance, checkout
can either get you on a branch, or simply extract files with options like re-creating merge conflicts. The second is only related to the first in that the first ("get on a branch") requires extracting files.