Mirror to GitHub/GitLab
A push copy of your list on an external forge: setup, tokens, what travels, and how to read the errors.
A list can be mirrored to a repository on GitHub, GitLab or any other forge that speaks git over HTTPS. SetFork stays the source of truth and the mirror is a shop window and a backup: after every new version SetFork pushes the changes out on its own.
The mirror is one-way. Edits made on the forge never travel back and will be overwritten by the next push. If you want to edit from a terminal, push to SetFork itself.
What travels
| Travels | Stays behind |
|---|---|
The main branch with the full version history | Suggestion branches (drafts) |
Version tags v1, v2, … and release tags | Suggestions, discussions, reviews |
README.md — the shop window | Stars, watchers, runs |
list.json — the canonical list | List settings and access rights |
.gitattributes |
Exactly as when GitHub and GitLab mirror each other, only git objects travel: commits, branches and tags. Everything that lives around the repository stays in SetFork.
Step 1. Create an empty repository on the forge
GitHub — New repository, with no README, .gitignore or licence.
GitLab — New project → Create blank project, and clear the Initialize repository with a README checkbox.
⚠️ Do not create the repository with a README. The first mirror push force-overwrites
any foreign history; GitHub allows that, but on GitLab the main branch is protected by
default and force pushes to it are refused — the first push would fail.
Mirror a private list into a private repository: SetFork does not check visibility on the forge side.
Step 2. Get a token
You never need to enter a username: SetFork fills it in by hostname
(x-access-token for GitHub, oauth2 for GitLab, git for everything else).
GitHub
A fine-grained token — Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token:
- Repository access → Only select repositories → pick the mirror repository;
- Permissions → Repository permissions → Contents → Read and write;
- expiry is up to you; once it lapses the mirror stops pushing and records the error in the settings.
A classic token works too — it needs the repo scope.
GitLab
Project → Settings → Access tokens → Add new token:
- Role —
Maintainer: themainbranch is protected by default, and only Maintainers and Owners may push to a protected branch; - Scopes —
write_repository(read-write git access over HTTPS).
A deploy token with the write_repository scope works as well.
Other forges
Gitea, Codeberg, Forgejo, Bitbucket: any token with write access to the repository.
Anything that accepts https://user:token@host/path.git will work.
Step 3. Turn the mirror on in SetFork
Open the list → Settings → Mirror and fill in:
- Repository URL —
https://github.com/you/your-repo(a trailing.gitis fine). Onlyhttps://, and no credentials in the address itself; - Access token — from step 2. It is encrypted and never shown again. On later saves an empty field means "keep the current one".
Save sends the first push immediately, and the status appears next to it: the time of the last sync, or the full text of the error.
From then on SetFork pushes by itself — after every event that moves main:
- publishing a new version in the interface;
- a
git pushinto the list; - merging a suggestion, including a manual conflict resolution;
- creating a release tag.
The Sync now button pushes on demand — use it to retry a failed push as well. There are no automatic retries: after an error the next attempt happens on the next version or on that button.
Disable mirror clears the URL and the token. The repository on the forge is left untouched.
How the mirror catches up
The push uses an explicit set of refs:
git push --prune <remote> \
+refs/heads/main:refs/heads/main \
+refs/tags/*:refs/tags/*Three consequences follow:
+(force) — the mirror must match SetFork. Hand-made commits on the forge'smainare lost at the next push.--prune— a tag removed in SetFork disappears from the mirror too. Unrelated branches on the forge are left alone: onlymainfalls under the rule.- No
--mirror: it pushes everything underrefs/, and GitHub'srefs/pull/*are read-only — such a push would fail halfway.
Errors and what they mean
Git's error text is shown in the settings in full, with credentials stripped out.
| Error | Cause |
|---|---|
src refspec refs/heads/main does not match any | The list has no versions yet — there is nothing to publish |
Write access to repository not granted (403) | The token lacks Contents: Read and write, or it does not cover this repository |
Invalid username or token / HTTP Basic: Access denied | The token expired, was revoked, or was copied incompletely |
Repository not found | A typo in the URL, or a private repository the token cannot reach |
pre-receive hook declined (GitLab) | The token's role is below Maintainer, or the repository already had history and a force push into a protected branch is required |
timeout after 60s | The forge is unreachable from the server; press "Sync now" later |
SETFORK_MIRROR_SECRET is not set | Self-hosted installs only — see below |
Self-hosting
The mirror needs a shared token-encryption secret — the SETFORK_MIRROR_SECRET
variable, identical for the application and for the git core: the application
encrypts the token, the core decrypts it when pushing. Without the variable the
section still works, but every push honestly records the error in the status.
The secret is a random string; changing it makes every stored token unreadable
(they have to be entered again).
The server also needs outbound HTTPS access to the forge.