{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreiaxun3bhpi3vp6oqy54ylu37nelrff4ahyasfeg54l64z7bxby5oy",
    "uri": "at://did:plc:5sgu76a53rz3n6unbykmovqy/app.bsky.feed.post/3mlfo4jvauab2"
  },
  "description": "A daily bash hack I run every morning to fetch and sync all my git repos at once, before code reviews and standup.",
  "path": "/sync-git-repos/",
  "publishedAt": "2026-05-09T06:56:48.000Z",
  "site": "https://sahilkapoor.com",
  "tags": [
    "remote",
    "github.com/kapoorsahil/git-scripts",
    "git_my_commits",
    "git_pr_all",
    "git_clone_all",
    "git_clean_branches",
    "git_size_all",
    "git_pr_review",
    "git_open"
  ],
  "textContent": "Six developers. Eight repos. You already have a sync problem.\n\nDay 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.\n\nMultiply 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.\n\nPicture 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.\n\n## The Script That Runs Before Anything Else\n\nThe 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.\n\nFor each `.git` directory inside the folder, the script does six things in order:\n\n  * Stashes any uncommitted work, including untracked files.\n  * Runs `git fetch --prune`, so deleted remote branches stop showing up locally.\n  * Fast-forwarding the active branch, with auto-fallback to a pinned branch if deleted on remote and hard-reset if it has diverged\n  * Syncing `development`, `production`, and `staging` branches, force-advancing via git update-ref if diverged\n  * Sweeps every other local branch with an upstream and tries to fast-forward.\n  * Restores the stash.\n\n\n\nThat is the whole morning routine. Run it from the folder, then open Slack.\n\n## When a Feature Branch Spans Multiple Repos\n\nSuppose 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.\n\n`git_fetch_all.sh` takes a branch name as its first argument:\n\n\n    git_fetch_all.sh checkout-redesign\n\nFor 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.\n\nNow everyone reviewing or pairing on the feature is on the same branch in every repo that matters.\n\n## Keeping Local Changes Safe Using git stash\n\nThis 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.\n\nImagine 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.\n\nThe script handles this with `git stash push -u`:\n\n\n    local stashed=0\n    if [ -n \"$(git status --porcelain)\" ]; then\n        if git stash push -u -m \"git_fetch_all auto-stash\" > /dev/null 2>&1; then\n            stashed=1\n        fi\n    fi\n\n    do_repo_sync\n    local rc=$?\n\n    if [ \"$stashed\" = \"1\" ]; then\n        if git stash pop > /dev/null 2>&1; then\n            echo \"restored stashed changes\"\n        else\n            echo \"stash pop had conflicts, your changes remain in 'git stash list'\"\n        fi\n    fi\n\nThe `-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.\n\nIf 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.\n\n## The Plumbing Command\n\nThe 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.\n\nThe 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.\n\nThe better way is this:\n\n\n    if git merge-base --is-ancestor \"$local_sha\" \"$remote_sha\"; then\n        git update-ref \"refs/heads/$branch\" \"$remote_sha\"\n    fi\n\n`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.\n\nMost developers never reach for `update-ref`, and that is what makes the script feel surgical instead of clumsy.\n\n## Take It Home\n\nThe 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:\n\n\n    git clone https://github.com/kapoorsahil/git-scripts.git ~/git-scripts\n    ln -s ~/git-scripts/git_fetch_all/git_fetch_all.sh /usr/local/bin/git-fetch-all\n\nEdit 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.\n\nSeven companion scripts live in the same repo, each in its own folder with its own README, each solving an adjacent multi-repo problem:\n\nScript | What it does\n---|---\ngit_my_commits | Aggregates your commits across every repo for standup\ngit_pr_all | Lists open PRs across every repo via `gh`\ngit_clone_all | Bulk-clones a GitHub org into a folder for a new machine\ngit_clean_branches | Cleans up `[gone]` branches that hang around after squash-merges\ngit_size_all | Finds which repos are eating your SSD\ngit_pr_review | Checks out a PR by URL for review\ngit_open | Opens a repo or file on GitHub from your terminal\n\nIf 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.\n\n## Rome Wasn't Built in a Day\n\nThe first version of `git_fetch_all.sh` was twenty lines. It pulled the current branch in every repo. That was all.\n\nA 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.\n\nThese 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.",
  "title": "Daily Hack: Sync Every Git Repo Before You Start Work",
  "updatedAt": "2026-05-21T09:47:32.838Z"
}