简体   繁体   中英

How do I checkout to a point in time (i.e. commit) of a Git branch?

I want to roll my Git branch back to a specific commit. So I run git log and find the commit SHA hash, and run git checkout <myhash> .

This usually works just fine, but this time something was fishy. As I looked at git log again, I see that the latest commit is correct, but I miss a lot of commits further down. It dawns on me: this commit is part of a merge from another branch ( another ), and I see the history from that branch, pre-merge.

master's log, pre-merge:        D-C-----B'-B-A-----F
another's log:              7-6-----5-4--------3-2-F
master's log, post-merge: M-7-6-D-C-5-4-B'-B-A-3-2-F

I checkout commit 5 and get the commits in the history from another :

5-4-3-2-F

But I'd like the commits in the history from master , post-merge:

5-4-B'-B-A-3-2-F

I've made a repo where you can test this:

$ git clone git://github.com/henrik242/Git-Branch-Test.git
$ cd Git-Branch-Test
$ git checkout -t origin/another
$ git log  ## The commits are named "test [2-7]"
$ git checkout master
$ git log  ## master's original commits are named "test [A-D]".  
$ git checkout 68c1226a0c  ## test 5 
$ git log  ## We now have the commits in the history from the "another" branch,
           ## even though this commit exists in the "master" branch as well

I can almost use git rebase -i to do what I want:

$ git branch back-in-time
$ git checkout back-in-time
$ git rebase -i 18b1a648bc  ## the SHA1 of 'test 4', the commit before 'test 5'

An editor spawns: Remove the unwanted commits. Save and exit, and git log now shows the wanted commits:

5-4-B'-B-A-3-2-F

The problem is that git rebase -i doesn't show the commits in the same order as git log , which makes it difficult to pick the right commits.

Merge does not intermingle commits, it just joins the branches as depicted in @Jefromi 's answer. So, of course, you can restore the state of the tree at any given time but if the time is prior to the merge, you will need to take care to pick the right branch. So, I guess you should use git log --oneline --graph so you can pick the latest commit on the respective branch at the given time. When you check that commit out, it will obviously only have the commit history of that branch at the given time.

If you wanted to really intermingle commits from both branches, you'd need a work-flow that uses git cherry-pick or git rebase -i to order the commits from both branches chronologically into a straight branch. This calls for all kinds of problems, the least being a lot of merge conflicts. Also, the intermediate states will be most likely semantically inconsistent.

I can't say this enough times: the history you're asking for never existed. If you want to manually create it, but there is nothing at all like that for you to check out. A merge does not in any way mash together commits into some kind of amalgamated history. If your question is "how do I manually create history like this" then ask that. But you seem to already know how to use rebase.

I think your primary misunderstanding may be of git-log's output. The order commits are shown by git log is essentially an arbitrary choice. History isn't linear, but it has to list them one by one, so it does. That order does not mean one commit is the previous one's parent, and therefore it does not mean that if you check out one of those commits, you'll have the union of everything printed below it. It may print lists of commits like you've put in your question, but in your example repository, this is what the history looks like. You can see it with gitk --all or git log --graph .

1 - 2 - 3 - 4 - 5 - 6 - 7 (another-branch)
 \                       \
  A - B - C - D - --------M (master)

That's it. A merge doesn't magically mash those branches together. It creates a single commit, whose parents are the merged commits, containing the combination of the content of the merged commits. So you can check out commit 2, or commit B, but there is no "commit 2 in the master branch". You could, if you like, check out commit B and merge commit 2, and see what you get.

The list of commits you say you want, as far as I can tell, is what you'd get by merging commits 5 and B, and flattening it in an arbitrary way. If you just want the result of the merge:

git checkout -b foo <commit-5>
git merge <commit-B>

If you actually want flat history like that:

git checkout -b foo <commit-5>
git rebase -i <commit-B>
# reorder them however it is you wanted them

But these are all user decisions. You're creating a state of the repository that never existed. Since it never existed, you have to create it.


As for the discussion about hashes in the comments, again, an SHA1 uniquely identifies a commit. The SHA1 for commit 2 is the SHA1 for that commit only. You have not proven me wrong with your test repo, you've proven me right: there is only one commit 2 there, and it only has one hash.

A commit is an object containing a few things: metadata (author/committer name, email, date; commit message), a reference to its parent(s), and the tree it represents (think of it as a snapshot of the content). So commit 2 represents that particular snapshot. Nothing more, nothing less. Its parent is commit 1. This is not a matter of timestamps; a commit knows its parents by their SHA1s. Commit 2's parent is commit 1, no matter what. There is only one commit 2, whether you reach it from the master branch or another branch - look at the picture, and you can see how it's in the master branch, via 7, 6, 5, 4, 3.

I'll just answer my own question: In order for this workflow to work (and make some kind of sense), I have to use a topological ordering of the git log: git log --topo-order . This way the logs look like this:

master's log, pre-merge:                D-C-B'-B-A-F
another's log:              7-6-5-4-3-2------------F
master's log, post-merge: M-7-6-5-4-3-2-D-C-B'-B-A-F

Now the order in git rebase -i makes sense, since it's in topological order as well. Just snip away the bottom commits until you reach the commit in question and save, and voila!

(As a side note, I needed this workflow in the first place because I tried to pinpoint which commit introduced a bug. Afterwards I realized git bisect does a better job on this than me.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM