Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 067 / 100

Git for teams: conventions that work — a practical guide

Shared habits reduce friction.

Expona Admin6 min read

Most teams don't fail because their developers can't code—they fail because everyone merges differently, commits with cryptic messages, and leaves dead branches scattered across the repository. Git for teams: conventions that work — a practical guide is about establishing the shared habits that reduce friction, accelerate code review, and make onboarding new engineers predictable instead of chaotic. When your team moves like one unit on version control, everything downstream—CI/CD, debugging, audits—becomes faster and cheaper.

Branch naming — feature/, fix/, chore/ with issue numbers

Your branch name is often the first thing reviewers see and the last thing you think about. A consistent naming scheme tells you what a branch does, ties it to tracked work, and makes automation straightforward. The pattern most teams adopt successfully is a prefix followed by the issue number and a kebab-cased summary.

Use these prefixes:

  • feature/ — new functionality or user-facing work
  • fix/ — bug fixes and hotfixes
  • chore/ — refactoring, dependency updates, config changes
  • docs/ — documentation only
  • test/ — test coverage or CI infrastructure (if not bundled with feature branches)

Follow with the issue or ticket number from your tracker, then a short description:

bash
feature/PROJ-1234-add-payment-retry-logic
fix/PROJ-5678-user-session-timeout-edge-case
chore/PROJ-9012-upgrade-postgres-driver
docs/PROJ-3456-api-authentication-guide

This naming scheme has three immediate payoffs. First, anyone glancing at the branch list knows the type and scope of work. Second, your issue tracker and Git history are now linked—tools like GitHub, GitLab, and Jira will auto-link these branches to tickets. Third, you can write automation: a CI pipeline can reject branches that don't match the pattern, or automatically move tickets to "In Review" when a branch is pushed.

In practice, enforce this at the team level by adding a pre-push hook or CI check. Many teams use Husky with a simple regex validation in a Node.js project, or a server-side hook in their Git provider.

Commit format — Conventional commits across repositories

Commit messages are where most teams stop thinking. They write "fix bug", "update stuff", or leave it blank. Then six months later, someone runs git log and finds nothing searchable, nothing that explains the why, and nothing that helps tools understand what changed.

Conventional Commits is a lightweight standard that makes commit history machine-readable and human-friendly. The format is simple:

bash
type(scope): subject

body

footer

The type is one of: feat, fix, refactor, test, docs, chore, perf, or ci. The scope is optional but valuable—name the area of code affected. The subject is imperative, lowercase, no period. The body explains the why, not the what. The footer references issues or breaking changes.

Real examples:

bash
feat(payment): add exponential backoff for payment gateway timeouts

Implement exponential backoff strategy to reduce cascading
failures when the payment gateway is slow to respond.
Max retries set to 5 with 1s base delay.

Fixes #PROJ-1234
Related-To: #PROJ-5600
bash
fix(auth): prevent session hijacking via secure cookie flags

Set HttpOnly and Secure flags on session cookies to prevent
client-side script access and transmission over HTTP.
Tested in Chrome, Firefox, Safari.

Fixes #PROJ-5678
bash
refactor(database): consolidate user query builders

Extract common filtering logic into a UserQueryBuilder class
to reduce duplication across three service files. No functional
change.

Enforce this with commitlint and Husky. Add a commit-msg hook to your repo:

bash
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky

npx husky install
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

Create a commitlint.config.js:

bash
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', [
      'feat', 'fix', 'refactor', 'test', 'docs', 'chore', 'perf', 'ci'
    ]],
    'subject-case': [2, 'never', ['start-case', 'pascal-case']],
    'scope-case': [2, 'always', 'kebab-case']
  }
};

Now every commit is validated before it hits the repository. Developers get immediate feedback, and your log stays clean and searchable. This becomes critical when you're generating changelogs, automating releases, or investigating when a bug was introduced.

Merge method — One method per repository

Teams often inherit three merge behaviors: merge commits, squash, and rebase. Each has trade-offs, and mixing them creates a jagged, confusing history that's hard to bisect or revert.

Merge commit (default): creates a commit that records the merge. Preserves full history but creates branching paths in the log.

Squash: combines all feature branch commits into one before merging. Clean, linear history but loses detail within the feature.

Rebase and fast-forward: replays feature commits on top of main as if they were made sequentially. Keeps history linear and granular but rewrites commit hashes (risky if the branch is shared).

Pick one per repository and enforce it. Most teams in production use squash for feature branches (reduces noise, keeps trunk clean) and merge commit or rebase for release branches (preserves traceability).

In GitHub, set this in your repository settings under "Merge button":

Setting Recommendation When to Use
Allow merge commits Off Noisy, rarely needed
Allow squash merging On Feature branches, keeps trunk clean
Allow rebase merging On for release branches only Release history, CI/CD pipelines

Document your team's choice in the repository README and enforce it via branch protection rules. If you only allow squash in CI, developers never have to think about it.

Cleanup — Auto-delete branches after merge

After a merge, the feature branch lingers. Weeks later, your repository looks like a forest of dead branches. This creates confusion ("Is this still active?"), clutters the UI, and wastes mental bandwidth during code review.

Enable automatic branch deletion in your Git provider. GitHub has it in repository settings under "Pull Requests":

  • Check "Automatically delete head branches"

Now when a PR is merged, the branch is gone immediately. If someone needs it later, Git's reflog keeps it accessible for 30 days anyway.

You can also clean up local stale branches with a script run periodically or as part of onboarding:

bash
# Delete local branches that have been merged to main
git branch --merged main | grep -v main | xargs git branch -d

# Delete branches tracking deleted remote branches
git remote prune origin
git branch -r --no-merged | xargs -I {} git branch -d -r {}

Add this to your team's setup script or CI pipeline. The goal is simple: no clutter, no guesswork about which branch is live.

Common mistakes

Mixing naming schemes. Some developers use underscores, others use dashes. Some skip the issue number. This breaks automation and makes logs hard to parse. Solution: enforce it in CI.

Vague commit messages. "Fixed stuff" tells you nothing. Commit messages are written once but read hundreds of times. Spend 30 seconds explaining why, not just what changed.

Using all three merge methods. Developers pick their favorite, the history becomes incoherent, and reverting or bisecting is a nightmare. Pick one and enforce it.

Leaving merged branches in place. Dead branches confuse new team members and clutter pull request dashboards. Automate deletion and make cleanup a habit.

Not documenting conventions. Rules only work if people know them. Write them in CONTRIBUTING.md, link them in your onboarding checklist, and revisit them in retrospectives.

Key takeaways

  • Use consistent branch names: feature/, fix/, chore/ prefixes with issue numbers. This ties Git to your tracker and enables automation.
  • Adopt Conventional Commits across all repositories. Machine-readable commits unlock changelog generation, intelligent reverting, and searchable history.
  • Enforce one merge method per repository (squash for features, rebase or merge commit for releases). Mixed methods create confusing, bisect-unfriendly histories.
  • Auto-delete branches after merge. Keep the repository clean and force clarity about which work is active.
  • Enforce all conventions in CI or pre-commit hooks. Rules only work if they're checked automatically before code reaches the repository.
  • Document conventions in CONTRIBUTING.md and reference them during code review. Shared habits compound: every commit and branch becomes more valuable over time.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles