Pull Requests and Code Review: Git Collaboration Best Practices

Master pull request workflows and code review — writing effective PR descriptions, review best practices, collaboration patterns, and team workflows.

published: reading time: 11 min read updated: March 31, 2026

Introduction

Pull requests (or merge requests in GitLab) are the primary mechanism for collaborative code integration in modern development. They transform Git from a version control system into a collaboration platform, enabling discussion, review, and quality gates before code reaches the main branch.

A well-crafted pull request respects the reviewer’s time, clearly communicates intent, and makes review effortless. A poorly crafted one creates friction, delays integration, and breeds resentment. The difference lies in discipline, empathy, and understanding that code review is a communication tool as much as a quality gate.

This guide covers the full PR lifecycle — from writing descriptions that reviewers appreciate, through effective review practices, to team workflows that make collaboration smooth and productive.

When to Use / When Not to Use

When to Use Pull Requests

  • Feature integration — any significant change deserves review before merging
  • Bug fixes — even small fixes benefit from a second pair of eyes
  • Refactoring — structural changes need careful review to avoid regressions
  • Knowledge sharing — PRs are a primary mechanism for team learning
  • Compliance — regulated environments require documented review trails

When Not to Use Pull Requests

  • Trivial typo fixes — commit directly if your team agrees on the threshold
  • Emergency hotfixes — fix first, PR after (but still create the PR)
  • Automated changes — bot-generated PRs should be minimal and auto-mergeable
  • Personal experiments — keep exploratory work in local branches

Core Concepts

A pull request is a request to merge changes from one branch into another. It includes a diff, a description, discussion threads, review approvals, and CI/CD status checks. The PR is the unit of collaborative code change.


graph TD
    Dev["Developer"] -->|Push branch| Remote["Remote Repository"]
    Remote -->|Create| PR["Pull Request"]
    PR -->|Notify| Reviewers["Reviewers"]
    Reviewers -->|Review| PR
    PR -->|CI Checks| CI["CI/CD Pipeline"]
    CI -->|Pass/Fail| PR
    PR -->|Approve| Approved["Approved"]
    Approved -->|Merge| Main["Main Branch"]
    PR -->|Request Changes| Dev
    Dev -->|Update| PR

Architecture or Flow Diagram


flowchart TD
    A["Complete feature branch"] --> Rebase["Rebase onto latest main"]
    Rebase --> Tests["Run tests locally"]
    Tests --> Push["git push origin feature-branch"]
    Push --> Create["Create Pull Request"]
    Create --> Describe["Write description:\nWhat, Why, How"]
    Describe --> Link["Link to issue/ticket"]
    Link --> Request["Request reviewers"]
    Request --> CI["CI runs automatically"]
    CI --> Check{"All checks pass?"}
    Check -->|No| Fix["Fix failures,\nupdate PR"]
    Check -->|Yes| Review["Team reviews code"]
    Fix --> CI
    Review --> Feedback{"Feedback?"}
    Feedback -->|Changes requested| DevFix["Address feedback,\npush updates"]
    Feedback -->|Approved| Merge["Merge PR"]
    DevFix --> Review
    Merge --> Cleanup["Delete branch"]
    Cleanup --> Done["Feature integrated"]

Step-by-Step Guide / Deep Dive

Creating an Effective PR


# Before creating the PR
git switch main
git pull origin main
git switch feature-branch
git rebase main              # Get current
git push --force-with-lease  # Update remote branch

# Create PR via CLI (GitHub)
gh pr create \
  --base main \
  --head feature-branch \
  --title "feat: add user authentication with JWT" \
  --body-file PR_DESCRIPTION.md

PR Description Template

## What

Brief summary of what this PR does.

## Why

Business or technical rationale for the change.
Links to issue: #123

## How

Key implementation decisions and approach.

## Testing

- [ ] Unit tests pass
- [ ] Integration tests pass
- [ ] Manual testing completed
- [ ] Edge cases considered

## Screenshots (if UI)

Before/after comparison.

## Risks

Any potential risks or areas needing extra review attention.

Code Review Checklist

## For Reviewers

### Correctness

- [ ] Does the code do what it claims?
- [ ] Are edge cases handled?
- [ ] Are there off-by-one errors?

### Security

- [ ] Is user input validated/sanitized?
- [ ] Are secrets handled properly?
- [ ] Is authentication/authorization correct?

### Performance

- [ ] Are there N+1 queries?
- [ ] Is caching used appropriately?
- [ ] Are there memory leaks?

### Maintainability

- [ ] Is the code readable?
- [ ] Are functions/methods focused?
- [ ] Is there adequate documentation?

### Testing

