Branches
Branching in Git allows us to "diverge" development lines, so that commits need not be based on the latest version.
Scenario
Imagine you are adding a feature that will takes weeks to complete, but you suddenly need to add an urgent bug fix. How to ship the bugfix without mixing the incomplete feature?
Answer: When we work on a feature (especially a large one), start it on a new branch. Only merge it to the main branch when it is ready.
When we initalize a repo, we are on a branch called "master". When we create a commit, the commit is added to the master branch.
Info
Technically speaking, a branch is just a reference to a commit. This reference is updated when a commit is created while this branch is active.
Creating branches
- To split the current branch into another branch,
use the command
git checkout -b $NEW_BRANCH
. This will create the branch$NEW_BRANCH
that ends at the current commit checked out. - Like checking out commits, we can checkout an existing branch
using the
git checkout $BRANCH
command. -
These commands will only create the branches locally. To make the branch appear on GitHub, you need to set the upstream of the branch to a branch on GitHub with the same name:
$ git push -u origin $BRANCH_NAME
Then the remote
origin/$BRANCH_NAME
is called the "upstream branch" for the local$BRANCH_NAME
, and the local$BRANCH_NAME
is called the "tracking branch" for the remoteorigin/$BRANCH_NAME
.
What does an upstream branch really mean?
When we type git push
or git pull
without specifying a remote and branch,
Git uses the upstream branch as the dstination/source to push to/pull from.
A branch is automatically created on GitHub when you try to push to it.
Try it yourself
Let's create an example repository with a few dummy commits (some output omitted):
$ git init
$ echo a > file.txt
$ git add file.txt
$ git commit -m "Initial commit"
[master (root-commit) 5eac39c] Initial commit
$ echo b > file.txt
## -a stages all tracked files.
## It is a convenient shorthand for add-and-commit
## if there are no untracked files.
$ git commit -am "Second commit"
[master 47743d3] Second commit
You can see that master
is now pointing to 47743d3
.
Let's diverge a new branch called two
at the current checkout (47743d3
):
$ git checkout -b two
Switched to a new branch 'two'
Then we create a new commit:
$ echo c > file.txt
$ git commit -am "Third commit"
[two 99c64ea] Third commit
1 file changed, 1 insertion(+), 1 deletion(-)
Let's use git log
to see the commits in each branch:
$ git log --oneline two
99c64ea (HEAD -> two) Third commit
47743d3 (master) Second commit
5eac39c Initial commit
You can see that master
is still at 47743d3
,
while two
is at 99c64ea
.
Let's change back to master
and see if the contents of file.txt
are reverted:
$ cat file.txt
c
$ git checkout master
Switched to branch 'master'
$ cat file.txt
b
In fact, remote branches are also modelled as branches
(which are separate branchs from the tracking branches).
You can checkout the latest commit of the remote branch
using git checkout origin/$BRANCH_NAME
(or git checkout remotes/origin/$BRANCH_NAME
to avoid confusion).
However, you cannot create commits on a remote branch;
you must do it on the remote-tracking branch and push it.
Hint
It is possible to create branch names with a /
.
For example, it is a convention for some people to use the branch name
issues/1234
for a bugfix for issue number 1234.
This is useful for grouping branches.
Merging branches
The most powerful feature of Git is that you can merge different branches.
- A branch can be merged into another branch that shares a common ancestor commit,
- In a merge, there are two branches called the "base" and the "head".
- The new changes (since the last common ancestor) in the head branch are merged into the base branch.
- After the merge, the base branch will contain the changes from both branches,
- The head branch remains unchanged after the merge.
- A "merge commit" is created on the base branch, which has two parent commits
- To perform a merge, first checkout the base commit, then run
git merge $HEAD_BRANCH
Try it yourself
Let's setup the scenario (some output omitted):
$ git init
$ echo a > file.txt
$ git add file.txt
$ git commit -am "Initial commit"
[master (root-commit) a8cea08] Initial commit
$ git checkout -b two
$ echo b > two.txt
$ git add two.txt
$ git commit -am "Second commit"
[two 952151e] Second commit
$ ls
file.txt two.txt
$ git checkout master
$ ls
file.txt
$ echo c > three.txt
$ git add three.txt
$ git commit -am "Added three.txt"
[master 5986012] Added three.txt
$ ls
file.txt three.txt
We can use the git log --graph --all --oneline
command
to get a pretty layout of the repo history on all branches:
$ git log --graph --all --oneline
* 5986012 (HEAD -> master) Added three.txt
| * 952151e (two) Second commit
|/
* a8cea08 Initial commit
Now let's merge two
into master
,
i.e. the base branch is master
, and the head branch is two
.
$ git merge two
Merge made by the 'recursive' strategy.
two.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 two.txt
This will open an editor to edit your commit message. In case you forgot, here are a few tips:
- If your default editor is
nano
, use^X-Y-<Enter>
to save and exit - If your default editor is
vi
/vim
, use:x<Enter>
to save and exit- You are strongly advised to change to another editor if you didn't already know vim
- For graphical editors, Git should continue merging after you have closed the editor.
Now let's see the state of the repository:
$ ls
file.txt three.txt two.txt
$ git log --graph --all --oneline
* d2d4403 (HEAD -> master) Merge branch 'two'
|\
| * 952151e (two) Second commit
* | 5986012 Added three.txt
|/
* a8cea08 Initial commit
Wow, both three.txt and two.txt are in the same branch!
Do not delete this test repository yet. We will use it again in the "Resolving merge conflicts" section.
Resolving merge conflicts
Sometimes we have changes to the same line on multiple branches. Then reasonably, Git cannot merge them together automatically. Then we have to resolve merge conflicts.
Try it yourself
Continuing the last example.
Let's create a branch four
from a8cea08
:
$ git checkout a8cea08
$ git checkout -b four
And create another commit that also creates two.txt
,
but with different contents:
$ echo four > two.txt
$ git commit -m "Commit four"
[four ef5d355] Commit four
1 file changed, 1 insertion(+)
create mode 100644 two.txt
Now let's try to merge two
into four
:
$ git merge two
CONFLICT (add/add): Merge conflict in two.txt
Auto-merging two.txt
Automatic merge failed; fix conflicts and then commit the result.
A conflict occurred in two.txt
!
We can check this with git status
:
$ git status
On branch four
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both added: two.txt
no changes added to commit (use "git add" and/or "git commit -a")
Let's look at what happens in two.txt
:
$ cat two.txt
<<<<<<< HEAD
four
=======
b
>>>>>>> two
This means that the HEAD
(the base branch) has the content four
,
while the branch two
(the head branch) has the content b
.
Edit the file until it becomes normal, then type git commit
to continue merging.
Rebasing (optional)
Rebase changes the "base" (parent commit) of a commit.
Suppose we have commits 1111111 -> 2222222 -> 3333333
.
Now we want to insert another commit aaaaaaa
(1111111 -> aaaaaaa
)
between 1111111
and 2222222
.
We can do this by rebasing 3333333
upon aaaaaaa
.
After the rebase, we will end up with 1111111 -> aaaaaaa -> 2222222 -> 3333333
,
where bbbbbbb
is almost the same as 2222222
(except it's based on aaaaaaa
),
and ccccccc
is almost the same as 3333333
(except it's based on bbbbbbb
).
Note that 2222222
and 3333333
are no longer used.
Caution
Do not rebase commits that you have already pushed! Otherwise your local history and remote history will diverge, and you will end up with very troublesome history.
These useful actions are possible with rebasing:
- Squashing commits together
- Changing commit messages
- You can change the last commit message with just
git commit --amend -m "New message"
, but you will needrebase
if there are newer commits since then.
- You can change the last commit message with just
- Changing commit content
- Deleting a commit from the history
- It may be possible that hackers recover your deleted commit from the git logs. If you accidentally committed a password, change your password and never use it agan.
To do these, use git rebase -i $LCA
,
where $LCA
is the sha of the last unmodified commit.
It allows you to edit a file that determines what actions to do during rebasing.
Furthermore, git pull --rebase
is a handy shorthand
to rebase unpushed commits upon the remote head.
It uses rebase
instead of merge
when remote and local diverge,
so there are fewer useless commits.