Skip to content

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 e4f5g6h

Key Learning

  • git reflog is 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 ccc888

Key Learning

  • Cherry-pick copies commits, doesn't move them
  • Original branch remains unchanged
  • Use -x flag 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 close

Alternative: 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 typo

Key Learning

  • squash combines commits and lets you edit the message
  • fixup combines commits and discards the message
  • reword lets you change just the commit message
  • drop removes 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 commit

Key Learning

  • Rebase replays your commits on top of another branch
  • Each commit may cause a separate conflict
  • Use git rebase --abort to cancel and return to original state
  • Use git rebase --skip to skip a commit entirely

Solution 5: The Bisect Investigation

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 reset

Automated 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 exit

Key Learning

  • git add -p is essential for clean commits
  • Each commit should be atomic (one logical change)
  • You can also use git reset -p to 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 fsck finds 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 main

Key Learning

  • --allow-unrelated-histories merges repos with no common ancestor
  • filter-repo is the modern replacement for filter-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 nothing

Key Learning

  • filter-repo is 10-100x faster than filter-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 commits

Alternative: 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 1 specifies 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 -e makes script exit on first error
  • Store rollback points before making changes
  • GIT_AUTHOR_DATE and GIT_COMMITTER_DATE control 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

SituationCommand
Lost commitsgit reflog
Extract specific commitsgit cherry-pick <hash>
Clean up historygit rebase -i HEAD~n
Find bug introductiongit bisect start
Stage partial changesgit add -p
Recover deleted stashgit fsck --unreachable
Merge unrelated repos--allow-unrelated-histories
Remove file from historygit filter-repo --path X --invert-paths
Recover detached commitsgit branch <name> <hash>
Revert mergegit revert -m 1 <merge-commit>
Atomic timestampsGIT_AUTHOR_DATE env var

Further Reading

Released under the MIT License.