A good[ish] website
Web development blog, loads of UI and JavaScript topics
Here's how to make, delete, list, and use branches. Plus solutions to some hairy branching scenarios you might find yourself in.
There's two prevailing branching schools:
The diagram for Git flow is so complex that I'm not even going to try to draw any pseudo graphics for it. See the flowchart here.
GitHub flow goes like this:
Master ●────●────●────●────●────●────●
\ /
Feature ┈┈┈┈┈┈┈●───●┈┈┈┈┈┈┈`</pre>
Github Flow, is so simple and intuitive that it can be explained in few bullet points. Actually in six:
- Anything in the
master
branch is deployable- To work on something new, create a descriptively named branch off of
master
(ie:new-oauth2-scopes
)- Commit to that branch locally and regularly push your work to the same named branch on the server
- When you need feedback or help, or you think the branch is ready for merging, open a pull request
- After someone else has reviewed and signed off on the feature, you can merge it into master
- Once it is merged and pushed to
master
, you can and should deploy immediately
I'll quote Scott Chacon's article once more:
[In git flow] one of the bigger issues for me is that it’s more complicated than I think most developers and development teams actually require. It’s complicated enough that a big helper script was developed to help enforce the flow.
I prefer the GitHub Flow exactly because of it's simplicity. This kind of simplicity might not necessarily be a requirement for you, both models serve a different purpose.
GitFlow is good for projects that do formal releases rarely (rarely being anything from weeks to months). Where as, it's overly complex when there are no release cycles, like my little blog here, or the GitHub, there are new features and bug fixes, but no version numbers.
# List branches
$ git branch
# Make a branch
$ git branch my-new-branch
# Change into a branch
$ git checkout my-new-branch
# To create and checkout a branch in one command, use the `-b` flag
$ git checkout -b my-new-branch
# Delete local branch
$ git branch -d my-new-branch
Here's a little bash script to generate a test repo to play with:
mkdir merge-test \
&& cd merge-test \
&& echo Banana > banana.txt \
&& echo Apple > apple.txt \
&& echo Kiwi > kiwi.txt \
&& git init \
&& git add -A \
&& git commit -m "Initial commit"
Put your beer down:
# See what branches there are, only master for now
$ git branch
* master
# Make a new branch
$ git branch new-fruit
# Then check again, the one with the asterisk
# is the active branch
$ git branch
* master
new-fruit
# Make changes to the new-fruit branch,
# start by switching to it by checking it out
$ git checkout new-fruit
# Make some new fruits, add, and commit them.
# Just paste it to your terminal.
$ echo Orange > orange.txt && git add -A && git commit -m "Added Orange"
$ echo Plum > plum.txt && git add -A && git commit -m "Added Plum"
$ echo Pear > pear.txt && git add -A && git commit -m "Added Pear"
Now there's more "meat" in the tree: two branches, and the other one has three commit more.
The tree can be printed out with the following command:
$ git log --graph --full-history --all --pretty=format:"%h%x09%d%x20%s"
* 622f05d (new-fruit) Add pear
* cd74786 Add Plum
* c6a23e2 Add Orange
* 4ff629a (HEAD, master) Initial commit
It's very straight, the tree is linear. More on that in the merging section of the article.
Presented with pseudo graphics it looks like this:
┌───┌───┌─ The new fruits
▼ ▼ ▼
new-fruit ●───●───●
/
master ●
▲
└── Initial commit
In Git, there are two main ways to integrate changes from one branch into another: the merge and the rebase.
When talking about branching, one must also talk about merging. It is the opposite of branching, you pull the branches back into the trunk. We'll talk about rebase later on in the article.
$ git merge new-branch
That will merge the new-branch
to the branch you are at. Git will decide the merging algorithm for you, which come in two types:
Fast forward merge can be used when there is linear path to the new branch. I.e. there has been no commits in the master
after the new-branch
diverged from it.
If your git tree looks something like this:
┌─ New feature
▼
newbranch┈┈┈┈●───●───●
/
master ━━●━━●
▲
└── Master
What the fast forward merge does, is quite simple, it just copies the new feature to the master:
┌─ New feature and Master
▼
●───●───●
/
master ●───●───●
Nothing really changes, the linear structure remains, all commits are the same.
Take the below, very common, git tree:
┌─ Your New feature branch
▼
newbranch ┈┈┈●───●───●
/
master━━●━━●━━●
▲
└── MASTER. Someone else merged
in their changes after you
branched out with your feature.
There's now two different histories that Git needs to smash together somehow. It does this by using data from 3 different commits (hence the name). The commits are: 1) master tip, 2) feature tip, 3) their ancestor.
┌── 2
▼
newbranch┈┈┈┈●───●───●
/
master━━●━━●━━●
▲ ▲
3 ──┘ └── 1
The three way merge also creates a merge commit.
Git just does this for you, it uses the fast forward merge if it can. Then there's also rebasing, it's related strongly to merging, that's the next chapter in the article.
Lets pick up from the test tree where we left of in the first example. It looked like this:
* 622f05d (new-fruit) Add pear
* cd74786 Add Plum
* c6a23e2 Add Orange
* 4ff629a (HEAD, master) Initial commit
Let's break the linearity by making a commit to the Master:
$ git checkout master
$ echo Pineapple > pineaple.txt
$ git add -A
$ git commit -m "Add Pineapple"
Now check it with the extended log command:
git log --graph --full-history --all --pretty=format:"%h%x09%d%x20%s"
* d41e17c (HEAD, master) Add Pineapple
| * 622f05d (new-fruit) Add pear
| * cd74786 Add Plum
| * c6a23e2 Add Orange
|/
* 4ff629a Initial commit
The new-fruit
branch is clearly visible now. Merge it into master
:
# Switch to master if not active
$ git checkout master
$ git merge new-fruit
Merge made by the 'recursive' strategy.
orange.txt | 1 +
pear.txt | 1 +
plum.txt | 1 +
3 files changed, 3 insertions(+)
create mode 100644 orange.txt
create mode 100644 pear.txt
create mode 100644 plum.txt
Git asks you to enter a commit message, since this is a three-way merge.
Now the log shows the following:
$ git log --graph --full-history --all --pretty=format:"%h%x09%d%x20%s"
* ef27c6c (HEAD, master) Merge branch 'new-fruit'
|\
| * 622f05d (new-fruit) Add pear
| * cd74786 Add Plum
| * c6a23e2 Add Orange
* | d41e17c Add Pineapple
|/
* 4ff629a Initial commit
The new-fruit
has merged to the trunk. Now, there's the "Merge branch 'new-fruit'" commit and the history is not linear. Rebasing can help in that.
Rebasing is similar to merging, it also combines branches, but completely different command from merging. Rebase rewrites history and reduces merge conflicts in doing that. What all this really means? Let's look how rebasing works to understand when to use it.
An example tree:
┌─ FEATURE
▼
newbranch ●───●
/
master───●───●───●─────●───●
▲
└── MASTER
After a rebase, the feature
branch kind of 'migrates' to the tip of the tree:
the "old" FEATURE ─┐ ┌─ FEATURE
▼ ▼
newbranch ●───● ●───●
/ /
master───●───●───●─────●───●
▲
└── MASTER
Essentially, rebase moves a commit from branch to another. But, those commits are not the same commits, they contain the same stuff, but they're new commits, they have different commit ID etc. So, rebase rewrites history.
Without rebase you wouldn't be able to retain linear history, rebasing tidies up the repo.
Rebasing wont replace merging, they're used side by side usually, first rebase then merge.
This is where we left off in our example tree:
$ git log --graph --full-history --all --pretty=format:"%h%x09%d%x20%s"
* ef27c6c (HEAD, master) Merge branch 'new-fruit'
|\
| * 622f05d (new-fruit) Add pear
| * cd74786 Add Plum
| * c6a23e2 Add Orange
* | d41e17c Add Pineapple
|/
* 4ff629a Initial commit
First let's rollback that merge commit, so we can test the rebasing better:
# Roll back 1 commit
$ git reset --hard HEAD~1
HEAD is now at d41e17c Add Pineapple
Now we want to:
master
to new-fruit
and thennew-fruit
to master
.$ git checkout new-fruit
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Add Orange
Applying: Add Plum
Applying: Add pear
# Then merge
$ git checkout master
$ git merge new-fruit
Note that it didn't ask us to write a commit message, because there's no need to do that, because Git only did a fast forward merge. Now the log shows a linear history:
$ git log --graph --full-history --all --pretty=format:"%h%x09%d%x20%s"
* f5777c2 (HEAD, new-fruit, master) Add pear
* 31a525b Add Plum
* 72c2e92 Add Orange
* d41e17c Add Pineapple
* 4ff629a Initial commit
There's one commit less and it's much tidier.
For instance, the Plum commit from before the rebase: * cd74786 Add Plum
, and after: * 31a525b Add Plum
. Different commit IDs, cause they're completely different commits now and the old commits are destroyed. History rewriting in action.
Atlassian has great article about the little nuance differences of merging and rebasing. Git's docs are amazing on this subject also.
You've started to work on a new feature, and forgot to make a new branch for it. This is what you have:
master ●───●───●───●───●───●
^
└── start a new feature
And this is what you want:
newbranch ┈┈┈┈┈●───●───●
/
master ●───●───●
This is very easy to solve with some branch juggling. Start by making a new branch:
$ git branch newbranch
Now, all your new stuff is safe in the new branch, and you can freely roll back on the master:
$ git reset --hard HEAD~3 # This goes back three commits
Or rollback to a commit id
$ git reset --hard <commit-id>
# Commit IDs look like this 5c00039c86ca321e9ac41557e6515da20cfeee44
# You can see your commit ID by issuing: git log
There you have it, newbranch
has the new stuff and master
has the old. Watch for not going back too far.
Then check out the newbranch
to continue work on it:
$ git checkout newbranch
You branch off from the trunk and hack on your feature, at this time someone else finishes their feature and pushes it to the master. When you try to push you get an error:
$ git push origin master
error: failed to push some refs to '/path/to/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Now, you need to pull the changes from the remote master to your branch, using the --rebase
flag:
$ git pull --rebase origin master
It grabs the new commits from the Master and puts them at the tip of your feature branch. Now you can push to it.
Branching and rebasing are totally confusing at first, but they're an essential part of Git. Learn to love them :) Please drop a comment if you got something to add.
Comments would go here, but the commenting system isn’t ready yet, sorry.