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
--parentsadds 1 to two extra shas signifying the parent chain. This is duplicated by--graphbut 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
barofftrunk - add two commits with message
XthenY - create your changes,
XandY, 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