Update With Merge #
This section builds on the previous Git Fundamentals section.
If you have not done that section you should still be able to do all the exercises here since each one starts with a script to get you to a known starting state.
Back NextProblem: Updating to the latest Main #
This will cover a very common situation and help you understand two critical features of git and how they help you solve the problem.
Problem: You are working on a repo in a topic(feature) branch and while you are doing so, the
main
branch for the repo gets updated with new code. You want to test your new feature alongside the new code.
Now you have two choices to solve this issue and get your branch up to date with
the latest main
The rest of this pages walks through pulling all the latest code into your branch with git merge
Setup #
Let’s start by setting up our test repo to a known state.
Show Script
You can simply click in the block below to copy the snippet and paste it into a terminal.
WARNING: This will DELETE everything from
~/gitrepo
if either exists Make a backup if you want to keep work from that repo.
cd ~
bash <<EOF
set -e
rm -rf ~/gitrepo
mkdir -p ~/gitrepo
cd ~/gitrepo
git init
echo "# My Git Repository" > README.md
git add README.md
mkdir assets
curl -Lo assets/gopher.png \
https://go.dev/doc/gopher/doc.png
git add assets
git commit -m "Initial Add"
touch api
echo "## Has an API" >> README.md
git add .
git commit -m "feat: add api"
git tag v0.0.1
git checkout -b topic
touch docs
git add .
echo "## Has Docs" >> README.md
git commit -m "feat: add docs"
echo "acls" >> api
echo "## Has ACLs" >> README.md
git add .
git commit -m "feat: add acls"
git checkout main
touch users
echo "## Has Users" >> README.md
git add .
git commit -m "feat: add users"
echo "refactored" >> api
echo "## API Refactored" >> README.md
git add .
git commit -m "refactor: api"
git checkout topic
echo
echo "======= Setup success! ======="
EOF
cd ~/gitrepo
cd ..
rmdir /S /Q gitrepo
mkdir gitrepo
cd gitrepo
git init
echo "# My Git Repository" > README.md
git add README.md
mkdir assets
echo "Gopher" > assets\gopher.png
git add assets
git commit -m "Initial Add"
type nul > api
echo "## Has an API" >> README.md
git add .
git commit -m "feat: add api"
git tag v0.0.1
git checkout -b topic
type nul > docs
git add .
echo "## Has Docs" >> README.md
git commit -m "feat: add docs"
echo "acls" >> api
echo "## Has ACLs" >> README.md
git add .
git commit -m "feat: add acls"
git checkout main
type nul > users
echo "## Has Users" >> README.md
git add .
git commit -m "feat: add users"
echo "refactored" >> api
echo "## API Refactored" >> README.md
git add .
git commit -m "refactor: api"
git checkout topic
echo "======= Setup success! ======="
Check the two logs #
The log for topic
git log topic --pretty=oneline
e4a3fc6118be3193724b4d44dd1c3e2361ceefb1 (HEAD -> topic) feat: add acls
e79af8b7d0f95d7ec80637051734c8bae5b22c45 feat: add docs
221f2c37b9de909e5e25b53031f752f8f1f0ebb4 (tag: v0.0.1) feat: add api
f74cd0034f3cba2fd6b26864e1e153ebae340b41 Initial Add
THe log for main
git log main --pretty=oneline
61545bbb1473e52bdab38ff78c1af57d47a5f9de (main) refactor: api
7ccefa1ff40dc32a49d1a9372b3265dd2c9a4ca1 feat: add users
221f2c37b9de909e5e25b53031f752f8f1f0ebb4 (tag: v0.0.1) feat: add api
f74cd0034f3cba2fd6b26864e1e153ebae340b41 Initial Add
The state of our branches #
Our local branches now look like this:
The bottom two commits are greyed out because there is no contention between these two repos
once they hit the feat: add api
commit.
Back Next*We won’t bother showing the tracking branch in this exercise, but it is still there
What is going to happen #
The entire idea of git merge
is that you are trying to pull in the latest
code from your merge head to your base branch.
It does this by essentially building a single new commit that contains the the entire latest state of the target.
Before we run the command, let’s understand what it’s going to do by breaking down exactly what is going to happen
Back NextUpstream content #
We can expand the commits in main
to see what files they
have updated or added.
Visualizing the merge commit #
While not 100% accurate, a good way to think about git merge
is to imagine
git building a single new commit that contains the sum of all the changes
you are going to pull in.
But it really only makes one commit with the final state. So it is better imagined like this:
Back NextApplying the merge commit #
Now that Git has made this commit. It can take the state of the repo as described
in that commit and merge it with the state of the repo as described in
the latest commit in topic
Back NextTwo major implications come with what will happen
- All of our original commits will remain untouched.
- Any merge conflicts will only need to be resolved once!
Do the merge #
git merge main
Auto-merging api
CONFLICT (content): Merge conflict in api
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
😱 Conflicts!
No one likes seeing a conflict on merge but sometimes they happen.
In our case, the fixes are simple. We can edit the README.md
and the api
files
to manually resolve the conflicts. Then once we are done we can mark them
as solved and commit them.
git add .
git commit
[topic 22abe1a] Merge branch 'main' into topic
Post-merge log #
If we check out git log, we can see that it contains all the commits
from both branches. And it also contains our new merge
commit.
git log --pretty=oneline
22abe1a5104e747d5361646e59c1d4b65896b449 (HEAD -> topic) Merge branch 'main' into topic
f5e5392a7004366c9a1f48799497bdbf7e883ff9 feat: add acls
42c8711920bf0017762446c33242953d968e4c8f (main) refactor: api
baa9e2f0cc6eb1e93edf1d3c5fba36b6ad5236a6 feat: add docs
0064f0d618f1f6e4d286fb27e081aa7d88246e27 feat: add users
98c289adc2a366186c3d3db1e8f987ef592243b7 (tag: v0.0.1) feat: add api
ad28d84b252411157d857c41931fbf9e8add1851 Initial Add
git log ordering #
You may have noticed that our stack diagram and the git log
have different orders.
That is not an accident.
Our stacks look like this:
In reality, we have to use a more complete git diagram to view the state of the repo at this time
%%{init: { 'theme': 'neutral' } }%% gitGraph commit id: "Initial add" tag: "v0.0.1" commit id: "feat: add api" branch topic commit id: "feat: add docs" commit id: "feat: add acls" checkout main commit id: "feat: add users" commit id: "refactor: api" checkout topic merge main id: "Merge branch 'main' into topic"
The same diagram could be shown like this:
%%{init: { 'theme': 'neutral' } }%% gitGraph commit id: "Initial add" tag: "v0.0.1" commit id: "feat: add api" branch topic checkout main commit id: "feat: add users" commit id: "refactor: api" checkout topic commit id: "feat: add docs" commit id: "feat: add acls" merge main id: "Merge branch 'main' into topic"
This is again a case where git
log can be confusing. Any tool that wants to take
the tree that is your git history and make into a serialized format
will need to decide how to order commits made on parallel tracks.
By default, git log
sorts commits by timestamp.
If you want, you can also visualize more complex branch histories using git itself:
git log --pretty=oneline --graph
* 22abe1a5104e747d5361646e59c1d4b65896b449 (HEAD -> topic) Merge branch 'main' into topic
|\
| * 42c8711920bf0017762446c33242953d968e4c8f (main) refactor: api
| * 0064f0d618f1f6e4d286fb27e081aa7d88246e27 feat: add users
* | f5e5392a7004366c9a1f48799497bdbf7e883ff9 feat: add acls
* | baa9e2f0cc6eb1e93edf1d3c5fba36b6ad5236a6 feat: add docs
|/
* 98c289adc2a366186c3d3db1e8f987ef592243b7 (tag: v0.0.1) feat: add api
* ad28d84b252411157d857c41931fbf9e8add1851 Initial Add
Pulling the changes from topic
to main
#
So now that we have the latest code in our branch, what about using merge
to get our changes pulled back into main
?
We can do that by simply checkout out main
and merging topic
git checkout main
git merge topic
Updating 42c8711..22abe1a
Fast-forward
README.md | 5 +++++
api | 4 ++++
docs | 0
3 files changed, 9 insertions(+)
create mode 100644 docs
Then check the log:
* 22abe1a5104e747d5361646e59c1d4b65896b449 (HEAD -> main, topic) Merge branch 'main' into topic
|\
| * 42c8711920bf0017762446c33242953d968e4c8f (main) refactor: api
| * 0064f0d618f1f6e4d286fb27e081aa7d88246e27 feat: add users
* | f5e5392a7004366c9a1f48799497bdbf7e883ff9 feat: add acls
* | baa9e2f0cc6eb1e93edf1d3c5fba36b6ad5236a6 feat: add docs
|/
* 98c289adc2a366186c3d3db1e8f987ef592243b7 (tag: v0.0.1) feat: add api
* ad28d84b252411157d857c41931fbf9e8add1851 Initial Add
In this case you can see one feature of merge
it did not make another merge
commit to bring our changes from topic back into main. This is because they both have the
exact same content already. So merge
just updated the pointer to main
to point
to the same commit as topic
does.
If you want to have a merge commit anytime you can use the --no-ff
flag to force git
to always make a merge commit.
What we end up now is a git history as shown below.
%%{init: { 'theme': 'neutral' } }%% gitGraph commit id: "Initial add" tag: "v0.0.1" commit id: "feat: add api" branch topic checkout main commit id: "feat: add users" commit id: "refactor: api" checkout topic commit id: "feat: add docs" commit id: "feat: add acls" merge main id: "Merge branch 'main' into topic" checkout main merge topic
Notice there is no extra commit, we are just effectively saying that main
starts at the same commit as topic
does.
Even if you add more commits to topic
it will have no effect, main will always start at the commit as it was when
the merge happened
%%{init: { 'theme': 'neutral' } }%% gitGraph commit id: "Initial add" tag: "v0.0.1" commit id: "feat: add api" branch topic checkout main commit id: "feat: add users" commit id: "refactor: api" checkout topic commit id: "feat: add docs" commit id: "feat: add acls" merge main id: "Merge branch 'main' into topic" checkout main merge topic checkout topic commit
Even if you add more commits to topic
, which would be unusual but not impossible,
it will have no effect, main will always start at the commit as it was when
the merge happened
And if a new branch is made of main
it will start at the commit
main
points to:
%%{init: { 'theme': 'neutral' } }%% gitGraph commit id: "Initial add" tag: "v0.0.1" commit id: "feat: add api" branch topic checkout main commit id: "feat: add users" commit id: "refactor: api" checkout topic commit id: "feat: add docs" commit id: "feat: add acls" merge main id: "Merge branch 'main' into topic" checkout main merge topic checkout topic commit checkout main branch topic2 commit
Back NextSummary #
So now that we have done a merge we can talk about what is good and bad about these merges.
Back NextPro: All of the original commits are untouched #
We have tracked every single committed state of our repo which means we could easily inspect the repo before and after our merge commit.
Back NextCon: All of the original commits are untouched #
Every commit is there, all the time. Not only do we have all these extra
Merge branch 'x' into y
commits, but any bad commits we created will stick around forever.
So a git diagram for merge-heavy branches can often look like this:
%%{init: { 'theme': 'neutral' } }%% gitGraph commit id: "Initial add" tag: "v0.0.1" commit id: "feat: add api" branch feat1 checkout feat1 commit id: "feat: add users" checkout main commit id: "feat: add docs" checkout feat1 merge main id: "Merge branch 'main' into feat1" commit id: "fix typo" commit id: "please work" checkout main commit id: "feat: add ACLs" checkout feat1 merge main id: "Merge branch 'main' into feat1" checkout main merge feat1 id: "Merge branch 'feat1' into main"
Thanks to all of our git merge
commands we get to enshrine every bad
commit and merge of main
-> feat1
to get the latest code into our git
history forever!
Pro: Conflicts only have to be solved once #
This is a big one: No matter how you do it, when you git merge
you only ever have to solve merge conflicts once. That is
because git only ever tries to merge two states together.
This means that even though we have to solve conflicts, we only ever
do it once per execution of the command. The same is not true of other methods
like rebase
Wrapping Up #
There is one other option for solving this issue. Using git rebase
which is covered
in the next section