Git Advanced Practical Lab - Set 1 Solutions
Warning: Only refer to this after attempting the questions yourself!
Solution 1: The Accidental Reset
Concept: Git Reflog
Git's reflog tracks every position HEAD has been at, even after destructive operations.
bash
# View the reflog to find your lost commits
git reflog
# You'll see something like:
# a1b2c3d HEAD@{0}: reset: moving to HEAD~5
# e4f5g6h HEAD@{1}: commit: test: add payment tests
# ...
# Reset to the commit before the accidental reset
git reset --hard HEAD@{1}
# Or use the specific commit hash from reflog
git reset --hard e4f5g6hKey Learning
git reflogis your safety net- Reflog entries expire after 90 days by default
- Works even for commits not on any branch
Solution 2: Surgical Commit Extraction
Concept: Cherry-Pick
bash
# First, note the commit hashes from develop
git log develop --oneline
# Let's say commits 3, 5, 8 have hashes: aaa333, bbb555, ccc888
# Switch to main
git checkout main
# Cherry-pick the specific commits in order
git cherry-pick aaa333
git cherry-pick bbb555
git cherry-pick ccc888
# Or do it in one command
git cherry-pick aaa333 bbb555 ccc888Key Learning
- Cherry-pick copies commits, doesn't move them
- Original branch remains unchanged
- Use
-xflag to add reference to original commit in message
Solution 3: The Messy History Cleanup
Concept: Interactive Rebase with Squash
bash
# Start interactive rebase for last 6 commits
git rebase -i HEAD~6
# In the editor, change the file to:
pick abc123 initial implementation
squash def456 forgot to add file
squash ghi789 actually fix the bug
squash jkl012 more WIP
squash mno345 WIP
squash pqr678 fix typo
# Save and close. In the next editor, write the new commit message:
feat: implement user authentication
# Save and closeAlternative: Fixup (discards commit messages)
bash
git rebase -i HEAD~6
pick abc123 initial implementation
fixup def456 forgot to add file
fixup ghi789 actually fix the bug
fixup jkl012 more WIP
fixup mno345 WIP
fixup pqr678 fix typoKey Learning
squashcombines commits and lets you edit the messagefixupcombines commits and discards the messagerewordlets you change just the commit messagedropremoves a commit entirely
Solution 4: The Diverged Branches
Concept: Rebase with Conflict Resolution
bash
# Switch to feature branch
git checkout feature/diverged
# Rebase onto main
git rebase main
# When conflicts occur, resolve them:
# Open the conflicted file (shared.txt)
# Keep both changes in order:
cat > shared.txt << 'EOF'
base
main line 1
main line 2
main line 3
feature line 1
feature line 2
feature line 3
feature line 4
EOF
# Stage the resolution
git add shared.txt
# Continue the rebase
git rebase --continue
# Repeat for each conflicting commitKey Learning
- Rebase replays your commits on top of another branch
- Each commit may cause a separate conflict
- Use
git rebase --abortto cancel and return to original state - Use
git rebase --skipto skip a commit entirely
Solution 5: The Bisect Investigation
Concept: Git Bisect (Binary Search)
bash
# Start bisect
git bisect start
# Mark current (latest) as bad
git bisect bad
# Mark a known good commit (before the bug)
git bisect good HEAD~20
# Git will checkout middle commit. Test it:
grep "/ 0" math.js && echo "BAD" || echo "GOOD"
# Mark accordingly
git bisect bad # or git bisect good
# Repeat until git identifies the culprit
# When found, git will show:
# "abc123 is the first bad commit"
# End bisect session
git bisect resetAutomated Bisect
bash
git bisect start HEAD HEAD~20
git bisect run sh -c 'grep "/ 0" math.js && exit 1 || exit 0'Key Learning
- Bisect uses binary search: O(log n) instead of O(n)
- Exit code 0 = good, non-zero = bad
- Exit code 125 = skip (can't test this commit)
Solution 6: The Partial Staging
Concept: Interactive/Patch Staging
bash
# Use patch mode to stage hunks interactively
git add -p app.js
# Git will show each change hunk. For the login function:
# Stage this hunk [y,n,q,a,d,s,e,?]?
# y = yes, stage this hunk
# n = no, skip this hunk
# s = split into smaller hunks
# e = manually edit the hunk
# Stage only the login function changes
# Type 'y' for login changes, 'n' for others
# Commit the first part
git commit -m "fix: validate login inputs"
# Stage and commit the rest
git add app.js
git commit -m "feat: implement logout and dashboard"Manual Hunk Editing
If hunks can't be split automatically:
bash
git add -p app.js
# When prompted, press 'e' to edit
# Remove lines you don't want to stage (delete the + lines)
# Save and exitKey Learning
git add -pis essential for clean commits- Each commit should be atomic (one logical change)
- You can also use
git reset -pto unstage hunks
Solution 7: The Lost Stash
Concept: Recovering Dangling Commits
bash
# Stashes are commits! Find dangling commits
git fsck --unreachable | grep commit
# Or specifically look for stash-like commits
git fsck --no-reflog | grep commit
# You'll see something like:
# unreachable commit abc123def456...
# Inspect each to find your stash
git show abc123def456
# Once found, recreate the stash or apply directly
git stash store -m "recovered stash" abc123def456
# Or just cherry-pick/checkout the work
git checkout abc123def456 -- .Alternative: Search reflog for stash operations
bash
git reflog | grep stash
# or
git log --oneline --all --decorate $(git fsck --no-reflog | awk '/commit/ {print $3}')Key Learning
- Git rarely truly deletes data immediately
git fsckfinds unreachable objects- Objects are garbage collected after
gc.pruneExpire(default 2 weeks)
Solution 8: The Multi-Root Merge
Concept: Subtree Merge with History
bash
cd repo-combined
# Add frontend as a remote and fetch
git remote add frontend ../repo-frontend
git fetch frontend
# Create a branch from frontend's history
git checkout -b frontend-branch frontend/main
# Move all files to frontend/ subdirectory while preserving history
git filter-repo --to-subdirectory-filter frontend/
# If filter-repo not available, use:
git filter-branch --tree-filter 'mkdir -p frontend && mv * frontend/ 2>/dev/null || true' HEAD
# Go back to main and merge
git checkout main
git merge frontend-branch --allow-unrelated-histories -m "Merge frontend repo"
# Repeat for backend
git remote add backend ../repo-backend
git fetch backend
git checkout -b backend-branch backend/main
git filter-repo --to-subdirectory-filter backend/
git checkout main
git merge backend-branch --allow-unrelated-histories -m "Merge backend repo"Alternative: Using subtree (simpler but different history)
bash
git subtree add --prefix=frontend ../repo-frontend main
git subtree add --prefix=backend ../repo-backend mainKey Learning
--allow-unrelated-historiesmerges repos with no common ancestorfilter-repois the modern replacement forfilter-branch- Subtree keeps history but in a different structure
Solution 9: The Commit Surgery
Concept: Rewriting History with filter-repo
bash
# Using git-filter-repo (recommended)
git filter-repo --path .env --invert-paths
# Using BFG Repo-Cleaner (alternative)
bfg --delete-files .env
# Using filter-branch (legacy, slower)
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch .env' \
--prune-empty --tag-name-filter cat -- --all
# Clean up
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Verify removal
git log --all --full-history -- .env
# Should return nothingKey Learning
filter-repois 10-100x faster thanfilter-branch- Always rotate secrets immediately after exposure
- Force push required after history rewrite
- All clones need to re-clone or reset
Solution 10: The Detached HEAD Recovery
Concept: Reflog and Branch Creation
bash
# Find the lost commits in reflog
git reflog
# Look for your experimental commits:
# abc1234 HEAD@{3}: commit: experiment: it works!
# def5678 HEAD@{4}: commit: experiment: try new approach
# ...
# Create a branch pointing to the last experimental commit
git branch feature/experiment abc1234
# Verify
git log feature/experiment --oneline
# Should show both experimental commitsAlternative: Using fsck
bash
# Find commits not reachable from any branch
git fsck --lost-found
# Check the commits in .git/lost-found/other/
ls .git/lost-found/other/Key Learning
- Detached HEAD commits aren't lost immediately
- Always create a branch before switching away in detached HEAD
- Reflog keeps references for ~90 days
Solution 11: The Selective Revert
Concept: Revert + Selective Cherry-pick
bash
# First, revert the merge (specify parent with -m 1)
git revert -m 1 HEAD
# -m 1 means keep main's side as the "mainline"
# Now cherry-pick or checkout specific files from the reverted branch
# Get the merge commit hash
MERGE_COMMIT=$(git rev-parse HEAD~1)
# Checkout only the files we want
git checkout $MERGE_COMMIT -- good.js another-good.js
# Commit the selective restore
git add good.js another-good.js
git commit -m "restore: bring back good changes from reverted merge"Alternative: Interactive revert
bash
# Revert with no auto-commit
git revert -m 1 HEAD --no-commit
# Unstage the files you want to keep
git reset HEAD good.js another-good.js
git checkout HEAD -- good.js another-good.js
# Now bad.js is staged for removal, good files are kept
git commit -m "revert: remove bad changes, keep good ones"Key Learning
-m 1specifies which parent to revert to (1 = main, 2 = feature)- Revert creates a new commit, doesn't delete history
- Can selectively checkout files from any commit
Solution 12: The Atomic Multi-Repo Update
Concept: Scripted Atomic Operations
bash
#!/bin/bash
set -e # Exit on any error
# Store original branch
ORIGINAL_BRANCH=$(git branch --show-current)
# Fixed timestamp for identical commits
export GIT_AUTHOR_DATE="2024-01-15T10:00:00"
export GIT_COMMITTER_DATE="2024-01-15T10:00:00"
# Branches to update
BRANCHES=("release/v1" "release/v2" "release/v3")
# Create the fix file
echo "security patch" > security-fix.js
# Store starting points for rollback
declare -A ROLLBACK_POINTS
for branch in "${BRANCHES[@]}"; do
ROLLBACK_POINTS[$branch]=$(git rev-parse $branch)
done
# Track success
SUCCESS=true
# Apply to each branch
for branch in "${BRANCHES[@]}"; do
git checkout "$branch"
cp ../security-fix.js . 2>/dev/null || cp security-fix.js .
git add security-fix.js
if ! git commit -m "security: patch CVE-2024-1234"; then
SUCCESS=false
break
fi
done
# Rollback if any failed
if [ "$SUCCESS" = false ]; then
echo "Rolling back all changes..."
for branch in "${BRANCHES[@]}"; do
git checkout "$branch"
git reset --hard "${ROLLBACK_POINTS[$branch]}"
done
echo "Rollback complete"
exit 1
fi
# Return to original branch
git checkout "$ORIGINAL_BRANCH"
echo "All branches updated successfully"Key Learning
set -emakes script exit on first error- Store rollback points before making changes
GIT_AUTHOR_DATEandGIT_COMMITTER_DATEcontrol timestamps- Always have a rollback strategy for multi-branch operations
Bonus: Complete Automation Script
bash
#!/bin/bash
# complete-lab-automation.sh
set -e
echo "=== Git Lab Automation ==="
# Create fresh repo
rm -rf git-lab-test && mkdir git-lab-test && cd git-lab-test
git init
echo "Setting up scenarios..."
# Setup and solve Question 1
echo "Q1: Reflog recovery..."
git checkout -b feature/payments 2>/dev/null || git checkout feature/payments
for i in {1..5}; do echo "v$i" > payment.js && git add . && git commit -m "payment $i"; done
LAST_GOOD=$(git rev-parse HEAD)
git reset --hard HEAD~5
git reset --hard $LAST_GOOD # Fix
[ "$(git log --oneline | wc -l)" -ge 5 ] && echo "✅ Q1 PASSED" || echo "❌ Q1 FAILED"
# Add more automated tests for other questions...
echo "=== Lab Complete ==="Quick Reference
| Situation | Command |
|---|---|
| Lost commits | git reflog |
| Extract specific commits | git cherry-pick <hash> |
| Clean up history | git rebase -i HEAD~n |
| Find bug introduction | git bisect start |
| Stage partial changes | git add -p |
| Recover deleted stash | git fsck --unreachable |
| Merge unrelated repos | --allow-unrelated-histories |
| Remove file from history | git filter-repo --path X --invert-paths |
| Recover detached commits | git branch <name> <hash> |
| Revert merge | git revert -m 1 <merge-commit> |
| Atomic timestamps | GIT_AUTHOR_DATE env var |