Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXXI · Fetch vs PullFetchVsPull

The IaC and team pull policy — choosing one rule and enforcing it

Intermediate⏱ ~22 mingit

What you'll learn

  • Articulate the cost of inconsistent pull strategies across a team — divergent histories, accidental force-pushes, noisy merge commits
  • Identify the four questions a team must answer to choose a pull policy: which branches are shared, who pushes, what history shape is desired, and what CI verifies
  • Encode the team pull policy in repo config (pull.rebase + per-branch overrides + setup script) so new engineers get it for free
  • Add a CI lint that asserts the effective pull config matches the policy before any deploy step
  • Diagnose the symptoms of an inconsistent pull policy and recommend a remediation path

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A team that has not decided how git pull should behave across its members will discover the consequences in production: divergent histories, accidental force-pushes, merge commits no one reviewed, audit trails that cannot be reconstructed. The fix is not to ban git pull or to force one mode on everyone. The fix is to choose one policy for the team, encode it in the repository’s configuration, and verify it in CI. This lesson is the synthesis of the previous five: what the team pull policy should be, how to write it down, how to enforce it, and how to recognise when it has drifted.

The cost of inconsistency

When every engineer on a team picks their own pull strategy (some merge, some rebase, some ff-only, some pull with autostash, some pull with custom flags), the shared history becomes the union of every individual’s habits. The consequences:

  • Merge commits of unknown origin. A merge commit on main with the message “Merge branch origin/main into main” was produced by an unattended git pull. The reviewer cannot tell from the message what was integrated or why.
  • Accidental force-pushes. An engineer with pull.rebase = true in their ~/.gitconfig rebases a branch that has already been pushed, then pushes the rewritten SHAs with --force. Other engineers’ clones diverge. The team loses an afternoon to coordination.
  • Divergent histories across clones. Two engineers on the same release/v1.4 branch see different histories because one rebased and one merged. Their git log --graph outputs disagree on the order of commits.
  • Audit gaps. An auditor reading the shared history cannot tell whether a force-push was intentional or accidental, because the policy was never written down.

The cost is paid in incidents, in coordination overhead, in time spent diagnosing divergent clones, and in the slow erosion of trust in the repository as the system of record.

The four questions a team must answer

A pull policy is a single decision that answers four questions:

  1. Which branches are shared? The team’s main and release branches are shared by definition; feature branches are typically personal until they are merged. The policy treats each class differently.
  2. Who pushes to the shared branches? If only CI pushes (via merge commits from reviewed pull requests), the shared branch is a write-once artefact and the policy for engineers pulling from it is “fast-forward only”. If engineers also push directly (a less mature workflow), the policy is “merge mode only, no force-pushes ever”.
  3. What history shape do we want? Linear (rebase-mode pulls, no merge nodes) is easier to read; non-linear (merge-mode pulls, explicit merge nodes) is easier to audit because every integration point is documented. Either is a valid choice; the team must pick one.
  4. What does CI verify? CI can lint the effective pull config (asserting that the per-branch overrides match the policy), refuse non-fast-forward pulls, refuse merge commits on protected branches, or refuse force-pushes. The answer to this question determines whether the policy is enforced or merely recommended.
flowchart LR
    A["team pull policy"] --> B["which branches are shared?"]
    A --> C["who pushes?"]
    A --> D["what history shape?"]
    A --> E["what does CI verify?"]
    B --> F["encode in repo config\n+ setup script"]
    C --> F
    D --> F
    E --> G["CI lint\n+ branch protection\n+ pre-deploy assertions"]

