Prereq:
You have to have the previous section finished and have the same state
B --- C foo
/
A trunk
Problem
Lets start off by creating some more commits and adding them to trunk
. This
way our branches diverge. Meaning they have commits that are unique in both
and have a common ancestor, A
.
Please add D
and E
in the same way we added B
and C
to trunk
Desired State:
B --- C foo
/
A --- D --- E trunk
Solution
➜ hello-git git:(foo) git checkout trunk
Switched to branch 'trunk'
➜ hello-git git:(trunk) echo "D" >> README.md
➜ hello-git git:(trunk) git add .
➜ hello-git git:(trunk) git commit -m 'D'
[trunk 79c5076] D
1 file changed, 1 insertion(+)
➜ hello-git git:(trunk) echo "E" >> README.md
➜ hello-git git:(trunk) git add .
➜ hello-git git:(trunk) git commit -m 'E'
[trunk a665b08] E
1 file changed, 1 insertion(+)
Now we have the following setup
B --- C foo
/
A --- D --- E trunk
and our logs have commit messages that should make it easy to track. So now we can talk about git merge and rebase
Git Merge and Rebase
Remember
A commit is a point in time with a specific state of the entire code base.
We have work done, but its on another branch. We need to get it back into our
main branch, trunk
... but how?
There really is only one strategy to merge
code from one branch to another, but
depending on the state different outcomes can happen. rebase
can help put a
branch into a pristine state
What is a merge?
A merge is attempting to combine two histories together that have diverged at some point in the past. There is a common commit point between the two, this is referred to as the best common ancestor ("merge base" is often the term used in the docs).
To put it simply, the first in common parent is the best common ancestor.
git then merges the sets of commits onto the merge base and creates a new commit at the tip of the branch that is being merged on with all the changes combined into one commit.
there is also extra information in the logs about these types of commits
How to merge
Merging is quite simple.
The branch your on is the target
branch and the branch you name in
<branchname>
will be the source
branch.
git merge <branchname>
Problem
Given the following state, merge foo
onto trunk
. Since we want to keep
foo
and trunk
in its current state for other problems later in this lesson.
Please start by branching off of trunk
. I named mine trunk-merge-foo
Finally, when you are done use git log
to see the resulting state of
trunk-merge-foo
State of your git
B --- C foo
/
A --- D --- E trunk-merge-foo
Solution
➜ hello-git git:(trunk) git checkout -b trunk-merge-foo
Switched to a new branch 'trunk-merge-foo'
git checkout -b trunk-merge-foo
is the same thing as the following:
git branch trunk-merge-foo
git checkout trunk-merge-foo
Fun fact
From man git-switch
you can see the following:
git switch [<options>] (-c|-C) <new-branch> [<start-point>]
Meaning we could of created a new branch by using git switch -c trunk-merge-foo
and switched to it.
lets merge in foo
into trunk-merge-foo
➜ hello-git git:(trunk-merge-foo) git merge foo
At this point you are presented with the EDITOR
. Git will use your system's
default editor for you to accept/make any changes to commit messages. You are
presented with the message that will be in the logs after merging these two
branches together.
I typically do not edit this merge message
Merge branch 'foo' into trunk-merge-foo
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
If vim is your default git editor :wq
to save the message and confirm the
commit
➜ hello-git git:(trunk-merge-foo) git merge foo
Merge made by the 'ort' strategy.
second.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 second.md
Lets use log
to check out what has happened here
--parents
adds 1 to two extra shas signifying the parent chain. This is duplicated by--graph
but instead of a graphical representation, its with shas.
➜ hello-git git:(trunk-merge-foo) git log --oneline --decorate --graph --parents
* ccf9a73 a665b08 16984cb (HEAD -> trunk-merge-foo) Merge branch 'foo' into trunk-merge-foo
|\
| * 16984cb 4ad6ccf (foo) C
| * 4ad6ccf cb75afe B
* | a665b08 79c5076 (trunk) E
* | 79c5076 cb75afe D
|/
* cb75afe A
How to read the lines
| * 16984cb 4ad6ccf (foo) C
commit 16984cb
has a parent of 4ad6ccf
with a named branch of foo
with a
commit message of C
The only commit that has a different look is the first line of the logs, which is the latest commit.
* ccf9a73 a665b08 16984cb (HEAD -> trunk-merge-foo) Merge branch 'foo' into trunk-merge-foo
ccf9a73
has two parents, a665b08
and 16984cb
. If you look at those
commits, they are the commits of trunk
and foo
. git merge
merged those
two commits together by finding the best common ancestor (merge base cb75afe
)
and playing the commits one at a time, start at cb75afe
, creating a new
commit, a merge commit, ccf9a73
.
Problem
Create the following git setup:
X - Y bar
/
A --- D --- E trunk
That means:
- branch
bar
offtrunk
- add two commits with message
X
thenY
- create your changes,
X
andY
, inbar.md
Solution
First checkout trunk
then use trunk
to create branch bar
➜ hello-git git:(trunk-merge-foo) git checkout trunk
➜ hello-git git:(trunk) git checkout -b bar
Create the two commits, X
and Y
, on bar
➜ hello-git git:(bar) echo "X" > bar.md
➜ hello-git git:(bar) git add bar.md
➜ hello-git git:(bar) git commit -m "X"
[bar 2f43452] X
1 file changed, 1 insertion(+)
create mode 100644 bar.md
➜ hello-git git:(bar) echo "Y" >> bar.md
➜ hello-git git:(bar) git add bar.md
➜ hello-git git:(bar) git commit -m "Y"
[bar b23e632] Y
1 file changed, 1 insertion(+)
we can verify that we have similar histories by using git log
again.
➜ hello-git git:(bar) git log --oneline --decorate --graph
# You should see the same ordering
* b23e632 (HEAD -> bar) Y
* 2f43452 X
* a665b08 (trunk) E
* 79c5076 D
* cb75afe A
Problem
merge bar
onto trunk
. Do not create a separate branch this time. Once you
merge see if you can spot the difference between a "3 way merge" vs a "fast
forward" merge.
Solution
First we simply checkout trunk
then execute git merge bar
➜ hello-git git:(bar) git checkout trunk
M README.md
Switched to branch 'trunk'
➜ hello-git git:(trunk) git merge bar
Updating a665b08..b23e632
Fast-forward
bar.md | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 bar.md
First difference
In the merge notes we see the following line:
Fast-forward
That means we have a fast forward merge
Second, the logs
You can view this FF merge with git log
➜ hello-git git:(trunk) git log --oneline --decorate --graph
* b23e632 (HEAD -> trunk, bar) Y
* 2f43452 X
* a665b08 E
* 79c5076 D
* cb75afe A
Excalidraw what happens
Rebase
Rebasing often gets a bad wrap. Part of this is because people don't really know why or when to use rebase and will end up using it incorrectly and thus yelling on the twitters that it "ruins their entire life." Gotta love twitter.
With the following repo setup
B --- C foo
/
A --- D --- E --- X --- Y trunk
We can demonstrate the power of rebase. What rebase will do is update foo
to
point to Y
instead of A
.
B --- C foo
/
A --- D --- E --- X --- Y trunk
THAT IS IT
That is all rebase does here. It updates the commit where the branch originally points to
This also means that in we decide to merge foo
into trunk
we can do a
ff-merge!
What Rebase Does
The basic steps of rebase is the following:
- execute
git rebase <targetbranch>
. I will refer to the current branch as<currentbranch>
later on - checkout the latest commit on
<targetbranch>
- play one commit at a time from
<currentbranch>
- once finished will update
<currentbranch>
to the current commit sha
How rebase works is important, it will lead to complication down the road if you don't understand this key fact
We will address that later
Problem
rebase foo
with trunk
. Please create a separate branch foo-rebase-trunk
.
This branch should be created off of foo
. Once you have rebased
foo-rebase-trunk
with trunk
check out the git logs with --graph
and
--decorate
to see what has happened (--oneline
can make it easier to read)
Solution
First we will create a new branch off of foo
and call it foo-rebase-trunk
➜ hello-git git:(trunk) git checkout foo
Switched to branch 'foo'
➜ hello-git git:(foo) git checkout -b foo-rebase-trunk
Switched to a new branch 'foo-rebase-trunk'
Now we need to perform the rebase
➜ hello-git git:(foo-rebase-trunk) git rebase trunk
Successfully rebased and updated refs/heads/foo-rebase-trunk.
git log
time to view what has happenend
➜ hello-git git:(foo-rebase-trunk) git log --oneline --decorate --graph
# you should have same history
* 3ade655 (HEAD -> foo-rebase-trunk) C
* d810248 B
* b23e632 (trunk, bar) Y
* 2f43452 X
* a665b08 E
* 79c5076 D
* cb75afe A
You will see that trunk
(which contains bar
after our fast forward merge)
is now the new base for foo-rebase-trunk
. In other words, foo
is no longer
diverging from trunk
. If we choose to merge foo
into trunk
we can do so
via ff-merge (no merge commit).
Some pros
When using rebase you can have a clean history with no merge commits. If you
are someone who uses git log
a lot this can really help with searching.
Some cons
It alters history of a branch. That means that if you already had foo
on a
remote git, you would have to force
push it to the remote again. We will go
over this more it shortly
Some cautionary words
NEVER CHANGE HISTORY OF A PUBLIC BRANCH. In other words, don't ever change
history of trunk
. But your own personal branch? I don't think it matters
and i think having a nice clean history can be very beneficial if you use git
to search for changes through time.
Wait.. isn't there more?
Yes, there is a lot more, but we are not going to go over it without more foundational knowledge