Pular para o conteúdo principal

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-pick to apply a fix to multiple branches
  • Used git bisect to find a bug-introducing commit
  • Configured pre-commit and commit-msg hooks
  • 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.0 and v1.0.0 are visible on GitHub
  • A GitHub release exists for each tag
  • git cherry-pick successfully applied the fix to the maintenance branch
  • git bisect identified the bug-introducing commit
  • commit-msg hook rejects messages that don't follow Conventional Commits
  • git reflog was used to recover "lost" commits

Summary

You've mastered the advanced Git techniques used by senior developers:

  1. Tags and releases — Formalize versions with clear semantic versioning
  2. Cherry-pick — Surgically apply specific commits across branches
  3. Bisect — Find bugs in O(log n) time instead of O(n)
  4. Hooks — Automate quality checks for every developer on the team
  5. Reflog — The ultimate safety net for recovered "lost" work

Congratulations — you've completed the Git & GitHub course!