Journal

Best Way to Use Git in a Team

Technology
Engineering Guide · Git & Version Control

Best Way to Use Git
in a Team

A practical, senior-engineer guide to branching strategies, pull request workflows, merge conflict resolution, CI/CD integration, and real-world team conventions that actually scale — whether your team is 2 people or 200.

18 min read 2,800+ words Updated May 2025
Git GitHub GitLab Bitbucket

1. Why Git Discipline Matters for Teams

Git is not just a backup tool. In a team environment it is the single source of truth for your entire codebase, your history of decisions, and the backbone of your deployment pipeline. Used carelessly, it becomes a source of daily pain — lost work, broken builds, and hour-long conflict sessions at the worst possible time.

After working across dozens of engineering teams — from 3-person startups to 300-person product orgs — the difference between teams that ship confidently and teams that live in fear of deployments almost always comes down to one thing: their Git workflow is either an asset or a liability.

73%
of devs cite bad Git practices as a top frustration
4×
faster incident recovery with clean branching history
60%
of merge conflicts are avoidable with discipline

This guide covers the full picture — from naming your branches to integrating Git into your CI/CD pipeline — with practical commands and real team workflow examples on GitHub, GitLab, and Bitbucket.

2. Branch Strategy & Naming Conventions

Your branching strategy is the skeleton of your team's workflow. The two most widely adopted strategies in 2025 are GitFlow (structured, release-oriented) and Trunk-Based Development (fast, CI/CD-friendly). Understanding both lets your team choose the right tool for your release cadence.

GitFlow — For Structured Release Teams

main     ──●─────────────────────────────────────●──  (production)
              │                                           │
release  ────────────────────●──────────────●       (v1.2.0 → hotfix → merge)
                             
develop  ──●──●────●─────────●─────────────●───────  (integration)
              │    │
feature  ────●────●─── feat/user-auth ──────          (squash → develop)
                  
hotfix   ────────●── hotfix/crash-fix ─────────●──    (→ main + develop)

Trunk-Based Development — For Fast CI/CD Teams

main  ──●──●──●──●──●──●──●──●──  (always deployable)
           │     │     │
feat  ─────●     │     ●──●        (short-lived, max 2 days)
                 
feat  ───────────●──●              (feature flag hides WIP)

Branch Naming Convention

Consistent naming makes branch lists scannable and CI rules easy to write:

branch-naming.sh
# Pattern: {type}/{ticket-id}-{short-description} # Feature work git checkout -b feature/PROJ-142-user-authentication git checkout -b feature/PROJ-199-dark-mode-toggle # Bug fixes git checkout -b fix/PROJ-201-login-redirect-loop git checkout -b bugfix/PROJ-215-cart-total-rounding # Hotfixes (production emergencies) git checkout -b hotfix/PROJ-217-payment-gateway-crash # Release branches git checkout -b release/v2.4.0 # Chores (infra, deps, CI) git checkout -b chore/upgrade-node-20 git checkout -b docs/update-contributing-guide
💡
Pro Tip — Branch Protection Rules
Always protect your main and develop branches. On GitHub: Settings → Branches → Add rule → Require PR reviews. On GitLab: Settings → Repository → Protected Branches. On Bitbucket: Repository Settings → Branch Permissions. No one — including repo admins — should push directly to production branches.
Strategy Best For Release Cadence Complexity
GitFlow Versioned software, mobile apps Weekly / monthly Higher
GitHub Flow Web apps, SaaS, continuous delivery Multiple times daily Low
Trunk-Based Dev High-velocity teams with strong CI On every merge Low (with discipline)
GitLab Flow Teams with environment-based deploys Per environment Medium

3. Writing Great Commit Messages

A commit message is a letter to your future self and your teammates. Six months from now, someone will be bisecting your commit history at 2am to find a regression. The quality of your commit messages directly determines how long that takes.

The Conventional Commits specification is the industry standard. It keeps history readable and enables automated changelogs, semantic versioning, and release automation.

conventional-commits.txt
# Format: type(scope): short summary (imperative, max 72 chars) # Body: blank line, then detailed explanation (wrap at 72 chars) # Footer: BREAKING CHANGE: or Closes #123 ──────── GOOD EXAMPLES ──────── feat(auth): add OAuth2 login with Google Implements Google Sign-In using passport-google-oauth20. Stores refresh token in encrypted session. Falls back to email/password if OAuth is unavailable. Closes #142 fix(cart): correct total when discount is applied twice Discount stacking produced a negative total for orders over $500. Added a floor of 0 to the discount accumulation function. Closes #201 chore(deps): upgrade React from 18.2 to 18.3 docs(api): add rate limiting section to README refactor(db): extract query builder into dedicated service ──────── BAD EXAMPLES ──────── WIP fix bug stuff updated files asdfgh
⚠️
Warning — Commits Are Permanent History
Never commit credentials, API keys, or passwords — even to a private repository. Use .gitignore, environment variables, and tools like git-secrets or truffleHog to scan commits before they reach the remote. Removing secrets from Git history is painful and never 100% guaranteed to be complete.

