Git, CI/CD & GitOpsXXI · Fetch vs PullFetchVsPull
The IaC and team pull policy — choosing one rule and enforcing it
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
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
mainwith the message “Merge branch origin/main into main” was produced by an unattendedgit pull. The reviewer cannot tell from the message what was integrated or why. - Accidental force-pushes. An engineer with
pull.rebase = truein their~/.gitconfigrebases 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.4branch see different histories because one rebased and one merged. Theirgit log --graphoutputs 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:
- Which branches are shared? The team’s
mainand release branches are shared by definition; feature branches are typically personal until they are merged. The policy treats each class differently. - 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”.
- 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.
- 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:
- Shared branches:
main,master,release/*,production. Personal branches: anything underfeature/*,fix/*,chore/*. - Who pushes: CI pushes to shared branches via merge commits from reviewed pull requests. Engineers do not push directly to shared branches.
- History shape: Linear for personal branches (rebase mode), explicit merge nodes for shared branches (merge mode), merge commits only from reviewed PRs.
- 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
mainwith no PR. A merge commit that was not produced by a reviewed pull request is the signature of an unattendedgit 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 --forceinstead 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/mainto “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 --graphoutputs 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
- 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.
- 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.
- Lint in CI. The lint is what catches engineers whose
~/.gitconfighas drifted from the repo config. A policy that is not linted is a policy that is not enforced. - 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.
- 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
Q1. What are the three pieces a team needs to enforce a pull policy beyond simply writing it down?
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.
Q3. Name the four questions a team must answer to set a pull policy, and explain how the answers compose into a single policy.
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.