Skip to main content

escouade_plugin_protocol/
forge.rs

1//! The data of the code hosts (docs/PLUGINS.md §4.5): what a host's pull requests, checks, reviews and deployments are,
2//! as the core and the plugins that serve a host (`forge/*`) both know them. They travel as JSON, in camelCase; a plugin
3//! written in another language writes the same.
4
5use serde::{Deserialize, Serialize};
6
7/// The most open pull requests of a repository a host lists (`forge/openPullRequests`).
8pub const OPEN_PRS: usize = 30;
9
10fn is_zero(n: &u64) -> bool {
11    *n == 0
12}
13
14/// A repository on a code host: its namespace (owner, organization, GitLab group and subgroups,
15/// Bitbucket workspace) and its name.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct RepoRef {
19    pub namespace: String,
20    pub name: String,
21}
22
23impl RepoRef {
24    /// `namespace/name`.
25    pub fn full(&self) -> String {
26        format!("{}/{}", self.namespace, self.name)
27    }
28
29    /// Exactly two segments: an owner and a repository (GitHub, Bitbucket, Gitea).
30    pub fn two(segments: &[String]) -> Option<Self> {
31        match segments {
32            [owner, name] => Some(Self {
33                namespace: owner.clone(),
34                name: name.clone(),
35            }),
36            _ => None,
37        }
38    }
39
40    /// Two segments or more: groups and subgroups, then the project (GitLab).
41    pub fn nested(segments: &[String]) -> Option<Self> {
42        match segments {
43            [groups @ .., name] if !groups.is_empty() => Some(Self {
44                namespace: groups.join("/"),
45                name: name.clone(),
46            }),
47            _ => None,
48        }
49    }
50}
51
52/// How a pull request merges.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub enum MergeStrategy {
56    Merge,
57    Squash,
58    Rebase,
59    /// Rebased onto its base, then merged with a merge commit (Azure DevOps' « semi-linear »).
60    SemiLinear,
61}
62
63/// How a pull request of a repository may merge, as the repository's settings say (ESC-62): the strategies they allow,
64/// and whether the host merges by itself once the checks pass (GitHub's « Allow auto-merge », Gitea's protected
65/// branch). Read by `Forge::merge_options`.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
67#[serde(rename_all = "camelCase", default)]
68pub struct MergeOptions {
69    pub strategies: Vec<MergeStrategy>,
70    pub auto_merge: bool,
71}
72
73/// A pull request (a merge request on GitLab) to open.
74#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
75pub struct PullRequestDraft {
76    pub title: String,
77    pub body: String,
78    /// The branch that brings the changes.
79    pub head: String,
80    /// The branch it merges into.
81    pub base: String,
82    pub draft: bool,
83    /// Who is asked for a review (logins).
84    pub reviewers: Vec<String>,
85    pub labels: Vec<String>,
86    /// The issue the pull request closes once merged: « Closes #n » ends its description.
87    pub closes: Option<u64>,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
91#[serde(rename_all = "camelCase")]
92pub enum PrState {
93    #[default]
94    Open,
95    Closed,
96    Merged,
97}
98
99/// A pull request as its host tells it (kept with an agent's data: `AgentMeta::prs`).
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
101#[serde(rename_all = "camelCase", default)]
102pub struct PullRequest {
103    pub number: u64,
104    pub url: String,
105    pub title: String,
106    pub state: PrState,
107    pub draft: bool,
108    pub head: String,
109    pub base: String,
110    /// Conflicts with its base: `Some(false)`; not known yet: none.
111    pub mergeable: Option<bool>,
112    /// Reviewers whose last say is an approval.
113    pub approvals: u32,
114    /// A reviewer's last say asks for changes.
115    pub changes_requested: bool,
116    /// Threads of comments on lines not resolved yet.
117    pub open_threads: u32,
118    /// The host merges it by itself once the checks pass.
119    pub auto_merge: bool,
120    /// Who opened it (a login, a name on Azure DevOps); empty when the host does not say.
121    #[serde(skip_serializing_if = "String::is_empty")]
122    pub author: String,
123    /// When it was opened (seconds since 1970); 0 when the host does not say.
124    #[serde(skip_serializing_if = "is_zero")]
125    pub created: u64,
126    /// The checks of its last commit, in one word; none when it has none.
127    pub checks: Option<CheckState>,
128    /// Its branch is a fork's: another repository's branch that only bears the same name.
129    pub fork: bool,
130    /// Of an agent of several repositories (ESC-169): the repository it was opened in.
131    #[serde(skip_serializing_if = "String::is_empty")]
132    pub repo: String,
133    /// The repository on the host its branch is in (`ada/demo`), when the host tells it: a
134    /// fork's, for one opened from a fork.
135    #[serde(skip_serializing_if = "String::is_empty")]
136    pub head_repo: String,
137    /// The repository on the host it is in (`acme/demo`), when it was read elsewhere than on the
138    /// remote of the project's target: on the remote the agent's branch tracks, or on the
139    /// repository that one was forked from. Empty: the target's.
140    #[serde(skip_serializing_if = "String::is_empty")]
141    pub host_repo: String,
142    /// Merged: the commit its merge made on its base (the merge commit, the squash commit, or the
143    /// last commit rebased), when the host tells it (ESC-67): what says in which environments a
144    /// ticket merged by its pull request is deployed.
145    #[serde(skip_serializing_if = "String::is_empty")]
146    pub merge_commit: String,
147    /// Whose review is asked and not given yet (logins; names on Azure DevOps), when the host
148    /// says it: the « Relecture demandée » filter of the project's pull requests (ESC-65).
149    #[serde(skip_serializing_if = "Vec::is_empty")]
150    pub reviewers: Vec<String>,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
154#[serde(rename_all = "camelCase")]
155pub enum CheckState {
156    #[default]
157    Pending,
158    Success,
159    Failure,
160    Skipped,
161}
162
163/// A check or a status of a pull request's last commit (a CI job).
164#[derive(Debug, Clone, PartialEq, Serialize, Default, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct Check {
167    pub name: String,
168    pub state: CheckState,
169    pub url: Option<String>,
170    /// What `Forge::job_log` asks for, when the host keeps its log.
171    pub job: Option<String>,
172    /// When it started (seconds since 1970), when the host says it: a pending one that started
173    /// is running, the others wait.
174    pub started: Option<u64>,
175    /// When it ended, when the host says it.
176    pub finished: Option<u64>,
177    /// Stopped before its end: a failure to the rest of Escouade, cancelled to the window.
178    pub cancelled: bool,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub enum ReviewState {
184    Approved,
185    ChangesRequested,
186    Commented,
187}
188
189/// A comment on a line of a review to publish.
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191pub struct ReviewLine {
192    pub path: String,
193    pub line: u32,
194    pub body: String,
195}
196
197/// A review to publish on a pull request, as the user validated it.
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199pub struct ReviewDraft {
200    pub state: ReviewState,
201    pub body: String,
202    pub lines: Vec<ReviewLine>,
203}
204
205/// How much of its calls a host leaves, as its answers say.
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct RateLimit {
209    pub limit: u32,
210    pub remaining: u32,
211    /// When it starts over (seconds since 1970).
212    pub reset: u64,
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct Review {
218    pub author: String,
219    pub state: ReviewState,
220    pub body: String,
221    /// When it was given (seconds since 1970); 0 when the host does not say.
222    #[serde(default, skip_serializing_if = "is_zero")]
223    pub created: u64,
224}
225
226/// A comment of a pull request, on one of its lines or not.
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct ReviewComment {
230    pub id: String,
231    pub author: String,
232    pub body: String,
233    pub path: Option<String>,
234    pub line: Option<u32>,
235    pub resolved: bool,
236    /// When it was written (seconds since 1970); 0 when the host does not say.
237    #[serde(default, skip_serializing_if = "is_zero")]
238    pub created: u64,
239    /// The lines of the diff it was written on, as the host keeps them (GitHub's and Gitea's
240    /// `diff_hunk`); none when the host does not say.
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub excerpt: Option<String>,
243}
244
245/// A deployment in one of the repository's environments.
246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
247#[serde(rename_all = "camelCase", default)]
248pub struct Deployment {
249    pub environment: String,
250    /// Its last state, in the host's word lowered: `success`, `failure`, `error`, `pending`,
251    /// `in_progress`, `queued`, `inactive` (a success a newer one replaced)…
252    pub state: String,
253    /// `deployments`: the address of the environment, else of its job. `environments`: the
254    /// environment's address alone (`log` holds the job's).
255    pub url: Option<String>,
256    /// The commit it deployed (its full id), when the host tells it (ESC-67).
257    #[serde(skip_serializing_if = "String::is_empty")]
258    pub sha: String,
259    /// When it was made (seconds since 1970); 0 when the host does not say.
260    #[serde(skip_serializing_if = "is_zero")]
261    pub created: u64,
262    /// Who made it (a login); empty when the host does not say.
263    #[serde(skip_serializing_if = "String::is_empty")]
264    pub author: String,
265    /// The address of the job that deployed it, to read why it failed.
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub log: Option<String>,
268}
269
270/// Who an account is on its host, as its token says.
271#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
272pub struct Identity {
273    /// The login.
274    pub user: String,
275    /// As the window shows it: "@ada".
276    pub label: String,
277    /// The rights the token has, when the host tells them (GitHub's classic tokens, GitLab);
278    /// none when it does not (fine-grained tokens, Bitbucket, Gitea).
279    pub scopes: Option<Vec<String>>,
280}