gitpair
Keep the git repositories of two machines in sync over ssh.
You work on a desktop and a laptop. Both have a ~/git folder with mostly the
same repositories, and you keep forgetting to push on one machine before
leaving it. gitpair connects to the other machine, compares every repository
on both sides and builds a plan:
- Repositories where one side is simply ahead on the same branch are fast-forwarded automatically.
- Diverged branches, dirty work trees and repositories it has never seen before are shown in a terminal UI where you decide what to do.
- Nothing goes through GitHub or any other server. Commits travel directly between the two machines, so unpushed and private work is synced too.
The configuration file is the same on both machines. gitpair figures out which host it is running on and treats the other one as the remote.
Installation
gitpair needs Python 3.11+ on the machine where you run it. The other machine
only needs git (2.28+), python3 and an ssh server.
uv tool install gitpair
# or
pipx install gitpair
Quick start
-
Make sure each machine can reach the other with
ssh <name>without a password prompt (key authentication, and optionally an entry in~/.ssh/config). -
Create the config and edit it:
gitpair init $EDITOR ~/.config/gitpair/config.toml
[hosts.desktop] ssh = "desktop.local" # how the other machine reaches this one root = "~/git" # where repositories live on this host [hosts.laptop] ssh = "laptop.local" root = "~/git" [repos] track = [] ignore = ["archived/*"]
-
Copy the same file to the other machine.
-
Run it:
gitpair plan # show what would happen; never touches branches, work # trees, uncommitted files or origin (see below) gitpair # review the plan in the terminal UI and apply it
gitpair plan never touches branches, work trees, uncommitted files or
origin. It does fetch each repository's peer branch into
refs/gitpair/<host>/<branch> and update FETCH_HEAD, which is how it
counts commits ahead/behind; see
docs/how-it-works.md.
In the terminal UI, press enter on a row to answer its question, a to accept
the first option, s to skip, x to run the plan and q to quit without
changing anything. Rows you leave unanswered are skipped.
gitpair plan and the terminal UI show each repository's status and action as
icons, to keep the table narrow: ↑/↓ ahead or behind, ⇅ diverged, ✎
dirty, ✚ new, →/← only on one host, ✗ git error, ∅ empty repository
or no branch to sync, ⚑ conflicting ignored files; ▶ for an action that
will run as shown, ? for one that still needs a decision, – for skip. A
legend for the icons used in the current table is printed under it. Use
gitpair plan --plain for ASCII letters instead, in scripts or terminals
without unicode support.
For cron jobs or shell hooks, gitpair sync --auto applies only the automatic
actions and never asks.
What gitpair does with each repository
| Situation | Default | Other options |
|---|---|---|
| Same commit on both hosts | nothing | |
| Same branch, one side ahead, both trees clean | fast-forward the other side | |
| Same branch, one side ahead, a dirty work tree | ask | fast-forward, skip |
| Same branch, diverged | ask | merge, reset either side, skip |
| Different branches checked out, or detached HEAD | skip and report | |
| Tracked repository missing on one host | clone it there | |
| Repository not in the config | ask | sync and track, ignore forever, skip |
Resets keep the old commit in refs/gitpair/backup/<branch>. Fast-forwards use
git merge --ff-only, so git refuses to touch uncommitted changes that
would be overwritten.
Per-repository options
[repo."work/api"]
sync_ignored = [".env", "data/fixtures"] # gitignored paths copied with ssh
autopush = true # push to origin when it is behind
sync_ignoredcopies files that git ignores (secrets, local data, build caches you do not want to rebuild) between the hosts. A file missing on one side is copied there automatically. A file present on both sides with a different size or mtime is a conflict: gitpair asks, offering "newer wins" (copy each conflicting file in the direction of the newer mtime) or skip the conflicting files (the one-sided files are still copied). In--auto, conflicts are skipped and the one-sided copies still run. Same mtime but a different size is only a warning; it is never copied. Deleted files are never deleted on the other host.autopushpushes the synced branch tooriginwhen origin is behind. It never forces: if origin has diverged, gitpair reports it and does nothing.
Declaring a [repo."..."] table also tracks the repository.
settings.autopush = true turns autopush on for every repository.
When you track or ignore a new repository, gitpair updates the config file and copies it to the other host, unless the copy there was edited separately. See docs/configuration.md for all settings and docs/how-it-works.md for the git commands behind each action.
Commands
gitpair [--config PATH] [--as HOST] [--remote HOST] [sync [--auto] | plan [--plain] | init]
--config: config file. Defaults to$GITPAIR_CONFIGor~/.config/gitpair/config.toml.--as: name of the current host, when the hostname does not match the config.--remote: host to sync with. Only required when the config lists more than two hosts.plan --plain: ASCII letters instead of unicode icons in the table.
Contributing
Bug reports and pull requests are welcome. Read CONTRIBUTING.md before opening a pull request.
License
Metadata
Release files for gitpair 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gitpair-0.1.0.tar.gz | 30.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gitpair-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.5 kB
Release files / gitpair-0.1.0.tar.gz
| Download URL | gitpair-0.1.0.tar.gz |
|---|---|
| Size | 30.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3aa011f4681c8854a81e2cc2373b9d9c867bfc7967883e3eaabc4eae970badb2
|
|
BLAKE2b-256 checksum How to use checksums |
2417b0133b6a55c37c0f562c4c2ee21380c001b9c722cf39027dfe29cafe7508
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / gitpair-0.1.0-py3-none-any.whl
| Download URL | gitpair-0.1.0-py3-none-any.whl |
|---|---|
| Size | 36.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5b05e67b47279560aa3543cb120fc2344bd9ef2983192bf1a30316a7f32bb388
|
|
BLAKE2b-256 checksum How to use checksums |
a53539edde4ca382d7a76056aa57fc90fa5d38e7b953ffc3a695f8d690bbed38
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|