agentleFS
ridermwridermw/my-skills/skills/sync-repos/SKILL.md

sync-repos

Skill0 starsChanged 2 months agoMITView on GitHub

Use when the user wants to update or sync many local git repositories at once — "pull latest for all repos", "sync all repos from main", "walk through the repos and pull", "update all my git repos", "fetch all clones", or bulk fast-forward across a folder of git checkouts. Non-destructive, fast-forward-only; never force/reset/stash/push.

3 other projects use a version of this.

Adopted by

Most recently taken up by

  • Other versions3 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 3

    1. egregore-labs/egregore★ 293same name
    2. codemonkey85/PKMDS-Blazor★ 240same name
    3. SmartAssets-io/skills★ 2same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repository5 other files it gives its agents.

The file

Show its rules and text

We split this skill into the rules it gives an agent

14 rules. 0 are common, 8 are distinctive, and 6 are very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 8

    Distinctive

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Update a folder full of git clones in one pass: fetch each, fast-forward the selected default or current branch when safe, then report …

    line 8
  2. Distinctive:

    Do not use this to reconcile a fork with heavy local divergence, resolve merge conflicts, or rebase — those need a per-repo interactive …

    line 23
  3. Distinctive:

    If none of these clearly applies, ask the user for the root before scanning.

    line 33
  4. Very specific:

    Read local state and fetch only needed remotes: Read the current branch, HEAD commit, and dirty flag (git status --porcelain).

    line 51
  5. Very specific:

    Do not use fetch --all: it exits non-zero when *any* remote fails, so a single unrelated broken remote would mark a perfectly healthy repo …

    line 62
  6. Distinctive:

    Fast-forward safely: Every result must be distinguishable — always capture the branch tip before and after the operation and derive the …

    line 85
  7. Distinctive:

    After remote reads, require the checkout branch, HEAD, and porcelain state to match the captured values before reporting checkout-dependent …

    line 90
  8. Very specific:

    Classify history before updating: Use git merge-base --is-ancestor in both directions.

    line 109
  9. Very specific:

    Never: run git reset, git checkout -f, git stash, git rebase, or any push.

    line 115
  10. Very specific:

    When the default branch was updated while another branch is checked out, suffix (on <current>).

    line 123
  11. Distinctive:

    Prefer running the loop and then presenting a clean table to the user rather than dumping raw tab output.

    line 268
  12. Very specific:

    Stale/missing origin/HEAD: use the server's advertised symbolic HEAD, not the cached alias.

    line 290
  13. Distinctive:

    Do not remove lock files or suggest a manual merge for non-divergent history.

    line 296
  14. Distinctive:

    Auth prompts: if a fetch would block on credentials, record it as an error and move on — never hang the whole run on one repo.

    line 301

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Sync Repos — bulk fetch + fast-forward across many local clones

Update a folder full of git clones in one pass: fetch each, fast-forward the selected default or current branch when safe, then report exactly what advanced, what was skipped, and what needs attention.

This is deliberately safe. It only ever runs git fetch and git merge --ff-only / git pull --ff-only. It never force-pushes, resets, checks out over dirty state, stashes, or discards anything. Repos with local changes or diverged history are reported, not touched.

When to use

  • "walk through the repos and pull latest" / "sync all repos from main"
  • "update all my git repos" / "fetch everything under ~/git"
  • Any request to refresh a directory of clones before starting work.

Do not use this to reconcile a fork with heavy local divergence, resolve merge conflicts, or rebase — those need a per-repo interactive decision. Flag such repos in the report and stop.

Inputs

  • root (optional): directory to scan. Default order:
    1. An explicit path the user gave.
    2. The current working directory if it contains 2+ git clones.
    3. ~/git as a fallback. If none of these clearly applies, ask the user for the root before scanning.
  • scope (optional): default-branch (default) or current-branch.
    • default-branch — update each repo's main/default branch (matches "sync from main"). Does not switch away from a dirty or feature branch.
    • current-branch — fast-forward whatever branch is checked out (matches "pull latest").

