Real-world Git Scenarios: Practical DevOps Use Cases
Learning Git Through Real Situations
Git tutorials often teach commands in isolation, but real-world development rarely follows a clean script. This guide presents 12 common scenarios you'll face in professional DevOps environments. Each scenario includes the full context, the exact commands used, the reasoning behind each step, and lessons learned. By working through these scenarios, you'll develop the judgment to handle any Git situation.
Core principle: In real-world Git, there's rarely one "right" answer. The best solution depends on your team's workflow, the urgency of the situation, and the impact on other developers. Always consider the human factor alongside the technical commands.
Scenario 1: Emergency Hotfix in Production
📱 The Situation
Context: Your e-commerce site has a critical bug in production—users can't complete checkout. The bug was introduced in the last release (v2.1.0), and you need to fix it immediately while maintaining ongoing development on the main branch.
Team workflow: You use GitFlow with main (production), develop (integration), and feature branches. The release tag v2.1.0 points to the current production code.
Goal: Create a fix, deploy it to production without including unfinished features from develop, and ensure the fix is merged back properly.
🔍 Step-by-Step Solution
$ git checkout v2.1.0
Note: switching to 'v2.1.0'. You are in 'detached HEAD' state.
# Create a branch to work on the fix
$ git checkout -b hotfix/checkout-error
# Step 2: Apply the fix
$ git add src/payment.js
$ git commit -m "hotfix: resolve null pointer in checkout flow"
# Step 3: Test locally, then deploy to production
$ git checkout main
$ git merge --no-ff hotfix/checkout-error -m "Merge hotfix v2.1.1"
$ git tag -a v2.1.1 -m "Hotfix release for checkout bug"
$ git push origin main --tags
# Step 4: Ensure fix is also merged into develop
$ git checkout develop
$ git merge --no-ff hotfix/checkout-error -m "Merge hotfix into develop"
$ git push origin develop
# Step 5: Clean up
$ git branch -d hotfix/checkout-error
📝 Explanation of Each Step:
- Creating from the tag (not main): Starting from v2.1.0 ensures the hotfix is based exactly on what's in production, not on any changes merged to main since the release.
- Detached HEAD is normal: Checking out a tag puts you in detached HEAD state—this is fine because we immediately create a branch.
- --no-ff on merges: This preserves the hotfix branch history, making it clear in the log that this was a hotfix.
- Merging to both main and develop: Critical step! The fix needs to be in both branches so it doesn't reappear later. If you skip merging to develop, the bug would return when you next deploy from develop.
- Tagging the release: Tags provide immutable references to exactly what was deployed, essential for debugging later.
⚠️ Critical Lesson: Always verify that hotfixes are merged into all active branches. Many teams have seen bugs reappear because the hotfix was applied to main but not develop, and the next release from develop reintroduced the bug.
Scenario 2: Abandoned Feature Branch Recovery
🗑️ The Situation
Context: A developer spent three weeks on a complex feature, then left the company. Their branch (feature/payment-overhaul) has 47 commits, but the work was never merged. Management now needs that feature for a client demo next week. The developer didn't push the branch regularly—it only exists on their laptop, which is now inaccessible.
Challenge: You need to recover the work from whatever remnants exist—maybe there's a local clone on another machine, or the commits were partially pushed.
🔍 Investigation and Recovery
$ git branch -a | grep feature/payment
remotes/origin/feature/payment-overhaul
# Good! The branch was pushed at least once.
$ git checkout -b recovery/payment-feature origin/feature/payment-overhaul
# Step 2: Check if the branch is complete
$ git log --oneline | head -10
$ git diff develop # See what's different from develop
# Step 3: If commits are missing, look for dangling commits
$ git fsck --lost-found | grep commit
dangling commit a1b2c3d4e5f678901234567890abcdef12345678
# Step 4: Examine each dangling commit
$ git show a1b2c3d4e5f678901234567890abcdef12345678
# Step 5: Recover relevant commits by cherry-picking
$ git checkout recovery/payment-feature
$ git cherry-pick a1b2c3d4e5f678901234567890abcdef12345678
# Step 6: If multiple commits, use rebase to reorder
$ git rebase -i HEAD~10
📝 Explanation of Recovery Strategy:
- Check remotes first: Always check if the branch was pushed—it's the easiest recovery path.
- Dangling commits: If the developer did local commits but never pushed, those commits might still exist in your local repository if you ever fetched from their machine or if they're in object database.
- git fsck: This command finds unreachable objects—commits that aren't referenced by any branch but still exist in the object database.
- Cherry-pick vs. merge: When recovering partial work, cherry-picking specific commits gives you control over what's included.
💡 Prevention: Implement a policy that all branches must be pushed to the remote daily, even if work-in-progress. Use branch protection rules to prevent accidental deletion of remote branches.
Scenario 3: Accidental Commit of Secrets
🔑 The Situation
Context: A developer accidentally committed a file containing AWS access keys and pushed to the remote repository. The commit happened 2 hours ago, and three other developers have already pulled the changes. The repository is private, but you must assume the secrets are compromised.
Timeline: 2:00 PM - Secret committed and pushed
2:15 PM - Developer realizes mistake
2:30 PM - You're called to handle the incident
⚠️ CRITICAL: First Action - Rotate Secrets NOW
Before touching Git, go to AWS/your service provider and rotate the exposed keys. The secret was public for 2 hours—assume it's compromised. Only after rotating secrets should you clean Git history.
🔍 Step-by-Step Emergency Response
# ... go to AWS console, revoke keys, generate new ones ...
# Step 2: Identify all commits containing the secret
$ git log --oneline -S "AKIA" --all
a1b2c3d Add configuration files
# Step 3: Use git filter-repo to purge the secret from history
# First, backup the repository
$ git clone --mirror . ../repo-backup
# Install git-filter-repo if needed
$ pip install git-filter-repo
# Remove the sensitive file from all history
$ git filter-repo --path config/aws-keys.json --invert-paths
# Step 4: Force push the clean history
$ git remote add origin [new-url] # Consider new URL
$ git push --force --all
$ git push --force --tags
# Step 5: Notify all developers
# They need to re-clone or reset their local repos
$ git fetch --all
$ git reset --hard origin/main
📝 Critical Lessons:
- Secrets first, Git second: Never spend time cleaning Git while secrets are still active. Rotate immediately.
- filter-repo vs. BFG: git filter-repo is the modern, recommended tool for history rewriting. It's faster and safer than older methods.
- Assume all clones are compromised: After force-pushing, every developer must reset their local repositories. Anyone who had the old history still has the secret locally.
- Consider new remote URL: If the repository was public or widely cloned, consider moving to a new repository URL entirely.
⚠️ Team Communication Template: "URGENT: A secret was accidentally committed. All developers must run: git fetch --all && git reset --hard origin/main. DO NOT push any work until you've done this. Any local branches based on the old history must be rebased. Contact me immediately if you have unpushed work."
Scenario 4: Release Branch with Last-Minute Changes
📦 The Situation
Context: You're preparing release v2.5.0. The release branch (release/v2.5.0) was created from develop two weeks ago. During testing, QA found several bugs that were fixed directly on the release branch. Now, just before release, marketing requests a last-minute copy change. Meanwhile, developers have merged new features into develop that shouldn't be in this release.
Goal: Apply the copy change, ensure all fixes are in develop, and create a clean release.
🔍 Release Management Workflow
$ git checkout release/v2.5.0
$ git add src/copy.js
$ git commit -m "release: update marketing copy for v2.5.0"
# Step 2: Ensure all bug fixes are in develop
$ git checkout develop
$ git merge --no-ff release/v2.5.0 -m "Merge release v2.5.0 fixes into develop"
# This brings in only the bug fixes, not the new features from develop
# Step 3: Finalize the release
$ git checkout main
$ git merge --no-ff release/v2.5.0 -m "Release v2.5.0"
$ git tag -a v2.5.0 -m "Production release v2.5.0"
$ git push origin main --tags
# Step 4: Clean up release branch
$ git branch -d release/v2.5.0
$ git push origin --delete release/v2.5.0
📝 Key Insights:
- Release branches isolate stabilization: By fixing bugs on the release branch, you keep develop free to continue with new features.
- Merging release into develop: Critical step to ensure bug fixes don't get lost. Without this, the fixes would only exist in the release branch and wouldn't be in future versions.
- No new features in release: Never merge develop into release at this stage—that would pull in unfinished features.
- Tagging releases: Tags provide immutable references. If you need to patch v2.5.0 later, you know exactly what was deployed.
Scenario 5: Monorepo Partial Deployment
🏗️ The Situation
Context: Your company uses a monorepo containing multiple services: api, web, mobile, and shared-libs. You need to set up CI/CD so that changes to a specific service only trigger deployment for that service, not the entire application. A developer changed a file in api/src/routes/users.js—only the API service should redeploy.
🔍 Solution: CI/CD with Path Filtering
name: Deploy API
on:
push:
paths:
- 'api/**'
- 'shared-libs/**' # API depends on shared libs
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for diff
- name: Check if API changed
id: changed
uses: tj-actions/changed-files@v35
with:
files: |
api/
shared-libs/
- name: Deploy API
if: steps.changed.outputs.any_changed == 'true'
run: ./deploy-api.sh
#!/bin/bash
# deploy-if-changed.sh
# Check what changed since last deploy
LAST_DEPLOY=$(git rev-parse HEAD^1)
CHANGED_FILES=$(git diff --name-only $LAST_DEPLOY HEAD)
# Check if API files changed
if echo "$CHANGED_FILES" | grep -q "^api/"; then
echo "API files changed - deploying"
./deploy-api.sh
else
echo "No API changes - skipping deployment"
fi
📝 Explanation:
- Path filtering in CI: GitHub Actions (and other CI systems) can trigger workflows based on which files changed.
- Dependency awareness: The API deployment also triggers when shared libraries change, because the API depends on them.
- Full history needed: To know what changed, CI needs more than a shallow clone—hence fetch-depth: 0.
- Conditional deployment: Even if the workflow triggers, the actual deploy step only runs if relevant files changed.
Scenario 6: Handling Large Files and Repository Size
📀 The Situation
Context: Your repository has grown to 5GB. Clones take 30 minutes, and git status is slow. Analysis shows that 3GB is from binary files (images, PDFs) that were added years ago and are still in history, even though they're no longer needed. You need to reduce repository size without losing code history.
🔍 Analysis and Cleanup
$ git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | grep "^blob" | sort -k3 -n -r | head -20
blob a1b2c3d 150000000 old/designs/master.psd
blob e4f5g6h 85000000 assets/logo.png
# Step 2: Find which commits introduced these files
$ git log --all --find-object=a1b2c3d
# Step 3: Use git filter-repo to remove the large files from history
# First, backup!
$ git clone --mirror . ../repo-backup
# Remove specific files from all history
$ git filter-repo --path old/designs/ --invert-paths
# Step 4: Move remaining large files to Git LFS
$ git lfs track "*.psd" "*.png" "*.pdf"
$ git add .gitattributes
$ git commit -m "chore: track binary files with LFS"
# Step 5: Force push the clean repository
$ git push --force --all
# Step 6: All developers must re-clone
# Notify team: "Repository history rewritten - please clone fresh"
📝 Important Considerations:
- Backup before rewriting: History rewriting is destructive. Always have a backup.
- Team coordination: Everyone must re-clone after force push. Plan a team-wide communication and possibly a code freeze.
- LFS for future binaries: After cleanup, use Git LFS to prevent future bloat.
- Consider shallow clones for CI: For CI/CD pipelines, use
git clone --depth 1to avoid downloading full history.
Scenario 7: Onboarding with Complex Git History
👋 The Situation
Context: A new developer joins your team. They're comfortable with Git basics but need to understand the project's complex branching history, find the right starting points for features, and learn team conventions. The repository has 10,000+ commits, multiple release branches, and years of history.
🔍 Onboarding Workflow
# Show main branches and their purpose
$ git branch -a | grep -E "main|develop|release"
# Show the commit graph to visualize workflow
$ git log --graph --oneline --decorate --all -50
# Step 2: Identify the right base for new features
# Most features branch from develop
$ git checkout develop
$ git pull origin develop
$ git checkout -b feature/onboarding-example
# Step 3: Show how to find relevant commits
# Find when a specific feature was added
$ git log --grep="payment integration" --oneline
# Find who worked on a file
$ git blame src/important-file.js
# Step 4: Demonstrate team conventions
# Show commit message format
$ git log --pretty=format:"%s" -5
# Show branch naming convention
$ git branch -a | grep feature/
# Step 5: Practice with safe scenarios
# Have them create a test branch, make changes, and create a PR
$ git checkout -b practice/test-pr
$ echo "test" > test.md
$ git add test.md
$ git commit -m "test: practice PR workflow"
$ git push origin practice/test-pr
📝 Onboarding Tips:
- Visualize the history: Use
git log --graphor GUI tools (GitKraken, Sourcetree) to help newcomers understand branch structure. - Create onboarding documentation: Document your team's Git workflow, branch naming, commit conventions, and common commands.
- Pair on first PR: Have an experienced developer pair with the newcomer on their first pull request.
- Safe sandbox: Create a practice repository where new team members can experiment without affecting real projects.
Scenario 8: Git Submodules in Microservices
🔌 The Situation
Context: Your team manages 15 microservices. There's a common library (shared-utils) used by all services. You need to share this library across repositories while maintaining version compatibility. You've chosen Git submodules as the solution.
🔍 Submodule Management Workflow
$ git submodule add git@github.com:company/shared-utils.git libs/shared
$ git commit -m "feat: add shared-utils as submodule"
# Step 2: Clone a repository with submodules
$ git clone --recurse-submodules git@github.com:company/service-auth.git
# Or after clone:
$ git submodule update --init --recursive
# Step 3: Update submodule to specific version
$ cd libs/shared
$ git checkout v2.1.0 # Pin to a specific tag
$ cd ../..
$ git add libs/shared
$ git commit -m "chore: update shared-utils to v2.1.0"
# Step 4: Work on the submodule and main repo together
# Make changes in submodule
$ cd libs/shared
$ git checkout -b feature/new-helper
$ # make changes, commit, push
# Update main repo to use the new submodule commit
$ cd ..
$ git add libs/shared
$ git commit -m "feat: integrate new helper from shared-utils"
# Step 5: When pulling changes that include submodule updates
$ git pull
$ git submodule update --init --recursive
📝 Submodule Best Practices:
- Pin to tags, not branches: Always reference specific tags or commits, not branch names, for reproducibility.
- Document submodule workflow: Submodules add complexity—document the commands everyone needs.
- Consider alternatives: For some teams, package managers (npm, Maven) or monorepos might be simpler than submodules.
- CI/CD considerations: Ensure CI pipelines initialize submodules correctly.
⚠️ Common Pitfall: Forgetting to run git submodule update after pulling can leave you with outdated submodule code. Create aliases or educate the team to always run git pull && git submodule update.
Scenario 9: Emergency Rollback in Production
⏪ The Situation
Context: A new deployment (v2.7.0) was released 10 minutes ago. Users are reporting errors. Monitoring shows a 50% error rate. You need to roll back immediately while the team investigates. The previous working version is v2.6.0.
🔍 Rollback Strategies
# Find the merge commit for v2.7.0
$ git log --oneline --merges -1
a1b2c3d Merge release/v2.7.0 into main
# Revert the merge commit (-m 1 specifies main as mainline)
$ git revert -m 1 a1b2c3d
$ git push origin main
# Strategy 2: Reset and force push (faster, but rewrites history)
# Only use if team can coordinate and history rewrite is acceptable
$ git checkout main
$ git reset --hard v2.6.0
$ git push --force-with-lease origin main
# Strategy 3: Create a hotfix branch from the last good tag
$ git checkout v2.6.0
$ git checkout -b hotfix/rollback-to-v2.6.0
$ # If any urgent fixes needed, add them here
$ git checkout main
$ git merge hotfix/rollback-to-v2.6.0
$ git tag -a v2.6.1 -m "Rollback to v2.6.0"
$ git push origin main --tags
📝 Choosing the Right Strategy:
- git revert is safest: It doesn't rewrite history, so other developers don't need to reset their local repositories. Good for shared branches.
- reset --hard is fastest: If you're certain no one else has pulled the bad commit, this works well. But if others have pulled, they'll have problems.
- Hotfix branch approach: Gives you flexibility to add emergency fixes during rollback.
💡 After Rollback: Once the emergency is handled, create a revert of the revert (or re-apply the fixes) to get back to a working state, then investigate what went wrong.
Scenario 10: Splitting a Monolith Repository
✂️ The Situation
Context: Your monolithic repository has grown unmanageable. You've decided to split it into three separate repositories: frontend, backend-api, and shared-libs. Each new repo needs to preserve the commit history for its files.
🔍 Repository Splitting with git filter-repo
$ git clone git@github.com:company/monolith.git split-frontend
$ cd split-frontend
# Step 2: Use filter-repo to keep only frontend files
$ git filter-repo --path frontend/ --path shared-libs/ --force
# This rewrites history to only include commits that touched these paths
# Step 3: Move files to root (if needed)
# If files were in frontend/, you might want them at root
$ git filter-repo --path-rename frontend/:''
# Step 4: Create new remote and push
$ git remote add origin git@github.com:company/frontend.git
$ git push -u origin main
# Step 5: Repeat for other components
$ git clone git@github.com:company/monolith.git split-backend
$ cd split-backend
$ git filter-repo --path backend/ --path shared-libs/ --force
$ git remote add origin git@github.com:company/backend-api.git
$ git push -u origin main
📝 Important Considerations:
- Preserve shared libraries: In this example, both frontend and backend need shared-libs. You'll have duplicate history in both repos—that's acceptable.
- Coordinate with team: Plan a "move day" when everyone stops working on the monolith.
- Update CI/CD: All pipelines need to point to new repositories.
- Consider submodules or package management: For shared code, use Git submodules or package managers after the split.
Scenario 11: Handling Multiple Release Versions
📚 The Situation
Context: Your software has multiple supported versions (v1.x, v2.x, v3.x). Customers on v1.x still receive security patches. You need to maintain release branches for each major version while continuing development on main.
🔍 Long-term Release Maintenance
$ git checkout -b release/v1.x v1.5.0 # From last v1 tag
$ git push -u origin release/v1.x
$ git checkout -b release/v2.x v2.3.0
$ git push -u origin release/v2.x
# Step 2: Apply security patch to v1.x
$ git checkout release/v1.x
$ git cherry-pick -x a1b2c3d # Security fix commit hash
$ git tag -a v1.5.1 -m "Security patch for v1.x"
$ git push origin release/v1.x --tags
# Step 3: Propagate fix to newer versions if applicable
$ git checkout release/v2.x
$ git cherry-pick -x a1b2c3d # Same fix for v2.x
$ git tag -a v2.3.1 -m "Security patch for v2.x"
$ git push origin release/v2.x --tags
# Step 4: Also merge to main for future releases
$ git checkout main
$ git cherry-pick -x a1b2c3d
$ git push origin main
📝 Release Management Best Practices:
- Cherry-pick with -x: The
-xflag adds a note in the commit message indicating where the commit came from, essential for traceability. - Tag every release: Tags like v1.5.1 make it clear exactly what was deployed.
- Document support policy: Be clear about which versions receive patches and for how long.
- Automate where possible: Script the cherry-picking process for security patches to reduce human error.
Scenario 12: Git Hooks for Code Quality
🔧 The Situation
Context: Your team struggles with inconsistent code style and accidentally committed secrets. You want to automate quality checks so that certain validations run before commits and pushes.
🔍 Implementing Git Hooks
#!/bin/sh
# Run linter on staged files
echo "Running linter..."
npx eslint --quiet $(git diff --cached --name-only --diff-filter=ACM | grep '\.jsx\?$')
if [ $? -ne 0 ]; then
echo "Linting failed. Commit aborted."
exit 1
fi
# Check for secrets (basic pattern matching)
if git diff --cached | grep -E "(AKIA|SECRET|PASSWORD)" > /dev/null; then
echo "Potential secret detected in staged changes"
exit 1
fi
# Server-side pre-receive hook (on Git server)
#!/bin/sh
# Check for merge commits in feature branches
while read oldrev newrev refname; do
if [[ $refname =~ ^refs/heads/feature/ ]]; then
if git rev-list --merges $oldrev..$newrev | grep .; then
echo "Feature branches should not contain merge commits"
exit 1
fi
fi
done
# package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm test"
}
},
"lint-staged": {
"*.js": ["eslint --fix", "git add"]
}
}
📝 Hook Implementation Strategies:
- Client-side vs. server-side: Client hooks can be bypassed (--no-verify). Server hooks are enforced. Use both for defense in depth.
- Speed matters: Pre-commit hooks should run quickly (under 2 seconds). Slow hooks frustrate developers.
- Hook managers: Tools like Husky make it easier to share hooks across the team.
- Gradual adoption: Start with informational hooks, then move to blocking ones as the team adapts.
Scenario Summary: Key Lessons
| Scenario | Key Takeaway |
|---|---|
| Emergency Hotfix | Always merge hotfixes into all active branches |
| Abandoned Feature | Use reflog and fsck to find lost commits |
| Accidental Secrets | Rotate secrets first, then clean history |
| Release Management | Release branches isolate stabilization from new development |
| Monorepo Deployment | Use path filtering for efficient CI/CD |
| Repository Size | Filter-repo and LFS manage large files |
| Submodules | Pin to tags, document workflow |
| Emergency Rollback | git revert is safer than reset for shared branches |
Frequently Asked Questions
The choice depends on your team's priorities:
- Use merge when: You want to preserve the exact history of when branches were integrated. Merge commits show the true timeline of development. Good for shared branches where multiple people collaborate.
- Use rebase when: You want a clean, linear history. Rebase makes feature branches look like they were developed sequentially. Good for preparing PRs and for local development before sharing.
- Real-world approach: Many teams rebase feature branches locally to keep them updated, then merge (with --no-ff) when integrating to main, giving a clean main history while preserving feature branch context.
For a team of 10, consider these factors:
- Trunk-based development: Short-lived feature branches, frequent merges to main. Works well with CI/CD and automated testing. Less overhead, fewer merge conflicts.
- GitHub Flow: Simple: main branch + feature branches. Everything goes through PRs. Good for continuous delivery.
- GitFlow (simplified): If you need formal releases, use main, develop, and release branches. But avoid the complexity of GitFlow unless you need multiple supported versions.
For most teams of 10 doing continuous delivery, trunk-based or GitHub Flow is sufficient and keeps things simple.
Database migrations require special care:
- Migration files in Git: Store migration scripts (e.g., Alembic, Flyway) in the repository, versioned with your code.
- Forward-only migrations: Design migrations to be forward-compatible. New code should work with old schema, and old code with new schema (during deployment).
- CI/CD pipeline: Run migrations before deploying new code, but ensure they're idempotent (can run multiple times safely).
- Rollback strategy: Have a plan for reverting migrations, or design them to be reversible.
- Zero-downtime pattern: For critical systems, use expand-migrate-contract pattern: add new schema, deploy code that uses both, migrate data, remove old schema.
Best practices for configuration management:
- Don't commit secrets: Never commit passwords, API keys, or tokens to Git.
- Use environment variables: Store configuration in environment variables, not files.
- Template files: Commit configuration templates (e.g., config.yml.template) with placeholders.
- External configuration: For complex setups, use configuration services (Consul, etcd) or secrets managers (Vault, AWS Secrets Manager).
- Environment-specific branches: Avoid this—it leads to drift. Use CI variables instead.
- Example pattern: Commit config/default.yml, and override with environment variables or mounted secrets in production.
For regulated industries (finance, healthcare):
- Signed commits: Require GPG signatures for all commits to prove authorship.
- Branch protection: Require PR approvals, no direct pushes to main.
- Audit trails: Use GitHub/GitLab audit logs to track who did what.
- Immutable history: Never force push to shared branches. Use revert instead of reset.
- Signed tags: Sign all release tags with GPG.
- Third-party audits: Ensure your Git hosting provider meets compliance standards (SOC2, HIPAA, etc.).
- Backup and retention: Maintain immutable backups of repositories for compliance retention periods.