Metadata-Version: 2.3
Name: git-buckets
Version: 0.3.1
Summary: Git over S3: clone, fetch and push repositories backed by an S3 bucket.
Author: Full Duplex Media
Author-email: Full Duplex Media <contact@fullduplex.media>
License: Apache 2.0
Requires-Dist: boto3>=1.43.77
Requires-Dist: click>=8.4.2
Requires-Dist: keyring>=25.7.0
Requires-Dist: keyring-pass>=0.9.3
Requires-Dist: pyyaml>=6.0.3
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# git-buckets

`gb` is the end-user CLI for a git-buckets deployment: git repos and Python package indexes over HTTPS, backed by S3.
Someone else runs the deployment. You log in once and then use stock git and uv.

## Install

Prereqs:
- git (≥ 2.13, ≥ 2.40 for bundle-accelerated clones)
- [git-lfs](https://git-lfs.com/)
- Python ≥ 3.14

```sh
uv tool install git-buckets
# or: pipx install git-buckets
```

## First login

```sh
gb login git.example.com
```

Once per deployment, never per bucket. This signs you in through the browser, stores the session, writes the
credential-helper stanza for `https://*.git.example.com` plus `transfer.bundleURI = true` into your global gitconfig,
and records the deployment.

On a box with no browser, `--no-browser` prints a URL to open elsewhere and reads the code back at a masked prompt.

```sh
gb auth status              # deployments known, the user on each, where each secret lives
gb logout git.example.com   # revoke the session token, erase the stored credentials
```

## Daily use

Remote URLs are `https://<bucket>.<deployment-domain>/<repo>`: the first hostname label is the bucket's public label,
the path is the repo name, as deep as you like. `git clone https://my-bucket.git.example.com/tools/cli/my-project`, then
fetch, pull and push are stock git as well.

**New repos are made by pushing.** There is no create command, simply push to a new path on an existing bucket and
you've created a new repo.

```sh
git init && git remote add origin https://my-bucket.git.example.com/tools/gui/my-new-project
git push -u origin main
```

**Signed commits.** Buckets require them by default: every commit a push introduces is checked, so set up signing before
the first push (`gpg.format ssh` + `user.signingkey` + `commit.gpgsign` is the short route).

**LFS.** Downloads at any size and uploads up to 5 GB per file need nothing installed: objects come from the same host
under the same credential. Above 5 GB an upload needs gb's transfer agent, which also makes both directions parallel and
resumable. Once per repo:

```sh
gb lfs install [--remote <name>]
```

**Packages.** Each bucket is one Python index. In the consuming project's `pyproject.toml`:

```toml
[[tool.uv.index]]
name = "my-bucket-index"
url = "https://gb@my-bucket.git.example.com/packages/pypi/"
explicit = true

[tool.uv.sources]
my-package = { index = "my-bucket-index" }

[tool.uv]
keyring-provider = "subprocess"
```

Then just `uv sync` as normal. The `gb@` is **required**: uv only does keyring discovery when the index URL carries a
username.

Publishing needs one more line on the index, in the publishing project rather than the consuming one, naming the repo
the package belongs to:

```toml
[[tool.uv.index]]
name = "my-bucket-index"
url = "https://gb@my-bucket.git.example.com/packages/pypi/"
publish-url = "https://gb@my-bucket.git.example.com/packages/pypi/upload/tools/cli/my-project/"
explicit = true
```

Then `uv build && uv publish --index my-bucket-index`. Push rights are publish rights: the repo in the upload URL is the
one your token must reach with `rw`.

**CI.** A runner has no login session: it assumes the team's token role via OIDC and sets
`UV_KEYRING_PROVIDER=subprocess` and `GB_KEYRING_HOSTS="*.git.example.com"`. `GB_KEYRING_HOSTS` is CI only, never needed
on a user machine.

## Repo lifecycle

Every subcommand takes `<bucket>/<repo>`, plus `-H/--host <domain>` when more than one deployment is registered.

```sh
gb repo list
gb repo info my-bucket/tools/cli/my-project          # head, refs, size, last push; -v lists every ref
gb repo protect my-bucket/tools/cli/my-project main  # blocks delete while the ref exists
gb repo unprotect my-bucket/tools/cli/my-project main
gb repo delete my-bucket/tools/cli/my-project --yes  # without --yes it only prints what would go
```

Delete takes the repo's LFS objects, locks, bundles, packages and rendered web tree with it. The undo path is S3
versioning on the operator's side, not a `gb` command. There is no rename.

## Where the secrets live

The refresh token (one year, fixed from sign-in) goes in your OS keyring, or in `~/.config/gb/hosts.yml` at 0600 when no
keyring works: gb says which, once. On a headless Linux box, `pass` + gpg-agent is the supported keyring. The hourly ID
and session tokens are just caches under `~/.local/state/gb/<domain>/`.

## Troubleshooting

- git prompts for a username and password: the host isn't registered. `gb auth status`, then `gb login`.
- `the session for <domain> has expired`: the refresh token is past its year, or was revoked. Log in again.
- A push is refused but a fetch works: your grant on that prefix is read-only.
- A package index won't authenticate: `GB_KEYRING_DEBUG=1 uv sync` prints why the keyring backend declined.