Procedure

  1. Discover clones. A clone is root itself, or a direct child of root, that contains a .git directory — nothing deeper is scanned. Linked worktrees and submodules, where .git is a file, are skipped (see Edge cases), and node_modules is pruned explicitly because a depth cap alone still matches <root>/node_modules/.git. Pointing at a single clone therefore syncs that clone; pointing at a folder of clones syncs each of them. If root is itself a clone and holds nested clones, both levels are in scope and every one of them appears in the report — nothing is updated invisibly, and every update is still fast-forward-only.
  2. Read local state and fetch only needed remotes. Read the current branch, HEAD commit, and dirty flag (git status --porcelain). Confirm an origin remote exists. Everything here resolves through origin, so check git remote get-url origin: a clone whose remote is named upstream would otherwise pass a bare "some remote exists" guard and then be misreported as no default branch. Then git fetch --prune --quiet origin, plus the current branch's tracking remote when needed for current-branch scope or a dirty checkout's behind-count. Read branch.<current>.remote directly: remote names can contain /, and . means a local upstream branch, not a remote to fetch. A clean default-branch update does not need the feature branch's remote. Do not use fetch --all: it exits non-zero when any remote fails, so a single unrelated broken remote would mark a perfectly healthy repo error: fetch failed. Record error: no origin remote when origin is missing, and error: fetch failed when a remote this run genuinely needs is unreachable.
  3. Resolve the server's default branch when needed, using git ls-remote --symref origin HEAD after fetching. Cached origin/HEAD can retain the old default after a rename; do not guess from main or master. Record both its branch name and advertised HEAD commit for verification. Missing symbolic HEAD means no default branch; a lookup failure or an unfetched advertised branch is an explicit error. A clean current-branch update does not depend on discovering a default branch.
  4. Verify the selected commit before using it. Resolve the selected tracking ref to a commit. For the default branch, compare it with the advertised HEAD commit. For a configured remote upstream, compare it with the exact branch.<current>.merge ref returned by git ls-remote --exit-code <tracking-remote> <merge-ref>. A local upstream (branch.<current>.remote = .) needs no remote check. Preserve configured fetch refspecs: never override an exclusion or fetch an excluded branch separately to make verification pass. A missing, unmapped, stale, or unverifiable selected ref is error: <reason>, before any update or behind-count. Use the verified commit ID for every later count, ancestry check, and update, not a tracking ref that could move.
  5. Fast-forward safely. Every result must be distinguishable — always capture the branch tip before and after the operation and derive the result from the difference. Never report success on exit code alone: merge --ff-only and fetch <b>:<b> both exit 0 when nothing moved, so an exit-code-only check cannot tell advanced from up-to-date. After remote reads, require the checkout branch, HEAD, and porcelain state to match the captured values before reporting checkout-dependent results. Recheck that same condition immediately before any branch update; only initially clean checkouts reach an update. A mismatch is checkout changed (skipped) without retrying that repo.
    • Dirty working tree → do not pull. Record dirty (skipped), N behind, counting with git rev-list --count <captured-HEAD>..<verified-commit> for its upstream, or the default branch when no upstream is configured. Report a verification/counting failure as an error rather than inventing a count.
    • scope=current-branch: if the current branch has no upstream, record no upstream (skipped) — do not silently fall back to the default branch. Otherwise fast-forward to the fetched upstream commit.
    • scope=default-branch, default branch is checked out: fast-forward to the fetched origin/<default> commit.
    • scope=default-branch, default branch is not checked out: update it without checkout by fetching the verified commit from the local repository (git fetch . <verified-commit>:refs/heads/<default>). This avoids a second network fetch racing the ancestry check. A branch checked out in another worktree is in use by another worktree (skipped).
    • Classify history before updating. Use git merge-base --is-ancestor in both directions. If the target already contains the upstream commit, report up-to-date and retain local commits. Only two non-ancestor tips mean diverged. An ancestry error, lock failure, permission failure, or failed update is error: <reason>, with diagnostic stderr preserved — never infer divergence from an operation's exit code alone.
  6. Never run git reset, git checkout -f, git stash, git rebase, or any push. Never pass --force.
  7. Report (table + one-line summary). The full result vocabulary is: advanced N commits · up-to-date · created local <default> · dirty (skipped), N behind · diverged (needs manual merge) · in use by another worktree (skipped) · detached (skipped) · no upstream (skipped) · checkout changed (skipped) · no default branch · error: <reason>. When the default branch was updated while another branch is checked out, suffix (on <current>).

    repobranchresult
    api-servicemainadvanced 6 commits
    web-clientfeature/xdirty (skipped), 2 behind
    shared-libmainup-to-date
    infra-toolsmaindiverged (needs manual merge)
    legacy-svcmainerror: fetch failed

    End with: N repos: X advanced, Y up-to-date, Z skipped, W diverged, V errors. List the diverged/error/dirty repos again as an explicit "needs attention" line so nothing important scrolls off.

Ready-to-run reference

Adapt the root as needed. This is read-only except for fetch + ff-only.

ROOT="${1:-$PWD}"; SCOPE="${2:-default-branch}"
# Never block on a credential prompt — one private repo must not hang the run.
export GIT_TERMINAL_PROMPT=0
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o BatchMode=yes}"
export LC_ALL=C
case "$SCOPE" in default-branch|current-branch) ;;
  *) printf 'error: unsupported scope: %s\n' "$SCOPE" >&2; exit 2;; esac
