Computing Foundations › Git and GitHub › Day 31
Hands-on lab — Day 31: Branching and Merging
- ← Back to the Day 31 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/computing-foundations/day-031-branching-and-merging/
Commands
Setup
cd labs/sections/computing-foundations/day-031-branching-and-merging Run
bash examples/branch_merge.sh
bash starter/branch_merge.sh Test
bash tests/run_tests.sh File tree
examples/branch_merge.sh expected-output/FIELDS.md expected-output/sample-run.txt metadata.yml README.md requirements/README.md security.md starter/branch_merge.sh starter/branching-worksheet.md tests/run_tests.sh troubleshooting.md
Lab README
Day 031 lab — Branch, Conflict, Merge
Lesson
- Lesson title: Branching and Merging
- Day number: 31 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-031-branching-and-merging
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-031-branching-and-mergingwhen the site is running.
Purpose
Day 31's lesson explains branching and merging in the abstract. This lab makes
it real: in a throwaway repository you create a feature branch, make two
branches disagree about the same line of a file, merge them into a genuine
conflict, read the conflict markers git leaves behind, resolve them, and commit
the merge. You also watch a clean fast-forward merge for contrast, and
inspect the branching history with git log --graph. By the end you have felt
the exact loop you will repeat on every real project.
Learning objectives
- Create and switch to a branch with
git switch -c. - Produce a real three-way merge conflict on purpose and read its markers.
- Resolve a conflict, stage it with
git add, and complete the merge commit. - Tell a fast-forward merge from a three-way merge by looking at the history.
- Read a
git log --graph --onelinediagram and find the merge commit in it.
Prerequisites
- The Day 31 lesson (read it first — it explains every term this lab uses).
- Day 30's lab habits:
git init,git add,git commit,git log. - A terminal with
gitinstalled (git --versionshould print a version).
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon, git 2.50.1).
- Linux — fully supported (any distribution with git ≥ 2.23).
- Windows — use Git for Windows' "Git Bash", or run the scripts unmodified inside WSL.
Hardware requirements
Any computer that runs git. The lab writes a few tiny text files into a temporary directory and deletes them on exit; it needs no meaningful disk, RAM, or GPU.
Required software
git≥ 2.23 (forgit switch; older git works via thegit checkoutsubstitutions introubleshooting.md).bashand the standard utilitiesmktemp,printf,sed,grep— all preinstalled on macOS and Linux.
Free and open-source options
Everything here is free and open source: git itself, bash, and every utility used. No account, API key, network connection, or purchase is required.
Installation
None beyond git. Move into this directory and you are ready:
cd labs/sections/computing-foundations/day-031-branching-and-merging
If git --version fails, see requirements/README.md for one-line install
commands per platform.
File structure
day-031-branching-and-merging/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── branch_merge.sh ← YOUR working file (5 exercises)
│ └── branching-worksheet.md ← record branches, merge type, conflict
├── examples/
│ └── branch_merge.sh ← completed reference implementation
├── tests/
│ └── run_tests.sh ← automated checks (build repo, assert git state)
├── expected-output/
│ ├── sample-run.txt ← a real captured run
│ └── FIELDS.md ← what is fixed vs what varies each run
├── requirements/
│ └── README.md ← dependency statement (just git)
├── troubleshooting.md
└── security.md
How to run
From this directory:
## 1. See the whole story run end to end
bash examples/branch_merge.sh
## 2. Your task: complete the five exercises in the starter, then run it
bash starter/branch_merge.sh
## 3. Check your work
bash tests/run_tests.sh
Each script builds a fresh temporary repository and deletes it on exit, so you can run them as many times as you like.
What the commands do
bash examples/branch_merge.sh— creates a throwaway repo with a local git identity, makes a base commit, branches off (git switch -c), edits a line on the branch, edits the same line differently back onmain, merges the branch (git merge) into a real conflict, resolves it (git add+git commit), prints thegit log --graphdiamond, and then does a separate clean fast-forward merge for contrast.bash starter/branch_merge.sh— the same skeleton with five numbered placeholder lines; each exercise comment names the exact git command to type in its place. Edit the file, then run it.bash tests/run_tests.sh— builds its own repository and asserts the real git outcomes: a branch was created, the merge conflicted, the conflict was resolved, the tree ends clean, the tip is a two-parent merge commit, and the graph shows the merge. It also runs the example script end to end.
Expected output
See expected-output/sample-run.txt — a real
captured run. The key moments (commit hashes and the temp path differ every
run — see expected-output/FIELDS.md):
--- Step 4: merge feature-blueberries into main (expect a conflict) ---
Auto-merging recipe.txt
CONFLICT (content): Merge conflict in recipe.txt
Automatic merge failed; fix conflicts and then commit the result.
...
<<<<<<< HEAD
milk: 1 cup buttermilk
=======
milk: 2 cups
>>>>>>> feature-blueberries
...
--- Step 6: history graph (the merge commit ties both lines together) ---
* 7c522b2 Merge branch 'feature-blueberries'
|\
| * 84e0a3a feature: use 2 cups of milk
* | fe77868 main: switch to buttermilk
|/
* 493ba6c Add base pancake recipe
Validation steps
- Run
bash examples/branch_merge.sh— it must reach the final=== Done. ===line and exit 0. - Confirm Step 4 prints
CONFLICT (content): Merge conflict in recipe.txtand the three marker lines (<<<<<<<,=======,>>>>>>>). - Confirm Step 5's resolved file shows
milk: 2 cups buttermilkand no marker lines. - Confirm Step 6's graph shows a
|\diamond with aMerge branch 'feature-blueberries'node. - Run the tests (next section) — all checks must pass.
Tests
bash tests/run_tests.sh
Expected final line: 10 checks, 0 failure(s). The command exits 0 on success
and non-zero on any failure, so it can run in CI. It needs no network.
Cleanup
Nothing to clean up. Every script works inside its own temporary directory and
removes it on exit (trap cleanup EXIT). No files are written outside that
directory, and your global git configuration is never touched.
Troubleshooting
See troubleshooting.md: older git without git switch,
reading and removing conflict markers, git merge --abort when you want to
start over, and why nothing persists after the script finishes.
Security notes
See security.md. Short version: temp directory only, no network,
a local-only .invalid git identity, no sudo, and short scripts you should
read before running.
Extension exercises
- Keep only one side. Re-run the conflict, but resolve it by keeping only
main's line (or only the feature's). Note howgit log --graphlooks identical — resolution content does not change the shape of history. - Abort instead of resolve. After the conflict appears, run
git merge --abortand confirm withgit statusthat the working tree returned to its pre-merge state. - Force a fast-forward to fail. Add a commit on
mainbefore merging a branch, then trygit merge --ff-only BRANCH. Explain why git refuses. - Visualize differently. Run
git log --graph --oneline --decorate --alland identify every branch pointer and the merge commit in the output.
Navigation
- Previous day: Day 30 — Git Fundamentals: Repositories, Staging, and
Commits (
labs/sections/computing-foundations/day-030-git-fundamentals-repositories-staging-and-commits/). - Next day: Day 32 — Remotes and GitHub
(
labs/sections/computing-foundations/day-032-remotes-and-github/).
Expected output
FIELDS.md
# Expected output — what is fixed and what varies
`sample-run.txt` in this directory is a **real captured run** of
`examples/branch_merge.sh` (macOS, git 2.50.1, 2026-07-12). Because git makes
new commits and a new temporary directory every time, two things change on
every run and are **not** errors:
- **The workspace path** on the `Workspace:` line (a random `mktemp` name such
as `/var/folders/.../day031-branch-merge.XXXXXX` on macOS or
`/tmp/day031-branch-merge.XXXXXX` on Linux).
- **The seven-character commit hashes** (`493ba6c`, `84e0a3a`, …). Git derives
each hash from the commit's content, author, and timestamp, so yours will
differ. Their **relationships** are what matter, not their values.
Everything else must appear, in this order, on every platform:
1. `=== Branch, Conflict, Merge ===`
2. Step 1: the base commit line `<hash> Add base pancake recipe`
3. Step 2: `feature-blueberries now says:` then `milk: 2 cups`
4. Step 3: `main now says:` then `milk: 1 cup buttermilk`
5. Step 4: git's own conflict report —
`CONFLICT (content): Merge conflict in recipe.txt`,
`UU recipe.txt`, and the three marker lines
`<<<<<<< HEAD`, `=======`, `>>>>>>> feature-blueberries`
6. Step 5: the resolved file with `milk: 2 cups buttermilk` and **no markers**
7. Step 6: a `git log --graph` diamond — a `Merge branch 'feature-blueberries'`
node above a `|\` split that rejoins at `|/`
8. Step 7: `Fast-forward` and a linear graph with the new commit on top
9. `=== Done. Temporary repository will be deleted on exit. ===`
## Platform differences
- **macOS** temp paths live under `/var/folders/...`; **Linux** under `/tmp`.
The script uses `${TMPDIR:-/tmp}`, so both are handled automatically.
- Some git versions phrase the fast-forward and merge lines slightly
differently (e.g. `Merge made by the 'ort' strategy`). The shape — a
conflict that is resolved into a two-parent merge commit, plus a separate
fast-forward — is identical.
- Git 2.23+ is assumed for `git switch`. On older git, `git checkout -b` and
`git checkout <branch>` do the same thing; see `troubleshooting.md`.
sample-run.txt
=== Branch, Conflict, Merge ===
Workspace: /var/folders/7j/4qzljp553ndfjm_y6zbygsz00000gn/T//day031-branch-merge.33ScBr
--- Step 1: base commit on main ---
493ba6c Add base pancake recipe
--- Step 2: create feature branch 'feature-blueberries' and edit line 3 ---
feature-blueberries now says:
milk: 2 cups
--- Step 3: back on main, edit the SAME line 3 differently ---
main now says:
milk: 1 cup buttermilk
--- Step 4: merge feature-blueberries into main (expect a conflict) ---
Auto-merging recipe.txt
CONFLICT (content): Merge conflict in recipe.txt
Automatic merge failed; fix conflicts and then commit the result.
Conflict detected. git status shows the unmerged file:
UU recipe.txt
Git rewrote recipe.txt with conflict markers:
# Pancakes
flour: 1 cup
<<<<<<< HEAD
milk: 1 cup buttermilk
=======
milk: 2 cups
>>>>>>> feature-blueberries
eggs: 1
--- Step 5: resolve the conflict, then add + commit ---
Resolved. Final recipe:
# Pancakes
flour: 1 cup
milk: 2 cups buttermilk
eggs: 1
--- Step 6: history graph (the merge commit ties both lines together) ---
* 7c522b2 Merge branch 'feature-blueberries'
|\
| * 84e0a3a feature: use 2 cups of milk
* | fe77868 main: switch to buttermilk
|/
* 493ba6c Add base pancake recipe
--- Step 7: a clean fast-forward merge (no divergence, no conflict) ---
Updating 7c522b2..277fc49
Fast-forward
serving.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 serving.txt
History after the fast-forward (feature-notes just extends the line):
* 277fc49 feature: add serving suggestion
* 7c522b2 Merge branch 'feature-blueberries'
|\
| * 84e0a3a feature: use 2 cups of milk
* | fe77868 main: switch to buttermilk
|/
* 493ba6c Add base pancake recipe
=== Done. Temporary repository will be deleted on exit. ===
Source files
examples/branch_merge.sh (5631 bytes)
#!/usr/bin/env bash
# Day 031 lab — completed reference implementation.
#
# "Branch, Conflict, Merge": builds a throwaway git repository in a temporary
# directory, then walks through the whole branching story end to end:
#
# 1. a base commit on the default branch
# 2. a feature branch that edits some lines
# 3. a competing edit to the SAME lines back on the default branch
# 4. a three-way merge that COLLIDES -> a real conflict
# 5. a programmatic conflict resolution (git add + git commit)
# 6. git log --graph to see the diamond shape the merge created
# 7. a separate branch that merges cleanly as a FAST-FORWARD
#
# Everything is local: no network, no GitHub, no credentials. The temporary
# repository is deleted on exit, so running this leaves your machine untouched.
set -euo pipefail
# --- Make a private, disposable workspace ------------------------------------
# mktemp -d creates a fresh directory with a random name. We remove it on exit
# (success OR failure) via a trap, so nothing lingers.
workdir="$(mktemp -d "${TMPDIR:-/tmp}/day031-branch-merge.XXXXXX")"
cleanup() { rm -rf "${workdir}"; }
trap cleanup EXIT
echo "=== Branch, Conflict, Merge ==="
echo "Workspace: ${workdir}"
echo
cd "${workdir}"
# --- Step 0: a repository with a LOCAL identity ------------------------------
# git init makes the current directory a repository. We set user.name and
# user.email with --local so this identity applies ONLY to this throwaway repo
# and never touches your real global git config. -b main names the first
# branch "main" so the output is predictable across git versions.
git init -q -b main
git config --local user.name "Day 31 Learner"
git config --local user.email "learner@example.invalid"
echo "--- Step 1: base commit on main ---"
# A tiny recipe file is our shared document. Everyone starts from this version.
printf '%s\n' \
"# Pancakes" \
"flour: 1 cup" \
"milk: 1 cup" \
"eggs: 1" > recipe.txt
git add recipe.txt
git commit -q -m "Add base pancake recipe"
git log --oneline
echo
# --- Step 2: a feature branch that changes a line ----------------------------
echo "--- Step 2: create feature branch 'feature-blueberries' and edit line 3 ---"
git switch -q -c feature-blueberries
# On the feature branch we double the milk. This rewrites line 3.
printf '%s\n' \
"# Pancakes" \
"flour: 1 cup" \
"milk: 2 cups" \
"eggs: 1" > recipe.txt
git commit -q -am "feature: use 2 cups of milk"
echo "feature-blueberries now says:"
sed -n '3p' recipe.txt
echo
# --- Step 3: a COMPETING edit to the same line, back on main -----------------
echo "--- Step 3: back on main, edit the SAME line 3 differently ---"
git switch -q main
# On main someone else changes line 3 to buttermilk instead. Two branches,
# two different values for the same line: this is what makes a conflict.
printf '%s\n' \
"# Pancakes" \
"flour: 1 cup" \
"milk: 1 cup buttermilk" \
"eggs: 1" > recipe.txt
git commit -q -am "main: switch to buttermilk"
echo "main now says:"
sed -n '3p' recipe.txt
echo
# --- Step 4: merge the feature branch -> a real three-way merge conflict -----
echo "--- Step 4: merge feature-blueberries into main (expect a conflict) ---"
# We want the merge to FAIL with a conflict, but 'set -e' would abort the
# script on git merge's non-zero exit. Guard it so we can react instead.
if git merge --no-edit feature-blueberries; then
echo "Unexpected: the merge succeeded without a conflict." >&2
exit 1
fi
echo
echo "Conflict detected. git status shows the unmerged file:"
git status --short
echo
echo "Git rewrote recipe.txt with conflict markers:"
cat recipe.txt
echo
# --- Step 5: resolve the conflict programmatically ---------------------------
echo "--- Step 5: resolve the conflict, then add + commit ---"
# A human would open the file and edit out the markers by hand. To keep this
# script deterministic we write the resolved file directly: we KEEP BOTH
# ideas — buttermilk AND more of it — which is a real editorial decision, not
# a blind "take theirs". The three marker lines (<<<<<<<, =======, >>>>>>>)
# are gone; that is what "resolved" means.
printf '%s\n' \
"# Pancakes" \
"flour: 1 cup" \
"milk: 2 cups buttermilk" \
"eggs: 1" > recipe.txt
# Prove no conflict markers remain before we stage the file.
if grep -qE '^(<<<<<<<|=======|>>>>>>>)' recipe.txt; then
echo "Conflict markers still present — resolution incomplete." >&2
exit 1
fi
git add recipe.txt # staging the file tells git "this conflict is handled"
git commit --no-edit -q # completes the merge; creates the MERGE COMMIT
echo "Resolved. Final recipe:"
cat recipe.txt
echo
# --- Step 6: view the history as a graph -------------------------------------
echo "--- Step 6: history graph (the merge commit ties both lines together) ---"
git log --graph --oneline
echo
# --- Step 7: a clean FAST-FORWARD merge on a separate branch -----------------
echo "--- Step 7: a clean fast-forward merge (no divergence, no conflict) ---"
# Branch off, add a brand-new file (touches nothing main changed), come back.
# Because main has not moved since we branched, git can merge by simply sliding
# main's pointer forward to the feature commit: a fast-forward, no merge commit.
git switch -q -c feature-notes
printf '%s\n' "Serve warm with syrup." > serving.txt
git add serving.txt
git commit -q -m "feature: add serving suggestion"
git switch -q main
git merge --ff-only feature-notes
echo
echo "History after the fast-forward (feature-notes just extends the line):"
git log --graph --oneline
echo
echo "=== Done. Temporary repository will be deleted on exit. ==="
metadata.yml (573 bytes)
lesson_id: D031
day: 31
kind: command-line-inspection
languages: [bash]
setup_commands:
- cd labs/sections/computing-foundations/day-031-branching-and-merging
run_commands:
- bash examples/branch_merge.sh
- bash starter/branch_merge.sh
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'none — each script deletes its own temporary repository on exit'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), git 2.50.1, bash tests/run_tests.sh → 10 checks, 0 failures'
requirements/README.md (1123 bytes)
# Dependencies — Day 031 lab
**One tool: `git`.** Nothing else is installed, downloaded, or configured.
- `git` ≥ 2.23 (for `git switch`; released August 2019). Check with
`git --version`. On older git, the lab still works with `git checkout`
substitutions documented in `troubleshooting.md`.
- `bash` ≥ 3.2 (preinstalled on macOS and every mainstream Linux distribution)
and the standard utilities `mktemp`, `printf`, `sed`, `grep` — all part of
the base system.
There is deliberately no `requirements.txt` or `package.json`: the lab creates
a throwaway repository in a temporary directory and needs no project of its
own.
## Installing git (if `git --version` fails)
- **macOS:** `xcode-select --install` (installs the Command Line Tools, which
include git), or install from the official git website.
- **Debian/Ubuntu:** `sudo apt update && sudo apt install git`
- **Fedora:** `sudo dnf install git`
- **Windows:** install Git for Windows, or run this lab inside WSL and use the
Linux path.
No network access is required to run the lab itself — only, possibly, to
install git the first time.
starter/branch_merge.sh (5091 bytes)
#!/usr/bin/env bash
# Day 031 lab — YOUR working file: "Branch, Conflict, Merge".
#
# This starter builds a throwaway git repository for you and makes the base
# commit. Your job is to complete the five numbered exercises below by filling
# in the exact git commands named in each comment, replacing every line that
# says: echo "EXERCISE N -- replace this line ...".
#
# The finished reference version is in examples/branch_merge.sh — try the
# exercises yourself first, then compare.
#
# Everything is local and offline. The temporary repository is deleted on exit,
# so you can run this as many times as you like without touching your machine.
set -euo pipefail
# --- A private, disposable workspace (already done for you) -------------------
workdir="$(mktemp -d "${TMPDIR:-/tmp}/day031-starter.XXXXXX")"
cleanup() { rm -rf "${workdir}"; }
trap cleanup EXIT
cd "${workdir}"
echo "=== Branch, Conflict, Merge (starter) ==="
echo "Workspace: ${workdir}"
echo
# --- Repo with a LOCAL identity + base commit (already done for you) ----------
git init -q -b main
git config --local user.name "Day 31 Learner"
git config --local user.email "learner@example.invalid"
printf '%s\n' "# Pancakes" "flour: 1 cup" "milk: 1 cup" "eggs: 1" > recipe.txt
git add recipe.txt
git commit -q -m "Add base pancake recipe"
echo "Base commit made on main. recipe.txt line 3 is: $(sed -n '3p' recipe.txt)"
echo
# =============================================================================
# EXERCISE 1 — create and switch to a feature branch.
# Use ONE command that both creates the branch and moves onto it:
# git switch -c feature-blueberries
# (older git: git checkout -b feature-blueberries)
# Replace the placeholder line below with that command.
# =============================================================================
echo "EXERCISE 1 (replace this line): create and switch to branch feature-blueberries with git switch -c"
# On the feature branch, change line 3 to "milk: 2 cups", then commit it.
printf '%s\n' "# Pancakes" "flour: 1 cup" "milk: 2 cups" "eggs: 1" > recipe.txt
git commit -q -am "feature: use 2 cups of milk"
echo "On feature branch, line 3 is now: $(sed -n '3p' recipe.txt)"
echo
# =============================================================================
# EXERCISE 2 — switch back to main.
# Use: git switch main (older git: git checkout main)
# Replace the placeholder line below with that command.
# =============================================================================
echo "EXERCISE 2 (replace this line): switch back to main with git switch main"
# Back on main, change the SAME line 3 differently, then commit it. Two branches
# now disagree about line 3 — the setup for a conflict.
printf '%s\n' "# Pancakes" "flour: 1 cup" "milk: 1 cup buttermilk" "eggs: 1" > recipe.txt
git commit -q -am "main: switch to buttermilk"
echo "On main, line 3 is now: $(sed -n '3p' recipe.txt)"
echo
# =============================================================================
# EXERCISE 3 — merge the feature branch into main.
# Use: git merge --no-edit feature-blueberries
# This WILL report a conflict — that is expected and correct. Because the
# script uses 'set -e', wrap the command so a conflict does not abort us:
# git merge --no-edit feature-blueberries || true
# Replace the placeholder line below with that guarded command.
# =============================================================================
echo "EXERCISE 3 (replace this line): merge feature-blueberries into main with git merge --no-edit (guard with || true)"
echo
echo "git status after the merge attempt:"
git status --short
echo
echo "recipe.txt now contains conflict markers:"
cat recipe.txt
echo
# =============================================================================
# EXERCISE 4 — resolve the conflict.
# A real resolution removes the three marker lines (<<<<<<<, =======, >>>>>>>)
# and leaves the wording you want. Here we keep BOTH ideas. Replace the
# placeholder line below with a command that writes the resolved line 3, e.g.:
# printf '%s\n' "# Pancakes" "flour: 1 cup" "milk: 2 cups buttermilk" "eggs: 1" > recipe.txt
# =============================================================================
echo "EXERCISE 4 (replace this line): write the resolved recipe.txt (line 3 = milk: 2 cups buttermilk, no markers)"
echo "recipe.txt after your resolution:"
cat recipe.txt
echo
# =============================================================================
# EXERCISE 5 — stage the resolved file and complete the merge.
# Two commands: stage the file, then commit to create the merge commit:
# git add recipe.txt
# git commit --no-edit
# Replace the placeholder line below with those two commands.
# =============================================================================
echo "EXERCISE 5 (replace this line): run git add recipe.txt then git commit --no-edit to finish the merge"
echo
echo "Final history graph:"
git log --graph --oneline
echo
echo "=== Done. Temporary repository will be deleted on exit. ==="
starter/branching-worksheet.md (1745 bytes)
# Branching worksheet — Day 031
Fill this in from your own run of `starter/branch_merge.sh` (after you have
completed the five exercises) or from your own hand-typed experiment. Keep it —
Week 5's project (the Versioned Notes Repository) reuses exactly these moves.
## Branches I created
| Branch name | Created with (command) | What I changed on it |
| ----------- | ---------------------- | -------------------- |
| | | |
| | | |
## The merge I performed
| Question | My answer |
| -------- | --------- |
| Which branch did I merge INTO? | |
| Which branch did I merge IN? | |
| Was it a **fast-forward** or a **three-way** merge? | |
| How do I know? (merge commit created? `|\` diamond in the graph?) | |
> Reminder: a **fast-forward** happens when the target branch has not moved
> since you branched, so git just slides its pointer forward — no merge commit.
> A **three-way merge** happens when both branches advanced, so git builds a
> new merge commit with two parents (and may hit a conflict on the way).
## The conflict I resolved
Paste the exact conflict block git wrote into the file (the three marker lines
and the two competing versions between them):
```text
<<<<<<< HEAD
=======
>>>>>>>
```
- The version above `=======` came from branch: ____________________
- The version below `=======` came from branch: ____________________
- The resolution I chose (kept theirs / kept mine / blended both): ___________
- The final line I committed: ____________________________________________
## One sentence: why did this conflict happen?
_(Hint: what did the two branches both do to the same line?)_
tests/run_tests.sh (4850 bytes)
#!/usr/bin/env bash
# Tests for the Day 031 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# These tests build their OWN throwaway repository in a temp directory and
# drive the same branch -> conflict -> resolve -> merge story the lab teaches,
# then assert on the resulting git state:
# * a feature branch was created
# * merging it produced a real conflict
# * the conflict was resolved and the repo ends CLEAN
# * the final history contains a real merge commit (two parents)
# * git log --graph renders the merge (the |\ diamond)
#
# Everything is local and offline. The temp repo is deleted on exit.
set -u
# Resolve the lab directory BEFORE we change into the temp workspace, because
# BASH_SOURCE becomes a relative path once we cd elsewhere.
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
failures=0
checks=0
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
# Refuse to run without git rather than failing mysteriously later.
if ! command -v git >/dev/null 2>&1; then
echo "FAIL: git is not installed or not on PATH." >&2
exit 1
fi
workdir="$(mktemp -d "${TMPDIR:-/tmp}/day031-tests.XXXXXX")"
cleanup() { rm -rf "${workdir}"; }
trap cleanup EXIT
cd "${workdir}" || { echo "FAIL: could not enter temp dir" >&2; exit 1; }
echo "Testing branch/conflict/merge behavior in ${workdir} ..."
# --- Build the scenario ------------------------------------------------------
git init -q -b main
git config --local user.name "Day 31 Test"
git config --local user.email "test@example.invalid"
printf '%s\n' "# Pancakes" "flour: 1 cup" "milk: 1 cup" "eggs: 1" > recipe.txt
git add recipe.txt
git commit -q -m "base"
# Feature branch edits line 3.
git switch -q -c feature-x
printf '%s\n' "# Pancakes" "flour: 1 cup" "milk: 2 cups" "eggs: 1" > recipe.txt
git commit -q -am "feature edit"
# 1) A branch was created.
if git branch --format='%(refname:short)' | grep -qx "feature-x"; then
check "a feature branch was created" "yes"
else
check "a feature branch was created" "no"
fi
# Competing edit on main, same line.
git switch -q main
printf '%s\n' "# Pancakes" "flour: 1 cup" "milk: 1 cup buttermilk" "eggs: 1" > recipe.txt
git commit -q -am "main edit"
# 2) Merging produces a conflict (non-zero exit + markers in the file).
if git merge --no-edit feature-x >/dev/null 2>&1; then
check "merging the divergent branches produced a conflict" "no"
else
check "merging the divergent branches produced a conflict" "yes"
fi
if grep -qE '^(<<<<<<<|=======|>>>>>>>)' recipe.txt; then
check "git wrote conflict markers into the file" "yes"
else
check "git wrote conflict markers into the file" "no"
fi
# git records the file as unmerged (status code UU) mid-conflict.
if [ -n "$(git ls-files --unmerged)" ]; then
check "git records the file as unmerged during the conflict" "yes"
else
check "git records the file as unmerged during the conflict" "no"
fi
# --- Resolve ----------------------------------------------------------------
printf '%s\n' "# Pancakes" "flour: 1 cup" "milk: 2 cups buttermilk" "eggs: 1" > recipe.txt
# 3) No conflict markers remain after resolution.
if grep -qE '^(<<<<<<<|=======|>>>>>>>)' recipe.txt; then
check "conflict markers removed after resolution" "no"
else
check "conflict markers removed after resolution" "yes"
fi
git add recipe.txt
git commit --no-edit -q
# 4) Working tree is clean after committing the merge.
if [ -z "$(git status --porcelain)" ]; then
check "repository is clean after resolving the merge" "yes"
else
check "repository is clean after resolving the merge" "no"
fi
# 5) The tip commit is a real merge commit (exactly two parents).
parents="$(git cat-file -p HEAD | grep -c '^parent ')"
if [ "${parents}" -eq 2 ]; then
check "the final commit is a merge commit (two parents)" "yes"
else
check "the final commit is a merge commit (two parents)" "no"
fi
# git rev-list agrees there is a merge in history.
if [ "$(git rev-list --merges --count HEAD)" -ge 1 ]; then
check "history contains at least one merge commit" "yes"
else
check "history contains at least one merge commit" "no"
fi
# 6) The graph renders the merge diamond (a line containing |\).
if git log --graph --oneline | grep -q '|\\'; then
check "git log --graph shows the merge diamond" "yes"
else
check "git log --graph shows the merge diamond" "no"
fi
# 7) The reference example script also runs clean end to end.
if bash "${lab_dir}/examples/branch_merge.sh" >/dev/null 2>&1; then
check "examples/branch_merge.sh runs end to end and exits 0" "yes"
else
check "examples/branch_merge.sh runs end to end and exits 0" "no"
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 031 lab
git: 'switch' is not a git command
Your git predates version 2.23 (August 2019). git switch and git restore
are the modern spellings; the classic git checkout still does everything:
git switch -c NAME→git checkout -b NAME(create and move onto a branch)git switch NAME→git checkout NAME(move onto an existing branch)
The scripts use git switch; either upgrade git or apply these substitutions.
Reading the conflict markers
When a merge conflicts, git rewrites the file with three marker lines around the clashing region:
<<<<<<< HEAD
milk: 1 cup buttermilk <- the version already on your current branch (main)
=======
milk: 2 cups <- the version coming from the branch you are merging
>>>>>>> feature-blueberries
To resolve: delete all three marker lines (<<<<<<<, =======,
>>>>>>>) and edit what remains until the file reads exactly the way you want
— which may be one side, the other, or a blend of both. Then git add the
file and git commit. The conflict is only "resolved" once no marker lines
remain; grep -n '^<<<<<<<\|^=======\|^>>>>>>>' FILE finds any you missed.
I panicked and want to start the merge over
A conflicted merge is completely reversible until you commit it:
git merge --abort
This throws away the half-finished merge and returns the working tree to
exactly how it was before you ran git merge. Nothing is lost. Then you can
try again when ready.
"You have not concluded your merge (MERGE_HEAD exists)"
Git is reminding you that a merge is still in progress. Either finish it
(git add the resolved files, then git commit) or cancel it
(git merge --abort). You cannot switch branches mid-conflict.
The graph doesn't show a |\ diamond
You probably got a fast-forward instead of a three-way merge — the two branches had not truly diverged, so git just slid the pointer forward and no merge commit was created. A diamond only appears when both branches added commits after they split. Step 4 of the lab forces divergence on purpose; Step 7 shows the fast-forward case for contrast.
nothing to commit, working tree clean when you expected a conflict
The branches did not actually change the same lines, so git merged them
automatically. Make sure both edits touch the same line of recipe.txt.
Nothing appears to persist after the script finishes
That is by design. Each script works inside a temporary directory that is
deleted on exit (the trap cleanup EXIT line). To keep a repository around
and poke at it yourself, create your own directory and run the git commands
there by hand.
Security notes
Security notes — Day 031 lab
- Everything happens in a temporary directory. Each script calls
mktemp -dto create a fresh, private workspace and deletes it on exit viatrap cleanup EXIT. The lab never writes into your real projects, your home directory, or any existing git repository. - No network, ever. There is no
git clone,git push,git pull, or any other remote operation.metadata.ymlrecordsrequires_network: false. The lab runs identically on a fully offline machine. - A local-only git identity. The scripts set
user.nameanduser.emailwithgit config --local, so the throwaway identity (learner@example.invalid) applies only to the temporary repository and never edits your global~/.gitconfig. The.invalidaddress is a reserved non-routable domain, so nothing could ever be sent to it. - No elevated privileges. Nothing here needs
sudo. If a tutorial ever tells you tosudoa git command you have not read, stop and read it first. - Read before you run. Both scripts are short and commented — read them before executing, a habit worth keeping for every script you download.