git-workflow
Git workflow automation with conventional commits, merge vs rebase decision trees, worktrees, SSH signing, and monorepo sparse checkout patterns.
Conventional Commit Format
# BAD: vague, no type
git commit -m "changes"
# GOOD: clear type, scope, imperative mood
git commit -m "feat(auth): add OAuth2 login flow"
git commit -m "fix(api): handle null response in getUserData"
git commit -m "docs(readme): update installation steps"
Format: type(scope): description
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
Breaking changes:
git commit -m "feat(api)!: remove deprecated v1 endpoints"
# Or in body:
git commit -m "feat(api): migrate to v2 endpoints
BREAKING CHANGE: v1 endpoints removed, use v2"
Setup Commit Linting
# Install commitlint
npm install --save-dev @commitlint/config-conventional @commitlint/cli
# Create config
cat > commitlint.config.js <<'EOF'
module.exports = { extends: ['@commitlint/config-conventional'] };
EOF
# Hook with Husky
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'
Merge vs Rebase Decision Tree
Question: Is the branch shared with others?
│
├─ YES → USE MERGE
│ └─ git merge feature-branch
│ (preserves branch history, safer for collaboration)
│
└─ NO → Ask: Do you want linear history?
│
├─ YES → USE REBASE
│ └─ git rebase main
│ (clean history, better for bisect)
│
└─ NO → USE MERGE
└─ git merge --no-ff feature-branch
(preserves feature branch context)
Rebase workflow for feature branches:
# Daily sync (before starting work)
git checkout feature-branch
git fetch origin
git rebase origin/main
# Interactive cleanup before PR
git rebase -i origin/main
# Squash "fix typo", "wip" commits
# After PR approval
git checkout main
git merge --ff-only feature-branch # or squash in GitHub UI
When rebase conflicts occur:
# BAD: panic and force push to main
git push --force origin main # NEVER DO THIS
# GOOD: resolve, test, continue
git rebase main
# Fix conflicts in files
git add resolved-file.js
git rebase --continue
npm test # Always test after resolving
git push --force-with-lease origin feature-branch
Abort and recover:
# Rebase went wrong? Abort it
git rebase --abort
# Already committed a bad rebase? Find previous state
git reflog
# Outputs: abc123 HEAD@{1}: rebase finished
# def456 HEAD@{2}: checkout: moving from main to feature
git reset --hard def456 # Go back before rebase
Git Worktrees (Multiple Branches Simultaneously)
# BAD: stashing and switching repeatedly
git stash
git checkout hotfix-branch
# fix bug
git checkout main
git stash pop
# GOOD: use worktrees
git worktree add ../myproject-hotfix hotfix-branch
cd ../myproject-hotfix
# work on hotfix while main branch untouched in ../myproject
# List all worktrees
git worktree list
# Remove when done
git worktree remove ../myproject-hotfix
Use case: review PR while keeping main branch clean:
git worktree add ../myproject-pr-review pr/123
cd ../myproject-pr-review
npm install
npm test
# Review done
cd ../myproject
git worktree remove ../myproject-pr-review
SSH Commit Signing Setup
# Generate signing key (Ed25519)
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/git_signing
# Add public key to GitHub: Settings → SSH and GPG keys → New SSH key
# Paste contents of ~/.ssh/git_signing.pub, select "Signing Key"
# Configure git to use SSH signing
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/git_signing.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# Test it
git commit --allow-empty -m "test: verify SSH signing"
git log --show-signature
# Should show: "Good signature with ED25519 key..."
Per-repository signing:
cd my-project
git config user.signingkey ~/.ssh/git_signing_work.pub # Different key for work
Monorepo with Sparse Checkout
Problem: Monorepo with 50 packages, you only work on 2.
# BAD: clone entire 5GB repo
git clone https://github.com/company/monorepo.git
# Downloads 5GB, slow npm install across all packages
# GOOD: sparse checkout
git clone --filter=blob:none --sparse https://github.com/company/monorepo.git
cd monorepo
git sparse-checkout init --cone
git sparse-checkout set packages/frontend packages/api
# Only these directories are checked out:
# packages/frontend/
# packages/api/
# Root files (package.json, etc.)
Add more packages later:
git sparse-checkout add packages/shared-ui
Full monorepo strategy:
# 1. Initialize sparse checkout
git clone --filter=blob:none --sparse https://github.com/company/monorepo.git
cd monorepo
# 2. Set up workspace
git sparse-checkout set packages/my-app libs/shared-utils
# 3. Install dependencies (only for checked out packages)
npm install # or pnpm/yarn
# 4. Configure CODEOWNERS (in repo root)
cat > .github/CODEOWNERS <<'EOF'
/packages/frontend/ @frontend-team
/packages/api/ @backend-team
/libs/shared-utils/ @platform-team
EOF
# 5. Tag releases per package
git tag @company/[email protected]
git tag @company/[email protected]
git push origin --tags
Merge Conflict Resolution
# List conflicted files
git diff --name-only --diff-filter=U
# BAD: manually edit without understanding
vim conflicted.js # Hope for the best
# GOOD: use strategy for simple conflicts
git checkout --theirs package-lock.json # Take their version
git checkout --ours config/local.json # Keep our version
git add package-lock.json config/local.json
# For complex conflicts, use mergetool
git mergetool --tool=vimdiff # or meld, kdiff3, vscode
Test after every conflict resolution:
git rebase --continue
npm test || (git rebase --abort && echo "Tests failed, fix conflicts")
Git Hooks with Lefthook
# Install lefthook (faster, polyglot alternative to Husky)
go install github.com/evilmartians/lefthook@latest
# Or: npm install lefthook --save-dev
# Create lefthook.yml
cat > lefthook.yml <<'EOF'
pre-commit:
parallel: true
commands:
lint:
glob: "*.{js,ts}"
run: eslint --fix {staged_files}
format:
glob: "*.{js,ts,json,md}"
run: prettier --write {staged_files}
types:
glob: "*.{ts,tsx}"
run: tsc --noEmit
commit-msg:
commands:
commitlint:
run: npx commitlint --edit {1}
pre-push:
commands:
test:
run: npm test
check-secrets:
run: gitleaks detect --no-git --verbose
EOF
# Install hooks
lefthook install
Branch Strategies
Trunk-Based Development (Recommended)
# 1. Create short-lived feature branch
git checkout -b feat/user-profile
# 2. Work in small increments (1-2 days max)
git commit -m "feat(profile): add avatar upload"
git commit -m "test(profile): add avatar validation tests"
# 3. Rebase daily
git fetch origin
git rebase origin/main
# 4. Open PR when ready (same day or next day)
gh pr create --title "Add user profile avatar upload"
# 5. Squash merge to main
gh pr merge --squash
Git Flow (Complex Release Cycles)
# Long-lived branches: main, develop
# Supporting: feature/*, release/*, hotfix/*
# Start feature
git checkout develop
git checkout -b feature/payment-integration
# Finish feature
git checkout develop
git merge --no-ff feature/payment-integration
git branch -d feature/payment-integration
# Create release branch
git checkout -b release/1.2.0 develop
# Fix bugs in release/1.2.0
# Merge to main and develop
git checkout main
git merge --no-ff release/1.2.0
git tag -a v1.2.0 -m "Release v1.2.0"
git checkout develop
git merge --no-ff release/1.2.0
git branch -d release/1.2.0
Only use Git Flow if:
- Multiple versions in production (e.g., SaaS with customer-specific deployments)
- Regulated industry requiring formal release process
- Large team (50+) with parallel release tracks
Troubleshooting
# Lost commits after reset
git reflog
git cherry-pick abc123 # Restore lost commit
# Committed to wrong branch
git log # Copy commit hash
git checkout correct-branch
git cherry-pick abc123
git checkout wrong-branch
git reset --hard HEAD~1 # Remove from wrong branch
# Remove file from history (leaked secret)
git filter-repo --path secrets.env --invert-paths
# Note: force push required, coordinate with team
# Undo last commit (keep changes staged)
git reset --soft HEAD~1
# Undo last commit (keep changes unstaged)
git reset HEAD~1
# Undo last commit (discard changes - CAREFUL)
git reset --hard HEAD~1