Skip to content

bridge: add todo.sr.ht (SourceHut) bridge - #1499

Open
mcepl wants to merge 10 commits into
git-bug:trunkfrom
openSUSE-Python:todosrht
Open

mcepl wants to merge 10 commits into
git-bug:trunkfrom
openSUSE-Python:todosrht

Conversation

@mcepl

@mcepl mcepl commented Nov 29, 2025 •

Copy link
Copy Markdown
Contributor

Adds a bridge for SourceHut's issue tracker, todo.sr.ht, using its GraphQL API. The first version was written with heavy AI assistance. I have since reviewed it, fixed what review and live testing found, and tested it against a real tracker. Reviews from people who know Go and the git-bug internals better than I do are very welcome.

Fixes: #1024

What it does

  • git bug bridge new --target=todosrht --url=https://todo.sr.ht/~owner/tracker --login=USER configures the bridge. It verifies the token by looking up the tracker.
  • Pull imports tickets with their comments, status changes and label changes. Assignment and mention events become warnings, because git-bug has no equivalent.
  • Push creates tickets and exports comments, status changes, title and body edits, and label changes. Missing labels are created on the tracker. Edits to comments other than the ticket body are skipped, because the API cannot edit them.
  • Both directions are idempotent. Operation metadata records SourceHut IDs, so repeated syncs do not duplicate events. Pulling a ticket that was first pushed from git-bug reuses the existing local bug.

Commits

  1. test(repository): makes the mock repository storage safe for concurrent access.
  2. bridge/todosrht: vendors the todo.sr.ht GraphQL schema (schema.graphqls). Tests validate every query and mutation against it.
  3. bridge/todosrht: adds the GraphQL client.
  4. bridge/todosrht: adds configuration, import, export and bridge registration.
  5. test(bridge/todosrht): adds mock-based tests, schema validation, and HTTP tests for request paths and redirects.
  6. docs: adds the usage guide, command help, man pages and the design notes.

Security

  • Base URLs must use HTTPS. Plain HTTP is allowed only for loopback hosts, for local testing.
  • The client refuses cross-origin redirects, so the bearer token is never re-sent to another host.
  • GIT_BUG_DEBUG=1 dumps requests and responses with the token redacted, and truncates response bodies.
  • Response size is capped at 32 MiB.

Testing

  • go test ./... passes.
  • I tested it live against https://todo.sr.ht/~mcepl/git-bug-bridge-testing. That covered configuration, import of a ticket created on SourceHut, and export of a new bug, a comment, adding and removing a label, and closing the ticket. I also imported a comment added with hut. Repeated pulls and pushes made no further changes, and hut confirmed the ticket state.

Known limitations

  • The live test did not cover title or body edits, reopening, or a tracker owned by someone other than the token holder.
  • A SourceHut personal access token cannot be restricted to one tracker. The documented grant (todo.sr.ht/PROFILE:RO todo.sr.ht/EVENTS:RO todo.sr.ht/TRACKERS:RW todo.sr.ht/TICKETS:RW) reaches every tracker the account can access.
  • Push ignores since and scans all local bugs. Metadata prevents duplicate exports, but push time grows with the number of bugs.
  • Queries for an owner-qualified tracker (~owner/name) are built by rewriting the me { ... } query templates. Schema tests cover the result, but separate templates would be clearer.
  • TrackerExists now only wraps GetTracker, and client_test.go tests only the mock client. Both could be removed.

RetriggerConfidence Score: 0/5 Tier: apex

The PR is not safe to merge while label exports fail and credential selection can send a token to the wrong SourceHut instance.

Findings

  1. P1 Label exports fail validation ▶
  2. P1 Security Tokens cross instance boundaries ▶
  3. P1 Identical comments collide ▶
  4. P1 Pull reverts local edits ▶
  5. P1 Label removals get skipped ▶
  6. P2 Ticket edits misattribute authors ▶
Summary

This PR adds a SourceHut todo.sr.ht bridge with GraphQL configuration, ticket and event import, operation export, tests, and usage documentation. It also adds concurrent-safe mock storage.

  • Pull maps tickets and events to git-bug operations and reconciles ticket edits.
  • Push creates tickets and exports comments, edits, status changes, and labels.
  • The new label lookup, credential selection, and import reconciliation paths need correction before merging.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  S[SourceHut tracker] -->|tickets and events| I[Importer]
  I --> G[git-bug operations]
  G --> E[Exporter]
  E -->|GraphQL mutations| S
  K[Stored credentials] --> I
  K --> E
Loading

Reviews (2) · Last reviewed commit: "test(bridge/todosrht): commit operations..."

@mcepl
mcepl marked this pull request as draft November 29, 2025 23:28
@mcepl
mcepl force-pushed the todosrht branch 9 times, most recently from 8c0d156 to e76ef40 Compare December 5, 2025 00:07
@sudoforge sudoforge mentioned this pull request Dec 6, 2025
@mcepl
mcepl force-pushed the todosrht branch 2 times, most recently from 032a598 to 45b5005 Compare January 28, 2026 00:15
@jimdigriz

