Metadata-Version: 2.5
Name: polygit
Version: 0.13.0
Summary: Manage multiple git repos from a single .pgit config file.
Project-URL: Repository, https://github.com/gschaetz/polygit
Author-email: Gary Schaetz <gary@schaetzkc.com>
License: MIT License
        
        Copyright (c) 2025 Gary Schaetz
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE.txt
Keywords: devtools,git,multi-repo,pgit,polygit,repos
Requires-Python: >=3.10
Requires-Dist: pyyaml>=6.0
Description-Content-Type: text/markdown

# polygit

Manage multiple git repos from a single `.pgit` config file.

```bash
uv tool install polygit
uv tool upgrade polygit    # later, to get the latest release
```

## Usage

```bash
pgit sync      # clone missing repos, fetch all
pgit status    # show repos with uncommitted changes
pgit pull      # pull all repos, auto-stashing local changes
pgit commit    # commit dirty repos without pushing
pgit push      # interactively commit and push dirty repos
pgit merge     # merge a target branch into each repo
pgit checkout  # switch every repo to a branch, or create it with -b
pgit repos     # discover drift between .pgit and the filesystem
pgit branches  # show current branch and tracking status per repo
```

`status`, `pull`, `push` *and* `sync` accept `--jobs N` to control how many
repositories are processed in parallel. The default (when omitted) is 8.

```bash
pgit sync --jobs 4
pgit status --jobs 1
pgit pull   --only github --dry-run --jobs 4
pgit branches --only github --jobs 4
```

`N` must be a positive integer; otherwise the command prints a clear error and
exits with status 2.

`status`, `pull` and `push` accept `--only <platform>` to operate on a single platform
(e.g. `github`) instead of all of them. `branches` also accepts `--only`:

```bash
pgit status --only gitlab
pgit pull   --only github --dry-run
pgit push   --only gitlab --all
pgit branches --only github
```

Passing an unknown platform prints an error to stderr and exits 1.


## search

`pgit search <TERM>` searches repos by name, path, or remote URL with ranked results:

```bash
pgit search polygit              # finds "polygit" and other repos containing the term
pgit search polygit --limit 10   # limit to top 10 matches
pgit search gsch --only github   # only GitHub repos matching "gsch"
pgit search term --json          # output as JSON array with name, path, platform, remote, score
```

Matching is by prefix, word boundary, or substring of the name, path, or `origin` remote URL; misspelled terms are not matched. `--limit N` (default: all) caps the results. `--limit 0` returns nothing. `--only <platform>` filters to one platform before ranking. `--jobs N` sets parallelism for fetching remotes. `TERM` is required and case-insensitive; missing or empty term exits with status 2.

## log

`pgit log` lists recent commits across all cloned repos, newest first. It reads local refs only and never fetches, so run `pgit sync` first to see remote activity:

```bash
pgit log                       # the 20 newest commits across all repos
pgit log --limit 50            # change the cap (--limit 0 prints nothing)
pgit log --since 2.weeks       # YYYY-MM-DD, or N.unit such as "2.weeks" / "3 days"
pgit log --author gary         # author name or email contains the text (case-insensitive, literal)
pgit log --only github --json  # JSON array with name, path, platform, hash, author, date, subject
```

Each line reads `<hash> <date> <name> (<author>) <subject>`. Commits come from each repo's current `HEAD`; repos that are not cloned or have no commits are skipped. Equal dates are ordered by repo path, then hash. An invalid `--since` or `--limit` exits with status 2. `--jobs N` sets parallelism.

## commit

`pgit commit` commits dirty repos without pushing to remotes:

```bash
pgit commit                # prompt for a message per repo (default)
pgit commit --all          # one message for all dirty repos
pgit commit --wip          # auto-commit with "WIP <timestamp>"
pgit commit --dry-run      # list dirty repos that would be committed
pgit commit --wip --json   # emit JSON array of commit results
```

Note: `--json` is available with `--all` or `--wip` modes.

## merge

`pgit merge <branch>` merges a target branch into the current branch of each cloned repo:

```bash
pgit merge main            # merge local 'main' into current branch of each repo
pgit merge main --no-ff    # force a merge commit
pgit merge main --squash   # stage the merged changes without committing
pgit merge main --dry-run  # list repos that would be merged or skipped; changes nothing
pgit merge main --json     # emit JSON array of merge results
```

`pgit merge` acts on local branches and does not fetch from remotes — run `pgit sync` first if you need updated remote branches. Repos with dirty working trees, detached HEADs, or missing target branches are skipped safely with a clear reason.

## checkout

`pgit checkout <branch>` switches every cloned repo to an existing local branch; with `-b` it creates the branch everywhere:

```bash
pgit checkout main                         # switch every repo to local 'main'
pgit checkout -b feat/login                # create 'feat/login' at each repo's current HEAD
pgit checkout -b feat/login --from origin/main   # create it at origin/main instead
pgit checkout -                            # switch every repo back to the branch it was on before
pgit checkout main --dry-run               # list what would happen; changes nothing
pgit checkout -b feat/login --json         # emit JSON array of results
```

Local work is never overwritten. Switching to an existing branch, or creating from `--from`, skips repos with uncommitted changes (untracked files count). Plain `-b` only adds a branch at the current commit, so it works on a dirty repo and your changes come along, as with `git checkout -b`.

Each repo is reported as `switched`, `created`, `up-to-date` or `failed`, or `skipped` with a reason: dirty working tree, detached HEAD, no commits yet, merge/rebase/cherry-pick in progress, branch not found (without `-b`), branch already exists (with `-b`), or start point not found. A skip in one repo never stops the others, and re-running is safe — repos already on the branch are reported as up to date.

