Skip to main content

Git Workflows: Pull Requests, GitHub Flow, Gitflow

How teams work with Git: the pull request flow, merge methods, range-diff, stacked branches with --update-refs, release backports, and picking a workflow.

Git does not prescribe how a team should work. It gives you branches, merges and remotes, and a workflow is the set of rules a team agrees on: where new work starts, how it is reviewed, how it reaches main, and how releases are made. This part walks through the most common one, a short-lived branch per change with a pull request, on the project with real commands, including the parts most tutorials skip: updating a branch during review, seeing what changed between two pushes, and stacking branches that depend on each other. Then it compares the well-known workflows and the repository settings that enforce them.

Prerequisites

  • The status-page repository with its remote and Sam's clone from Part 6.
  • Rebasing (Part 5) and force-pushing with a lease (Part 6).

The Pull Request Flow

A pull request (GitHub, Bitbucket, Gitea) or merge request (GitLab) is a request to merge one branch into another, with a page for discussion, review and automated checks. Git itself has no pull requests; the hosting service adds them on top of branches. The flow looks like this:

1. Branch, commit, push

You add a list of recent changes to the page. The branch has two commits: a script, saved as scripts/changes.sh with LF line endings, and the documentation for it.

#!/usr/bin/env bash
# Print the newest commits as an HTML list, for the status page.
set -euo pipefail
cd "$(dirname "$0")/.."

echo '<ul id="changes">'
git log -5 --format='  <li>%h %s</li>'
echo '</ul>'

On GitHub, you would now open the pull request in the browser (the push prints a link) or with gh pr create --fill, which uses the commit messages for the title and description. Open it early, as a draft if it is not ready, so that the checks run and people can see what you are working on.

2. Update the branch during review

A reviewer notices that merge commits fill the list with "Merge branch ..." lines and asks you to leave them out. Fix it in a fixup commit: a commit marked to be folded into an earlier one later. Reviewers see the small change on its own, and the branch can still end with clean commits:

While you were busy, Sam's pull request was merged into main:

Before the merge, bring your branch up to date: rebase it onto the new main and fold the fixup into its commit in one go. --autosquash moves every fixup! commit behind the commit it fixes and squashes it. Since Git 2.44 it also works without -i; the rebase.autoSquash true setting turns it on by default, but only for interactive rebases, so keep the option on the command line here:

3. See what changed between two versions of the branch

After a rebase, the usual "compare" view is useless: every commit has a new hash and a new base. git range-diff compares two versions of a series commit by commit, which is what a reviewer wants to see after a force push. Compare what is on the remote (@{upstream}, the old version) with your branch:

Each line pairs an old commit with a new one: ! means the commit changed (followed by a diff of the two diffs), = means it is the same change, and < means the commit exists only in the old series (the fixup, now part of the first commit). origin/main@{1} is where origin/main was before the last fetch, which is the base the old series started from. GitHub and GitLab show a similar "compare changes" link after a force push.

Push the rewritten branch:

4. Merge and clean up

Your team merges pull requests with a merge commit, so the branch stays visible in the history. On the hosting service, that is the green button; here it is the same Git operation:

Merge Methods on a Pull Request

GitHub, GitLab and Bitbucket let the repository choose which merge buttons exist. They produce different histories from the same branch. To see them side by side, push a demo branch with two commits, then apply each method in a scratch clone:

Method History on main Keep the branch's commits? Good when
Create a merge commit The branch's commits plus a merge commit Yes, unchanged Commits are clean and meaningful, and you want to see where each change came from
Squash and merge One new commit per pull request No: one commit, new hash Branches have messy "wip" commits; one pull request is one change
Rebase and merge The branch's commits replayed on top, no merge commit Copies with new hashes You want a linear history with every commit kept

--force-rebase in the demo makes new commits even though the branch was already on top of main, as GitHub's button does. Two side effects to know: after a squash or rebase merge, the branch's own commits are not in main, so git branch -d refuses to delete it unless its upstream (for example the same branch on the remote) still contains them (Part 5), and commits that GitHub rewrites on "Rebase and merge" are not signed by you (Part 9). Many teams allow only one method, so the history looks the same everywhere.

Stacked Branches with --update-refs

Sometimes the next change depends on one that is still in review. Instead of waiting, you stack a second branch on top of the first, and open a second pull request with the first branch as its base. The pain used to come when the first branch changed: after you rebased it, the second branch still pointed to the old commits. git rebase --update-refs (Git 2.38+) rewrites every branch in the stack in one go.

Create a stack: docs/alerts adds an alerts page, docs/alerts.md:

# Alerts

The status page has no alerting of its own.

Page the on-call person when a service is down for 5 minutes.
Open a ticket when a service is slow for an hour.
Ignore single failed checks.

and links it from the runbook; docs/alert-examples builds on it with examples:

Review of the first pull request asks for a change in its first commit: say what the rules are for. Make the change on top of the stack as a fixup, then rebase the whole stack with --update-refs:

Both branches moved: docs/alerts now points to the rewritten second commit, and docs/alert-examples sits on top of it. Set git config --global rebase.updateRefs true to make this the default. Pushing the rewritten stack takes one command: git push --force-with-lease --force-if-includes origin docs/alerts docs/alert-examples.

