Computing FoundationsGit and GitHub › Day 33

Hands-on lab — Day 33: Pull Requests and Code Review

Commands

Setup

cd labs/sections/computing-foundations/day-033-pull-requests-and-code-review

Run

bash examples/pr_flow.sh
bash starter/pr_flow.sh

Test

bash tests/run_tests.sh

File tree

examples/pr_flow.sh
expected-output/FIELDS.md
expected-output/sample-run.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/pr_flow.sh
starter/pr-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 033 lab — Simulate a Pull Request Locally

Lesson

Purpose

Day 33's lesson explains the pull-request-and-review workflow: propose a change on a branch, open a PR, review and iterate, then merge. This lab makes it concrete without needing a GitHub account or a network connection. Because a pull request is just review and merge gates wrapped around Git branches and merges, you rehearse the whole loop locally: make a change on a feature branch, produce the exact diff and commit log a pull request would show, simulate a round of review with a follow-up commit, then merge two different ways to see how the choice of merge strategy shapes history.

Learning objectives

  • Create a feature branch and make a single, focused commit — the substance of a pull request.
  • Produce a pull request's core artifacts locally: git diff main..feature and git log main..feature.
  • Simulate the review-and-iterate loop by adding an "addressed feedback" commit.
  • Merge with --no-ff to create a merge commit, exactly as a pull-request merge does, and read the result in git log --graph.
  • Contrast the squash strategy (git merge --squash) and see it collapse a branch into a single commit on main.

Prerequisites

  • The Day 33 lesson (read it first — it explains every concept this lab makes concrete).
  • Days 30–32: comfort creating branches, committing, and merging with Git.
  • A terminal and Git installed (git --version should print 2.0 or newer; 2.23+ preferred).

Supported operating systems

  • macOS — fully supported (tested on macOS with bash and system Git).
  • Linux — fully supported (any distribution with git, bash, mktemp, sed).
  • Windows — use Git Bash or WSL, where the scripts run unmodified. Plain PowerShell is not supported for this lab.

Hardware requirements

Any computer that can run Git. The lab creates a tiny temporary repository and writes nothing of size.

Required software

  • git (2.0 or newer; 2.23+ enables git switch, but the scripts fall back to git checkout automatically).
  • bash (3.2 or newer — preinstalled on macOS and Linux).
  • Standard utilities mktemp, sed, printf (all preinstalled).

Free and open-source options

Everything here is free and open source: Git itself and every command used ship with your OS or are freely installable. No account, API key, network, or purchase is needed. A real pull request additionally needs a free account on a hosting platform and network access; this lab covers that side conceptually so you can practice the mechanics offline.

Installation

None beyond Git. Clone the repository (or copy this directory) and change into it:

cd labs/sections/computing-foundations/day-033-pull-requests-and-code-review

File structure

day-033-pull-requests-and-code-review/
├── README.md                       ← you are here
├── metadata.yml                    ← machine-readable lab metadata
├── starter/
│   ├── pr_flow.sh                  ← YOUR working file (5 exercises)
│   └── pr-worksheet.md             ← worksheet for the practice assignment
├── examples/
│   └── pr_flow.sh                  ← completed reference implementation
├── tests/
│   └── run_tests.sh                ← automated checks (11 checks)
├── expected-output/
│   ├── sample-run.txt              ← a real captured run
│   └── FIELDS.md                   ← the landmarks every run must show
├── requirements/
│   └── README.md                   ← dependency statement (just Git)
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished flow first
bash examples/pr_flow.sh

## 2. Your task: complete the five exercises in the starter, then run it
bash starter/pr_flow.sh

## 3. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/pr_flow.sh — runs the reference flow in a throwaway repo: creates a feature branch, commits a focused change, prints git diff main..feature and git log main..feature (the pull request), adds an "addressed feedback" commit, merges with git merge --no-ff to create a merge commit, and then contrasts a git merge --squash on a second branch. The repo is deleted on exit.
  • bash starter/pr_flow.sh — the same flow with five numbered exercises for you to complete. Each : "FILL-IN ..." line names the exact command to substitute (git switch -c feature, git add/git commit, git diff/git log, git merge --no-ff). Edit the file, replace every FILL-IN line, then run it.
  • bash tests/run_tests.sh — checks that the starter is valid bash and still has its five exercises, then runs the reference flow in an inspectable repo and verifies the real Git state: the feature branch exists with the focused change and the review follow-up commit, a two-parent merge commit was created by --no-ff, and the squash landed a second branch as a single commit on main.

