Skip to content
741 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.

Merge branch 'mirroring' into artifacts-mode1//! Mirroring: a repository kept in step with copies of it on other hosts.
2//!
3//! Every linked repository has exactly one leader, where work happens; the
4//! others follow it. A **mirror** follows a remote that leads (GitHub,
5//! another g1t, any git host). While it stands by it is an exact, read-only
6//! copy that runs nothing. Someone can **take over**: g1t leads for a
7//! while, then **hands back**, sending what was done to the remote. A
8//! repository g1t leads can be **mirrored to** any number of followers.
9//!
10//! The integrations service keeps the links (`remotes`) and decides; the
11//! repos service keeps each repository's [`RepoMirror`], so pushes and
12//! merges are refused or allowed without asking anyone.
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.13//!
14//! What a mirror offers on g1t is one rule, [`RepoMirror::offers`], by
15//! state (`mirrorOffers` in the TypeScript contracts):
16//!
17//! | Area | Standing by | CI failover | Taken over | Handing back |
18//! | --- | --- | --- | --- | --- |
19//! | `Code`: files, commits, branches, tags, compare, releases (read) | ✓ | ✓ | ✓ | ✓ |
20//! | `Security`: what scanning found, read-only | ✓ | ✓ | ✓ | ✓ |
21//! | `Insights`, `Settings` (mirroring settings included) | ✓ | ✓ | ✓ | ✓ |
22//! | `Workflows`: runs on g1t | with Keep CI warm | ✓ | ✓ | – |
23//! | `Issues`, `Pulls`, `Agents`, `Deployments` | – | – | ✓ | – |
24//! | `Updates`: security and dependency updates, upkeep, anything that opens a branch or a pull request on its own | – | – | ✓ | – |
25//!
26//! The sidebar lists only what is offered, a page not offered says so in
27//! place of its list or form, every service refuses the writes, and g1t's
28//! own automation checks the same rule before it opens anything.
Merge branch 'mirroring' into artifacts-mode29
30use std::collections::BTreeMap;
31
32use serde::{Deserialize, Serialize};
33
34use crate::User;
35
36/// Where a mirror stands with the remote it follows.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum MirrorState {
40 /// A quiet copy: it follows every push and runs nothing.
41 #[default]
42 Standby,
43 /// The remote keeps the code; g1t runs its workflows (CI failover).
44 Ci,
45 /// g1t leads for now: pushes, pull requests, agents and workflows work.
46 Takeover,
47 /// Sending what was done during a takeover back to the remote. The
48 /// repository is read-only until it is done.
49 HandingBack,
50}
51
52impl MirrorState {
53 pub fn as_str(self) -> &'static str {
54 match self {
55 MirrorState::Standby => "standby",
56 MirrorState::Ci => "ci",
57 MirrorState::Takeover => "takeover",
58 MirrorState::HandingBack => "handing_back",
59 }
60 }
61
62 pub fn parse(text: &str) -> Option<MirrorState> {
63 Some(match text {
64 "standby" => MirrorState::Standby,
65 "ci" => MirrorState::Ci,
66 "takeover" => MirrorState::Takeover,
67 "handing_back" => MirrorState::HandingBack,
68 _ => return None,
69 })
70 }
71
72 /// Whether g1t leads, so the repository takes writes.
73 pub fn leads(self) -> bool {
74 self == MirrorState::Takeover
75 }
76
77 /// Whether g1t copies the remote's pushes in.
78 pub fn follows(self) -> bool {
79 matches!(self, MirrorState::Standby | MirrorState::Ci)
80 }
81}
82
83/// A repository's tie to the remote it mirrors, as the repos service keeps
84/// it on [`crate::repos::Repo`]. Absent for a repository that leads.
85#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct RepoMirror {
88 pub state: MirrorState,
89 /// The remote, for people: `github.com/acme/web`.
90 pub remote: String,
91 /// Its web address.
92 pub url: String,
93 /// RFC 3339: when it entered this state.
94 pub since: String,
95 /// Run `.g1t/workflows` on pushes copied in while it stands by.
96 #[serde(default)]
97 pub warm: bool,
98 /// Run `.github/workflows` as well, in CI failover and during a takeover.
99 #[serde(default)]
100 pub github_workflows: bool,
101 /// Hold jobs that name an `environment:` for approval, in CI failover
102 /// and during a takeover, so nothing deploys twice.
103 #[serde(default)]
104 pub hold_deploys: bool,
105}
106
107impl RepoMirror {
108 /// Whether the repository takes pushes, merges, issues and agents.
109 pub fn writable(&self) -> bool {
110 self.state.leads()
111 }
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.112
113 /// Whether the repository offers `area` now: the table in this
114 /// module's comment. A mirror g1t has taken over offers everything;
115 /// one standing by or handing back is a read-only copy (code, what
116 /// security scanning found, insights, settings). Workflows run in CI
117 /// failover, and while standing by only when the link keeps CI warm.
118 pub fn offers(&self, area: RepoArea) -> bool {
119 if self.writable() || area.read_only() {
120 return true;
121 }
122 match area {
123 RepoArea::Workflows => self.state == MirrorState::Ci || (self.state == MirrorState::Standby && self.warm),
124 _ => false,
125 }
126 }
127}
128
129/// The parts of a repository a mirror may or may not offer: the sidebar's
130/// pages, and `Updates` for what g1t does on its own (security and
131/// dependency updates, upkeep, anything that opens a branch or a pull
132/// request without being asked).
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum RepoArea {
136 Code,
137 Issues,
138 Pulls,
139 Agents,
140 Workflows,
141 Deployments,
142 Security,
143 Insights,
144 Settings,
145 Updates,
Merge branch 'mirroring' into artifacts-mode146}
147
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.148impl RepoArea {
149 pub const ALL: [RepoArea; 10] = [
150 RepoArea::Code,
151 RepoArea::Issues,
152 RepoArea::Pulls,
153 RepoArea::Agents,
154 RepoArea::Workflows,
155 RepoArea::Deployments,
156 RepoArea::Security,
157 RepoArea::Insights,
158 RepoArea::Settings,
159 RepoArea::Updates,
160 ];
161
162 /// Whether every mirror offers it, whatever its state: an exact copy, read.
163 pub fn read_only(self) -> bool {
164 matches!(self, RepoArea::Code | RepoArea::Security | RepoArea::Insights | RepoArea::Settings)
165 }
166}
167
168/// Whether a repository with `mirror` offers `area`: everything when it
169/// leads, else [`RepoMirror::offers`].
170pub fn offers(mirror: Option<&RepoMirror>, area: RepoArea) -> bool {
171 mirror.is_none_or(|mirror| mirror.offers(area))
172}
173
174/// Why g1t's own updates wait on a mirror: the sentence the security pages
175/// and the service share (`mirrorUpdatesWait` in the TypeScript contracts).
176pub fn updates_wait_message(namespace: &str, name: &str, mirror: &RepoMirror) -> String {
177 format!(
178 "Security updates wait until {namespace}/{name} is taken over or moved to g1t. It is a mirror of {remote}, so nothing opens a branch or a pull request here on its own.",
179 remote = mirror.remote
180 )
181}
182
Merge branch 'mirroring' into artifacts-mode183/// Why a mirror refuses a write, for people and for `git push`.
184pub fn mirror_message(namespace: &str, name: &str, mirror: &RepoMirror) -> String {
185 match mirror.state {
186 MirrorState::HandingBack => format!(
187 "{namespace}/{name} is handing back to {}. It takes changes again once that is done.",
188 mirror.remote
189 ),
190 _ => format!(
191 "{namespace}/{name} is a mirror of {remote}, so it is read-only here. Push to {remote}, or take over in Settings → Mirroring to work on g1t.",
192 remote = mirror.remote
193 ),
194 }
195}
196
197/// The kinds of host a remote can be. Each is an adapter in integrations
198/// (`src/remotes.rs`); a new host is one more arm.
199#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(rename_all = "snake_case")]
201pub enum RemoteProvider {
202 /// Through g1t's GitHub App.
203 Github,
204 /// Another g1t: g1t.sh, or one run by its owner.
205 G1t,
206 /// Any git host over HTTPS, with a username and token.
207 Git,
208}
209
210impl RemoteProvider {
211 pub fn as_str(self) -> &'static str {
212 match self {
213 RemoteProvider::Github => "github",
214 RemoteProvider::G1t => "g1t",
215 RemoteProvider::Git => "git",
216 }
217 }
218
219 pub fn parse(text: &str) -> Option<RemoteProvider> {
220 Some(match text {
221 "github" => RemoteProvider::Github,
222 "g1t" => RemoteProvider::G1t,
223 "git" => RemoteProvider::Git,
224 _ => return None,
225 })
226 }
227}
228
229/// Which side leads.
230#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
231#[serde(rename_all = "snake_case")]
232pub enum RemoteRole {
233 /// The remote leads; the repository on g1t is its mirror.
234 Leader,
235 /// g1t leads; the remote is kept in step with it.
236 Follower,
237}
238
239impl RemoteRole {
240 pub fn as_str(self) -> &'static str {
241 match self {
242 RemoteRole::Leader => "leader",
243 RemoteRole::Follower => "follower",
244 }
245 }
246}
247
248/// A link's state. A leader is `standby`, `ci`, `takeover` or
249/// `handing_back` (see [`MirrorState`]); a follower is `following`, or
250/// `stuck` when the remote refused what g1t sent.
251#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "snake_case")]
253pub enum RemoteState {
254 Standby,
255 Ci,
256 Takeover,
257 HandingBack,
258 Following,
259 Stuck,
260}
261
262impl RemoteState {
263 pub fn as_str(self) -> &'static str {
264 match self {
265 RemoteState::Standby => "standby",
266 RemoteState::Ci => "ci",
267 RemoteState::Takeover => "takeover",
268 RemoteState::HandingBack => "handing_back",
269 RemoteState::Following => "following",
270 RemoteState::Stuck => "stuck",
271 }
272 }
273
274 pub fn parse(text: &str) -> RemoteState {
275 match text {
276 "ci" => RemoteState::Ci,
277 "takeover" => RemoteState::Takeover,
278 "handing_back" => RemoteState::HandingBack,
279 "following" => RemoteState::Following,
280 "stuck" => RemoteState::Stuck,
281 _ => RemoteState::Standby,
282 }
283 }
284
285 /// The mirror state of a leader in this state.
286 pub fn mirror(self) -> Option<MirrorState> {
287 MirrorState::parse(self.as_str())
288 }
289}
290
291/// Who hears that a remote stopped answering. Nobody is woken up unless
292/// they asked to be.
293#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(rename_all = "snake_case")]
295pub enum Notify {
296 /// The repository shows it, and that is all.
297 #[default]
298 Banner,
299 /// Also an inbox item for the repository's admins.
300 Inbox,
301}
302
303/// When a takeover is handed back once the remote answers again.
304#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
305#[serde(rename_all = "snake_case")]
306pub enum HandBack {
307 /// Only when someone hands it back.
308 Ask,
309 /// On its own when every branch goes back without a decision.
310 #[default]
311 WhenClean,
312}
313
314/// What a follower does about pushes made on the remote itself.
315#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(rename_all = "snake_case")]
317pub enum RemotePushes {
318 /// Fast-forwards are taken in; anything else is shown as diverged.
319 #[default]
320 Adopt,
321 /// g1t's branches are pushed over them (what they pointed at is kept).
322 Overwrite,
323}
324
325/// The levers on a link. Every one defaults to doing nothing on its own.
326#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase", default)]
328pub struct MirrorSettings {
329 pub notify: Notify,
330 /// Take over on its own once the remote has not answered for this many
331 /// minutes. `None`: only when someone takes over.
332 pub take_over_after: Option<u32>,
333 pub hand_back: HandBack,
334 /// Run `.g1t/workflows` on pushes copied in while standing by.
335 pub keep_ci_warm: bool,
336 /// Run `.github/workflows` in CI failover and during a takeover.
337 pub github_workflows: bool,
338 /// Hold deploy jobs for approval in CI failover and during a takeover.
339 pub hold_deploys: bool,
Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.340 /// Report the statuses g1t sets on a commit (workflow runs, deploys)
341 /// to the GitHub commit, while g1t runs workflows for the mirror: CI
342 /// failover, a takeover, or CI kept warm. GitHub links only.
343 pub report_checks: bool,
Merge branch 'mirroring' into artifacts-mode344 /// For followers.
345 pub remote_pushes: RemotePushes,
346}
347
348impl Default for MirrorSettings {
349 fn default() -> Self {
350 MirrorSettings {
351 notify: Notify::Banner,
352 take_over_after: None,
353 hand_back: HandBack::WhenClean,
354 keep_ci_warm: false,
355 github_workflows: true,
356 hold_deploys: true,
Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.357 report_checks: true,
Merge branch 'mirroring' into artifacts-mode358 remote_pushes: RemotePushes::Adopt,
359 }
360 }
361}
362
363/// The shortest and longest wait a person may set before an automatic
364/// takeover.
365pub const TAKE_OVER_AFTER_MINUTES: (u32, u32) = (5, 24 * 60);
366
367/// A repository's link to a remote.
368#[derive(Clone, Debug, Serialize, Deserialize)]
369#[serde(rename_all = "camelCase")]
370pub struct Remote {
371 pub id: String,
372 pub repo_id: String,
373 /// `workspace/name` on g1t.
374 pub repo: String,
375 pub provider: RemoteProvider,
376 pub role: RemoteRole,
377 /// For people: `github.com/acme/web`.
378 pub name: String,
379 /// Its web address.
380 pub url: String,
381 pub state: RemoteState,
382 pub state_since: String,
383 /// Who put it in this state: a username, or `g1t` when it was automatic.
384 pub state_by: Option<String>,
385 /// Whether its host answers. A remote that refuses g1t's credential
386 /// still answers: that is [`Remote::last_error`].
387 pub reachable: bool,
388 pub unreachable_since: Option<String>,
389 pub synced_at: Option<String>,
390 pub last_error: Option<String>,
Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.391 /// Why g1t could not report checks to the remote, until it next can:
392 /// the GitHub App lacks the Commit statuses permission, say.
393 #[serde(default)]
394 pub checks_problem: Option<String>,
Merge branch 'mirroring' into artifacts-mode395 pub settings: MirrorSettings,
396 pub created_at: String,
397}
398
399/// What happens to one ref when a takeover is handed back.
400#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
401#[serde(rename_all = "snake_case")]
402pub enum RefAction {
403 /// Nothing moved on either side, or both moved to the same commit.
404 Same,
405 /// Only g1t moved: pushed to the remote.
406 Push,
407 /// Only the remote moved: copied in.
408 Fetch,
409 /// g1t moved, but the remote protects the branch: sent as a pull
410 /// request from `g1t/handback/<branch>`, and g1t follows the remote.
411 PullRequest,
412 /// Both moved, apart. Waits for a [`RefDecision`].
413 Diverged,
414}
415
416/// A person's decision for a diverged ref.
417#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
418#[serde(rename_all = "snake_case")]
419pub enum RefDecision {
420 /// g1t's commit is pushed over the remote's.
421 KeepOurs,
422 /// The remote's commit is taken; g1t's is kept under `refs/g1t/replaced/`.
423 KeepTheirs,
424 /// g1t's commits go to the remote as a pull request.
425 PullRequest,
426}
427
428#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
429#[serde(rename_all = "camelCase")]
430pub struct RefPlan {
431 #[serde(rename = "ref")]
432 pub git_ref: String,
433 /// The commit both sides agreed on when the takeover began.
434 pub base: Option<String>,
435 pub ours: Option<String>,
436 pub theirs: Option<String>,
437 pub action: RefAction,
438 /// For a diverged ref, what someone decided.
439 pub decision: Option<RefDecision>,
440}
441
442/// What handing a takeover back would do, ref by ref.
443#[derive(Clone, Debug, Default, Serialize, Deserialize)]
444#[serde(rename_all = "camelCase")]
445pub struct HandbackPlan {
446 pub refs: Vec<RefPlan>,
447 /// Whether the remote answered, so the plan reflects it.
448 pub reachable: bool,
449 /// Whether it can go back now: the remote answers and every diverged
450 /// ref has a decision.
451 pub ready: bool,
452}
453
454/// Decides one ref of a hand-back. `base` is what both sides agreed on when
455/// the takeover began. A ref only g1t moved goes back by push, or by pull
456/// request when the remote protects it.
457pub fn ref_action(base: Option<&str>, ours: Option<&str>, theirs: Option<&str>, protected: bool) -> RefAction {
458 if ours == theirs {
459 return RefAction::Same;
460 }
461 let ours_moved = ours != base;
462 let theirs_moved = theirs != base;
463 match (ours_moved, theirs_moved) {
464 (false, _) => RefAction::Fetch,
465 (true, false) if protected && ours.is_some() && theirs.is_some() => RefAction::PullRequest,
466 (true, false) => RefAction::Push,
467 (true, true) => RefAction::Diverged,
468 }
469}
470
471impl HandbackPlan {
472 pub fn new(refs: Vec<RefPlan>, reachable: bool) -> Self {
473 let ready = reachable
474 && refs
475 .iter()
476 .all(|plan| plan.action != RefAction::Diverged || plan.decision.is_some());
477 HandbackPlan { refs, reachable, ready }
478 }
479
480 /// Whether it needs nobody: no ref is diverged.
481 pub fn clean(&self) -> bool {
482 self.reachable && !self.refs.iter().any(|plan| plan.action == RefAction::Diverged)
483 }
484}
485
486/// Everything about a repository's links, for its pages and API.
487#[derive(Clone, Debug, Default, Serialize, Deserialize)]
488#[serde(rename_all = "camelCase")]
489pub struct MirrorView {
490 pub remotes: Vec<Remote>,
491 /// While a takeover is on or being handed back.
492 pub plan: Option<HandbackPlan>,
493 /// Whether the viewer may change the links and take over.
494 pub can_manage: bool,
495 /// What the last action did that people should know: pull requests it
496 /// opened, branches it could not bring up to date.
497 #[serde(default, skip_serializing_if = "Vec::is_empty")]
498 pub notes: Vec<String>,
499}
500
501/// A repository's links in brief, for lists.
502#[derive(Clone, Debug, Serialize, Deserialize)]
503#[serde(rename_all = "camelCase")]
504pub struct RemoteBrief {
505 pub repo_id: String,
506 pub role: RemoteRole,
507 pub name: String,
508 pub state: RemoteState,
509 pub reachable: bool,
510 /// When g1t last copied from it or pushed to it.
511 #[serde(default)]
512 pub synced_at: Option<String>,
513}
514
515/// The payload of the `mirror.*` events: `mirror.unreachable` (a mirror's
516/// remote stopped answering), `mirror.reachable` (it answers again),
517/// `mirror.state_changed` (stood by, CI failover, taken over, handing back,
518/// handed back) and `mirror.moved_in` (moved to g1t for good).
519#[derive(Clone, Debug, Default, Serialize, Deserialize)]
520#[serde(rename_all = "camelCase")]
521pub struct MirrorEvent {
522 pub repo_id: String,
523 /// `workspace/name`.
524 pub repo: String,
525 pub remote_id: String,
526 /// `github.com/acme/web`.
527 pub remote: String,
528 /// A `RemoteState` as text; absent once moved in.
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub state: Option<String>,
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub from: Option<String>,
533 /// Who did it: a username, or `g1t` when it happened on its own.
534 #[serde(skip_serializing_if = "Option::is_none")]
535 pub by: Option<String>,
536 /// What happened, for people.
537 pub title: String,
538 #[serde(skip_serializing_if = "Option::is_none")]
539 pub detail: Option<String>,
540 /// Usernames to tell in their inbox (the workspace's owners, when the
541 /// link's settings ask for it).
542 #[serde(default, skip_serializing_if = "Vec::is_empty")]
543 pub notify: Vec<String>,
544 /// The repository's mirroring settings page.
545 pub link: String,
546}
547
548// --- Methods of the integrations service ----------------------------------
549
550/// `mirror_view`: a repository's links, as the viewer may see them.
551#[derive(Debug, Serialize, Deserialize)]
552#[serde(rename_all = "camelCase")]
553pub struct MirrorViewArgs {
554 pub viewer: Option<User>,
555 pub repo_id: String,
556}
557
558/// `mirror_briefs`: the links of these repositories. Returns `[RemoteBrief]`.
559#[derive(Debug, Serialize, Deserialize)]
560#[serde(rename_all = "camelCase")]
561pub struct MirrorBriefsArgs {
562 pub repo_ids: Vec<String>,
563}
564
565/// `mirror_take_over`, `mirror_hand_back_plan`, `mirror_sync`: one
566/// repository's mirror. Return `Outcome<MirrorView>`.
567#[derive(Debug, Serialize, Deserialize)]
568#[serde(rename_all = "camelCase")]
569pub struct MirrorActArgs {
570 pub actor: User,
571 pub repo_id: String,
572}
573
574/// `mirror_ci`: starts or ends CI failover. Returns `Outcome<MirrorView>`.
575#[derive(Debug, Serialize, Deserialize)]
576#[serde(rename_all = "camelCase")]
577pub struct MirrorCiArgs {
578 pub actor: User,
579 pub repo_id: String,
580 pub on: bool,
581}
582
583/// `mirror_hand_back`: sends a takeover back, with decisions for diverged
584/// refs. Refused while a diverged ref has none. Returns
585/// `Outcome<MirrorView>`.
586#[derive(Debug, Serialize, Deserialize)]
587#[serde(rename_all = "camelCase")]
588pub struct MirrorHandBackArgs {
589 pub actor: User,
590 pub repo_id: String,
591 #[serde(default)]
592 pub decisions: BTreeMap<String, RefDecision>,
593}
594
595/// `mirror_settings`: changes a link's levers. Returns `Outcome<Remote>`.
596#[derive(Debug, Serialize, Deserialize)]
597#[serde(rename_all = "camelCase")]
598pub struct MirrorSettingsArgs {
599 pub actor: User,
600 pub remote_id: String,
601 pub settings: MirrorSettings,
602}
603
604/// `mirror_add`: links a repository to a remote on another g1t or any git
605/// host. GitHub links are made by `github_import`. A leader can only be
606/// added to an empty repository, which is then filled from it. Returns
607/// `Outcome<Remote>`.
608#[derive(Debug, Serialize, Deserialize)]
609#[serde(rename_all = "camelCase")]
610pub struct MirrorAddArgs {
611 pub actor: User,
612 pub repo_id: String,
613 pub provider: RemoteProvider,
614 pub role: RemoteRole,
615 /// The remote's https clone address.
616 pub url: String,
617 #[serde(default)]
618 pub username: Option<String>,
619 /// A token for it. Kept sealed; never shown again.
620 #[serde(default)]
621 pub token: Option<String>,
622}
623
624/// `mirror_move_in`: moves a mirror to g1t for good. The repository stops
625/// being a mirror and g1t stops tracking the remote: pushes made there no
626/// longer come here. Allowed while it stands by, in CI failover, or during
627/// a takeover (what g1t holds is kept as it is, nothing is handed back).
628/// With `keep_remote_updated`, the remote becomes a follower instead of
629/// being unlinked: g1t pushes to it from then on. Returns
630/// `Outcome<MirrorView>`.
631#[derive(Debug, Serialize, Deserialize)]
632#[serde(rename_all = "camelCase")]
633pub struct MirrorMoveInArgs {
634 pub actor: User,
635 pub repo_id: String,
636 #[serde(default)]
637 pub keep_remote_updated: bool,
638}
639
640/// `mirror_remove`: unlinks a remote. A mirror becomes an ordinary
641/// repository with what it has. Refused during a takeover. Returns
642/// `Outcome<bool>`.
643#[derive(Debug, Serialize, Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub struct MirrorRemoveArgs {
646 pub actor: User,
647 pub remote_id: String,
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 #[test]
655 fn hand_back_decides_each_ref_from_the_base() {
656 let (a, b, c) = (Some("a"), Some("b"), Some("c"));
657 assert_eq!(ref_action(a, a, a, false), RefAction::Same);
658 assert_eq!(ref_action(a, b, b, false), RefAction::Same, "both moved to the same commit");
659 assert_eq!(ref_action(a, b, a, false), RefAction::Push);
660 assert_eq!(ref_action(a, b, a, true), RefAction::PullRequest, "a protected branch goes as a pull request");
661 assert_eq!(ref_action(a, a, b, false), RefAction::Fetch);
662 assert_eq!(ref_action(a, b, c, false), RefAction::Diverged);
663 assert_eq!(ref_action(None, b, None, true), RefAction::Push, "a new branch is pushed");
664 assert_eq!(ref_action(a, None, a, false), RefAction::Push, "a deleted branch is deleted there");
665 assert_eq!(ref_action(None, None, b, false), RefAction::Fetch, "a branch made there is copied in");
666 }
667
668 #[test]
669 fn a_plan_is_ready_once_every_diverged_ref_is_decided() {
670 let diverged = |decision| RefPlan {
671 git_ref: "refs/heads/docs".into(),
672 base: Some("a".into()),
673 ours: Some("b".into()),
674 theirs: Some("c".into()),
675 action: RefAction::Diverged,
676 decision,
677 };
678 let plan = HandbackPlan::new(vec![diverged(None)], true);
679 assert!(!plan.ready && !plan.clean());
680 let plan = HandbackPlan::new(vec![diverged(Some(RefDecision::KeepTheirs))], true);
681 assert!(plan.ready && !plan.clean());
682 assert!(!HandbackPlan::new(Vec::new(), false).ready, "not while the remote is away");
683 assert!(HandbackPlan::new(Vec::new(), true).clean());
684 }
685
686 #[test]
687 fn only_a_takeover_takes_writes() {
688 let mut mirror = RepoMirror { remote: "github.com/acme/web".into(), ..RepoMirror::default() };
689 for (state, writable) in [
690 (MirrorState::Standby, false),
691 (MirrorState::Ci, false),
692 (MirrorState::Takeover, true),
693 (MirrorState::HandingBack, false),
694 ] {
695 mirror.state = state;
696 assert_eq!(mirror.writable(), writable, "{state:?}");
697 assert_eq!(MirrorState::parse(state.as_str()), Some(state));
698 assert_eq!(RemoteState::parse(state.as_str()).mirror(), Some(state));
699 }
700 assert!(mirror_message("acme", "web", &RepoMirror { remote: "github.com/acme/web".into(), ..RepoMirror::default() })
701 .contains("is a mirror of github.com/acme/web"));
702 }
703
704 #[test]
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.705 fn a_mirror_offers_a_read_only_copy_until_it_is_taken_over() {
706 let mirror = |state| RepoMirror { state, remote: "github.com/acme/web".into(), ..RepoMirror::default() };
707 let read = [RepoArea::Code, RepoArea::Security, RepoArea::Insights, RepoArea::Settings];
708 let work = [RepoArea::Issues, RepoArea::Pulls, RepoArea::Agents, RepoArea::Deployments, RepoArea::Updates];
709 for state in [MirrorState::Standby, MirrorState::Ci, MirrorState::HandingBack] {
710 for area in read {
711 assert!(mirror(state).offers(area), "{state:?} {area:?}");
712 }
713 for area in work {
714 assert!(!mirror(state).offers(area), "{state:?} {area:?}");
715 }
716 }
717 for area in RepoArea::ALL {
718 assert!(mirror(MirrorState::Takeover).offers(area), "{area:?}");
719 assert!(offers(None, area), "a repository that leads offers {area:?}");
720 }
721 // Workflows: CI failover and takeovers, and a standby kept warm.
722 assert!(!mirror(MirrorState::Standby).offers(RepoArea::Workflows));
723 assert!(RepoMirror { warm: true, ..mirror(MirrorState::Standby) }.offers(RepoArea::Workflows));
724 assert!(mirror(MirrorState::Ci).offers(RepoArea::Workflows));
725 assert!(!mirror(MirrorState::HandingBack).offers(RepoArea::Workflows));
726 assert!(!RepoMirror { warm: true, ..mirror(MirrorState::HandingBack) }.offers(RepoArea::Workflows));
727 let message = updates_wait_message("acme", "web", &mirror(MirrorState::Standby));
728 assert!(message.starts_with("Security updates wait until acme/web is taken over or moved to g1t."));
729 assert!(message.contains("github.com/acme/web"));
730 }
731
732 #[test]
Merge branch 'mirroring' into artifacts-mode733 fn settings_default_to_doing_nothing_on_their_own() {
734 let settings: MirrorSettings = serde_json::from_str("{}").unwrap();
735 assert_eq!(settings, MirrorSettings::default());
736 assert_eq!(settings.notify, Notify::Banner);
737 assert_eq!(settings.take_over_after, None);
738 assert!(!settings.keep_ci_warm);
Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.739 assert!(settings.report_checks, "checks are reported whenever g1t runs workflows, unless turned off");
Merge branch 'mirroring' into artifacts-mode740 }
741}

This file's history is long; its oldest lines are credited to the oldest commit read.