The fixup and the commits after it change different lines of docs/alerts.md. Had the fixup touched the lines right next to a later commit's change, the rebase would have stopped with a conflict at that commit, as any rebase does. Keep fixups small and close to what they fix.

Merge the stack bottom first. Because each branch is now on top of the one below, both merge as fast-forwards:

GitHub added stacked pull requests as a public preview in 2026, with a gh stack CLI extension (gh extension install github/gh-stack) that manages the branches and their pull requests; Stacked pull requests on GitHub looks at what it fixes and when a stack is the wrong shape.

Release Branches and Backports

If you support released versions while main moves on, you need a branch per release line and a way to copy fixes to it. Start release/0.2 from the v0.2.0 tag:

A user reports that the footer is hard to read in dark mode. The fix goes to main first, like every change: a lighter footer color inside the dark scheme block of style.css.

git cherry-pick applies the changes of one commit as a new commit on the current branch. -x adds the "(cherry picked from commit ...)" line, so anyone can trace the backport to the original. Fix on main first and copy to release branches, not the other way round; otherwise the next release can miss the fix. How to cherry-pick commits covers ranges and conflicts.

Choose a Workflow

Workflow How it works Fits
GitHub flow main is always deployable; every change is a short branch and a pull request; deploy after merge Web services and teams that deploy often
Trunk-based development Everyone merges into main at least daily, through very short branches or straight commits; unfinished features hide behind feature flags Teams with good automated tests and continuous delivery
Release branches GitHub flow plus a release/x.y branch per supported version, fixes cherry-picked from main Software that customers install and keep for a while
Gitflow Long-lived main and develop, plus feature/*, release/* and hotfix/* branches with fixed merge rules Scheduled releases of versioned software with several supported versions
Forking workflow Contributors push to their own fork and open pull requests to the original repository Open source, or anywhere contributors cannot push to the main repository

Most teams that deploy a service land on GitHub flow or trunk-based development: few long-lived branches, small pull requests, and main that always works. Gitflow adds a lot of merging for little gain when you deploy continuously; its author added a note to the original article in 2020 saying so, and recommending a simpler workflow such as GitHub flow for software that is delivered continuously (A successful Git branching model). It still fits products with planned releases and several maintained versions.

Whatever you pick, two rules matter more than the name: keep branches short-lived (days, not weeks), and keep main releasable (every merge passes the checks).

Set Up the Repository for a Team

The workflow only holds if the hosting service enforces it. On GitHub, use rulesets (Settings, Rules; the older "branch protection rules" still work) for main:

  • Require a pull request before merging, with at least one approval, and dismiss approvals when new commits are pushed.
  • Require status checks to pass: your CI jobs (the CI/CD guide builds them).
  • Block force pushes and restrict deletions.
  • Optionally require linear history (allows only squash and rebase merges) and require signed commits (Part 9).
  • A merge queue tests each pull request together with everything merged before it, so main stays green when many pull requests merge on the same day. It costs CI time per merge (what one merge costs in CI).

A CODEOWNERS file (in .github/, the repository root or docs/) requests reviews automatically from the people responsible for each path, and with the "require review from code owners" rule, their approval is mandatory:

# .github/CODEOWNERS: the last matching pattern wins
*                @OWNER/web-team
/scripts/        @OWNER/platform-team
/docs/runbook.md @OWNER/on-call

GitLab has the same ideas under other names: protected branches, approval rules, code owners and merge trains.

Review Well

  • Small pull requests. A few hundred changed lines at most. Split big changes into a stack.
  • Describe the why in the pull request, link the issue, and say how you tested it.
  • Test the branch locally when the diff is not enough. gh pr checkout 123 does it in one step; with plain Git on GitHub, git fetch origin pull/123/head:pr-123 fetches the pull request into a branch called pr-123.
  • After a force push, review with range-diff (or the "compare" link) instead of reading everything again.
  • Do not let pull requests sit. A branch that waits for a week needs a rebase and a second review.

Common Mistakes

  • Long-lived feature branches. Merge in small slices behind a feature flag instead.
  • Pushing fixups as "address review" commits and merging them as they are. Squash them with --autosquash before merging, or use squash merges.
  • Fixing a bug only on the release branch. Fix it on main and cherry-pick it to the release.
  • Settings that only exist in people's heads. Put them into rulesets and CODEOWNERS.

Summary

  • A pull request is a branch plus review and checks; keep it small and open it early.
  • Use fixup commits during review and git rebase --autosquash origin/main before merging; git range-diff shows what changed between pushes.
  • Merge commits, squash merges and rebase merges give different histories; pick one for the repository.
  • git rebase --update-refs keeps stacked branches in line.
  • Fix on main, then git cherry-pick -x to release branches.
  • GitHub flow or trunk-based development fits most teams that deploy continuously; rulesets enforce it.

Next: Git History, where you search the history, find the commit that broke something, and undo it.

Keep Learning

Updated:

Tags

GitVersion ControlGitHubBranchingCollaborationDevOps

Found an issue?