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}