`pgit checkout -` switches each repo to the branch it was on before its last switch (git's `checkout -`), resolved per repo, so a tree mixing `main` and `master` can be restored in one command. Running it twice toggles back. A repo with no previous branch (never switched, previous state detached, or the branch was deleted) is skipped as `no previous branch`. `-` cannot be combined with `-b`.

`pgit checkout` reads local refs only and never fetches, so a branch that exists only on the remote counts as not found; run `pgit sync` first and use `-b <branch> --from origin/<branch>` to start from it. `--from` is only valid with `-b`.

## Configuration

Create a `.pgit` file in the root directory that contains your repos:

```yaml
github:
  your-username:
    repo-one:
    repo-two:
  some-org:
    their-repo: {remote: "some-org/their-repo"}

gitlab:
  your-namespace/group:
    private-repo:
    another-repo:
```

`pgit` discovers this file by walking up from the current directory — so running `pgit status` anywhere inside your repos root just works.

### Remote overrides

If a repo is cloned to a local path that differs from its remote path, use the `remote` key:

```yaml
github:
  my-local-name:
    project: {remote: "upstream-org/project"}
```

## push modes

```bash
pgit push                # prompt for a message per repo (default)
pgit push --all          # one message for all dirty repos
pgit push --wip          # auto-commit with "WIP <timestamp>"
```

## dry run

```bash
pgit pull --dry-run      # show ahead/behind per repo; changes nothing
pgit push --dry-run      # list dirty repos that would be committed and pushed
```

`pull --dry-run` compares against the last fetched state of each upstream, so run `pgit sync` first for current numbers.

## branches

`pgit branches` shows the current branch and upstream tracking status for each cloned repository:

```bash
pgit branches
```

Output shows one line per repo:

- **✓** — up to date with upstream
- **↑** — ahead of upstream (needs a push)
- **↓** — behind upstream (needs a pull)
- **⚠** — diverged (both ahead and behind), upstream deleted on the remote, or detached HEAD
- **~** — branch has no upstream configured (also an empty repo)

```text
✓ github/gschaetz/polygit: main -> origin/main (up to date)
↑ github/gschaetz/tools: main -> origin/main (2 ahead)
↓ github/gschaetz/notes: main -> origin/main (3 behind)
⚠ github/gschaetz/dotfiles: main -> origin/main (3 ahead, 1 behind)
⚠ github/gschaetz/old: feat -> origin/feat (upstream gone)
⚠ github/gschaetz/lib: detached HEAD at a1b2c3d
~ github/gschaetz/experiment: feature-xyz (no upstream)
```

Like `pull --dry-run`, this reads the last fetched state of each upstream, so run `pgit sync` first for current numbers.

Use `--only <platform>` to filter by platform (e.g. `pgit branches --only gitlab`).
Use `--jobs N` to control parallelism (default: 8).

### branches --json

```bash
pgit branches --json   # machine-readable status for every cloned repo
```

Prints a single JSON array on stdout and nothing else. Unlike `status --json`, clean repos are included:

```json
[{"platform": "github", "path": "gschaetz/polygit", "branch": "main", "sha": null,
  "upstream": "origin/main", "ahead": 0, "behind": 0, "state": "tracking"}]
```

`state` is `tracking`, `none` (no upstream), `gone` (upstream deleted on the remote) or `detached`. When detached, `branch` is `null` and `sha` holds the short commit hash. `upstream` is `null` when there is none.

## review

`pgit review` opens a PR/MR for every cloned repo whose current branch has been pushed and is ahead of the default branch:

```bash
pgit review                # prompt per repo (default)
pgit review --all          # one PR title/body for all eligible repos
pgit review --auto         # auto-generate title from branch name, minimal body
pgit review --auto --dry-run
pgit review --base release
pgit review --json
```

Detection: a repo qualifies when it is tracking the same-named upstream branch (`feat/x` → `origin/feat/x`), that branch is ahead of the base, and it is not itself the base branch. Only local refs are read; run `pgit sync` first if you want current numbers.

The PR base is the remote's default branch (`origin/HEAD`, falling back to `main` then `master`). Pass `--base <branch>` to override it (e.g. for release branches).

With `--auto`, PR titles are generated from branch names (`feat/auth` → `feat: auth`). With `--all`, one title/body is prompted for and shared. In interactive mode you get a per-repo prompt with a diff summary and a blank title skips the repo.

`review` creates PRs via the GitHub/GitLab API when a token is available:

```bash
GH_TOKEN=<token> pgit review --auto     # GitHub
GITLAB_TOKEN=<token> pgit review --auto # GitLab
```

Token scopes: GitHub needs `repo`; GitLab needs `api`. Without a token, `pgit review` prints ready-to-click PR/MR URLs for manual creation.

```bash
pgit review --auto --dry-run   # list candidates with planned titles/URLs; creates nothing
pgit review --only github      # review only GitHub repos
pgit review --jobs 4            # parallelism for non-interactive modes (default: 8)
```

`--json` (with `--all` or `--auto`) prints a single JSON array on stdout and nothing else.

```json
[{"platform": "github", "path": "r", "branch": "feat/x", "status": "created", "url": "https://...", "error": null}]
```

`status` is `created` (PR created via API), `manual` (no token — URL only), `planned` (`--dry-run`), or `failed`.

Branches must be pushed first — run `pgit push` before `pgit review`.

## status --json

```bash
pgit status --json   # machine-readable list of dirty repos
```

Prints a single JSON array on stdout and nothing else:

```json
[{"platform": "github", "path": "gschaetz/mousehat", "changed_files": 2}]
```

`changed_files` is the number of entries in `git status --short`. Clean repos are omitted, so the array is empty when there is nothing to commit.

## repos — drift detection

`pgit repos` compares what's actually on disk against your `.pgit` config and reports:

- **Remote mismatch** — repo is at the right path but the git remote differs (e.g. SSH vs HTTPS)
- **Wrong location** — remote is tracked in `.pgit` but cloned to a different directory
- **Untracked** — repo on disk not referenced in `.pgit` at all
- **Missing** — in `.pgit` but not yet cloned (run `pgit sync`)

It then offers to update `.pgit` or rename directories interactively.

## Migrating from repos.yaml

If you were using the `sync.sh` predecessor, rename your `repos.yaml` to `.pgit` — the format is identical.

## Requirements

- Python 3.10+
- git