Expected output

See expected-output/sample-run.txt — a real captured run. Commit hashes and the temp-directory path differ on every run; expected-output/FIELDS.md lists the landmarks that must appear regardless. The key moments:

== Step 5: merge the PR with a merge commit (--no-ff) ==
History after the merge (note the two-parent merge commit):
*   8140fdc Merge branch 'feature'
|\
| * 123e011 Address review: clarify wording
| * ec70f7e Add greeting line to notes
|/
* 188b3b9 Initial notes

The two-parent merge commit is exactly what a pull-request merge produces; the squash step later shows the contrasting single-commit history.

Validation steps

  1. Run bash examples/pr_flow.sh — it must exit without errors and print both the merge-commit graph (Step 5) and the squashed commit (Step 6).
  2. Complete starter/pr_flow.sh, replacing every FILL-IN line, and run it — it must reach the final == Done ... == line.
  3. Confirm the merge graph shows a two-parent merge commit and the squash shows a single commit on main.
  4. Run the tests (next section) — all checks must pass.

Tests

bash tests/run_tests.sh

Expected final line: 11 checks, 0 failure(s). The command exits 0 on success and non-zero on any failure, so it can run in CI. It uses no network.

Cleanup

Nothing to clean up: every script works inside a temporary directory that is deleted on exit, and nothing is written into this repository or your home directory. To reset your edits to the starter, restore it from git: git checkout -- starter/pr_flow.sh.

Troubleshooting

See troubleshooting.md for the full list (Git not installed, missing identity, git switch on older Git, fast-forward vs merge commit, sed -i portability, graph glyphs).

Security notes

See security.md. Short version: the scripts run no network calls, need no elevated privileges, work only in a temporary directory, and set the commit identity locally so your global Git config is never touched.

Extension exercises

  1. Before merging, run git log main..feature --stat to see the per-file line counts a reviewer scans to judge how big a change is.
  2. Deliberately create a merge conflict: change the same line on main that your feature branch changed, commit it, then merge — resolve the conflict, git add the file, and complete the merge. This mirrors a hosting platform reporting "this branch has conflicts."
  3. Try the rebase and merge strategy: git switch feature, then git rebase main, then a fast-forward merge into main. Compare the resulting linear history with the merge-commit and squash graphs.
  • Previous day: Day 32 — Remotes and GitHub (labs/sections/computing-foundations/day-032-remotes-and-github/).
  • Next day: Day 34 — Undoing Things: Reset, Revert, and Reflog (labs/sections/computing-foundations/day-034-undoing-things-reset-revert-and-reflog/, to be written).

Expected output

FIELDS.md

# Required output landmarks (all platforms)

`sample-run.txt` in this directory is a real captured run of
`examples/pr_flow.sh` (macOS, bash, 2026-07-12). **Commit hashes and the
temporary directory path change on every run** — that is normal, because Git
computes each commit hash from its content, author, and timestamp. What must
appear on every platform, in this order, is:

1. `Working in throwaway repo: <a temp path>`
2. `== main starts here ==` followed by an `Initial notes` commit.
3. `== Step 1: create a feature branch ==`
4. `== Step 3: the PR ...` showing a `git diff main..feature` that adds the
   line about a pull request being a proposal to merge a branch, then a
   `git log main..feature` listing the `Add greeting line to notes` commit.
5. `== Step 4: ...` where the PR now lists **two** commits: the original plus
   `Address review: clarify wording`.
