Pull, Commit, Push on a Shared Repo
The pull → commit → push loop, and why each step exists once more than one person or agent commits to the same repo. It’s built around a real incident: four commits from a colleague landed on a laptop while someone else was mid-edit in the same file. You get the diagnosis, not just the happy path.
Sanitized — no real hosts, paths, or project names. Continues the sensor-logger example, now with more than one contributor. Assumes Git-from-First-Principles: this page leans hard on the fact that a branch is a pointer and git pull moves it, which is the whole explanation for the incident below.
You can run the loop against your fork of the lab repo from Start-Here-What-Git-Is — a fork gives you a remote you own, so every push here is one you’re allowed to make.
The scenario
sensor-logger is the small C project from the rest of this series — it reads a probe, clamps out-of-range values, and a watchdog alerts if no reading has arrived in 48 hours. It lives on a shared remote, and three people work on it.
| Who | Machine | Typically does |
|---|---|---|
| Ana | dev-laptop (Windows) | Interactive work; owns the reading and clamp code |
| Ben | build-server (Linux) | Cron watchdog, analytics; owns the deployment |
| Cy | dev-laptop (CLI) | Occasional contributor; works on Ana’s machine |
Nobody is coordinating in real time. Ben can push from the Linux box at any moment, and that single fact is what makes the loop matter.
Whether the three are people or AI agents changes nothing — the mechanics are identical, and an agent is if anything more likely to act on a stale read because it never gets a feeling that something looks off.
Why a shared repo is different
On a solo repo your working tree only changes when you change it. Read a file, edit it, commit — nothing moved underneath you.
On a shared repo, the tree can change between your read and your write. Not because someone edited your files remotely — that never happens — but because you pulled, or a tool did. A pull moves your branch pointer and rewrites your working tree from someone else’s commits. Any assumption you formed before it is now suspect.
The loop exists to shrink that window and to make it obvious when it happens anyway.
What actually happened
- Cy read
sensor.hto check the valid range before touching the clamp. It said#define MAX_VALID 4095. - Ana ran
git pulla few minutes later. It fast-forwarded Ben’s commits onto the laptop. - Cy tried to edit the line it had read. The edit was refused — the file no longer matched what had been read.
- Re-reading showed
#define MAX_VALID 1023. Ben had switched the build to the 10-bit TMP-2 probe, which is correct for the hardware on his bench.
Nobody was careless, and Ben’s change was right for Ben’s machine. But the calibration table in README.md still described TMP-1 as 0–4095, and every reading above 1023 now silently clamped down to it. Had Cy’s edit gone through blind, it would have been written against a constant that no longer meant what Cy believed it meant.
That is the failure mode a shared repo produces, and no amount of care by any one person prevents it — which is why the loop below has a re-read step and a look-at-the-output step instead of trusting anybody’s memory.
Diagnosing “did the tree move under me?”
The confusing part: git diff was empty. Files had clearly changed, but git reported no local modifications. That combination is the tell.
git diff # empty -> working tree matches HEAD
ls -la --time-style=full-iso # but mtimes are minutes old, and there are new files
Empty diff plus fresh mtimes plus files you don’t recognize means HEAD moved — not “someone edited my working copy.” From Git-from-First-Principles you already know why the diff is empty: pull moved the branch pointer and rewrote the working tree to match it, so the two agree perfectly. They just agree about a different commit than the one you read.
Confirm it with the journal:
$ git reflog -3
50aa492 HEAD@{0}: pull --ff-only origin main: Fast-forward
d9147b8 HEAD@{1}: clone: from https://github.com/<you>/sensor-logger.git
Then git log --oneline -6 shows exactly whose commits arrived.
Rule of thumb: if a file’s contents surprise you, check
git reflogbefore you doubt your own memory.git difftells you what you changed; the reflog tells you what happened to the branch. More on it in Undoing-Things-in-Git.
The loop
1. Pull before you stage
git pull --ff-only
--ff-only is the safety. It refuses, rather than auto-creating a merge commit, if the branches have diverged. You want that refusal — it tells you to stop and think instead of silently entangling two histories. (What “diverged” means, and what to do about it, is Merging-Rebasing-and-Reading-History.)
Already have uncommitted work? Bank it, pull, restore:
git stash push -u -m "wip: describe it"
git pull --ff-only
git stash pop
See Using-git-stash-Safely for what -u is doing there, and for what happens when that pop lands on top of someone else’s changes and conflicts. Short version: a conflicted pop never loses your work.
2. Verify you’re on solid ground
git log --oneline -1 # what is HEAD now?
git status -sb # branch, ahead/behind, changed files
Then re-read any file you plan to edit. Notes you took before the pull may describe code that no longer exists. This is the step that would have caught the incident above.
3. Make the change, then prove it runs
make test; echo "exit: $?"
(That step needs a C compiler. The git exercises in this course don’t, so skip it if you haven’t got one — everything else on this page still works.)
Run it, because a change that passes review but was never executed is a guess. But running it is not where the job ends, and Ben’s commit is the proof. With MAX_VALID switched to 1023, the suite still passes:
$ make test
test_sensor:
ok negative readings are handled
ok full-scale readings are handled
Both checks green — and the program’s behaviour changed anyway:
$ ./build/sensor
raw= -12 clamped= 0
raw= 0 clamped= 0
raw= 512 clamped= 512
raw= 4096 clamped= 1023
That last line read clamped= 4095 before Ben’s commit. The tests assert that out-of-range readings are handled, not what they are clamped to, so a genuine regression walked straight through a green suite. Passing tests mean “nothing I asserted broke” — they never mean “nothing broke.” Read the output as well.
4. Review what you’re about to commit
git status --short
git diff --stat
git diff # actually read it
Never git add -A without looking first — that’s how editor configs, credentials, and unrelated work get swept in. Ignoring-Files-in-Git is how you stop most of that happening by accident.
5. Commit with a message that says who and why
git add -A
git commit
Where several people or agents share a repo, identify the author in the subject, because git log is the only place Ben will ever see that Cy touched his code. Every contributor commits through the same account on shared machines, so the git author field can’t tell them apart:
[Cy] fix(range): pick the upper bound from the configured probe
That’s the Conventional-Commits format with a contributor tag in front — the tag says who, the type and scope say what kind of change. The body explains why, and flags anything you deliberately left undone:
MAX_VALID 1023 is right for the TMP-2 on Ben's bench, but it silently
clamped every TMP-1 reading above 1023 on the laptop, and make test stayed
green because it never asserts the clamped value. Selects the bound from
the configured probe rather than hardcoding either one.
tests/test_sensor.sh still only checks that readings are handled, not what
they clamp to - left for Ben, since tightening it means agreeing what the
TMP-3 connector reports.
That last paragraph is worth more than the code change. It stops the next person re-litigating a decision you already made on purpose.
6. Push, then verify it landed
git push origin main
Don’t trust the absence of an error — check:
git fetch origin
git log --oneline -1 origin/main # should be your commit
git status -sb # should show no ahead/behind
When push is rejected
You will see one of two messages, and the difference tells you something:
! [rejected] main -> main (fetch first)
! [rejected] main -> main (non-fast-forward)
fetch first means the remote has commits you have never seen — you haven’t fetched since they landed. non-fast-forward means you have fetched, and your branch and the remote’s have genuinely diverged: you both built on the same commit in different directions.
Either way somebody pushed while you were working, and this is the loop telling you it worked. You are being stopped from overwriting their commits. Recover by replaying your work on top of theirs:
git pull --rebase origin main
# resolve any conflicts, then:
git push origin main
Never reach for git push --force here. On a shared repo it deletes the other contributor’s commits from the remote, and on an unattended box like a build server nobody notices until something breaks days later. The one rule from Merging-Rebasing-and-Reading-History applies with full force: those commits are shared history.
Full sequence (copy/paste template)
# 0. Land somewhere clean
git stash push -u -m "wip" # only if you have uncommitted work
git pull --ff-only
git stash pop # only if you stashed
# 1. Know where you are
git log --oneline -1
git status -sb
# 2. ... make changes; RE-READ the files first ...
# 3. Prove it runs
make test; echo "exit: $?"
# 4. Review before staging
git status --short
git diff
# 5. Commit, tagged with the contributor
git add -A
git commit -m "[Cy] fix(range): short imperative subject
Why, plus anything deliberately left undone."
# 6. Push and verify
git push origin main
git fetch origin && git log --oneline -1 origin/main
The part that isn’t git
Pushing fixed the laptop and it fixed the repo. It did not fix the watchdog running on the build server, because that is a compiled binary built from a checkout weeks old — not a live copy of your source. It kept failing until Ben pulled and rebuilt and restarted it.
Git syncs repositories. It does not sync deployments, cron jobs, running processes, or binaries. When a change has consumers beyond the repo, list them in the commit body and tell the humans — the loop has no step that does it for you.
Next: Using-git-stash-Safely — the traps in step 1’s stash, and how to get your work back when a pop conflicts.