- [ ] Are there tests for new functionality?
- [ ] Do tests cover edge cases?
- [ ] Are tests readable and maintainable?

Review Response Workflow


# After receiving review feedback
git switch feature-branch
# Make changes
git add .
git commit -m "fix: address review comments - validate email format"
git push origin feature-branch

# Or amend if the PR hasn't been reviewed yet
git commit --amend
git push --force-with-lease origin feature-branch

Production Failure Scenarios + Mitigations

ScenarioImpactMitigation
Merging without reviewBugs reach productionRequire minimum approvals in branch protection
Rubber-stamp reviewsDefects slip throughRotate reviewers; require substantive comments
Large PRs (500+ lines)Reviewers skim, miss issuesLimit PRs to 400 lines; split large changes
Stale PRs (weeks old)Conflicts accumulate, context lostReview within 24 hours; rebase regularly
Merging failing CIBroken main branchBlock merge on CI status checks

Branch Protection Rules


# GitHub CLI: Set up branch protection
gh api repos/{owner}/{repo}/branches/main/protection \
  --method PUT \
  --field required_pull_request_reviews='{"required_approving_review_count":2}' \
  --field required_status_checks='{"strict":true,"contexts":["ci/test"]}' \
  --field enforce_admins=true \
  --field restrictions=null

Trade-offs

ApproachProsCons
Required reviewsQuality gate, knowledge sharingSlower integration, bottlenecks
Single approvalFast, lightweightLess thorough review
Self-mergeFastest, trusted teamsNo quality gate
Squash mergeClean historyLoses individual commit context
Merge commitPreserves full historyNoisier git log
Rebase mergeLinear historyCan hide integration complexity

Implementation Snippets


# GitHub: Create PR with labels and assignees
gh pr create \
  --title "feat: add rate limiting to API" \
  --body "Implements token bucket algorithm for API rate limiting" \
  --label "enhancement" \
  --assignee @me \
  --reviewer senior-dev,security-team

# GitLab: Create merge request
glab mr create \
  --source-branch feature/rate-limiting \
  --target-branch main \
  --title "feat: add rate limiting" \
  --description "Implements rate limiting using token bucket" \
  --assignee @me \
  --reviewer @team-lead

# Review PR locally
gh pr checkout 123
# Review code, then:
gh pr review 123 --approve
# or:
gh pr review 123 --comment "Please add tests for edge case"

Observability Checklist

  • Logs: Record PR creation, review, and merge events
  • Metrics: Track PR size, review time, and approval rate
  • Alerts: Alert on stale PRs (>7 days without review)
  • Traces: Link PRs to deployed versions and incidents
  • Dashboards: Display team review throughput and bottlenecks

Security/Compliance Notes

  • Require minimum number of approvals for production branches
  • Block merges when CI security scans fail
  • Document review decisions for audit compliance
  • Use CODEOWNERS files to auto-assign security-sensitive reviews
  • Never bypass branch protection rules, even for emergencies

Common Pitfalls / Anti-Patterns

  • Mega PRs — changesets over 400 lines get superficial reviews
  • Description-less PRs — reviewers shouldn’t have to read code to understand intent
  • Ignoring review feedback — address every comment, even to explain why you disagree
  • Reviewing your own PR — get a second pair of eyes, always
  • Merging without waiting for CI — automated checks catch what humans miss
  • Leaving branches after merge — delete merged branches to keep the repo clean

Quick Recap Checklist

  • Rebase onto main before creating PR
  • Write a clear PR description with What, Why, How
  • Link PR to issue/ticket
  • Request appropriate reviewers
  • Wait for CI checks to pass
  • Address all review feedback
  • Require minimum approvals before merge
  • Delete branch after merge

Interview Q&A

What makes a good pull request description?

A good PR description answers what changed, why it changed, and how it was implemented. It links to the relevant issue, describes testing performed, notes any risks, and provides context that isn't obvious from the diff alone. The goal is to make the reviewer's job as easy as possible.

Why should pull requests be kept small (under 400 lines)?

Research shows that review effectiveness drops significantly after 200-400 lines. Reviewers start skimming, missing defects, and providing superficial feedback. Small PRs get faster, more thorough reviews and are easier to revert if problems are discovered after merge.

What is the difference between squash merge, regular merge, and rebase merge?

Squash merge combines all PR commits into one, creating a clean history but losing individual commit context. Regular merge creates a merge commit preserving all commits and the branch structure. Rebase merge replays commits onto the target branch for a linear history. Choose based on your team's history preferences.

How should you handle review feedback you disagree with?

Respond respectfully with reasoning. Explain your perspective with evidence — benchmarks, documentation, or design principles. If consensus isn't reached, escalate to a tech lead or team discussion. Never ignore feedback or merge over objections. The goal is the best code, not winning an argument.