4. Pull Request Workflow That Actually Works

The pull request (or merge request on GitLab) is the fundamental unit of collaboration in modern software teams. A well-structured PR workflow catches bugs, spreads knowledge, and keeps your main branch healthy. A poorly structured one creates bottlenecks and friction.

The Complete PR Lifecycle

Create a focused, short-lived branch
Branch from the latest main or develop. Keep scope tight — one feature or fix per PR. Long-running branches are the root cause of most merge conflicts.
Write code with tests
Run your test suite locally before pushing. On GitHub: gh pr create --draft opens a draft PR early — great for getting feedback on direction before implementation is complete.
Open PR with a descriptive template
Use a PR template (.github/pull_request_template.md) that includes: What changed, Why it changed, How to test it, Screenshots (for UI), and Links to tickets.
CI checks run automatically
Linting, tests, build, and security scans trigger on PR open and every subsequent push. Block merging until all checks pass. Never merge a red PR — even under deadline pressure.
Code review (minimum 1 approver)
Reviewers leave constructive comments. Author responds or resolves. Keep review cycles short — aim to review PRs within 4 business hours. Stale PRs are a team morale and velocity killer.
Squash-merge with a clean commit message
On merge, squash all branch commits into one clean Conventional Commit on main. Delete the branch. Ship it.

GitHub PR Template Example

.github/pull_request_template.md
## Summary Brief description of what this PR does and why. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to break) - [ ] Refactor / tech debt - [ ] Documentation update ## How to Test 1. Check out this branch 2. Run `npm install && npm test` 3. Navigate to /settings → verify toggle works ## Screenshots (UI changes only) | Before | After | | ------ | ----- | | img | img | ## Checklist - [ ] Tests added or updated - [ ] No new linting errors - [ ] Self-reviewed my own code - [ ] Linked to Jira / Linear ticket - [ ] No secrets or credentials committed ## Linked Issue Closes #142

5. Code Review Best Practices

Code review is a skill, not just a gate. The goal is not to find every possible flaw — it is to share knowledge, maintain consistency, and catch the issues that automated tools miss.

🎯
Review the right things
Focus on logic, security, architecture, and edge cases. Let linters handle style — that is not a human's job.
⏱️
Keep PRs small
PRs under 400 lines get thorough reviews. PRs over 1,000 lines get rubber-stamped. Small scope = better feedback.
💬
Comment with context
Don't just say "change this." Explain the why: "This could cause N+1 queries — consider eager-loading with include()."
🚦
Use comment prefixes
Prefix comments: nit: (optional), must: (blocking), Q: (genuine question). Reduces ambiguity.
👍
Praise good code
If you see an elegant solution, say so. Code review culture should not be purely critical — celebrate good engineering.
🔁
Max 2 review cycles
If a PR needs more than 2 back-and-forth rounds, schedule a synchronous call. Async debate rarely resolves complex issues fast.
🧠
Team Leader Note
Set a team SLA for PR reviews — something like "all open PRs reviewed within one business day". Unreviewed PRs are a hidden form of technical debt. They block your teammates, cause branch drift, and kill morale faster than almost anything else. Make reviewing others' code a first-class engineering responsibility, not an afterthought.

6. Merge vs. Rebase — Making the Right Call

One of the most debated topics in team Git workflows. The honest answer: both have a place, and the right choice depends on context.

merge-vs-rebase.sh
# ── MERGE: Preserves full history, creates merge commit ── git checkout develop git merge feature/PROJ-142-user-authentication # Creates: Merge branch 'feature/...' into develop # Good for: merging release branches, preserving context # ── SQUASH MERGE: One clean commit on target branch ── git merge --squash feature/PROJ-142-user-authentication git commit -m "feat(auth): add OAuth2 login with Google (#142)" # Good for: keeping main/develop history clean # Best practice for most teams with PR workflows # ── REBASE: Linear history, replays commits ── git checkout feature/PROJ-142-user-authentication git rebase develop # Good for: updating a feature branch with latest develop # Rule: NEVER rebase shared/public branches # ── INTERACTIVE REBASE: Clean up before PR ── git rebase -i HEAD~4 # Squash WIP commits, fix messages, reorder # Do this BEFORE opening PR, not after
📌
Team Rule of Thumb
Rebase local, merge remote. Use rebase to keep your local feature branch current with develop. Use squash-merge when completing a PR into a shared branch. The golden rule: never force-push to a branch that other people are working on.

