Skip to content

Latest commit

 

History

139 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GitSync

Mirror every repo you can access — across GitLab, GitHub, and Bitbucket — to your Mac, and keep them current. From the menu bar.

Idempotent, schedule-aware, and safe: it never overwrites your local work.

The Repositories window showing a filterable inventory of repos grouped by sync status, with a live sync-progress panel at the top

What it does

GitSync logs into the Git hosts you use, discovers every repository you have access to, and clones or updates each one to a folder on your Mac. Run it once and you have a local mirror of everything; leave it on a schedule and that mirror stays current — search code locally, grep across hundreds of repos, work offline, keep a backup.

It is safe by design. For each repo it clones if missing, otherwise fetches and fast-forwards the default branch only when that's safe. It never force- pushes, resets, or discards local changes. Uncommitted edits and diverged branches are reported, not touched.

Highlights

  • Many hosts, many accounts. Self-hosted GitLab, GitHub.com, and Bitbucket Cloud — configured as independent providers. Run several of the same kind (e.g. two GitLab instances) side by side, each with its own host, token, and disk folder.
  • A live inventory of everything. One searchable, filterable window listing every repo it knows about — cloned, not-yet-cloned, diverged, dirty, stale — grouped and color-coded by status.
  • Never loses your work. Fast-forward only; uncommitted changes survive updates; collisions and diverged branches are surfaced, not clobbered.
  • Fast. A pure-Swift engine drives git directly with a large worker pool — thousands of repos in a couple of minutes, with a live "what's each worker doing right now" panel so a stall is obvious.
  • Schedule-aware. Sync automatically every N hours, with sleep-aware catch-up for runs missed while your Mac was asleep, and Launch at Login. A host that's unreachable (VPN down) is isolated and retried cheaply — it never drags the others down or touches your repos.
  • No config files. Everything lives in the app; tokens go in the macOS Keychain. Nothing to edit by hand, no environment variables to set.

Screenshots

The menu bar
The GitSync menu-bar dropdown
Run on demand, jump to the inventory, open the activity log, or open Settings — all from the menu bar. The icon spins while syncing and flags anomalies when a run finishes.
Providers
Settings → Providers, listing configured sync sources
Add a provider per source. Each has its own host/scope, token, disk folder, skip patterns, and sync scope.
Editing a provider
The provider editor sheet
Host, scope, token, and the folder its repos clone into — validated against your other providers so two can't collide on disk.

Install

Requires macOS 15 (Sequoia) or later.

Download GitSync.app from the latest release, move it to /Applications, and launch it. On first launch macOS may quarantine it (unsigned by an Apple Developer ID); if it won't open, clear the quarantine flag:

xattr -dr com.apple.quarantine /Applications/GitSync.app

After that, GitSync updates itselfCheck for updates… in the menu (or the periodic check you can opt into) downloads and installs new versions in place. Updates are verified by an EdDSA signature, so only releases signed with the project's private key will install.

Build from source

./Tools/make-signing-cert.sh    # one-time: stable signing identity (see below)
./build.sh release              # produces .build/release/GitSync.app
cp -r .build/release/GitSync.app /Applications/
xattr -dr com.apple.quarantine /Applications/GitSync.app   # first launch only
open /Applications/GitSync.app

Setup

On first launch GitSync walks you through adding your first provider. After that, manage everything in Settings → Providers (⌘,). Add a provider per source:

Host You provide Token scopes
GitLab Instance Host (e.g. gitlab.example.com) + a personal access token read_api, read_repository
GitHub The Organization + a token Classic: repo · Fine-grained: Contents: Read, Metadata: Read
Bitbucket The Workspace slug + your Username + an API token read:repository:bitbucket, read:workspace:bitbucket

Bitbucket, read this. Create the token at id.atlassian.com with “Create API token with scopes — not the plain “Create API token” button next to it. The plain button makes a token with no scopes, which authenticates but then 401s on every call (GitSync now flags this specifically as “Token has no scopes”). Pick Bitbucket and grant read:repository:bitbucket + read:workspace:bitbucket. Your Bitbucket account password never works — the API rejects it.

