Keeps every git repository in a directory you configure up to date with its
remote, automatically, in the background, on macOS via launchd.
It only ever does a git pull --ff-only --prune. It never touches a repo that
isn't safely fast-forwardable, and it never switches branches for you:
- Repos on a branch other than
main/masterare left alone. - Repos whose current branch has no upstream (or a deleted/gone upstream) are left alone.
- If a fast-forward pull would conflict with local changes, git aborts on its own — nothing is overwritten.
Every skip/failure is written to a log file. Instead of firing a system notification on every single run (every 5 minutes, forever), it batches everything into one notification per day, sent the first time the job runs after a configurable hour (default: 9am local time) if — and only if — there's something to report. No news, no notification.
If you keep long-lived clones of your repos around (for example, one base
clone per project, with actual work happening in separate worktrees or
branches elsewhere), the base clones tend to drift out of date. This just
keeps them current so git pull is never something you have to remember to
do.
Requires macOS (launchd, osascript), bash, and git.
GIT_SYNC_DEV_DIR has no default — it must be set before installing, so the
job never ends up scanning a directory you didn't choose:
mkdir -p ~/.config/git-sync
echo 'GIT_SYNC_DEV_DIR="$HOME/dev"' >> ~/.config/git-sync/config.shThen:
git clone git@github.com:wdsrocha/git-sync.git
cd git-sync
./bin/git-sync installThis will:
- Symlink
git-sync.shandbin/git-syncinto~/.local/bin/. - Generate a
launchdjob fromlaunchd/git-sync.plist.templateinto~/Library/LaunchAgents/. - Load the job with
launchctl.
Since the installed script is a symlink back into this clone, git pull-ing
this repo picks up updates automatically — no need to reinstall after every
change (unless you changed configuration that only takes effect at install
time, like the interval or the launchd label).
Set any of these as environment variables before running bin/git-sync install, or put them in ~/.config/git-sync/config.sh (sourced by both the
worker script and the CLI):
| Variable | Default | Meaning |
|---|---|---|
GIT_SYNC_DEV_DIR |
(required, no default) | Directory to scan for repositories (one level deep) |
GIT_SYNC_EXCLUDE_DIRS |
(none) | Space-separated directory names to skip entirely |
GIT_SYNC_NOTIFY_HOUR |
9 |
Earliest local hour (0-23) to send the daily digest |
GIT_SYNC_LABEL |
local.git-sync |
launchd label, and therefore the plist filename |
GIT_SYNC_INTERVAL |
300 |
Seconds between runs (install-time only) |
GIT_SYNC_LOG_FILE |
$GIT_SYNC_DEV_DIR/sync_errors.log |
Where events are logged |
GIT_SYNC_MAX_LOG_LINES |
1000 |
Max lines kept in the log file; oldest lines are trimmed past this |
GIT_SYNC_EXCLUDE_DIRS is meant for directories that intentionally don't
follow the "one clean base clone per repo" convention — e.g. a directory
where you keep ad hoc worktrees for whatever you're actively working on.
Example config file:
# ~/.config/git-sync/config.sh
GIT_SYNC_DEV_DIR="$HOME/code"
GIT_SYNC_EXCLUDE_DIRS="worktrees scratch"
GIT_SYNC_NOTIFY_HOUR=8GIT_SYNC_DEV_DIR must be set here (or as an environment variable) before
running git-sync install — there's no built-in default.
git-sync status # is the launchd job loaded? last log lines?
git-sync logs # follow the log file
git-sync run # run the sync once, right now, outside of launchd,
# printing a one-line status per repo
git-sync uninstall # unload the job, remove the symlinksgit-sync run prints one line per repo under GIT_SYNC_DEV_DIR, e.g.:
project-a up to date
project-b updated (3 new commits)
project-c skipped (excluded)
project-d no upstream (branch=main)
project-e non-main branch (wip)
project-f pull failed
For every immediate subdirectory of GIT_SYNC_DEV_DIR that isn't in
GIT_SYNC_EXCLUDE_DIRS and contains a .git directory:
- If the current branch has no valid upstream, skip and log
no_upstream. - If the current branch isn't
mainormaster, skip and lognon_main_branch. - Otherwise, run
git pull --ff-only --prune --quiet. If that fails, logpull_failed.
All three cases feed into the same once-a-day notification digest.
MIT, see LICENSE.