refactor: move modules into levels
This commit is contained in:
130
03_advanced/01-rebasing/README.md
Normal file
130
03_advanced/01-rebasing/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Module 07: Rebasing
|
||||
|
||||
## Learning Objectives
|
||||
|
||||
By the end of this module, you will:
|
||||
- Understand what rebasing is and how it works
|
||||
- Know the difference between merge and rebase
|
||||
- Perform a rebase to integrate changes from one branch to another
|
||||
- Understand when to use rebase vs merge
|
||||
- Know the golden rule of rebasing
|
||||
|
||||
## Challenge Description
|
||||
|
||||
You have a repository with a `main` branch and a `feature` branch. While you were working on the feature branch, new commits were added to `main`. You want to incorporate those changes into your feature branch while maintaining a clean, linear history.
|
||||
|
||||
Your task is to:
|
||||
1. Review the current commit history
|
||||
2. Rebase the `feature` branch onto `main`
|
||||
3. Verify that the history is now linear
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### What is Rebasing?
|
||||
|
||||
Rebasing is the process of moving or combining a sequence of commits to a new base commit. Instead of creating a merge commit like `git merge` does, rebasing rewrites the commit history by replaying your commits on top of another branch.
|
||||
|
||||
### Rebase vs Merge
|
||||
|
||||
**Merge:**
|
||||
```
|
||||
A---B---C feature
|
||||
/ \
|
||||
D---E---F---G main
|
||||
```
|
||||
Creates a merge commit (G) that ties the histories together.
|
||||
|
||||
**Rebase:**
|
||||
```
|
||||
A'--B'--C' feature
|
||||
/
|
||||
D---E---F main
|
||||
```
|
||||
Replays commits A, B, C on top of F, creating new commits A', B', C' with the same changes but different commit hashes.
|
||||
|
||||
### Benefits of Rebasing
|
||||
|
||||
- **Cleaner history**: Linear history is easier to read and understand
|
||||
- **Simpler log**: No merge commits cluttering the history
|
||||
- **Easier bisecting**: Finding bugs with `git bisect` is simpler with linear history
|
||||
|
||||
### The Golden Rule of Rebasing
|
||||
|
||||
**Never rebase commits that have been pushed to a public/shared repository.**
|
||||
|
||||
Why? Because rebasing rewrites history by creating new commits. If others have based work on the original commits, rebasing will cause serious problems for collaborators.
|
||||
|
||||
**Safe to rebase:**
|
||||
- Local commits not yet pushed
|
||||
- Feature branches you're working on alone
|
||||
- Cleaning up your work before creating a pull request
|
||||
|
||||
**Never rebase:**
|
||||
- Commits already pushed to a shared branch (like `main` or `develop`)
|
||||
- Commits that others might have based work on
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# Rebase current branch onto another branch
|
||||
git rebase <branch-name>
|
||||
|
||||
# View commit history as a graph
|
||||
git log --oneline --graph --all
|
||||
|
||||
# If conflicts occur during rebase:
|
||||
# 1. Resolve conflicts in files
|
||||
# 2. Stage the resolved files
|
||||
git add <file>
|
||||
# 3. Continue the rebase
|
||||
git rebase --continue
|
||||
|
||||
# Abort a rebase if something goes wrong
|
||||
git rebase --abort
|
||||
|
||||
# Check which branch you're on
|
||||
git branch
|
||||
|
||||
# Switch to a branch
|
||||
git switch <branch-name>
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Run the verification script to check your solution:
|
||||
|
||||
```bash
|
||||
.\verify.ps1
|
||||
```
|
||||
|
||||
The verification will check that:
|
||||
- You're on the feature branch
|
||||
- The rebase was completed successfully
|
||||
- The history is linear (no merge commits)
|
||||
- All commits from both branches are present
|
||||
|
||||
## Tips
|
||||
|
||||
- Always check your commit graph with `git log --oneline --graph --all` before and after rebasing
|
||||
- If you encounter conflicts during rebase, resolve them just like merge conflicts
|
||||
- Use `git rebase --abort` if you want to cancel the rebase and start over
|
||||
- Rebasing rewrites history, so the commit hashes will change
|
||||
- Only rebase local commits that haven't been shared with others
|
||||
|
||||
## When to Use Rebase vs Merge
|
||||
|
||||
**Use Rebase when:**
|
||||
- You want a clean, linear history
|
||||
- Working on a local feature branch that hasn't been shared
|
||||
- Updating your feature branch with the latest changes from main
|
||||
- You want to clean up commits before submitting a pull request
|
||||
|
||||
**Use Merge when:**
|
||||
- Working on a shared/public branch
|
||||
- You want to preserve the complete history including when branches diverged
|
||||
- You're merging a completed feature into main
|
||||
- You want to be safe and avoid rewriting history
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
Rebasing is a powerful tool for maintaining a clean project history. While merging is safer and preserves exact history, rebasing creates a more readable linear timeline. Understanding both techniques and knowing when to use each is essential for effective Git workflow management.
|
||||
22
03_advanced/01-rebasing/reset.ps1
Normal file
22
03_advanced/01-rebasing/reset.ps1
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resets the rebasing challenge environment.
|
||||
|
||||
.DESCRIPTION
|
||||
Removes the existing challenge directory and runs setup.ps1
|
||||
to create a fresh challenge environment.
|
||||
#>
|
||||
|
||||
Write-Host "Resetting challenge environment..." -ForegroundColor Yellow
|
||||
|
||||
# Remove existing challenge directory if present
|
||||
if (Test-Path "challenge") {
|
||||
Remove-Item -Path "challenge" -Recurse -Force
|
||||
Write-Host "Removed existing challenge directory." -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Run setup script
|
||||
Write-Host "Running setup script...`n" -ForegroundColor Cyan
|
||||
& ".\setup.ps1"
|
||||
123
03_advanced/01-rebasing/setup.ps1
Normal file
123
03_advanced/01-rebasing/setup.ps1
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sets up the rebasing challenge environment.
|
||||
|
||||
.DESCRIPTION
|
||||
Creates a Git repository with diverged branches to practice rebasing.
|
||||
The feature branch needs to be rebased onto main.
|
||||
#>
|
||||
|
||||
# Remove existing challenge directory if present
|
||||
if (Test-Path "challenge") {
|
||||
Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow
|
||||
Remove-Item -Path "challenge" -Recurse -Force
|
||||
}
|
||||
|
||||
# Create challenge directory
|
||||
Write-Host "Creating challenge environment..." -ForegroundColor Cyan
|
||||
New-Item -ItemType Directory -Path "challenge" | Out-Null
|
||||
Set-Location "challenge"
|
||||
|
||||
# Initialize git repository
|
||||
git init | Out-Null
|
||||
git config user.name "Workshop User" | Out-Null
|
||||
git config user.email "user@workshop.local" | Out-Null
|
||||
|
||||
# Create initial file and commit
|
||||
$readme = @"
|
||||
# My Project
|
||||
|
||||
A sample project for learning Git rebasing.
|
||||
"@
|
||||
|
||||
Set-Content -Path "README.md" -Value $readme
|
||||
git add README.md
|
||||
git commit -m "Initial commit" | Out-Null
|
||||
|
||||
# Create and switch to feature branch
|
||||
git checkout -b feature | Out-Null
|
||||
|
||||
# Add commits on feature branch
|
||||
$feature1 = @"
|
||||
# My Project
|
||||
|
||||
A sample project for learning Git rebasing.
|
||||
|
||||
## Features
|
||||
|
||||
- Feature A: User authentication
|
||||
"@
|
||||
|
||||
Set-Content -Path "README.md" -Value $feature1
|
||||
git add README.md
|
||||
git commit -m "Add feature A" | Out-Null
|
||||
|
||||
$feature2 = @"
|
||||
# My Project
|
||||
|
||||
A sample project for learning Git rebasing.
|
||||
|
||||
## Features
|
||||
|
||||
- Feature A: User authentication
|
||||
- Feature B: Data validation
|
||||
"@
|
||||
|
||||
Set-Content -Path "README.md" -Value $feature2
|
||||
git add README.md
|
||||
git commit -m "Add feature B" | Out-Null
|
||||
|
||||
# Switch back to main and add commits (simulating other work happening on main)
|
||||
git checkout main | Out-Null
|
||||
|
||||
$main1 = @"
|
||||
# My Project
|
||||
|
||||
A sample project for learning Git rebasing.
|
||||
|
||||
## Installation
|
||||
|
||||
Run \`npm install\` to install dependencies.
|
||||
"@
|
||||
|
||||
Set-Content -Path "README.md" -Value $main1
|
||||
git add README.md
|
||||
git commit -m "Add installation instructions" | Out-Null
|
||||
|
||||
$main2 = @"
|
||||
# My Project
|
||||
|
||||
A sample project for learning Git rebasing.
|
||||
|
||||
## Installation
|
||||
|
||||
Run \`npm install\` to install dependencies.
|
||||
|
||||
## Configuration
|
||||
|
||||
Copy \`config.example.json\` to \`config.json\` and update settings.
|
||||
"@
|
||||
|
||||
Set-Content -Path "README.md" -Value $main2
|
||||
git add README.md
|
||||
git commit -m "Add configuration instructions" | Out-Null
|
||||
|
||||
# Switch back to feature branch
|
||||
git checkout feature | Out-Null
|
||||
|
||||
# Return to module directory
|
||||
Set-Location ..
|
||||
|
||||
Write-Host "`n========================================" -ForegroundColor Green
|
||||
Write-Host "Challenge environment created!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "`nYou are now on the 'feature' branch." -ForegroundColor Cyan
|
||||
Write-Host "The 'main' branch has new commits that aren't in your feature branch." -ForegroundColor Cyan
|
||||
Write-Host "`nYour task:" -ForegroundColor Yellow
|
||||
Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White
|
||||
Write-Host "2. View the commit history: git log --oneline --graph --all" -ForegroundColor White
|
||||
Write-Host "3. Rebase the 'feature' branch onto 'main'" -ForegroundColor White
|
||||
Write-Host "4. View the history again to see the linear result" -ForegroundColor White
|
||||
Write-Host "`nRun '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan
|
||||
154
03_advanced/01-rebasing/verify.ps1
Normal file
154
03_advanced/01-rebasing/verify.ps1
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Verifies the rebasing challenge solution.
|
||||
|
||||
.DESCRIPTION
|
||||
Checks that the user successfully rebased the feature branch onto main,
|
||||
resulting in a clean, linear history.
|
||||
#>
|
||||
|
||||
Set-Location "challenge" -ErrorAction SilentlyContinue
|
||||
|
||||
# Check if challenge directory exists
|
||||
if (-not (Test-Path "../verify.ps1")) {
|
||||
Write-Host "Error: Please run this script from the module directory" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path ".")) {
|
||||
Write-Host "Error: Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Verifying your solution..." -ForegroundColor Cyan
|
||||
|
||||
# Check if git repository exists
|
||||
if (-not (Test-Path ".git")) {
|
||||
Write-Host "[FAIL] No git repository found." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check current branch
|
||||
$currentBranch = git branch --show-current 2>$null
|
||||
if ($currentBranch -ne "feature") {
|
||||
Write-Host "[FAIL] You should be on the 'feature' branch." -ForegroundColor Red
|
||||
Write-Host "Current branch: $currentBranch" -ForegroundColor Yellow
|
||||
Write-Host "Hint: Use 'git checkout feature' to switch to feature branch" -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if there's an ongoing rebase
|
||||
if (Test-Path ".git/rebase-merge") {
|
||||
Write-Host "[FAIL] Rebase is not complete. There may be unresolved conflicts." -ForegroundColor Red
|
||||
Write-Host "Hint: Resolve any conflicts, then use:" -ForegroundColor Yellow
|
||||
Write-Host " git add <file>" -ForegroundColor White
|
||||
Write-Host " git rebase --continue" -ForegroundColor White
|
||||
Write-Host "Or abort with: git rebase --abort" -ForegroundColor White
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get all commits on feature branch
|
||||
$featureCommits = git log --oneline feature 2>$null
|
||||
if (-not $featureCommits) {
|
||||
Write-Host "[FAIL] No commits found on feature branch." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check commit count (should be 5: 1 initial + 2 main + 2 feature)
|
||||
$commitCount = (git rev-list --count feature 2>$null)
|
||||
if ($commitCount -ne 5) {
|
||||
Write-Host "[FAIL] Expected 5 commits on feature branch, found $commitCount" -ForegroundColor Red
|
||||
Write-Host "Hint: The rebased feature branch should contain:" -ForegroundColor Yellow
|
||||
Write-Host " - 1 initial commit" -ForegroundColor White
|
||||
Write-Host " - 2 commits from main" -ForegroundColor White
|
||||
Write-Host " - 2 commits from feature" -ForegroundColor White
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get commit messages in reverse chronological order (newest first)
|
||||
$commits = git log --pretty=format:"%s" feature 2>$null
|
||||
|
||||
# Convert to array and reverse to get chronological order (oldest first)
|
||||
$commitArray = $commits -split "`n"
|
||||
[array]::Reverse($commitArray)
|
||||
|
||||
# Check that commits are in the expected order after rebase
|
||||
$expectedOrder = @(
|
||||
"Initial commit",
|
||||
"Add installation instructions",
|
||||
"Add configuration instructions",
|
||||
"Add feature A",
|
||||
"Add feature B"
|
||||
)
|
||||
|
||||
$orderCorrect = $true
|
||||
for ($i = 0; $i -lt $expectedOrder.Length; $i++) {
|
||||
if ($commitArray[$i] -ne $expectedOrder[$i]) {
|
||||
$orderCorrect = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $orderCorrect) {
|
||||
Write-Host "[FAIL] Commit order is incorrect after rebase." -ForegroundColor Red
|
||||
Write-Host "Expected order (oldest to newest):" -ForegroundColor Yellow
|
||||
$expectedOrder | ForEach-Object { Write-Host " $_" -ForegroundColor White }
|
||||
Write-Host "`nActual order:" -ForegroundColor Yellow
|
||||
$commitArray | ForEach-Object { Write-Host " $_" -ForegroundColor White }
|
||||
Write-Host "`nHint: The feature commits should come AFTER the main commits" -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that main branch still has only 3 commits (initial + 2 main commits)
|
||||
$mainCommitCount = (git rev-list --count main 2>$null)
|
||||
if ($mainCommitCount -ne 3) {
|
||||
Write-Host "[FAIL] Main branch should have 3 commits, found $mainCommitCount" -ForegroundColor Red
|
||||
Write-Host "Hint: You should rebase feature onto main, not the other way around" -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that there are no merge commits (parent count should be 1 for all commits)
|
||||
$mergeCommits = git log --merges --oneline feature 2>$null
|
||||
if ($mergeCommits) {
|
||||
Write-Host "[FAIL] Found merge commits in history. Rebasing should create a linear history." -ForegroundColor Red
|
||||
Write-Host "Hint: Use 'git rebase main' instead of 'git merge main'" -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Verify that feature branch contains all commits from main
|
||||
$mainCommits = git log --pretty=format:"%s" main 2>$null
|
||||
$featureCommitMessages = git log --pretty=format:"%s" feature 2>$null
|
||||
|
||||
foreach ($mainCommit in ($mainCommits -split "`n")) {
|
||||
if ($featureCommitMessages -notcontains $mainCommit) {
|
||||
Write-Host "[FAIL] Feature branch is missing commits from main." -ForegroundColor Red
|
||||
Write-Host "Missing: $mainCommit" -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Success!
|
||||
Write-Host "`n========================================" -ForegroundColor Green
|
||||
Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "`nYou have successfully:" -ForegroundColor Cyan
|
||||
Write-Host "- Rebased the feature branch onto main" -ForegroundColor White
|
||||
Write-Host "- Created a clean, linear commit history" -ForegroundColor White
|
||||
Write-Host "- Preserved all commits from both branches" -ForegroundColor White
|
||||
Write-Host "`nYour feature branch now has a linear history!" -ForegroundColor Green
|
||||
Write-Host "Run 'git log --oneline --graph --all' to see the result.`n" -ForegroundColor Cyan
|
||||
|
||||
Set-Location ..
|
||||
exit 0
|
||||
159
03_advanced/02-interactive-rebase/README.md
Normal file
159
03_advanced/02-interactive-rebase/README.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Module 08: Interactive Rebase
|
||||
|
||||
## Learning Objectives
|
||||
|
||||
By the end of this module, you will:
|
||||
- Understand what interactive rebase is and when to use it
|
||||
- Learn the different interactive rebase commands (pick, reword, squash, fixup, drop)
|
||||
- Clean up commit history by squashing related commits
|
||||
- Reword commit messages to be more descriptive
|
||||
- Use reset and commit techniques to achieve similar results
|
||||
|
||||
## Challenge Description
|
||||
|
||||
You have a feature branch with messy commit history - multiple "WIP" commits, typos in commit messages, and commits that should be combined. Before submitting a pull request, you want to clean up this history.
|
||||
|
||||
Your task is to:
|
||||
1. Review the current commit history with its messy commits
|
||||
2. Combine the four feature commits into a single, well-described commit
|
||||
3. Keep the initial commit unchanged
|
||||
4. End up with a clean, professional commit history
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### What is Interactive Rebase?
|
||||
|
||||
Interactive rebase (`git rebase -i`) is a powerful tool that lets you modify commit history. Unlike regular rebase which simply moves commits, interactive rebase lets you:
|
||||
- Reorder commits
|
||||
- Edit commit messages (reword)
|
||||
- Combine multiple commits (squash/fixup)
|
||||
- Split commits into smaller ones
|
||||
- Delete commits entirely
|
||||
- Edit the contents of commits
|
||||
|
||||
### Interactive Rebase Commands
|
||||
|
||||
When you run `git rebase -i HEAD~3`, Git opens an editor with a list of commits and commands:
|
||||
|
||||
```
|
||||
pick abc1234 First commit
|
||||
pick def5678 Second commit
|
||||
pick ghi9012 Third commit
|
||||
```
|
||||
|
||||
You can change `pick` to other commands:
|
||||
|
||||
- **pick**: Keep the commit as-is
|
||||
- **reword**: Keep the commit but edit its message
|
||||
- **edit**: Pause the rebase to amend the commit
|
||||
- **squash**: Combine this commit with the previous one, keeping both messages
|
||||
- **fixup**: Like squash, but discard this commit's message
|
||||
- **drop**: Remove the commit entirely
|
||||
|
||||
### When to Use Interactive Rebase
|
||||
|
||||
Interactive rebase is perfect for:
|
||||
- Cleaning up work-in-progress commits before creating a pull request
|
||||
- Fixing typos in commit messages
|
||||
- Combining related commits into logical units
|
||||
- Removing debug commits or experimental code
|
||||
- Creating a clean, professional history
|
||||
|
||||
**Remember**: Only rebase commits that haven't been pushed to a shared branch!
|
||||
|
||||
## Alternative Approach: Reset and Recommit
|
||||
|
||||
Since interactive rebase requires an interactive editor, this challenge uses an alternative approach that achieves the same result:
|
||||
|
||||
1. **Reset soft**: Move HEAD back while keeping changes staged
|
||||
```bash
|
||||
git reset --soft HEAD~4
|
||||
```
|
||||
This keeps all changes from the last 4 commits but "uncommits" them.
|
||||
|
||||
2. **Create a new commit**: Commit all changes with a clean message
|
||||
```bash
|
||||
git commit -m "Your new clean commit message"
|
||||
```
|
||||
|
||||
This technique is useful when you want to combine multiple commits into one without using interactive rebase.
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# View commit history
|
||||
git log --oneline
|
||||
|
||||
# See the last N commits in detail
|
||||
git log -n 5
|
||||
|
||||
# Reset to N commits back, keeping changes staged
|
||||
git reset --soft HEAD~N
|
||||
|
||||
# Reset to N commits back, keeping changes unstaged
|
||||
git reset --mixed HEAD~N
|
||||
|
||||
# Reset to N commits back, discarding all changes (DANGEROUS!)
|
||||
git reset --hard HEAD~N
|
||||
|
||||
# Create a new commit
|
||||
git commit -m "message"
|
||||
|
||||
# Amend the last commit
|
||||
git commit --amend -m "new message"
|
||||
|
||||
# Interactive rebase (requires interactive editor)
|
||||
git rebase -i HEAD~N
|
||||
|
||||
# Check current status
|
||||
git status
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Run the verification script to check your solution:
|
||||
|
||||
```bash
|
||||
.\verify.ps1
|
||||
```
|
||||
|
||||
The verification will check that:
|
||||
- You have exactly 2 commits total (initial + your combined feature commit)
|
||||
- The feature commit contains all the changes from the original 4 feature commits
|
||||
- The commit message is clean and descriptive
|
||||
- All expected files are present
|
||||
|
||||
## Challenge Steps
|
||||
|
||||
1. Navigate to the challenge directory
|
||||
2. View the current messy commit history: `git log --oneline`
|
||||
3. Use `git reset --soft HEAD~4` to uncommit the last 4 commits while keeping changes
|
||||
4. Check status with `git status` - you should see all changes staged
|
||||
5. Create a new commit with a clean message: `git commit -m "Add user profile feature with validation"`
|
||||
6. Verify with `git log --oneline` - you should now have clean history
|
||||
7. Run the verification script
|
||||
|
||||
## Tips
|
||||
|
||||
- Always check your history with `git log --oneline` before and after
|
||||
- `git reset --soft` is safer than `--hard` because it keeps your changes
|
||||
- You can use `git reset --soft HEAD~N` where N is the number of commits to undo
|
||||
- If you make a mistake, run `.\reset.ps1` to start over
|
||||
- In real projects with interactive rebase, you would use `git rebase -i HEAD~N` and modify the commit list in the editor
|
||||
|
||||
## What is Git Reset?
|
||||
|
||||
`git reset` moves the current branch pointer to a different commit:
|
||||
|
||||
- **--soft**: Moves HEAD but keeps changes staged
|
||||
- **--mixed** (default): Moves HEAD and unstages changes, but keeps them in working directory
|
||||
- **--hard**: Moves HEAD and discards all changes (dangerous!)
|
||||
|
||||
Reset is useful for:
|
||||
- Undoing commits while keeping changes (`--soft`)
|
||||
- Unstaging files (`--mixed`)
|
||||
- Discarding commits entirely (`--hard`)
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
Cleaning up commit history is an important skill for professional development. Whether you use interactive rebase or reset-and-recommit, the goal is the same: create a clear, logical history that makes your code review easier and your project history more understandable. Messy WIP commits are fine during development, but cleaning them up before merging shows attention to detail and makes your codebase more maintainable.
|
||||
22
03_advanced/02-interactive-rebase/reset.ps1
Normal file
22
03_advanced/02-interactive-rebase/reset.ps1
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resets the interactive rebase challenge environment.
|
||||
|
||||
.DESCRIPTION
|
||||
Removes the existing challenge directory and runs setup.ps1
|
||||
to create a fresh challenge environment.
|
||||
#>
|
||||
|
||||
Write-Host "Resetting challenge environment..." -ForegroundColor Yellow
|
||||
|
||||
# Remove existing challenge directory if present
|
||||
if (Test-Path "challenge") {
|
||||
Remove-Item -Path "challenge" -Recurse -Force
|
||||
Write-Host "Removed existing challenge directory." -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Run setup script
|
||||
Write-Host "Running setup script...`n" -ForegroundColor Cyan
|
||||
& ".\setup.ps1"
|
||||
133
03_advanced/02-interactive-rebase/setup.ps1
Normal file
133
03_advanced/02-interactive-rebase/setup.ps1
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sets up the interactive rebase challenge environment.
|
||||
|
||||
.DESCRIPTION
|
||||
Creates a Git repository with messy commit history that needs to be
|
||||
cleaned up using reset and recommit techniques.
|
||||
#>
|
||||
|
||||
# Remove existing challenge directory if present
|
||||
if (Test-Path "challenge") {
|
||||
Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow
|
||||
Remove-Item -Path "challenge" -Recurse -Force
|
||||
}
|
||||
|
||||
# Create challenge directory
|
||||
Write-Host "Creating challenge environment..." -ForegroundColor Cyan
|
||||
New-Item -ItemType Directory -Path "challenge" | Out-Null
|
||||
Set-Location "challenge"
|
||||
|
||||
# Initialize git repository
|
||||
git init | Out-Null
|
||||
git config user.name "Workshop User" | Out-Null
|
||||
git config user.email "user@workshop.local" | Out-Null
|
||||
|
||||
# Create initial commit
|
||||
$readme = @"
|
||||
# User Management System
|
||||
|
||||
A simple user management application.
|
||||
"@
|
||||
|
||||
Set-Content -Path "README.md" -Value $readme
|
||||
git add README.md
|
||||
git commit -m "Initial commit" | Out-Null
|
||||
|
||||
# Create messy commits that should be squashed
|
||||
|
||||
# Commit 1: WIP user profile
|
||||
$userProfile = @"
|
||||
class UserProfile:
|
||||
def __init__(self, name, email):
|
||||
self.name = name
|
||||
self.email = email
|
||||
"@
|
||||
|
||||
Set-Content -Path "user_profile.py" -Value $userProfile
|
||||
git add user_profile.py
|
||||
git commit -m "WIP: user profile" | Out-Null
|
||||
|
||||
# Commit 2: Add validation (typo in message)
|
||||
$userProfileWithValidation = @"
|
||||
class UserProfile:
|
||||
def __init__(self, name, email):
|
||||
self.name = name
|
||||
self.email = email
|
||||
|
||||
def validate(self):
|
||||
if not self.name or not self.email:
|
||||
raise ValueError('Name and email are required')
|
||||
return True
|
||||
"@
|
||||
|
||||
Set-Content -Path "user_profile.py" -Value $userProfileWithValidation
|
||||
git add user_profile.py
|
||||
git commit -m "add validaton" | Out-Null # Intentional typo
|
||||
|
||||
# Commit 3: Fix validation
|
||||
$userProfileFixed = @"
|
||||
class UserProfile:
|
||||
def __init__(self, name, email):
|
||||
self.name = name
|
||||
self.email = email
|
||||
|
||||
def validate(self):
|
||||
if not self.name or not self.email:
|
||||
raise ValueError('Name and email are required')
|
||||
if '@' not in self.email:
|
||||
raise ValueError('Invalid email format')
|
||||
return True
|
||||
"@
|
||||
|
||||
Set-Content -Path "user_profile.py" -Value $userProfileFixed
|
||||
git add user_profile.py
|
||||
git commit -m "fix validation bug" | Out-Null
|
||||
|
||||
# Commit 4: Add tests (another WIP commit)
|
||||
$tests = @"
|
||||
import unittest
|
||||
from user_profile import UserProfile
|
||||
|
||||
class TestUserProfile(unittest.TestCase):
|
||||
def test_validate_correct_user_data(self):
|
||||
user = UserProfile('John', 'john@example.com')
|
||||
self.assertTrue(user.validate())
|
||||
|
||||
def test_reject_missing_name(self):
|
||||
user = UserProfile('', 'john@example.com')
|
||||
with self.assertRaises(ValueError):
|
||||
user.validate()
|
||||
|
||||
def test_reject_invalid_email(self):
|
||||
user = UserProfile('John', 'invalid-email')
|
||||
with self.assertRaises(ValueError):
|
||||
user.validate()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
"@
|
||||
|
||||
Set-Content -Path "test_user_profile.py" -Value $tests
|
||||
git add test_user_profile.py
|
||||
git commit -m "WIP tests" | Out-Null
|
||||
|
||||
# Return to module directory
|
||||
Set-Location ..
|
||||
|
||||
Write-Host "`n========================================" -ForegroundColor Green
|
||||
Write-Host "Challenge environment created!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "`nYou have a repository with messy commit history:" -ForegroundColor Cyan
|
||||
Write-Host "- WIP commits" -ForegroundColor Yellow
|
||||
Write-Host "- Typos in commit messages" -ForegroundColor Yellow
|
||||
Write-Host "- Multiple commits that should be combined" -ForegroundColor Yellow
|
||||
Write-Host "`nYour task:" -ForegroundColor Yellow
|
||||
Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White
|
||||
Write-Host "2. View the messy history: git log --oneline" -ForegroundColor White
|
||||
Write-Host "3. Use 'git reset --soft HEAD~4' to uncommit the last 4 commits" -ForegroundColor White
|
||||
Write-Host "4. Create a single clean commit with a descriptive message" -ForegroundColor White
|
||||
Write-Host " Example: git commit -m 'Add user profile feature with validation'" -ForegroundColor White
|
||||
Write-Host "`nRun '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan
|
||||
172
03_advanced/02-interactive-rebase/verify.ps1
Normal file
172
03_advanced/02-interactive-rebase/verify.ps1
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Verifies the interactive rebase challenge solution.
|
||||
|
||||
.DESCRIPTION
|
||||
Checks that the user successfully cleaned up the commit history
|
||||
by combining multiple messy commits into a single clean commit.
|
||||
#>
|
||||
|
||||
Set-Location "challenge" -ErrorAction SilentlyContinue
|
||||
|
||||
# Check if challenge directory exists
|
||||
if (-not (Test-Path "../verify.ps1")) {
|
||||
Write-Host "Error: Please run this script from the module directory" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path ".")) {
|
||||
Write-Host "Error: Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Verifying your solution..." -ForegroundColor Cyan
|
||||
|
||||
# Check if git repository exists
|
||||
if (-not (Test-Path ".git")) {
|
||||
Write-Host "[FAIL] No git repository found." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check commit count (should be exactly 2: initial + combined feature commit)
|
||||
$commitCount = (git rev-list --count HEAD 2>$null)
|
||||
if ($commitCount -ne 2) {
|
||||
Write-Host "[FAIL] Expected exactly 2 commits, found $commitCount" -ForegroundColor Red
|
||||
if ($commitCount -gt 2) {
|
||||
Write-Host "Hint: You have too many commits. Use 'git reset --soft HEAD~N' to combine them." -ForegroundColor Yellow
|
||||
Write-Host " Where N is the number of commits to undo (should be 4 in this case)." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Hint: You may have reset too far back. Run ../reset.ps1 to start over." -ForegroundColor Yellow
|
||||
}
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get commit messages
|
||||
$commits = git log --pretty=format:"%s" 2>$null
|
||||
$commitArray = $commits -split "`n"
|
||||
|
||||
# Check that first commit is still the initial commit
|
||||
if ($commitArray[1] -ne "Initial commit") {
|
||||
Write-Host "[FAIL] The initial commit should remain unchanged." -ForegroundColor Red
|
||||
Write-Host "Expected first commit: 'Initial commit'" -ForegroundColor Yellow
|
||||
Write-Host "Found: '$($commitArray[1])'" -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that the second commit message is clean (not WIP, not with typos)
|
||||
$featureCommit = $commitArray[0]
|
||||
if ($featureCommit -match "WIP|wip") {
|
||||
Write-Host "[FAIL] Commit message still contains 'WIP'." -ForegroundColor Red
|
||||
Write-Host "Current message: '$featureCommit'" -ForegroundColor Yellow
|
||||
Write-Host "Hint: Create a clean, descriptive commit message." -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($featureCommit -match "validaton") {
|
||||
Write-Host "[FAIL] Commit message contains a typo ('validaton')." -ForegroundColor Red
|
||||
Write-Host "Current message: '$featureCommit'" -ForegroundColor Yellow
|
||||
Write-Host "Hint: Use a properly spelled commit message." -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that commit message is not empty and has reasonable length
|
||||
if ($featureCommit.Length -lt 10) {
|
||||
Write-Host "[FAIL] Commit message is too short. Be more descriptive." -ForegroundColor Red
|
||||
Write-Host "Current message: '$featureCommit'" -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that required files exist
|
||||
if (-not (Test-Path "user_profile.py")) {
|
||||
Write-Host "[FAIL] user_profile.py not found." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path "test_user_profile.py")) {
|
||||
Write-Host "[FAIL] test_user_profile.py not found." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that user_profile.py contains all expected features
|
||||
$userProfileContent = Get-Content "user_profile.py" -Raw
|
||||
|
||||
# Should have the class
|
||||
if ($userProfileContent -notmatch "class UserProfile") {
|
||||
Write-Host "[FAIL] user_profile.py should contain UserProfile class." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Should have validation method
|
||||
if ($userProfileContent -notmatch "def validate\(") {
|
||||
Write-Host "[FAIL] user_profile.py should contain validate() method." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Should have email format validation (the final fix from commit 3)
|
||||
if ($userProfileContent -notmatch "'@'.*in.*email|email.*in.*'@'") {
|
||||
Write-Host "[FAIL] user_profile.py should contain email format validation." -ForegroundColor Red
|
||||
Write-Host "Hint: Make sure all changes from all 4 commits are included." -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that test file has content
|
||||
$testContent = Get-Content "test_user_profile.py" -Raw
|
||||
|
||||
if ($testContent -notmatch "class.*TestUserProfile") {
|
||||
Write-Host "[FAIL] test_user_profile.py should contain TestUserProfile tests." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that we have at least 3 test cases
|
||||
$testMatches = ([regex]::Matches($testContent, "def test_")).Count
|
||||
if ($testMatches -lt 3) {
|
||||
Write-Host "[FAIL] test_user_profile.py should contain at least 3 test cases." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Verify that the latest commit contains changes to both files
|
||||
$filesInLastCommit = git diff-tree --no-commit-id --name-only -r HEAD 2>$null
|
||||
if ($filesInLastCommit -notcontains "user_profile.py") {
|
||||
Write-Host "[FAIL] The feature commit should include user_profile.py" -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($filesInLastCommit -notcontains "test_user_profile.py") {
|
||||
Write-Host "[FAIL] The feature commit should include test_user_profile.py" -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Success!
|
||||
Write-Host "`n========================================" -ForegroundColor Green
|
||||
Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "`nYou have successfully:" -ForegroundColor Cyan
|
||||
Write-Host "- Combined 4 messy commits into 1 clean commit" -ForegroundColor White
|
||||
Write-Host "- Created a descriptive commit message" -ForegroundColor White
|
||||
Write-Host "- Preserved all code changes" -ForegroundColor White
|
||||
Write-Host "- Cleaned up the commit history" -ForegroundColor White
|
||||
Write-Host "`nYour commit history is now clean and professional!" -ForegroundColor Green
|
||||
Write-Host "Run 'git log --oneline' to see the result.`n" -ForegroundColor Cyan
|
||||
Write-Host "Key takeaway: Clean commit history makes code review easier" -ForegroundColor Yellow
|
||||
Write-Host "and your project more maintainable.`n" -ForegroundColor Yellow
|
||||
|
||||
Set-Location ..
|
||||
exit 0
|
||||
208
03_advanced/03-worktrees/README.md
Normal file
208
03_advanced/03-worktrees/README.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Module 13: Worktrees
|
||||
|
||||
## Learning Objectives
|
||||
|
||||
By the end of this module, you will:
|
||||
- Understand what Git worktrees are and when to use them
|
||||
- Create and manage multiple working directories for the same repository
|
||||
- Work on multiple branches simultaneously
|
||||
- Understand the benefits of worktrees over stashing or cloning
|
||||
- Remove and clean up worktrees
|
||||
|
||||
## Challenge Description
|
||||
|
||||
You're working on a feature when an urgent bug report comes in. Instead of stashing your work or creating a separate clone, you'll use Git worktrees to work on both the feature and the bugfix simultaneously in different directories.
|
||||
|
||||
Your task is to:
|
||||
1. Create a worktree for the bugfix on a separate branch
|
||||
2. Fix the bug in the worktree
|
||||
3. Commit and verify the fix
|
||||
4. Continue working on your feature in the main working directory
|
||||
5. Clean up the worktree when done
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### What are Git Worktrees?
|
||||
|
||||
A worktree is an additional working directory attached to the same repository. Each worktree can have a different branch checked out, allowing you to work on multiple branches simultaneously without switching.
|
||||
|
||||
### Traditional Workflow vs Worktrees
|
||||
|
||||
**Traditional (switching branches):**
|
||||
```
|
||||
main-repo/
|
||||
- Switch to bugfix branch
|
||||
- Fix bug
|
||||
- Switch back to feature branch
|
||||
- Continue feature work
|
||||
- (Requires stashing or committing incomplete work)
|
||||
```
|
||||
|
||||
**With Worktrees:**
|
||||
```
|
||||
main-repo/ <- feature branch
|
||||
worktrees/bugfix/ <- bugfix branch
|
||||
|
||||
Work in both simultaneously!
|
||||
```
|
||||
|
||||
### Why Use Worktrees?
|
||||
|
||||
**Advantages:**
|
||||
- Work on multiple branches at the same time
|
||||
- No need to stash or commit incomplete work
|
||||
- Each worktree has its own working directory and index
|
||||
- Share the same Git history (one `.git` directory)
|
||||
- Faster than cloning the entire repository
|
||||
- Perfect for code reviews, comparisons, or parallel development
|
||||
|
||||
**Use Cases:**
|
||||
- Urgent bug fixes while working on a feature
|
||||
- Code reviews (checkout PR in separate worktree)
|
||||
- Comparing implementations side by side
|
||||
- Running tests on one branch while coding on another
|
||||
- Building different versions simultaneously
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# List all worktrees
|
||||
git worktree list
|
||||
|
||||
# Add a new worktree
|
||||
git worktree add <path> <branch>
|
||||
git worktree add ../bugfix bugfix-branch
|
||||
|
||||
# Create new branch in worktree
|
||||
git worktree add <path> -b <new-branch>
|
||||
git worktree add ../feature-new -b feature-new
|
||||
|
||||
# Remove a worktree
|
||||
git worktree remove <path>
|
||||
git worktree remove ../bugfix
|
||||
|
||||
# Prune stale worktree information
|
||||
git worktree prune
|
||||
|
||||
# Move a worktree
|
||||
git worktree move <old-path> <new-path>
|
||||
|
||||
# Lock a worktree (prevent deletion)
|
||||
git worktree lock <path>
|
||||
git worktree unlock <path>
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Run the verification script to check your solution:
|
||||
|
||||
```bash
|
||||
.\verify.ps1
|
||||
```
|
||||
|
||||
The verification will check that:
|
||||
- You created a worktree for the bugfix
|
||||
- The bug was fixed and committed in the worktree
|
||||
- Your feature work continued in the main directory
|
||||
- Both branches have the expected changes
|
||||
|
||||
## Challenge Steps
|
||||
|
||||
1. Navigate to the challenge directory
|
||||
2. You're in main-repo with a feature branch checked out
|
||||
3. View current worktrees: `git worktree list`
|
||||
4. Create a worktree for bugfix: `git worktree add ../bugfix-worktree -b bugfix`
|
||||
5. Navigate to the worktree: `cd ../bugfix-worktree`
|
||||
6. Fix the bug in calculator.js (fix the divide by zero check)
|
||||
7. Commit the fix: `git add . && git commit -m "Fix divide by zero bug"`
|
||||
8. Go back to main repo: `cd ../main-repo`
|
||||
9. Continue working on your feature
|
||||
10. Add a new method to calculator.js
|
||||
11. Commit your feature
|
||||
12. List worktrees: `git worktree list`
|
||||
13. Remove the worktree: `git worktree remove ../bugfix-worktree`
|
||||
14. Run verification
|
||||
|
||||
## Tips
|
||||
|
||||
- Worktree paths are typically siblings of your main repo (use `../worktree-name`)
|
||||
- Each worktree must have a different branch checked out
|
||||
- Can't checkout the same branch in multiple worktrees
|
||||
- The main `.git` directory is shared, so commits in any worktree are visible everywhere
|
||||
- Worktrees are listed in `.git/worktrees/`
|
||||
- Use `git worktree remove` to clean up, or just delete the directory and run `git worktree prune`
|
||||
- Worktrees persist across restarts until explicitly removed
|
||||
|
||||
## Common Worktree Workflows
|
||||
|
||||
### Urgent Bugfix
|
||||
```bash
|
||||
# Currently on feature branch with uncommitted changes
|
||||
git worktree add ../hotfix -b hotfix
|
||||
|
||||
cd ../hotfix
|
||||
# Fix the bug
|
||||
git add .
|
||||
git commit -m "Fix critical bug"
|
||||
git push origin hotfix
|
||||
|
||||
cd ../main-repo
|
||||
# Continue working on feature
|
||||
```
|
||||
|
||||
### Code Review
|
||||
```bash
|
||||
# Review a pull request without switching branches
|
||||
git fetch origin pull/123/head:pr-123
|
||||
git worktree add ../review-pr-123 pr-123
|
||||
|
||||
cd ../review-pr-123
|
||||
# Review code, test it
|
||||
# Run: npm test, npm start, etc.
|
||||
|
||||
cd ../main-repo
|
||||
git worktree remove ../review-pr-123
|
||||
```
|
||||
|
||||
### Parallel Development
|
||||
```bash
|
||||
# Work on two features simultaneously
|
||||
git worktree add ../feature-a -b feature-a
|
||||
git worktree add ../feature-b -b feature-b
|
||||
|
||||
# Terminal 1
|
||||
cd feature-a && code .
|
||||
|
||||
# Terminal 2
|
||||
cd feature-b && code .
|
||||
```
|
||||
|
||||
### Build Comparison
|
||||
```bash
|
||||
# Compare builds between branches
|
||||
git worktree add ../release-build release-v2.0
|
||||
|
||||
cd ../release-build
|
||||
npm run build
|
||||
# Test production build
|
||||
|
||||
# Meanwhile, continue development in main repo
|
||||
```
|
||||
|
||||
## Worktree vs Other Approaches
|
||||
|
||||
### vs Stashing
|
||||
- **Stash**: Temporary, one at a time, requires branch switching
|
||||
- **Worktree**: Persistent, multiple simultaneously, no switching
|
||||
|
||||
### vs Cloning
|
||||
- **Clone**: Full copy, separate `.git`, uses more disk space
|
||||
- **Worktree**: Shared `.git`, less disk space, instant sync
|
||||
|
||||
### vs Branch Switching
|
||||
- **Switching**: Requires clean working directory, one branch at a time
|
||||
- **Worktree**: Keep dirty working directory, multiple branches active
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
Git worktrees are a powerful but underutilized feature that can significantly improve your workflow. They eliminate the need for constant branch switching, stashing, or maintaining multiple clones. Whether you're handling urgent fixes, reviewing code, or comparing implementations, worktrees provide a clean and efficient solution. Once you understand worktrees, you'll find many situations where they're the perfect tool for the job.
|
||||
22
03_advanced/03-worktrees/reset.ps1
Normal file
22
03_advanced/03-worktrees/reset.ps1
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resets the worktrees challenge environment.
|
||||
|
||||
.DESCRIPTION
|
||||
Removes the existing challenge directory and runs setup.ps1
|
||||
to create a fresh challenge environment.
|
||||
#>
|
||||
|
||||
Write-Host "Resetting challenge environment..." -ForegroundColor Yellow
|
||||
|
||||
# Remove existing challenge directory if present
|
||||
if (Test-Path "challenge") {
|
||||
Remove-Item -Path "challenge" -Recurse -Force
|
||||
Write-Host "Removed existing challenge directory." -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Run setup script
|
||||
Write-Host "Running setup script...`n" -ForegroundColor Cyan
|
||||
& ".\setup.ps1"
|
||||
123
03_advanced/03-worktrees/setup.ps1
Normal file
123
03_advanced/03-worktrees/setup.ps1
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sets up the worktrees challenge environment.
|
||||
|
||||
.DESCRIPTION
|
||||
Creates a Git repository with a feature in progress, ready for
|
||||
demonstrating the use of worktrees for parallel work.
|
||||
#>
|
||||
|
||||
# Remove existing challenge directory if present
|
||||
if (Test-Path "challenge") {
|
||||
Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow
|
||||
Remove-Item -Path "challenge" -Recurse -Force
|
||||
}
|
||||
|
||||
# Create challenge directory
|
||||
Write-Host "Creating challenge environment..." -ForegroundColor Cyan
|
||||
New-Item -ItemType Directory -Path "challenge" | Out-Null
|
||||
Set-Location "challenge"
|
||||
|
||||
# Create main repository
|
||||
New-Item -ItemType Directory -Path "main-repo" | Out-Null
|
||||
Set-Location "main-repo"
|
||||
|
||||
# Initialize git repository
|
||||
git init | Out-Null
|
||||
git config user.name "Workshop User" | Out-Null
|
||||
git config user.email "user@workshop.local" | Out-Null
|
||||
|
||||
# Create initial calculator with a bug
|
||||
$calculator = @"
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
# BUG: No division by zero check!
|
||||
def divide(self, a, b):
|
||||
return a / b
|
||||
"@
|
||||
|
||||
Set-Content -Path "calculator.py" -Value $calculator
|
||||
git add calculator.py
|
||||
git commit -m "Initial calculator implementation" | Out-Null
|
||||
|
||||
$readme = @"
|
||||
# Calculator Project
|
||||
|
||||
A simple calculator with basic operations.
|
||||
|
||||
## Features
|
||||
- Addition
|
||||
- Subtraction
|
||||
- Multiplication
|
||||
- Division (has a bug!)
|
||||
"@
|
||||
|
||||
Set-Content -Path "README.md" -Value $readme
|
||||
git add README.md
|
||||
git commit -m "Add README" | Out-Null
|
||||
|
||||
# Create feature branch and start working on it
|
||||
git checkout -b feature-advanced-math | Out-Null
|
||||
|
||||
# Add work in progress on feature branch
|
||||
$calculatorWithFeature = @"
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
# BUG: No division by zero check!
|
||||
def divide(self, a, b):
|
||||
return a / b
|
||||
|
||||
# New feature: power function (work in progress)
|
||||
def power(self, a, b):
|
||||
return a ** b
|
||||
|
||||
# TODO: Add square root function
|
||||
# TODO: Add logarithm function
|
||||
"@
|
||||
|
||||
Set-Content -Path "calculator.py" -Value $calculatorWithFeature
|
||||
git add calculator.py
|
||||
git commit -m "Add power function (WIP: more math functions coming)" | Out-Null
|
||||
|
||||
# Return to challenge directory
|
||||
Set-Location ..
|
||||
|
||||
Write-Host "`n========================================" -ForegroundColor Green
|
||||
Write-Host "Challenge environment created!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "`nSituation:" -ForegroundColor Cyan
|
||||
Write-Host "You're working on the 'feature-advanced-math' branch" -ForegroundColor White
|
||||
Write-Host "You have plans to add more math functions (see TODOs)" -ForegroundColor White
|
||||
Write-Host "`nUrgent: A critical bug was discovered in the divide function!" -ForegroundColor Red
|
||||
Write-Host "It doesn't check for division by zero." -ForegroundColor Red
|
||||
Write-Host "`nInstead of stashing your feature work, use a worktree:" -ForegroundColor Yellow
|
||||
Write-Host "`nYour task:" -ForegroundColor Yellow
|
||||
Write-Host "1. Navigate to main-repo: cd challenge/main-repo" -ForegroundColor White
|
||||
Write-Host "2. Create a worktree: git worktree add ../bugfix-worktree -b bugfix" -ForegroundColor White
|
||||
Write-Host "3. Go to worktree: cd ../bugfix-worktree" -ForegroundColor White
|
||||
Write-Host "4. Fix the bug in calculator.py:" -ForegroundColor White
|
||||
Write-Host " Add a check: if (b === 0) throw new Error('Division by zero');" -ForegroundColor White
|
||||
Write-Host "5. Commit the fix: git add . && git commit -m 'Fix divide by zero bug'" -ForegroundColor White
|
||||
Write-Host "6. Return to main-repo: cd ../main-repo" -ForegroundColor White
|
||||
Write-Host "7. Complete your feature: Add square root method to calculator.py" -ForegroundColor White
|
||||
Write-Host "8. Commit: git add . && git commit -m 'Add square root function'" -ForegroundColor White
|
||||
Write-Host "9. Clean up worktree: git worktree remove ../bugfix-worktree" -ForegroundColor White
|
||||
Write-Host "`nRun '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan
|
||||
165
03_advanced/03-worktrees/verify.ps1
Normal file
165
03_advanced/03-worktrees/verify.ps1
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Verifies the worktrees challenge solution.
|
||||
|
||||
.DESCRIPTION
|
||||
Checks that the user successfully used worktrees to fix a bug
|
||||
while continuing work on a feature.
|
||||
#>
|
||||
|
||||
Set-Location "challenge" -ErrorAction SilentlyContinue
|
||||
|
||||
# Check if challenge directory exists
|
||||
if (-not (Test-Path "../verify.ps1")) {
|
||||
Write-Host "Error: Please run this script from the module directory" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path ".")) {
|
||||
Write-Host "Error: Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Verifying your solution..." -ForegroundColor Cyan
|
||||
|
||||
# Check if main-repo exists
|
||||
if (-not (Test-Path "main-repo")) {
|
||||
Write-Host "[FAIL] main-repo directory not found." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
Set-Location "main-repo"
|
||||
|
||||
# Check if it's a git repository
|
||||
if (-not (Test-Path ".git")) {
|
||||
Write-Host "[FAIL] main-repo is not a git repository." -ForegroundColor Red
|
||||
Set-Location ../..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if bugfix branch exists
|
||||
$branches = git branch --all 2>$null
|
||||
if ($branches -notmatch "bugfix") {
|
||||
Write-Host "[FAIL] bugfix branch not found." -ForegroundColor Red
|
||||
Write-Host "Hint: Create a worktree with: git worktree add ../bugfix-worktree -b bugfix" -ForegroundColor Yellow
|
||||
Set-Location ../..
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "[PASS] Bugfix branch exists!" -ForegroundColor Green
|
||||
|
||||
# Check bugfix branch for the fix
|
||||
git checkout bugfix 2>$null | Out-Null
|
||||
|
||||
if (-not (Test-Path "calculator.py")) {
|
||||
Write-Host "[FAIL] calculator.py not found on bugfix branch." -ForegroundColor Red
|
||||
Set-Location ../..
|
||||
exit 1
|
||||
}
|
||||
|
||||
$bugfixCalc = Get-Content "calculator.py" -Raw
|
||||
|
||||
# Check if division by zero check was added
|
||||
if ($bugfixCalc -notmatch "b === 0|b == 0|division by zero|divide by zero") {
|
||||
Write-Host "[FAIL] Division by zero check not found in bugfix branch." -ForegroundColor Red
|
||||
Write-Host "Hint: Add a check in the divide method to prevent division by zero" -ForegroundColor Yellow
|
||||
Set-Location ../..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check for commit on bugfix branch
|
||||
$bugfixCommits = git log --pretty=format:"%s" bugfix 2>$null
|
||||
if ($bugfixCommits -notmatch "bug|fix|division|divide") {
|
||||
Write-Host "[FAIL] No bugfix commit found on bugfix branch." -ForegroundColor Red
|
||||
Write-Host "Hint: Commit your fix with a descriptive message" -ForegroundColor Yellow
|
||||
Set-Location ../..
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "[PASS] Bug fixed on bugfix branch!" -ForegroundColor Green
|
||||
|
||||
# Check feature branch for continued work
|
||||
git checkout feature-advanced-math 2>$null | Out-Null
|
||||
|
||||
$featureCalc = Get-Content "calculator.py" -Raw
|
||||
|
||||
# Check if square root function was added
|
||||
if ($featureCalc -notmatch "sqrt|squareRoot") {
|
||||
Write-Host "[FAIL] Square root function not found on feature branch." -ForegroundColor Red
|
||||
Write-Host "Hint: Add the square root method to calculator.py on feature-advanced-math branch" -ForegroundColor Yellow
|
||||
Set-Location ../..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that feature work was committed
|
||||
$featureCommits = git log --pretty=format:"%s" feature-advanced-math 2>$null
|
||||
$featureCommitArray = $featureCommits -split "`n"
|
||||
|
||||
# Should have at least 3 commits: initial + README + power + sqrt
|
||||
if ($featureCommitArray.Count -lt 4) {
|
||||
Write-Host "[FAIL] Not enough commits on feature branch." -ForegroundColor Red
|
||||
Write-Host "Expected: initial, README, power, and square root commits" -ForegroundColor Yellow
|
||||
Write-Host "Found $($featureCommitArray.Count) commits" -ForegroundColor Yellow
|
||||
Set-Location ../..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if the latest feature commit is about square root
|
||||
if ($featureCommitArray[0] -notmatch "sqrt|square|root") {
|
||||
Write-Host "[FAIL] Latest commit on feature branch should be about square root." -ForegroundColor Red
|
||||
Write-Host "Latest commit: $($featureCommitArray[0])" -ForegroundColor Yellow
|
||||
Set-Location ../..
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "[PASS] Feature work completed!" -ForegroundColor Green
|
||||
|
||||
# Check if worktree was cleaned up (bugfix-worktree should not exist or be removed)
|
||||
Set-Location ..
|
||||
$worktreeStillExists = Test-Path "bugfix-worktree"
|
||||
|
||||
if ($worktreeStillExists) {
|
||||
Write-Host "[WARNING] bugfix-worktree directory still exists." -ForegroundColor Yellow
|
||||
Write-Host "Hint: Clean up with: git worktree remove ../bugfix-worktree" -ForegroundColor Yellow
|
||||
# Don't fail on this, just warn
|
||||
}
|
||||
|
||||
# Check worktree list
|
||||
Set-Location "main-repo"
|
||||
$worktrees = git worktree list 2>$null
|
||||
|
||||
# Verify that the concept was understood (they should have created the worktree at some point)
|
||||
# We can check this by looking for the bugfix branch existence
|
||||
if ($branches -notmatch "bugfix") {
|
||||
Write-Host "[FAIL] No evidence of worktree usage." -ForegroundColor Red
|
||||
Set-Location ../..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Success!
|
||||
Write-Host "`n========================================" -ForegroundColor Green
|
||||
Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "`nYou have successfully:" -ForegroundColor Cyan
|
||||
Write-Host "- Created a worktree for the bugfix" -ForegroundColor White
|
||||
Write-Host "- Fixed the division by zero bug" -ForegroundColor White
|
||||
Write-Host "- Committed the fix on the bugfix branch" -ForegroundColor White
|
||||
Write-Host "- Continued feature work in parallel" -ForegroundColor White
|
||||
Write-Host "- Added the square root function" -ForegroundColor White
|
||||
Write-Host "- Committed the feature work" -ForegroundColor White
|
||||
|
||||
if (-not $worktreeStillExists) {
|
||||
Write-Host "- Cleaned up the worktree" -ForegroundColor White
|
||||
}
|
||||
|
||||
Write-Host "`nYou now understand Git worktrees!" -ForegroundColor Green
|
||||
Write-Host "`nKey takeaway:" -ForegroundColor Yellow
|
||||
Write-Host "Worktrees let you work on multiple branches simultaneously" -ForegroundColor White
|
||||
Write-Host "without stashing, switching, or cloning the repository.`n" -ForegroundColor White
|
||||
|
||||
Set-Location ../..
|
||||
exit 0
|
||||
231
03_advanced/04-bisect/README.md
Normal file
231
03_advanced/04-bisect/README.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Module 14: Bisect - Finding Bugs with Binary Search
|
||||
|
||||
## Learning Objectives
|
||||
|
||||
By the end of this module, you will:
|
||||
- Understand what git bisect is and when to use it
|
||||
- Use binary search to find the commit that introduced a bug
|
||||
- Mark commits as good or bad during bisection
|
||||
- Automate bisect with test scripts
|
||||
- Understand the efficiency of binary search for debugging
|
||||
|
||||
## Challenge Description
|
||||
|
||||
A bug has appeared in your calculator application, but you don't know which commit introduced it. The project has many commits, and manually checking each one would take too long. You'll use `git bisect` to efficiently find the culprit commit using binary search.
|
||||
|
||||
Your task is to:
|
||||
1. Start a bisect session
|
||||
2. Mark the current commit as bad (bug exists)
|
||||
3. Mark an old commit as good (bug didn't exist)
|
||||
4. Test commits and mark them good or bad
|
||||
5. Let Git find the first bad commit
|
||||
6. Identify what change introduced the bug
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### What is Git Bisect?
|
||||
|
||||
Git bisect uses binary search to find the commit that introduced a bug. Instead of checking every commit linearly, it cuts the search space in half with each test, making it extremely efficient.
|
||||
|
||||
### Binary Search Efficiency
|
||||
|
||||
**Linear Search (manual checking):**
|
||||
- 100 commits = up to 100 tests
|
||||
- 1000 commits = up to 1000 tests
|
||||
|
||||
**Binary Search (bisect):**
|
||||
- 100 commits = ~7 tests
|
||||
- 1000 commits = ~10 tests
|
||||
|
||||
Formula: log₂(n) tests needed for n commits
|
||||
|
||||
### How Bisect Works
|
||||
|
||||
```
|
||||
Commits: A---B---C---D---E---F---G---H
|
||||
✓ ✓ ✓ ? ? ? ? ✗
|
||||
|
||||
1. Start: Mark H (bad) and A (good)
|
||||
2. Git checks middle: E
|
||||
3. You test E: bad ✗
|
||||
|
||||
Commits: A---B---C---D---E
|
||||
✓ ✓ ✓ ? ✗
|
||||
|
||||
4. Git checks middle: C
|
||||
5. You test C: good ✓
|
||||
|
||||
Commits: C---D---E
|
||||
✓ ? ✗
|
||||
|
||||
6. Git checks: D
|
||||
7. You test D: bad ✗
|
||||
|
||||
Result: D is the first bad commit!
|
||||
```
|
||||
|
||||
### When to Use Bisect
|
||||
|
||||
Use bisect when:
|
||||
- You know a bug exists now but didn't exist in the past
|
||||
- You have many commits to check
|
||||
- You can reliably test for the bug
|
||||
- You want to find exactly when something broke
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# Start bisect session
|
||||
git bisect start
|
||||
|
||||
# Mark current commit as bad
|
||||
git bisect bad
|
||||
|
||||
# Mark a commit as good
|
||||
git bisect good <commit-hash>
|
||||
git bisect good HEAD~10
|
||||
|
||||
# After testing current commit
|
||||
git bisect good # This commit is fine
|
||||
git bisect bad # This commit has the bug
|
||||
|
||||
# Skip a commit (if you can't test it)
|
||||
git bisect skip
|
||||
|
||||
# End bisect session and return to original state
|
||||
git bisect reset
|
||||
|
||||
# Visualize bisect process
|
||||
git bisect visualize
|
||||
git bisect view
|
||||
|
||||
# Automate with a test script
|
||||
git bisect run <test-script>
|
||||
git bisect run npm test
|
||||
git bisect run ./test.sh
|
||||
|
||||
# Show bisect log
|
||||
git bisect log
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Run the verification script to check your solution:
|
||||
|
||||
```bash
|
||||
.\verify.ps1
|
||||
```
|
||||
|
||||
The verification will check that:
|
||||
- You completed a bisect session
|
||||
- You identified the correct commit that introduced the bug
|
||||
- You understand which change caused the problem
|
||||
|
||||
## Challenge Steps
|
||||
|
||||
1. Navigate to the challenge directory
|
||||
2. View the bug: run the calculator and see it fails
|
||||
3. Start bisect: `git bisect start`
|
||||
4. Mark current as bad: `git bisect bad`
|
||||
5. Mark old commit as good: `git bisect good HEAD~10`
|
||||
6. Git will checkout a middle commit
|
||||
7. Test the current commit (run the test or check manually)
|
||||
8. Mark it: `git bisect good` or `git bisect bad`
|
||||
9. Repeat testing until Git identifies the bad commit
|
||||
10. Note the commit hash and message
|
||||
11. End bisect: `git bisect reset`
|
||||
12. Check the identified commit: `git show <bad-commit-hash>`
|
||||
13. Create a file named `bug-commit.txt` with the bad commit hash
|
||||
14. Run verification
|
||||
|
||||
## Tips
|
||||
|
||||
- Always start with a known good commit (far enough back)
|
||||
- Keep a clear way to test each commit (script or manual steps)
|
||||
- Use `git bisect log` to see your progress
|
||||
- `git bisect reset` returns you to your original state
|
||||
- You can bisect on any criteria, not just bugs (performance, features, etc.)
|
||||
- Automate with `git bisect run` for faster results
|
||||
- Each bisect step cuts remaining commits in half
|
||||
- Skip commits you can't build/test with `git bisect skip`
|
||||
|
||||
## Manual vs Automated Bisect
|
||||
|
||||
### Manual Bisect
|
||||
```bash
|
||||
git bisect start
|
||||
git bisect bad
|
||||
git bisect good HEAD~20
|
||||
|
||||
# For each commit Git checks out:
|
||||
npm test
|
||||
git bisect good # or bad
|
||||
|
||||
git bisect reset
|
||||
```
|
||||
|
||||
### Automated Bisect
|
||||
```bash
|
||||
git bisect start
|
||||
git bisect bad
|
||||
git bisect good HEAD~20
|
||||
git bisect run npm test
|
||||
# Git automatically tests each commit
|
||||
git bisect reset
|
||||
```
|
||||
|
||||
The test script should exit with:
|
||||
- 0 for good (test passes)
|
||||
- 1-127 (except 125) for bad (test fails)
|
||||
- 125 for skip (can't test this commit)
|
||||
|
||||
## Bisect Workflow Example
|
||||
|
||||
### Finding a Performance Regression
|
||||
```bash
|
||||
# App is slow now, was fast 50 commits ago
|
||||
git bisect start
|
||||
git bisect bad
|
||||
git bisect good HEAD~50
|
||||
|
||||
# Create test script
|
||||
echo '#!/bin/bash\ntime npm start | grep "Started in"' > test.sh
|
||||
chmod +x test.sh
|
||||
|
||||
git bisect run ./test.sh
|
||||
# Git finds the commit that made it slow
|
||||
```
|
||||
|
||||
### Finding When a Feature Broke
|
||||
```bash
|
||||
git bisect start
|
||||
git bisect bad
|
||||
git bisect good v1.0.0 # Last known good version
|
||||
|
||||
# For each commit
|
||||
npm test -- user-login.test.js
|
||||
git bisect good # or bad
|
||||
|
||||
# Found! Commit abc123 broke login
|
||||
git show abc123
|
||||
```
|
||||
|
||||
## Common Bisect Pitfalls
|
||||
|
||||
### Pitfall 1: Testing Incorrectly
|
||||
- Make sure your test is consistent
|
||||
- Automate when possible to avoid human error
|
||||
- Use the same test for every commit
|
||||
|
||||
### Pitfall 2: Wrong Good Commit
|
||||
- If the "good" commit actually has the bug, bisect will fail
|
||||
- Choose a commit you're confident was working
|
||||
|
||||
### Pitfall 3: Multiple Bugs
|
||||
- Bisect finds one commit at a time
|
||||
- If multiple bugs exist, they might confuse the search
|
||||
- Fix found bugs and bisect again for others
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
Git bisect is a powerful debugging tool that turns a tedious manual search into an efficient automated process. By leveraging binary search, you can quickly pinpoint problematic commits even in repositories with thousands of commits. This is invaluable for debugging regressions, performance issues, or any situation where something that worked before is now broken. Mastering bisect makes you a more effective debugger and shows deep Git proficiency.
|
||||
22
03_advanced/04-bisect/reset.ps1
Normal file
22
03_advanced/04-bisect/reset.ps1
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resets the bisect challenge environment.
|
||||
|
||||
.DESCRIPTION
|
||||
Removes the existing challenge directory and runs setup.ps1
|
||||
to create a fresh challenge environment.
|
||||
#>
|
||||
|
||||
Write-Host "Resetting challenge environment..." -ForegroundColor Yellow
|
||||
|
||||
# Remove existing challenge directory if present
|
||||
if (Test-Path "challenge") {
|
||||
Remove-Item -Path "challenge" -Recurse -Force
|
||||
Write-Host "Removed existing challenge directory." -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Run setup script
|
||||
Write-Host "Running setup script...`n" -ForegroundColor Cyan
|
||||
& ".\setup.ps1"
|
||||
311
03_advanced/04-bisect/setup.ps1
Normal file
311
03_advanced/04-bisect/setup.ps1
Normal file
@@ -0,0 +1,311 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sets up the bisect challenge environment.
|
||||
|
||||
.DESCRIPTION
|
||||
Creates a Git repository with multiple commits where a bug is
|
||||
introduced in one of them. Students use bisect to find it.
|
||||
#>
|
||||
|
||||
# Remove existing challenge directory if present
|
||||
if (Test-Path "challenge") {
|
||||
Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow
|
||||
Remove-Item -Path "challenge" -Recurse -Force
|
||||
}
|
||||
|
||||
# Create challenge directory
|
||||
Write-Host "Creating challenge environment..." -ForegroundColor Cyan
|
||||
New-Item -ItemType Directory -Path "challenge" | Out-Null
|
||||
Set-Location "challenge"
|
||||
|
||||
# Initialize git repository
|
||||
git init | Out-Null
|
||||
git config user.name "Workshop User" | Out-Null
|
||||
git config user.email "user@workshop.local" | Out-Null
|
||||
|
||||
# Commit 1: Initial calculator
|
||||
$calc1 = @"
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a + b
|
||||
"@
|
||||
Set-Content -Path "calculator.py" -Value $calc1
|
||||
git add calculator.py
|
||||
git commit -m "Initial calculator with add function" | Out-Null
|
||||
|
||||
# Commit 2: Add subtract
|
||||
$calc2 = @"
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
"@
|
||||
Set-Content -Path "calculator.py" -Value $calc2
|
||||
git add calculator.py
|
||||
git commit -m "Add subtract function" | Out-Null
|
||||
|
||||
# Commit 3: Add multiply
|
||||
$calc3 = @"
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
"@
|
||||
Set-Content -Path "calculator.py" -Value $calc3
|
||||
git add calculator.py
|
||||
git commit -m "Add multiply function" | Out-Null
|
||||
|
||||
# Commit 4: Add divide
|
||||
$calc4 = @"
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
def divide(self, a, b):
|
||||
if b == 0:
|
||||
raise ValueError('Division by zero')
|
||||
return a / b
|
||||
"@
|
||||
Set-Content -Path "calculator.py" -Value $calc4
|
||||
git add calculator.py
|
||||
git commit -m "Add divide function" | Out-Null
|
||||
|
||||
# Commit 5: Add modulo
|
||||
$calc5 = @"
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
def divide(self, a, b):
|
||||
if b == 0:
|
||||
raise ValueError('Division by zero')
|
||||
return a / b
|
||||
|
||||
def modulo(self, a, b):
|
||||
return a % b
|
||||
"@
|
||||
Set-Content -Path "calculator.py" -Value $calc5
|
||||
git add calculator.py
|
||||
git commit -m "Add modulo function" | Out-Null
|
||||
|
||||
# Commit 6: BUG - Introduce error in add function
|
||||
$calc6 = @"
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a - b # BUG: Should be a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
def divide(self, a, b):
|
||||
if b == 0:
|
||||
raise ValueError('Division by zero')
|
||||
return a / b
|
||||
|
||||
def modulo(self, a, b):
|
||||
return a % b
|
||||
"@
|
||||
Set-Content -Path "calculator.py" -Value $calc6
|
||||
git add calculator.py
|
||||
git commit -m "Refactor add function for clarity" | Out-Null
|
||||
|
||||
# Commit 7: Add power function (bug still exists)
|
||||
$calc7 = @"
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a - b # BUG: Should be a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
def divide(self, a, b):
|
||||
if b == 0:
|
||||
raise ValueError('Division by zero')
|
||||
return a / b
|
||||
|
||||
def modulo(self, a, b):
|
||||
return a % b
|
||||
|
||||
def power(self, a, b):
|
||||
return a ** b
|
||||
"@
|
||||
Set-Content -Path "calculator.py" -Value $calc7
|
||||
git add calculator.py
|
||||
git commit -m "Add power function" | Out-Null
|
||||
|
||||
# Commit 8: Add square root
|
||||
$calc8 = @"
|
||||
import math
|
||||
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a - b # BUG: Should be a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
def divide(self, a, b):
|
||||
if b == 0:
|
||||
raise ValueError('Division by zero')
|
||||
return a / b
|
||||
|
||||
def modulo(self, a, b):
|
||||
return a % b
|
||||
|
||||
def power(self, a, b):
|
||||
return a ** b
|
||||
|
||||
def sqrt(self, a):
|
||||
return math.sqrt(a)
|
||||
"@
|
||||
Set-Content -Path "calculator.py" -Value $calc8
|
||||
git add calculator.py
|
||||
git commit -m "Add square root function" | Out-Null
|
||||
|
||||
# Commit 9: Add absolute value
|
||||
$calc9 = @"
|
||||
import math
|
||||
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a - b # BUG: Should be a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
def divide(self, a, b):
|
||||
if b == 0:
|
||||
raise ValueError('Division by zero')
|
||||
return a / b
|
||||
|
||||
def modulo(self, a, b):
|
||||
return a % b
|
||||
|
||||
def power(self, a, b):
|
||||
return a ** b
|
||||
|
||||
def sqrt(self, a):
|
||||
return math.sqrt(a)
|
||||
|
||||
def abs(self, a):
|
||||
return abs(a)
|
||||
"@
|
||||
Set-Content -Path "calculator.py" -Value $calc9
|
||||
git add calculator.py
|
||||
git commit -m "Add absolute value function" | Out-Null
|
||||
|
||||
# Commit 10: Add max function
|
||||
$calc10 = @"
|
||||
import math
|
||||
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a - b # BUG: Should be a + b
|
||||
|
||||
def subtract(self, a, b):
|
||||
return a - b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
def divide(self, a, b):
|
||||
if b == 0:
|
||||
raise ValueError('Division by zero')
|
||||
return a / b
|
||||
|
||||
def modulo(self, a, b):
|
||||
return a % b
|
||||
|
||||
def power(self, a, b):
|
||||
return a ** b
|
||||
|
||||
def sqrt(self, a):
|
||||
return math.sqrt(a)
|
||||
|
||||
def abs(self, a):
|
||||
return abs(a)
|
||||
|
||||
def max(self, a, b):
|
||||
return a if a > b else b
|
||||
"@
|
||||
Set-Content -Path "calculator.py" -Value $calc10
|
||||
git add calculator.py
|
||||
git commit -m "Add max function" | Out-Null
|
||||
|
||||
# Create a test file
|
||||
$test = @"
|
||||
import sys
|
||||
from calculator import Calculator
|
||||
|
||||
calc = Calculator()
|
||||
|
||||
# Test addition (this will fail due to bug)
|
||||
result = calc.add(5, 3)
|
||||
if result != 8:
|
||||
print(f'FAIL: add(5, 3) returned {result}, expected 8')
|
||||
sys.exit(1)
|
||||
|
||||
print('PASS: All tests passed')
|
||||
sys.exit(0)
|
||||
"@
|
||||
Set-Content -Path "test.py" -Value $test
|
||||
|
||||
# Return to module directory
|
||||
Set-Location ..
|
||||
|
||||
Write-Host "`n========================================" -ForegroundColor Green
|
||||
Write-Host "Challenge environment created!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "`nSituation:" -ForegroundColor Cyan
|
||||
Write-Host "The calculator has a bug - addition doesn't work correctly!" -ForegroundColor Red
|
||||
Write-Host "calc.add(5, 3) returns 2 instead of 8" -ForegroundColor Red
|
||||
Write-Host "`nThe bug was introduced somewhere in the last 10 commits." -ForegroundColor Yellow
|
||||
Write-Host "Manually checking each commit would be tedious." -ForegroundColor Yellow
|
||||
Write-Host "Use git bisect to find it efficiently!" -ForegroundColor Green
|
||||
Write-Host "`nYour task:" -ForegroundColor Yellow
|
||||
Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White
|
||||
Write-Host "2. Test the bug: python test.py (it will fail)" -ForegroundColor White
|
||||
Write-Host "3. Start bisect: git bisect start" -ForegroundColor White
|
||||
Write-Host "4. Mark current as bad: git bisect bad" -ForegroundColor White
|
||||
Write-Host "5. Mark old commit as good: git bisect good HEAD~10" -ForegroundColor White
|
||||
Write-Host "6. Git will checkout a commit - test it: python test.py" -ForegroundColor White
|
||||
Write-Host "7. Mark result: git bisect good (if test passes) or git bisect bad (if it fails)" -ForegroundColor White
|
||||
Write-Host "8. Repeat until Git finds the first bad commit" -ForegroundColor White
|
||||
Write-Host "9. Note the commit hash" -ForegroundColor White
|
||||
Write-Host "10. End bisect: git bisect reset" -ForegroundColor White
|
||||
Write-Host "11. Create bug-commit.txt with the bad commit hash" -ForegroundColor White
|
||||
Write-Host "`nHint: The bug is in commit 6 ('Refactor add function for clarity')" -ForegroundColor Cyan
|
||||
Write-Host "Run '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan
|
||||
133
03_advanced/04-bisect/verify.ps1
Normal file
133
03_advanced/04-bisect/verify.ps1
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Verifies the bisect challenge solution.
|
||||
|
||||
.DESCRIPTION
|
||||
Checks that the user successfully used git bisect to find
|
||||
the commit that introduced the bug.
|
||||
#>
|
||||
|
||||
Set-Location "challenge" -ErrorAction SilentlyContinue
|
||||
|
||||
# Check if challenge directory exists
|
||||
if (-not (Test-Path "../verify.ps1")) {
|
||||
Write-Host "Error: Please run this script from the module directory" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path ".")) {
|
||||
Write-Host "Error: Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Verifying your solution..." -ForegroundColor Cyan
|
||||
|
||||
# Check if git repository exists
|
||||
if (-not (Test-Path ".git")) {
|
||||
Write-Host "[FAIL] No git repository found." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Make sure we're not in a bisect session
|
||||
$bisectHead = Test-Path ".git/BISECT_HEAD"
|
||||
if ($bisectHead) {
|
||||
Write-Host "[FAIL] You're still in a bisect session." -ForegroundColor Red
|
||||
Write-Host "Hint: End the bisect with: git bisect reset" -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if bug-commit.txt exists
|
||||
if (-not (Test-Path "bug-commit.txt")) {
|
||||
Write-Host "[FAIL] bug-commit.txt not found." -ForegroundColor Red
|
||||
Write-Host "Hint: After finding the bad commit, create a file with its hash:" -ForegroundColor Yellow
|
||||
Write-Host " echo 'commit-hash' > bug-commit.txt" -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Read the commit hash from the file
|
||||
$userCommit = (Get-Content "bug-commit.txt" -Raw).Trim()
|
||||
|
||||
if (-not $userCommit) {
|
||||
Write-Host "[FAIL] bug-commit.txt is empty." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get all commit hashes
|
||||
$allCommits = git log --pretty=format:"%H" --reverse 2>$null
|
||||
$commitArray = $allCommits -split "`n"
|
||||
|
||||
# The bug was introduced in commit 6 (index 5 in 0-based array)
|
||||
# This is the "Refactor add function for clarity" commit
|
||||
$badCommitMessage = git log --pretty=format:"%s" --grep="Refactor add function" 2>$null
|
||||
$actualBadCommit = git log --pretty=format:"%H" --grep="Refactor add function" 2>$null
|
||||
|
||||
if (-not $actualBadCommit) {
|
||||
Write-Host "[FAIL] Could not find the expected bad commit." -ForegroundColor Red
|
||||
Write-Host "Something may be wrong with the repository setup." -ForegroundColor Yellow
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if user found the correct commit
|
||||
if ($userCommit -ne $actualBadCommit) {
|
||||
# Maybe they provided a short hash
|
||||
if ($actualBadCommit -like "$userCommit*") {
|
||||
Write-Host "[PASS] Correct commit identified (using short hash)!" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[FAIL] Incorrect commit identified." -ForegroundColor Red
|
||||
Write-Host "You identified: $userCommit" -ForegroundColor Yellow
|
||||
Write-Host "Expected: $actualBadCommit" -ForegroundColor Yellow
|
||||
Write-Host "Expected commit message: 'Refactor add function for clarity'" -ForegroundColor Yellow
|
||||
Write-Host "`nHint: Use git bisect to find where the add function broke:" -ForegroundColor Yellow
|
||||
Write-Host " git bisect start" -ForegroundColor White
|
||||
Write-Host " git bisect bad" -ForegroundColor White
|
||||
Write-Host " git bisect good HEAD~10" -ForegroundColor White
|
||||
Write-Host " # Then test with: node test.py" -ForegroundColor White
|
||||
Write-Host " git bisect good # or bad" -ForegroundColor White
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "[PASS] Correct commit identified!" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Verify the commit actually has the bug
|
||||
git checkout $actualBadCommit 2>$null | Out-Null
|
||||
$calcContent = Get-Content "calculator.py" -Raw
|
||||
|
||||
if ($calcContent -notmatch "add\(a, b\)[\s\S]*?return a - b") {
|
||||
Write-Host "[WARNING] The identified commit doesn't seem to have the expected bug." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Return to latest commit
|
||||
git checkout $(git branch --show-current 2>$null) 2>$null | Out-Null
|
||||
if (-not $?) {
|
||||
git checkout main 2>$null | Out-Null
|
||||
}
|
||||
|
||||
# Success!
|
||||
Write-Host "`n========================================" -ForegroundColor Green
|
||||
Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "`nYou have successfully:" -ForegroundColor Cyan
|
||||
Write-Host "- Used git bisect to perform a binary search" -ForegroundColor White
|
||||
Write-Host "- Tested commits systematically" -ForegroundColor White
|
||||
Write-Host "- Identified the exact commit that introduced the bug" -ForegroundColor White
|
||||
Write-Host "- Found: '$badCommitMessage'" -ForegroundColor White
|
||||
Write-Host "`nThe bug was in commit 6 out of 10 commits." -ForegroundColor Yellow
|
||||
Write-Host "Manual checking: up to 10 tests" -ForegroundColor Yellow
|
||||
Write-Host "With bisect: only ~4 tests needed!" -ForegroundColor Green
|
||||
Write-Host "`nYou now understand git bisect!" -ForegroundColor Green
|
||||
Write-Host "`nKey takeaway:" -ForegroundColor Yellow
|
||||
Write-Host "Bisect uses binary search to efficiently find bugs," -ForegroundColor White
|
||||
Write-Host "saving massive amounts of time in large codebases.`n" -ForegroundColor White
|
||||
|
||||
Set-Location ..
|
||||
exit 0
|
||||
Reference in New Issue
Block a user