Repo Sync — safe git updates on a server
Distro support
Fully portable. This skill is git-only and identical on both families
(Debian/Ubuntu and the RHEL family: Fedora, RHEL, CentOS Stream, Rocky, Alma,
Oracle). The doctrine below — pull --rebase --autostash, porcelain
dirty-checks, never reset --hard/clean -fd — is distro-neutral. The only
difference is installing git itself.
| Concept | Debian/Ubuntu | RHEL family |
|---|---|---|
| Install git | apt install git |
dnf install git |
Everything else (git pull --rebase --autostash, status, conflict recovery) |
identical | identical |
In sk-* scripts use pkg_install git from common.sh if you must ensure git
is present. See docs/multi-distro/plan.md.
Use when
- Writing, reviewing, or running any script that pulls git repos on a server (a repo-update menu, a deploy hook, a cron sync).
- Updating one or many repos on a managed host as part of deployment or routine maintenance.
- Deciding how an automated update should treat local, uncommitted, or untracked changes in a server-side working tree.
Do not use when
- The task is a one-off
git pulla developer runs by hand in their own checkout and watches the output of. - The task is authoring the generic script scaffold rather than the git
behaviour; for the script contract load
linux-bash-scriptingfirst, then apply this doctrine to the git steps.
Required Inputs
| Artefact | Source | Required? | If absent |
|---|---|---|---|
| Repository path(s) and intended branch | Operator or approved registry | yes | Skip the target; never search and update arbitrary repos |
| Interactive or unattended mode | Scheduler/operator | yes | Default to report-only planning |
| Remote/authentication and post-pull command | Repository owner | for update/build | Do not infer credentials or run a build |
| Deployment/change authority | Service owner | for integration | Fetch/status only; do not integrate commits |
Capability Contract
Read/search status and fetch inspection may be read-only. Pull/rebase changes the working tree and needs explicit authority. Never delete, reset, auto-resolve, force-push, expose credentials, or run an unapproved post-pull command.
Degraded Mode
If network, credentials, Git, upstream tracking, or a clean conflict state is unavailable, preserve the tree and report branch, status, and the failed step. Do not call a fetched or conflicted repository updated.
Decision Rules
| State | Action | Failure avoided |
|---|---|---|
| Dirty tracked work | Warn and use --rebase --autostash |
Lost local edits |
| Untracked files present | Preserve them and report collision risk | Deleted uploads/config |
| Rebase or stash conflict | Stop with recovery commands | Destructive auto-resolution |
| Detached HEAD or missing upstream | Stop and request intended branch | Updating the wrong history |
Workflow
- Confirm the path is a git working tree (
<path>/.gitexists). - Check the working tree state with
git status --porcelainbefore touching it. - Pull with
git pull --rebase --autostashso local work is stashed, the rebase runs, and the work is re-applied on top. - On conflict, stop and tell the operator how to recover — never discard.
- Leave untracked files in place, always.
- Run any post-pull build only after a clean, successful update.
Quality standards
- An update must never destroy uncommitted or untracked work.
- A dirty working tree is reported to the operator, never silently wiped.
- A failed rebase or stash re-apply leaves the operator a clear recovery path.
- The same script is safe to run twice (idempotent) and safe to run on a repo someone edited five minutes ago.
Anti-patterns
git reset --hard HEADin automation. Fix: preserve local work with rebase/autostash and stop on conflict.git clean -fdor-fdx. Fix: leave untracked files untouched and report collisions.- Resetting local changes as a routine pre-pull step. Fix: inventory and preserve the dirty state.
- Auto-resolving or auto-aborting a rebase/stash conflict. Fix: leave state intact for the owner.
- Removing uploads,
.env, or generated config to avoid conflicts. Fix: stop and report the conflicting path.
Outputs
| Artefact | Consumer | Acceptance condition |
|---|---|---|
| Per-repo update result | Operator | Records branch, before/after commit, dirty state, and outcome |
| Conflict handoff | Repository owner | Gives exact status plus safe continue/abort/stash recovery |
| Post-pull result | Service owner | Runs only after successful integration and records exit status |
Evidence Produced
| Artefact | Acceptance |
|---|---|
| Repository update evidence | Includes porcelain status, branch/upstream, before/after IDs, pull/autostash state, build exit, skipped repos, and redacted credentials |
Capability Recovery
On conflict, leave Git's state intact and show git status, git rebase --continue, git rebase --abort, and git stash list. Never choose the recovery path for the owner.
Worked Example
A production checkout has a modified tracked config and an untracked upload. Report both, run the authorised git pull --rebase --autostash, leave the upload untouched, and record commit IDs. If the autostash conflicts, stop before the build and hand the exact recovery state to the owner.
References
../../docs/continuous-improvement/safe-reversible-operations-standard.md../../docs/continuous-improvement/value-stream-5s-qc-story.mdreferences/safe-update-pattern.md
The doctrine (binding on every repo-update script)
This is a STANDARD, not a suggestion. It exists because a repo-update menu
script once ran git reset --hard HEAD + git clean -fd before pulling and
silently wiped a developer's uncommitted edits. That must never be possible
again on any server we manage.
- NEVER use
git reset --hardin an automated or menu repo-update script. It destroys uncommitted changes to tracked files with no undo. - NEVER use
git clean -fd(or-fdx) in an automated or menu repo-update script. It deletes untracked files — uploads,.env, generated config — that git is not tracking precisely because they must survive. - Prefer
git pull --rebase --autostash.--autostashstashes any local changes before the rebase and re-applies them afterwards, so local work is preserved through the update. This is the default pull command for every server-side update. - Detect a dirty working tree before pulling with
git status --porcelain. If it is non-empty, warn the operator that local changes exist (and will be stashed and re-applied, not discarded). Never discard them. - On a rebase or stash-reapply conflict, STOP. Do not auto-resolve, do
not auto-abort. Report the conflict and the recovery options to the
operator:
git rebase --continueafter resolving the conflicting files, orgit rebase --abortto return to the pre-pull state, andgit stash list— the autostash is preserved here; recover it withgit stash poponce the tree is clean.
- Untracked files are left in place, always. An update never removes a file git is not tracking.
The safe pattern (copy-paste)
update_repo_safely() {
local path="$1"
[[ -d "$path/.git" ]] || { echo "not a git repo: $path" >&2; return 1; }
cd "$path" || return 1
# 1. Detect a dirty working tree — warn, never wipe.
if [[ -n "$(git status --porcelain)" ]]; then
echo "WARNING: $path has local changes."
echo " They will be stashed and re-applied, not discarded."
fi
# 2. Pull with rebase + autostash: local work is preserved.
if git pull --rebase --autostash; then
echo "updated: $(git rev-parse --abbrev-ref HEAD) -> $(git log -1 --oneline)"
else
# 3. Conflict — stop and hand the operator a recovery path.
echo "ERROR: pull/rebase hit a conflict in $path." >&2
echo " Resolve the files, then: git rebase --continue" >&2
echo " Or roll back the pull: git rebase --abort" >&2
echo " Your stashed local work: git stash list (recover with: git stash pop)" >&2
return 1
fi
}
What this never does: no git reset --hard, no git clean, no removal of
untracked files, no automatic conflict resolution.
Canonical script on this server
/usr/local/bin/update-all-repos is the menu-driven repo-update tool that
must exist on every managed server (see
notes/update-all-repos-setup.md). It
must follow this doctrine: a porcelain dirty-check plus
git pull --rebase --autostash, never git reset --hard + git clean -fd.
The sk-update-all-repos script (linux-site-deployment) is the engine
version of the same tool and is held to the same standard.
If you find any repo-update script on a server still doing git reset --hard
or git clean -fd, that is a bug to fix, not a pattern to copy.
Scripts
This skill installs the following scripts to /usr/local/bin/. To install:
sudo install-skills-bin linux-repo-sync
| Script | Source | Core? | Purpose |
|---|---|---|---|
| sk-update-all-repos | scripts/sk-update-all-repos.sh | no | Pull every registered git repo on this server using git pull --rebase --autostash and a porcelain dirty-check; interactive menu plus --all/--repo flags. This is the engine implementation of the safe-update doctrine above (shared with linux-site-deployment, which installs it as a core tool). |
Verify
# A repo-update script is safe iff this returns nothing:
grep -nE 'git[[:space:]]+reset[[:space:]]+--hard|git[[:space:]]+clean[[:space:]]+-fd' /usr/local/bin/update-all-repos
# And iff it pulls with autostash:
grep -n 'pull --rebase --autostash' /usr/local/bin/update-all-repos