-2

There are a lot of questions regarding this on here and none of them specifically answers it.


I used git log and I want to go back to a specific commit without removing any previous history. I just want to create a new commit of that old revision.

I read the answers on here and most of them said to use git reset HARD <commit hash>, however, other answers said not to use this as it is not good practice since it will remove history.

I also used git checkout <commit hash> and that is only a read only option.

All I want to do is go back to a previous commit without altering any previous history.

[per Q&A in comments, edited somewhat]

  • Q: If you have commits "A, B, C, D, E" and HEAD is now at E, and you want HEAD to be at C, do you want "A, B, C" or do you want "A, D, E, B, C"?

  • A: It should still exist and remain the same so my new published work will be "A, B, C, D, E, C" where C is HEAD now.

Community
  • 1
  • 1
robben
  • 181
  • 3
  • 13
  • Your question is vague. In particular, what exactly does "previous history" mean? Does it mean "previous to the commit you want to back to" or "previous as in any history that already exists, before *or* after that commit"? The easiest way to continue working from that point would to first checkout that commit, then create and checkout a branch, then start committing. This would lose nothing. – Lasse V. Karlsen May 15 '17 at 15:28
  • @LasseV.Karlsen But i don't want to create a new branch. I want that revision to be the HEAD of that current branch. And by "previous history" i mean all commits that happened before. – robben May 15 '17 at 15:31
  • Before **what**? Your current HEAD (after that commit) or before "in time", anything already committed should stay the same. Can you please post an example of what you have and what you want to have? – Lasse V. Karlsen May 15 '17 at 15:33
  • If you have commits "A, B, C, D, E" and HEAD is now at E, and you want HEAD to be at B, do you want "A, B" or do you want "A, C, D, E, B"? – Lasse V. Karlsen May 15 '17 at 15:34
  • @LasseV.Karlsen For example, the history has "A, B, C, D, E" and now HEAD is currently on "E". All I want to do is go back to revision "C" and make that the new HEAD. – robben May 15 '17 at 15:35
  • And what should happen to D and E? – Lasse V. Karlsen May 15 '17 at 15:37
  • @LasseV.Karlsen It should still exist and remain the same so my new published work will be `"A, B, C, D, E, C"` where C is HEAD now. – robben May 15 '17 at 15:39
  • OK, so you want disregard anything that D and E did, but you don't want to remove the commits? Say D added a file and E changed that file, you want to "go back" to how C was, without that file? Is that it? – Lasse V. Karlsen May 15 '17 at 15:40
  • @LasseV.Karlsen Correct. Sorry for making this complicated. – robben May 15 '17 at 15:46
  • Not sure why this is getting down voted? Please explain why you down voted! – robben May 15 '17 at 16:55
  • @robben: I'm not one of the downvoters, but I will say that the example "if the previous series of commits were `A--B--C--D--E` I would like to have a new commit that looks exactly like `C`" would have helped a lot. :-) – torek May 15 '17 at 19:30
  • @torek Lol i bet :). I will add that in there – robben May 15 '17 at 19:38

4 Answers4

1

TL;DR answer

Make sure your repository is clean (git status says nothing to commit, etc), and that you are at the top of your work-tree (where the hidden .git directory lives). Then:

git checkout master
git rm -rf -- .                 # remove it all: the scary step :-)
git checkout <hash-of-C> -- .   # but this puts everything back
git commit

(for the last command, you can optionally add -C <hash-of-C> again, to re-use C's log message). Use git log or similar to find a suitable hash ID for commit C.

There's a slightly shorter way, and in a lot of cases you don't need the git rm -rf -- . step at all, but I'll leave that for after the explanation.

There are several other ways to do this, but the above is the most straightforward.

Explanation

Git is all about adding new commits while keeping everything already committed, forever.

Hence, suppose you have a repository with just five commits, which you made by starting with one commit A:

A   <--master

and then adding a second commit B that looks back at A:

A <-B   <--master

and then a third commit C:

A <-B <-C   <--master

and so on to eventually end up with:

A--B--C--D--E   <-- master

In each case here, we've used a single-uppercase-letter ID (A, B, ... Z) for our commits, which means we'd run out after just 26. Git uses those incomprehensible hash IDs instead, so it will never run out of unique IDs for its objects, but the downside is that they're incomprehensible and we have to just cut-and-paste them or whatever. Moreover, Git assigns them; we have no control over the hash IDs; we just make commits and suddenly there's a new hash.

Note also that A does not point to any earlier commit, because it can't: there is no earlier commit. All other commits, however, point back to their parent. The name master simply points to the latest commit on branch master. That, in fact, is how Git knows that it's the latest commit—and how Git finds the earlier commits, too!

Again, Git is all about adding new commits. If you have seen some of the Star Trek episodes with the Borg, I like to call Git the Borg of Source Control: when you commit, it will add your technological distinctiveness to its collective. You run git commit, Git saves everything in the new commit, and Git makes the current branch (master, in this case) point to the new commit. The new commit automatically points back to whatever was the tip of the branch before.

What you want, then, is to make a new commit that "looks just like C" except for one thing: it points back to E, rather than to C. All the rest then happens automatically:

A--B--C--D--E--C'  <-- master

where the name C' means "looks and smells a lot like C, but not quite the same" (because it points back to E, not to B—and it probably has a different date-stamp too).

That's all fine for defining the goal, but how do you make this new commit that "looks and smells a lot like C"? Well, each commit has an attached source tree. When you make a commit, Git turns what Git calls the index into the attached source tree. This is why you have to git add things after you edit them in your work-tree: git add means "copy the updated version I put in my work-tree, into the index." This overwrites the old, un-edited index version, so now the updated file is ready to commit.

This is a key fact about Git: The index is where you build the next commit you will make.

In normal usage, you just git checkout a branch, which sets your index and work-tree to match the tip commit of that branch—such as E for master. Then you edit some file(s), git add them to copy them back into this hidden index, and git commit to make the new commit out of them.

In this case, though, you want to get all your files back the way they were at commit C. The git checkout command can do this: instead of:

git checkout <hash-or-branch>

we need the longer form (which ideally should have been a different Git command, but it isn't):

git checkout <hash-or-branch> -- <files>

This tells Git: go look in the hash I gave you (if you give it a branch name, it turns the branch name into a hash ID) and then copy each of its files to my work-tree, writing that file "through" the index.

If your commit C has six files (README and five others), each of those files is copied into the index and then on into your work-tree. So now your work-tree README, and the other files, are updated to match commit C. They're also already staged for committing: you don't have to git add them to the index because git checkout copied them through the index.

The reason we might need to git rm -rf -- . first is that commit E might have seven files: maybe in D or E, you git add-ed a file new.ext that didn't exist in commit C. If that's the case, git checkout <hash-of-C> -- . won't remove new.ext at all. Hence, we do a first pass to empty out the index and work-tree entirely, so that the git checkout <hash-of-C> -- . re-populates the index and work-tree from commit C without leaving anything behind from E.

Now you're ready to commit as usual, making a new commit as usual. Adding the -C flag to git commit tells Git to retrieve the initial commit message from an existing commit (it's a bit of eerie or maybe sad coincidence that we're copying a commit we have been calling C, and using a -C flag, but it's just coincidence).

Note that if you now git show this new commit C', what Git will do is extract commit E, then extract C', and then diff the two. What you will get is Git's instructions for "how to convert the contents of commit E into the contents of commit C". This is another key item about Git: a git diff old new produces a set of instructions, Git's way of telling you how to convert commit old into commit new. It's not necessarily how you did it, it's just some way to do it. When you compare adjacent commits, like A-vs-B or D-vs-E, you see an approximation of what you did. When you compare distant ones, like A vs E, you get Git's description of how to go straight from A to E. But each commit itself has a complete snapshot of every file, as it was in the index at the time you ran git commit.

The slightly shorter way

If you've read this far, you might want to know a short-cut that avoids doing git rm -rf -- .. Instead of the four commands above, we can use three:

git checkout master
git read-tree --reset -u <hash-of-C>
git commit

The first and last are the same; it's the middle one that's mysterious. The git read-tree command is an internal Git command—one of what Git calls plumbing commands, meant for use in scripts—that manipulates the index, given particular commit or tree hash IDs (really, anything that Git can convert to a tree hash). The --reset option means "throw out the current index and replace it with the result of reading". The -u flag means "update the work-tree based on what happened in the index."

This means that if there are seven files in the index now, but the commit we read has just six, Git will remove the extra file (from the index and, with -u, the work-tree). So it accomplishes the removal as well as the refilling, all in one step.

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

If you only want to revisit a specific commit, then use git checkout <SHA>. From there you'll enter into detached HEAD mode, which won't alter your commit history at all.

If you want to save your changes, you'll need to create a branch based on that specific SHA with git checkout -b.

Makoto
  • 104,088
  • 27
  • 192
  • 230
  • I want to revisit a specific commit and push that as the latest publish work. – robben May 15 '17 at 15:24
  • I've described the process for doing that succinctly - at least, insofar as getting the older SHA and creating a branch. How you release that to as your latest work is up to you - merge it into master or use that as your new authoritative branch. – Makoto May 15 '17 at 15:26
  • So you mean you want to remove any history that follows it? – Lasse V. Karlsen May 15 '17 at 15:29
  • But i don't want to create a new branch. I just want to commit that previous revision hash to be the new HEAD without having to modify any previous commits. – robben May 15 '17 at 15:29
  • @LasseV.Karlsen All previous history should be unaltered – robben May 15 '17 at 15:30
  • Again you're being vague. What do you mean by "previous"? Please explain what you want the final result to be like because you're using terms that seems to contradict what you want. – Lasse V. Karlsen May 15 '17 at 15:32
  • To answer your question without helping you (because it seems this is not what you want), to make HEAD point to that commit, do a checkout, `git checkout SHA`. Now HEAD is on that commit. – Lasse V. Karlsen May 15 '17 at 15:32
  • @LasseV.Karlsen By "previous history" i mean all commits that happened before it. All I want to do is create a new HEAD based off that revision hash. – robben May 15 '17 at 15:33
  • @robben: You seem to misunderstand. Without rewriting history in any way, the *only* way to get a specific commit ***and*** make any modifications has been described above. If you want to go all the way back to that specific commit and make that *the* authoritative HEAD, then you're going to wind up rewriting history. – Makoto May 15 '17 at 15:37
0

Have you thought about just checking out the commit in question?

git checkout sv6e5uj566

where sv6e5uj566 is replaced with the hash of the commit you're interested in. Then you can create a branch from there with

git checkout -b your-new-branch-name

and start making changes from there. This won't affect your history at all, but allows you to go back to that specific commit and branch from there.

wogsland
  • 9,106
  • 19
  • 57
  • 93
0

To go back in history, just do a git checkout providing the ID of the revision you want to go back to. You can then commit there and, if you want, you can create a branch there. Or you can create the branch on the revision you want to go back to, then check it out and then commit.

eftshift0
  • 26,375
  • 3
  • 36
  • 60