Tutorial#
This walks through mirroring one repository between a Forgejo instance and a Radicle node, from nothing. Replace the names below with your own:
| Placeholder | Stands for |
|---|---|
git.example.org |
your Forgejo instance |
seed.example.org |
your Radicle node's radicle-httpd address |
alice |
the Forgejo account that owns the repo |
my-project |
the repository |
Prerequisites#
- A Forgejo instance you can create repos and tokens on.
- A machine with
git, theradCLI, and a runningradicle-node— see Radicle's own seeder guide if you don't have one yet. - Go 1.23+, to build
graft.
1. Build graft#
git clone https://git.example.org/alice/graft.git
cd graft
go build -o graft ./cmd/sync
sudo mv graft /usr/local/bin/graft
2. Create a Forgejo token#
graft needs a token scoped to write:repository and write:issue on the repo it syncs.
curl -X POST -H "Authorization: token $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"graft","scopes":["write:repository","write:issue"]}' \
https://git.example.org/api/v1/users/alice/tokens
Save the returned token somewhere graft can read and nowhere else can:
mkdir -p /etc/graft
echo "$TOKEN" > /etc/graft/my-project.token
chmod 600 /etc/graft/my-project.token
3. Create the repo on Forgejo#
curl -X POST -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"my-project","auto_init":true,"default_branch":"main"}' \
https://git.example.org/api/v1/user/repos
auto_init gives it one commit, so there's something to mirror.
4. Create the repo on Radicle#
Radicle repos are created from an existing git checkout. Clone the one you just made and initialize it:
git clone https://git.example.org/alice/my-project.git
cd my-project
rad init --name my-project --default-branch main --public
This prints a Repository ID (RID), something like rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5. Write it down — the config needs it.
If you have other Radicle nodes that should also carry this repo, tell them to seed it:
rad seed rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
5. Write the config#
# /etc/graft/config.yaml
sync_interval: 5m
state_db: /var/lib/graft/state.db
repos:
- name: my-project
forgejo:
base_url: https://git.example.org
owner: alice
repo: my-project
token_file: /etc/graft/my-project.token
radicle:
rid: rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
http_base_url: https://seed.example.org
rad_home: /home/graft/.radicle
sync:
git: true
issues: true
patches: true
rad_home is the RAD_HOME of the identity graft pushes as — usually a dedicated one, created with rad auth under whatever user runs graft.
6. Run it once#
mkdir -p /var/lib/graft
graft -config /etc/graft/config.yaml -once
No output means no errors. Check both sides:
curl -s https://seed.example.org/api/v1/repos/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
If you open an issue on the Forgejo repo and run graft -once again, it shows up on Radicle within that one pass.
7. Run it as a service#
sudo cp systemd/graft.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now graft
It now mirrors both repos every sync_interval, in both directions, on its own.
Troubleshooting#
"token does not have at least one of required scope(s)" — the Forgejo token is missing write:issue or write:repository. Regenerate it with both.
"forgejo and rad diverged since last sync" — both sides got commits since the last pass that aren't ancestors of each other. graft won't guess which one wins; merge them by hand on either side and it'll pick up cleanly next pass.
"'remote-rad' is not a git command" — git-remote-rad (installed alongside rad) isn't on PATH for the user running graft. Add its directory to the systemd unit's Environment=PATH=....
A patch/PR never shows up on the other side — check that the corresponding repo isn't still empty on the destination. The first git sync has to land before patches/issues referencing that content can be mirrored.