PR Lifecycle Architecture


flowchart TD
    Dev["Developer pushes branch"] --> Create["Create PR/MR"]
    Create --> AutoChecks["Automated Checks\nCI, lint, tests, security scan"]
    AutoChecks --> Pass{"All checks pass?"}
    Pass -->|No| Fix["Fix failures,\nupdate PR"]
    Pass -->|Yes| Review["Human Code Review"]
    Fix --> AutoChecks
    Review --> Feedback{"Feedback?"}
    Feedback -->|Changes requested| DevFix["Address feedback,\npush updates"]
    Feedback -->|Approved| Approve["Approved (min reviewers)"]
    DevFix --> Review
    Approve --> MergeType{"Merge strategy?"}
    MergeType -->|Squash| Squash["Squash merge\nSingle commit"]
    MergeType -->|Rebase| RbMerge["Rebase merge\nLinear history"]
    MergeType -->|Merge commit| McMerge["Merge commit\nPreserves history"]
    Squash --> Cleanup["Delete source branch"]
    RbMerge --> Cleanup
    McMerge --> Cleanup
    Cleanup --> Deploy["Deploy to production"]

    classDef stage fill:#16213e,color:#00fff9
    class Dev,Create,AutoChecks,Review,Approve,Squash,RbMerge,McMerge,Cleanup,Deploy stage

Production Failure: Merging Unreviewed PRs

Scenario: A team member marks their own PR as approved to bypass the review requirement. The PR contains a subtle SQL injection vulnerability that a reviewer would have caught. CI passes because the test suite doesn’t cover this input path. The PR is merged and deployed.

Impact: Security vulnerability in production. The review process — designed as a quality gate — was bypassed, allowing unvetted code to reach users.

Mitigation:

  • Require minimum approvals from different people (not the author)
  • Enable CODEOWNERS to auto-assign appropriate reviewers
  • Block self-approval in platform settings
  • Require CI to pass before merge is allowed
  • Prevent merging stale branches — require rebase onto latest main

# GitHub: Enforce review requirements
gh api repos/{owner}/{repo}/branches/main/protection \
  --method PUT \
  --field required_pull_request_reviews='{
    "required_approving_review_count": 2,
    "dismiss_stale_reviews": true,
    "require_code_owner_reviews": true,
    "require_last_push_approval": true
  }'

# CODEOWNERS file (.github/CODEOWNERS)
# Auto-assign reviewers based on file paths
*.py @backend-team
*.ts @frontend-team
src/auth/ @security-team

Trade-offs: PR Merge Strategies

StrategyHistory ImpactAttributionRevert EaseBest For
Squash mergeSingle commit on main, PR branch history lostOriginal author preserved, individual commits lostEasy — single commit revertSmall PRs, feature branches with WIP commits
Rebase mergeLinear history, all commits replayedIndividual commits preserved with original authorsEasy — revert individual commitsTeams wanting clean history with full attribution
Merge commitNon-linear, merge commit records integration pointFull history preserved, including all commitsEasy — revert merge commit (all changes at once)Large features, audit compliance, release branches

Observability Checklist: PR Metrics

  • Review time: Average time from PR creation to merge (target: <24 hours)
  • Comment density: Comments per 100 lines of code (healthy: 2-5 per 100 lines)
  • CI pass rate: Percentage of PRs passing CI on first attempt (target: >80%)
  • PR size distribution: Track lines changed per PR (healthy: <400 lines)
  • Stale PR count: PRs open >7 days without activity (target: <5% of total)
  • Review turnaround: Time from review request to first comment (target: <4 hours)
  • Revert rate: Percentage of merged PRs that are later reverted (target: <2%)

# GitHub CLI: PR metrics
gh pr list --state merged --json mergedAt,createdAt,additions,deletions \
  | jq '[.[] | {
      review_time: ((.mergedAt | fromdateiso8601) - (.createdAt | fromdateiso8601)) / 3600,
      lines_changed: (.additions + .deletions)
    }]'

Resources

Category

Related Posts

Git Blame and Annotate: Line-by-Line Code Attribution

Master git blame for line-by-line code attribution, understanding code history, finding when code changed, and using blame effectively for code comprehension.

#git #version-control #git-blame

Git Merge and Merge Strategies Explained

Deep dive into Git merge strategies — fast-forward, three-way, recursive, ours, subtree. Learn when each strategy applies and how to control merge behavior.

#git #version-control #merge

Git Remote Management: Adding, Removing, and Configuring Remotes

Master git remote operations — adding, removing, renaming remotes, managing multiple remotes, and configuring remote URLs for effective collaboration.

#git #version-control #remote