Feature comparison against other forges, scoped to one user #185

closed cmc opened this on 2026-09-07 18:04 UTC

Discussion

cmc 2026-09-07 18:04 UTC

A comparison of everything Forgejo, Gitea and Sourcehut have produces a long table where gitbay says no a lot, and the noes that are decisions are already recorded in the FAQ. Scope it to a question instead: for one specific user we want to attract, what would stop them moving here?

Candidates:

  • CLI-native individual: pages (#15) and LFS (#16) shipped and the notification inbox reaches every surface, so the honest gaps are not known yet. Finding them is the work: take one such person's daily flow on their current forge and walk it here.
  • Small team: the collaboration surface (deploy keys, approvals, CODEOWNERS, review threads, notifications) is verified by tests and by nobody else.

Pick the user first; the comparison follows. Output: a comment here with the gaps ranked, each pointing at an issue or a decision not to.

cmc 2026-09-07 21:14 UTC

User: the CLI-native individual. One person, a handful of public repositories, most of them small tools with tagged releases and one that builds a personal site. Today on sourcehut (builds.sr.ht, pages, hut) or Codeberg (Forgejo, Woodpecker, tea). Interacts with the forge from a terminal, wants email for the rest, opens a browser to read a diff. The small-team candidate is not walked: with one human on the instance there is nobody to walk it with, and #139 already records that the collaboration surface is verified by tests alone.

The walk, in the order their day runs: register, create, push, build, release, publish the site, take bug reports, hear about them, keep a GitHub mirror, and bring history across. Against v1.15.0.

Works, no gap. Everything from stock OpenSSH; go install through the vanity path; raw file URLs, build badges, archive downloads; pages with custom domains (#15); LFS over SSH and HTTPS (#16); releases with assets and sha256 sums (#8); deploy keys read-only or --rw; push mirror to GitHub (#18); signed-commit verification by SSH or PGP key; closes #N from commit messages (#31); a login link by mail for a browser with no key (#155); the inbox on every surface (#113).

Already decided, and this user will hit it. No package registry, no email patch flow, no federation (FAQ). HTTPS is public-read only, so a private module needs GOPRIVATE and an insteadOf rewrite to SSH (Threat-Model, "anonymous surfaces carry no credentials"). The web editor cannot commit to a repository that requires signatures, so such a wiki is push-only (Parity). None of these need a new row.

Gaps, ranked by how early in the day they stop the move.

  1. CI does not run for their repositories on gitbay.org. The FAQ says so and #184 is where it gets decided. For a sourcehut user this is the product: builds on push, on a tag, on a schedule, publishing a release from the build. Until #184 lands one way or the other, the honest answer to "does my tool build on push" is no. Two things follow once it does: ci.yml has no artifacts, so a build that produces binaries has nowhere to put them except release asset add, and the only key scope that can run that command is full, so publishing from CI means a full-scope key in a build secret. A release-scoped key, or an artifact step, is the missing piece. Points at #184; the scope is a new issue once #184 says CI is a feature.

  2. The default branch is main and nothing changes it. repo create inits with --initial-branch=main, the store's default_branch column defaults to main, and both SetHead and UpdateDefaultBranch are reached only by repo import. A repository that arrives as master or trunk (sourcehut users, older projects) pushes fine and then HEAD names a branch that does not exist: git clone prints remote HEAD refers to nonexistent ref, unable to checkout and leaves an empty working tree (reproduced against a bare repo), and every surface reads the stored main and asks git for a branch that is not there. Day one, first push. No issue. Wants repo settings default-branch <name>, and probably a first push that moves an unborn HEAD to whatever arrived.

  3. No repository rename. repo transfer moves between owners and org rename exists, but a repository keeps the name it was created with. Renaming a project is routine for someone with many small ones. No issue. Wants repo rename <owner/name> <new-name>, with the same "clone URLs change" note transfer carries.

  4. History import speaks the GitHub API only. repo import takes git from any HTTPS remote, so code moves from anywhere. repo import-issues (#17) walks GitHub-shaped endpoints; --api-base exists but has only been pointed at api.github.com. Forgejo's API is close enough to GitHub's that Codeberg might work through it, and nobody has tried; sourcehut's tracker export (JSON) is not read at all. For this user the choice is losing the tracker or not moving. New issue: try Codeberg through --api-base and record the result; decide sourcehut separately.

  5. No Atom or RSS feeds. Not on releases, commits, or the activity feed; nothing serves application/atom+xml. Forgejo and sourcehut both do, and the people who follow this user's tools subscribe to releases that way. feed exists as a command, so the data is there and the row is a renderer. No issue.

  6. Issues take no attachments. A bug report with a screenshot means hosting the image elsewhere. Forgejo has attachments; sourcehut does not, so a sourcehut migrant will not miss it and a Codeberg one will. Release assets already stream to disk and back, so the storage exists. Decide, and if no, record it in the FAQ with the reason (the forge serving user-uploaded content on its own origin). No issue.

  7. Notifications: inbox and mail cannot be separated. #113 closed with this left open: there is no "inbox but no mail", and no default watch state. A person who reads the inbox from the CLI gets every row twice, and the only way to stop the mail is repo mute, which also stops the inbox. Reopen the tail of #113 or open a new issue.

  8. Nothing like paste.sr.ht. Sourcehut users share a snippet or a log through it several times a week. Neither Forgejo nor gitbay has the object. A decision either way belongs in the FAQ so the absence reads as one.

Small and unrelated, found on the walk. The Users wiki page still describes wikis as <repo>.wiki companion repositories, which #170 removed; the paragraph should point at .gitbay/wiki/.

What this says. The blockers are not features the FAQ declined. They are CI compute on the hosted instance (1) and three small pieces of repository plumbing (2, 3, 4) that GitHub, Forgejo and sourcehut all have and that only surface when a stranger arrives with existing repositories. Items 2 and 3 are afternoons; 4 is a test run against Codeberg before it is a design; 1 is #184.

cmc 2026-09-07 21:56 UTC

User: the small FOSS org. Three maintainers with different roles, one outside contributor, an org namespace, forks, review gates. Today on Codeberg (Forgejo) or GitHub. Walked with real accounts on gitbay.org against v1.15.0, so the results below are observed, not read from the code. Fixtures, left in place for inspection: users tt-alice (org admin), tt-bob (team core, write), tt-carol (team docs, read then write), tt-dave (outsider); org ttorg with members-role none; repos ttorg/widget, ttorg/lib, tt-dave/widget (fork); ttorg/widget!1 merged, !2 open, #1 closed. None of the accounts has an email address, so mail delivery was not exercised; every notification claim below is about the inbox.

The walk: create the org and teams, push a repo with CODEOWNERS (widget.go @tt-alice, docs/ @tt-carol), protect main, require one approval, codeowners and resolved threads; the outsider forks, pushes, opens a merge request; a maintainer is asked to review, comments on a line, requests changes; the contributor revises; threads resolve; owners approve; the admin merges; a release ships; a maintainer is offboarded.

Works, observed. Fork, merge request from the fork, mr review request, the inbox row it produces, diff-line thread, request changes, revision tracking, resolve, approve, the codeowners refusal naming the files and owners, fast-forward merge, merged and closed rows in the author's and a watcher's inboxes. Draft merge requests stay out of the review queue and refuse to merge. A write holder cannot change settings, delete the repository, or grant access (exit 4 each). An outsider cannot transfer into the org; the admin can. A deploy key clones and is refused every control command. Teams, roles, members-role none, milestones, releases with assets and sha256. Self-approval by the author is recorded and marked counts: false, and the merge gate ignores it.

Gaps, ranked by consequence for the team.

  1. Removing a member from the org leaves their team access intact. org members remove ttorg tt-bob, then bob pushed a branch to ttorg/widget, approved !2, and mr show reports his approval as counting. RemoveOrgMember (internal/store/orgs.go:147) deletes the org_members row and nothing in team_members; org team show lists him after removal, and re-adding him to the org needed no team add. org team add requires membership, so the invariant is assumed and not kept. Offboarding is the one collaboration action that has to be right. No issue; this one is a bug.

  2. Branch protection does not require a merge request. protect is no force-push and no delete (internal/policy/access.go:101). Bob, with write through a team, pushed a commit straight to the protected main while require_approvals 1, require_codeowners and require_resolved were on. The gates apply to mr merge only. GitHub, Forgejo and GitLab all have a "changes reach this branch through a merge request" mode, and for a team it is the point of protection. No issue. A flag on protect, or a separate setting.

  3. A rebase stales every approval. Bob pushed to main while !1 was under review, so a fast-forward was refused. Dave rebased; the diff was unchanged; every approval went stale and the merge needed bob, alice and carol to approve again, codeowners included. On an ff-only repository (this one) any merge request that waits behind another pays this on every catch-up. CI already keys on the tree (#177); reviews key on the head sha. Seen in the same run: dave's own approval, stale before the rebase, read as fresh after it (counts: false, so harmless, but wrong). No issue.

  4. The gates are invisible until the merge fails. Alice's merge was refused three times, each naming one thing: approvals, then codeowners, then fast-forward. mr show lists reviews and the web page says "Reviews: no reviews yet, Checks: none reported" and nothing about what the repository requires or which owners are outstanding. Carol approved while she had read only; the command accepted it and the refusal said her approval was "missing" rather than that it could not count. No issue. A gates block on mr show and the page: required N, owners outstanding per file, threads open, ff possible.

  5. No view of effective access. repo access list ttorg/widget is empty: bob's write and carol's read come through teams and are listed only on org team show, one team at a time. Nothing answers "who can push here". Related, the answers to an outsider disagree: org show and org members list return the member list to anyone, org team list and org team show answer "no organization". No issue.

  6. Tags are not protectable. Bob deleted v0.0.1 with git push origin :v0.0.1; the release stayed, anchored to a tag that no longer exists, and the releases page serves it. Protection covers refs/heads/ only. Forgejo has tag protection. No issue.

  7. Mentions do not notify. @tt-bob in an issue body produced no inbox row for bob. internal/autolink renders the link and nothing subscribes the person (#6 was display). mr review request covers merge requests; issues have no equivalent short of assigning. No issue.

  8. Everything is per repository. Labels, milestones, secrets, CODEOWNERS, protection and watch state are per repository; closes #N acts in the same repository only (internal/control/commitrefs.go:15). An org with several repositories recreates its labels in each and cannot close ttorg/widget#1 from a commit to ttorg/lib. Org-level labels and cross-repo closes are what Forgejo migrants will ask for first. Decide, and record it either way.

  9. Stock OpenSSH needs a quoting note. ssh git@gitbay.org repo create ttorg/widget --description "a widget" fails with unexpected argument "widget": ssh joins its arguments with spaces and the server tokenizes the result, so a multi-word value needs a second layer of quotes. --help over stock ssh is unknown flag; the form is help <noun>. The Users page says stock ssh "behaves identically"; it should say this. Docs.

  10. No CI for the org's repositories on gitbay.org. Same as the individual walk: #184.

What this says. The review loop itself holds up under four people, and the refusals are correct. The gaps are around it: membership that does not cascade (1), protection that reviews cannot rely on (2), and a re-review cost on every rebase (3) that this repository's own ff-only policy makes routine. 1 is a bug fix, 2 and 3 are one setting and one change of key each, 4 and 5 are reads. The fixtures can go with repo delete on the three repositories, org delete ttorg --yes, and admin user delete on the four accounts.

cmc 2026-09-07 21:59 UTC

Issues for the gaps above, so each item points at a number.

CLI-native individual

item issue
1. CI on gitbay.org #184
2. default branch hard-coded to main #189
3. repository rename #190
4. history import from Codeberg and sourcehut #191
5. Atom and RSS feeds #192
6. issue attachments (decide) #193
7. inbox without mail #194
8. pastes (decide) #195
Users wiki stale companion-wiki paragraph #204

Small FOSS org

item issue
1. org member removal leaves team access #196
2. protection cannot require a merge request #197
3. a rebase stales every approval #198
4. gates invisible until the merge fails #199
5. effective access view, outsider answers #200
6. tag protection, release with a deleted tag #201
7. mentions do not notify #202
8. org-level labels and cross-repo closes (decide) #203
9. stock-ssh quoting note #204
10. CI on gitbay.org #184

Not opened: the release-scoped key or artifact step for publishing from CI, which waits on #184 deciding that CI is a feature.

cmc 2026-09-07 22:57 UTC

Merge requests for the gaps, one stack, merge from the bottom with --strategy ff:

MR issue change
!335 #196 removing an org member drops their team memberships
!336 #204 Users wiki: stock-ssh quoting, help <noun>, .gitbay/wiki/
!337 #189 repo settings default-branch; an unborn HEAD follows the first push
!338 #190 repo rename
!339 #197 repo settings require-mr: protected branches merge-only
!340 #201 repo settings protect-tag; a release keeps its tag
!341 #200 repo access list reports effective access; one rule for outsiders
!342 #198 a rebase that keeps the diff keeps the approvals
!343 #199 merge gates on mr show, the page, and one refusal naming all of them
!344 #202 @mentions file an inbox row and join the thread
!345 #194 notifications settings mail on|off (the default watch state is still open)
!346 #192 Atom feeds: releases, log, owner activity

Not touched, since each is a decision or a live run: #184 (B), #191, #193, #195, #203.

cmc 2026-09-12 03:13 UTC

Every row this walk produced is closed: #189–#204, #184 (option B, runners attached to repositories), #191, #193, #195 (snippets, v1.20.0), #203. Closing the index.