History

Add remote repository publish and discovery support (#2482)

Julius Marminge committed 6 months ago

Commit d7969261 parent f7748a033 files · +2502 −157Browse files

Showing the changes to apps/server/src/sourceControl/SourceControlRepositoryService.test.ts (1 of 33 files). Show all

Unified · Split

apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+322−0
@@ -0,0 +1,322 @@
1+import * as NodeServices from "@effect/platform-node/NodeServices";
2+import { assert, it } from "@effect/vitest";
3+import { Effect, FileSystem, Layer } from "effect";
4+import { ChildProcessSpawner } from "effect/unstable/process";
5+
6+import { GitCommandError, type SourceControlProviderError } from "@t3tools/contracts";
7+
8+import { ServerConfig } from "../config.ts";
9+import {
10+ GitVcsDriver,
11+ type ExecuteGitResult,
12+ type GitVcsDriverShape,
13+} from "../vcs/GitVcsDriver.ts";
14+import { SourceControlProviderRegistry } from "./SourceControlProviderRegistry.ts";
15+import type { SourceControlProviderShape } from "./SourceControlProvider.ts";
16+import { SourceControlRepositoryService, layer } from "./SourceControlRepositoryService.ts";
17+
18+const CLONE_URLS = {
19+ nameWithOwner: "octocat/t3code",
20+ url: "https://github.com/octocat/t3code",
21+ sshUrl: "git@github.com:octocat/t3code.git",
22+};
23+
24+function makeProvider(
25+ overrides: Partial<SourceControlProviderShape> = {},
26+): SourceControlProviderShape {
27+ const unsupported = (operation: string) =>
28+ Effect.die(`unexpected provider operation ${operation}`) as Effect.Effect<
29+ never,
30+ SourceControlProviderError
31+ >;
32+
33+ return {
34+ kind: "github",
35+ listChangeRequests: () => unsupported("listChangeRequests"),
36+ getChangeRequest: () => unsupported("getChangeRequest"),
37+ createChangeRequest: () => unsupported("createChangeRequest"),
38+ getRepositoryCloneUrls: () => Effect.succeed(CLONE_URLS),
39+ createRepository: () => Effect.succeed(CLONE_URLS),
40+ getDefaultBranch: () => Effect.succeed(null),
41+ checkoutChangeRequest: () => unsupported("checkoutChangeRequest"),
42+ ...overrides,
43+ };
44+}
45+
46+function processOutput(): ExecuteGitResult {
47+ return {
48+ exitCode: ChildProcessSpawner.ExitCode(0),
49+ stdout: "",
50+ stderr: "",
51+ stdoutTruncated: false,
52+ stderrTruncated: false,
53+ };
54+}
55+
56+function makeLayer(input: {
57+ readonly provider?: SourceControlProviderShape;
58+ readonly git?: Partial<GitVcsDriverShape>;
59+}) {
60+ return layer.pipe(
61+ Layer.provide(
62+ Layer.mock(SourceControlProviderRegistry)({
63+ get: () => Effect.succeed(input.provider ?? makeProvider()),
64+ }),
65+ ),
66+ Layer.provide(
67+ Layer.mock(GitVcsDriver)({
68+ execute: () => Effect.succeed(processOutput()),
69+ ensureRemote: () => Effect.succeed("origin"),
70+ pushCurrentBranch: () =>
71+ Effect.succeed({
72+ status: "pushed" as const,
73+ branch: "feature/remote-v1",
74+ upstreamBranch: "origin/feature/remote-v1",
75+ setUpstream: true,
76+ }),
77+ ...input.git,
78+ }),
79+ ),
80+ Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-repos-" })),
81+ Layer.provideMerge(NodeServices.layer),
82+ );
83+}
84+
85+it.effect("looks up repositories through the requested provider without search", () => {
86+ const calls: Array<{ cwd: string; repository: string }> = [];
87+ const provider = makeProvider({
88+ getRepositoryCloneUrls: (input) =>
89+ Effect.sync(() => {
90+ calls.push({ cwd: input.cwd, repository: input.repository });
91+ return CLONE_URLS;
92+ }),
93+ });
94+
95+ return Effect.gen(function* () {
96+ const service = yield* SourceControlRepositoryService;
97+ const result = yield* service.lookupRepository({
98+ provider: "github",
99+ repository: "octocat/t3code",
100+ cwd: "/workspace",
101+ });
102+
103+ assert.deepStrictEqual(result, { provider: "github", ...CLONE_URLS });
104+ assert.deepStrictEqual(calls, [{ cwd: "/workspace", repository: "octocat/t3code" }]);
105+ }).pipe(Effect.provide(makeLayer({ provider })));
106+});
107+
108+it.effect("clones a looked-up repository into the requested destination", () =>
109+ Effect.gen(function* () {
110+ const fs = yield* FileSystem.FileSystem;
111+ const parent = yield* fs.makeTempDirectoryScoped({
112+ prefix: "t3-source-control-clone-parent-",
113+ });
114+ const destinationPath = `${parent}/t3code`;
115+ const cloneCalls: Array<{ cwd: string; args: ReadonlyArray<string> }> = [];
116+
117+ yield* Effect.gen(function* () {
118+ const service = yield* SourceControlRepositoryService;
119+ const result = yield* service.cloneRepository({
120+ provider: "github",
121+ repository: "octocat/t3code",
122+ destinationPath,
123+ protocol: "https",
124+ });
125+
126+ assert.deepStrictEqual(result, {
127+ cwd: destinationPath,
128+ remoteUrl: CLONE_URLS.url,
129+ repository: { provider: "github", ...CLONE_URLS },
130+ });
131+ assert.deepStrictEqual(cloneCalls, [
132+ {
133+ cwd: parent,
134+ args: ["clone", CLONE_URLS.url, "t3code"],
135+ },
136+ ]);
137+ }).pipe(
138+ Effect.provide(
139+ makeLayer({
140+ git: {
141+ execute: (input) =>
142+ Effect.sync(() => {
143+ cloneCalls.push({ cwd: input.cwd, args: input.args });
144+ return processOutput();
145+ }),
146+ },
147+ }),
148+ ),
149+ );
150+ }).pipe(Effect.provide(NodeServices.layer)),
151+);
152+
153+it.effect("publishes by creating the repository, adding a remote, and pushing upstream", () => {
154+ const createCalls: Array<{ cwd: string; repository: string; visibility: string }> = [];
155+ const remoteCalls: Array<{ cwd: string; preferredName: string; url: string }> = [];
156+ const pushCalls: Array<{ cwd: string; remoteName: string | null | undefined }> = [];
157+ const provider = makeProvider({
158+ createRepository: (input) =>
159+ Effect.sync(() => {
160+ createCalls.push({
161+ cwd: input.cwd,
162+ repository: input.repository,
163+ visibility: input.visibility,
164+ });
165+ return CLONE_URLS;
166+ }),
167+ });
168+
169+ return Effect.gen(function* () {
170+ const service = yield* SourceControlRepositoryService;
171+ const result = yield* service.publishRepository({
172+ cwd: "/workspace",
173+ provider: "github",
174+ repository: "octocat/t3code",
175+ visibility: "private",
176+ remoteName: "origin",
177+ protocol: "ssh",
178+ });
179+
180+ assert.deepStrictEqual(result, {
181+ repository: { provider: "github", ...CLONE_URLS },
182+ remoteName: "origin",
183+ remoteUrl: CLONE_URLS.sshUrl,
184+ branch: "feature/remote-v1",
185+ upstreamBranch: "origin/feature/remote-v1",
186+ status: "pushed",
187+ });
188+ assert.deepStrictEqual(createCalls, [
189+ { cwd: "/workspace", repository: "octocat/t3code", visibility: "private" },
190+ ]);
191+ assert.deepStrictEqual(remoteCalls, [
192+ { cwd: "/workspace", preferredName: "origin", url: CLONE_URLS.sshUrl },
193+ ]);
194+ assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin" }]);
195+ }).pipe(
196+ Effect.provide(
197+ makeLayer({
198+ provider,
199+ git: {
200+ ensureRemote: (input) =>
201+ Effect.sync(() => {
202+ remoteCalls.push(input);
203+ return "origin";
204+ }),
205+ pushCurrentBranch: (cwd, _fallbackBranch, options) =>
206+ Effect.sync(() => {
207+ pushCalls.push({ cwd, remoteName: options?.remoteName });
208+ return {
209+ status: "pushed" as const,
210+ branch: "feature/remote-v1",
211+ upstreamBranch: "origin/feature/remote-v1",
212+ setUpstream: true,
213+ };
214+ }),
215+ },
216+ }),
217+ ),
218+ );
219+});
220+
221+it.effect("publishes to the remote name returned by ensureRemote", () => {
222+ const pushCalls: Array<{ cwd: string; remoteName: string | null | undefined }> = [];
223+
224+ return Effect.gen(function* () {
225+ const service = yield* SourceControlRepositoryService;
226+ const result = yield* service.publishRepository({
227+ cwd: "/workspace",
228+ provider: "github",
229+ repository: "octocat/t3code",
230+ visibility: "private",
231+ remoteName: "origin",
232+ protocol: "ssh",
233+ });
234+
235+ assert.equal(result.remoteName, "origin-1");
236+ assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin-1" }]);
237+ }).pipe(
238+ Effect.provide(
239+ makeLayer({
240+ git: {
241+ ensureRemote: () => Effect.succeed("origin-1"),
242+ pushCurrentBranch: (cwd, _fallbackBranch, options) =>
243+ Effect.sync(() => {
244+ pushCalls.push({ cwd, remoteName: options?.remoteName });
245+ return {
246+ status: "pushed" as const,
247+ branch: "feature/remote-v1",
248+ upstreamBranch: `${options?.remoteName ?? "missing"}/feature/remote-v1`,
249+ setUpstream: true,
250+ };
251+ }),
252+ },
253+ }),
254+ ),
255+ );
256+});
257+
258+it.effect("publish succeeds with status remote_added when the local repo has no commits", () => {
259+ let pushCalls = 0;
260+ return Effect.gen(function* () {
261+ const service = yield* SourceControlRepositoryService;
262+ const result = yield* service.publishRepository({
263+ cwd: "/workspace",
264+ provider: "github",
265+ repository: "octocat/t3code",
266+ visibility: "private",
267+ remoteName: "origin",
268+ protocol: "ssh",
269+ });
270+
271+ assert.deepStrictEqual(result, {
272+ repository: { provider: "github", ...CLONE_URLS },
273+ remoteName: "origin",
274+ remoteUrl: CLONE_URLS.sshUrl,
275+ branch: "main",
276+ status: "remote_added",
277+ });
278+ assert.strictEqual(pushCalls, 0);
279+ }).pipe(
280+ Effect.provide(
281+ makeLayer({
282+ git: {
283+ execute: (input) =>
284+ input.args[0] === "rev-parse"
285+ ? Effect.fail(
286+ new GitCommandError({
287+ operation: input.operation,
288+ command: "git rev-parse --verify HEAD",
289+ cwd: input.cwd,
290+ detail: "fatal: Needed a single revision",
291+ }),
292+ )
293+ : Effect.succeed(processOutput()),
294+ statusDetails: () =>
295+ Effect.succeed({
296+ isRepo: true,
297+ hasOriginRemote: true,
298+ isDefaultBranch: true,
299+ branch: "main",
300+ upstreamRef: null,
301+ hasWorkingTreeChanges: false,
302+ workingTree: { files: [], insertions: 0, deletions: 0 },
303+ hasUpstream: false,
304+ aheadCount: 0,
305+ behindCount: 0,
306+ aheadOfDefaultCount: 0,
307+ }),
308+ pushCurrentBranch: () =>
309+ Effect.sync(() => {
310+ pushCalls += 1;
311+ return {
312+ status: "pushed" as const,
313+ branch: "main",
314+ upstreamBranch: "origin/main",
315+ setUpstream: true,
316+ };
317+ }),
318+ },
319+ }),
320+ ),
321+ );
322+});