The four answers compose into a single policy. A typical infrastructure team’s answers:

  1. Shared branches: main, master, release/*, production. Personal branches: anything under feature/*, fix/*, chore/*.
  2. Who pushes: CI pushes to shared branches via merge commits from reviewed pull requests. Engineers do not push directly to shared branches.
  3. History shape: Linear for personal branches (rebase mode), explicit merge nodes for shared branches (merge mode), merge commits only from reviewed PRs.
  4. CI verification: PR-only pushes (branch protection), ff-only on CI clones, lint asserting the per-branch rebase config, no force-push detection on shared branches.

Encoding the policy in the repo

The policy becomes real when it is written down in code that runs automatically, not in a wiki page that nobody reads. The three pieces are:

1. The repo-level config (set by a setup script in the repository):

#!/usr/bin/env bash
# scripts/setup-git-config.sh
# Run once after cloning. Idempotent.

set -euo pipefail

# Default: rebase on personal branches
git config pull.rebase true

# Override: merge on shared branches (preserve SHAs, document integration)
for branch in main master production; do
    git config "branch.${branch}.rebase" false
done

# Match release/* branches too
git config --get-regexp '^branch\.release\..*\.rebase$' \
    | while read -r key _; do
        git config "${key%.rebase}.rebase" false
    done

# Prune dead refs on every fetch
git config fetch.prune true

# Push only the current branch by default; never push tags implicitly
git config push.default current
git config --unset push.followTags 2>/dev/null || true

echo "git config set for this clone."

The for loop over release/* uses git config --get-regexp to discover every release branch that has been worked on recently and sets its rebase override. New release branches get the override the first time an engineer runs the script.

2. The CI lint (runs as the first step of every pipeline):

#!/usr/bin/env bash
# ci/lint-pull-config.sh
# Asserts the effective pull config matches the team policy.

set -euo pipefail

CURRENT_BRANCH="$(git symbolic-ref --short HEAD)"

# Per-branch expectations
case "$CURRENT_BRANCH" in
    main|master|production)
        expected_rebase=false
        ;;
    release/*)
        expected_rebase=false
        ;;
    feature/*|fix/*|chore/*)
        expected_rebase=true
        ;;
    *)
        echo "Unknown branch class: $CURRENT_BRANCH"
        exit 1
        ;;
esac

# Read the effective setting (per-branch override wins over repo default)
actual_rebase="$(git config --get "branch.${CURRENT_BRANCH}.rebase" \
    || git config --get pull.rebase \
    || echo false)"

if [ "$actual_rebase" != "$expected_rebase" ]; then
    echo "Pull config mismatch on $CURRENT_BRANCH:"
    echo "  expected: rebase=$expected_rebase"
    echo "  actual:   rebase=$actual_rebase"
    echo "Run scripts/setup-git-config.sh to fix."
    exit 1
fi

echo "Pull config OK on $CURRENT_BRANCH (rebase=$actual_rebase)."

The lint runs as the first step of every CI pipeline. If the effective setting does not match the policy, the pipeline fails with a clear message and the engineer is pointed at the setup script that will fix it.

3. Branch protection (set on the Git host — GitHub, GitLab, Gitea):

  • Require pull-request reviews before merge to main, master, release/*, production.
  • Disallow direct pushes to those branches.
  • Disallow force-pushes to those branches.
  • Require status checks (including the lint above) to pass before merge.

Branch protection is the host-side enforcement of the policy; the repo config and CI lint are the client-side enforcement. Together, they make the policy hard to circumvent.

Diagnosing a drifted policy

The symptoms of a team whose pull policy has drifted:

  • Merge commits on main with no PR. A merge commit that was not produced by a reviewed pull request is the signature of an unattended git pull. The fix is branch protection that disallows direct pushes.
  • Force-pushes on shared branches. A force-push that was not coordinated is the signature of a rebase-mode pull followed by git push --force instead of --force-with-lease. The fix is branch protection that disallows force-pushes and CI linting that catches rebase-mode pulls on shared branches.
  • Engineers running git reset --hard origin/main to “fix” their clone. A reset is the recovery from a rebase gone wrong; if engineers are doing it regularly, the rebase is happening too often on the wrong branches. The fix is per-branch overrides.
  • Different git log --graph outputs across teammates. Two engineers on the same branch seeing different histories means one of them rebased and the other merged. The fix is consistency: choose one mode per branch class and enforce it.

The diagnostic is the same in every case: ask each engineer to run git config --show-origin --get-regexp '^(pull|branch)\.' and compare the outputs. The differences are the drift.

Production discipline

  1. Choose the policy first; enforce it second. A policy that has not been agreed is a policy that will be argued about every time it bites. Get the four questions answered and the answers written down before shipping the setup script.
  2. Encode in the repo, not in the wiki. The setup script is the policy. The wiki page is documentation about the policy. New engineers run the script; they do not read the wiki.
  3. Lint in CI. The lint is what catches engineers whose ~/.gitconfig has drifted from the repo config. A policy that is not linted is a policy that is not enforced.
  4. Branch protection on the host. The repo config and CI lint are client-side; branch protection is server-side. Without server-side enforcement, an engineer with direct push access can bypass the policy.
  5. Audit the policy periodically. The team grows, repositories grow, branches come and go. The setup script needs updates as new branch classes appear (e.g., hotfix/* for security patches); the lint needs updates as new exceptions are added; the branch protection rules need review when the team structure changes.

Cross-course references

  • Terraform for Production Sysadmins - Part XVIII (TeamConfig) is the canonical example of a team pull policy for an infrastructure repository, with a setup script, a CI lint, and branch protection rules.
  • GitOps with Argo CD - Part VI (MultiTenant) extends the pull policy concept to multi-tenant GitOps, where each tenant’s repository has its own setup script and lint, but the team-wide policy is consistent.
  • Ansible for Production Sysadmins - Part XL (BranchPolicy) covers the Ansible repository’s pull policy, with the same three pieces (script, lint, branch protection) and a fourth: a CODEOWNERS file that defines who reviews changes to the policy itself.

Quiz

Knowledge check · 4 questions

  1. Q1. What are the three pieces a team needs to enforce a pull policy beyond simply writing it down?

  2. Q2. A team that has documented its pull policy in a wiki page but has not encoded it in repo config, CI lint, or branch protection has enforced the policy.

  3. Q3. Name the four questions a team must answer to set a pull policy, and explain how the answers compose into a single policy.

  4. Q4. A team discovers their shared `main` branch has been force-pushed three times in the past month by three different engineers, each of whom was running `git pull` with rebase mode (because of a global setting). Design a remediation plan.

    A 12-person infrastructure team has been operating for six months without an explicit pull policy. Three engineers have set `pull.rebase = true` in their `~/.gitconfig`. Over the past month, each has at some point pulled `main` with rebase mode, rewritten their local commits ahead of origin/main, and force-pushed the rewritten SHAs. Other engineers' clones have diverged each time. The team lead asks you to design a remediation plan that prevents future incidents and recovers trust in the shared history.

Passing score: 75%. Answers are checked in this browser.