6. `== Step 5: merge the PR with a merge commit (--no-ff) ==` whose
   `git log --graph --oneline` shows a two-parent **merge commit**
   (`Merge branch 'feature'`) with the `|\` / `|/` branch lines.
7. `== Step 6: contrast the SQUASH strategy ==` where a second branch with two
   `wip` commits lands on `main` as a **single** `Add feature2 note (squashed)`
   commit — the two `wip` commits do **not** appear in `main`'s history.

## Platform differences

- **macOS vs Linux:** identical output. The script uses only portable Git
  commands; the one `sed -i` in-place edit is written with a `.bak` backup so
  it works on both the BSD `sed` (macOS) and GNU `sed` (Linux).
- **Older Git (< 2.23):** `git switch` is unavailable; the script falls back to
  `git checkout -b` / `git checkout` automatically, so the output is the same.
- **Git graph glyphs:** some terminals render the `git log --graph` connector
  lines with slightly different spacing; the two-parent merge structure is the
  invariant, not the exact characters.

sample-run.txt

Working in throwaway repo: /var/folders/7j/4qzljp553ndfjm_y6zbygsz00000gn/T//pr-flow.7iGRFH

== main starts here ==
6db5482 Initial notes

== Step 1: create a feature branch ==
Committed the focused change on 'feature'.

== Step 3: the PR — what you are proposing to merge into main ==
--- git diff main..feature (the change) ---
diff --git a/notes.md b/notes.md
index 2411724..fb868fd 100644
--- a/notes.md
+++ b/notes.md
@@ -1,3 +1,5 @@
 # Course Notes
 
 ## Day 33: Pull Requests
+
+A pull request is a proposal to merge a branch, reviewed before it lands.

--- git log main..feature (commits the PR would show) ---
0364097 Add greeting line to notes

== Step 4: reviewer asked to clarify wording — push a follow-up commit ==
The PR now contains two commits (original + addressed feedback):
0b7a18c Address review: clarify wording
0364097 Add greeting line to notes

== Step 5: merge the PR with a merge commit (--no-ff) ==
History after the merge (note the two-parent merge commit):
*   9ec1cf9 Merge branch 'feature'
|\  
| * 0b7a18c Address review: clarify wording
| * 0364097 Add greeting line to notes
|/  
* 6db5482 Initial notes

== Step 6: contrast the SQUASH strategy on a second branch ==
Updating 9ec1cf9..fcf269e
Fast-forward
Squash commit -- not updating HEAD
 notes.md | 4 ++++
 1 file changed, 4 insertions(+)
feature2 had two 'wip' commits; squash landed them as ONE commit on main:
* fdd66d3 Add feature2 note (squashed)
*   9ec1cf9 Merge branch 'feature'
|\  
| * 0b7a18c Address review: clarify wording
| * 0364097 Add greeting line to notes
|/  
* 6db5482 Initial notes

== Done. The merge commit and the squashed commit show the two strategies side by side. ==
Throwaway repo /var/folders/7j/4qzljp553ndfjm_y6zbygsz00000gn/T//pr-flow.7iGRFH will be removed on exit.

Source files

examples/pr_flow.sh (4659 bytes)
#!/usr/bin/env bash
# Day 033 lab — completed reference implementation.
#
# Simulates the pull-request-and-review workflow entirely LOCALLY, with no
# GitHub account and no network. A pull request is just review and merge
# gates wrapped around Git branches and merges, so we can rehearse the whole
# logic with plain Git:
#
#   1. Create a throwaway repo with a LOCAL identity (never touches your global config).
#   2. Make a focused change on a `feature` branch and commit it.
#   3. Produce the "pull request": `git diff main..feature` and `git log main..feature`.
#   4. Simulate a round of review by adding an "addressed feedback" commit.
#   5. Merge with --no-ff to create a merge commit, like a PR merge, and show the graph.
#   6. Contrast a squash-merge on a second branch.
#
# Everything happens in a temporary directory that is removed on exit.
set -euo pipefail

# --- Set up an isolated throwaway repository ---------------------------------
# By default the throwaway repo is deleted on exit. The test harness sets
# PR_FLOW_KEEP=1 to inspect the final repo state; it then removes the dir itself.
work="$(mktemp -d "${TMPDIR:-/tmp}/pr-flow.XXXXXX")"
cleanup() { rm -rf "${work}"; }
if [ -z "${PR_FLOW_KEEP:-}" ]; then
  trap cleanup EXIT
fi

echo "Working in throwaway repo: ${work}"
git init -q -b main "${work}" 2>/dev/null || { git init -q "${work}"; git -C "${work}" symbolic-ref HEAD refs/heads/main; }
cd "${work}"

# A LOCAL identity, scoped to this repo only — your global Git config is untouched.
git config user.email "learner@example.com"
git config user.name "Course Learner"

# --- Seed main with an initial commit ----------------------------------------
cat > notes.md <<'EOF'
# Course Notes

## Day 33: Pull Requests
EOF
git add notes.md
git commit -q -m "Initial notes"
echo
echo "== main starts here =="
git log --oneline

# --- Step 1: branch off main, like starting a pull request -------------------
echo
echo "== Step 1: create a feature branch =="
git switch -c feature 2>/dev/null || git checkout -q -b feature

# --- Step 2: make ONE focused change and commit it ---------------------------
cat >> notes.md <<'EOF'

A pull request is a proposal to merge a branch, reviewed before it lands.
EOF
git add notes.md
git commit -q -m "Add greeting line to notes"
echo "Committed the focused change on 'feature'."

# --- Step 3: produce the "pull request" --------------------------------------
echo
echo "== Step 3: the PR — what you are proposing to merge into main =="
echo "--- git diff main..feature (the change) ---"
git --no-pager diff main..feature
echo
echo "--- git log main..feature (commits the PR would show) ---"
git --no-pager log main..feature --oneline

# --- Step 4: simulate review by addressing feedback --------------------------
echo
echo "== Step 4: reviewer asked to clarify wording — push a follow-up commit =="
# Reviewer comment: "spell out that review happens before the merge, plainly."
sed -i.bak 's/reviewed before it lands\./reviewed by teammates before it lands in main./' notes.md
rm -f notes.md.bak
git add notes.md
git commit -q -m "Address review: clarify wording"
echo "The PR now contains two commits (original + addressed feedback):"
git --no-pager log main..feature --oneline

# --- Step 5: merge with --no-ff, like a PR merge, and show the graph ---------
echo
echo "== Step 5: merge the PR with a merge commit (--no-ff) =="
git switch main 2>/dev/null || git checkout -q main
git merge --no-ff -q -m "Merge branch 'feature'" feature
echo "History after the merge (note the two-parent merge commit):"
git --no-pager log --graph --oneline

# --- Step 6: contrast a squash-merge on a second branch ----------------------
echo
echo "== Step 6: contrast the SQUASH strategy on a second branch =="
git switch -c feature2 2>/dev/null || git checkout -q -b feature2
cat >> notes.md <<'EOF'

Squash merge collapses a branch's commits into a single commit on main.
EOF
git add notes.md
git commit -q -m "wip: draft squash note"
# A messy second commit, the kind squash is meant to tidy away.
printf '\nStill drafting the squash example.\n' >> notes.md
git add notes.md
git commit -q -m "wip: fix typo"

git switch main 2>/dev/null || git checkout -q main
git merge --squash feature2
git commit -q -m "Add feature2 note (squashed)"
echo "feature2 had two 'wip' commits; squash landed them as ONE commit on main:"
git --no-pager log --graph --oneline

echo
echo "== Done. The merge commit and the squashed commit show the two strategies side by side. =="
if [ -n "${PR_FLOW_KEEP:-}" ]; then
  echo "PR_FLOW_REPO=${work}"
else
  echo "Throwaway repo ${work} will be removed on exit."
fi
metadata.yml (600 bytes)
lesson_id: D033
day: 33
kind: command-line-inspection
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-033-pull-requests-and-code-review
run_commands:
  - bash examples/pr_flow.sh
  - bash starter/pr_flow.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/pr_flow.sh  # optional: reset your edits (repos are temp dirs, auto-removed)'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (bash, system Git), bash tests/run_tests.sh → 11 checks, 0 failure(s).'
requirements/README.md (1186 bytes)
# Dependencies — Day 033 lab

**One dependency: Git.** This lab simulates the pull-request workflow with
local branches only, so it needs nothing beyond a shell and Git:

- `bash` ≥ 3.2 (preinstalled on macOS and every mainstream Linux distribution)
- `git` ≥ 2.0 (2.23 or newer preferred, so `git switch` is available; the
  scripts fall back to `git checkout` on older versions)
- Standard OS utilities: `mktemp`, `sed`, `printf` — all part of the base
  system on macOS and Linux.

There is no `requirements.txt`/`package.json` here; the scripts install
nothing.

## About a *real* pull request

A genuine pull request lives on a hosting platform (such as GitHub or GitLab)
and therefore needs, in real life:

- a free account on the platform, and
- network access to push a branch and open the request in the browser or with
  the platform's command-line tool.

This lab deliberately needs **neither**. Because a pull request is review and
merge gates wrapped around Git branches and merges, you can rehearse the entire
logic offline with the tools above. The lesson covers the account-and-network
side conceptually; you will open a real pull request in the Week 5 project.
starter/pr_flow.sh (3262 bytes)
#!/usr/bin/env bash
# Day 033 lab — YOUR working file: simulate a pull request locally.
#
# The scaffolding below creates a throwaway repo with a LOCAL identity and an
# initial commit on main. Your job is to complete the FIVE numbered exercises,
# each of which names the exact command to run. Replace every line that reads
#   : "FILL-IN ..."
# with the real command shown in the comment above it, then run:
#   bash starter/pr_flow.sh
#
# The completed reference is in examples/pr_flow.sh — try this yourself first.
set -euo pipefail

# --- Scaffolding (already done for you) --------------------------------------
work="$(mktemp -d "${TMPDIR:-/tmp}/pr-flow-starter.XXXXXX")"
trap 'rm -rf "${work}"' EXIT
echo "Working in throwaway repo: ${work}"
git init -q -b main "${work}" 2>/dev/null || { git init -q "${work}"; git -C "${work}" symbolic-ref HEAD refs/heads/main; }
cd "${work}"
git config user.email "learner@example.com"
git config user.name "Course Learner"
printf '# Course Notes\n\n## Day 33: Pull Requests\n' > notes.md
git add notes.md
git commit -q -m "Initial notes"
echo "main starts at:"; git --no-pager log --oneline

# --- Exercise 1: create a feature branch (like starting a pull request) -------
# Command:  git switch -c feature      (older Git:  git checkout -b feature)
: "FILL-IN exercise 1: replace this line with the branch-creating command"

# --- Make a focused change (already written for you) -------------------------
printf '\nA pull request is a proposal to merge a branch, reviewed before it lands.\n' >> notes.md

# --- Exercise 2: stage and commit the focused change -------------------------
# Commands:  git add notes.md   then   git commit -m "Add greeting line to notes"
: "FILL-IN exercise 2a: replace with 'git add notes.md'"
: "FILL-IN exercise 2b: replace with the commit command and message above"

# --- Exercise 3: produce the PR — the diff and the commit log ----------------
# Commands:  git --no-pager diff main..feature   then   git --no-pager log main..feature --oneline
echo; echo "== The pull request: what you propose to merge into main =="
: "FILL-IN exercise 3a: replace with the diff command above"
: "FILL-IN exercise 3b: replace with the log command above"

# --- Simulate review, then Exercise 4: commit the addressed feedback ---------
sed -i.bak 's/reviewed before it lands\./reviewed by teammates before it lands in main./' notes.md
rm -f notes.md.bak
# Commands:  git add notes.md   then   git commit -m "Address review: clarify wording"
: "FILL-IN exercise 4a: replace with 'git add notes.md'"
: "FILL-IN exercise 4b: replace with the commit command and message above"

# --- Exercise 5: merge the PR with a MERGE COMMIT (--no-ff) -------------------
# Commands:  git switch main    (older: git checkout main)
#            git merge --no-ff -m "Merge branch 'feature'" feature
#            git --no-pager log --graph --oneline
echo; echo "== Merge the pull request (merge-commit strategy) =="
: "FILL-IN exercise 5a: replace with the command to switch back to main"
: "FILL-IN exercise 5b: replace with the --no-ff merge command above"
: "FILL-IN exercise 5c: replace with the graph log command above"

echo; echo "== Done — you have simulated a pull request from branch to merge. =="
starter/pr-worksheet.md (1960 bytes)
# Pull-request worksheet — Day 033

Fill this in for the change you made in the hands-on exercise. It is the same
thing you would type into the description box of a real pull request. Keep it
when you are done — a clear description you can point to is worth more than any
advice about writing one.

---

## 1. Pull-request description

Write it exactly as you would on a hosting platform.

**Title** (one line, imperative, under ~60 characters):

> _e.g. "Add greeting line to course notes"_

**Description** (what the change does and, crucially, *why*):

> _2–4 sentences. What did you change? Why was it needed? What problem does it
> solve or what does it add?_

**How I verified it** (tests, manual checks, the diff you read):

> _e.g. "Ran the script; git diff main..feature shows only the intended line;
> the merge produced a clean merge commit with no conflicts."_

---

## 2. What the diff shows

Paste the output of `git diff main..feature` (or list the files and the exact
lines it touched):

```text
_paste git diff main..feature here_
```

Files touched: _______________________________________________

Lines added / removed: ________________________________________

---

## 3. Merge strategy

Which strategy did you use to bring `feature` into `main`?

- [ ] Merge commit (`git merge --no-ff`)
- [ ] Squash and merge (`git merge --squash`)
- [ ] Rebase and merge

**Why this strategy fit this change** (2–3 sentences):

> _e.g. "I used a merge commit so the review-and-revise history — the original
> commit plus the 'addressed feedback' commit — stays visible in main." OR
> "I squashed because the two 'wip' commits were noise and I wanted one tidy,
> revertible commit on main."_

---

## 4. Reflection (optional but recommended)

In one or two sentences: what would a reviewer most likely comment on if this
were a real pull request, and how would you respond?

> _______________________________________________________________
tests/run_tests.sh (4237 bytes)
#!/usr/bin/env bash
# Tests for the Day 033 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Runs the completed reference flow in an inspectable throwaway repo and
# verifies the real Git state a pull request produces:
#   - a `feature` branch exists with the focused change + an "addressed
#     feedback" follow-up commit (the diff/commits a PR would show),
#   - the merge-commit strategy (--no-ff) produced a two-parent merge commit,
#   - the squash strategy landed a second branch as a SINGLE commit on main
#     (its intermediate "wip" commits absent from main's history).
# No network is used. Exits 0 on success, non-zero on any failure.
set -u

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
}

# 1. The starter must be valid bash and still contain the five exercises.
echo "Checking starter/pr_flow.sh ..."
if bash -n "${lab_dir}/starter/pr_flow.sh" 2>/dev/null; then
  check "starter is syntactically valid bash" "yes"
else
  check "starter is syntactically valid bash" "no"
fi
exercise_count="$(grep -c 'FILL-IN exercise' "${lab_dir}/starter/pr_flow.sh")"
[ "${exercise_count}" -ge 5 ] && check "starter has at least 5 exercises" "yes" || check "starter has at least 5 exercises" "no"

# 2. Run the reference flow, keeping the repo so we can inspect the end state.
echo "Running examples/pr_flow.sh (kept for inspection) ..."
output=""
if output="$(PR_FLOW_KEEP=1 bash "${lab_dir}/examples/pr_flow.sh" 2>&1)"; then
  check "reference flow runs and exits 0" "yes"
else
  check "reference flow runs and exits 0" "no"
  echo "${output}" | sed 's/^/    /'
  echo; echo "${checks} checks, ${failures} failure(s)."
  exit 1
fi

repo="$(printf '%s\n' "${output}" | sed -n 's/^PR_FLOW_REPO=//p' | tail -n 1)"
if [ -n "${repo}" ] && [ -d "${repo}/.git" ]; then
  check "inspectable throwaway repo was created" "yes"
else
  check "inspectable throwaway repo was created" "no"
  echo; echo "${checks} checks, ${failures} failure(s)."
  exit 1
fi
# Always clean up the kept repo ourselves.
trap 'rm -rf "${repo}"' EXIT

g() { git -C "${repo}" "$@"; }

# 3. The feature branch exists (the PR's source branch).
g show-ref --verify --quiet refs/heads/feature && check "feature branch exists" "yes" || check "feature branch exists" "no"

# 4. The PR's commits are on feature: the focused change AND the review follow-up.
feat_log="$(g log feature --oneline 2>/dev/null)"
printf '%s\n' "${feat_log}" | grep -q "Add greeting line to notes" && check "feature has the focused change commit" "yes" || check "feature has the focused change commit" "no"
printf '%s\n' "${feat_log}" | grep -q "Address review" && check "feature has the addressed-feedback commit" "yes" || check "feature has the addressed-feedback commit" "no"

# 5. The diff a PR would show is non-empty between main's base and feature's tip
#    (feature's notes.md contains the proposed line).
g show "feature:notes.md" 2>/dev/null | grep -q "proposal to merge a branch" && check "git diff main..feature would show the proposed change" "yes" || check "git diff main..feature would show the proposed change" "no"

# 6. The no-ff merge produced a two-parent merge commit on main.
merge_count="$(g rev-list --merges --count main 2>/dev/null)"
[ "${merge_count:-0}" -ge 1 ] && check "merge-commit strategy created a merge commit (--no-ff)" "yes" || check "merge-commit strategy created a merge commit (--no-ff)" "no"

# 7. The squash strategy landed as a SINGLE commit on main; the wip commits are absent.
main_log="$(g log main --oneline 2>/dev/null)"
printf '%s\n' "${main_log}" | grep -q "squashed" && check "squash produced a single 'squashed' commit on main" "yes" || check "squash produced a single 'squashed' commit on main" "no"
wip_on_main="$(printf '%s\n' "${main_log}" | grep -c 'wip')"
[ "${wip_on_main}" -eq 0 ] && check "squash collapsed the branch (no wip commits on main)" "yes" || check "squash collapsed the branch (no wip commits on main)" "no"

echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 033 lab

git: command not found

Git is not installed. On macOS, xcode-select --install provides it; on Debian/Ubuntu, sudo apt install git; on Fedora, sudo dnf install git. Verify with git --version.

fatal: not a git repository

You tried to run a Git command outside the throwaway repo. Run examples/pr_flow.sh (or starter/pr_flow.sh), which creates and enters the temp repo for you. If you are experimenting by hand, cd into the temp path the script printed at the top of its output.

Please tell me who you are / empty ident name

Git has no commit identity configured. The scripts set one locally inside the throwaway repo, so this should not happen when you run them. If you are working by hand, set it in that repo only:

git config user.email "you@example.com"
git config user.name "Your Name"

Using git config without --global scopes it to the current repository and leaves your global settings alone.

git: 'switch' is not a git command

Your Git predates version 2.23 (August 2019), when switch was added. The scripts fall back to git checkout automatically. If you are typing commands yourself, use git checkout -b feature to create a branch and git checkout main to move between them — the effect is identical.

The merge did not create a merge commit

You merged without --no-ff while main had not moved, so Git "fast-forwarded" and left no merge point. Redo it with git merge --no-ff feature. A pull-request merge always records a merge commit; --no-ff (no fast-forward) is how you force one locally.

sed: -i may not be used with stdin or an unexpected notes.md.bak file

The scripts use sed -i.bak (with a backup suffix) specifically so the same command works on both macOS (BSD sed) and Linux (GNU sed), then delete the .bak file. If you adapted the command and dropped the suffix, restore it: sed -i.bak 's/old/new/' notes.md && rm -f notes.md.bak.

Git shows the graph lines differently than the sample

Terminal width and font can shift where the |\ and |/ connector lines sit in git log --graph. The important thing is the two-parent merge commit, not the exact spacing. The tests check the merge structure, not the glyphs.

Permission denied when running a script

Run it through bash explicitly: bash examples/pr_flow.sh. If you prefer ./examples/pr_flow.sh, first chmod +x examples/pr_flow.sh.

Security notes

Security notes — Day 033 lab

  • What the scripts do: create a throwaway Git repository in a temporary directory (mktemp -d), make a few commits, run diffs and merges, and print the results. They make no network connections and need no elevated privileges.
  • Temporary directory only: all work happens inside a fresh temp directory, which the reference and starter scripts delete on exit (the test harness sets PR_FLOW_KEEP=1 to inspect the repo, then removes it itself). Nothing is written into your project, your home directory, or any existing repository.
  • Your global Git config is untouched: the scripts set the commit identity (user.name, user.email) locally, inside the throwaway repo only, using a placeholder address (learner@example.com). Your own git config --global settings are never read or changed.
  • No credentials, no remotes: the lab never authenticates, never adds a remote, and never pushes. There is nothing to leak. When you later open a real pull request, treat the diff and every comment as permanent and potentially public — never paste secrets, API keys, or personal data into them.
  • Reading before running: both scripts are short and commented — read them first. Running unread shell scripts is a common way developers get compromised; every lab script in this course is small enough to read and understand before executing.