review_queue/source/mod.rs
1//! The `ReviewSource` abstraction: sources say *what* to check out, the `vcs` module decides
2//! *how*. See the design plan for the GitHub/moz-phab implementations; this module currently
3//! only has the shared types and trait.
4
5use std::path::Path;
6
7use anyhow::Result;
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10
11pub mod diffstat;
12pub mod github;
13pub mod moz_phab;
14
15/// `{source}/{id}` identity for a review; re-exported here since sources produce it.
16pub use crate::state::ReviewKey;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Lifecycle {
20 Open,
21 /// Landed/closed/abandoned/merged: safe to remove the workspace once clean.
22 Resolved,
23}
24
25/// Serialized into `state.json` (as part of `ReviewEntry`) so a review's kind survives between
26/// `sync` and a later on-demand `rq fetch`.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub enum ReviewKind {
29 Direct,
30 Group(String),
31}
32
33/// A repo referenced by a review, before it's resolved to a canonical local repo. Serialized into
34/// `state.json` (as part of `ReviewEntry`) so `rq fetch`/the TUI can resolve it later without
35/// re-querying the source.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct RepoRef {
38 /// Candidate clone URLs (mirrors, ssh/https variants) - matched against discovered workdir
39 /// repos and the tool-managed clone registry after normalization.
40 pub urls: Vec<String>,
41 pub display_name: String,
42}
43
44#[derive(Debug, Clone)]
45pub struct Review {
46 pub key: ReviewKey,
47 pub title: String,
48 pub author: String,
49 pub url: String,
50 pub repo: RepoRef,
51 /// Diff ID / head SHA (or comma-joined stack of diff IDs for Phabricator). Used to detect
52 /// updates that require re-syncing the workspace.
53 pub version: String,
54 pub kind: ReviewKind,
55 /// Diffstat (same summary format `git diff --stat`/`jj diff --stat` print), fetched
56 /// best-effort as part of the same `fetch_queue()` call that built this `Review` - `None` if
57 /// the source couldn't get one (no active diff, a Conduit/API hiccup, etc). Carried straight
58 /// into `state.json`'s `ReviewEntry` so `rq show`'s TUI can show it with no further network
59 /// calls of its own.
60 pub diff_stat: Option<String>,
61 /// The PR description / Phabricator revision summary (the commit message body - the title is
62 /// carried separately in `title`). Carried into `state.json` alongside `diff_stat` so the TUI
63 /// can show it with no network calls of its own.
64 pub description: Option<String>,
65 /// This review's ancestors within its stack, bottom-most first, excluding itself and any
66 /// already-landed ones (those are part of the base). Empty for a review that isn't stacked.
67 pub ancestors: Vec<ReviewKey>,
68}
69
70#[derive(Debug, Clone)]
71pub struct Patch {
72 pub title: String,
73 /// `"Name <email>"`, ready to pass straight to `git commit --author`/`jj commit --author`.
74 pub author: String,
75 pub message: String,
76 pub diff: String,
77}
78
79#[derive(Debug, Clone)]
80pub enum Checkout {
81 /// e.g. GitHub: fetch `refspec` from the canonical repo's own origin, expect it to resolve
82 /// to `commit`. The git backend only ever needs `refspec`/`commit`; `fork`, when present,
83 /// is used solely by the jj backend, which tracks the PR head's own remote+branch rather
84 /// than importing an anonymous ref (see `vcs::jj`).
85 Ref {
86 refspec: String,
87 commit: String,
88 fork: Option<ForkRef>,
89 },
90 /// Apply `patches` bottom-to-top on top of `base` (or the canonical repo's default branch
91 /// tip if `base` is unset). No current source produces this - `moz_phab` delegates the whole
92 /// apply step to the `moz-phab` CLI instead (see `ExternalCommand`) - but it's kept as a
93 /// generic extension point for a future source that fetches raw diffs itself.
94 Patches {
95 base: Option<String>,
96 patches: Vec<Patch>,
97 },
98 /// Delegate the entire checkout to an external command, run with the given `env` added and
99 /// `cwd` set to the workspace. The workspace starts at the canonical repo's default branch
100 /// tip (same starting point as `Patches` with `base: None`) - the command is expected to
101 /// move it wherever it needs to itself (`moz-phab patch --apply-to base` resolves and checks
102 /// out the revision's actual base on its own, including cases a source's own Conduit calls
103 /// can't cheaply replicate, like an unlanded base needing a git-cinnabar hg/git translation -
104 /// confirmed against real Mozilla Phabricator, not just moz-phab's source). A nonzero exit
105 /// leaves the workspace in place for inspection, same as a failed `Patches` application.
106 ExternalCommand {
107 program: String,
108 args: Vec<String>,
109 env: Vec<(String, String)>,
110 },
111}
112
113/// The PR head's own remote+branch, e.g. `https://github.com/alice/firefox` + `feature-x`.
114/// `None` when the source fork has been deleted, in which case the jj backend falls back to a
115/// raw fetch-and-import of `refspec`/`commit` instead.
116#[derive(Debug, Clone)]
117pub struct ForkRef {
118 /// Name to register the remote under in the canonical jj repo. Callers should use `"origin"`
119 /// instead of registering a redundant remote when this normalizes to the canonical repo's
120 /// own origin URL (a same-repo branch PR).
121 pub remote_name: String,
122 pub remote_url: String,
123 pub branch: String,
124}
125
126#[async_trait]
127pub trait ReviewSource: Send + Sync {
128 /// Instance name from config, e.g. "moz" - distinguishes multiple configured instances of
129 /// the same source type and forms the `source` half of a `ReviewKey`.
130 fn name(&self) -> &str;
131
132 async fn fetch_queue(&self) -> Result<Vec<Review>>;
133
134 /// `canonical_repo` is the already-resolved local repo this review's workspace will be built
135 /// from - most sources ignore it, but one that needs to prepare local repo state first (e.g.
136 /// `moz_phab` writing `.git/.arcconfig` so `moz-phab` can find the right Phabricator
137 /// instance) needs it before it can build the `Checkout`.
138 async fn checkout_spec(&self, review: &Review, canonical_repo: &Path) -> Result<Checkout>;
139
140 /// Refresh the lifecycle of reviews that are no longer in the queue (e.g. changes were
141 /// requested, or the reviewer was removed) so `sync` knows whether to keep the workspace.
142 async fn fetch_status(&self, ids: &[String]) -> Result<Vec<(String, Lifecycle)>>;
143
144 /// Whether `message` (a commit message from a workspace built for a stack containing
145 /// `review`) is the commit that carries `review`'s patch. Lets a stack workspace be
146 /// positioned at one member's patch. Sources that don't stack reviews never need this.
147 fn is_commit_for(&self, _review: &ReviewKey, _message: &str) -> bool {
148 false
149 }
150}