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 (2 of 14 files). Show all

Unified · Split

2 files
  1. apps/server/src/sourceControl/SourceControlDiscovery.test.ts +77 −0
  2. apps/server/src/sourceControl/SourceControlDiscovery.ts +178 −0
apps/server/src/sourceControl/SourceControlDiscovery.test.ts+77−0
@@ -0,0 +1,77 @@
1import { assert, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { Effect, Layer } from "effect";
4import { ChildProcessSpawner } from "effect/unstable/process";
5import { VcsProcessSpawnError } from "@t3tools/contracts";
6
7import { ServerConfig } from "../config.ts";
8import * as VcsProcess from "../vcs/VcsProcess.ts";
9import { SourceControlDiscovery, layer } from "./SourceControlDiscovery.ts";
10
11const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({
12 exitCode: ChildProcessSpawner.ExitCode(0),
13 stdout,
14 stderr: "",
15 stdoutTruncated: false,
16 stderrTruncated: false,
17});
18
19it.effect("reports implemented tools separately from locally available CLIs", () => {
20 const testLayer = layer.pipe(
21 Layer.provide(
22 ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-discovery-" }),
23 ),
24 Layer.provide(
25 Layer.mock(VcsProcess.VcsProcess)({
26 run: (input) => {
27 if (input.command === "git") {
28 return Effect.succeed(processOutput("git version 2.51.0\n"));
29 }
30 if (input.command === "gh") {
31 return Effect.succeed(processOutput("gh version 2.83.0\n"));
32 }
33 return Effect.fail(
34 new VcsProcessSpawnError({
35 operation: input.operation,
36 command: input.command,
37 cwd: input.cwd,
38 cause: new Error(`${input.command} not found`),
39 }),
40 );
41 },
42 }),
43 ),
44 Layer.provideMerge(NodeServices.layer),
45 );
46
47 return Effect.gen(function* () {
48 const discovery = yield* SourceControlDiscovery;
49 const result = yield* discovery.discover;
50
51 assert.deepStrictEqual(
52 result.versionControlSystems.map((item) => ({
53 kind: item.kind,
54 implemented: item.implemented,
55 status: item.status,
56 })),
57 [
58 { kind: "git", implemented: true, status: "available" },
59 { kind: "jj", implemented: false, status: "missing" },
60 { kind: "sapling", implemented: false, status: "missing" },
61 ],
62 );
63 assert.deepStrictEqual(
64 result.sourceControlProviders.map((item) => ({
65 kind: item.kind,
66 implemented: item.implemented,
67 status: item.status,
68 })),
69 [
70 { kind: "github", implemented: true, status: "available" },
71 { kind: "gitlab", implemented: false, status: "missing" },
72 { kind: "azure-devops", implemented: false, status: "missing" },
73 { kind: "bitbucket", implemented: false, status: "missing" },
74 ],
75 );
76 }).pipe(Effect.provide(testLayer));
77});
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