Git & GitHub track

Lesson 05 · Lessons

GitHub Collaboration

Forks, pull requests, issues, reviews and CODEOWNERS

Lessons6 min readFree lesson

When contributing to open source projects or working on team projects Fork·PR(Pull Request)·Issue·Code Review are the core flows of collaboration. In this section, you will understand GitHub’s UI-centric features and Learn how to protect code quality with branch protection rules and CODEOWNERS settings.

Learning Objectives

  • The fork → branch → PR flow is explained and can be performed directly.
  • Issues can be used to plan work and connect with PR.
  • You can leave comments and reflect requested changes in Code Review.
  • The main branch can be protected from direct push with branch protection rules.
  • You can automatically designate a specific of-file/directory reviewer with the CODEOWNERS file.

Key concepts

1) Fork & PR flow

Original repository (upstream)
    ↓  fork
my repository (origin)
    ↓  clone
local operation
    ↓  git switch -c feature/xxx
↓ commit
    ↓  git push origin feature/xxx
Create a PR from the GitHub UI
    ↓  upstream main ← feature/xxx
Code Review → Approval → Merge

2) Issue creation and PR connection

Include the issue number (e.g. #42) in your PR description or commit message and it will be automatically linked.

# PR description example
Closes #42

# Example commit message
fix: resolve login error (#42)

Issue is automatically closed when merging PRs when using keywords Closes, Fixes, Resolves.

3) Code Review word collection

Abbreviations Meaning
LGTM Looks Good To Me (Approved)
nit Minor improvement suggestions
RFC Request for Comments
WIP Work In Progress
NB Nota Bene (notice)

4) Branch protection rules

GitHub repository → Settings → Branches → Add rule

Branch name pattern: main
✅ Require a pull request before merging
✅ Require approvals: 1
✅ Dismiss stale pull request approvals
✅ Require status checks to pass
✅ Restrict who can push to matching branches

5) CODEOWNERS

.github/CODEOWNERS file:

# Default owner of the entire repository
*   @username

# The docs directory belongs to the technical-writers team
docs/   @org/technical-writers

# Security-related files
security.md   @security-team

See as example

Example 1 — ex01_fork.sh: fork workflow simulation (using bare repository)

#!/usr/bin/env bash
#GitHub fork is a UI operation, so it simulates the flow with a local bare repository.
WORKDIR=$(mktemp -d)

git init --bare "$WORKDIR/upstream.git" -q
git init --bare "$WORKDIR/fork.git" -q       #my fork role

#upstream initialization
git clone "$WORKDIR/upstream.git" "$WORKDIR/upstream_local" -q
cd "$WORKDIR/upstream_local"
git config user.name "maintainer" && git config user.email "[email protected]"
echo "#project" > README.md
git add . && git commit -q -m "init"
git push origin main -q

#Synchronize with fork (fork in GitHub = upstream clone push)
git clone "$WORKDIR/upstream.git" "$WORKDIR/my_fork" -q
cd "$WORKDIR/my_fork"
git config user.name "contributor" && git config user.email "[email protected]"
git remote add upstream "$WORKDIR/upstream.git"

#Working on feature branches
git switch -c feature/add-docs
echo "## How to use" >> README.md
git add . && git commit -q -m "docs: add usage section"

echo "=== feature branch log ==="
git log --oneline --graph --all

rm -rf "$WORKDIR"

Execution result

=== feature branch log ===
* a1b2c3d (HEAD -> feature/add-docs) docs: add usage section
* b2c3d4e (origin/main, main) init

Key point: The open source flow is to work on the feature branch after forking and contribute upstream with PR.


Example 2 — ex02_pr.sh: PR simulation (replaced by local merge)

#!/usr/bin/env bash
WORKDIR=$(mktemp -d)
git init --bare "$WORKDIR/origin.git" -q

git clone "$WORKDIR/origin.git" "$WORKDIR/repo" -q
cd "$WORKDIR/repo"
git config user.name "developer" && git config user.email "[email protected]"
echo "main function" > main.py && git add . && git commit -q -m "init"
git push origin main -q

git switch -c feature/login
echo "def login(): pass" >> main.py
git add . && git commit -q -m "feat: add login function"
git push origin feature/login -q

echo "=== PR simulation: feature/login → main merge ==="
git switch main
git merge --no-ff feature/login -m "merge: PR#1 add login function"
git push origin main -q

echo ""
echo "=== Log after merge ==="
git log --oneline --graph

rm -rf "$WORKDIR"

Execution result

=== PR simulation: feature/login → main merge ===

