Chapter 5 — GitHub Collaboration
Beyond `git push`/`pull`, GitHub adds people-shaped features: forks, pull requests, code review, issue tracking, and branch protection. This chapter covers the social side of Git.
What you'll learn
- 1Fork a repo, open a Pull Request (PR), and respond to review feedback.
- 2Use issues, labels and milestones to coordinate work.
- 3Set up branch protection rules to prevent direct `main` pushes.
- 4Add `CODEOWNERS` so the right reviewers are auto-requested.
Core Concepts
1) Fork → PR flow
upstream/main ──┐
│ fork
your-fork/main ──┴── branch ──── commit ──── push ──── open PR ──── review ──── mergeStandard external-contributor flow.
2) PR anatomy
| Section | Purpose |
|---|---|
| Title | One-line summary |
| Body | What changed and why |
| Reviewers | Specific people to ping |
| Labels | `bug`, `feature`, `docs`… |
| Linked issues | `Closes #42` auto-closes the issue on merge |
3) Reviewing code
Inline comments live on a specific line. The whole PR can also be:
- **Approved** ✅
- **Request changes** 🛑
- **Comment** (no judgment)
4) Branch protection
In the repo's Settings → Branches:
- Require pull request before merge
- Require N approvals
- Require status checks (CI) to pass
- Block force-push and deletion
5) `CODEOWNERS`
A file `.github/CODEOWNERS` mapping path patterns to GitHub handles:
*.ts @frontend-team
docs/ @docs-team
/.github/ @adminGitHub auto-requests review from these users when a PR touches their paths.
Examples
Example 1 — `ex01_fork.sh`: simulate fork + PR (locally)
**Output**
=== fork simulation ===
upstream → fork → branch → commit → push (to fork)Key: open PR on GitHub UI; the local side just commits + pushes.
Example 2 — `ex02_pr.sh`: prepare a PR-ready branch
**Output**
=== PR simulation ===
branch created
commits clean
ready for `gh pr create`Key: keep PR scope small (one logical change per PR).
Example 3 — `ex03_issue.sh`: link commits to issues
**Output**
=== Issue simulation ===
commit message: "fix: handle null input (closes #42)"
on merge → #42 auto-closesKey: commit/PR message magic words: `closes`, `fixes`, `resolves` + `#N`.
Example 4 — `ex04_codeowners.sh`: writing a CODEOWNERS file
**Output**
=== CODEOWNERS ===
file written
GitHub will auto-request the matching reviewer on PRs.Key: protected branches + CODEOWNERS = enforced review for sensitive paths.
Common mistakes
- **Pushing directly to `main`** — bypass review, break others. Enable branch protection.
- **Huge PRs** — 1000-line diffs scare reviewers; split work into reviewable pieces.
- **No PR description** — reviewers waste time figuring out intent. Write *why*, not *what*.
- **Ignoring CI red marks** — fix the failure, don't merge around it.
- **Force-pushing to a shared branch** — rewrites history, breaks teammates' clones.
Recap
- Fork → branch → commit → push → PR → review → merge is the standard flow.
- A PR is a *proposed* change; explain it well.
- Branch protection rules turn good practice into enforcement.
- `CODEOWNERS` automates reviewer assignment.
Try it
cd src
chmod +x ex0*.sh
./ex01_fork.sh
./ex02_pr.sh
./ex03_issue.sh
./ex04_codeowners.sh💻 Examples
Runnable examples — see the output yourself.
#!/usr/bin/env bash
set -euo pipefail
WORKDIR=$(mktemp -d)
echo "Working directory: $WORKDIR"
# upstream role bare repository
git init --bare -b main "$WORKDIR/upstream.git" -q
# upstream initial
git clone "$WORKDIR/upstream.git" "$WORKDIR/upstream_local" -q
cd "$WORKDIR/upstream_local"
git config user.name "maintainer" && git config user.email "main@example.com"
git config commit.gpgsign false
echo "# using " > README.md
echo "feature A" > feature_a.py
git add . && git commit -q -m "init: initial commit"
git push origin main -q
echo "upstream initial done"
# contributor fork (= upstream )
git clone "$WORKDIR/upstream.git" "$WORKDIR/my_fork" -q
cd "$WORKDIR/my_fork"
git config user.name "contributor" && git config user.email "contributor@example.com"
git remote add upstream "$WORKDIR/upstream.git"
echo ""
echo "=== fork repository of remote list ==="
git remote -v
# feature branch create (after) work
git switch -c feature/improve-docs
echo "## " >> README.md
echo "1. " >> README.md
git add . && git commit -q -m "docs: add usage section (#1)"
echo ""
echo "=== contributor branch log ==="
git log --oneline --graph --all
echo ""
echo "=== (GitHub UI at PR create) ==="
echo "git push origin feature/improve-docs"
echo "→ GitHub at upstream:main ← origin:feature/improve-docs using PR create"
rm -rf "$WORKDIR"
=== fork simulation ===
upstream → fork → branch → commit → push (to fork)#!/usr/bin/env bash
set -euo pipefail
WORKDIR=$(mktemp -d)
git init --bare -b main "$WORKDIR/origin.git" -q
git clone "$WORKDIR/origin.git" "$WORKDIR/repo" -q
cd "$WORKDIR/repo"
git config user.name "developer" && git config user.email "dev@example.com"
git config commit.gpgsign false
echo "# " > README.md
echo "def main(): pass" > app.py
git add . && git commit -q -m "init: project setup"
git push origin main -q
# PR : feature branch → push → merge
git switch -c feature/login
cat >> app.py << 'EOF'
def login(user, password):
# log
return user == "admin"
EOF
git add . && git commit -q -m "feat: implement login function
Closes #3"
git push origin feature/login -q
echo "=== PR branch status ==="
git log --oneline --graph --all
# (after) main using merge (--no-ff using merge commit create)
git switch main
git merge --no-ff feature/login -m "merge: PR#1 — feat: implement login"
git push origin main -q
echo ""
echo "=== PR merge (after) log ==="
git log --oneline --graph
# merge branch cleanup
git branch -d feature/login
git push origin --delete feature/login -q
echo ""
echo "feature/login branch done"
rm -rf "$WORKDIR"
=== PR simulation ===
branch created
commits clean
ready for `gh pr create`#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "developer" && git config user.email "dev@example.com"
git config commit.gpgsign false
echo "def app(): pass" > app.py
git add . && git commit -q -m "init"
echo "=== Issue commit message ==="
echo ""
echo "Issue #7: log "
echo ""
# fix/issue-7 branch at edit
git switch -c fix/issue-7
cat > app.py << 'EOF'
def login(user, password):
if not password:
raise ValueError("Password cannot be empty")
return True
EOF
git add .
git commit -m "fix: prevent empty password login
- input ValueError
- add
Closes #7"
echo "=== commit log ==="
git log --oneline
echo ""
echo "=== commit (before) message ==="
git log -1 --format="%B"
echo ""
echo "=== GitHub UI at of auto ==="
echo "PR commit at 'Closes #7' "
echo "PR merge Issue #7 auto using ."
rm -rf "$REPO"
=== Issue simulation ===
commit message: "fix: handle null input (closes #42)"
on merge → #42 auto-closes#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "admin" && git config user.email "admin@example.com"
git config commit.gpgsign false
mkdir -p .github/workflows docs src/backend src/frontend
cat > .github/CODEOWNERS << 'EOF'
# CODEOWNERS — PR create auto using .
# : file
* @admin-user
# doc
docs/ @tech-writer @admin-user
#
src/backend/ @backend-team
#
src/frontend/ @frontend-team
# CI/CD using — DevOps
.github/workflows/ @devops-team
EOF
echo "" > src/backend/server.py
echo "" > src/frontend/index.html
echo "doc" > docs/guide.md
echo "Hello" > .github/workflows/ci.yml
git add .
git commit -m "chore: add CODEOWNERS and project skeleton"
echo "=== CODEOWNERS file ==="
cat .github/CODEOWNERS
echo ""
echo "=== repository file ==="
git ls-files
rm -rf "$REPO"
=== CODEOWNERS ===
file written
GitHub will auto-request the matching reviewer on PRs.📝 Exercises
Try them yourself first, then open the solution to compare.
Problem 1 (hw01.sh)
Goal: Write a `.github/CODEOWNERS` file that assigns: - `*.md` files to `@docs-team` - `/.github/` directory to `@admin` - All other files to `@team`
- Filename: hw01.sh
▶Toggle solution
#!/usr/bin/env bash
set -euo pipefail
WORKDIR=$(mktemp -d)
git init --bare -b main "$WORKDIR/upstream.git" -q
# upstream initial
git clone "$WORKDIR/upstream.git" "$WORKDIR/upstream_local" -q
git -C "$WORKDIR/upstream_local" config user.name "maintainer"
git -C "$WORKDIR/upstream_local" config user.email "main@example.com"
git -C "$WORKDIR/upstream_local" config commit.gpgsign false
echo "# using " > "$WORKDIR/upstream_local/README.md"
git -C "$WORKDIR/upstream_local" add .
git -C "$WORKDIR/upstream_local" commit -q -m "init"
git -C "$WORKDIR/upstream_local" push origin main -q
# fork
git clone "$WORKDIR/upstream.git" "$WORKDIR/my_fork" -q
git -C "$WORKDIR/my_fork" config user.name "contributor"
git -C "$WORKDIR/my_fork" config user.email "contrib@example.com"
git -C "$WORKDIR/my_fork" config commit.gpgsign false
git -C "$WORKDIR/my_fork" remote add upstream "$WORKDIR/upstream.git"
cd "$WORKDIR/my_fork"
git switch -c feature/new-page
echo "# " > new_page.md
git add . && git commit -q -m "docs: add new page"
# upstream at push (fork simulation)
git push upstream feature/new-page -q
# upstream at PR merge
cd "$WORKDIR/upstream_local"
git fetch origin -q
git merge --no-ff origin/feature/new-page -m "merge: PR — add new page"
echo "=== final ==="
git log --oneline --graph --all
rm -rf "$WORKDIR"
Problem 2 (hw02.sh)
Goal: Write a sample PR description (markdown) that includes: - Summary (2-3 bullets) - Linked issue (e.g., `Closes #42`) - Test plan (3 checkboxes)
- Filename: hw02.sh
▶Toggle solution
#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "admin" && git config user.email "admin@example.com"
git config commit.gpgsign false
mkdir -p .github src docs
cat > .github/CODEOWNERS << 'EOF'
* @admin
src/ @backend
docs/ @writer
EOF
echo "def api(): pass" > src/api.py
echo "# API doc" > docs/api.md
git add . && git commit -q -m "chore: initial project structure"
# Issue #12 edit
git switch -c fix/issue-12
echo "def api(version=1): pass" > src/api.py
git add .
git commit -m "fix: add version parameter to api
Closes #12"
echo "=== commit message ==="
git log -1 --format="%B"
echo "=== CODEOWNERS ==="
cat .github/CODEOWNERS
rm -rf "$REPO"
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