|
| 1 | +# git-rain — Multi-Repo Sync CLI |
| 2 | + |
| 3 | +<p align="center"> |
| 4 | + <img src="https://img.shields.io/badge/status-1.0-brightgreen" alt="Status: 1.0"> |
| 5 | + <img src="https://img.shields.io/badge/go-1.24.2-blue" alt="Go 1.24.2"> |
| 6 | + <img src="https://img.shields.io/badge/license-MIT-blue" alt="License: MIT"> |
| 7 | +</p> |
| 8 | + |
| 9 | +> The reverse of [`git-fire`](https://github.com/git-fire/git-fire). |
| 10 | +
|
| 11 | +``` |
| 12 | +git-fire → commit + push everything out |
| 13 | +git-rain → pull everything back down |
| 14 | +``` |
| 15 | + |
| 16 | +`git-rain` discovers all your local git repositories and syncs them from their remotes in one command — fast-forwarding branches, updating non-checked-out refs, and skipping anything that would rewrite local-only commits (unless you ask it to). |
| 17 | + |
| 18 | +Invocation note: `git-rain` and `git rain` are equivalent when `git-rain` is on your PATH. |
| 19 | + |
| 20 | +## Table of Contents |
| 21 | + |
| 22 | +- [Quick Start](#quick-start) |
| 23 | +- [Install](#install) |
| 24 | + - [Homebrew (macOS/Linuxbrew)](#homebrew-macoslinuxbrew) |
| 25 | + - [WinGet (Windows)](#winget-windows) |
| 26 | + - [Linux native packages (.deb / .rpm)](#linux-native-packages-deb--rpm) |
| 27 | + - [Go install](#go-install) |
| 28 | + - [Binary archive (manual)](#binary-archive-manual) |
| 29 | + - [PATH setup (required)](#path-setup-required) |
| 30 | + - [Verify install](#verify-install) |
| 31 | + - [Build from source](#build-from-source) |
| 32 | +- [How It Works](#how-it-works) |
| 33 | +- [Key Features](#key-features) |
| 34 | +- [Core Commands](#core-commands) |
| 35 | +- [Flags](#flags) |
| 36 | +- [Configuration](#configuration) |
| 37 | +- [TUI Selector](#tui-selector) |
| 38 | +- [Safe Mode vs Risky Mode](#safe-mode-vs-risky-mode) |
| 39 | +- [Registry](#registry) |
| 40 | +- [Security Notes](#security-notes) |
| 41 | +- [Contributing](#contributing) |
| 42 | +- [License](#license) |
| 43 | + |
| 44 | +## Quick Start |
| 45 | + |
| 46 | +```bash |
| 47 | +# preview first — shows what would be synced without touching anything |
| 48 | +git-rain --dry-run |
| 49 | + |
| 50 | +# sync all repos under the configured scan path |
| 51 | +git-rain |
| 52 | + |
| 53 | +# interactive TUI: pick exactly which repos to sync |
| 54 | +git-rain --select |
| 55 | +``` |
| 56 | + |
| 57 | +## Install |
| 58 | + |
| 59 | +| Method | Command | Platform | |
| 60 | +|---|---|---| |
| 61 | +| Homebrew | `brew install git-fire/tap/git-rain` | macOS, Linuxbrew | |
| 62 | +| WinGet | `winget install git-rain.git-rain` | Windows | |
| 63 | +| Linux package | Download `.deb` or `.rpm` from [GitHub Releases](https://github.com/git-fire/git-rain/releases) | Linux | |
| 64 | +| Go | `go install github.com/git-rain/git-rain@latest` | All (Go 1.24.2+) | |
| 65 | +| Binary archive | [GitHub Releases](https://github.com/git-fire/git-rain/releases) | All | |
| 66 | + |
| 67 | +### Homebrew (macOS/Linuxbrew) |
| 68 | + |
| 69 | +```bash |
| 70 | +brew tap git-fire/tap |
| 71 | +brew install git-rain |
| 72 | +``` |
| 73 | + |
| 74 | +### WinGet (Windows) |
| 75 | + |
| 76 | +```powershell |
| 77 | +winget install git-rain.git-rain |
| 78 | +``` |
| 79 | + |
| 80 | +### Linux native packages (`.deb` / `.rpm`) |
| 81 | + |
| 82 | +Download from [GitHub Releases](https://github.com/git-fire/git-rain/releases), then: |
| 83 | + |
| 84 | +```bash |
| 85 | +# Debian/Ubuntu |
| 86 | +sudo dpkg -i ./git-rain_<version>_amd64.deb |
| 87 | + |
| 88 | +# Fedora/RHEL/CentOS (dnf) |
| 89 | +sudo dnf install ./git-rain_<version>_amd64.rpm |
| 90 | +``` |
| 91 | + |
| 92 | +### Go install |
| 93 | + |
| 94 | +```bash |
| 95 | +go install github.com/git-rain/git-rain@latest |
| 96 | +``` |
| 97 | + |
| 98 | +Or pin an explicit release: |
| 99 | + |
| 100 | +```bash |
| 101 | +go install github.com/git-rain/git-rain@v1.0.0 |
| 102 | +``` |
| 103 | + |
| 104 | +### Binary archive (manual) |
| 105 | + |
| 106 | +Download and extract the right archive from [GitHub Releases](https://github.com/git-fire/git-rain/releases), then place the binary on your `PATH`. |
| 107 | + |
| 108 | +### PATH setup (required) |
| 109 | + |
| 110 | +**Go install (Linux/macOS):** |
| 111 | +```bash |
| 112 | +export PATH="$HOME/go/bin:$PATH" |
| 113 | +``` |
| 114 | +Add that line to `~/.zshrc` or `~/.bashrc` to persist. |
| 115 | + |
| 116 | +**Manual binary install (Linux/macOS):** |
| 117 | +```bash |
| 118 | +chmod +x git-rain |
| 119 | +sudo mv git-rain /usr/local/bin/ |
| 120 | +``` |
| 121 | + |
| 122 | +**Manual binary install (Windows PowerShell):** |
| 123 | +```powershell |
| 124 | +New-Item -ItemType Directory -Force "$env:USERPROFILE\bin" | Out-Null |
| 125 | +Move-Item .\git-rain.exe "$env:USERPROFILE\bin\git-rain.exe" -Force |
| 126 | +``` |
| 127 | +Then add `$env:USERPROFILE\bin` to your user `PATH` if not already present. |
| 128 | + |
| 129 | +### Verify install |
| 130 | + |
| 131 | +```bash |
| 132 | +git-rain --version |
| 133 | +which git-rain |
| 134 | +``` |
| 135 | + |
| 136 | +### Build from source |
| 137 | + |
| 138 | +```bash |
| 139 | +git clone https://github.com/git-fire/git-rain.git |
| 140 | +cd git-rain |
| 141 | +make build # produces ./git-rain |
| 142 | +make install # installs to ~/.local/bin/git-rain |
| 143 | +``` |
| 144 | + |
| 145 | +Requires Go 1.24.2+. |
| 146 | + |
| 147 | +## How It Works |
| 148 | + |
| 149 | +1. **Scan** — walks your configured scan path discovering git repositories |
| 150 | +2. **Fetch** — runs `git fetch --all --prune` for each repo |
| 151 | +3. **Sync** — for each local branch with a tracked upstream: |
| 152 | + - If the branch can be fast-forwarded: updates it |
| 153 | + - If the branch has local-only commits: skips (safe mode) or backs up and resets (risky mode) |
| 154 | + - If the working tree is dirty on the checked-out branch: skips |
| 155 | +4. **Report** — per-branch outcomes: updated, up-to-date, skipped (with reason), failed |
| 156 | + |
| 157 | +## Key Features |
| 158 | + |
| 159 | +- **One-command sync** — discover and update all repos from a single invocation |
| 160 | +- **Safety-first defaults** — never rewrites local-only commits; dirty worktrees are skipped, not clobbered |
| 161 | +- **Risky mode** — opt-in destructive realignment: creates a `git-rain-backup-*` ref, then hard-resets to upstream |
| 162 | +- **Non-checked-out branches** — updated directly without touching the worktree |
| 163 | +- **Interactive TUI** — streaming repo selector lets you pick exactly which repos to sync |
| 164 | +- **Registry** — discovered repos persist across runs; mark repos ignored to skip them permanently |
| 165 | +- **Dry run** — preview all repos that would be synced without making any changes |
| 166 | +- **Fetch-only mode** — run `git fetch --all --prune` everywhere without touching local refs |
| 167 | + |
| 168 | +## Core Commands |
| 169 | + |
| 170 | +```bash |
| 171 | +# dry run — preview repos, no changes |
| 172 | +git-rain --dry-run |
| 173 | + |
| 174 | +# default run — scan and sync all repos |
| 175 | +git-rain |
| 176 | + |
| 177 | +# interactive repo selection before syncing |
| 178 | +git-rain --select |
| 179 | + |
| 180 | +# fetch only (no local ref updates) |
| 181 | +git-rain --fetch-only |
| 182 | + |
| 183 | +# sync only known registry repos, skip filesystem scan |
| 184 | +git-rain --no-scan |
| 185 | + |
| 186 | +# scan a specific path |
| 187 | +git-rain --path ~/projects |
| 188 | + |
| 189 | +# risky mode — realign local-only commits after creating backup branches |
| 190 | +git-rain --risky |
| 191 | + |
| 192 | +# generate example config file |
| 193 | +git-rain --init |
| 194 | +``` |
| 195 | + |
| 196 | +## Flags |
| 197 | + |
| 198 | +| Flag | Description | |
| 199 | +|---|---| |
| 200 | +| `--dry-run` | Show what would be synced without making changes | |
| 201 | +| `--select` | Interactive TUI repo selector before syncing | |
| 202 | +| `--fetch-only` | Fetch from all remotes but skip local ref updates | |
| 203 | +| `--path <dir>` | Scan path override (default: config `global.scan_path`) | |
| 204 | +| `--no-scan` | Skip filesystem scan; hydrate only known registry repos | |
| 205 | +| `--risky` | Allow destructive local branch realignment after creating backup refs | |
| 206 | +| `--init` | Generate example `~/.config/git-rain/config.toml` | |
| 207 | +| `--config <file>` | Use an explicit config file path | |
| 208 | +| `--force-unlock-registry` | Remove stale registry lock file without prompting | |
| 209 | +| `--version` | Print version and exit | |
| 210 | + |
| 211 | +## Configuration |
| 212 | + |
| 213 | +Config file: `~/.config/git-rain/config.toml` |
| 214 | + |
| 215 | +Generate an example: |
| 216 | + |
| 217 | +```bash |
| 218 | +git-rain --init |
| 219 | +``` |
| 220 | + |
| 221 | +Key options: |
| 222 | + |
| 223 | +```toml |
| 224 | +[global] |
| 225 | +scan_path = "/home/you/projects" # root to discover repos under |
| 226 | +scan_depth = 5 # max directory depth |
| 227 | +scan_workers = 8 # parallel scan workers |
| 228 | +risky_mode = false # enable risky mode globally |
| 229 | +default_mode = "safe" # "safe" or "risky" |
| 230 | +disable_scan = false # skip scan; use registry only |
| 231 | + |
| 232 | +scan_exclude = [ |
| 233 | + "node_modules", |
| 234 | + ".cache", |
| 235 | + "vendor", |
| 236 | +] |
| 237 | +``` |
| 238 | + |
| 239 | +All options can be overridden with environment variables using the `GIT_RAIN_` prefix: |
| 240 | + |
| 241 | +```bash |
| 242 | +GIT_RAIN_GLOBAL_RISKY_MODE=true git-rain |
| 243 | +GIT_RAIN_GLOBAL_SCAN_PATH=/tmp/repos git-rain |
| 244 | +``` |
| 245 | + |
| 246 | +## TUI Selector |
| 247 | + |
| 248 | +`git-rain --select` opens an interactive selector. Repositories stream in as the filesystem scan finds them — no waiting for the full scan to complete before you can start picking. |
| 249 | + |
| 250 | +**Key bindings:** |
| 251 | + |
| 252 | +| Key | Action | |
| 253 | +|---|---| |
| 254 | +| `space` | Toggle repo selection | |
| 255 | +| `a` | Select all / deselect all | |
| 256 | +| `enter` | Confirm selection and begin sync | |
| 257 | +| `q` / `esc` | Abort | |
| 258 | +| `↑` / `↓` | Navigate | |
| 259 | + |
| 260 | +## Safe Mode vs Risky Mode |
| 261 | + |
| 262 | +| Situation | Safe mode (default) | Risky mode (`--risky`) | |
| 263 | +|---|---|---| |
| 264 | +| Branch is fast-forwardable | ✓ Updated | ✓ Updated | |
| 265 | +| Branch has local-only commits | ⊘ Skipped | ⚠ Backed up + reset | |
| 266 | +| Checked-out branch, dirty worktree | ⊘ Skipped | ⊘ Skipped | |
| 267 | +| No upstream tracked | ⊘ Skipped | ⊘ Skipped | |
| 268 | + |
| 269 | +In risky mode, a `git-rain-backup-<branch>-<timestamp>` ref is created before any hard reset so local work is always recoverable. |
| 270 | + |
| 271 | +## Registry |
| 272 | + |
| 273 | +Discovered repositories are stored in `~/.config/git-rain/repos.toml`. Each entry tracks path, name, status, and last-seen time. |
| 274 | + |
| 275 | +Repo statuses: |
| 276 | +- `active` — present on disk and eligible for sync |
| 277 | +- `missing` — was discovered previously but the directory is gone |
| 278 | +- `ignored` — permanently excluded from sync |
| 279 | + |
| 280 | +The registry uses a file lock to prevent concurrent `git-rain` instances from corrupting it. If a previous run exited uncleanly, `git-rain` detects the stale lock and prompts to remove it (or pass `--force-unlock-registry` for non-interactive use). |
| 281 | + |
| 282 | +## Security Notes |
| 283 | + |
| 284 | +`git-rain` shells out to the system `git` binary and inherits your existing git credentials (SSH agent, credential helper, etc.). No credentials are stored or transmitted by `git-rain` itself. |
| 285 | + |
| 286 | +Secret detection: `git-rain` sanitizes error messages and log output to avoid echoing paths or git output that might contain tokens. This is a best-effort measure — keep secrets out of repo paths and remote URLs. |
| 287 | + |
| 288 | +## Contributing |
| 289 | + |
| 290 | +Contributions are welcome. Tests use [git-testkit](https://github.com/git-fire/git-testkit) for building git repository fixtures in integration-style tests. Prefer table-driven tests and real `git` invocations over mocks. |
| 291 | + |
| 292 | +```bash |
| 293 | +make test-race # run all tests with race detector |
| 294 | +make lint # go vet |
| 295 | +``` |
| 296 | + |
| 297 | +Open an issue before starting large changes. |
| 298 | + |
| 299 | +## License |
| 300 | + |
| 301 | +MIT. See [LICENSE](LICENSE). |
0 commit comments