7. Handling Merge Conflicts Like a Pro

Merge conflicts are not a sign of broken process — they are a natural consequence of parallel work. The goal is to make them rare, easy to understand, and quick to resolve.

Prevention First

  • Keep branches short-lived. The longer a branch lives, the more it diverges. Target 1–3 days maximum.
  • Sync frequently. Pull from develop into your feature branch every morning: git pull --rebase origin develop
  • Communicate actively. If two engineers are working near the same files, coordinate. Async chat is fine: "Hey, I'm refactoring the auth module today."
  • Modular architecture reduces conflicts. Well-bounded modules with clear ownership have far fewer conflicts than monolithic files.
  • One concern per PR. A PR that touches 30 files across the whole codebase will conflict with everything. Scope it tightly.

Resolving Conflicts Step by Step

conflict-resolution.sh
# Step 1 — Pull latest and start rebase git checkout feature/PROJ-142-user-authentication git fetch origin git rebase origin/develop # Git will pause and show you conflicting files: # CONFLICT (content): Merge conflict in src/auth/service.ts # Step 2 — Open the conflicted file. You'll see: <<<<<<< HEAD (your branch) const login = async (email: string) => { return authService.loginWithOAuth(email); ||||||| original const login = async (email: string) => { return authService.login(email); ======= const login = async (email: string, password: string) => { return authService.loginWithCredentials(email, password); >>>>>>> origin/develop # Step 3 — Use a merge tool for complex conflicts git mergetool # opens configured GUI tool # Popular tools: VS Code (built-in), IntelliJ, vimdiff, kdiff3 # Step 4 — After resolving, mark as resolved and continue git add src/auth/service.ts git rebase --continue # Step 5 — Force push your rebased branch (only YOUR branch) git push origin feature/PROJ-142-user-authentication --force-with-lease # --force-with-lease is safer than --force: fails if someone else pushed
⚠️
Warning — Never Force-Push Shared Branches
git push --force on main or develop will overwrite teammates' commits and cause catastrophic data loss. Only ever force-push your own feature branches. Configure branch protection to block force-pushes on shared branches entirely.

8. CI/CD Integration with Git

Your Git workflow should be the trigger for your entire delivery pipeline. When Git events drive CI/CD, you get automatic quality gates, environment deployments, and release automation without manual toil.

GitHub Actions — Automated PR Checks

.github/workflows/pr-check.yml
name: PR Quality Gate on: pull_request: branches: [main, develop] jobs: quality-gate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Lint run: npm run lint - name: Type check run: npm run typecheck - name: Unit & integration tests run: npm test -- --coverage - name: Build run: npm run build security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Audit dependencies run: npm audit --audit-level=high - name: Scan for secrets uses: trufflesecurity/trufflehog-actions-scan@master

GitLab CI/CD — Branch-Based Deployments

.gitlab-ci.yml
stages: - test - build - deploy test: stage: test script: - npm ci - npm run lint - npm test rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' deploy-staging: stage: deploy script: - echo "Deploying to staging..." - ./deploy.sh staging environment: name: staging url: https://staging.yourapp.com rules: - if: '$CI_COMMIT_BRANCH == "develop"' deploy-production: stage: deploy script: - ./deploy.sh production environment: name: production url: https://yourapp.com rules: - if: '$CI_COMMIT_BRANCH == "main"' when: manual # Require human approval for prod
Bitbucket Pipelines
Bitbucket Pipelines uses bitbucket-pipelines.yml in the root directory. The same principle applies: trigger test pipelines on all branches, deploy to staging on develop merges, and require manual approval for production deploys from main. Use Bitbucket Deployments to track environment history and rollback targets.

9. Common Team Git Mistakes (and How to Fix Them)

Even experienced teams make these. The first step to fixing a problem is recognizing it.

