← Back to Git series
🔀
Git & GitHub Deep Dive
fork · PR · Issue · Review · branch protection

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.

forkPRissuereview
Duration
1-2 hours
Level
📊 Beginner–Intermediate
Prerequisite
🎯 Chapter 4
Outcome
PR-and-review collaboration flow on GitHub

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

text
upstream/main  ──┐
                 │  fork
your-fork/main ──┴── branch ──── commit ──── push ──── open PR ──── review ──── merge

Standard external-contributor flow.

2) PR anatomy

SectionPurpose
TitleOne-line summary
BodyWhat changed and why
ReviewersSpecific 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:

text
*.ts        @frontend-team
docs/       @docs-team
/.github/   @admin

GitHub auto-requests review from these users when a PR touches their paths.

Examples

Example 1 — `ex01_fork.sh`: simulate fork + PR (locally)

**Output**

text
=== 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**

text
=== 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**

text
=== Issue simulation ===
commit message: "fix: handle null input (closes #42)"
on merge → #42 auto-closes

Key: commit/PR message magic words: `closes`, `fixes`, `resolves` + `#N`.

Example 4 — `ex04_codeowners.sh`: writing a CODEOWNERS file

**Output**

text
=== CODEOWNERS ===
file written
GitHub will auto-request the matching reviewer on PRs.

Key: protected branches + CODEOWNERS = enforced review for sensitive paths.

Common mistakes

  1. **Pushing directly to `main`** — bypass review, break others. Enable branch protection.
  2. **Huge PRs** — 1000-line diffs scare reviewers; split work into reviewable pieces.
  3. **No PR description** — reviewers waste time figuring out intent. Write *why*, not *what*.
  4. **Ignoring CI red marks** — fix the failure, don't merge around it.
  5. **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

bash
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.

ex01_fork.shsimulate fork + PR (locally)
CODE
#!/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"
▶ Output
=== fork simulation ===
upstream → fork → branch → commit → push (to fork)
ex02_pr.shprepare a PR-ready branch
CODE
#!/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"
▶ Output
=== PR simulation ===
branch created
commits clean
ready for `gh pr create`
ex03_issue.shlink commits to issues
CODE
#!/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"
▶ Output
=== Issue simulation ===
commit message: "fix: handle null input (closes #42)"
on merge → #42 auto-closes
ex04_codeowners.shwriting a CODEOWNERS file
CODE
#!/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"
▶ Output
=== CODEOWNERS ===
file written
GitHub will auto-request the matching reviewer on PRs.

📝 Exercises

Try them yourself first, then open the solution to compare.

Exercise 1

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`

Requirements
  • Filename: hw01.sh
Toggle solution
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"
Exercise 2

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)

Requirements
  • Filename: hw02.sh
Toggle solution
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"
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