Command:
$ git checkout -b feauture/fix_code_quality
# (note: had the typo originally!)
After creating the branch:
feauture/fix_code_qualitymain (beaea6c)Key insight: Creating a branch is cheap - it's just creating a pointer (a few bytes)!
Making the first commit:
$ vim main.py # Fix code formatting
$ git add main.py
$ git commit -m "fix code formatting using ruff format"
Before the commit:
After the commit (032f36c):
What happened:
032f36cfeauture/fix_code_quality pointer forward to 032f36cmain pointer stayed at beaea6c (unchanged!)feauture/fix_code_qualityOnly the current branch pointer moves when you commit.
Other branches stay where they are!
This is how feature branches provide isolation - your work on the feature branch doesn't affect main.
Making more commits:
$ git commit -m "lasse ruff check laufen..." # e341990
$ git commit -m "style: fix code quality..." # 1ff0a77
Final state after 3 commits on branch:
What we see:
main is still at beaea6c (where we branched from)feauture/fix_code_quality has moved forward through 3 commitsSwitching back to main:
$ git checkout main
Before the checkout:
After the checkout:
Only HEAD moved!
beaea6cfeauture/fix_code_quality still points to 1ff0a77This is all that git checkout does - it moves the HEAD pointer!
When you merge the feature branch:
$ git checkout main
$ git merge feauture/fix_code_quality
# (Or merge via GitHub PR)
Result: Fast-forward merge
After the merge (fast-forward):
What happened:
main pointer forward to 1ff0a77main and feauture/fix_code_quality now point to the same commitIn the example above, things were easy — main hadn't moved. In real teams, multiple developers work in parallel: while we were on the feature branch, someone else merged their PR.
main and our feature have diverged — a fast-forward is no longer possible. The question: how do we get our three commits back into main cleanly?
$ git checkout main
$ git merge --no-ff feature/fix_code_quality
# Creates a merge commit M3 with two parents: M2 and C3
M3 is a new commit with two parents: M2 (from main) and C3 (from the feature branch). The feature commits keep their original hashes — they are not copied; they are simply now reachable from main via M3.
Pros
git log --graph Cons
Typical use: teams that want feature lineage to remain visible — every merge is an explicit integration point in git log --graph.
What's in the merge commit?
Git generates a default message and opens our editor:
Merge branch 'feature/fix_code_quality' into main
Set or override directly:
$ git merge --no-ff feature/... \
-m "Merge: code quality (PR #42)"
On GitHub via PR — auto-generated:
Merge pull request #42 from user/feature/fix_code_quality
<PR description as body>
What happens to the feature branch?
In practice, delete it after the merge:
$ git branch -d feature/fix_code_quality
$ git push origin --delete feature/fix_code_quality
Are our commits gone? No. C1, C2, C3 remain reachable via M3:
git log still shows themgit blame still finds their authorsgit checkout C2 jumps straight to itA branch is just a pointer — deleting the pointer does not delete the commits.
$ git checkout main
$ git merge --squash feature/fix_code_quality
$ git commit -m "Fix code quality (PR #42)"
# On GitHub: the "Squash and merge" button
S1 is a new commit that combines all changes from C1, C2, C3 into a single diff. C1, C2, C3 themselves are not in main's history — the feature branch is typically deleted afterwards.
Pros
main Cons
maingit bisect is less granularTypical use: GitHub repos configured with "Squash and merge" as default — feature authors make as many small commits as they want, and the merge collapses them into one clean main commit.
$ git checkout feature/fix_code_quality
$ git rebase main # C1, C2, C3 are "replayed" onto M2
$ git push --force-with-lease
$ git checkout main
$ git merge feature/fix_code_quality # Now a fast-forward is possible!
C1', C2', C3' have new hashes — they are replayed copies of the original C1, C2, C3. The originals only exist in the reflog.
Pros
git log reads like a story Cons
Golden rule: only rebase branches that nobody else is working on — otherwise you destroy your colleagues' history.
| Strategy | History on main |
When it fits |
|---|---|---|
| Fast-Forward | Linear, no merge traces | Solo projects, main hasn't moved |
Merge Commit (--no-ff) |
Branched, complete | Teams that want to see feature lineage |
| Squash & Merge | Linear, one commit per PR | Many small "WIP" commits; clean main |
| Rebase + FF | Linear, all commits | Updating a feature branch locally before PR |
In practice: teams decide per repo, not per PR. GitHub repos are often configured to Squash-only or Merge-Commit-only. Rebase usually stays a local tool to keep your own feature branch clean.
Important: don't pick the "perfect" strategy — pick one that fits the team and apply it consistently.
Creating the lecture-04 tag:
$ git tag -a lecture-04 -m "End of Chapter 02 (Refactoring): Code quality fixes"
Current state with tag:
All four pointers point to the SAME commit 1ff0a77!
Tags are just like branches - they're pointers to commits!
The difference:
Both are just references to commits - not copies of commits.
Fixing the typo:
$ git branch -m feauture/fix_code_quality feature/fix_code_quality
What happened:
Before rename:
After rename:
The commit 1ff0a77 never changed!
We just:
This is why:
lecture-04 still works (points to commit, not branch name)Without branches (pushing to main):
main: A → B → C → D(broken) → E(fix) → F(fix) → G(works)
↑
Everyone sees broken code
Other devs blocked
Hard to revert
With feature branches:
main: A → B → C ────────────→ M (merge)
feature: D → E → F → G (tested)
↑
Only you see this
Safe to experiment
Easy to delete
5 benefits: isolation · safety · experimentation · review · easy rollback
View branches:
$ git branch
main
* feature/fix-code-quality ← You're here (*)
feature/improve-road-curve
Create a branch:
$ git branch feature/new-feature # Create, stay on current branch
$ git checkout -b feature/new-feature # Create AND switch (classic)
$ git switch -c feature/new-feature # Create AND switch (modern)
Switch between branches:
$ git checkout feature/fix-code-quality # classic
$ git switch feature/fix-code-quality # modern
# Delete a branch
$ git branch -d feature/old-feature # Safe — only if merged
$ git branch -D feature/old-feature # Force — even if not merged
# Inspect branches
$ git branch # Local branches
$ git branch -a # All branches (including remotes)
$ git branch --merged main # Branches merged into main
$ git branch --no-merged # Branches not yet merged
Delete after merging keeps the repo clean. Branches are cheap — create a fresh one for the next feature.
mainmain when completeGolden rule: Never push directly to main. Always use feature branches.
(We'll enforce this with branch protection in a few slides.)
Good — descriptive, prefixed
feature/improve-road-curve
fix/intersection-calculation
refactor/ray-calculation
docs/add-usage-instructions
fix/42-intersection-bug
Bad — meaningless, vague
branch1
test
dominik-stuff
fix
new
Best practices:
kebab-case)feature/, fix/, refactor/, docs/mainmainSteps 1–3: local · Steps 4–9: collaborative · Step 10: cleanup
# 1. Create feature branch from updated main
$ git checkout main
$ git pull origin main
$ git checkout -b feature/fix-code-quality
# 2. Implement — small, logical commits
$ vim main.py
$ git add main.py
$ git commit -m "Fix PEP8: separate imports onto different lines"
$ vim main.py
$ git add main.py
$ git commit -m "Fix PEP8: add spacing around operators"
# 3. Push to GitHub (first time: -u links local
remote)
$ git push -u origin feature/fix-code-quality
Good commits: small, focused, descriptive · reference the standard (PEP8) · run checks first.
A Pull Request asks: "Please review and merge my changes."
On GitHub:
main · Compare: feature/fix-code-qualityThe PR is now visible to the whole team — they can comment, review, and approve.
5. Automated checks (GitHub Actions)
Ruff check passed
Ruff format check passed
Pyright type check passed
Tests pass
All checks passed — Ready to merge!
If a check fails, fix locally → push → PR re-runs automatically.
6. Code review — reviewers look for:
7. Address feedback — push more commits, PR updates automatically.
8. Merge (on GitHub, three strategies):
| Strategy | What it does |
|---|---|
| Merge commit | Keeps all commits + creates merge commit (default) |
| Squash and merge | Combines all commits into one |
| Rebase and merge | Replays commits linearly on top of main |
9. Delete feature branch — click "Delete branch" on GitHub, then locally:
$ git checkout main
$ git pull origin main
$ git branch -d feature/fix-code-quality
Despite the name, a pull request is really a merge request.
You're asking maintainers to pull your changes into their branch. Historical term; modern meaning is:
"Please review and merge my changes."
Why PRs matter:
mainEach part has a dedicated tab in the GitHub UI — reviewers move through them in order.
Size matters
Good PR — 50–300 lines
Bad PR — 2,000+ lines
Description template
## What
One-sentence summary.
## Why
Motivation, link to issue.
## How
Key implementation decisions.
## Testing
Commands run, results observed.
## Checklist
- [ ] Tests updated
- [ ] Docs updated
If the PR is too large — split it into multiple smaller PRs.
# Code in the PR
def generate_road_profile(num_points=100, x_max=80):
x = np.linspace(0, x_max, num_points)
x_norm = x / x_max
y = 0.015 * x_norm**3 * x_max + 0.3 * np.sin(2 * np.pi * x_norm) + 0.035 * x_norm * x_max
y = y - y[0]
return x, y
Reviewer's comments:
-> tuple[np.ndarray, np.ndarray]num_points > 0 and x_max > 0?0.015, 0.3, 0.035) — extract as named constants?Author responds: push a follow-up commit; the PR updates automatically.
| State | Meaning |
|---|---|
| Open | Under review, can still be modified, not yet merged |
| Merged | Accepted and merged into target branch — branch can be deleted |
| Closed | Rejected or abandoned — can be reopened if needed |
| Draft | Work in progress, not ready for review — useful for early feedback |
Tip: Open your PR as a draft early if you want feedback on direction before investing more time.
The problem: nothing technically stops you from doing this —
$ git checkout main
$ git commit -m "Quick fix"
$ git push origin main # Bypasses the PR process!
The fix: configure branch protection on GitHub (Settings → Branches):
$ git push origin main
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Cannot push directly to main. Use a pull request.
To github.com:yourname/yourrepo.git
! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'github.com:yourname/yourrepo.git'
Benefits:
main without a reviewIn Part 2 we add GitHub Actions to run checks on every PR:
# .github/workflows/quality.yml
name: Code Quality
on:
pull_request: # Trigger on PRs (not direct pushes)
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv sync --dev
- run: uv run ruff check .
When you open a PR, GitHub runs all of this automatically and blocks merge on failure.
1. Create feature branch from main
2. Implement feature (small commits)
3. Push branch to GitHub
4. Open Pull Request
↓
5. Automated checks run (GitHub Actions)
Ruff linter ·
Ruff format ·
Pyright ·
Tests
↓
6. If checks fail → fix → push → checks run again
↓
7. Request review → address feedback → get approval
↓
8. Merge to main (only possible if checks pass + approved)
9. Delete feature branch
This is how professional teams ship high-quality code.
Goal: Fix one real code quality issue in the Road Profile Viewer using the full feature branch workflow.
The target: remove the unused HelperFunction in main.py — one of the 14 Ruff errors from Chapter 02.
# Remove or comment out the unused function:
# def HelperFunction(val):
# """Unused helper function that violates naming convention"""
# result=val*2
# return result
You'll create a branch, make the fix, open a PR, merge, and clean up.
# 1. Start from an updated main
$ git checkout main
$ git pull origin main
# 2. Create the feature branch
$ git checkout -b feature/remove-unused-helper
# 3. Make your change
$ vim main.py # Comment out or delete HelperFunction
# 4. Verify the fix locally
$ uv run ruff check main.py
# → Should show ONE less error than before
# 5. Commit
$ git status
$ git add main.py
$ git commit -m "Fix PEP8: Remove unused HelperFunction"
# 6. Push to GitHub (first time: -u)
$ git push -u origin feature/remove-unused-helper
7. On GitHub → open Pull Request
Fix PEP8: Remove unused HelperFunction8. Review your own PR on the "Files changed" tab
# 9. Merge on GitHub — click "Merge pull request" → "Confirm merge" → "Delete branch"
# 10. Clean up locally
$ git checkout main
$ git pull origin main
$ git branch -d feature/remove-unused-helper
# 11. Verify
$ git log --oneline
# → You should see: "Fix PEP8: Remove unused HelperFunction"
$ uv run ruff check main.py
# → One less error!
Challenge: repeat this process for each of the remaining 13 Ruff errors — one branch, one PR per fix.
Did you:
main branch?If yes to all → you're thinking like a professional developer.
Starting a feature
git checkout main
git pull origin main
git checkout -b feature/my-feature
While working
git status
git diff
git add <file>
git commit -m "Message"
Pushing
git push -u origin feature/my-feature # first time
git push # after
Finishing (after PR merged)
git checkout main
git pull origin main
git branch -d feature/my-feature
Information
git status
git log --oneline --graph --all
git branch
git diff
git diff --staged
git show <commit-hash>
Undoing
git restore <file> # discard working-dir change
git restore --staged <file> # unstage (keep change)
git reset HEAD~1 # undo last commit (keep files)
git commit --amend # edit last commit message
Committed to the wrong branch (not pushed yet)
git reset HEAD~1
git checkout correct-branch
git add .
git commit -m "Message"
Update feature branch with latest main
git checkout feature/my-feature
git pull origin main # resolve conflicts if any
git push
Forgot to create a feature branch
git stash
git checkout -b feature/new-feature
git stash pop
git add . && git commit -m "Message"
Start over on one file
git checkout <file>
Before — Cowboy coding
$ git add .
$ git commit -m "stuff"
$ git push origin main
#
Hope nothing breaks!
After — Professional workflow
$ git checkout main
$ git pull origin main
$ git checkout -b feature/fix-code-quality
# ... work ...
$ git add main.py
$ git commit -m "Fix PEP8: separate imports..."
$ git push -u origin feature/fix-code-quality
# open PR · review · merge
$ git checkout main && git pull
$ git branch -d feature/fix-code-quality
main — always use feature branchesmain — always pull before branchinggit status is your friend — run it constantly