Computing FoundationsGit and GitHub › Day 31

Hands-on lab — Day 31: 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-merging when 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 --oneline diagram 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 git installed (git --version should 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 (for git switch; older git works via the git checkout substitutions in troubleshooting.md).
  • bash and the standard utilities mktemp, 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 on main, merges the branch (git merge) into a real conflict, resolves it (git add + git commit), prints the git log --graph diamond, 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

  1. Run bash examples/branch_merge.sh — it must reach the final === Done. === line and exit 0.
  2. Confirm Step 4 prints CONFLICT (content): Merge conflict in recipe.txt and the three marker lines (<<<<<<<, =======, >>>>>>>).
  3. Confirm Step 5's resolved file shows milk: 2 cups buttermilk and no marker lines.
  4. Confirm Step 6's graph shows a |\ diamond with a Merge branch 'feature-blueberries' node.
  5. 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

  1. Keep only one side. Re-run the conflict, but resolve it by keeping only main's line (or only the feature's). Note how git log --graph looks identical — resolution content does not change the shape of history.
  2. Abort instead of resolve. After the conflict appears, run git merge --abort and confirm with git status that the working tree returned to its pre-merge state.
  3. Force a fast-forward to fail. Add a commit on main before merging a branch, then try git merge --ff-only BRANCH. Explain why git refuses.
  4. Visualize differently. Run git log --graph --oneline --decorate --all and identify every branch pointer and the merge commit in the output.
  • 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 NAMEgit checkout -b NAME (create and move onto a branch)
  • git switch NAMEgit 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 -d to create a fresh, private workspace and deletes it on exit via trap 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.yml records requires_network: false. The lab runs identically on a fully offline machine.
  • A local-only git identity. The scripts set user.name and user.email with git config --local, so the throwaway identity (learner@example.invalid) applies only to the temporary repository and never edits your global ~/.gitconfig. The .invalid address 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 to sudo a 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.