0

This question is related to the one I asked today. I have read varying methods on both my post and other posts on SO which has got me very confused, so I am clarifying here.

Let's say I am in branch1, I pull branch2 in it, the merge can be either fast forward or recursive. The pull brings in a lot of commits in branch1, which I want to undo. One approach I have seen on SO is using the command

git reset --hard SHOW-HEAD

which seems to working fine.

I also tried this to accomplish the same by this command

git reset --hard HEAD~1

I have read on few comments that this command won't work if pull brings in multiple commits. However, according to my observation, irrespective of the number of commits, HEAD~1 always refers to last commit before the pull. Is that correct? Or is there any scenario where it might not hold?

Here is my git reflog, The ce5fceb pull brought in multiple commits, but git reflog moved up by 1 commit only. And by doing git reset --hard HEAD~1,I could undo that git pull.

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
b2c928b HEAD@{3}: commit: commmit at 11:32
a6fdbef HEAD@{4}: commit: commit at 7:35
40a147c HEAD@{5}: commit: commit at 7:27

Community
  • 1
  • 1
Max
  • 9,100
  • 25
  • 72
  • 109

2 Answers2

1

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.

torek
  • 448,244
  • 59
  • 642
  • 775
0

In order to undo pull (or whatever) all you need is to make HEAD point at the commit that HEAD pointed to before the pull operation. You can find it in reflog (you know that). Based on the reflog you showed I can say that this is HEAD@{2} (or f8e8370), then:

git reset --hard HEAD@{2}

or you can specify the old value of a reference

git reset --hard f8e8370
neshkeev
  • 6,280
  • 3
  • 26
  • 47