Git Developer Guide
How to git pull or fetch every repo in a folder at once
Fetch everything first
If your repositories sit side by side in one folder, a short loop updates all of them. Start with git fetch: it downloads new commits and moves remote-tracking branches such as origin/main, but never touches your files or your local branches, so it is safe to run on every repo, including ones with work in progress.
for repo in ~/Projects/*/; do
repo=${repo%/}
[ -e "$repo/.git" ] || continue
echo "Fetching $repo"
git -C "$repo" fetch --all --prune --quiet || echo " fetch failed: $repo"
done
Folders that are not repositories are skipped. --all fetches every remote, and --prune drops remote-tracking branches that were deleted on the server. Because "$repo" is always quoted, paths with spaces work, and the loop pastes as-is into zsh (the macOS default) or bash.
Repos nested deeper, or fetched in parallel
If some repos live inside group folders, let find locate them. -print0 and read -d '' pass each path through intact, whatever characters it contains:
find ~/Projects -maxdepth 4 -name .git -prune -print0 |
while IFS= read -r -d '' gitdir; do
repo=${gitdir%/.git}
echo "Fetching $repo"
git -C "$repo" fetch --all --prune --quiet </dev/null || echo " fetch failed: $repo"
done
Fetching is mostly waiting on the network, so with dozens of repos it pays to run eight at a time:
find ~/Projects -maxdepth 4 -name .git -prune -print0 |
xargs -0 -n 1 -P 8 sh -c '
repo=${1%/.git}
git -C "$repo" fetch --all --prune --quiet || echo "fetch failed: $repo"
' sh
Pull or fetch?
- Fetch only updates what you know about the remote. Nothing in your working tree changes, so it can never lose work.
- Pull is a fetch followed by a merge or rebase into the branch you currently have checked out. Your other local branches stay where they were.
- Diverged branches are where bulk pulls hurt. With no
pull.rebaseorpull.ffsetting, current Git refuses with Need to specify how to reconcile divergent branches. With one set, it will merge or start a rebase in repos you are not looking at.
The safe pattern is to fetch everything, pull with --ff-only only where it is a clean fast-forward, and handle the rest by hand.
A pull loop that leaves dirty repos alone
for repo in ~/Projects/*/; do
repo=${repo%/}
[ -e "$repo/.git" ] || continue
if [ -n "$(git -C "$repo" status --porcelain --untracked-files=no)" ]; then
echo "SKIP $repo (uncommitted changes)"
elif ! git -C "$repo" rev-parse --quiet --verify '@{u}' >/dev/null; then
echo "SKIP $repo (detached HEAD or no upstream)"
elif git -C "$repo" pull --ff-only --quiet; then
echo "OK $repo"
else
echo "FAIL $repo (diverged: sort it out by hand)"
fi
done
- Uncommitted changes: Git aborts a pull that would overwrite a file you have edited, but one that succeeds leaves new upstream code mixed into half-finished work. The loop skips those repos outright.
- Untracked files are ignored by the check. If an incoming file would overwrite one, Git stops and the loop prints
FAIL. - No upstream:
@{u}fails on a detached HEAD, a branch that was never pushed, or an upstream deleted on the remote. - Diverged:
--ff-onlyonly ever moves a branch forward. If you have local commits too, it changes nothing and reportsFAIL.
After the fetch: GitMon
GitMon does not fetch or pull for you: it is read-only and never runs git. It reads the remote-tracking refs your last fetch left behind, so once the loop above has run, the menu bar shows which repos are behind (↓), ahead (↑), dirty, detached, never pushed, or pointing at an upstream that no longer exists. Those are the repos the pull loop skipped or failed on.