Skip to content

Gitea/Forgejo bridge (importer) - #1628

Open
mcepl wants to merge 44 commits into
git-bug:trunkfrom
openSUSE-Python:gitea_bridge
Open

mcepl wants to merge 44 commits into
git-bug:trunkfrom
openSUSE-Python:gitea_bridge

Conversation

@mcepl

@mcepl mcepl commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Part of stack: #1629 → #1632 → #1628

Depends on #1632 (tokens) and #1629 (pnpm-overrides). Please review and merge earlier PRs in the stack first.

This PR is meant as continuation of #1565 (rebase on the current trunk and also further fixes).

This adds a Gitea/Forgejo bridge to git bug bridge. The importer brings in issues with their comments, comment edits, labels, title changes and open/closed status. There is no exporter yet.

It supersedes #1077 and builds on #1565. The history is the work of several people, rebased onto current trunk:

  • Tian Yuanhao wrote the initial bridge (2021).
  • Jacob Gamble fixed crashes on missing users (2023).
  • Matthew Cengia updated it for the git-bug refactor and module path change, and added the first config tests (2023–2024, WIP: Gitea/Forgejo bridge #1077).
  • jyn rewrote the iterators around the timeline API, added a fake Gitea API server, a large test suite and property-based round-trip tests (2026, WIP: Gitea/Forgejo bridge #1565).
  • I rebased it, made it build without a local SDK checkout and fixed the import bugs listed below.

I'm opening this as a draft to get CI results on all platforms and early feedback on the approach.

What works

  • Configuration. git bug bridge new --target=gitea accepts a web URL, a clone URL or an SSH remote, and validates the token and repository.
  • Issues. They are imported with author, creation time, title and description. Pull requests are excluded.
  • Timeline events. Comments, comment edits, label changes, title changes (change_title) and close/reopen events are replayed in order, attributed to whoever made them.
  • Re-imports. Running the import again doesn't duplicate anything. Each imported operation stores the timeline event it came from, and events that are already present are skipped. since is sent on both the issue listing and the timeline requests, so later runs fetch only issues and events newer than the last import.
  • Labels. After the timeline is replayed, the bug's labels are brought in line with the issue's current labels on the server, which picks up labels renamed upstream. Only labels that came from Gitea are removed, so labels added locally in git-bug are kept. Names compare case-insensitively.
  • Deleted users. They are imported as a shared "Ghost" identity instead of aborting the import.
  • Empty titles. A title that is empty after cleanup gets the <empty string> placeholder the GitHub bridge uses.

What doesn't work yet

  • No exporter. NewExporter returns nil, so git bug bridge push to Gitea reports that export isn't supported.
  • Unimported data. Assignees, milestones, attachments and references aren't imported.
  • Unknown causes of label drift. When the labels are brought in line with the server, the change is credited to the issue author, because Gitea doesn't record who renamed a label.
  • Leftover stub. updateIssue is an empty function; close and reopen events now do what it was meant for.

Changes on top of #1565

Build

  • Removed the committed go.work and go.work.sum from the whole history. They pointed at ../go-sdk, a local checkout of the SDK, so the build failed for anyone without it, CI included.
  • Depend on the published gitea.dev/sdk v1.2.0. The code needs the SDK fix that changed TimelineComment.Label from []*Label to *Label, which v1.1.0 doesn't have. Also require pgregory.net/rapid v1.3.0 for the property tests.
  • Regenerated the man and markdown pages, which still listed the target as gitea-preview and would fail CI's generated-files check.

URL parsing

splitURL panicked on URLs with fewer than two path segments (https://host/owner). It also forced https, which broke http://localhost:3000, and dropped the path of instances installed under a subpath. It now:

  • returns an error for short paths and unsupported schemes;
  • keeps http and https as given;
  • maps git://, ssh:// (including a port) and git@host:owner/repo remotes to https on the same host;
  • keeps a subpath (https://example.com/gitea/);
  • finds the repository in page URLs such as /owner/repo/issues/12.

Importer

  • Comments were corrupting data. Comment IDs weren't copied from the API, and the duplicate check always matched the bug's last operation. The first comment on an issue replaced the issue description, and the second failed with "comment not found". Both are now imported as separate comments.
  • Close and reopen hit panic("todo"). They are now imported as status changes.
  • Renames from a real server were ignored. The code looked for a rename event, but Gitea and Forgejo send change_title with the new title in new_title. The test fixtures used the same wrong name and are corrected.
  • Label events now carry their ID and timestamp, and an event for a label that was deleted upstream no longer dereferences nil.
  • Errors raised after a bug is created now carry its ID.
  • The fake API server includes each issue's current labels in issue listings, as real Gitea does, so label reconciliation is tested.
  • gofmt. The Gitea files are now formatted; six of them weren't.

Documentation

  • Updated the Gitea column of the importer tables in doc/feature-matrix.md. It still described the first draft, which imported only titles and bodies. Comments, comment edits, labels, status, title edits, identities, incremental import with resume and the test suite are now marked as supported. The exporter tables stay ❌.

TestImportTitleUnsafeAfterCleanup expected a title of "\x1b" to fail the import, while two other tests expected an empty-after-cleanup title to get a placeholder. I kept the placeholder and changed that test.

Testing

  • bridge/gitea has 91 test functions: unit tests against the fake API server, iterator tests and property-based round-trip tests. I added tests for two comments on one issue and for a label event whose label was deleted.
  • All tests pass locally on Linux with Go 1.26, as do gofmt, go vet ./... and the generated-files check.
  • Not run: the race detector (my local Go build lacks its runtime; CI doesn't use -race either), and Windows and macOS, which is part of why this is a draft.
  • Nothing has been tested against a live Gitea or Forgejo instance. The tests that call gitea.com are skipped in CI and need tokens in GITEA_TOKEN_PRIVATE / GITEA_TOKEN_PUBLIC to run.

Skipped tests:

  • Exporter round trips. The round-trip tests for export are skipped until an exporter exists.
  • Attachments. The timeline API doesn't expose them.
  • Stale skip reasons. Two tests are skipped with a message saying gitea.dev/[email protected] lacks the needed fields. The branch now uses v1.2.0, so they can probably be enabled.

Follow-ups

  • Enable the two SDK-gated tests mentioned above.
  • Test against a real Gitea and a real Forgejo instance.
  • Write the exporter.

RetriggerConfidence Score: 5/5

The changes since the previous review appear safe to merge, with no outstanding previous threads or accepted new findings.

Summary

The PR adds a Gitea/Forgejo bridge and updates shared credential, configuration, and push behavior. Since the previous review, the only change replaces the configuration-writing method in two dangling-identity tests. No new actionable issue was established.

Reviews (16) · Last reviewed commit: "bridge/core: update config tests for git..." · Reviewed by Greptile

@mcepl mcepl changed the title Gitea bridge WIP: Gitea/Forgejo bridge (importer) Sep 24, 2026
@mcepl
mcepl marked this pull request as draft September 24, 2026 11:33
@mcepl mcepl mentioned this pull request Sep 24, 2026
@mcepl
mcepl force-pushed the gitea_bridge branch 4 times, most recently from ccc88d2 to c3b1d49 Compare September 25, 2026 17:16
@mcepl
mcepl force-pushed the gitea_bridge branch 3 times, most recently from 54b05df to ce833ae Compare September 25, 2026 20:34
@mcepl mcepl mentioned this pull request Sep 26, 2026
@mcepl
mcepl force-pushed the gitea_bridge branch 4 times, most recently from cd322a5 to a9adb24 Compare October 3, 2026 08:28
mcepl added 5 commits October 4, 2026 22:26
When git-bug.identity in the local git config points to an identity
that doesn't exist in the repository (removed, never pulled, or the
config was copied from another repository), several code paths failed
with the cryptic "identity doesn't exist" error:

- core.FinishConfig, used by every bridge's "bridge new", only treated
  identity.ErrNoIdentitySet as "no user configured" and aborted on any
  other error. Bridge configuration could therefore never complete,
  while the credential had already been stored, so every retry left
  another orphaned token in the keyring.

- LoadRepoEnsureUser / LoadBackendEnsureUser returned the raw
  ErrNotFound, giving the user no hint about what is missing or how to
  fix it (identity.GetUserIdentity silently unsets the dangling config
  at that point too).

FinishConfig now checks the configured user identity first. When a
dangling identity is detected, it prints a notice and repairs it by
adopting an existing identity tagged with the bridge login if one exists,
or by creating a new tagged identity and setting it as current. The
ensure-user helpers are factored into ensureUser(), which turns the
not-found case into an actionable message pointing to
"git bug user adopt" / "git bug user new".

Regression tests cover FinishConfig with a dangling identity, both
when a matching identity exists and when a new one must be created.
auth.List returned credentials in whatever order the keyring backend
listed its keys, which is arbitrary. Callers in all bridges (github,
gitlab, jira importers and exporters) simply use creds[0], so when
several matching credentials exist, e.g. after a token was regenerated
or a bridge configuration was retried, the bridge could keep picking an
old, revoked token. The newly added valid one was silently ignored and
every pull failed with an authentication error from the remote.

Sort the result by creation time, most recent first, with the ID as a
tie breaker so that the order is fully deterministic. Stored timestamps
preserve full RFC3339Nano resolution so credentials added in the same
second still sort deterministically newest-first, while remaining backward
compatible with legacy integer timestamps. Adding a fresh credential now
supersedes older ones for every caller without having to touch each bridge.
Bridges for self-hosted trackers (gitlab, jira) only select credentials
carrying a "base-url" metadata matching their configuration, but
"bridge auth add-token" had no way to set it. Tokens added through it
were therefore never used by those bridges, without any warning.

Add a -b/--base-url flag, consistent with "bridge new", which stores
the value as base-url metadata on the token.

Also report a clear error when --user doesn't match any identity.
"-u" means --user here but --url in "bridge new", which makes it easy
to pass a URL by mistake; previously that produced only the cryptic
"identity doesn't exist".

The man page and markdown documentation are regenerated.
…lients

In githubExporter.cacheAllClient and gitlabExporter.cacheAllClient, the
loop over credentials built each client from creds[0] instead of the
iterated cred. If tokens existed for multiple accounts, every identity
client was configured with whichever token appeared first in the slice.
Because auth.List orders credentials newest-first, adding a token for a
secondary account caused exports to run with that account's token, even
for the configured default login.

Build each identity client and assign the default token from the
matching iterated token instead of creds[0].
… matching

When bridges for trackers like GitLab and Jira query credentials by base
URL, auth.List uses exact string matching on metadata. Differences such
as a trailing slash (for example, the default "https://gitlab.com/"
versus user input "https://gitlab.com") caused credential lookups to fail
silently, making valid tokens unusable by the bridge.

Introduce auth.NormalizeBaseURL to canonicalize URLs by lowercasing scheme
and host and removing trailing slashes. Apply this normalization when
storing base-url metadata in "bridge auth add-token" as well as in GitLab
and Jira bridge configuration. In auth.List option matching, compare
base-url metadata values after normalization so queries match regardless
of trailing slashes in either the stored credential or the query filter.
@mcepl

mcepl commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor Author

This is unmanageably messy, I will reorganise it few reviewable commits. This is a mess.

Unconsolidated version is in https://codefloe.com/mcepl/git-bug/src/branch/gitea_bridge_before_consolidation

mcepl added 2 commits October 5, 2026 12:15
Bridges skip bugs imported from another tracker on push, so a bug cannot
be kept in more than one tracker. Add ExportOptions with a Foreign flag and
Bridge.ExportAllWithOptions to request that. Exporters opt in through the
ForeignExporter interface; for any other bridge the request fails instead
of being ignored, and it is enabled before Init so the exporter can verify
the remote tracker first.

The interface documents the contract for implementations: refuse remote
trackers that hold issues not created by mirroring, store remote ids under
repository-scoped keys from MirrorMetaKey so they never clash with the
metadata of the bridge the bug came from, and credit original authors with
MirrorAttribution, because the remote shows everything as written by the
token owner.
Expose ExportOptions.Foreign on the command line, describe its rules in the
command help and the bridge guide, and regenerate the command reference.
No bridge on trunk implements mirroring yet, so the option currently fails
with an explanatory error.
Comment thread bridge/gitea/export.go
Comment thread bridge/gitea/config.go
Comment thread bridge/gitea/config.go Outdated
Comment thread bridge/gitea/export.go Outdated
Comment thread bridge/gitea/import.go Outdated
Comment thread bridge/gitea/export.go
Comment thread bridge/gitea/config.go Outdated
Comment thread bridge/gitea/iterator/iterator.go Outdated
Comment thread bridge/gitea/import_test.go
Comment thread bridge/gitea/import.go
@mcepl mcepl changed the title WIP: Gitea/Forgejo bridge (importer) Gitea/Forgejo bridge (importer) Oct 5, 2026
Comment thread bridge/gitea/export.go Outdated
Comment thread bridge/gitlab/config.go Outdated
Comment thread bridge/gitea/import.go Outdated
Comment thread bridge/gitea/export.go
Comment thread commands/bridge/bridge_push.go Outdated
Comment thread commands/bridge/bridge_push.go Outdated
Comment thread bridge/gitea/import.go
Comment thread bridge/jira/config.go Outdated
@greptile-apps

This comment has been minimized.

@greptile-apps

greptile-apps Bot commented Oct 5, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

Comment thread bridge/jira/import.go Outdated
Comment thread bridge/gitea/export.go
Comment thread bridge/jira/config.go Outdated
Comment thread bridge/jira/jira.go
Comment thread commands/bridge/bridge_pull.go Outdated
Comment thread bridge/jira/jira.go Outdated
Comment thread bridge/jira/config.go Outdated
Comment thread bridge/jira/config.go
Comment thread bridge/jira/config.go Outdated
Comment thread bridge/jira/config.go Outdated

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant