I checked out an older commit and suddenly my latest commit vanished
One of my biggest surprises when I started using Git was that after checking out an older commit, it looked like my latest commit had disappeared.
You can think of a commit as a snapshot of your repository at particular point in time. Each commit receives a unique commit id, for example 3f56bcaa. We'll understand it better through a series of practical exercises.
Exercise: Create 3 commits
Suppose the following scenario. I create a git repository with git init, with 3 commits: commit A, commit B and commit C. The commit history looks like this:
A <- B <- C <- main, HEAD
What happens when I check out commit B using git checkout B? Let's find out. Let's also have a look at hashes of each commit, as they allow to get to any place in git tree.
After checkout, the graph looks like this:
A <- B <- (C <- main = invisible)
^
HEAD
OK, I can see commit B, but what happened to commit C? I should be able to go back, right?
Exercise: Go back to branch main or C.
You can go back by checking out a branch. git checkout main solves the problem. You can also go back using git checkout
A <- B <- C ← main
Commit C never disappeared, but we could not see it when we went to B. We were able to go back either using git checkout main or git checkout commit-hash.
Question: Why does commit C or branch main disappear, when we go back to commit B?
Commit C never disappears, because the history of your commits forms a direct acyclic graph. It is a graph in which each node points in one direction. The graph does not contain links back. However, Git lets you jump to any commit if you know its hash or have a branch pointing at it.
Exercise: Create a branch, and make commit to it
We will now create a branch and make a commit. In terminal, we write
git branch feature
A new branch feature was created. As we said previously, branch is a pointer to commit hash. It moves forward with each commit: A - B - C <- main, feature, HEAD
We created branch feature, but we are still on branch main. We need to check out the branch if we want to advance it with each new commit:
git checkout feature
We will now make a commit: git commit -m "D". We now see the following graph using git lg. Notice: feature moved, main did not move.
A - B - C - D <- feature, HEAD
^
main
This is the part that surprises many people. They think of a branch as a collection of commits. Branch is a movable label attached to a commit. It is a common misconception that git users associate a branch with specific commit. A branch is a label attached to a commit. It moves forward as you commit.
Exercise: Go back to branch main. Notice that git lg does not show feature.
What happens if we switch to branch main by git checkout main? Answer:
A - B - C <- main
Where is D? Did Git remove our branch completely? From C there is no path that reveals commit D. But Git still knows where D is.
We can go back to branch feature with git checkout feature. The git tree looks familiar again:
A - B - C - D <- feature
^
main
Exercise: Remove feature
What happens if we remove branch feature? Will D get deleted? Not immediately. It becomes an unreachable commit, which you can still checkout if you remember its commit id.
Be careful: Git will eventually garbage-collect unreachable commits. Until Git garbage collects it, you can still recover it using commit hash.
git branch -D feature
git checkout <commit-hash>
Remember:
each commit is identified by a commit hash. You can use this hash to go to any checkout. It is hard to remember hexadecimal commit hash, so you can use branches instead. A branch is a movable pointer to a commit.
a commit hash uniquely identifies a git commit.
main is a branch. You can add, remove and delete branches as you wish. They are here to help you navigate the Git commit graph, so you don’t have to remember commit hashes.
We can use multiple branches to work on different features, versions etc. It is just a pointer to a commit id.