Suppose we have two branches, A and B, and their commits both have the same parent commit (the one the master points to). As shown in the figure:

Now we’re on branch B, and then we rabase on branch A. As shown in the figure:

We often encounter this situation in our daily development. Suppose that branch A and branch B are two independent feature branches, but we accidentally rebase them incorrectly. Now it is equivalent to that the originally independent businesses in the two feature branches are combined, which of course we do not want to see, so how to undo it?

One solution is to use the reflog command.

Use reflog to undo the base change

If you enter git reflog, you will see a log like this:

The final output is actually the earliest operation, let’s analyze it one by one:

  1. HEAD@{8}: Here we create the initial commit
  2. HEAD@{7} : branch a is checked out
  3. HEAD@{6} : commit on branch a, note that master branch has not changed
  4. HEAD@{5} : go back from branch A to branch master
  5. HEAD@{4} : branch B checked out
  6. HEAD@{3} : commit on branch B. Note that the master branch has not changed
  7. HEAD@{2} : This step starts to change the base to branch A. First switch to branch A
  8. HEAD@{1} : base branch A on the commit corresponding to branch B
  9. HEAD@{0} : end of base change, because the base change is initiated on B, so finally cut back to branch B

If we want to undo the rebase, simply type the following command:

git reset --hard HEAD@{3}
Copy the code

At this point, you have “restored” to the state before rebase. Don’t worry, I’ll explain how to do it later.

An introduction to how Git works

In order to understand how Git works and the principles behind the commands, I think it’s important to have a basic understanding of git’s model.

First, every Git directory has a hidden directory named.git where everything about git is stored (except global configuration). There are several subdirectories and files in this directory. The files are not really important, they are all configuration information. I will describe the HEAD file in this directory. There are several subdirectories:

  1. Info: This directory is not important and contains an exclude file and.gitignoreThe file is similar except that this file is not under version control, so you can do some personal configuration.
  2. Hooks: This directory is easy to understand. It is used to put hooks into Git and to do custom configuration before and after a given task is triggered. This is a separate topic that will not be covered in this article.
  3. Objects: used to hold all objects in Git.
  4. Logs: Records the movement of each branch, described separately below.
  5. Refs: Used to record all references, described separately below.

This article focuses on the functions of the last three folders.

Git object

Git is object oriented! Git is object oriented! Git is object oriented!

Yes, Git is object-oriented, and a lot of things are objects. Let me give you a simple example to help you understand the concept. Suppose we are in an empty repository, edit two files, and then commit. What objects are there at this point?

First, there are two data objects, one for each file. When a file is modified, even if a new letter is added, a new data object is generated.

Second, there will be a tree object that maintains a set of data objects, called a tree object because it can hold not only data objects, but also another tree object. For example, when two files and a folder are committed, there are three objects in the tree object, two of which are data objects, and the folder is represented by another tree object. So the recursion can represent any level of files.

Finally, there are commit objects, each of which has a tree object that represents the files involved in a particular commit. In addition, each commit has its own parent commit, which points to the object of the last commit. Of course, the submission object will also contain the submission time, the name of the submitter, email and other auxiliary information, not to say.

Given that we only have one branch, this is enough to explain how git commit history is calculated. It does not store a complete commit history, but rather a complete history that can be obtained by looking forward through the parent commit object.

Note the picture at the beginning. Branch B points to submission 9cbb015.

git cat-file -t 9cbb015
git cat-file -p 9cbb015
Copy the code

Here we use the cat-file command, where the -t argument prints the type of the object, and the -p argument intelligently recognizes the type and prints the contents. The output is as shown in the figure:

You can see that 9cBB015 is a commit object, which contains the tree object, the parent commit object, and various configuration information. We can print the tree object again to see:

This means that only the begin file is modified in this commit, and the data object for which the begin file is printed is output.

Git reference

Git is object oriented. Yes, branches and tags are Pointers to commit objects. This can be verified:

cat .git/refs/heads/a
Copy the code

All local branches are stored in git/refs/heads. Each branch has a file. The contents of the file are shown in the following figure:

As you can see, 4a3a88d is exactly the commit that branch A points to in the first figure of this article.

Now that we’ve figured out the secret of git branches, now that we have a record of all branches and a parent commit object for each commit, we can get a commit state like SourceTree or the first figure at the beginning of this article.

As for the tag, it is also a reference and can be understood as a branch that cannot be moved. You can only always point to a fixed commit.

The last special reference is HEAD, which can be interpreted as a pointer to a pointer. To prove this, look at the.git/HEAD file:

Refs /heads/b is a file that contains the commit object to which branch B is pointing. It’s important to understand this, otherwise you won’t understand the difference between checkout and Reset.

Both commands change the point of HEAD. The difference is that checkout does not change the branch HEAD points to, while reset does. For example, executing the following two commands on branch B will cause the HEAD to point to the commit 4a3a88d (which branch A points to) :

git checkout a
git reset --hard a
Copy the code

But checkout only changes the HEAD orientation, not branch B orientation. Reset not only changes the point of HEAD, but because HEAD points to branch B, it also points to the commit 4a3a88d.

The git log

In the.git/logs directory, there is a folder and a HEAD file, and every time the HEAD reference changes location, a record is added to.git/logs/HEAD. The.git/logs/refs/heads directory has multiple files, each with a branch that records when the branch’s location changes.

Git /logs/HEAD

Undo the rebase principle

Git maintains commit objects, tree objects, and data objects for each commit, but it does not maintain branch points for each commit. As we saw in the branch section, a branch is simply a file that holds committed objects and does not record historical information. Even in the previous section, we learned that branch changes are recorded, but they are not bound to a commit object.

That is, there is no branch snapshot in Git at the time of a commit

So how do we undo rebase with reset? Here’s another fact to clarify. As mentioned earlier, the branch state you see at any given moment through the SourceTree or Git log is plotted by a list of all branches, the commits to which each branch points, and the parent commit of each commit.

First, the files under git/refs/heads tell us how many branches there are. The contents of each file tell us which commit the branch is pointing to. With this commit, you can plot the commit history of the branch. The submitted history of all the molecules is what we see.

But let’s be clear: not all commit objects will be visible. For example, if we move a branch forward once, the commit line of that branch will lose one node. If no other commit line contains the node, the node will not be visible.

So after rebase was completed, we thought we saw the following commit line:

df0f2c5(master) --- 4a3a88d(a) --- 9cbb015(b)
Copy the code

It actually goes like this:

df0f2c5(master) --- 4a3a88d(a) --- 9d0618e(b)
   |
9cbb015
Copy the code

There is still a fork on the master branch, and the original commit 9cBB015 still exists, but it is not visible because the commit line without the branch contains it. But with reflog, we can retrieve every move of the HEAD, so we can see the commit.

When we execute this command:

git reset --hard HEAD@{3}
Copy the code

Look again at the output of reflog:

HEAD@{3} is actually an abbreviation of 9cbb015 to the left of it, so the above command is equivalent to:

git reset --hard 9cbb015
Copy the code

As mentioned earlier, reset moves not only HEAD but also the branch HEAD points to, so the result of this command is that HEAD and branch B both point to the commit 9cbb015, which seems to undo rebase.

But remember, there is still a commit on branch A, and 9d0618e just doesn’t have a branch pointing to it, so it doesn’t show. But it does exist. Strictly speaking, we haven’t really cancelled the rebase.