r() { printf '%s\t%s\t%s\n' "$1" "$2" "$3"; }
error() {
  r "$name" "$target" "error: $1"
  [ -z "$2" ] || printf '%s: %s\n' "$name" "$2" >&2
}
checkout_unchanged() {
  local branch head state
  if branch="$(git -C "$repo" branch --show-current)"; then :;
  else error "cannot revalidate checkout branch" ""; return 1; fi
  if head="$(git -C "$repo" rev-parse --verify HEAD)"; then :;
  else error "cannot revalidate checkout HEAD" ""; return 1; fi
  if state="$(git -C "$repo" status --porcelain)"; then :;
  else error "cannot revalidate working tree" ""; return 1; fi
  if [ "$branch" != "$cur" ] || [ "$head" != "$initial_head" ] || [ "$state" != "$dirty" ]; then
    r "$name" "$target" "checkout changed (skipped)"; return 1
  fi
  return 0
}
find "$ROOT" -maxdepth 2 -name node_modules -prune -o -name .git -type d -print | while IFS= read -r g; do
  repo="$(dirname "$g")"; name="$(basename "$repo")"
  cur="$(git -C "$repo" branch --show-current 2>/dev/null)"
  [ -z "$cur" ] && { r "$name" "detached" "detached (skipped)"; continue; }
  target="$cur"
  git -C "$repo" remote get-url origin >/dev/null 2>&1 || { r "$name" "$cur" "error: no origin remote"; continue; }
  if initial_head="$(git -C "$repo" rev-parse --verify HEAD)"; then :;
  else error "cannot read checkout HEAD" ""; continue; fi
  # Git diagnostics stay on stderr, never inside values parsed as data.
  if dirty="$(git -C "$repo" status --porcelain)"; then :;
  else error "cannot read working-tree state" ""; continue; fi
  if err="$(git -C "$repo" fetch --prune --quiet origin 2>&1)"; then :;
  else error "fetch failed (origin)" "$err"; continue; fi
  upr="$(git -C "$repo" config --get "branch.$cur.remote")"
  if { [ "$SCOPE" = current-branch ] || [ -n "$dirty" ]; } &&
     [ -n "$upr" ] && [ "$upr" != origin ] && [ "$upr" != . ]; then
    if err="$(git -C "$repo" fetch --prune --quiet "$upr" 2>&1)"; then :;
    else error "fetch failed ($upr)" "$err"; continue; fi
  fi
  upstream="$(git -C "$repo" rev-parse --verify --quiet --symbolic-full-name '@{u}' 2>/dev/null)"
  merge_ref="$(git -C "$repo" config --get "branch.$cur.merge")"
  if { [ "$SCOPE" = current-branch ] || [ -n "$dirty" ]; } &&
     [ -z "$upstream" ] && { [ -n "$upr" ] || [ -n "$merge_ref" ]; }; then
    error "configured upstream ref unavailable (check fetch filters)" ""; continue
  fi
  ref="$upstream"; remote="$upr"; expected=""
  if { [ "$SCOPE" = default-branch ] && [ -z "$dirty" ]; } ||
     { [ -n "$dirty" ] && [ -z "$upstream" ]; }; then
    if advertised="$(git -C "$repo" ls-remote --symref origin HEAD)"; then :;
    else error "default branch lookup failed" ""; continue; fi
    def="$(printf '%s\n' "$advertised" | sed -n 's#^ref: refs/heads/\([^[:space:]]*\)[[:space:]]HEAD$#\1#p')"
    [ -n "$def" ] || { r "$name" "$cur" "no default branch"; continue; }
    ref="refs/remotes/origin/$def"; remote=origin
    expected="$(printf '%s\n' "$advertised" | awk '$2 == "HEAD" {print $1}')"
    [ -n "$dirty" ] || target="$def"
  else
    [ -n "$ref" ] || { r "$name" "$cur" "no upstream (skipped)"; continue; }
    if [ "$remote" != . ]; then
      [ -n "$remote" ] && [ -n "$merge_ref" ] ||
        { error "cannot identify configured upstream" ""; continue; }
      if advertised="$(git -C "$repo" ls-remote --exit-code "$remote" "$merge_ref")"; then :;
      else error "upstream lookup failed ($remote $merge_ref)" ""; continue; fi
      expected="$(printf '%s\n' "$advertised" | awk -v ref="$merge_ref" '$2 == ref {print $1}')"
    fi
  fi
  if desired="$(git -C "$repo" rev-parse --verify "$ref^{commit}")"; then :;
  else error "selected ref unavailable ($ref); check fetch filters" ""; continue; fi
  if [ "$remote" != . ]; then
    [ -n "$expected" ] || { error "selected remote commit was not advertised" ""; continue; }
    [ "$desired" = "$expected" ] ||
      { error "stale tracking ref ($ref); check fetch filters or retry" ""; continue; }
  fi
  checkout_unchanged || continue
  if [ -n "$dirty" ]; then
    if behind="$(git -C "$repo" rev-list --count "$initial_head..$desired")"; then
      r "$name" "$cur" "dirty (skipped), $behind behind"
    else error "cannot count commits behind" ""; fi
    continue
  fi
  suffix=""; [ "$target" = "$cur" ] || suffix=" (on $cur)"
  if [ "$target" = "$cur" ]; then before="$initial_head"
  else before="$(git -C "$repo" rev-parse --verify --quiet "refs/heads/$target")"; fi
  if [ -n "$before" ]; then
    if git -C "$repo" merge-base --is-ancestor "$desired" "$before"; then
      r "$name" "$target" "up-to-date$suffix"; continue
    else
      rc=$?; [ "$rc" -eq 1 ] || { error "cannot compare history" ""; continue; }
    fi
    if git -C "$repo" merge-base --is-ancestor "$before" "$desired"; then :;
    else
      rc=$?
      if [ "$rc" -eq 1 ]; then r "$name" "$target" "diverged (needs manual merge)"
      else error "cannot compare history" ""; fi
      continue
    fi
  fi
  checkout_unchanged || continue
  if [ "$target" = "$cur" ]; then
    if err="$(git -C "$repo" merge --ff-only "$desired" --quiet 2>&1)"; then :;
    else error "merge failed" "$err"; continue; fi
  else
    if err="$(git -C "$repo" fetch --quiet . "$desired:refs/heads/$target" 2>&1)"; then :;
    else
      case "$err" in
        *"checked out at"*|*"current branch"*) r "$name" "$target" "in use by another worktree (skipped)";;
        *) error "local branch update failed" "$err";;
      esac
      continue
    fi
  fi
  if after="$(git -C "$repo" rev-parse --verify "refs/heads/$target")"; then :;
  else error "cannot read updated branch" ""; continue; fi
  if [ -z "$before" ]; then r "$name" "$target" "created local $target$suffix"
  elif [ "$before" = "$after" ]; then r "$name" "$target" "up-to-date$suffix"
  elif count="$(git -C "$repo" rev-list --count "$before..$after")"; then
    r "$name" "$target" "advanced $count commits$suffix"
  else error "cannot count advanced commits" ""; fi
