History

Add source control settings discovery

Julius Marminge committed 6 months ago

Commit dc2e2651 parent 128b43214 files · +644 −3Browse files

Showing the changes to apps/server/src/sourceControl/SourceControlDiscovery.ts (1 of 14 files). Show all

Unified · Split

apps/server/src/sourceControl/SourceControlDiscovery.ts+178−0
@@ -0,0 +1,178 @@
1import {
2 type SourceControlDiscoveryResult,
3 type SourceControlProviderDiscoveryItem,
4 type SourceControlProviderKind,
5 type VcsDiscoveryItem,
6 type VcsDriverKind,
7} from "@t3tools/contracts";
8import { Context, Effect, Layer, Option } from "effect";
9
10import { ServerConfig } from "../config.ts";
11import * as VcsProcess from "../vcs/VcsProcess.ts";
12
13interface DiscoveryProbe {
14 readonly label: string;
15 readonly executable: string;
16 readonly versionArgs: ReadonlyArray<string>;
17 readonly implemented: boolean;
18 readonly installHint: string;
19}
20
21type VcsProbe = DiscoveryProbe & {
22 readonly kind: VcsDriverKind;
23};
24
25type ProviderProbe = DiscoveryProbe & {
26 readonly kind: SourceControlProviderKind;
27};
28
29const VCS_PROBES: ReadonlyArray<VcsProbe> = [
30 {
31 kind: "git",
32 label: "Git",
33 executable: "git",
34 versionArgs: ["--version"],
35 implemented: true,
36 installHint: "Install Git from https://git-scm.com/downloads or with your package manager.",
37 },
38 {
39 kind: "jj",
40 label: "Jujutsu",
41 executable: "jj",
42 versionArgs: ["--version"],
43 implemented: false,
44 installHint: "Install Jujutsu with `brew install jj` or from https://github.com/jj-vcs/jj.",
45 },
46 {
47 kind: "sapling",
48 label: "Sapling",
49 executable: "sl",
50 versionArgs: ["--version"],
51 implemented: false,
52 installHint: "Install Sapling (`sl`) from https://sapling-scm.com/.",
53 },
54];
55
56const SOURCE_CONTROL_PROVIDER_PROBES: ReadonlyArray<ProviderProbe> = [
57 {
58 kind: "github",
59 label: "GitHub",
60 executable: "gh",
61 versionArgs: ["--version"],
62 implemented: true,
63 installHint: "Install GitHub CLI with `brew install gh` or from https://cli.github.com/.",
64 },
65 {
66 kind: "gitlab",
67 label: "GitLab",
68 executable: "glab",
69 versionArgs: ["--version"],
70 implemented: false,
71 installHint:
72 "Install GitLab CLI with `brew install glab` or from https://gitlab.com/gitlab-org/cli.",
73 },
74 {
75 kind: "azure-devops",
76 label: "Azure DevOps",
77 executable: "az",
78 versionArgs: ["--version"],
79 implemented: false,
80 installHint:
81 "Install Azure CLI with `brew install azure-cli`, then add Azure DevOps support with `az extension add --name azure-devops`.",
82 },
83 {
84 kind: "bitbucket",
85 label: "Bitbucket",
86 executable: "bb",
87 versionArgs: ["--version"],
88 implemented: false,
89 installHint: "Install a Bitbucket CLI (`bb`) and authenticate it for your Bitbucket workspace.",
90 },
91];
92
93function firstNonEmptyLine(text: string): Option.Option<string> {
94 const line = text
95 .split(/\r?\n/)
96 .map((entry) => entry.trim())
97 .find((entry) => entry.length > 0);
98 return line === undefined ? Option.none() : Option.some(line);
99}
100
101function detailFromCause(cause: unknown): Option.Option<string> {
102 if (cause instanceof Error && cause.message.trim().length > 0) {
103 return Option.some(cause.message.trim());
104 }
105 return Option.none();
106}
107
108export interface SourceControlDiscoveryShape {
109 readonly discover: Effect.Effect<SourceControlDiscoveryResult>;
110}
111
112export class SourceControlDiscovery extends Context.Service<
113 SourceControlDiscovery,
114 SourceControlDiscoveryShape
115>()("t3/source-control/SourceControlDiscovery") {}
116
117export const layer = Layer.effect(
118 SourceControlDiscovery,
119 Effect.gen(function* () {
120 const config = yield* ServerConfig;
121 const process = yield* VcsProcess.VcsProcess;
122
123 const probe = <Kind extends VcsDriverKind | SourceControlProviderKind>(
124 input: DiscoveryProbe & { readonly kind: Kind },
125 ) =>
126 process
127 .run({
128 operation: "source-control.discovery.probe",
129 command: input.executable,
130 args: input.versionArgs,
131 cwd: config.cwd,
132 timeoutMs: 5_000,
133 maxOutputBytes: 8_000,
134 truncateOutputAtMaxBytes: true,
135 })
136 .pipe(
137 Effect.map((result) => ({
138 kind: input.kind,
139 label: input.label,
140 executable: input.executable,
141 implemented: input.implemented,
142 status: "available" as const,
143 version: Option.orElse(firstNonEmptyLine(result.stdout), () =>
144 firstNonEmptyLine(result.stderr),
145 ),
146 installHint: input.installHint,
147 detail: Option.none<string>(),
148 })),
149 Effect.catch((cause) =>
150 Effect.succeed({
151 kind: input.kind,
152 label: input.label,
153 executable: input.executable,
154 implemented: input.implemented,
155 status: "missing" as const,
156 version: Option.none<string>(),
157 installHint: input.installHint,
158 detail: detailFromCause(cause),
159 }),
160 ),
161 );
162
163 return SourceControlDiscovery.of({
164 discover: Effect.all({
165 versionControlSystems: Effect.all(
166 VCS_PROBES.map((entry) => probe(entry)) as ReadonlyArray<Effect.Effect<VcsDiscoveryItem>>,
167 { concurrency: "unbounded" },
168 ),
169 sourceControlProviders: Effect.all(
170 SOURCE_CONTROL_PROVIDER_PROBES.map((entry) => probe(entry)) as ReadonlyArray<
171 Effect.Effect<SourceControlProviderDiscoveryItem>
172 >,
173 { concurrency: "unbounded" },
174 ),
175 }),
176 });
177 }),
178);

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t