Skip to main content

Module moz_phab

Module moz_phab 

Source
Expand description

Mozilla’s Phabricator, via the moz-phab CLI. Not a general-purpose Phabricator source - see the module name. moz-phab is the tool every Mozilla contributor already uses to submit and apply patches; delegating the actual checkout to it, rather than reimplementing diff fetching and application ourselves, was a deliberate pivot after an earlier from-scratch implementation turned out to duplicate work moz-phab already does more robustly. Everything below the “Checkout” heading was verified empirically against the real, live Mozilla Phabricator and a real moz-phab install (not just read from its source), specifically because the earlier from-scratch approach had exactly this kind of thing wrong.

Auth: POST form-encoded to {url}/api/{method} with api.token=.... Nested params flatten to constraints[members][0]=... (see flatten_params). Token comes from, in order: config token, token_cmd, then ~/.arcrc (hosts["{url}/api/"].token, which moz-phab already writes, so it usually works with no setup). The same resolved token is passed to moz-phab itself via MOZPHAB_PHABRICATOR_API_TOKEN, so both halves always agree on identity rather than moz-phab silently falling back to its own (possibly different) ~/.arcrc lookup.

Queue: user.whoami -> project.search {constraints:{members:[me]}} (my groups, unless include_groups is off) -> differential.revision.search {queryKey:"active", attachments:{reviewers:true}}, paginating on cursor.after. Bucketed locally (mirroring Phabricator’s own DifferentialRevisionRequiredActionResultBucket, which Conduit doesn’t expose - this part is transcribed from myqonly’s real, working addon/services/phabricator-service.mjs):

  • skip if fields.authorPHID is me
  • skip if my (or my group’s) reviewer entry has status == "resigned"
  • require fields.status.value == "needs-review"
  • require a reviewer entry of mine (or a group I’m in) with status in blocking|rejected|rejected-older|added|commented (accepted is deliberately excluded - nothing left to do)

Author PHIDs are resolved to usernames with one batched user.search {constraints:{phids:[...]}} call over every distinct author in the page, rather than showing raw PHIDs in rq show/rq path.

Every per-revision Conduit lookup after the initial queue fetch is batched across all actionable revisions rather than issued once per revision - a queue of N revisions used to cost roughly N + N * stack_depth extra calls (one diffusion.repository.search per revision, plus a edge.search/differential.revision.search pair per stack hop per revision), which is easy to trip Phabricator’s rate limit on. Now:

  • Repos: one diffusion.repository.search {constraints:{phids:[...]}} over every distinct repositoryPHID among actionable revisions (repo_refs_for).
  • Stacks: resolve_stacks walks every actionable revision’s parent chain in lockstep, one edge.search + differential.revision.search pair per depth level covering every stack still in flight, instead of per revision. A 20-revision queue with 3-deep stacks now costs ~1 (repos) + 32 (stack levels) calls instead of ~20 + 20(1+2*3).
  • Diffstats: diff_stats_for resolves every actionable revision’s diffPHID through one batched differential.diff.search call, then fetches each resulting diff’s raw text via differential.getrawdiff - unlike the above, this one is one call per revision, since getrawdiff has no batch form; see diff_stats_for’s own doc comment for why (and for why it’s getrawdiff, not the more obvious-looking differential.querydiffs).

fetch_queue fetches each Review’s diffstat itself (via diff_stats_for, above) rather than rq show’s TUI fetching it lazily on expand, so the TUI never blocks on Conduit.

version is the comma-joined dateModified of every revision in the stack (walked via edge.search, same as before), not diff ids - moz-phab re-resolves the live diff/base itself on every invocation regardless of what we pass it, so this only needs to answer “has anything about this stack changed since we last synced,” and dateModified answers that with zero extra Conduit calls beyond the stack walk we’re already doing (resolve_stacks’ own differential.revision.search calls already return it).

§Checkout

checkout_spec returns Checkout::ExternalCommand { program: "moz-phab", args: ["patch", "D<id>", "--apply-to", "base", "--yes", "--name", <key>], env }. moz-phab patch handles everything the earlier implementation did by hand, and does it better:

  • Walks the full dependency stack itself (confirmed live: patching a revision automatically discovered and applied its parent too, unprompted).
  • Resolves the actual base commit itself, including a fallback our own fields.refs-based base lookup never had: if the recorded base isn’t a public/landed commit (e.g. it belongs to another unlanded stack, or needs a git-cinnabar hg<->git translation), it walks forward to the latest landed ancestor and applies there instead (“Base revision … is not public … Applying the patch at … instead” - observed live against mozilla-central).
  • --yes fully suppresses its interactive “patch the full stack?” prompt.
  • Exits 1 on failure (a real patch-doesn’t-apply case was observed live), leaving the workspace checked out at the resolved base with nothing applied - which is exactly the Status::ApplyFailed “leave it for inspection” contract the Vcs backends already have.

Requires repository.callsign in .arcconfig, not just phabricator.uri - confirmed live: moz-phab patch fails outright with “Failed to determine the Phabricator callsign for this repository” without it, even though static analysis of moz-phab’s own source suggested the callsign was only needed by submit. checkout_spec looks the callsign up itself (diffusion.repository.search) and writes both fields to .git/.arcconfig - never the repo root’s own .arcconfig, which might be absent, tracked, or (as seen live in a real non-central repo) present but missing the callsign. .git/.arcconfig is checked first and never touches tracked working-tree content; every canonical repo here is colocated with a real .git by construction (tool-managed clones always are; jj canonical repos always are too, by our own design choice), so this path needs no per-VCS fallback.

Known limitation, not yet handled: moz-phab defaults to the remote named origin and warns (“Multiple remotes found. Defaulting to ‘origin’.”) if a repo has more than one. Every tool-managed clone only ever has one remote, so this never bites the common path - but a discovered workdir checkout with multiple/nonstandard remotes (common for Mozilla developers, e.g. a central/try naming scheme instead of origin) may need git.remote set in their own ~/.moz-phab-config, which this tool doesn’t configure on their behalf.

Whether moz-phab itself is installed and runnable is never checked by this module - only checkout_spec’s ExternalCommand actually shells out to it, and testing this module shouldn’t require moz-phab to be on PATH.

fetch_status: differential.revision.search {constraints:{ids:[...]}}. status.value of published (landed) or abandoned is Lifecycle::Resolved; a revision id not found in the response at all is also treated as Resolved, so a review that’s vanished (e.g. lost access) doesn’t linger forever.

Structs§

MozPhabSource

Constants§

NAME
This source’s hardcoded id-namespace prefix - see crate::state::ReviewKey.