repowatch
Every git repo under a folder, compared across all of its remotes.
Point repowatch at your code folder. It finds every repo, then shows one row per repo with a column per remote, so you can see at a glance where each copy stands: local, GitHub, a self-hosted Gitea, a backup.
It's built for people who keep the same code in more than one place, for example GitHub plus a work or home Gitea, or one machine at work and another at home. It answers three questions:
- Where does everything stand? A matrix of every repo against every remote.
- Can it level them for me, safely?
syncfast-forwards your secondary remote and your local checkout to match the primary, in both directions. It never force-pushes or merges. - What's all this branch clutter?
prunedeletes branches whose work is already inmain, including squash-merged ones, and explains every branch it keeps.
Install
Requires Python 3.11+ and git on your PATH.
pipx install repowatch # or: uv tool install repowatch
From source:
git clone https://github.com/markgwharry/repowatch && cd repowatch
pipx install .
On Windows, use Windows Terminal, which is the default on Windows 11. The legacy console mangles the arrows. In Git Bash, write paths with forward slashes (
./repowatch), because backslashes get eaten.
Quick start
repowatch ~/code # interactive matrix
repowatch ~/code --fetch # fetch every remote first
repowatch status ~/code # print the matrix once
repowatch status ~/code --json # machine-readable, for scripts and dashboards
repowatch init # write a commented config file
Set root in the config and plain repowatch opens your code folder.
Reading the matrix
Repo Branch Local github gitea Status
core-lib main 965f254 ✓ 965f254 = 965f254 = ok
cli-tool main e5c17be ✓ f4a93cd ↑2 f4a93cd ↑2 push
ticker main 5cd32d9 ✓ 5cd32d9 = e88d620 ↑1 mirror lag
work/tracker main 577c22d ✓ 5296d38 ↓1 577c22d = pull mirror lag
work/analysis main fe0af99 ~1 ?1 $1 883a7b7 ↑1 ↓1 c458ab1 ↑1 dirty untracked stash diverged
Each remote cell shows that remote's tip of your current branch, and how your local HEAD compares with it:
| Symbol | Meaning |
|---|---|
= |
Same commit |
↑n |
Local has n commits the remote doesn't (push) |
↓n |
Remote has n commits local doesn't (pull) |
↑n ↓m |
Diverged |
— |
Branch doesn't exist on that remote |
✗ |
Last fetch failed (offline, VPN down, auth) |
| cyan | A mirror remote is lagging. Expected; sync deals with it |
Local shows HEAD plus + staged, ~ modified, ? untracked, ! conflicts and $ stashes, or ✓ when clean.
Columns are labelled by host, not remote name. origin → github.com and upstream → github.com both appear under github. Map your own hosts in the config.
TUI keys
| Key | Action |
|---|---|
r |
Rescan (local only, fast) |
f / F |
Fetch the selected repo / all repos |
s / S |
Sync the selected repo / all repos: shows the plan first, y applies it |
a |
Add a remote (pre-filled from a template) |
o |
Open the folder |
d |
Show only repos that need attention |
q |
Quit |
Sync
Keeps a secondary remote (e.g. Gitea) and your local checkout level with a primary remote (e.g. GitHub).
repowatch sync # dry run: the plan, nothing changed
repowatch sync --apply
repowatch sync --apply --repo my-service --repo docs
Everything is fast-forward only:
| Situation | What sync does |
|---|---|
| Primary ahead of secondary | Pushes primary's tip to secondary |
| Branch only on primary | Creates it on secondary |
| Secondary ahead of primary | Pushes secondary's tip to primary, so work done on the secondary flows up |
| Primary and secondary diverged | Flags it and touches nothing |
| Branch only on secondary | Flags it: could be new there, or deleted on primary |
| Local checkout behind and clean | Fast-forwards it |
| Local dirty, detached or diverged | Leaves it alone and flags it |
| Local has unpushed commits | Flags it. Pushing your own work stays your decision |
By default only main/master are synced and dependabot/* is skipped. Change sync_branches and sync_exclude (glob lists) in the config.
A protected branch on the primary rejects a secondary-ahead push. That's reported as a failed step and nothing else is affected. Unattended runs over SSH need a key without a passphrase, or a running agent; otherwise that remote shows as unreachable and is skipped.
Run sync on a schedule
repowatch schedule # hourly via Windows Task Scheduler (silent, via pythonw)
repowatch schedule --every 30
repowatch schedule --remove
Output goes to sync.log in the config folder and rotates at 1 MB. On macOS and Linux, schedule prints a crontab line for you to add.
Prune
Deletes stale branches on the primary remote.
repowatch prune # dry run: what goes, and why the rest stay
repowatch prune --apply
repowatch prune --include-unmerged 180 # also abandoned branches, >180 days old, with no open PR
repowatch prune -v # also list merged branches it keeps
| Kind | Meaning | Default |
|---|---|---|
| merged | Tip already in the default branch (merge, fast-forward or rebase) | Deleted |
| squash-merged | Its combined change is already in the default branch as one commit | Deleted |
| unmerged | Has commits the default branch doesn't | Kept and listed |
It never deletes the default branch, anything matching prune_keep (default main, master, develop, release/*), anything younger than prune_min_age_days (default 7), or the branch you have checked out. Unmerged branches are only deleted with --include-unmerged, and only after the GitHub CLI (gh) confirms they have no open PR. Without gh, they're never touched. Deletions are verified against the remote afterwards.
To stop branches piling up again: gh repo edit OWNER/REPO --delete-branch-on-merge.
Configuration
repowatch init writes a commented file to:
- Windows:
%APPDATA%\repowatch\config.toml - macOS / Linux:
~/.config/repowatch/config.toml, or$XDG_CONFIG_HOME/repowatch/config.toml
root = "~/code"
depth = 3 # folder levels to search
refresh_seconds = 30 # TUI local auto-refresh; 0 = off
fetch_timeout = 25 # seconds, per remote
column_order = ["github", "gitea"]
primary_labels = ["github"] # sync's source of truth; prune's target
mirror_labels = ["gitea"] # kept level by sync; lag shown as "mirror lag"
sync_branches = ["main", "master"]
sync_exclude = ["dependabot/*"]
prune_keep = ["main", "master", "develop", "release/*"]
prune_min_age_days = 7
[hosts] # host -> column label (exact match, then substring)
"git.example.internal" = "gitea"
"192.168.1.20" = "gitea"
[remote_templates] # pre-fills the TUI's add-remote dialog
gitea = "https://git.example.internal/{owner}/{repo}.git"
Use repowatch --config other.toml … to point at a different file.
How it behaves
- Remote columns are only as fresh as your last fetch.
rand the auto-refresh are local-only;f,F,--fetch,syncandprunego to the network. - It never prompts. Terminal prompts, credential-manager pop-ups and interactive SSH are disabled. A remote that needs input shows
✗and the reason, rather than hanging. Cached credentials (credential managers, keychain, ssh-agent) work normally. - Each remote is fetched separately, with a timeout, so an unreachable Gitea doesn't hold up GitHub.
- Discovery stops at the first
.git. It doesn't descend into repos, and it skipsnode_modules,.venv,buildand similar. - Don't keep repos in a cloud-synced folder (OneDrive, Dropbox, iCloud). Sync clients lock and duplicate files inside
.git. Your remotes are the backup.
Contributing
Issues and PRs are welcome. See CONTRIBUTING.md. The test suite builds throwaway repos with GitHub- and Gitea-style remotes for every state repowatch distinguishes, and runs on Windows, macOS and Linux.
Licence
Metadata
Release files for repowatch 0.4.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 | Provenance | |
|---|---|---|---|---|
| repowatch-0.4.0.tar.gz | 33.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| repowatch-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 62.6 kB
Release files / repowatch-0.4.0.tar.gz
| Download URL | repowatch-0.4.0.tar.gz |
|---|---|
| Size | 33.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9a8460f1aa61f1717a0e534757ef3182933ff237213f8659548f82f8b23eb2cb
|
|
BLAKE2b-256 checksum How to use checksums |
615886bdc7cf4fb093fadb07cae3f5aa441699406db33c8149c96d3e69d7fb00
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logRelease files / repowatch-0.4.0-py3-none-any.whl
| Download URL | repowatch-0.4.0-py3-none-any.whl |
|---|---|
| Size | 29.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
318c4b85ac57bbcaf903e87ba9b2f5add29b0efadb927be96ca17ec1d05fedd8
|
|
BLAKE2b-256 checksum How to use checksums |
02fa70b244ae7304e2ead054a6945dd784fa9287277dbd2ec2ab2299349b0a8e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logSecurity for repowatch 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for repowatch-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.