Skip to content

feat(tools/sourcehut, tools/vcs): add SourceHut forge bridge and implement Mercurial backend#670

Open
onlyarnav wants to merge 11 commits into
apache:mainfrom
onlyarnav:sourcehut-forge-bridge
Open

feat(tools/sourcehut, tools/vcs): add SourceHut forge bridge and implement Mercurial backend#670
onlyarnav wants to merge 11 commits into
apache:mainfrom
onlyarnav:sourcehut-forge-bridge

Conversation

@onlyarnav

Copy link
Copy Markdown

Summary

  • Adds tools/sourcehut as a forge bridge: Implements ticket tracking (todo.sr.ht), mailing list patchset review (lists.sr.ht), CI build status (builds.sr.ht), and repository reads (git.sr.ht / hg.sr.ht) using SourceHut's GraphQL APIs.
  • Implements MercurialBackend in tools/vcs: Replaces the Mercurial (hg) stub with a complete, fully functional command-line wrapper backend supporting bookmarks, staging, commits, diffs, logs, and worktree purging.
  • Updates registries and docs: Registers tools/sourcehut capabilities (contract:tracker + contract:source-control + contract:mail-archive) and marks Mercurial support as complete.

Type of change

  • Skill change (.claude/skills/<name>/) — eval fixtures updated below
  • Tool / bridge contract (tools/<system>/*.md)
  • Python package (tools/*/ with pyproject.toml)
  • Groovy reference impl
  • Cross-cutting (RFC, AGENTS.md, sandbox, privacy-LLM)
  • Documentation (docs/, README.md, CONTRIBUTING.md)
  • Project template (projects/_template/)
  • CI / dev loop (prek, workflows, validators)
  • Other:

Test plan

  • prek run --all-files passes (verified with local pre-flight checks, skipping link checker hook lychee which requires MSVC toolchain to build from source on Windows).
  • For Python packages touched: uv run pytest / ruff check / mypy passes
    • Ran pytest on tools/vcs (19 passed, 3 skipped because hg is not installed on the local system).
    • Ran pytest on tools/sourcehut (13 passed offline using mocked HTTP calls).
    • Ran skill-and-tool-validate directly; all validations passed cleanly with 0 hard schema errors.

RFC-AI-0004 compliance

  • HITL — any new mutation is gated on explicit user confirmation (e.g. CLI operations for creating tickets/comments).
  • Sandbox — no new unrestricted host access; network reach declared in the adapter.
  • Vendor neutrality — placeholders (<PROJECT>, <tracker>, <upstream>, <security-list>) used in all skill / tool prose (verified via validator).
  • Conversational + correctable — agentic-override path documented if behaviour is adopter-tunable
  • Write-access discipline — no autonomous outbound messages; drafts only, sent on confirmation
  • Privacy LLM — private content does not reach a non-approved LLM; redactor invoked where needed

Linked issues

Refs #601 (Mercurial support), Refs #607 (SourceHut integration).

Notes for reviewers (optional)

  • This project was built pair-programming with the user using the Antigravity Gemini 3.5 Flash AI coding assistant.
  • Mercurial support uses the standard hg CLI. The tests gracefully skip if hg is not installed locally.
  • SourceHut integration uses standard library urllib to keep the CLI wrapper dependency-free.
  • Patchsets on lists.sr.ht are mapped into standard pull requests by treating the cover letter email as the description, individual patches as commits, and mailing list replies as review comments.

Comment thread tools/sourcehut/src/magpie_sourcehut/cli.py Fixed
Comment thread tools/sourcehut/src/magpie_sourcehut/client.py Fixed
Comment thread tools/sourcehut/src/magpie_sourcehut/lists.py Fixed
Comment thread tools/sourcehut/tests/test_sourcehut.py Fixed
@onlyarnav onlyarnav force-pushed the sourcehut-forge-bridge branch 2 times, most recently from 59e01f8 to 7b3880b Compare July 1, 2026 16:54

@onlyarnav onlyarnav left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

All the changes suggested by GitHub Actions bot have been implemented, now working on CI pipeline failing

@onlyarnav onlyarnav force-pushed the sourcehut-forge-bridge branch from 7b3880b to bdb110f Compare July 1, 2026 17:05
@onlyarnav onlyarnav changed the title Sourcehut forge bridge feat(tools/sourcehut, tools/vcs): add SourceHut forge bridge and implement Mercurial backend Jul 1, 2026
@justinmclean

Copy link
Copy Markdown
Member

Thanks for the contribution. I ran the PR review skill over it, and there are a couple of minor things you might want to fix:

A scattering of minor/nit observations (one markdown-table break in the docs, two hg-backend contract-parity concerns, two sourcehut nits), none of which gate the merge.

Approach looks solid on both fronts — the SourceHut bridge is clean stdlib-only code with real tests, and the Mercurial backend fills in a genuine extension point with matching @hg_required tests. A few observations below, none blocking; the one I'd most like a second look at is a markdown table that the docs hunk appears to break.
docs/vendor-neutrality.md — Status at a glance table split by an inserted blank line (docs/vendor-neutrality.md:84)

A blank line terminates a GFM table, so the "Project governance" row renders as loose text below a truncated table. The lychee and doctoc hooks won't flag this (not a link or TOC entry), so it needs a manual fix — remove the inserted blank line. While you're in this file, the earlier hunk rewriting the "non-Git/non-Hg systems" list also leaves a stray blank line before [Jujutsu] that splits the sentence; worth tidying in the same pass.
Smaller observations

