A Git workflow for teams
I've watched version control chaos cost teams hours of debugging, merge conflicts, and lost context about why a line of code exists. A Git workflow for teams isn't just about moving code from one place to another—it's about building a searchable, trustworthy history that your future self will thank you for. The conventions I'm sharing come from running production systems where I needed to bisect failures at 3 AM and understand what happened three months ago.
Why workflow matters more than tool
Git is the tool. Workflow is the discipline. You can use the fanciest Git GUI or command-line wizardry, but without clear conventions, your history becomes noise. I've inherited codebases where branches were named fix-stuff and new-feature, commits were squashed randomly, and nobody knew which tag matched production. That's when I learned that workflow is infrastructure for your team's thinking.
The workflow I'm describing has worked for teams of 3 to 40+ engineers across e-commerce, CRM, trading, and SaaS platforms. It's not opinionated beyond reason—there are many valid approaches—but this one balances simplicity, safety, and searchability.
Branch naming — searchable and clear
A good branch name tells you what changed and why, without needing to open the PR. I use a prefix-based system that took me minutes to learn but saved hours in communication.
The pattern: type/ticket-id-short-description
Examples:
feature/LEAD-1234-assignment-distribution— a new feature for distributing leadsfix/PAY-567-timeout-handling— a bug fix for payment timeoutsrefactor/CORE-890-logging-middleware— internal improvementchore/OPS-12-dependency-upgrade— maintenance workdocs/README-setup-instructions— documentation
Why this matters:
- Your task tracking system becomes queryable. I can grep for all branches related to ticket
LEAD-1234. - New team members understand scope immediately.
feature/is different fromfix/, which is different fromchore/. - Git history remains human-readable. When you deploy and something breaks, you can trace back through branch names.
- Search tools work. Most Git UIs let you filter by branch prefix, making it trivial to find work-in-progress.
I've worked on projects with hundreds of branches. Without naming conventions, it was a scavenger hunt to find the right one. With them, you can git branch -a | grep feature/PAY and instantly see payment-related work.
Rebase before merging — a clean, linear history
This is where workflow philosophy gets practical. When you merge a feature branch, you have a choice: merge commit (creating a diamond shape in history) or rebase (keeping history linear). I almost always rebase.
Why linear history matters:
Imagine debugging a production bug using git bisect. Binary search works best on a linear timeline. With merge commits, you're jumping between timelines, and your bisect has to explore both branches. On a large repository, this costs real time.
Linear history is also easier to read with git log --oneline. You see the actual flow of work, not a tangled graph.
The workflow:
# Your feature branch has diverged from main
# Before merging, sync with main's latest
git fetch origin
git rebase origin/main
# If there are conflicts, resolve them
# and continue the rebase
git add .
git rebase --continue
# Force-push to your branch (safe, because you own it)
git push -f origin feature/LEAD-1234-assignment
# Now merge via PR—the merge will be fast-forward
# (or use squash if your team prefers one commit per feature)
One caveat: never rebase branches that others are using. If your feature branch is shared, rebasing rewrites history and breaks everyone else's checkouts. Rebase is safe only on branches you own.
I've debugged live incidents by running git log --oneline and immediately spotting which commit introduced the regression. That linear history is invaluable.
Protect main — required reviews and passing checks
Your main branch is the source of truth for production. It deserves protection. I use three guardrails:
- Branch protection rules: GitHub, GitLab, and Gitea all support this. Require at least one approval before merge. Require all status checks to pass. Dismiss stale reviews when commits are pushed.
- Status checks: Lint, test, build, security scanning—anything that fails should block the merge.
- Code review culture: A check is an automaton. A review is a human thinking critically about your code.
On the e-commerce platform I worked on, we had a rule: no merge without review, no merge if tests fail. It caught a critical bug before it reached production—a race condition in order processing that only showed up under specific timing. The test would have failed; the reviewer would have asked a question. Both defenses worked.
Here's what a protected main looks like in GitHub settings:
- Require pull request reviews before merging
- Dismiss stale pull request approvals when new commits are pushed
- Require branches to be up to date before merging
- Require status checks to pass before merging
The last one is critical. If your CI/CD doesn't run on every PR, you're not actually enforcing quality—you're just hoping people remember to run tests locally.
Tag releases — every production deploy maps to a tag
A tag is a named reference to a commit. I use semantic versioning: v1.2.3 for major.minor.patch. Every time code goes to production, it gets a tag.
Why:
- Traceability: You can instantly point to the exact code running in production. No guessing, no "I think it was the one from Tuesday."
- Rollback precision: If production breaks, you roll back to the last known good tag.
- Release notes: You can generate a changelog between tags.
git log v1.1.0..v1.2.0shows every commit in that release. - Regulatory compliance: If your system processes payments or sensitive data, auditors want to know exactly which code version was running when.
The process:
# After final testing, create an annotated tag
git tag -a v1.2.3 -m "Release 1.2.3: payment timeout fixes"
# Push it to origin
git push origin v1.2.3
# Your CI/CD sees the tag and deploys that specific commit
# You can now reference it forever
git show v1.2.3 # see exactly what was deployed
git log v1.2.0..v1.2.3 # what changed between versions
On one trading platform, a bug manifested only after 24 hours in production. We had three version candidates and needed to know which one was live. Tags made it instant: git describe $(git rev-parse HEAD) told us we were on v2.15.7. We could then trace that version's changes and find the culprit in minutes instead of hours.
Enforcing workflow as a team
Workflow only works if everyone follows it. Here's how I've embedded it:
- Documentation: Write it down in your README or contributing guide. One page, no more.
- Automation: Use pre-commit hooks to lint, format, and block commits that don't match conventions. Use branch protection to enforce review and testing.
- Example: When onboarding, walk a new engineer through the workflow step-by-step on a real branch. Repetition builds habit.
- Feedback: When you review PRs, gently reinforce the workflow. "This branch name doesn't match our convention—let's rename it."
I've never seen a team naturally drift toward clean workflow. It requires small, consistent pressure. But once it's embedded, it becomes invisible—engineers just do it because that's how things work here.
Common mistakes
Mixing feature and bug fixes in one branch: Create separate branches. Even if they touch the same file, keeping work logically separated makes bisecting and reviewing easier.
Rebasing shared branches: If someone else is using your branch, don't rebase. Use merge commits or ask the team to check out fresh once you've rebased.
Forgetting to tag releases: I've seen teams that tag sporadically. Then production breaks and nobody knows which commit is live. Automate tagging in your deployment pipeline. Every production push should create a tag.
Ignoring failed status checks: "It passed locally" is not a substitute for CI. If your test fails in CI, your code has a problem. Fix it or understand why the test is wrong before merging.
Branch names that don't link to tickets: feature/payment-stuff is not searchable. feature/PAY-567-timeout-handling is. The ticket ID is your lifeline to context.
Key takeaways
- Use prefixed, ticket-linked branch names so your repository is searchable and self-documenting.
- Rebase before merging to keep history linear, making bisects and code archaeology faster.
- Protect main with required reviews and passing checks—it's your source of truth.
- Tag every production release with semantic versioning so you can always answer "what code is running right now?"
- Workflow is only valuable if the team enforces it consistently. Document it, automate it where you can, and reinforce it during code review.
- Your Git history will outlive your memory. Write it as if someone will read it at 2 AM during an incident, because they will.






Comments
Loading comments…