02 Code Quality in Practice: Feature Branch Development - From Cowboy Coding to Professional Workflows
October 2025 (9533 Words, 53 Minutes)
1. Introduction: The Problem with Pushing to Main
You’ve learned about code quality tools in Chapter 02 (Code Quality in Practice)—Ruff for linting and formatting, Pyright for type checking. You know how to check if your code meets professional standards.
But here’s what typically happens in student projects:
# Write some code
$ git add .
$ git commit -m "Added new feature"
$ git push origin main
# Hope for the best 🤞
This workflow has serious problems:
- No review before deployment - Your code goes straight to production (the
mainbranch) - No chance to catch issues - Once pushed, it’s live
- Breaking changes affect everyone - If your code has bugs, the entire team is blocked
- No collaboration - Other developers can’t review or suggest improvements
- Difficult to track what changed - All commits mixed together on
main
Real-world consequence:
Imagine you’re working on a team project. You push broken code to main at 11 PM. Your teammate pulls the latest code the next morning to start their day. Their entire development environment is now broken. They spend 2 hours debugging before realizing the issue is in your code from last night. This is why professional teams don’t push directly to main.
2. Learning Objectives
By the end of this lecture, you will:
- Understand Git’s fundamental concepts: working directory, staging area, commits, and branches
- Master feature branch workflows: creating, working on, and merging feature branches
- Learn pull request workflows: how to request code review before merging
- Understand branch protection: preventing direct pushes to
main - Prepare for CI/CD: setting up the foundation for automated quality checks (Chapter 02 (Automation and CI/CD))
- Apply professional collaboration patterns: how real software teams work
3. Part 1: Git Fundamentals - Understanding What You’ve Been Doing
You’ve been using Git, but do you understand what’s really happening? Let’s demystify the core concepts.
3.1 The Three States of Git
Git has three main states where your files can reside:
1. Working Directory (Modified)
- Files you’re actively editing
- Changes not yet saved to Git
- Status: “I’ve changed this file but haven’t told Git yet”
2. Staging Area (Staged)
- Changes marked to be included in the next commit
- A preview of your next commit
- Status: “I want these changes in my next commit”
3. Repository (Committed)
- Changes permanently saved in Git’s history
- Snapshot of your project at a point in time
- Status: “This is part of my project’s history”
The Git Workflow:
Working Directory → Staging Area → Repository
(Modified) (Staged) (Committed)
↓ ↓ ↓
edit file git add file git commit -m "..."
3.2 Visualizing Git’s Three States
Example scenario: Let’s fix some of the code quality issues in our Road Profile Viewer’s main.py:
# 1. WORKING DIRECTORY (Modified)
# You edit main.py to fix the import statement
$ vim main.py # Change: import sys,os → import sys\nimport os
$ git status
Changes not staged for commit:
modified: main.py
# 2. STAGING AREA (Staged)
$ git add main.py
$ git status
Changes to be committed:
modified: main.py
# 3. REPOSITORY (Committed)
$ git commit -m "Fix PEP8 violation: separate imports onto different lines"
$ git status
nothing to commit, working tree clean
What just happened:
1. Working Directory (Modified)
┌─────────────────────────────────┐
│ main.py (edited) │
│ Fixed: import sys,os │
│ → import sys │
│ → import os │
└─────────────────────────────────┘
↓ git add
2. Staging Area (Staged)
┌─────────────────────────────────┐
│ main.py │
│ Ready for commit │
└─────────────────────────────────┘
↓ git commit
3. Repository (Committed)
┌─────────────────────────────────┐
│ Commit: a1b2c3d │
│ "Fix PEP8 violation: separate │
│ imports onto different lines" │
│ Timestamp: 2025-10-16 │
└─────────────────────────────────┘
3.3 Why the Staging Area Exists
Question: Why not just commit directly from working directory?
Answer: The staging area gives you control over what goes into each commit.
Example - Separating Concerns:
# You've fixed multiple issues in main.py:
$ git status
Changes not staged for commit:
modified: main.py # Fixed multiple PEP8 violations
# Instead of ONE big commit, create logical commits:
# First: Fix import issues
$ git add -p main.py # Select only import-related changes
$ git commit -m "Fix PEP8: Separate imports and remove unused imports"
# Second: Fix spacing issues
$ git add -p main.py # Select only spacing changes
$ git commit -m "Fix PEP8: Add proper spacing around operators"
# Third: Fix naming convention
$ git add main.py # Add remaining changes
$ git commit -m "Fix PEP8: Rename HelperFunction to helper_function"
Benefits:
- ✅ Each commit has a single, clear purpose (imports, spacing, naming)
- ✅ Easier to understand history later (“which commit broke imports?”)
- ✅ Easier to revert specific changes (just revert the naming change if needed)
- ✅ Better for code review (reviewer sees logical progression)
3.4 Git Status - Your Best Friend
The git status command shows you exactly what state your files are in:
$ git status
On branch feature/fix-code-quality
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: main.py ← STAGED (import fixes)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: main.py ← MODIFIED (spacing fixes not yet staged)
Untracked files:
(use "git add <file>..." to include in what will be committed)
.ruff_cache/ ← UNTRACKED (tool cache, should ignore)
Interpretation:
- Staged files (
main.py- import changes): Will be included in next commit - Modified files (
main.py- spacing changes): Changed but not staged (won’t be in next commit) - Untracked files (
.ruff_cache/): New directory Git doesn’t track (add to.gitignore)
Pro tip: Run git status constantly. It’s your map showing where you are and what’s about to happen.
3.5 Understanding Commits
A commit is a snapshot of your project at a specific point in time.
What’s in a commit?
Commit: a1b2c3d4
Author: Student Name <student@example.com>
Date: 2025-10-16 10:30:00 +0200
Fix PEP8 violations: imports and spacing
- Separate multiple imports onto different lines
- Remove unused sys and os imports
- Add proper spacing around operators in generate_road_profile
- Add spaces after commas in function parameters
Changed files:
main.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
Key components:
- Commit hash (
a1b2c3d4...): Unique identifier for this commit - Author & Date: Who made the change and when
- Commit message: Description of what changed and why
- Diff: Actual code changes (4 lines added, 4 lines removed)
Commits are permanent - Once committed, that snapshot exists forever in Git’s history (unless you force-delete it).
3.6 Viewing Git History
See commit history:
$ git log --oneline --graph --all
* d4e5f6g (HEAD -> feature/fix-code-quality) Fix PEP8: Rename helper_function
* a1b2c3d Fix PEP8: Add proper spacing around operators
* 7h8i9j0 Fix PEP8: Separate imports and remove unused ones
* k1l2m3n (origin/main, main) Add Road Profile Viewer application
* f9e8d7c Initial commit
Breakdown:
*= Commit in the historyHEAD ->= Where you currently are (on feature branch, fixing code quality)(origin/main, main)= Branch pointers (main branch)d4e5f6g= Commit hash (shortened)Fix PEP8...= Commit message (describes the quality fix)
3.7 Common Git Status Scenarios
Scenario 1: Clean Working Tree
$ git status
On branch main
nothing to commit, working tree clean
✅ All changes are committed. Safe to switch branches.
Scenario 2: Uncommitted Changes
$ git status
On branch main
Changes not staged for commit:
modified: main.py
⚠️ You have unsaved changes. Either commit them or stash them before switching branches.
Scenario 3: Staged but Not Committed
$ git status
On branch main
Changes to be committed:
modified: main.py
⚠️ Changes are staged but not yet saved to history. Commit them!
Scenario 4: Both Staged and Unstaged
$ git status
On branch feature/fix-code-quality
Changes to be committed:
modified: main.py ← Staged (import fixes)
Changes not staged for commit:
modified: main.py ← Not staged (spacing fixes just made)
⚠️ You staged the import fixes in main.py, then edited it again to fix spacing. Next commit will only include the import fixes unless you git add again.
4. Part 2: Branches - Working in Isolation
4.1 What is a Branch?
A branch is an independent line of development. Think of it as a parallel universe where you can make changes without affecting the main timeline.
Analogy: Writing a book
main branch: Chapter 1 → Chapter 2 → Chapter 3 → Published Book
↓
feature branch: Chapter 2 → Experiment with alternative ending
→ Rewrite chapter completely
→ Preview with test readers
→ Decide if it's better
↓
Merge back → Use new version in final book
4.2 The Technical Reality: Branches Are Pointers
Important misconception: Many students think a branch is a commit. This is wrong.
The truth: A branch is just a lightweight, movable pointer to a commit.
What actually exists in Git:
-
Commits: The actual snapshots of your code. They form a chain (history) and never change once created. Each commit has a unique hash (like
1ff0a77). -
Branches: Just labels that point to commits. They’re movable references, not commits themselves.
-
Tags: Also pointers to commits, but they don’t move (they’re permanent markers).
Visual representation:
All three pointers (main, feature/fix-code-quality, v1.0) reference the same commit 1ff0a77. They’re not copies of the commit—they’re just labels pointing to it.
What happens when you create a branch:
$ git checkout -b feature/new-feature
Git doesn’t copy any commits. It just creates a new pointer called feature/new-feature that points to the same commit you were on.
What happens when you commit on a branch:
$ git commit -m "Add new feature"
Git:
- Creates a new commit (e.g.,
2aa3b44) - Moves the current branch pointer to point to this new commit
- Leaves other branch pointers unchanged
Before commit:
After commit:
What happens when you rename a branch:
$ git branch -m feauture/fix feature/fix
Git doesn’t change any commits. It just:
- Creates a new pointer called
feature/fixpointing to the same commit - Deletes the old pointer called
feauture/fix
The commits themselves are unchanged, which is why tags and other references still work perfectly!
What about HEAD?
You’ve probably seen HEAD in Git output and wondered what it means. HEAD is another pointer—a special one that tells Git where you are right now.
The simple explanation:
HEAD is like a “You Are Here” marker on a map. It points to your current location in the Git history.
How HEAD works:
Most of the time, HEAD points to a branch pointer, which points to a commit. This is called “attached HEAD” state.
In this diagram:
1ff0a77is a commit (the actual code snapshot)mainis a branch pointer (points to the commit)HEADpoints tomain(you’re currently on the main branch)
When you switch branches, HEAD moves:
$ git checkout feature/fix-code-quality
Before switching:
After switching:
HEAD now points to the feature branch instead of main. That’s all git checkout or git switch does—it moves the HEAD pointer.
When you make a commit:
$ git commit -m "Add validation"
Before commit:
After commit:
Git creates the new commit 2aa3b44, moves the branch pointer (feature/fix-code-quality) to point to it, and HEAD follows along because it’s pointing to that branch.
You’ve already seen HEAD!
Remember this from earlier in the lecture?
$ git log --oneline --graph --all
* d4e5f6g (HEAD -> feature/fix-code-quality) Fix PEP8: Rename helper_function
* a1b2c3d Fix PEP8: Add proper spacing around operators
(HEAD -> feature/fix-code-quality) means:
- You are on the
feature/fix-code-qualitybranch - That branch points to commit
d4e5f6g - The arrow
->shows HEAD pointing to the branch
Detached HEAD (brief mention):
Sometimes you’ll see a warning about “detached HEAD state.” This happens when HEAD points directly to a commit instead of to a branch:
$ git checkout 1ff0a77 # Checking out a specific commit by hash
Detached HEAD:
HEAD → 1ff0a77 (no branch pointer in between)
Git warns you because any commits you make won’t belong to a branch and could be lost. You’ll rarely need this in normal work, but now you know what it means if you see it!
Key takeaway:
- HEAD = “You are here” pointer
- Usually: HEAD → branch → commit (attached HEAD)
- Rarely: HEAD → commit (detached HEAD, Git will warn you)
git checkout/git switch= Move HEAD to a different branch- When you commit, the branch HEAD points to moves forward
Why this matters:
- Branches are cheap: Creating a branch is just creating a pointer (a few bytes)
- Branches are fast: No copying of files or commits needed
- Multiple branches can point to the same commit: That’s perfectly normal
- Deleting a branch doesn’t delete commits: It just removes the pointer
- HEAD tells you where you are: Understanding HEAD helps you understand what Git commands actually do
Example from real use:
$ git log --oneline --all --graph
* 1ff0a77 (HEAD -> main, tag: lecture-04, origin/main, feature/fix-code-quality) Add Road Profile Viewer
* e341990 Fix imports
* 032f36c Initial commit
Here you can see:
- Commit
1ff0a77exists (the actual code snapshot) - Four pointers all reference it:
HEAD,main,tag: lecture-04,origin/main, andfeature/fix-code-quality - None of these pointers ARE the commit—they just POINT TO it
📝 Practice Time: Test Your Understanding!
Now that you’ve learned how Git branches work as pointers, it’s the perfect time to solidify your understanding through practice.
👉 Git Pointers Exercise: Understanding Branches and HEAD
This exercise contains 15 carefully designed questions that will help you:
- Visualize how pointers move during Git operations
- Understand the difference between branches, commits, and HEAD
- Practice identifying merge types (fast-forward vs three-way)
- Master common Git workflows through visual diagrams
Each question shows you Git graphs with commit hashes, branches, and pointers. You’ll need to predict what happens after various Git commands—exactly the kind of thinking that will make you confident using Git!
Why practice now?
- The concepts are fresh in your mind
- You’ll immediately apply what you just learned
- The exercise reinforces pointer mechanics before we move on
- It’s easier to learn incrementally than to cram later
Take 15-20 minutes to work through the exercise. Don’t worry if you don’t get everything right on the first try—that’s how we learn! The explanations will help clarify any misunderstandings.
4.3 Why Use Branches?
Without branches (pushing to main):
main: A → B → C → D (broken!) → E (fix D) → F (fix E) → G (works again)
↑
Everyone sees broken code
Other developers blocked
Difficult to revert
With feature branches:
main: A → B → C --------------------------------→ M (merge: tested feature)
↘ ↗
feature branch: D → E (broken) → F (fixed) → G (tested)
↑
Only you see this
Safe to experiment
Easy to delete if bad
Benefits:
- Isolation: Your experiments don’t affect others
- Safety: Main branch stays stable and deployable
- Experimentation: Try ideas without fear of breaking production
- Review: Others can review your changes before merging
- Rollback: Easy to abandon bad ideas by deleting the branch
4.4 Branch Operations
View all branches:
$ git branch
main
* feature/fix-code-quality ← You're here (*)
feature/improve-road-curve
Create a new branch:
# Create branch but stay on current branch
$ git branch feature/new-feature
# Create branch AND switch to it
$ git checkout -b feature/new-feature
# or (modern syntax)
$ git switch -c feature/new-feature
Switch between branches:
$ git checkout feature/fix-code-quality
# or (modern syntax)
$ git switch feature/fix-code-quality
Delete a branch:
$ git branch -d feature/old-feature # Safe delete (only if merged)
$ git branch -D feature/old-feature # Force delete (even if not merged)
4.5 The Main Branch
The main branch (sometimes called master in older repositories) is special:
- Default branch: Created automatically when you initialize a repository
- Production branch: Should always be stable and deployable
- Protected: In professional teams, you cannot push directly to
main - Merge target: Feature branches merge into
mainwhen complete
Golden rule: Never push directly to main. Always use feature branches.
4.6 Feature Branch Naming Conventions
Good branch names are descriptive and follow a pattern:
Common patterns:
feature/improve-road-curve # New feature
fix/intersection-calculation # Bug fix
refactor/ray-calculation # Code improvement
docs/add-usage-instructions # Documentation update
Bad branch names:
branch1 # Meaningless
test # Too vague
dominik-stuff # Not descriptive
fix # Which fix?
Best practices:
- Use lowercase with hyphens (
kebab-case) - Start with a prefix (
feature/,fix/,refactor/) - Be descriptive but concise
- Reference issue numbers if applicable:
fix/42-intersection-bug
5. Part 3: Feature Branch Workflow
5.1 The Complete Feature Development Lifecycle
Here’s how professional developers work with feature branches:
1. Create feature branch from main
↓
2. Implement feature (multiple commits)
↓
3. Push branch to GitHub
↓
4. Open Pull Request (PR)
↓
5. Automated checks run (we'll set this up in Chapter 02 (Automation and CI/CD)!)
↓
6. Code review by team
↓
7. Address feedback (more commits)
↓
8. Approval + checks pass
↓
9. Merge to main
↓
10. Delete feature branch
Let’s walk through this step by step.
5.2 Step 1: Create Feature Branch
Always start from an up-to-date main branch:
# 1. Switch to main
$ git checkout main
# 2. Pull latest changes from GitHub
$ git pull origin main
# 3. Create and switch to feature branch
$ git checkout -b feature/fix-code-quality
What this does:
- Ensures your branch starts from the latest production code
- Creates an isolated environment for your work
- Prevents conflicts with other developers’ changes
5.3 Step 2: Implement Your Feature
Work as you normally would, but make small, logical commits:
# Start fixing code quality issues from Chapter 02
$ vim main.py # Fix import statement
# Check what changed
$ git status
$ git diff
# Stage your changes
$ git add main.py
# Commit with descriptive message
$ git commit -m "Fix PEP8: Separate imports onto different lines"
# Continue working...
$ vim main.py # Fix spacing issues
$ git add main.py
$ git commit -m "Fix PEP8: Add proper spacing around operators"
# More fixes...
$ vim main.py # Fix function naming
$ git add main.py
$ git commit -m "Fix PEP8: Rename HelperFunction to helper_function"
# Check your commit history
$ git log --oneline
7h8i9j0 Fix PEP8: Rename HelperFunction to helper_function
d4e5f6g Fix PEP8: Add proper spacing around operators
a1b2c3d Fix PEP8: Separate imports onto different lines
Good commit practices:
✅ Do:
- Make small, focused commits (one type of fix per commit: imports, spacing, naming)
- Write clear commit messages that reference the standard (PEP8)
- Commit often (easier to revert if a fix breaks something)
- Run checks before committing:
uv run ruff check .
❌ Don’t:
- Make huge commits with all 14 fixes at once
- Use vague messages like “Fixed stuff” or “WIP” or “more fixes”
- Commit without testing (run Ruff first!)
5.4 Step 3: Push Your Branch to GitHub
First time pushing this branch:
$ git push -u origin feature/fix-code-quality
What this does:
- Uploads your local branch to GitHub
-u(or--set-upstream) links your local branch to the remote branch- After the first push, you can just use
git push
Subsequent pushes (after more commits):
$ git push
Check on GitHub:
- Go to your repository on GitHub
- Click “Branches” or “Code” tab
- See your new branch listed
- GitHub may show a banner: “Compare & pull request”
5.5 Step 4: Open a Pull Request (PR)
A Pull Request (PR) is a request to merge your feature branch into main.
On GitHub:
- Click “Pull requests” tab
- Click “New pull request”
- Base branch:
main(where you want to merge TO) - Compare branch:
feature/fix-code-quality(your feature branch) - Review the changes (diff) - you’ll see all your PEP8 fixes highlighted
- Click “Create pull request”
- Fill in:
- Title: Brief description (e.g., “Fix PEP8 violations in main.py”)
- Description: What does this PR do? Why? Any special notes?
- Click “Create pull request”
What to include in PR description:
## Description
Fixes all PEP8 violations found by Ruff in `main.py` (14 errors from Chapter 02 (Code Quality in Practice)).
## Changes
- **Import fixes**: Separate multiple imports onto different lines, remove unused imports
- **Spacing fixes**: Add proper spacing around operators and after commas
- **Naming fixes**: Rename `HelperFunction` to `helper_function` (snake_case)
- **Line length fixes**: Break long comments to fit within 88 character limit
## Testing
- All Ruff checks pass: `uv run ruff check .`
- Code formatting verified: `uv run ruff format --check .`
- Application still runs correctly: `uv run road-profile-viewer`
## Before/After
- Before: 14 Ruff errors
- After: 0 Ruff errors ✅
## Related Issues
Addresses code quality issues identified in Chapter 02 (Code Quality in Practice).
5.6 Step 5: Automated Checks Run
This is where Chapter 02 (Automation and CI/CD) comes in!
When you open a PR, GitHub Actions can automatically:
- ✅ Run code quality checks (Ruff)
- ✅ Run type checking (Pyright)
- ✅ Run tests (pytest)
- ✅ Check code coverage
- ❌ Block merge if checks fail
Example PR status:
✅ Ruff check passed (0 errors, was 14)
✅ Ruff format check passed
✅ Pyright type check passed
✅ Application runs successfully
All checks passed - Ready to merge!
Or if you forgot to fix something:
❌ Ruff check failed
- main.py:312:1: E501 Line too long (149 > 88 characters)
❌ Ruff format check failed
- main.py would be reformatted
Cannot merge until checks pass.
In this case, you’d need to:
# Fix the remaining issue locally
$ vim main.py # Break the long line
$ uv run ruff check . # Verify it's fixed
$ git add main.py
$ git commit -m "Fix PEP8: Break long comment into multiple lines"
$ git push # This updates the PR automatically!
We’ll set this up in Chapter 02 (Automation and CI/CD)! For now, just understand that PRs can have automated quality gates.
5.7 Step 6: Code Review
Code review is when team members read your code and provide feedback.
Reviewers look for:
- Correctness: Does the code do what it claims?
- Quality: Is it readable, maintainable, well-tested?
- Design: Is this the right approach?
- Style: Does it follow conventions?
- Edge cases: What could go wrong?
Example review comments:
# Your code:
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
# Review comment: "What happens if x_road is empty or None?"
# Review comment: "Should we validate that angle_degrees is within valid range?"
angle_rad = -np.deg2rad(angle_degrees)
slope = np.tan(angle_rad)
# ... rest of function
Responding to feedback:
# Make requested changes
$ vim main.py
# Commit the changes
$ git add main.py
$ git commit -m "Add validation for empty road profile and angle range"
# Push to the same branch (updates the PR automatically!)
$ git push
The PR automatically updates with your new commits! No need to create a new PR.
5.8 Step 7: Address Feedback
Keep making changes until:
- ✅ All automated checks pass
- ✅ All review comments are addressed
- ✅ Reviewers approve the PR
Each time you push, the PR updates:
Commit history on PR:
a1b2c3d Fix PEP8: Separate imports onto different lines
d4e5f6g Fix PEP8: Add proper spacing around operators
7h8i9j0 Fix PEP8: Rename HelperFunction to helper_function
k1l2m3n Add type hints to generate_road_profile ← New commit (from review)
m4n5o6p Fix PEP8: Break long comment into lines ← Another new commit
5.9 Step 8: Merge to Main
Once approved and all checks pass:
On GitHub:
- Click “Merge pull request”
- Choose merge strategy:
- Merge commit: Keeps all commits (default)
- Squash and merge: Combines all commits into one
- Rebase and merge: Replays commits on top of main
- Click “Confirm merge”
- Success! Your feature is now in
main
What happens:
Before merge:
main: A → B → C
↘
feature: D → E → F
After merge:
main: A → B → C → M (merge commit, includes D, E, F)
5.10 Step 9: Delete Feature Branch
After merging, delete the feature branch:
On GitHub:
- Click “Delete branch” button (appears after merge)
Locally:
# Switch back to main
$ git checkout main
# Pull the merged changes
$ git pull origin main
# Delete local feature branch
$ git branch -d feature/fix-code-quality
Why delete?
- Keeps repository clean
- Prevents confusion (branch already merged)
- Branches are cheap—create new ones for new features
6. Part 4: Pull Requests Explained
6.1 What is a Pull Request?
Despite the name, a pull request is actually a merge request. You’re requesting that your changes be merged into another branch (usually main).
Why it’s called “pull request”:
Historical reasons. You’re asking the maintainers to “pull” your changes into their repository.
Modern interpretation:
“Please review and merge my changes.”
6.2 Anatomy of a Pull Request
A PR consists of:
1. Title & Description
- What the PR does
- Why it’s needed
- Any special considerations
2. Commits
- All commits from your feature branch
- Automatically updates when you push more commits
3. Files Changed (Diff)
- Visual representation of what changed
- Line-by-line comparison
- Easy to review
4. Conversation
- Discussion thread
- Review comments
- Approvals/rejections
5. Checks
- Automated tests/linting (we’ll add this in Chapter 02 (Automation and CI/CD)!)
- Build status
- Code coverage
6.3 PR Best Practices
Size matters:
✅ Good PR: 50-300 lines changed
- Easy to review
- Clear purpose
- Less likely to have bugs
❌ Bad PR: 2,000+ lines changed
- Overwhelming to review
- Likely to be rubber-stamped (approved without real review)
- More likely to introduce bugs
If your PR is too large: Break it into multiple PRs.
Example - Breaking up a large feature:
Instead of:
❌ PR #1: "Complete Road Profile Viewer refactoring" (2000 lines)
Do this:
✅ PR #1: "Refactor road profile generation into separate module" (100 lines)
✅ PR #2: "Refactor ray calculation functions" (80 lines)
✅ PR #3: "Refactor intersection detection logic" (150 lines)
✅ PR #4: "Improve Dash UI components and layout" (120 lines)
✅ PR #5: "Add comprehensive tests for all modules" (200 lines)
Good PR descriptions:
## What
Adds input validation to prevent crashes with invalid road profiles and camera angles.
## Why
The application crashes when edge cases occur (empty data, extreme angles). This makes it more robust (addresses #42).
## How
- Added validation for empty `x_road` and `y_road` arrays in `find_intersection()`
- Added angle range validation (warn if angle is outside -90° to 90° useful range)
- Added proper error handling with informative messages
- Created unit tests for edge cases
## Testing
- All tests pass
- Manual testing: Verified app handles empty arrays gracefully
- Verified angle validation shows appropriate warnings
- Application no longer crashes with invalid inputs
## Screenshots
(if UI changes)
## Checklist
- [x] Code follows style guide
- [x] Tests added/updated
- [x] Documentation updated
- [x] No breaking changes
6.4 Reviewing Pull Requests
As a reviewer, look for:
1. Does it work?
- Logic is correct
- Edge cases handled
- No obvious bugs
2. Is it tested?
- Unit tests included
- Edge cases covered
- Tests actually test the feature
3. Is it readable?
- Clear variable names
- Good comments (when needed)
- Follows style guide
4. Is it maintainable?
- Not overly complex
- Reuses existing code
- Documented if necessary
Example review comment:
# Code in 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
# Review comment:
"""
Consider adding type hints for better code documentation:
def generate_road_profile(num_points: int = 100, x_max: float = 80) -> tuple[np.ndarray, np.ndarray]:
...
Also, should we validate that num_points > 0 and x_max > 0?
The magic numbers (0.015, 0.3, etc.) could be extracted as named constants for clarity.
"""
6.5 PR States
Open:
- PR is under review
- Can still be modified
- Not yet merged
Merged:
- PR accepted and merged into target branch
- Feature now part of main codebase
- Branch can be deleted
Closed:
- PR rejected or abandoned
- Changes not merged
- Can be reopened if needed
Draft:
- Work in progress
- Not ready for review yet
- Useful for showing progress early
7. Part 5: Branch Protection - Preventing Direct Pushes
7.1 The Problem
Even with feature branches and PRs, nothing stops you from doing this:
$ git checkout main
$ git commit -m "Quick fix"
$ git push origin main # Bypasses PR process!
This defeats the entire purpose of feature branches.
7.2 Branch Protection Rules
Branch protection makes it impossible to push directly to main. You must use a PR.
On GitHub (repository settings):
- Go to Settings → Branches
- Click “Add branch protection rule”
- Branch name pattern:
main - Enable:
- ✅ Require a pull request before merging
- ✅ Require approvals (at least 1 review)
- ✅ Require status checks to pass (we’ll add CI checks in Chapter 02 (Automation and CI/CD)!)
- ✅ Do not allow bypassing the above settings (even admins must use PRs)
Now if you try to push directly:
$ 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'
✅ Mission accomplished! You’re forced to use the proper workflow.
7.3 Benefits of Branch Protection
1. Forces code review
- No code reaches
mainwithout review - Catches bugs before production
2. Enforces quality checks
- Automated tests must pass
- Linting/formatting must pass
- Type checks must pass
3. Prevents accidents
- Can’t accidentally push to wrong branch
- Protects against force pushes
4. Audit trail
- Every change has a PR
- Clear history of who approved what
- Discussion archived
8. Part 6: Connecting to CI/CD (Preview of Chapter 02)
8.1 Where We Are Now
You now understand:
- ✅ Git’s three states (working directory, staging, commits)
- ✅ Branches and why they’re important
- ✅ Feature branch workflow
- ✅ Pull requests as code review mechanism
- ✅ Branch protection to enforce PR workflow
But we’re still missing automation!
8.2 What’s Next in Chapter 02
In the next part of this lecture, we’ll add GitHub Actions to automatically run checks on every PR:
# .github/workflows/quality.yml
name: Code Quality
on:
pull_request: # ← Trigger on PRs (not pushes to main!)
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 .
- run: uv run ruff format --check .
- run: uv run pyright
What this will do:
When you open a PR, GitHub automatically:
- ✅ Runs Ruff linter
- ✅ Checks code formatting
- ✅ Runs Pyright type checker
- ✅ Shows results directly on the PR
- ❌ Blocks merge if checks fail
This is the power of CI/CD:
Developer creates PR
↓
GitHub Actions runs automatically
↓
All checks pass ✅
↓
Reviewer sees: "Safe to review, checks passed"
↓
Approve and merge
8.3 The Complete Professional Workflow
Combining feature branches + PRs + automated checks:
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 formatter
✅ Pyright type checker
✅ Unit tests (if you have them)
↓
6. If checks fail ❌
→ Fix issues
→ Push again (PR updates)
→ Checks run again
↓
7. If checks pass ✅
→ 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.
9. Part 7: Practical Exercise - Your First Feature Branch
9.1 Exercise: Fix One Code Quality Issue Using Feature Branch Workflow
Scenario: Fix the unused HelperFunction in main.py (one of the 14 Ruff errors from Chapter 02 (Code Quality in Practice)).
Follow the complete workflow:
Step 1: Start from main
$ git checkout main
$ git pull origin main
Step 2: Create feature branch
$ git checkout -b feature/remove-unused-helper
Step 3: Make your change
Edit main.py:
# Remove or comment out the unused function:
# def HelperFunction(val):
# """Unused helper function that violates naming convention"""
# result=val*2
# return result
Step 4: Verify the fix
$ uv run ruff check main.py
# Should show one less error!
Step 5: Commit your change
$ git status
$ git add main.py
$ git commit -m "Fix PEP8: Remove unused HelperFunction"
Step 6: Push to GitHub
$ git push -u origin feature/remove-unused-helper
Step 7: Open Pull Request on GitHub
- Go to your repository on GitHub
- Click “Compare & pull request”
- Fill in title: “Fix PEP8: Remove unused HelperFunction”
- Fill in description:
## Description Removes unused `HelperFunction` that was causing F841 error in Ruff. ## Changes - Removed unused function that violated naming convention ## Testing - Ruff check passes: `uv run ruff check main.py` - Application still runs: `uv run road-profile-viewer` ## Before/After - Before: Ruff reported F841 (unused function) - After: Error resolved ✅ - Click “Create pull request”
Step 8: Review your own PR
- Look at the “Files changed” tab
- See the deletion highlighted in red
- Verify that no other code was accidentally changed
Step 9: Merge
(Since you verified the fix works)
- Click “Merge pull request”
- Click “Confirm merge”
- Click “Delete branch”
Step 10: Clean up locally
$ git checkout main
$ git pull origin main
$ git branch -d feature/remove-unused-helper
Step 11: Verify
$ git log --oneline
# You should see: "Fix PEP8: Remove unused HelperFunction"
$ uv run ruff check main.py
# One less error! 📉
Congratulations! You just:
- ✅ Fixed a real code quality issue
- ✅ Used professional feature branch workflow
- ✅ Created a focused, reviewable PR
- ✅ Maintained a clean Git history
Challenge: Repeat this process for each of the remaining 13 Ruff errors! Each one gets its own feature branch and PR.
9.2 Exercise Checklist
Did you:
- ✅ Start from an up-to-date
mainbranch? - ✅ Create a descriptive feature branch name?
- ✅ Make a focused change?
- ✅ Write a clear commit message?
- ✅ Push to GitHub?
- ✅ Open a pull request?
- ✅ Merge only after review?
- ✅ Delete the feature branch after merging?
If yes to all: You’re thinking like a professional developer!
10. Part 8: Common Workflows and Commands Reference
10.1 Daily Workflow Commands
Starting a new feature:
git checkout main
git pull origin main
git checkout -b feature/my-new-feature
While working:
git status # Check what changed
git diff # See exact changes
git add <file> # Stage specific file
git add . # Stage all changes
git commit -m "Message" # Commit staged changes
Pushing changes:
git push -u origin feature/my-new-feature # First push
git push # Subsequent pushes
Finishing a feature:
# (Merge via PR on GitHub)
git checkout main
git pull origin main
git branch -d feature/my-new-feature
10.2 Useful Git Commands
Information commands:
git status # Current state
git log --oneline --graph --all # Visual commit history
git branch # List branches
git diff # Changes not staged
git diff --staged # Changes staged for commit
git show <commit-hash> # Details of specific commit
Undoing changes:
git restore <file> # Discard changes in working directory
git restore --staged <file> # Unstage file (keep changes)
git reset HEAD~1 # Undo last commit (keep changes)
git commit --amend # Modify last commit message
Branch management:
git branch # List local branches
git branch -a # List all branches (including remote)
git branch -d <branch> # Delete merged branch
git branch -D <branch> # Force delete branch
10.3 Common Git Scenarios
Scenario 1: I committed to the wrong branch!
# If you haven't pushed yet:
git reset HEAD~1 # Undo commit, keep changes
git checkout correct-branch
git add .
git commit -m "Message"
Scenario 2: I need to update my feature branch with latest main
git checkout feature/my-feature
git pull origin main # Merge main into feature branch
# Resolve conflicts if any
git push
Scenario 3: I want to start over on this file
git checkout <file> # Discard all changes to file
Scenario 4: I forgot to create a feature branch
# If you haven't committed:
git stash # Save changes temporarily
git checkout -b feature/new-feature # Create branch
git stash pop # Restore changes
git add .
git commit -m "Message"
10.4 Cleaning Up: Understanding Remote-Tracking Branches
After working with feature branches and merging pull requests, you might notice your Git repository showing branches that no longer exist. This section explains why this happens and how to clean it up.
The Three Types of Branches
When you run git branch -a, you see three types of references:
$ git branch -a
main ← Local branch
feature/new-feature ← Local branch
remotes/origin/main ← Remote-tracking branch
remotes/origin/feature/xyz ← Remote-tracking branch
Local branches (main, feature/new-feature):
- Your own branches where you work
- Exist only on your computer
- You create them with
git checkout -b
Remote-tracking branches (remotes/origin/*):
- Local copies of what exists on GitHub
- Automatically updated when you
git fetchorgit pull - You don’t directly work on these—they’re bookmarks showing GitHub’s state
The remote repository (GitHub):
- The actual branches on the server
- What your team sees
- Where PRs are merged
The Bookmark Analogy
Think of remote-tracking branches like bookmarks in your browser:
- GitHub = The actual websites
- Remote-tracking branches = Your bookmarks pointing to GitHub’s branches
- Local branches = Your own notes/drafts (separate from bookmarks)
The problem: When a branch is deleted on GitHub (after merging a PR), your bookmark still points to it until you clean up!
Why Stale References Happen
Here’s a typical workflow that creates stale references:
- You create a feature branch and push it to GitHub
- You create a PR and it gets merged
- GitHub automatically deletes the branch after merge ✅
- Your local Git still has
remotes/origin/feature/xyz(stale!)
Example:
# After PR is merged and branch deleted on GitHub
$ git branch -a
main
remotes/origin/main
remotes/origin/feature/old-pr ← This branch no longer exists on GitHub!
Git doesn’t automatically remove these references because:
- It doesn’t know if the deletion was intentional
- You might have been offline when the deletion happened
- Git errs on the side of keeping information
Understanding git fetch --prune
The --prune flag tells Git: “Remove local references to remote branches that no longer exist.”
$ git fetch --prune
What this does:
- Fetch: Download new commits and branches from GitHub
- Prune: Remove remote-tracking branches that no longer exist on GitHub
Example output:
$ git fetch --prune
From https://github.com/user/repo
- [deleted] (none) -> origin/feature/old-pr
- [deleted] (none) -> origin/feature/another-merged
Translation: “These remote-tracking branches were deleted locally because they no longer exist on GitHub.”
The Difference: git fetch --prune vs git remote prune
git fetch --prune (Recommended):
git fetch --prune
- Downloads new commits/branches AND removes stale references
- One command to do everything
git remote prune origin:
git remote prune origin
- Only removes stale references (doesn’t fetch new data)
- Use when you just want to clean up without downloading
Preview before pruning:
$ git remote prune origin --dry-run
Pruning origin
URL: https://github.com/user/repo
* [would prune] origin/feature/old-pr
* [would prune] origin/feature/another-merged
Complete Branch Cleanup Workflow
After merging a PR and having the branch deleted on GitHub:
# Step 1: Switch to main and update it (with automatic pruning)
git checkout main
git pull --prune
# Step 2: Delete your local feature branch
git branch -d feature/my-merged-feature
Why these steps?
git checkout mainswitches to your local main branch (even if it’s outdated)git pull --prunedoes THREE things at once:- Fetches updates from GitHub (updates
origin/mainbookmark) - Merges those updates into your local main (moves main pointer forward)
- Removes stale remote-tracking branches (like
remotes/origin/feature/my-merged-feature)
- Fetches updates from GitHub (updates
git branch -dremoves your local working copy of the branch
Understanding the workflow:
Before cleanup:
GitHub main: A---B---C---D---E (your PR merged here)
origin/main: A---B---C (outdated bookmark)
local main: A---B---C (you're here after checkout)
After git pull --prune:
GitHub main: A---B---C---D---E
origin/main: A---B---C---D---E (bookmark updated!)
local main: A---B---C---D---E (fast-forwarded!)
Note: git pull --prune combines git fetch --prune + git merge origin/main into one command!
Automatic Pruning (Recommended Setup)
You can configure Git to always prune when fetching:
$ git config --global fetch.prune true
Now every git fetch and git pull will automatically clean up stale references!
Why enable this:
- One less thing to remember
- Always have a clean view of branches
- No stale references cluttering your
git branch -aoutput
To check if it’s enabled:
$ git config --global --get fetch.prune
true
What About Local Branches?
Important: Remote pruning only affects remote-tracking branches (remotes/origin/*), not your local branches.
You must manually delete local branches:
$ git branch -d feature/my-merged-feature # Delete if merged
$ git branch -D feature/my-merged-feature # Force delete
Git keeps your local branches because:
- You might have uncommitted work
- You might want to reference the branch later
- It’s safer to require explicit deletion
Common Scenarios
Scenario 1: Branch was merged via PR, but Git says “not fully merged”
$ git branch -d feature/xyz
error: The branch 'feature/xyz' is not fully merged.
hint: If you are sure you want to delete it, run 'git branch -D feature/xyz'.
Why: PR was merged with “Squash and merge” or “Rebase and merge”—this creates new commits instead of using your original commits.
Solution: Use capital -D to force delete:
$ git branch -D feature/xyz
Check first that the PR was indeed merged on GitHub before force deleting!
Scenario 2: How do I see which branches are merged?
# Show branches merged into current branch
$ git branch --merged
# Show branches merged into main
$ git branch --merged main
# Show branches NOT yet merged
$ git branch --no-merged
Scenario 3: Batch cleanup of all merged branches
# Delete all local branches that have been merged into main
$ git branch --merged main | grep -v "^\* main" | xargs git branch -d
Warning: Only run this if you’re sure all listed branches are actually merged!
Why Clean Up Matters
Benefits of keeping your branches clean:
- Clarity:
git branch -aonly shows branches that actually exist - Faster: Less clutter means easier to find what you need
- Professional: Shows you understand Git’s architecture
- Prevents mistakes: Won’t accidentally work on deleted branches
It’s not critical: Stale references don’t break anything—they just cause confusion.
Visual Summary
Before cleanup:
Your Local Git:
├── Local branches:
│ ├── main
│ └── feature/old-pr (merged, but you forgot to delete)
└── Remote-tracking branches:
├── remotes/origin/main ✅ (exists on GitHub)
├── remotes/origin/feature/old-pr ❌ (deleted on GitHub)
└── remotes/origin/feature/another-old ❌ (deleted on GitHub)
After git fetch --prune and git branch -d:
Your Local Git:
├── Local branches:
│ └── main (clean!)
└── Remote-tracking branches:
└── remotes/origin/main ✅ (exists on GitHub)
Quick Reference: Cleanup Commands
# See all branches (including remote-tracking)
git branch -a
# Preview what would be pruned
git remote prune origin --dry-run
# Remove stale remote-tracking branches
git fetch --prune
# Or just prune without fetching
git remote prune origin
# Delete merged local branch
git branch -d feature/merged-branch
# Force delete local branch (if squash merged)
git branch -D feature/merged-branch
# Enable automatic pruning (recommended)
git config --global fetch.prune true
Key Takeaways
- Remote-tracking branches are local bookmarks to GitHub’s branches
- Stale references happen when branches are deleted on GitHub
git fetch --pruneremoves these stale references- Local branches must be deleted separately with
git branch -d - Enable automatic pruning with
git config --global fetch.prune true - Cleanup is optional but keeps your repository tidy
Think of branch cleanup as tidying your desk—your work doesn’t depend on it, but it makes everything clearer and more professional!
11. Summary: From Cowboy Coding to Professional Workflows
11.1 What You Learned
Git Fundamentals:
- ✅ Three states: Working directory → Staging area → Repository
- ✅ Commits as snapshots of your project
- ✅ Branches as parallel development lines
- ✅ Why staging area exists (control over commits)
Feature Branch Workflow:
- ✅ Never push directly to
main - ✅ Create feature branches for all changes
- ✅ Use descriptive branch names
- ✅ Make small, focused commits
- ✅ Delete branches after merging
Pull Requests:
- ✅ PRs enable code review before merging
- ✅ Keep PRs small (50-300 lines)
- ✅ Write descriptive PR descriptions
- ✅ Address feedback with new commits
- ✅ PRs are merge requests, not pull requests
Branch Protection:
- ✅ Prevents direct pushes to
main - ✅ Enforces PR workflow
- ✅ Requires code review
- ✅ Can require automated checks (next lecture!)
Professional Practices:
- ✅ Always start from up-to-date
main - ✅ One feature = one branch
- ✅ Commit early, commit often
- ✅ Write clear commit messages
- ✅ Review your own PRs before asking others
11.2 The Transformation
Before this lecture (Cowboy Coding):
$ git add .
$ git commit -m "stuff"
$ git push origin main # 🔥 Hope nothing breaks!
After this lecture (Professional Workflow):
$ git checkout main
$ git pull origin main
$ git checkout -b feature/fix-code-quality
# ... work on feature ...
$ git add main.py
$ git commit -m "Fix PEP8: Separate imports onto different lines"
$ git push -u origin feature/fix-code-quality
# ... open PR, get review, address feedback ...
# ... merge via PR on GitHub ...
$ git checkout main
$ git pull origin main
$ git branch -d feature/fix-code-quality
11.3 Why This Matters
1. Quality
- Code is reviewed before merging
- Automated checks ensure standards (next lecture!)
- Main branch stays stable
2. Collaboration
- Multiple developers can work simultaneously
- No stepping on each other’s toes
- Clear communication via PRs
3. Reversibility
- Easy to revert bad changes
- Clear history of who changed what
- Branches can be abandoned without affecting main
4. Professionalism
- Industry-standard workflow
- Shows you understand modern development practices
- Makes you a better team player
5. Confidence
- Experiment without fear
- Main branch is always deployable
- Changes are isolated until proven
11.4 Preparing for Chapter 02
You now have the foundation for CI/CD:
- ✅ Feature branches to isolate changes
- ✅ Pull requests as quality gates
- ✅ Branch protection to enforce workflow
Next lecture, we’ll add automation:
- GitHub Actions to run checks automatically
- Automated linting, formatting, type checking
- Block merge if quality checks fail
- See results directly on PRs
The complete picture:
Feature Branch → PR → Automated Checks → Code Review → Merge
↑
We'll build this next!
12. Key Takeaways
Remember these principles:
- Never push directly to
main- Always use feature branches - Branches are cheap - Create a new branch for every feature/fix
- Commits are permanent - Write clear commit messages
- PRs enable collaboration - Use them for all changes, even if you’re working alone
- Small PRs are better - 50-300 lines is ideal
- Branch protection is essential - Enforce your workflow with rules
- Start from updated
main- Always pull before creating a branch - Delete merged branches - Keep your repository clean
git statusis your friend - Use it constantly- Feature branch workflow is industry standard - Learn it now, use it forever
You’re now ready to work like a professional developer!
In Chapter 02 (Automation and CI/CD), we’ll automate quality checks to make this workflow even more powerful.
13. Further Reading
Official Git Documentation:
- Git Basics - Getting a Git Repository
- Git Branching - Branches in a Nutshell
- Git Branching - Basic Branching and Merging
GitHub Documentation:
Interactive Learning:
- Learn Git Branching - Visual, interactive Git tutorial
- GitHub Skills - Hands-on GitHub learning
Industry Practices:
- A successful Git branching model (Git Flow)
- GitHub Flow (Simpler alternative)