feat: rebase module 8
This commit is contained in:
159
module-08-interactive-rebase/README.md
Normal file
159
module-08-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
module-08-interactive-rebase/reset.ps1
Normal file
22
module-08-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"
|
||||
143
module-08-interactive-rebase/setup.ps1
Normal file
143
module-08-interactive-rebase/setup.ps1
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/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 {
|
||||
constructor(name, email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
Set-Content -Path "user-profile.js" -Value $userProfile
|
||||
git add user-profile.js
|
||||
git commit -m "WIP: user profile" | Out-Null
|
||||
|
||||
# Commit 2: Add validation (typo in message)
|
||||
$userProfileWithValidation = @"
|
||||
class UserProfile {
|
||||
constructor(name, email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
validate() {
|
||||
if (!this.name || !this.email) {
|
||||
throw new Error('Name and email are required');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
Set-Content -Path "user-profile.js" -Value $userProfileWithValidation
|
||||
git add user-profile.js
|
||||
git commit -m "add validaton" | Out-Null # Intentional typo
|
||||
|
||||
# Commit 3: Fix validation
|
||||
$userProfileFixed = @"
|
||||
class UserProfile {
|
||||
constructor(name, email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
validate() {
|
||||
if (!this.name || !this.email) {
|
||||
throw new Error('Name and email are required');
|
||||
}
|
||||
if (!this.email.includes('@')) {
|
||||
throw new Error('Invalid email format');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
Set-Content -Path "user-profile.js" -Value $userProfileFixed
|
||||
git add user-profile.js
|
||||
git commit -m "fix validation bug" | Out-Null
|
||||
|
||||
# Commit 4: Add tests (another WIP commit)
|
||||
$tests = @"
|
||||
const assert = require('assert');
|
||||
const UserProfile = require('./user-profile');
|
||||
|
||||
describe('UserProfile', () => {
|
||||
it('should validate correct user data', () => {
|
||||
const user = new UserProfile('John', 'john@example.com');
|
||||
assert.strictEqual(user.validate(), true);
|
||||
});
|
||||
|
||||
it('should reject missing name', () => {
|
||||
const user = new UserProfile('', 'john@example.com');
|
||||
assert.throws(() => user.validate());
|
||||
});
|
||||
|
||||
it('should reject invalid email', () => {
|
||||
const user = new UserProfile('John', 'invalid-email');
|
||||
assert.throws(() => user.validate());
|
||||
});
|
||||
});
|
||||
"@
|
||||
|
||||
Set-Content -Path "user-profile.test.js" -Value $tests
|
||||
git add user-profile.test.js
|
||||
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
module-08-interactive-rebase/verify.ps1
Normal file
172
module-08-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.js")) {
|
||||
Write-Host "[FAIL] user-profile.js not found." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path "user-profile.test.js")) {
|
||||
Write-Host "[FAIL] user-profile.test.js not found." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that user-profile.js contains all expected features
|
||||
$userProfileContent = Get-Content "user-profile.js" -Raw
|
||||
|
||||
# Should have the class
|
||||
if ($userProfileContent -notmatch "class UserProfile") {
|
||||
Write-Host "[FAIL] user-profile.js should contain UserProfile class." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Should have validation method
|
||||
if ($userProfileContent -notmatch "validate\(\)") {
|
||||
Write-Host "[FAIL] user-profile.js should contain validate() method." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Should have email format validation (the final fix from commit 3)
|
||||
if ($userProfileContent -notmatch "includes\('@'\)") {
|
||||
Write-Host "[FAIL] user-profile.js 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 "user-profile.test.js" -Raw
|
||||
|
||||
if ($testContent -notmatch "describe.*UserProfile") {
|
||||
Write-Host "[FAIL] user-profile.test.js should contain UserProfile tests." -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that we have at least 3 test cases
|
||||
$testMatches = ([regex]::Matches($testContent, "it\(")).Count
|
||||
if ($testMatches -lt 3) {
|
||||
Write-Host "[FAIL] user-profile.test.js 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.js") {
|
||||
Write-Host "[FAIL] The feature commit should include user-profile.js" -ForegroundColor Red
|
||||
Set-Location ..
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($filesInLastCommit -notcontains "user-profile.test.js") {
|
||||
Write-Host "[FAIL] The feature commit should include user-profile.test.js" -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
|
||||
Reference in New Issue
Block a user