common-mistakes.sh
# ── MISTAKE 1: Committing to main directly ── # Fix: Branch protection rules (no direct pushes to main) # ── MISTAKE 2: Huge, multi-week branches ── # Fix: Feature flags let you merge incomplete features safely # The code ships, the feature hides behind a flag until ready if (featureFlags.isEnabled('new-checkout-flow')) { // new implementation } else { // existing implementation } # ── MISTAKE 3: Forgetting to pull before branching ── # Symptom: Your branch is weeks behind, conflicts everywhere git checkout develop git pull --rebase origin develop # Always do this first git checkout -b feature/new-thing # ── MISTAKE 4: git add . without reviewing ── # Fix: Always inspect what you're committing git diff --staged # Review staged changes git add -p # Stage interactively, hunk by hunk # ── MISTAKE 5: Committing compiled files / node_modules ── # Fix: Comprehensive .gitignore from project start echo "node_modules/" >> .gitignore echo "dist/" >> .gitignore echo ".env" >> .gitignore echo ".env.local" >> .gitignore # ── MISTAKE 6: Broken main / develop ── # Symptom: "Don't deploy today, main is broken" # Fix: Required CI checks + no direct pushes + PR-only workflow # If main IS broken, use git revert (not reset) to undo: git revert HEAD # Creates a new revert commit safely git push origin main # Push the revert — no history rewrite
🚨
Never Do This in Production
git reset --hard on a shared branch, git push --force to main, deleting a remote branch that others are using, or git clean -fd without checking what it will delete. These commands permanently destroy work and are nearly impossible to recover from in a team context. When in doubt, create a backup branch first: git checkout -b backup/before-experiment.

10. The Team Git Checklist

Use this as a health check for your team's Git practices. Walk through it in your next engineering retrospective.

Repository Setup

  • Branch protection enabled on main and develop — no direct pushes
  • Required CI checks must pass before merging any PR
  • Minimum 1 approving review required before merge
  • Stale branch cleanup — delete branches after merge automatically
  • Comprehensive .gitignore committed from day one

Daily Developer Habits

  • Pull and rebase from develop every morning before starting work
  • Conventional commit messages on every commit — no "fix", "stuff", "WIP"
  • Review staged changes before committing — use git diff --staged
  • Keep PRs small — under 400 lines when possible
  • Review teammates' PRs within one business day

Team Workflow

  • Agreed branching strategy documented in CONTRIBUTING.md
  • PR template configured in .github/ or equivalent
  • CI/CD pipeline runs on every PR and every merge to main
  • No secrets in Git history — scanned on every push
  • Git workflow reviewed in retrospective every quarter

Frequently Asked Questions

Answers to the most common Git team workflow questions.

What is the best branching strategy for a small team of 2–5 developers?
For small teams, GitHub Flow is the best starting point: one main branch, short-lived feature branches, and deploy on every merge. It is simple, fast, and avoids the overhead of GitFlow's multiple long-lived branches. Only add more branch complexity when your release cadence actually demands it.
Should we use merge commits or squash merges?
Most teams benefit from squash merges for feature branches into main — one clean Conventional Commit per PR keeps the history readable. Reserve merge commits (with --no-ff) for release branches where preserving the merge context is important. Avoid rebase merges for team workflows — they rewrite history and can cause confusion when multiple engineers share a branch.
How do I undo a commit that has already been pushed?
Always use git revert for pushed commits — it creates a new commit that undoes the changes without rewriting history. Never use git reset --hard on pushed commits in a team setting. Example: git revert abc1234 && git push origin main. If the bad commit introduced a security vulnerability like a leaked secret, contact your team and platform support immediately — a force-push to history may be necessary in extreme cases, but it should be a coordinated team action.
How many reviewers should a pull request require?
For most teams, one required approver is the right default. Requiring two approvers doubles the review bottleneck and often results in rubber-stamp approvals from the second reviewer. Exception: require two reviewers for security-sensitive code (auth, payments, data access) or architectural changes that affect the whole team. The quality of the review matters more than the number of approvers.
How do we prevent sensitive data from being committed to Git?
A layered approach works best: (1) a thorough .gitignore that excludes all .env files, (2) a pre-commit hook using git-secrets or detect-secrets that scans staged files before committing, (3) CI-level scanning using TruffleHog or GitGuardian on every push, and (4) team training so everyone understands what must never be in version control. Use environment variables and secret managers (AWS Secrets Manager, HashiCorp Vault, GitHub Encrypted Secrets) for all credentials.
What is the difference between git fetch and git pull?
git fetch downloads changes from the remote without modifying your working directory or current branch — it updates your remote-tracking branches (e.g. origin/main). git pull is effectively git fetch followed by a merge (or rebase if configured). In a team setting, prefer git fetch origin followed by git rebase origin/develop over a plain git pull — it gives you more control over how remote changes integrate with your local work.

Leave a reply

Share via
Copy link