Skip to content
422 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1//! GitHub: signing in with a GitHub account, and bringing repositories
2//! across through g1t's GitHub App. Mirrors `packages/contracts/src/github.ts`.
3//!
4//! One GitHub App does both. Its user authorization (OAuth web flow, with
5//! PKCE) signs people in and lets g1t list what they installed it on; its
6//! installations give g1t access to the repositories they chose. The app
7//! is optional: with none configured every method here says so, and the
8//! site shows no GitHub buttons.
9//!
10//! The identity service (`POST /rpc/<method>`) keeps linked accounts and
11//! their user tokens:
12//!
13//! - `github_enabled` (no arguments) returns `bool`.
14//! - `github_start` takes `GithubStartArgs`, returns `Outcome<GithubStart>`.
15//! - `github_finish` takes `GithubFinishArgs`, returns `Outcome<GithubFinished>`.
16//! - `github_pending` takes `GithubPendingArgs`, returns `Outcome<GithubPending>`.
17//! - `github_sign_up` takes `GithubSignUpArgs`, returns `Outcome<SignedIn>`.
18//! - `github_claim` takes `GithubClaimArgs`, returns `Outcome<GithubAccount>`.
19//! - `github_account` takes `UserArgs`, returns `GithubAccountView`.
20//! - `github_unlink` takes `UserArgs`, returns `Outcome<bool>`.
21//! - `github_user_token` takes `GithubUserTokenArgs`, returns `Outcome<String>`.
22//! - `github_revoked` takes `GithubRevokedArgs`, returns `u32`.
23//! - `github_usernames` takes `GithubUsernamesArgs`, returns a map.
24//!
25//! The integrations service keeps installations and linked repositories,
26//! and receives the app's webhook at `https://api.g1t.sh/hooks/github`:
27//!
28//! - `github_status` takes `GithubStatusArgs`, returns `Outcome<GithubAppStatus>`.
29//! - `github_add_installation` takes `GithubInstallationArgs`, returns
30//! `Outcome<GithubInstallation>`.
31//! - `github_remove_installation` takes `GithubInstallationArgs`, returns
32//! `Outcome<bool>`.
A GitHub App installed straight on GitHub can be added to a workspace: Import from GitHub lists the installations your GitHub account can see that this workspace hasn't added, each with Add for owners, and coming back from GitHub without g1t's own start asks which of your workspaces to add it to instead of stopping; the GitHub guide says how.33//! - `github_visible_installations` takes `GithubVisibleArgs`, returns
34//! `Outcome<Vec<GithubVisibleInstallation>>`.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look35//! - `github_repositories` takes `GithubRepositoriesArgs`, returns
36//! `Outcome<GithubRepositories>`.
37//! - `github_import` takes `GithubImportArgs`, returns `Outcome<GithubRepoLink>`.
38//! - `github_link` takes `GithubLinkArgs`, returns `Option<GithubRepoLink>`.
39//! - `github_unlink_repo` takes `GithubUnlinkRepoArgs`, returns `Outcome<bool>`.
40//! - `github_sync` takes `GithubUnlinkRepoArgs`, returns `Outcome<GithubRepoLink>`.
41//! - `github_receive` takes `GithubReceiveArgs`, returns `integrations::Received`.
42
43use std::collections::HashMap;
44
45use serde::{Deserialize, Serialize};
46
47use crate::User;
48use crate::identity::SignedIn;
49
50/// Why someone is sent to GitHub.
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum GithubPurpose {
54 /// Sign in, or create an account.
55 SignIn,
56 /// Link GitHub to the signed-in account.
57 Link,
58}
59
60impl GithubPurpose {
61 pub fn as_str(self) -> &'static str {
62 match self {
63 GithubPurpose::SignIn => "sign_in",
64 GithubPurpose::Link => "link",
65 }
66 }
67}
68
69#[derive(Debug, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct GithubStartArgs {
72 pub purpose: GithubPurpose,
73 /// The signed-in person, for `link`.
74 #[serde(default)]
75 pub user: Option<User>,
76 /// Exactly the callback registered on the app, such as
77 /// `https://g1t.sh/auth/github/callback`.
78 pub redirect_uri: String,
79 /// A same-site path to return to afterwards.
80 #[serde(default)]
81 pub next: String,
82 /// An invite code, from `/register?invite=…`, carried through GitHub
83 /// for a new account while g1t is invite-only.
84 #[serde(default)]
85 pub invite_code: Option<String>,
86}
87
88/// Where to send the browser, and the state to bind to it in a cookie.
89#[derive(Debug, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct GithubStart {
92 pub authorize_url: String,
93 pub state: String,
94}
95
96#[derive(Debug, Serialize, Deserialize)]
97pub struct GithubFinishArgs {
98 pub state: String,
99 pub code: String,
100}
101
102/// How a return from GitHub ended.
103#[derive(Debug, Serialize, Deserialize)]
104#[serde(tag = "kind", rename_all = "snake_case")]
105pub enum GithubFinished {
106 /// Signed in to the account the GitHub account is linked to, or to a
107 /// new one made with it.
108 SignedIn {
109 #[serde(rename = "signedIn")]
110 signed_in: SignedIn,
111 created: bool,
112 next: String,
113 },
114 /// Linked to the account that asked.
115 Linked { login: String, next: String },
116 /// An account with one of its verified emails exists: the person signs
117 /// in to it, and `github_claim` then links it.
118 NeedsLink { pending: String, login: String, next: String },
119 /// A new account, but the GitHub login cannot be its username.
120 /// A new account, but the GitHub login cannot be its username, or g1t
121 /// is invite-only and no invite code came with the sign-in.
122 NeedsUsername {
123 pending: String,
124 login: String,
125 suggestion: String,
126 next: String,
127 #[serde(rename = "inviteRequired")]
128 invite_required: bool,
129 },
130}
131
132#[derive(Debug, Serialize, Deserialize)]
133pub struct GithubPendingArgs {
134 pub pending: String,
135}
136
137/// A GitHub sign-in waiting on a username or on signing in to link.
138#[derive(Debug, Serialize, Deserialize)]
139pub struct GithubPending {
140 pub login: String,
141 /// `link` or `username`.
142 pub kind: String,
143 pub suggestion: Option<String>,
144 pub next: String,
145 /// Whether the person must give an invite code to create the account.
146 #[serde(rename = "inviteRequired", default)]
147 pub invite_required: bool,
148}
149
150#[derive(Debug, Serialize, Deserialize)]
151pub struct GithubSignUpArgs {
152 pub pending: String,
153 pub username: String,
154 /// Needed while g1t is invite-only, unless one came with the sign-in.
155 #[serde(rename = "inviteCode", default)]
156 pub invite_code: Option<String>,
157}
158
159#[derive(Debug, Serialize, Deserialize)]
160pub struct GithubClaimArgs {
161 pub pending: String,
162 pub user: User,
163}
164
165/// A linked GitHub account. The numeric id is what identifies it; the
166/// login is for showing, and is refreshed at each sign-in.
167#[derive(Clone, Debug, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct GithubAccount {
170 pub github_id: u64,
171 pub login: String,
172 /// RFC 3339.
173 pub linked_at: String,
174 /// Whether g1t holds a working user token, needed to list installations.
175 pub authorized: bool,
176}
177
178#[derive(Debug, Default, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct GithubAccountView {
181 /// Whether this g1t has a GitHub App configured for sign-in.
182 pub enabled: bool,
183 pub account: Option<GithubAccount>,
184 /// Whether the account has a password, so GitHub is not its only way in.
185 pub has_password: bool,
186}
187
188#[derive(Debug, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct GithubUserTokenArgs {
191 pub user_id: String,
192}
193
194#[derive(Debug, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct GithubRevokedArgs {
197 pub github_id: u64,
198}
199
200/// `github_usernames`: the g1t usernames of linked GitHub accounts. Returns
201/// a map from GitHub id, as a string, to username.
202#[derive(Debug, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct GithubUsernamesArgs {
205 pub github_ids: Vec<u64>,
206}
207
208// --- The app's installations and repositories (integrations) -------------
209
210/// How a GitHub repository comes to g1t.
211#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
212#[serde(rename_all = "snake_case")]
213pub enum GithubMode {
214 /// Copied once. The g1t repository is then its own.
215 Import,
216 /// g1t follows GitHub: every push there is fetched here.
217 Mirror,
218 /// GitHub follows g1t: every push here is pushed there.
219 Push,
220}
221
222impl GithubMode {
223 pub fn as_str(self) -> &'static str {
224 match self {
225 GithubMode::Import => "import",
226 GithubMode::Mirror => "mirror",
227 GithubMode::Push => "push",
228 }
229 }
230
231 pub fn parse(text: &str) -> GithubMode {
232 match text {
233 "mirror" => GithubMode::Mirror,
234 "push" => GithubMode::Push,
235 _ => GithubMode::Import,
236 }
237 }
238}
239
240#[derive(Clone, Debug, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct GithubInstallation {
243 pub id: u64,
244 pub workspace: String,
245 /// The GitHub user or organization it is installed on.
246 pub account: String,
247 /// `User` or `Organization`.
248 pub account_type: String,
249 /// `all` or `selected` repositories.
250 pub repository_selection: String,
251 pub suspended: bool,
252 /// Where to change which repositories it can see.
253 pub settings_url: String,
254 pub created_at: String,
255}
256
257#[derive(Debug, Serialize, Deserialize)]
258#[serde(rename_all = "camelCase")]
259pub struct GithubStatusArgs {
260 pub viewer: User,
261 pub workspace: String,
262}
263
264#[derive(Debug, Default, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase")]
266pub struct GithubAppStatus {
267 /// Whether this g1t has a GitHub App configured for repositories.
268 pub configured: bool,
269 /// `https://github.com/apps/<slug>/installations/new`.
270 pub install_url: Option<String>,
271 /// Whether the viewer has linked GitHub with a working user token.
272 pub linked: bool,
273 pub installations: Vec<GithubInstallation>,
274}
275
276#[derive(Debug, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct GithubInstallationArgs {
279 pub actor: User,
280 pub workspace: String,
281 pub installation_id: u64,
282}
283
A GitHub App installed straight on GitHub can be added to a workspace: Import from GitHub lists the installations your GitHub account can see that this workspace hasn't added, each with Add for owners, and coming back from GitHub without g1t's own start asks which of your workspaces to add it to instead of stopping; the GitHub guide says how.284/// `github_visible_installations`: the app's installations the person's
285/// own GitHub account can see, for claiming one that was installed on
286/// GitHub directly rather than from g1t.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look287#[derive(Debug, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
A GitHub App installed straight on GitHub can be added to a workspace: Import from GitHub lists the installations your GitHub account can see that this workspace hasn't added, each with Add for owners, and coming back from GitHub without g1t's own start asks which of your workspaces to add it to instead of stopping; the GitHub guide says how.289pub struct GithubVisibleArgs {
290 pub actor: User,
291}
292
293/// An installation of the app, as the person's GitHub account sees it.
294#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct GithubVisibleInstallation {
297 pub id: u64,
298 /// The GitHub user or organization it is installed on.
299 pub account: String,
300 /// `User` or `Organization`.
301 pub account_type: String,
302 /// `all` or `selected` repositories.
303 pub repository_selection: String,
304 pub suspended: bool,
305 pub settings_url: String,
306 /// The person's workspaces it is recorded in already.
307 pub recorded_in: Vec<String>,
308}
309
310#[derive(Debug, Serialize, Deserialize)]
311#[serde(rename_all = "camelCase")]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look312pub struct GithubRepositoriesArgs {
313 pub actor: User,
314 pub workspace: String,
315 pub installation_id: u64,
316 #[serde(default)]
317 pub page: Option<u32>,
318}
319
320#[derive(Clone, Debug, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct GithubRepository {
323 pub id: u64,
324 /// `owner/name`.
325 pub full_name: String,
326 pub name: String,
327 pub private: bool,
328 pub description: Option<String>,
329 pub default_branch: String,
330 /// The g1t repository already linked to it, as `workspace/name`.
331 pub linked_to: Option<String>,
332}
333
334#[derive(Debug, Default, Serialize, Deserialize)]
335#[serde(rename_all = "camelCase")]
336pub struct GithubRepositories {
337 pub repositories: Vec<GithubRepository>,
338 pub total: u32,
339 pub page: u32,
340 pub per_page: u32,
341}
342
343#[derive(Debug, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct GithubImportArgs {
346 pub actor: User,
347 pub workspace: String,
348 pub installation_id: u64,
349 pub github_repo_id: u64,
350 /// The g1t repository's name; the GitHub one's when left out.
351 #[serde(default)]
352 pub name: Option<String>,
353 pub mode: GithubMode,
354 /// Private on g1t; as on GitHub when left out.
355 #[serde(default)]
356 pub private: Option<bool>,
357 /// Also copy issues, with their labels, milestone and state.
358 #[serde(default)]
359 pub issues: bool,
360}
361
362/// A g1t repository's tie to a GitHub repository.
363#[derive(Clone, Debug, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct GithubRepoLink {
366 pub repo_id: String,
367 /// `workspace/name` on g1t.
368 pub repo: String,
369 pub installation_id: u64,
370 pub github_repo_id: u64,
371 /// `owner/name` on GitHub.
372 pub full_name: String,
373 pub mode: GithubMode,
374 /// RFC 3339; the last time refs were copied either way.
375 pub synced_at: Option<String>,
376 pub last_error: Option<String>,
377 /// Issues copied so far, when they were asked for.
378 pub issues_imported: u32,
379}
380
381#[derive(Debug, Serialize, Deserialize)]
382#[serde(rename_all = "camelCase")]
383pub struct GithubLinkArgs {
384 pub repo_id: String,
385}
386
387#[derive(Debug, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct GithubUnlinkRepoArgs {
390 pub actor: User,
391 pub repo_id: String,
392}
393
394#[derive(Debug, Serialize, Deserialize)]
395pub struct GithubReceiveArgs {
396 /// Header names in lowercase.
397 pub headers: HashMap<String, String>,
398 pub body: String,
399}
A GitHub App installed straight on GitHub can be added to a workspace: Import from GitHub lists the installations your GitHub account can see that this workspace hasn't added, each with Add for owners, and coming back from GitHub without g1t's own start asks which of your workspaces to add it to instead of stopping; the GitHub guide says how.400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn a_visible_installation_reads_as_the_site_expects() {
407 let item = GithubVisibleInstallation {
408 id: 7,
409 account: "flagon-io".into(),
410 account_type: "Organization".into(),
411 repository_selection: "all".into(),
412 suspended: false,
413 settings_url: "https://github.com/organizations/flagon-io/settings/installations/7".into(),
414 recorded_in: vec!["flagon-io".into()],
415 };
416 let json = serde_json::to_value(&item).unwrap();
417 assert_eq!(json["accountType"], "Organization");
418 assert_eq!(json["repositorySelection"], "all");
419 assert_eq!(json["recordedIn"][0], "flagon-io");
420 assert_eq!(serde_json::from_value::<GithubVisibleInstallation>(json).unwrap(), item);
421 }
422}