Skip to content
109 linesCodeBlameRaw
1import type { MirrorAddInput, MirrorView, Result } from "@g1t/contracts";
2
3import type { Route } from "./+types/settings-mirroring";
4import { type MirrorOutcome, MirroringSettings } from "../../components/mirror-settings";
5import { RepoSettingsHeading } from "../../components/repo-settings-heading";
6import { page } from "../../lib/meta";
7import { decisionsFrom, mirrorSettingsFrom } from "../../lib/mirror";
8import { refusal, requireInsider } from "../../lib/access.server";
9import { mirrors, repos } from "../../lib/services.server";
10import { assertSameOrigin, requireUser, unwrap } from "../../lib/session.server";
11
12export function meta({ params, ...args }: Route.MetaArgs) {
13 return page(args, { title: `Mirroring · ${params.owner}/${params.repo} · g1t` });
14}
15
16export async function loader({ params, context, request }: Route.LoaderArgs) {
17 // Admins; to anyone without a role here the page does not exist.
18 const { viewer, repo } = await requireInsider(context, params, "manage_integrations");
19 const mirror = repo.mirror ?? null;
20 // The hand-back plan asks the remote about every branch, so it is read
21 // only when someone asks to review it, and only while g1t has taken over.
22 const planRequested = new URL(request.url).searchParams.has("plan") && mirror?.state === "takeover";
23 if (planRequested && viewer) {
24 const planned = await mirrors.plan(viewer, repo.id);
25 if (planned.ok) return { mirror, view: planned.value, planRequested, planError: null };
26 return { mirror, view: unwrap(await mirrors.view(viewer, repo.id)), planRequested, planError: planned.error.message };
27 }
28 return { mirror, view: unwrap(await mirrors.view(viewer, repo.id)), planRequested, planError: null };
29}
30
31export async function action({ request, params, context }: Route.ActionArgs): Promise<MirrorOutcome> {
32 assertSameOrigin(request);
33 const user = requireUser(context, request);
34 const form = await request.formData();
35 const intent = String(form.get("intent") ?? "");
36 const failed = (error: string): MirrorOutcome => ({ intent, ok: false, message: null, error });
37 const [refused, found] = await Promise.all([
38 refusal(context, params, "manage_integrations"),
39 repos.get({ namespace: params.owner, name: params.repo }, user),
40 ]);
41 if (refused) return failed(refused);
42 if (!found.ok) return failed(found.error.message);
43 const repoId = found.value.id;
44 const done = (result: Result<MirrorView | unknown>, message: string | null): MirrorOutcome =>
45 result.ok
46 ? {
47 intent,
48 ok: true,
49 message,
50 error: null,
51 notes: (result.value as Partial<MirrorView> | null)?.notes ?? [],
52 }
53 : failed(result.error.message);
54 switch (intent) {
55 case "take-over":
56 return done(await mirrors.takeOver(user, repoId), "g1t leads now.");
57 case "ci-on":
58 return done(await mirrors.ci(user, repoId, true), "CI failover is on.");
59 case "ci-off":
60 return done(await mirrors.ci(user, repoId, false), "CI failover is off.");
61 case "sync":
62 return done(await mirrors.sync(user, repoId), "Synced.");
63 case "hand-back":
64 return done(await mirrors.handBack(user, repoId, decisionsFrom(form.entries())), "Handing back.");
65 case "move-in":
66 return done(await mirrors.moveIn(user, repoId, form.get("keepRemoteUpdated") === "on"), "Moved to g1t.");
67 case "settings": {
68 // The settings the form does not carry keep their values.
69 const view = await mirrors.view(user, repoId);
70 if (!view.ok) return failed(view.error.message);
71 const remote = view.value.remotes.find((one) => one.id === String(form.get("remoteId") ?? ""));
72 if (!remote) return failed("That remote is no longer linked to this repository.");
73 return done(await mirrors.settings(user, remote.id, mirrorSettingsFrom(form, remote.settings)), "Saved.");
74 }
75 case "add": {
76 const text = (name: string) => String(form.get(name) ?? "").trim() || null;
77 const input: MirrorAddInput = {
78 provider: form.get("provider") === "g1t" ? "g1t" : "git",
79 role: form.get("role") === "leader" ? "leader" : "follower",
80 url: text("url") ?? "",
81 username: text("username"),
82 token: text("token"),
83 };
84 const added = await mirrors.add(user, repoId, input);
85 return added.ok ? { intent, ok: true, message: `Added ${added.value.name}.`, error: null } : failed(added.error.message);
86 }
87 case "remove":
88 return done(await mirrors.remove(user, String(form.get("remoteId") ?? "")), "Removed.");
89 }
90 return failed("Unknown action.");
91}
92
93export default function Mirroring({ loaderData, actionData, params }: Route.ComponentProps) {
94 const base = `/${params.owner}/${params.repo}`;
95 return (
96 <>
97 <RepoSettingsHeading base={base} />
98 <MirroringSettings
99 base={base}
100 full={`${params.owner}/${params.repo}`}
101 mirror={loaderData.mirror}
102 view={loaderData.view}
103 planRequested={loaderData.planRequested}
104 planError={loaderData.planError}
105 result={actionData}
106 />
107 </>
108 );
109}