Each provider also has its own disk folder, skip patterns, an Include archived repos toggle, and a sync scope (sync everything, or only repos you've explicitly tracked). Tokens are stored in the macOS Keychain — never on disk in plain text.

Other settings live under Behavior (parallel workers, clone depth, network timeout) and Schedule (sync automatically every N hours, automatic update checks, and Launch at Login).

Skipping repos

Each provider's Skip patterns field takes a comma-separated list of repo names or path prefixes to skip (case-insensitive, prefix match), e.g. legacy-monorepo, some-group/archive/. Skipped repos still appear in the inventory (marked skipped) so you know they exist — their on-disk state is just left alone.

Using it

  • Run now from the menu, or let the schedule do it.
  • Open Repositories (⌘H) to browse the inventory: search by path, filter by status or host, click a repo to reveal it in Finder, or right-click for per-repo actions (sync just this one, add to skip list, copy SSH URL, move to Trash).
  • Move to Trash is the only destructive action, and it's careful: it refuses any repo with uncommitted changes or unpushed commits, and everything it does remove goes to the macOS Trash (recoverable), never rm.

Activity log

Every sync, one-off resync, per-repo outcome, and deletion is written to the macOS unified log — so there's a durable record of what happened without GitSync storing log files itself. Open activity log… (⌘L) opens Terminal on a live, filtered tail of exactly GitSync's entries (recent history first, then streaming). Or run it yourself from any terminal:

log show   --predicate 'subsystem == "com.uPaymeiFixit.GitSync"' --info --last 1d
log stream --predicate 'subsystem == "com.uPaymeiFixit.GitSync"' --info

(The system keeps these on a rolling budget — durable for days, not forever.)


Under the hood

Architecture, code signing, and test harnesses

Architecture

Single Swift package (Package.swift at the repo root, sources under Sources/GitSync/):

GitSync.app
├── AppState              — observable source of truth + event router
├── SyncEngine            — pure-Swift sync engine; drives git directly
│   ├── PlatformDiscovery — GitLab/GitHub/Bitbucket REST clients
│   └── RepoSyncer        — the clone-or-update decision tree
├── BufferSink/EventBuffer— engine emits events; batched + coalesced for the UI
├── InventoryStore        — persistent repo state keyed by (providerID, platform, rel)
├── ProviderStore         — configured sync sources (UserDefaults) + tokens (Keychain)
├── SettingsStore         — shared run config (UserDefaults)
├── RunLog                — unified-logging record of runs, outcomes, deletions
├── SparkleUpdater        — in-app auto-update (Sparkle, EdDSA-signed releases)
└── MenuBarExtra UI       — three icon states: idle / running / attention

Releases are signed for auto-update with Tools/sparkle-release.sh <version> <zip>, which EdDSA-signs the archive (private key in the maintainer's Keychain) and writes an <item> into appcast.xml — the feed Sparkle reads from the repo's main branch. build.sh embeds Sparkle.framework into Contents/Frameworks and code-signs its nested helpers inside-out under the app's identity (the hardened runtime is left off for self-signed builds so Library Validation lets the same-identity framework load).

The engine runs git in-process with the inherited environment (so git's ~/.config, credential helpers, and ssh config behave as they do in a shell). The inventory persists at ~/Library/Application Support/GitSync/inventory.json (a JSON array of Repo records; it's a rebuildable cache — delete the file and the next run repopulates it).

Code signing & keychain prompts

build.sh signs with a stable local identity ("GitSync Self-Signed") so the keychain ACL on your stored tokens matches across rebuilds — without that, macOS re-prompts for your login password for every secret on each launch. Tools/make-signing-cert.sh (run once) creates the identity and trusts it for code signing — both matter: a stable signature that isn't trusted still triggers prompts. If you made the cert with an older version of that script (no trust step), repair it without recreating it:

security find-certificate -c "GitSync Self-Signed" -p > /tmp/gitsync.pem
security add-trusted-cert -r trustRoot -p codeSign \
    -k "$HOME/Library/Keychains/login.keychain-db" /tmp/gitsync.pem

For distribution to other Macs, set SIGN_IDENTITY to a Developer ID Application identity and notarize the bundle.

CLI test harnesses

The Command Line Tools toolchain ships no XCTest, so the executable carries its own diagnostic modes:

.../Contents/MacOS/GitSync --verify-parser              # event wire-format parser
.../Contents/MacOS/GitSync --smoke-test                 # engine wiring (no providers)
.../Contents/MacOS/GitSync --load-test                  # EventBuffer throughput
.../Contents/MacOS/GitSync --trash-test                 # delete-path safety
.../Contents/MacOS/GitSync --whitelist-test             # tracked-only filter
.../Contents/MacOS/GitSync --provider-migration-test    # legacy→provider migration
.../Contents/MacOS/GitSync --provider-validation-test   # provider folder-collision guard
.../Contents/MacOS/GitSync --provider-filter-test       # repo-list chips keyed by provider, not kind
.../Contents/MacOS/GitSync --provider-removal-test      # provider delete purges its rows; path-resolution order
.../Contents/MacOS/GitSync --abort-reset-test           # cancel doesn't poison later syncs
.../Contents/MacOS/GitSync --scheduler-test             # due/catch-up logic
.../Contents/MacOS/GitSync --connection-test            # credential test + error classification
.../Contents/MacOS/GitSync --legacy-keychain-cleanup-test  # one-time legacy-token cleanup (no repeat prompts)

Each of these guards one specific way a big run has wedged. They exist because the symptom is always the same — repo rows frozen on "starting", no progress, no error — while the cause has been different every time:

.../Contents/MacOS/GitSync --parallelism-test           # fan-out really overlaps (not serialized on the actor)
.../Contents/MacOS/GitSync --abort-contention-test      # abort polling stays lock-free under 128 workers
.../Contents/MacOS/GitSync --stream-eof-test            # reader ends on git exiting, not on pipe EOF
.../Contents/MacOS/GitSync --fd-leak-test               # pipe FDs closed per subprocess
.../Contents/MacOS/GitSync --reachability-gate-test     # both sync paths refuse a down host instead of stalling
.../Contents/MacOS/GitSync --stall-timeout-test         # connect stall vs slow transfer are separate deadlines
.../Contents/MacOS/GitSync --pool-leak-test             # worker threads retire; pools don't accumulate to the 6144 ceiling

About

Sync every git repo you have to disk. Clone your organization's codebase.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages