radicle-heartwood-lfs/LFS-IPFS.md

162 lines
7.5 KiB
Markdown

# Git LFS support, backed by IPFS
This is a fork of [Radicle's `heartwood`][heartwood] adding Git LFS (large file) support,
with large file content stored on each contributor's own local IPFS node rather than on a
central server.
[heartwood]: https://github.com/radicle-dev/heartwood
## Why not a server?
Radicle's own FAQ notes that its first-generation protocol was built on IPFS and moved away
from it — IPFS is a content-addressed store, and repositories are mutable, so it wasn't a fit
for Radicle's core storage. That objection doesn't apply here: Git LFS's own pattern already
separates a small, mutable pointer file (which Radicle replicates exactly as it replicates any
other git object) from immutable large-file content addressed by hash — which is precisely
what IPFS is good at. This fork only uses IPFS for that second part.
A seed-hosted HTTP LFS server was considered and prototyped first, but rejected: it would have
recreated a single point of failure, and Radicle's node/address book has no existing mechanism
to advertise a seed's HTTP endpoint (only P2P gossip addresses), so real seed discovery would
have needed new protocol work outside this fork's scope. Instead, every peer uses their own
local IPFS node, and the file-CID mapping travels with the repository itself.
## How it works
- **The mapping**: a `cid=<cid>` git note on `refs/notes/rad-lfs`, attached to each LFS
pointer file's own git blob hash (not the LFS oid — a different value). Verified against
this repo's actual replication code (`references_of` in
`crates/radicle/src/storage/git.rs`) that Radicle replicates every ref under a peer's
namespace except `refs/tmp/heads/*` — there's no fixed allowlist, so this notes ref
replicates the same way `refs/heads`/`refs/tags`/`refs/cobs` do, once the push/fetch
refspecs below are configured.
- **On commit**: a pre-commit hook (installed by `rad lfs init`) adds each staged LFS-tracked
file to your local IPFS daemon, pins it, and writes the note above.
- **On push/pull**: the actual bytes move through
[`rad-lfs-transfer`][rad-lfs-transfer], a Git LFS custom transfer agent — see that repo for
details. It's a separate binary/repository since it has nothing Radicle-specific in it; it
only needs a local IPFS daemon and a git repository with the notes above.
- **Seeding**: `rad seed` now also pins a repository's known LFS objects in your local IPFS
node (mirroring "seeding = keep a full copy"); `rad unseed` unpins them. Known prototype
limitation: no cross-repository pin refcounting, so unseeding one repository can unpin
content still needed by another seeded repository that happens to share the same CID.
[rad-lfs-transfer]: https://git.hswro.org/mab122/radicle-lfs-transfer
## Prerequisites
- A [Rust toolchain](https://rustup.rs) (stable; see `rust-toolchain.toml` for the exact
version this workspace pins), Git, and OpenSSH.
- [Git LFS](https://git-lfs.com) (`git-lfs` on your `PATH`) — only needed to *use* `rad lfs`,
not to build or install anything.
- A local IPFS (Kubo) daemon — also only needed to *use* `rad lfs`, not to build/install.
Install [Kubo](https://docs.ipfs.tech/install/) and make sure `ipfs` is on your `PATH`.
On Arch Linux:
```sh
sudo pacman -S --needed rust git openssh base-devel # build
sudo pacman -S --needed git-lfs kubo # use `rad lfs`
```
## Build & install
Follows upstream `heartwood`'s own install convention (`cargo install --root ~/.radicle`, one
shared install root for the whole stack), with `radicle-lfs-transfer` added the same way:
```sh
git clone --branch rad-lfs-ipfs --recurse-submodules \
ssh://git@git.hswro.org:9022/mab122/radicle-heartwood-lfs.git
cd radicle-heartwood-lfs
cargo install --path crates/radicle-cli --force --locked --root ~/.radicle
cargo install --path crates/radicle-node --force --locked --root ~/.radicle
cargo install --path crates/radicle-remote-helper --force --locked --root ~/.radicle
cargo install --path radicle-lfs-transfer --force --locked --root ~/.radicle
```
Add `~/.radicle/bin` to your `PATH` (e.g. in `~/.bashrc`/`~/.zshrc`):
```sh
export PATH="$HOME/.radicle/bin:$PATH"
```
Confirm everything landed correctly:
```sh
rad --version
command -v rad-lfs-transfer # just needs to resolve; it only speaks git-lfs's stdin/stdout protocol, no --help output
```
If you already cloned without `--recurse-submodules`, run `git submodule update --init` before
building — otherwise `radicle-lfs-transfer/` will be an empty directory and that last
`cargo install` step will fail.
## Run
Nothing extra to run for `rad`/`radicle-node` themselves — use them exactly as upstream
(`rad auth`, `radicle-node`, etc.; see the main [README](README.md) and
[`docs/`](https://app.radicle.xyz/nodes/seed.radicle.xyz/rad:zzz)). The only new moving part is
the IPFS daemon, needed only when you use `rad lfs`:
```sh
ipfs init # first time only, creates ~/.ipfs
ipfs daemon # leave running in the background while you use `rad lfs`
```
## Use
Inside a Radicle repository you're a contributor on (i.e. it already has a `rad` remote from
`rad init`/`rad clone`):
```sh
# One-time per repository (with the IPFS daemon above already running)
rad lfs init
# Then it's just Git LFS, as normal
git lfs track "*.psd" # or whatever large-file patterns you need
git add .gitattributes
git add my-large-file.psd
git commit -m "Add asset" # pre-commit hook pins it to IPFS and records the CID here
rad push # pushes the commit and the refs/notes/rad-lfs mapping together
```
Someone else cloning the same repository:
```sh
rad clone rad:<repo-id>
cd <repo>
rad lfs init # one-time, same as above (needs their own IPFS daemon running)
git lfs pull # fetches large files via IPFS instead of git
```
`rad seed`/`rad unseed` additionally pin/unpin a repository's known LFS objects in your local
IPFS node, so seeding a repository keeps a full copy of its large files too, not just its git
history.
### What happens without an IPFS daemon running
- `rad lfs init` checks upfront and refuses with a clear message
(`no IPFS (Kubo) daemon reachable at http://127.0.0.1:5001 — start one with 'ipfs daemon'`)
rather than silently continuing.
- `rad seed`/`rad unseed` print a warning and skip pinning/unpinning — seeding/unseeding itself
still succeeds.
- **Nothing else in this fork needs IPFS at all.** Building, installing, and using `rad` for
anything other than `rad lfs`/pinning works exactly like upstream `heartwood`, with zero IPFS
dependency.
## Troubleshooting
| Symptom | Cause / fix |
|---|---|
| `` `rad lfs init` must be run inside a Radicle repository working copy `` | Run it inside a directory that already has a `rad` remote (from `rad init` or `rad clone`), not a plain git repo. |
| `Git LFS is not installed (the 'git-lfs' binary was not found on your PATH)` | Install [Git LFS](https://git-lfs.com). |
| `no IPFS (Kubo) daemon reachable at ...` | Start one: `ipfs daemon` (see [Run](#run) above). |
| `rad-lfs-transfer: command not found` during `git lfs push`/`pull` | `cargo install --path radicle-lfs-transfer` didn't complete, or `~/.radicle/bin` isn't on `PATH`. |
| Large file didn't get pinned (commit went through, but no `rad-lfs` note) | The pre-commit hook silently skips pinning if the `ipfs` CLI isn't on `PATH` — check `command -v ipfs`. Re-add the file and commit again once it is. |
## Status
This is a prototype, not upstream Radicle functionality. See the commits on the
`rad-lfs-ipfs` branch for the full change set relative to upstream `heartwood`.