=== Log after merge ===
*   c3d4e5f merge: PR#1 add login function
|\
| * b2c3d4e feat: add login function
|/
* a1b2c3d init

Bottom line: PR is a safety measure that merges branches after review rather than pushing them directly.


Example 3 — ex03_issue.sh : Concatenate issue number to commit message

#!/usr/bin/env bash
REPO=$(mktemp -d)
cd "$REPO"
git init -q
git config user.name "developer" && git config user.email "[email protected]"

echo "app code" > app.py && git add . && git commit -q -m "init"

#Issue #7: Login bug fix
git switch -c fix/issue-7
echo "# Bug fixed" >> app.py
git add .
git commit -m "fix: resolve login error

Closes #7"

echo "=== Check commit message ==="
git log --oneline -n 1
git show HEAD --format="%B" --no-patch

rm -rf "$REPO"

Execution result

=== Check commit message ===
a1b2c3d fix: resolve login error

fix: resolve login error

Closes #7

Key point: If you use Closes #N in the PR description, the issue will be automatically closed when merging.


Example 4 — ex04_codeowners.sh: Creating CODEOWNERS file

#!/usr/bin/env bash
REPO=$(mktemp -d)
cd "$REPO"
git init -q
git config user.name "manager" && git config user.email "[email protected]"

mkdir -p .github docs src

cat > .github/CODEOWNERS << 'EOF'
#Default owner: All files
*   @admin-user

#document directory
docs/   @tech-writer

#source code
src/    @backend-team

#CI/CD settings
.github/workflows/   @devops-team
EOF

echo "app code" > src/app.py
echo "document" > docs/guide.md

git add .
git commit -m "chore: add CODEOWNERS and project structure"

echo "=== CODEOWNERS contents ==="
cat .github/CODEOWNERS

rm -rf "$REPO"

Execution result

=== CODEOWNERS contents ===
# Default owner: All files
*   @admin-user
...

Key point: CODEOWNERS automatically requests the Team/Individual reviewer specified when creating a PR.


Seeing from a different perspective

GitHub features Metaphor
Issue to-do notes
PR Request approval from superior
Code Review Payment review
LGTM / Approve Payment stamp
Merge Official reflection
CODEOWNERS List of contact persons by department

Common Mistakes

  1. Push directly to main without PR — Without branch protection rules, team member review will be skipped.
  2. Creating a PR without an Issue — Reviewing is difficult because there is no context as to why this work is needed.
  3. Closes #N omitted in PR description — Issue is not automatically closed, making management cumbersome.
  4. Omit upstream synchronization in fork repository — Conflicts increase due to no upstream changes.
  5. Do not resolve all review comments and merge — unresolved feedback remains in the code.

Cleanup

  • fork → feature branch → PR → review → Merge is the standard flow of team collaboration.
  • By linking Issue and PR to Closes #N, task tracking is automated.
  • Branch protection rules prevent direct pushes to main and enforce a minimum number of reviewers.
  • Designate the By-file/directory person in charge as CODEOWNERS and receive automatic review requests.

Try it yourself

cd 05_GitHub_Collaboration/src
chmod +x *.sh
./ex01_fork.sh
./ex02_pr.sh
./ex03_issue.sh
./ex04_codeowners.sh

Application:

  1. Fork the actual GitHub repository and upload a PR for correcting typos.
  2. Create an issue template (.github/ISSUE_TEMPLATE/) to create a bug report form.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/ex01_fork.shShell
#!/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 initialization
git clone "$WORKDIR/upstream.git" "$WORKDIR/upstream_local" -q
cd "$WORKDIR/upstream_local"
git config user.name "maintainer" && git config user.email "[email protected]"
    git config commit.gpgsign false
echo "# Open source project" > README.md
echo "Function A" > feature_a.py
git add . && git commit -q -m "init: initial commit"
git push origin main -q
echo "upstream initialization complete"

#Contributor forks (= replicates upstream)
git clone "$WORKDIR/upstream.git" "$WORKDIR/my_fork" -q
cd "$WORKDIR/my_fork"
git config user.name "contributor" && git config user.email "[email protected]"
git remote add upstream "$WORKDIR/upstream.git"

echo ""
echo "=== remote list of fork repositories ==="
git remote -v

#Actions after creating a feature branch
git switch -c feature/improve-docs
echo "## How to use" >> README.md
echo "1. Installation" >> 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 creates a PR here) ==="
echo "git push origin feature/improve-docs"
echo "→ Create PR with upstream:main ← origin:feature/improve-docs on GitHub"

rm -rf "$WORKDIR"
src/ex02_pr.shShell
#!/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 "[email protected]"
    git config commit.gpgsign false