tools/vcs/src/magpie_vcs/init.py:1372 — create_branch maps to hg bookmark , but the abstract contract is "Create and switch to a new line of work" and the Git binding switches (git checkout -b). hg bookmark creates/activates a bookmark without moving the working directory the way a branch checkout does, and the new test commits on the same parent so it doesn't exercise the "switched to a different line of work" semantics. Not a definite bug — flagging for a maintainer with hg expertise.

tools/vcs/src/magpie_vcs/init.py:1396 — reset_worktree uses hg purge --all, which removes .hgignore'd files too, whereas the Git binding's git clean -fd deletes only untracked files. If the per-run reset is meant to preserve ignored build/cache artefacts, drop --all. There's also no @hg_required test for reset_worktree (the git backend has one), so this difference is unverified.

tools/sourcehut/src/magpie_sourcehut/cli.py:496 — ticket label with neither --add nor --remove is a silent no-op that still prints the ticket and exits 0. Consider requiring at least one (argparse required-mutually-exclusive group), or a "nothing to do" note.

tools/sourcehut/README.md:141 — Features says the VCS integration "Reads repo metadata and refs", but repo.py's query pulls only id/name/description/visibility/updated — no refs. Tighten the wording or add refs to the query.

This review was drafted by an AI-assisted tool. The findings below are observations, not blockers; an Apache Magpie maintainer — a real person — will take the next look at the PR. If you think a finding is mis-applied, please reply on the PR and a maintainer will weigh in.

@onlyarnav onlyarnav requested a review from potiuk July 2, 2026 06:43

@potiuk potiuk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this — a well-structured, dependency-free bridge and a clean Mercurial backend, and nice quick turnaround on the earlier fixes. The top item below is CI-blocking after your merge with main; the rest are mostly medium/low. Two doc items land on lines this PR didn't change, so I couldn't anchor them inline:

  • tools/vcs/README.md:92 still lists hg as “extension point | operations raise a clear error → #601”, contradicting the working backend this PR adds (the module docstring at tools/vcs/src/magpie_vcs/__init__.py also still calls Mercurial an extension point).
  • docs/vendor-neutrality.md:208 (Tool adapters table) still lists Mercurial only as an extension point, while L163/L396/L488 in the same file now say “Mercurial (complete)”.

Comment thread tools/sourcehut/README.md

# SourceHut (sr.ht) Forge Bridge

**Capability:** contract:tracker + contract:source-control + contract:mail-archive

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Heads-up: after the merge with main this now breaks CI. tools/sourcehut declares contract:tracker + contract:source-control + contract:mail-archive, but main recently gained tools/vendor-neutrality-score (#672), which requires every contract tool's README to declare **Kind:** and **Vendor:**. Without them its loader raises ValueError and the vendor-neutrality-score pytest job fails. Please add, e.g.:

**Kind:** implementation

**Vendor:** SourceHut

(Bonus: that also makes SourceHut count as an additional backend for tracker / source-control / mail-archive in the neutrality score.)

if err_errors:
err_msgs = [e.get("message", "Unknown error") for e in err_errors]
raise SourceHutError(f"HTTP {exc.code}: {'; '.join(err_msgs)}")
except Exception:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The inner except Exception: pass catches the raise SourceHutError(...) two lines above, so the parsed GraphQL error messages are always discarded and callers fall through to the generic HTTP request … failed with status N (L85). A 400 with {"errors":[{"message":"invalid tracker"}]} surfaces only the status, never the message. Suggest building the message string inside the try and raising after it, or narrowing the inner except to (json.JSONDecodeError, KeyError, TypeError).

if ns.subcommand == "graphql":
vars_dict = None
if ns.variables:
vars_dict = json.loads(ns.variables)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

main() has no error handling: json.loads(ns.variables) raises JSONDecodeError on malformed --variables, and any SourceHutError from the calls below escapes as a raw traceback rather than a clean message. Consider wrapping the dispatch in try/except (SourceHutError, json.JSONDecodeError) as e:print(f"magpie-sourcehut: {e}", file=sys.stderr); return 2.

if base:
args.extend(["-r", base])
if paths:
args.extend(paths)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

diff (here) and log (L414) append paths without a -- terminator, unlike GitBackend (L232/L253) and this class's own stage (L426). A path beginning with -, or named like an hg flag, is parsed as an option → wrong output or an hg error. Suggest args.extend(["--", *paths]) in both.

emails = [edge.get("node") for edge in edges if edge.get("node")]

# Sort emails by date if possible
with contextlib.suppress(Exception):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

contextlib.suppress(Exception) swallows any sort failure, and then emails[0] is used as the cover letter — a malformed node would silently yield the wrong description/author. Narrow to (TypeError, AttributeError), or validate the nodes before sorting.

mlist = res.get("list") or {}
patches_conn = mlist.get("patches") or {}
edges = patches_conn.get("edges") or []
return [edge.get("node") for edge in edges if edge.get("node")]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

edge.get("node") assumes every edge is a dict; a null edge in the connection raises AttributeError. Same pattern at L122 and the patches loop at L147. Cheap to guard (if edge / if patch is not None).

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.

4 participants