main ← Production. Protected. Only receives merges from dev.
└── dev ← Integration. All feature work merges here first.
├── feat/ ← New features
├── fix/ ← Bug fixes
├── refactor/
├── chore/
└── docs/
Golden rules:
- Never push directly to
mainordev. - Always work on a feature branch.
- Always rebase before opening a PR (keeps history linear).
- Always squash-merge feature branches into
dev(one commit per feature). - Always create a merge commit when merging
dev→main(preserves release boundary).
# Make sure dev is up to date
git checkout dev
git pull origin dev
# Create your feature branch
git checkout -b feat/user-authenticationBranch naming: <type>/<short-description>
| Type | Use |
|---|---|
feat/ |
New feature |
fix/ |
Bug fix |
refactor/ |
Code restructuring (no behavior change) |
chore/ |
Dependencies, tooling, config |
docs/ |
Documentation only |
perf/ |
Performance improvement |
test/ |
Adding or updating tests |
Make small, logical commits as you work:
# Stage specific files (not git add .)
git add src/features/auth/components/login-form.tsx
git add src/features/auth/api/use-login.ts
# Commit with conventional commit message
git commit -m "feat(auth): add login form with email/password"Commit message rules:
- Format:
<type>(<scope>): <summary> - Imperative mood: "add", not "added" or "adds"
- Lowercase, no period, max 72 chars
- Body (optional): explain why, not what
# Multi-line commit when you need a body
git commit -m "fix(convex): handle null cursor in pagination
The paginate() call returns null cursor on the last page.
Previously this caused a runtime error when attempting to
fetch the next page.
Closes #42"While working, dev may receive new merges. Stay updated with rebase (not merge):
# Fetch latest changes
git fetch origin
# Rebase your branch on top of latest dev
git rebase origin/devIf there are conflicts:
# Git will pause at the conflicting commit
# 1. Fix the conflicts in your editor
# 2. Stage the resolved files
git add <resolved-files>
# 3. Continue the rebase
git rebase --continue
# If things go wrong and you want to abort
git rebase --abortWhy rebase instead of merge?
- Rebase replays your commits on top of dev → linear history, no merge bubbles.
- Merge creates an extra merge commit and tangled history.
- Linear history is easier to read, bisect, and revert.
Before opening a PR, squash WIP/fixup commits into clean logical units:
# Squash last 3 commits into one (adjust number as needed)
git rebase -i HEAD~3This opens an editor. Change pick to squash (or s) for commits you want to fold:
pick abc1234 feat(auth): add login form skeleton
squash def5678 wip: styling tweaks
squash ghi9012 fix typo in login form
Save and close. Git will let you write a new combined commit message:
feat(auth): add login form with email/password validation
Common interactive rebase commands:
| Command | What it does |
|---|---|
pick |
Keep the commit as-is |
squash |
Merge into previous commit |
fixup |
Like squash but discard this message |
reword |
Keep commit, edit the message |
drop |
Delete the commit entirely |
# First push (set upstream)
git push -u origin feat/user-authentication
# Subsequent pushes
git push
# After a rebase, you need force push (your branch only!)
git push --force-with-lease--force-with-lease vs --force:
--force-with-leaseis safer — it fails if someone else pushed to your branch.--forceoverwrites blindly. Never use on shared branches.- Never force push to
devormain.
Open the PR:
gh pr create --base dev --title "feat(auth): add login form" --body "## Summary
- Added email/password login form
- Connected to Convex auth backend
- Added form validation with error states
## Test Plan
- [ ] Login with valid credentials
- [ ] Login with invalid email shows error
- [ ] Login with wrong password shows error
- [ ] Empty form shows validation messages"After approval, squash merge into dev:
# Via GitHub CLI
gh pr merge --squash --delete-branchOr use the GitHub UI: select "Squash and merge" from the merge dropdown.
Why squash merge?
- One clean commit per feature in
dev. - Internal WIP commits don't pollute the history.
- Easy to revert an entire feature with one
git revert.
# Switch back to dev
git checkout dev
# Pull the squash-merged commit
git pull origin dev
# Delete the local feature branch
git branch -d feat/user-authentication
# Prune remote tracking branches that were deleted
git fetch --pruneWhen dev is stable and ready for production:
# Make sure dev is up to date
git checkout dev
git pull origin dev
# Create a release PR (merge commit, not squash)
gh pr create --base main --title "release: v0.2.0" --body "## Summary
- feat(auth): add login/signup flow
- fix(dashboard): loading state on slow connections
- refactor(convex): optimize query indexes
## Test Plan
- [ ] Full regression test on staging
- [ ] Verify all new features work end-to-end"Merge with a merge commit (not squash):
gh pr merge --mergeWhy merge commit for releases?
- Preserves the individual feature commits in main's history.
- The merge commit marks a clear release boundary.
- Easy to see what was included in each release.
After merging, tag the release:
git checkout main
git pull origin main
git tag -a v0.2.0 -m "Release v0.2.0"
git push origin v0.2.0| Merge Into | Strategy | Why |
|---|---|---|
dev ← feature branch |
Squash merge | One clean commit per feature |
main ← dev |
Merge commit | Preserves history, marks release boundary |
# Undo the last commit but keep changes staged
git reset --soft HEAD~1
# Create the proper branch
git checkout -b feat/my-feature
# Commit there
git commit -m "feat: the thing I was working on"Don't merge the other branch. Cherry-pick the specific commit:
git cherry-pick <commit-hash>Or if you truly need the whole branch, rebase onto it (but this is rare and should be discussed with the team).
If a rebase is painful, you can squash first then rebase:
# Squash all your commits into one
git reset --soft origin/dev
git commit -m "feat(auth): my feature (squashed)"
# Now rebase — only one commit to resolve conflicts for
git rebase origin/devSince we squash-merge, each feature is one commit in dev:
git checkout dev
git pull origin dev
# Revert the specific feature commit
git revert <commit-hash>
git push origin dev# Branch from main (not dev)
git checkout main
git pull origin main
git checkout -b fix/critical-bug
# Fix, commit, push
git commit -m "fix: prevent crash on null user session"
git push -u origin fix/critical-bug
# PR into main directly
gh pr create --base main --title "fix: prevent crash on null user session"
# After merge, backport to dev
git checkout dev
git pull origin dev
git cherry-pick <hotfix-commit-hash>
git push origin devRun these once per machine for a cleaner experience:
# Default to rebase when pulling (avoids accidental merge commits)
git config --global pull.rebase true
# Auto-prune deleted remote branches on fetch
git config --global fetch.prune true
# Better diff algorithm
git config --global diff.algorithm histogram
# Sort branches by most recent commit
git config --global branch.sort -committerdate
# Default branch for new repos
git config --global init.defaultBranch main# Start work
git checkout dev && git pull origin dev
git checkout -b feat/my-feature
# Commit
git add <files>
git commit -m "feat(scope): description"
# Stay updated
git fetch origin && git rebase origin/dev
# Clean up before PR
git rebase -i HEAD~N
# Push
git push -u origin feat/my-feature # first time
git push --force-with-lease # after rebase
# Open PR
gh pr create --base dev --title "feat(scope): description"
# After merge, clean up
git checkout dev && git pull origin dev
git branch -d feat/my-feature
git fetch --prune