done

Prefer running the loop and then presenting a clean table to the user rather than dumping raw tab output. If there are many repos, run the fetches and summarize; do not narrate each repo.

Edge cases

  • Worktrees / submodules: discovery matches only a .git directory, so linked worktrees and submodules (where .git is a file) are skipped by construction. Do not auto-update submodules unless asked.
  • Branch checked out elsewhere: a needed local ref update refuses when the branch is checked out in another worktree. Report in use by another worktree (skipped), not diverged. If the branch already contains the upstream tip, no update is needed and it is up-to-date.
  • No origin remote: everything here resolves through origin, so a repo whose only remote is named something else (upstream on a fork, a renamed remote) must be reported, not silently misread as no default branch. Guard with git remote get-url origin. Report error: no origin remote.
  • Several remotes: fetch only origin and, when needed for a current-branch merge or dirty behind-count, the configured tracking remote. . is local; names containing / are used intact. fetch --all couples success to remotes it never reads — one broken remote fails the whole fetch and the repo is reported as error: fetch failed while origin is perfectly healthy.
  • Stale/missing origin/HEAD: use the server's advertised symbolic HEAD, not the cached alias. Missing server HEAD is no default branch; a network failure is an error. Current-branch scope needs neither when clean.
  • Ahead-only history: preserve the extra local commits and report up-to-date — nothing from the upstream is missing.
  • Locks or permissions: report the operational error and its diagnostic. Do not remove lock files or suggest a manual merge for non-divergent history.
  • Detached HEAD: report detached (skipped), never fast-forward.
  • No upstream (current-branch scope): report no upstream (skipped) rather than quietly switching to the default branch — a silent scope change is worse than a skip.
  • Auth prompts: if a fetch would block on credentials, record it as an error and move on — never hang the whole run on one repo.
  • Large trees: cap discovery depth at 2; if the user points at a huge root, confirm scope before scanning thousands of directories.

Stop condition

Done when every discovered repo has a recorded result and the summary line is produced. Anything requiring a human decision (diverged, dirty with important changes, auth error) is surfaced in the "needs attention" line — do not attempt to resolve it automatically.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.