External Publication
Visit Post

Daily Hack: Sync Every Git Repo Before You Start Work

Sahil Kapoor's Playbook May 9, 2026
Source

Six developers. Eight repos. You already have a sync problem.

Day one is one repo with one engineer. Day thirty is three services, four engineers, four feature branches per service, and a code review every afternoon. Every time someone reviews a PR in one service, they need their local copy of that service current, the API service it calls current, and the shared types library current. The same is true on the next PR, and on the one after that.

Multiply this across six developers committing daily to eight repos and you get the real problem. It is not "scaling git." It is keeping enough things current that nobody is reviewing yesterday's code or testing against last week's API.

Picture the project folder for the gaming team I worked on. It lived at ~/game/, and inside it sat eight repos as siblings: game-service for the core game loop, payment-service for in-app purchases, matchmaking-service, leaderboard-service, accounts-service, analytics-service, a web client, and a mobile client. Each one deployed independently. Each had its own development, staging, and production branches. And each was where one or two engineers committed several times a day.

The Script That Runs Before Anything Else

The first thing I do every morning, before standup, before opening PRs, before anything else, is cd into the project folder and run one script: git_fetch_all.sh. By the time my coffee is cool enough to drink, every repo is fetched, every long-running branch is current, and the active branch in each repo has whatever changes my teammates pushed overnight.

For each .git directory inside the folder, the script does six things in order:

  • Stashes any uncommitted work, including untracked files.
  • Runs git fetch --prune, so deleted remote branches stop showing up locally.
  • Fast-forwarding the active branch, with auto-fallback to a pinned branch if deleted on remote and hard-reset if it has diverged
  • Syncing development, production, and staging branches, force-advancing via git update-ref if diverged
  • Sweeps every other local branch with an upstream and tries to fast-forward.
  • Restores the stash.

That is the whole morning routine. Run it from the folder, then open Slack.

When a Feature Branch Spans Multiple Repos

Suppose you are working on a redesign that spans multiple repos: the checkout flow, say, which touches payment-service, accounts-service, and the web client. The team convention is to create the same branch name in each one (checkout-redesign) so the work is easy to find later. Your teammate pushes the branch to all three repos, pings you to review, and now you need the same branch checked out across all three before you can pull their code, run it, or comment on it intelligently.

git_fetch_all.sh takes a branch name as its first argument:

git_fetch_all.sh checkout-redesign

For each repo, the script fetches that branch from origin and checks it out. If a repo does not have the branch on remote, because the feature does not touch it, the script falls back to development so you never end up half-checked-out on a stale branch.

Now everyone reviewing or pairing on the feature is on the same branch in every repo that matters.

Keeping Local Changes Safe Using git stash

This is the part nobody warns you about: git pull on a working tree with uncommitted changes is not safe. Modified files are usually fine, but untracked files are not.

Imagine you have a new file notes.md that you have not added yet, and the upstream pull brings in its own notes.md. git silently overwrites yours, with no conflict and no warning, and your file is gone.

The script handles this with git stash push -u:

local stashed=0
if [ -n "$(git status --porcelain)" ]; then
    if git stash push -u -m "git_fetch_all auto-stash" > /dev/null 2>&1; then
        stashed=1
    fi
fi

do_repo_sync
local rc=$?

if [ "$stashed" = "1" ]; then
    if git stash pop > /dev/null 2>&1; then
        echo "restored stashed changes"
    else
        echo "stash pop had conflicts, your changes remain in 'git stash list'"
    fi
fi

The -u flag is the important one, because it pulls untracked files into the stash too. Without it, new files you have not committed are left where they are, and git happily moves things around them. With it, the script is safe to run on any tree, mid-feature or not.

If pop conflicts, which only happens when the pull moved a file you had modified, the stash entry is left in git stash list. Nothing is destroyed, and you resolve the conflict manually.

The Plumbing Command

The branches I want at HEAD whether or not I have them checked out are development, production, and staging: development for daily syncing, production for hotfix branches, and staging for QA reviews.

The naive way to keep them current is to checkout, pull, and checkout back. That is three commands per branch, with the working tree shuffled twice and a real chance of a merge prompt if local has diverged.

The better way is this:

if git merge-base --is-ancestor "$local_sha" "$remote_sha"; then
    git update-ref "refs/heads/$branch" "$remote_sha"
fi

git update-ref moves a branch ref forward without ever checking it out. The feature branch I am on stays the active one while development quietly advances underneath, with no working tree shuffle and no merge prompt to handle. The script is fast-forward only by design, so if local has diverged from remote, the is-ancestor check fails and the script leaves the branch alone.

Most developers never reach for update-ref, and that is what makes the script feel surgical instead of clumsy.

Take It Home

The script lives in a public repo at github.com/kapoorsahil/git-scripts. Open it in the browser to read through the source, or clone it into the directory you keep your tools in:

git clone https://github.com/kapoorsahil/git-scripts.git ~/git-scripts
ln -s ~/git-scripts/git_fetch_all/git_fetch_all.sh /usr/local/bin/git-fetch-all

Edit the ALWAYS_PULL_BRANCHES array at the top of the script to match your team's long-running branches (main, dev, qa, whatever you use), and you are set.

Seven companion scripts live in the same repo, each in its own folder with its own README, each solving an adjacent multi-repo problem:

Script What it does
git_my_commits Aggregates your commits across every repo for standup
git_pr_all Lists open PRs across every repo via gh
git_clone_all Bulk-clones a GitHub org into a folder for a new machine
git_clean_branches Cleans up [gone] branches that hang around after squash-merges
git_size_all Finds which repos are eating your SSD
git_pr_review Checks out a PR by URL for review
git_open Opens a repo or file on GitHub from your terminal

If you have a script you want to add, or an improvement to one that is already there, open a PR. I will read every one.

Rome Wasn't Built in a Day

The first version of git_fetch_all.sh was twenty lines. It pulled the current branch in every repo. That was all.

A year later I added the stash logic, after the second time I lost untracked work. A few months after that came the pinned-branch loop with update-ref. Then the [gone]-branch detection. Then per-repo size and elapsed time in the output. The colors went in just last month, because I was tired of squinting at white text every morning.

These scripts grow the way useful personal tools grow: a few lines at a time, every time something annoys me enough to fix. The companion scripts in the same repo started the same way, and most of them keep getting small additions every few months as I run into new edge cases.

Discussion in the ATmosphere

Loading comments...