feat: add initital bisect module

This commit is contained in:
Bjarke Sporring
2026-01-04 17:53:59 +01:00
parent 4b7593b1e7
commit 8920af4b5c
4 changed files with 763 additions and 0 deletions

231
module-14-bisect/README.md Normal file
View 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.

View 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"

377
module-14-bisect/setup.ps1 Normal file
View File

@@ -0,0 +1,377 @@
#!/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 {
add(a, b) {
return a + b;
}
}
module.exports = Calculator;
"@
Set-Content -Path "calculator.js" -Value $calc1
git add calculator.js
git commit -m "Initial calculator with add function" | Out-Null
# Commit 2: Add subtract
$calc2 = @"
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
}
module.exports = Calculator;
"@
Set-Content -Path "calculator.js" -Value $calc2
git add calculator.js
git commit -m "Add subtract function" | Out-Null
# Commit 3: Add multiply
$calc3 = @"
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
}
module.exports = Calculator;
"@
Set-Content -Path "calculator.js" -Value $calc3
git add calculator.js
git commit -m "Add multiply function" | Out-Null
# Commit 4: Add divide
$calc4 = @"
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
}
module.exports = Calculator;
"@
Set-Content -Path "calculator.js" -Value $calc4
git add calculator.js
git commit -m "Add divide function" | Out-Null
# Commit 5: Add modulo
$calc5 = @"
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
modulo(a, b) {
return a % b;
}
}
module.exports = Calculator;
"@
Set-Content -Path "calculator.js" -Value $calc5
git add calculator.js
git commit -m "Add modulo function" | Out-Null
# Commit 6: BUG - Introduce error in add function
$calc6 = @"
class Calculator {
add(a, b) {
return a - b; // BUG: Should be a + b
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
modulo(a, b) {
return a % b;
}
}
module.exports = Calculator;
"@
Set-Content -Path "calculator.js" -Value $calc6
git add calculator.js
git commit -m "Refactor add function for clarity" | Out-Null
# Commit 7: Add power function (bug still exists)
$calc7 = @"
class Calculator {
add(a, b) {
return a - b; // BUG: Should be a + b
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
modulo(a, b) {
return a % b;
}
power(a, b) {
return Math.pow(a, b);
}
}
module.exports = Calculator;
"@
Set-Content -Path "calculator.js" -Value $calc7
git add calculator.js
git commit -m "Add power function" | Out-Null
# Commit 8: Add square root
$calc8 = @"
class Calculator {
add(a, b) {
return a - b; // BUG: Should be a + b
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
modulo(a, b) {
return a % b;
}
power(a, b) {
return Math.pow(a, b);
}
sqrt(a) {
return Math.sqrt(a);
}
}
module.exports = Calculator;
"@
Set-Content -Path "calculator.js" -Value $calc8
git add calculator.js
git commit -m "Add square root function" | Out-Null
# Commit 9: Add absolute value
$calc9 = @"
class Calculator {
add(a, b) {
return a - b; // BUG: Should be a + b
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
modulo(a, b) {
return a % b;
}
power(a, b) {
return Math.pow(a, b);
}
sqrt(a) {
return Math.sqrt(a);
}
abs(a) {
return Math.abs(a);
}
}
module.exports = Calculator;
"@
Set-Content -Path "calculator.js" -Value $calc9
git add calculator.js
git commit -m "Add absolute value function" | Out-Null
# Commit 10: Add max function
$calc10 = @"
class Calculator {
add(a, b) {
return a - b; // BUG: Should be a + b
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
modulo(a, b) {
return a % b;
}
power(a, b) {
return Math.pow(a, b);
}
sqrt(a) {
return Math.sqrt(a);
}
abs(a) {
return Math.abs(a);
}
max(a, b) {
return a > b ? a : b;
}
}
module.exports = Calculator;
"@
Set-Content -Path "calculator.js" -Value $calc10
git add calculator.js
git commit -m "Add max function" | Out-Null
# Create a test file
$test = @"
const Calculator = require('./calculator.js');
const calc = new Calculator();
// Test addition (this will fail due to bug)
const result = calc.add(5, 3);
if (result !== 8) {
console.log('FAIL: add(5, 3) returned ' + result + ', expected 8');
process.exit(1);
}
console.log('PASS: All tests passed');
process.exit(0);
"@
Set-Content -Path "test.js" -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: node test.js (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: node test.js" -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
module-14-bisect/verify.ps1 Normal file
View 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.js" -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.js" -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