⚘ use your pds as a git remote if you want to ⚘
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

working version

notplants 0caa8bae 9321eb01

+147 -13
+1
.gitignore
··· 1 1 /target 2 2 scripts/pds-dev/pds-data/ 3 3 scripts/pds-dev/pds.env 4 + tests/.testenv
+129
README.md
··· 1 + # pds-git-remote 2 + 3 + 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. 4 + 5 + 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. 6 + 7 + ## Quick start 8 + 9 + ```bash 10 + # build 11 + cargo build 12 + 13 + # add the binary to your PATH 14 + export PATH="$(pwd)/target/debug:${PATH}" 15 + 16 + # log in to your PDS 17 + git-remote-pds auth login --pds-url https://your-pds.example.com --handle alice.example.com 18 + 19 + # push an existing repo 20 + cd my-project 21 + git remote add pds pds://alice.example.com/my-project 22 + git push pds main 23 + 24 + # clone it elsewhere 25 + git clone pds://alice.example.com/my-project 26 + ``` 27 + 28 + ## How it works 29 + 30 + 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. 31 + 32 + **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. 33 + 34 + **Fetch/Clone**: reads the remote state record, downloads bundles the local repo doesn't have yet, and applies them with `git bundle unbundle`. 35 + 36 + Bundles larger than 40 MB are automatically chunked into multiple blobs to stay within PDS upload limits. 37 + 38 + ### State record 39 + 40 + Each repository is stored under the `sh.pdsbackup.git.state` collection as a single record. The record contains: 41 + 42 + - **refs** — current branch tips (name + SHA) 43 + - **bundles** — ordered chain of bundle entries, each with blob CIDs, prerequisite commits, and tip commits 44 + 45 + ## Authentication 46 + 47 + ### `auth login` 48 + 49 + ```bash 50 + git-remote-pds auth login --pds-url https://your-pds.example.com --handle alice.example.com 51 + ``` 52 + 53 + Prompts for your password and stores credentials in `~/.config/pds-git-remote/auth.json`. 54 + 55 + ### `auth status` 56 + 57 + ```bash 58 + git-remote-pds auth status 59 + ``` 60 + 61 + Shows stored credentials. 62 + 63 + ### `auth logout` 64 + 65 + ```bash 66 + git-remote-pds auth logout --handle alice.example.com 67 + ``` 68 + 69 + ### Environment variables 70 + 71 + For CI or scripting, you can authenticate without stored credentials: 72 + 73 + | Variable | Description | 74 + |----------|-------------| 75 + | `PDS_HANDLE` + `PDS_PASSWORD` | Log in on the fly | 76 + | `PDS_ACCESS_TOKEN` + `PDS_DID` | Use a token directly | 77 + | `PDS_URL` | Override PDS endpoint (skips identity resolution) | 78 + 79 + ## Identity resolution 80 + 81 + The `pds://handle/repo` URL format uses AT Protocol identity resolution: 82 + 83 + 1. Resolve handle to DID via `com.atproto.identity.resolveHandle` 84 + 2. Resolve DID to PDS endpoint via PLC directory or `did:web` 85 + 86 + Set `PDS_URL` to skip resolution and point directly at a PDS (useful for local development). 87 + 88 + ## Development 89 + 90 + ### Running tests 91 + 92 + ```bash 93 + # unit and integration tests (no PDS required) 94 + cargo test 95 + 96 + # e2e tests (requires a running PDS — see below) 97 + cargo test --features e2e 98 + ``` 99 + 100 + ### Local PDS (Docker) 101 + 102 + ```bash 103 + ./scripts/pds-dev/start.sh # start PDS at localhost:3000 104 + ./scripts/pds-dev/create-account.sh alice secret123 # create a test account 105 + ./scripts/pds-dev/run-e2e.sh # full test harness 106 + ./scripts/pds-dev/stop.sh # tear down 107 + ``` 108 + 109 + ### Remote PDS testing 110 + 111 + Create a `tests/.testenv` file: 112 + 113 + ``` 114 + PDS_URL=https://your-pds.example.com 115 + PDS_HANDLE=you.your-pds.example.com 116 + PDS_PASSWORD=your-password 117 + ``` 118 + 119 + Then run: 120 + 121 + ```bash 122 + ./scripts/remote-test/run.sh 123 + ``` 124 + 125 + This tests push, clone, and incremental fetch against the configured PDS. 126 + 127 + ## License 128 + 129 + MIT
+14 -8
scripts/remote-test/run.sh
··· 16 16 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 17 17 CRATE_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" 18 18 19 - # ── check required env vars ────────────────────────────────────────── 20 - for var in PDS_URL PDS_HANDLE PDS_PASSWORD; do 21 - if [ -z "${!var:-}" ]; then 22 - echo "ERROR: ${var} is not set" 23 - echo "Usage: PDS_URL=https://... PDS_HANDLE=alice.example PDS_PASSWORD=secret $0" 24 - exit 1 25 - fi 26 - done 19 + # ── load credentials ───────────────────────────────────────────────── 20 + TESTENV="${CRATE_DIR}/tests/.testenv" 21 + if [ -f "${TESTENV}" ]; then 22 + echo "Loading ${TESTENV}..." 23 + source "${TESTENV}" 24 + else 25 + for var in PDS_URL PDS_HANDLE PDS_PASSWORD; do 26 + if [ -z "${!var:-}" ]; then 27 + echo "ERROR: ${var} is not set and no tests/.testenv found" 28 + echo "Usage: PDS_URL=https://... PDS_HANDLE=alice.example PDS_PASSWORD=secret $0" 29 + exit 1 30 + fi 31 + done 32 + fi 27 33 28 34 echo "PDS_URL = ${PDS_URL}" 29 35 echo "PDS_HANDLE = ${PDS_HANDLE}"
+3 -5
src/remote_helper.rs
··· 4 4 //! between standard git commands and the PDS-backed bundle storage. 5 5 //! All diagnostic output goes to stderr; stdout is protocol-only. 6 6 7 - use std::path::PathBuf; 8 - 9 7 use pds_git_remote::auth; 10 8 use pds_git_remote::fetch::{download_and_apply, find_new_bundles, read_remote_state}; 11 9 use pds_git_remote::identity; ··· 190 188 // read remote state 191 189 let state = read_remote_state(&client, &did, repo_name).await?; 192 190 193 - // determine the git dir for the local repo 194 - let git_dir = std::env::var("GIT_DIR").unwrap_or_else(|_| ".git".to_string()); 195 - let repo_path = PathBuf::from(&git_dir); 191 + // determine the repo root (git sets CWD to the working tree when invoking remote helpers) 192 + let repo_path = 193 + std::env::current_dir().map_err(|e| format!("failed to get current directory: {}", e))?; 196 194 197 195 // find which bundles we need 198 196 let new_bundles = find_new_bundles(&repo_path, &state.bundles).await?;