Git Strategy and PR Workflow
A consistent git workflow keeps your repo healthy and your team productive. This post covers branch naming, commit conventions, squash merges, and the full PR workflow using the gh CLI.
Git is where development coordination either works or falls apart. Inconsistent branch names, vague commit messages, and no review discipline make the repo harder to navigate over time. The solution is not more process, it is just consistent conventions.
This post covers the branch model, commit style, squash merge strategy, and the full PR workflow using the gh CLI.
TL;DR: Use feat/fix/refactor prefixes for branches, conventional commit format for messages, squash merge to main, and gh CLI for the full PR lifecycle. Delete branches after merge.
Branch Model
Keep branch names short but descriptive. The format is type/short-description.
feat/add-user-authentication fix/payment-webhook-timeout refactor/extract-email-service
The type prefix tells you what kind of change it is at a glance. feat is new functionality. fix is a bug fix. refactor is code change with no behavior change. There are others like chore, docs, test, but feat, fix, and refactor cover most cases.
Keep the description in kebab-case, two to four words maximum. You want enough to understand the branch without a novel.
Avoid branch names like fix-bug, hotfix, or working. They tell you nothing. feature is not a description, it is a placeholder.
Commit Conventions
Use conventional commit format. It is simple, machine-readable, and makes changelogs automatic.
format: type(scope): description
Examples:
feat(auth): add JWT refresh token rotation fix(payments): handle webhook timeout gracefully refactor(api): extract user validation into service class docs(readme): update local setup instructions
The type is the same set as branch prefixes. Scope is optional but useful for larger projects. Description is a short summary of what changed, written in the imperative mood.
Bad commit messages: "updated stuff", "fix", "WIP", "asdf". These tell you nothing.
Good commit messages tell you exactly what changed and why it mattered. If you cannot write a clear description in one line, the commit is probably doing too much and should be split.
Squash Merge Strategy
Squash merge means all commits in a PR are collapsed into one commit on the target branch. This keeps the main branch history clean and readable.
On a feature branch, you might commit frequently as a safety net. "WIP", "oops fix typo", "actually this", "final fix". These are fine locally but should not clutter the main history. Squash merge collapses all of that into one clean commit with a proper message.
To squash merge via gh CLI:
gh pr merge --admin --squash
This merges the PR and collapses all commits into one. The PR title becomes the commit message, so write a good PR title.
After merging, delete the branch. It is no longer needed and keeping around stale branches creates noise.
gh pr merge --admin --squash && gh repo delete-remote-branch
Or if you prefer the manual version:
git checkout main git pull origin main git branch -d feature-branch-name
Delete the remote separately if needed. Most teams script this so it happens automatically after merge.
Full PR Workflow with gh CLI
The typical workflow looks like this.
Create and switch to a new branch:
git checkout -b feat/add-rate-limiting
Make your changes, commit:
git add . git commit -m "feat(api): add rate limiting middleware"
Push and create the PR in one step:
gh pr create --title "feat(api): add rate limiting middleware" --body "Adds rate limiting to protect the API from abuse. Limits to 100 req/min per IP."
If you need reviewers:
gh pr review --approve gh pr comment -m "LGTM, good implementation"
Merge after approval:
gh pr merge --squash --delete-branch
The --delete-branch flag handles branch deletion as part of the merge.
For PRs that need text description, write a good body. At minimum, describe what the change does and why. Link to related issues. A PR with a clear description gets reviewed faster.
When to Force Push
On feature branches before merge, force pushing is fine and sometimes necessary. You might squash commits, reorder history, or fix a mistake. This is your branch.
On shared branches or main, never force push. It rewrites history for everyone and will cause problems.
If you are the only person working on a branch, force push is safe. If other people have checked it out, coordinate first.
Pulling Changes from Main
If main has moved forward while you work on a feature branch, rebase to keep your commits clean:
git fetch origin git rebase origin/main
Resolve any conflicts, then force push your rebased branch:
git push --force-with-lease
--force-with-lease is safer than --force. It fails if someone else has pushed changes you have not seen, preventing accidental overwrites.
Keeping the Repo Healthy
A few habits keep the repo easy to work with.
Rebase instead of merging main into your feature branch. Merging creates an extra merge commit that clutters history. Rebase replays your commits on top of current main cleanly.
Keep commits logical. Each commit should represent one atomic change. If you find yourself writing "and also" in a commit message, split it.
Review your own PR before requesting review. Read the diff, check for debug code, make sure the commit message is clear. Your review is the first filter.
Write good PR descriptions. They become part of the project history through squash merge. A bad PR title becomes a bad commit message.
Delete branches after merge. Stale branches are noise and make it harder to find what is active.
Sources: Conventional Commits | GitHub CLI Docs