Copy link
Copy Markdown

I seem to be unable to get this code to work.

git bug bridge push default reports:

exported 0 issues with default bridge

git bug bridge pull default reports:

import error on entity 60a687b649533bf0e0511d8f3573a062370fe2cf277f02425add5e4dbee4323f: failed to get events for ticket 1: failed to fetch events: GraphQL request failed with status 422: 422 Unprocessable Entity
imported 0 issues and 0 identities with default bridge

@mcepl
mcepl force-pushed the todosrht branch 2 times, most recently from 8a1bebe to 0d216d6 Compare September 13, 2026 20:53
@mcepl

mcepl commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Still broken? I have rebased this branch on the top of trunk.

Otherwise, I really don’t want to work on this PR any more. Given the sharp anti-AI stance of SourceHut, I am moving elsewhere. Anybody, feel free, to take this PR over.

@mcepl
mcepl force-pushed the todosrht branch 2 times, most recently from d146faa to beae5b8 Compare September 24, 2026 09:29
mcepl added 5 commits October 4, 2026 22:38
Add a Billy-backed test storage implementation and protect mock repository state against concurrent access. Keep test infrastructure separate from the new bridge so reviewers can assess the storage change independently.
Record the API schema beside the bridge so client queries, mutations, and response types can be reviewed against the service contract without relying on an external copy.
Add authenticated HTTP transport, GraphQL request and response types, pagination queries, and ticket, event, and label mutations. Keep the protocol client separate from bridge synchronization and configuration.
Register the bridge, resolve tracker credentials, import remote tickets and events, and export local operations. Combine the mutually dependent importer and exporter so the bridge can compile as one implementation commit.
Provide mock GraphQL methods and tests for the client, configuration, debug transport, importer, and exporter. Fold compilation repairs into the tests rather than leaving later fixes to earlier test commits.
@mcepl mcepl changed the title feat(bridge): add SourceHut todo bridge implementation bridge: add todo.sr.ht (SourceHut) bridge Oct 4, 2026
@mcepl
mcepl marked this pull request as ready for review October 4, 2026 21:52
Document tracker URL, login, token, and bridge usage in help and reference pages. Include the design plan with the user-facing guide.
Comment thread bridge/todosrht/export.go Outdated
Comment thread bridge/todosrht/import.go Outdated
Comment thread bridge/todosrht/import.go
Comment thread bridge/todosrht/client.go Outdated
Comment thread bridge/todosrht/client.go
Comment thread bridge/todosrht/config.go
Comment thread bridge/todosrht/import.go Outdated
Comment thread bridge/todosrht/import.go Outdated
Comment thread doc/usage/todosrht.md Outdated
Comment thread doc/design/todosrht-implementation-plan.md Outdated
mcepl added 4 commits October 5, 2026 01:23
… handling

Address feedback from code review on the todo.sr.ht bridge:

- Require verified tracker and base URL metadata before accepting
  creation metadata ticket IDs on export, preventing operations on
  unverified bugs from modifying tickets on the configured tracker.
- Track imported event operations with change-level IDs to prevent
  ErrMultipleMatch on repeat pulls of events containing multiple changes.
- Reconcile remote edits to ticket title and body during import when
  reusing existing bugs.
- Support explicit null body in UpdateTicketInput to allow clearing
  ticket descriptions on SourceHut without losing edits.
- Make label export idempotent by checking remote ticket labels before
  adding or removing them.
- Ensure credentials added without base URL metadata can be used to
  initialize the bridge, and tag selected credentials with base URL.
- Isolate user identities by instance base URL to avoid collisions
  across multiple SourceHut instances.
- Align ValidParams and error messages with the --project flag.
- Establish chronological order for imported events across pages.
- Silence repetitive warnings for unsupported assignment and mention events.
- Surface server GraphQL error details on non-200 HTTP responses and clean
  unused variables when querying the authenticated user.
- Document label color limitations and replace the obsolete roadmap
  in the design documentation.
Comment thread bridge/todosrht/export.go
}

