And the Second Top Voted Question on StackOverflow Is...
...about version reversions on StackOverflow. Learn what commands you should know to correct any mistakes in your repo.
Join the DZone community and get the full member experience.
Join For FreeIf you access the StackOverflow votes page, you would see the top voted questions on StackOverflow.
It's not hard to notice that 3 out of top 4 voted questions on StackOverflow are on Git!
This may mean one of two things (or both): Git is hard/unintuitive, or Git is one of the most common technologies used. It's the combination of both.
Let's analyze the top voted question for Git: "How do you undo the most recent commit in Git?"
Or in a picture:
The user says he has committed but haven't pushed yet. Why do you think this is perplexing in Git? Here are some of the adjacenta questions that might be asked:
- You might expect an API with
revert/undo/checkout
into a different commit. - If it's committed, do you undo it with a new commit? Can you go back in time?
- Git says everything is stored forever, so how do you really undo?
- What is the command to go back in time?
- There is always this warning about changing and undoing stuff that was already pushed. Are we at risk?
Even if some of the above APIs existed, there are too many options. Let's review the first answer.
The first answer suggests to use the git reset
command. The best way to think about git reset
is as if you can ask the HEAD pointer to move across the Git history graph.
Explanation of Terms
In Git, we commit. Each commit adds to the history, which is a commit in time, right?
History can be described as a graph, meaning each commit is pointing to another commit (parent commit in case of non-merge commit), and thus we have a directed acyclic graph, yet another term.
So the answer asked the user to go back in time with the git reset HEAD~
command:
We have moved to the past!
HEAD is pointing to the current branch location, ~
is pointing to minus one pointer, the parent of the current commit that head is pointer to.
So by using the reset command, we are telling Git: "Git whatever you are pointing to right now (HEAD) point to minus 1 place." The previous place, in other words. And Git happily does this for us.
Note that as we didn't specify any flags (like --hard
) to the reset commands; it will not change anything in our working directory, so only the head moved there.
As we wanted to revert something in our current working directory, and we just moved the HEAD backward before that change, this means that our current directory is now cluttered with the change we wanted to revert.
So just manually revert that change locally on your working directory and make another commit.
A Few Words About The HEAD
We said that HEAD is pointing to the last commit in the currently checked out branch. As gitglossary tells us:
HEAD: The current branch. In more detail: Your working tree is normally derived from the state of the tree referred to by HEAD. HEAD is a reference to one of the heads in your repository, except when using a detached HEAD, in which case it directly references an arbitrary commit.
HEAD is normally pointing to the current branch...except when not, and when it's not it's pointing directly to a commit. Let's see an example. We have a local Git repository, so let's print the HEAD.
$ cat .git/HEAD # => HEAD is a file in .git directory - yeah on the base dir, let's print it.
ref: refs/heads/master # => So head is simply this line of text, this looks like a branch, let's print it.
$ cat .git/refs/heads/master # => Now we are printing what head points to, it should be the commit of the branch..
a15d580cc90d47a88f7f971914d45ff5a0e30eef # => So this is the commit which master points to. But how do we know this number is a commit?
$ git cat-file -t a15d580cc90d47a88f7f971914d45ff5a0e30eef
commit # => Yes git is saying this SHA-1 is a commit. Was not persuaded yet? How about this:
$ git cat-file -p a15d580cc90d47a88f7f971914d45ff5a0e30eef
tree 7d80e5c527e9a1ec7f79f68386ce9710f1e048ce # It makes shadow like a commit.
parent ddf47ffcb19e2aee4839cae40e79fd7579fc637f # It walks like a commit.
author Tom <tomer@email.com> 1537007909 +0300 # It sings like a commit.
committer Tom <tomer@email.com> 1537007909 +0300 # It burks like a commit.
my commit message # => It talks like a commit.
# So its a commit :)
$ git log --oneline # => is the commit a15d580... really the head?
* a15d580 - (HEAD -> master) test (2 days ago) # => Yes a15d580 is indeed our latest commit where head points to!
* ddf47ff - (develop) added file to folder (9 days ago)
* 9d0e101 - hi (10 days ago)
Basically, the picture is like this:
What we see in the above picture is the bash commands above, we see that:
- HEAD points to current branch tip.
- Our current branch ref points to a commit in our branch in our case it's the latest.
- The commit points to a tree.
- A tree points to a list of blobs and trees (and trees in turn point to a list of trees and blobs, tree is directory).
And thus when we ask Git to move to previous commit with git reset --soft HEAD~
we have asked git that our current branch which is pointed by the HEAD should point to one previous commit that's all.
Let's do the reset to one previous commit.
$ git reset --soft HEAD~ # Git please move HEAD to point to one previous commit.
$ cat .git/HEAD
ref: refs/heads/master # => Didn't move! it points to the same place to the master.
$ cat refs/heads/master
ddf47ff (HEAD -> master, develop) added file to folder # => Aha so master branch pointer did move and HEAD simply points to our branch as the diagram shows.
9d0e101 hi
But what would be the parent of that commit?
As we have moved our HEAD
one commit to the past this past commit would be the parent of our current commit thus we are "rewriting
" our local git history.
So after we do Git reset and go back one commit in our local repo, we are making local fixes in our working directory and then we commit. This commit is a new commit but it has the same parent as the commit that we are fixing.
So if other users already have this commit old commit of ours which we have just "detached" out of the repo and we force it into remote repo, it would cause them to get "Recovering from upstream rebase" and other nasty stuff.
We can push that to the main repository but be careful with that because this would mess up things for others. What would happen when they try to pull it and find that their parent is different than yours?
And the answer finishes with "You could have just done git commit --amend
" which is true, but that is yet another answer that we wo;; investigate in another post.
Not All Is Lost
One last note: if you want to revert the git reset you can use the reflog
which stores like a log of everything you do and just like git reset you can use the reflog
in order to go back your going back in time. This can be achieved with:
git reset 'HEAD@{1}'
Which tells Git, "Hey Git, remember I wanted a reset, well now I want you to reset to one previous point in time just before I did the reset."
Opinions expressed by DZone contributors are their own.
Comments