Hands-on Lab TP6 — Advanced Git
Hands-on Lab 75 min Module 05
Objectives
By the end of this lab, you will have:
- Created annotated tags following SemVer
- Created a GitHub release with release notes
- Used
git cherry-pickto apply a fix to multiple branches - Used
git bisectto find a bug-introducing commit - Configured
pre-commitandcommit-msghooks - Recovered "lost" commits using
git reflog
Setup
mkdir git-tp6-advanced
cd git-tp6-advanced
git init
echo "*.pyc" > .gitignore
echo "__pycache__/" >> .gitignore
Create calculator.py:
"""Advanced calculator with history."""
history = []
def calculate(operation, a, b):
"""Perform a calculation and record it in history."""
operations = {
"add": lambda x, y: x + y,
"subtract": lambda x, y: x - y,
"multiply": lambda x, y: x * y,
"divide": lambda x, y: x / y if y != 0 else None,
}
if operation not in operations:
raise ValueError(f"Unknown operation: {operation}")
result = operations[operation](a, b)
history.append({"operation": operation, "a": a, "b": b, "result": result})
return result
def get_history():
"""Return calculation history."""
return history.copy()
def clear_history():
"""Clear calculation history."""
history.clear()
Create README.md:
# Advanced Calculator
A Python calculator with operation history.
## Usage
```python
from calculator import calculate, get_history
result = calculate("add", 3, 4) # 7
history = get_history()
```bash
git add .
git commit -m "feat: initial calculator with history"
gh repo create git-tp6-advanced --public
git push -u origin main
Part 1: Tags and Releases
Add the statistics function to the end of calculator.py:
def statistics():
"""Calculate statistics on history."""
if not history:
return {"count": 0}
results = [h["result"] for h in history if h["result"] is not None]
return {
"count": len(history),
"valid_results": len(results),
"min": min(results) if results else None,
"max": max(results) if results else None,
"average": sum(results) / len(results) if results else None,
}
git add calculator.py
git commit -m "feat: add statistics function"
git tag -a v0.1.0 -m "Version 0.1.0 - Initial beta release"
git show v0.1.0
git push --tags
Create a GitHub Release
gh release create v0.1.0 \
--title "v0.1.0 - Initial Beta Release" \
--notes "First beta release with basic arithmetic and statistics." \
--prerelease
gh release list
gh release view v0.1.0
Part 2: git cherry-pick
git switch -c maintenance/v0.1
git switch main
Add this broken function to the end of calculator.py:
def dangerous_function():
# This causes a critical error in production
return 1/0
git add calculator.py
git commit -m "feat: add new experimental function (BROKEN)"
Add this validation function to the end of calculator.py:
def validate_inputs(a, b, operation):
"""Validate inputs before calculation."""
if not isinstance(a, (int, float)):
raise TypeError(f"Expected number, got {type(a)}")
if not isinstance(b, (int, float)):
raise TypeError(f"Expected number, got {type(b)}")
if operation == "divide" and b == 0:
raise ValueError("Cannot divide by zero")
return True
git add calculator.py
git commit -m "fix: add input validation for all operations"
# Get the hash of the fix commit
FIX_HASH=$(git log --oneline -1 --format="%H")
echo "Fix commit hash: $FIX_HASH"
# Apply this fix to the maintenance branch WITHOUT the experimental function
git switch maintenance/v0.1
git cherry-pick $FIX_HASH
# Verify: only the fix is present (not the broken function)
cat calculator.py
git log --oneline
Part 3: git bisect
git switch main
git switch -c bisect-exercise
echo "version = '1.0'" > version.py
git add version.py
git commit -m "chore: add version file"
Add to the end of calculator.py:
def format_result(result, precision=2):
"""Format a result for display."""
if result is None:
return "Error"
return f"{result:.{precision}f}"
git add calculator.py
git commit -m "feat: add result formatting"
Replace version.py with this content (introduces the bug):
version = '1.1'
MAX_HISTORY = 0 # BUG: should be a positive number or None!
git add version.py
git commit -m "chore: add MAX_HISTORY config"
Add to the end of calculator.py:
def export_history_csv():
"""Export history as CSV string."""
if not history:
return "operation,a,b,result"
rows = ["operation,a,b,result"]
for h in history:
rows.append(f"{h['operation']},{h['a']},{h['b']},{h['result']}")
return "\n".join(rows)
git add calculator.py
git commit -m "feat: add CSV history export"
echo "# Advanced calculator" >> README.md
git add README.md
git commit -m "docs: update README"
git log --oneline
Create test_bug.sh with this content:
#!/bin/bash
# Exits 1 if MAX_HISTORY = 0 bug is present, 0 otherwise
if grep -q "MAX_HISTORY = 0" version.py 2>/dev/null; then
exit 1
fi
exit 0
chmod +x test_bug.sh
git bisect start
git bisect bad HEAD
git bisect good bisect-exercise~5
git bisect run ./test_bug.sh
git bisect reset
rm test_bug.sh
Part 4: Git Hooks Configuration
git switch main
pip install pre-commit
Create .pre-commit-config.yaml:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: detect-private-key
git add .pre-commit-config.yaml
git commit -m "chore: add pre-commit configuration"
pre-commit install
pre-commit install --hook-type commit-msg
Create .git/hooks/commit-msg with this content:
#!/bin/bash
COMMIT_MSG=$(cat "$1")
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf|ci|revert)(\(.+\))?: .{1,100}"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "Invalid commit message format!"
echo "Use: type(scope): description"
exit 1
fi
exit 0
chmod +x .git/hooks/commit-msg
# Test with a bad commit message (should be rejected)
echo "test" > test-file.txt
git add test-file.txt
git commit -m "this is wrong"
# Commit with correct format
git commit -m "test: add test file for hook validation"
rm test-file.txt
git add -A
git commit -m "chore: remove test file"
Part 5: Recover with Reflog
echo "important function 1" > important.py
git add important.py
git commit -m "feat: add important feature 1"
echo "important function 2" >> important.py
git add important.py
git commit -m "feat: add important feature 2"
# Oops! Accidentally reset 3 commits ago
git reset --hard HEAD~3
git log --oneline
cat important.py # File is gone!
# Use reflog to find the lost commits
git reflog
# Recover (adjust the number based on your reflog output)
git reset --hard HEAD@{2}
git log --oneline
cat important.py # File is back!
Part 6: Final Release
git switch main
git pull
git tag -a v1.0.0 -m "Version 1.0.0 - First Stable Release"
git push --tags
gh release create v1.0.0 \
--title "v1.0.0 - First Stable Release" \
--generate-notes
gh release view v1.0.0
Validation Checklist
- Tags
v0.1.0andv1.0.0are visible on GitHub - A GitHub release exists for each tag
-
git cherry-picksuccessfully applied the fix to the maintenance branch -
git bisectidentified the bug-introducing commit -
commit-msghook rejects messages that don't follow Conventional Commits -
git reflogwas used to recover "lost" commits
Summary
You've mastered the advanced Git techniques used by senior developers:
- Tags and releases — Formalize versions with clear semantic versioning
- Cherry-pick — Surgically apply specific commits across branches
- Bisect — Find bugs in O(log n) time instead of O(n)
- Hooks — Automate quality checks for every developer on the team
- Reflog — The ultimate safety net for recovered "lost" work
Congratulations — you've completed the Git & GitHub course!