// Fetch current remote ticket to reconcile labels idempotently
currentTicket, err := client.GetTicket(ctx, bugTodoSourceHutID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Label exports fail validation When exporting a label change, this lookup calls GetTicket, whose query requests ticket(id:) at the GraphQL root. The checked-in SourceHut schema exposes that field only under a tracker. The request therefore fails validation, so label changes cannot be pushed. The schema test does not include this query.

Comment thread bridge/todosrht/import.go

var matchingCreds []auth.Credential
for _, cred := range creds {
if base, ok := cred.GetMetadata(auth.MetaKeyBaseURL); !ok || base == conf[confKeyBaseUrl] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Tokens cross instance boundaries A token saved with bridge auth add-token has login metadata but no base URL, so this filter accepts it for any configured instance. If a user has such a token for instance A alongside a configured token for instance B with the same login, pull can select A’s token and send it to B on the first tracker lookup. The exporter accepts the same token in cacheAllClient. Configuring B verifies its selected token, not the other stored tokens. How this was verified: Login-only tokens pass both instance filters, and the client attaches the selected token to requests sent to the configured base URL.

Comment thread bridge/todosrht/import.go
if err := json.Unmarshal(*c.Author, &author); err != nil {
return "", err
}
return author.CanonicalName + "\x00" + c.Text, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Identical comments collide Revision matching uses only the author and comment text. If one comment is edited to “fixed” and the same author later posts a separate comment saying “fixed”, the revision map treats the new comment as another edit of the first. The separate comment disappears locally, while its event is marked imported, so later pulls cannot recover it.

Comment thread bridge/todosrht/import.go
Comment on lines +308 to +320
cleanSubject := text.CleanupOneLine(ticket.Subject)
metadata := map[string]string{metaKeyTodoSourceHutId: fmt.Sprintf("ticket:%d:%d", ticket.Id, editTime)}
if cleanSubject != "" && cleanSubject != snapshot.Title {
op, err := b.SetTitleRaw(author, editTime, cleanSubject, metadata)
if err != nil {
return nil, false, err
}
ji.out <- core.NewImportTitleEdition(b.Id(), op.Id())
}

cleanBody := text.Cleanup(ticket.Body)
if len(snapshot.Comments) > 0 && cleanBody != snapshot.Comments[0].Message {
commentID, _, err := b.EditCreateCommentRaw(author, editTime, cleanBody, metadata)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Pull reverts local edits Reconciliation treats every difference between the local bug and the fetched ticket as a remote edit. If a local title or body edit has been committed but not pushed, a full pull with --no-resume against the unchanged remote ticket creates an imported edit that restores the old remote value. This overwrites the local change before it can be pushed.

Comment thread bridge/todosrht/export.go
out <- core.NewExportWarning(fmt.Errorf("label '%s' not found on SourceHut, cannot remove", labelName), b.Id())
continue
}
if _, onTicket := currentLabelNames[labelName]; !onTicket {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Label removals get skipped The remote-label map uses exact names, but the label ID lookup ignores case. If a local removal names Bug and the remote label is named bug, the exporter finds its ID, then decides it is absent and skips the removal. It still marks the operation exported, leaving the label on the remote ticket with no retry. The same mismatch can also cause a redundant add.

Comment thread bridge/todosrht/import.go
cleanSubject := text.CleanupOneLine(ticket.Subject)
metadata := map[string]string{metaKeyTodoSourceHutId: fmt.Sprintf("ticket:%d:%d", ticket.Id, editTime)}
if cleanSubject != "" && cleanSubject != snapshot.Title {
op, err := b.SetTitleRaw(author, editTime, cleanSubject, metadata)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Ticket edits misattribute authors Imported title and body edits use the ticket’s original submitter as their author. When another SourceHut user makes an edit, the git-bug timeline credits the submitter instead, leaving readers with a misleading edit history.

@jimdigriz

Copy link
Copy Markdown

As a SourceHut user looking to move from GitHub, I see here:

When I flagged this code did not work I had already tried rebasing before you did so yourself and recommend I retry. That sort of hints you do not use this code day-to-day and had not tried your rebasing effort which makes me somewhat twitchy about using it.

What do you recommend SourceHut users do here? Besides I suspect that community is unlikely to receive well a majority LLM written bridge :)

For me, I am going to write my own bridge.

@mcepl

mcepl commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

As a SourceHut user looking to move from GitHub, I see here:
What do you recommend SourceHut users do here? Besides I suspect that community is unlikely to receive well a majority LLM written bridge :)

For me, I am going to write my own bridge.

Yes, I am leaving Sourcehut (partially because of their LLM policy, but mostly because my users just seem to irrationally hate email and I have much better feedback on web-based forge; and of course, there are other reasons as well), and yes I expect to really use daily Gitea/Forgejo so #1628 is much more relevant to me. Mostly, I wanted to make this as a kind of farewell letter to Sourcehut, and I hate to leave an unfinished business behind.

Concerning the quality of coding: I am not a native Golang programmer, so I cannot really evaluate how good the best models are writing it, but with Python (and C to some extent), which are languages are used every day for my work, it seems to me that the latest models (Opus 5.5 and ChatGPT 5+ ones) are on the same level or better than what I could write by hand, much much better than they were just a year ago when I started to write this PR.

What should Sourcehut users do? Well, I would hope they could take this branch, and take it further and maintain it. I believe that there is a lot of work and testing I did (completely manually, it is a way more than me blindly pressing Enter to pi), which is better that it is done rather than not. Writing it all over again just because you write it seems like an unnecessary exercise in time-wasting. But, of course, it is your time and you can do whatever you want to do with it, and it is mainly @MichaelMure ’s project and it is his decision what to do with git-bug.

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.

Sourcehut Support

2 participants