echo "# app" > README.md
echo "def main(): pass" > app.py
git add . && git commit -q -m "init: project setup"
git push origin main -q

#PR flow: Create feature branch → push → merge
git switch -c feature/login
cat >> app.py << 'EOF'

def login(user, password):
    #Login processing
    return user == "admin"
EOF
git add . && git commit -q -m "feat: implement login function

Closes #3"
git push origin feature/login -q

echo "=== PR target branch status ==="
git log --oneline --graph --all

#After review, merge into main (create merge commit with --no-ff )
git switch main
git merge --no-ff feature/login -m "merge: PR#1 — feat: implement login"
git push origin main -q

echo ""
echo "=== Log after PR merge ==="
git log --oneline --graph

#Clean up merged branches
git branch -d feature/login
git push origin --delete feature/login -q
echo ""
echo "Completed deletion of feature/login branch"

rm -rf "$WORKDIR"
src/ex03_issue.shShell
#!/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 "[email protected]"
    git config commit.gpgsign false

echo "def app(): pass" > app.py
git add . && git commit -q -m "init"

echo "=== Issue linked commit message example ==="
echo ""
echo "Issue #7: Bug allowing empty password when logging in"
echo ""

#Fix bug in branch fix/issue-7
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

- ValueError occurs when entering an empty password
- Related test cases to be added

Closes #7"

echo "=== Commit Log ==="
git log --oneline

echo ""
echo "=== Latest commit full message ==="
git log -1 --format="%B"

echo ""
echo "=== Automatic integration in GitHub UI ==="
echo "If you have 'Closes #7' in your PR description or commit"
echo "Issue #7 will be automatically closed when merging PRs."

rm -rf "$REPO"
src/ex04_codeowners.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "manager" && git config user.email "[email protected]"
    git config commit.gpgsign false

mkdir -p .github/workflows docs src/backend src/frontend

cat > .github/CODEOWNERS << 'EOF'
#CODEOWNERS — Automatically assigns reviewers when creating a PR.

#Default: All files
*   @admin-user

#document
docs/   @tech-writer @admin-user

#backend source
src/backend/   @backend-team

#frontend source
src/frontend/  @frontend-team

#CI/CD workflow — for security reasons, DevOps teams only
.github/workflows/   @devops-team
EOF

echo "backend" > src/backend/server.py
echo "front end" > src/frontend/index.html
echo "document" > 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 structure ==="
git ls-files

rm -rf "$REPO"

Assignment

Problem 1

Simulate the entire flow of fork → feature branch → PR merge with a local bare repository.

  • File name: hw01.sh
  • Requirements:
  1. Create a upstream.git (bare) repository and push the initial commit.
  2. Clone to the my_fork/ directory and add upstream remote.
  3. Create a feature/new-page branch, add new_page.md and commit.
  4. Push to the bare repository of the fork repository itself (or directly upstream).
  5. Simulate PR merge with --no-ff merge in upstream.
  6. Print the final history with git log --oneline --graph --all.

Problem 2

Create a repository containing the CODEOWNERS file and issue-linked commit messages.

  • File name: hw02.sh
  • Requirements:
  1. Initialize temporary storage.
  2. Create directories and files with the structure below:
    .github/CODEOWNERS
    src/api.py
    docs/api.md
    
  3. Set src/ → @backend, docs/ → @writer in CODEOWNERS.
  4. Edit src/api.py in the fix/issue-12 branch and Commit with a commit message including Closes #12.
  5. Print the commit message as git log -1 --format="%B".

Check the correct answer

After writing it yourself, compare it with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

hw01.shShell
#!/usr/bin/env bash
set -euo pipefail

WORKDIR=$(mktemp -d)

git init --bare -b main "$WORKDIR/upstream.git" -q

#upstream initialization
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 "[email protected]"
git -C "$WORKDIR/upstream_local" config commit.gpgsign false
echo "#project" > "$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 "[email protected]"
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" > new_page.md
git add . && git commit -q -m "docs: add new page"

#Push directly to upstream (fork simulation)
git push upstream feature/new-page -q

#Merge PR from upstream
cd "$WORKDIR/upstream_local"
git fetch origin -q
git merge --no-ff origin/feature/new-page -m "merge: PR — add new page"

echo "=== Final History ==="
git log --oneline --graph --all

rm -rf "$WORKDIR"
hw02.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "manager" && git config user.email "[email protected]"
    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 Documentation" > docs/api.md

git add . && git commit -q -m "chore: initial project structure"

#Fixed Issue #12
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"

Learn this lesson with a mentor

Work through the Git & GitHub track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now