···11+# pds-git-remote
22+33+A git remote helper that stores repositories on an [AT Protocol](https://atproto.com) Personal Data Server (PDS). Push, clone, and fetch git repos using `pds://` URLs — your code lives alongside your Bluesky data.
44+55+Repositories are stored as chains of incremental [git bundles](https://git-scm.com/docs/git-bundle) uploaded as PDS blobs, tracked by a mutable state record.
66+77+## Quick start
88+99+```bash
1010+# build
1111+cargo build
1212+1313+# add the binary to your PATH
1414+export PATH="$(pwd)/target/debug:${PATH}"
1515+1616+# log in to your PDS
1717+git-remote-pds auth login --pds-url https://your-pds.example.com --handle alice.example.com
1818+1919+# push an existing repo
2020+cd my-project
2121+git remote add pds pds://alice.example.com/my-project
2222+git push pds main
2323+2424+# clone it elsewhere
2525+git clone pds://alice.example.com/my-project
2626+```
2727+2828+## How it works
2929+3030+Git invokes `git-remote-pds` automatically when it sees a `pds://` URL. The remote helper speaks git's [remote helper protocol](https://git-scm.com/docs/gitremote-helpers) over stdin/stdout.
3131+3232+**Push**: creates a git bundle of new commits, uploads it as a PDS blob, and writes a state record tracking the bundle chain and current refs.
3333+3434+**Fetch/Clone**: reads the remote state record, downloads bundles the local repo doesn't have yet, and applies them with `git bundle unbundle`.
3535+3636+Bundles larger than 40 MB are automatically chunked into multiple blobs to stay within PDS upload limits.
3737+3838+### State record
3939+4040+Each repository is stored under the `sh.pdsbackup.git.state` collection as a single record. The record contains:
4141+4242+- **refs** — current branch tips (name + SHA)
4343+- **bundles** — ordered chain of bundle entries, each with blob CIDs, prerequisite commits, and tip commits
4444+4545+## Authentication
4646+4747+### `auth login`
4848+4949+```bash
5050+git-remote-pds auth login --pds-url https://your-pds.example.com --handle alice.example.com
5151+```
5252+5353+Prompts for your password and stores credentials in `~/.config/pds-git-remote/auth.json`.
5454+5555+### `auth status`
5656+5757+```bash
5858+git-remote-pds auth status
5959+```
6060+6161+Shows stored credentials.
6262+6363+### `auth logout`
6464+6565+```bash
6666+git-remote-pds auth logout --handle alice.example.com
6767+```
6868+6969+### Environment variables
7070+7171+For CI or scripting, you can authenticate without stored credentials:
7272+7373+| Variable | Description |
7474+|----------|-------------|
7575+| `PDS_HANDLE` + `PDS_PASSWORD` | Log in on the fly |
7676+| `PDS_ACCESS_TOKEN` + `PDS_DID` | Use a token directly |
7777+| `PDS_URL` | Override PDS endpoint (skips identity resolution) |
7878+7979+## Identity resolution
8080+8181+The `pds://handle/repo` URL format uses AT Protocol identity resolution:
8282+8383+1. Resolve handle to DID via `com.atproto.identity.resolveHandle`
8484+2. Resolve DID to PDS endpoint via PLC directory or `did:web`
8585+8686+Set `PDS_URL` to skip resolution and point directly at a PDS (useful for local development).
8787+8888+## Development
8989+9090+### Running tests
9191+9292+```bash
9393+# unit and integration tests (no PDS required)
9494+cargo test
9595+9696+# e2e tests (requires a running PDS — see below)
9797+cargo test --features e2e
9898+```
9999+100100+### Local PDS (Docker)
101101+102102+```bash
103103+./scripts/pds-dev/start.sh # start PDS at localhost:3000
104104+./scripts/pds-dev/create-account.sh alice secret123 # create a test account
105105+./scripts/pds-dev/run-e2e.sh # full test harness
106106+./scripts/pds-dev/stop.sh # tear down
107107+```
108108+109109+### Remote PDS testing
110110+111111+Create a `tests/.testenv` file:
112112+113113+```
114114+PDS_URL=https://your-pds.example.com
115115+PDS_HANDLE=you.your-pds.example.com
116116+PDS_PASSWORD=your-password
117117+```
118118+119119+Then run:
120120+121121+```bash
122122+./scripts/remote-test/run.sh
123123+```
124124+125125+This tests push, clone, and incremental fetch against the configured PDS.
126126+127127+## License
128128+129129+MIT
+14-8
scripts/remote-test/run.sh
···1616SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
1717CRATE_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
18181919-# ── check required env vars ──────────────────────────────────────────
2020-for var in PDS_URL PDS_HANDLE PDS_PASSWORD; do
2121- if [ -z "${!var:-}" ]; then
2222- echo "ERROR: ${var} is not set"
2323- echo "Usage: PDS_URL=https://... PDS_HANDLE=alice.example PDS_PASSWORD=secret $0"
2424- exit 1
2525- fi
2626-done
1919+# ── load credentials ─────────────────────────────────────────────────
2020+TESTENV="${CRATE_DIR}/tests/.testenv"
2121+if [ -f "${TESTENV}" ]; then
2222+ echo "Loading ${TESTENV}..."
2323+ source "${TESTENV}"
2424+else
2525+ for var in PDS_URL PDS_HANDLE PDS_PASSWORD; do
2626+ if [ -z "${!var:-}" ]; then
2727+ echo "ERROR: ${var} is not set and no tests/.testenv found"
2828+ echo "Usage: PDS_URL=https://... PDS_HANDLE=alice.example PDS_PASSWORD=secret $0"
2929+ exit 1
3030+ fi
3131+ done
3232+fi
27332834echo "PDS_URL = ${PDS_URL}"
2935echo "PDS_HANDLE = ${PDS_HANDLE}"
+3-5
src/remote_helper.rs
···44//! between standard git commands and the PDS-backed bundle storage.
55//! All diagnostic output goes to stderr; stdout is protocol-only.
6677-use std::path::PathBuf;
88-97use pds_git_remote::auth;
108use pds_git_remote::fetch::{download_and_apply, find_new_bundles, read_remote_state};
119use pds_git_remote::identity;
···190188 // read remote state
191189 let state = read_remote_state(&client, &did, repo_name).await?;
192190193193- // determine the git dir for the local repo
194194- let git_dir = std::env::var("GIT_DIR").unwrap_or_else(|_| ".git".to_string());
195195- let repo_path = PathBuf::from(&git_dir);
191191+ // determine the repo root (git sets CWD to the working tree when invoking remote helpers)
192192+ let repo_path =
193193+ std::env::current_dir().map_err(|e| format!("failed to get current directory: {}", e))?;
196194197195 // find which bundles we need
198196 let new_bundles = find_new_bundles(&repo_path, &state.bundles).await?;