1import { SourceControlProviderKind } from "@t3tools/contracts";
2import * as NodePath from "@effect/platform-node/NodePath";
3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { assert, it } from "@effect/vitest";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Path from "effect/Path";
8import * as Layer from "effect/Layer";
9import * as PlatformError from "effect/PlatformError";
10import * as Schema from "effect/Schema";
11import { ChildProcessSpawner } from "effect/process";
12
13import { GitCommandError, SourceControlProviderError } from "@t3tools/contracts";
14
15import * as ServerConfig from "../config.ts";
16import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
17import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi";
18import * as BitbucketSourceControlProvider from "@t3tools/source-control-bitbucket/server/BitbucketSourceControlProvider";
19import type * as SourceControlProvider from "@t3tools/source-control-core/server/SourceControlProvider";
20import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
21import * as SourceControlRepositoryService from "./SourceControlRepositoryService.ts";
22
23const isSourceControlProviderError = Schema.is(SourceControlProviderError);
24
25const CLONE_URLS = {
26 nameWithOwner: "octocat/t3code",
27 url: "https://github.com/octocat/t3code",
28 sshUrl: "git@github.com:octocat/t3code.git",
29};
30
31function makeProvider(
32 overrides: Partial<SourceControlProvider.SourceControlProvider["Service"]> = {},
33): SourceControlProvider.SourceControlProvider["Service"] {
34 const unsupported = (operation: string) =>
35 Effect.die(`unexpected provider operation ${operation}`) as Effect.Effect<
36 never,
37 SourceControlProviderError
38 >;
39
40 return {
41 kind: SourceControlProviderKind.make("github"),
42 listChangeRequests: () => unsupported("listChangeRequests"),
43 getChangeRequest: () => unsupported("getChangeRequest"),
44 createChangeRequest: () => unsupported("createChangeRequest"),
45 getRepositoryCloneUrls: () => Effect.succeed(CLONE_URLS),
46 createRepository: () => Effect.succeed(CLONE_URLS),
47 getDefaultBranch: () => Effect.succeed(null),
48 checkoutChangeRequest: () => unsupported("checkoutChangeRequest"),
49 ...overrides,
50 };
51}
52
53function processOutput(): GitVcsDriver.ExecuteGitResult {
54 return {
55 exitCode: ChildProcessSpawner.ExitCode(0),
56 stdout: "",
57 stderr: "",
58 stdoutTruncated: false,
59 stderrTruncated: false,
60 };
61}
62
63function layer(input: {
64 readonly provider?: SourceControlProvider.SourceControlProvider["Service"];
65 readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;
66 readonly fileSystem?: FileSystem.FileSystem;
67}) {
68 const layerService = SourceControlRepositoryService.layer.pipe(
69 Layer.provide(
70 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
71 resolveLink: () => undefined,
72 get: () => Effect.succeed(input.provider ?? makeProvider()),
73 }),
74 ),
75 Layer.provide(
76 Layer.mock(GitVcsDriver.GitVcsDriver)({
77 execute: () => Effect.succeed(processOutput()),
78 ensureRemote: () => Effect.succeed("origin"),
79 pushCurrentBranch: () =>
80 Effect.succeed({
81 status: "pushed" as const,
82 branch: "feature/remote-v1",
83 upstreamBranch: "origin/feature/remote-v1",
84 setUpstream: true,
85 }),
86 ...input.git,
87 }),
88 ),
89 Layer.provide(
90 ServerConfig.layerTest(
91 process.cwd(),
92 input.fileSystem ? "/tmp/t3-source-control-repos" : { prefix: "t3-source-control-repos-" },
93 ),
94 ),
95 );
96
97 return input.fileSystem
98 ? layerService.pipe(
99 Layer.provide(Layer.succeed(FileSystem.FileSystem, input.fileSystem)),
100 Layer.provideMerge(NodePath.layer),
101 )
102 : layerService.pipe(Layer.provideMerge(NodeServices.layer));
103}
104
105it.effect("looks up repositories through the requested provider without search", () => {
106 const calls: Array<{ cwd: string; repository: string }> = [];
107 const provider = makeProvider({
108 getRepositoryCloneUrls: (input) =>
109 Effect.sync(() => {
110 calls.push({ cwd: input.cwd, repository: input.repository });
111 return CLONE_URLS;
112 }),
113 });
114
115 return Effect.gen(function* () {
116 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
117 const result = yield* service.lookupRepository({
118 provider: SourceControlProviderKind.make("github"),
119 repository: "octocat/t3code",
120 cwd: "/workspace",
121 });
122
123 assert.deepStrictEqual(result, {
124 provider: SourceControlProviderKind.make("github"),
125 ...CLONE_URLS,
126 });
127 assert.deepStrictEqual(calls, [{ cwd: "/workspace", repository: "octocat/t3code" }]);
128 }).pipe(Effect.provide(layer({ provider })));
129});
130
131it.effect("preserves provider failures without deriving the repository message from them", () => {
132 const providerCause = new SourceControlProviderError({
133 provider: SourceControlProviderKind.make("github"),
134 operation: "getRepositoryCloneUrls",
135 cwd: "/workspace",
136 repository: "octocat/t3code",
137 detail: "credential token abc123 was rejected",
138 });
139 const provider = makeProvider({
140 getRepositoryCloneUrls: () => Effect.fail(providerCause),
141 });
142
143 return Effect.gen(function* () {
144 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
145 const error = yield* Effect.flip(
146 service.lookupRepository({
147 provider: SourceControlProviderKind.make("github"),
148 repository: "octocat/t3code",
149 cwd: "/workspace",
150 }),
151 );
152
153 assert.strictEqual(error.provider, "github");
154 assert.strictEqual(error.operation, "lookupRepository");
155 assert.strictEqual(error.detail, "The source control operation could not be completed.");
156 assert.strictEqual(
157 error.message,
158 "Source control repository operation lookupRepository failed for github: The source control operation could not be completed.",
159 );
160 assert.strictEqual(error.cause, providerCause);
161 }).pipe(Effect.provide(layer({ provider })));
162});
163
164it.effect(
165 "reports the safe Bitbucket locator detail through repository lookup and publishing",
166 () =>
167 Effect.gen(function* () {
168 const locatorCause = new BitbucketApi.BitbucketRepositoryLocatorError({
169 repository: "credential-token-abc123",
170 });
171 const provider = yield* BitbucketSourceControlProvider.make.pipe(
172 Effect.provide(
173 Layer.mock(BitbucketApi.BitbucketApi)({
174 getRepositoryCloneUrls: () => Effect.fail(locatorCause),
175 createRepository: () => Effect.fail(locatorCause),
176 }),
177 ),
178 );
179
180 yield* Effect.gen(function* () {
181 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
182 const input = {
183 provider: SourceControlProviderKind.make("bitbucket"),
184 repository: locatorCause.repository,
185 cwd: "/workspace",
186 };
187 for (const operation of [
188 service.lookupRepository(input).pipe(Effect.asVoid),
189 service.publishRepository({ ...input, visibility: "private" }).pipe(Effect.asVoid),
190 ]) {
191 const error = yield* Effect.flip(operation);
192
193 assert.strictEqual(error.provider, "bitbucket");
194 assert.strictEqual(
195 error.detail,
196 "Bitbucket repositories must be specified as workspace/repository.",
197 );
198 assert.notInclude(error.message, locatorCause.repository);
199 assert.instanceOf(error.cause, SourceControlProviderError);
200 assert.strictEqual(
201 isSourceControlProviderError(error.cause) ? error.cause.cause : null,
202 locatorCause,
203 );
204 }
205 }).pipe(Effect.provide(layer({ provider })));
206 }),
207);
208
209it.effect("keeps a plain Bitbucket locator lookalike out of the repository message", () => {
210 const provider = makeProvider({
211 kind: SourceControlProviderKind.make("bitbucket"),
212 getRepositoryCloneUrls: () =>
213 Effect.fail(
214 new SourceControlProviderError({
215 provider: SourceControlProviderKind.make("bitbucket"),
216 operation: "getRepositoryCloneUrls",
217 cwd: "/workspace",
218 detail: "credential token abc123 was rejected",
219 cause: {
220 _tag: "BitbucketRepositoryLocatorError",
221 repository: "t3code",
222 detail: "credential token abc123 was rejected",
223 },
224 }),
225 ),
226 });
227
228 return Effect.gen(function* () {
229 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
230 const error = yield* Effect.flip(
231 service.lookupRepository({
232 provider: SourceControlProviderKind.make("bitbucket"),
233 repository: "t3code",
234 }),
235 );
236
237 assert.strictEqual(error.detail, "The source control operation could not be completed.");
238 assert.notInclude(error.message, "abc123");
239 }).pipe(Effect.provide(layer({ provider })));
240});
241
242it.effect("keeps arbitrary Bitbucket request failures out of the repository message", () =>
243 Effect.gen(function* () {
244 const cause = new BitbucketApi.BitbucketRequestError({
245 operation: "getRepository",
246 cause: new Error("credential token abc123 was rejected"),
247 });
248 const provider = yield* BitbucketSourceControlProvider.make.pipe(
249 Effect.provide(
250 Layer.mock(BitbucketApi.BitbucketApi)({
251 getRepositoryCloneUrls: () => Effect.fail(cause),
252 }),
253 ),
254 );
255
256 yield* Effect.gen(function* () {
257 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
258 const error = yield* Effect.flip(
259 service.lookupRepository({
260 provider: SourceControlProviderKind.make("bitbucket"),
261 repository: "pingdotgg/t3code",
262 cwd: "/workspace",
263 }),
264 );
265
266 assert.strictEqual(error.detail, "The source control operation could not be completed.");
267 assert.notInclude(error.message, "abc123");
268 assert.notInclude(error.message, cause.detail);
269 }).pipe(Effect.provide(layer({ provider })));
270 }),
271);
272
273it.effect("clones a looked-up repository into the requested destination", () =>
274 Effect.gen(function* () {
275 const fs = yield* FileSystem.FileSystem;
276 const path = yield* Path.Path;
277 const parent = yield* fs.makeTempDirectoryScoped({
278 prefix: "t3-source-control-clone-parent-",
279 });
280 const destinationPath = path.join(parent, "t3code");
281 const cloneCalls: Array<{ cwd: string; args: ReadonlyArray<string> }> = [];
282
283 yield* Effect.gen(function* () {
284 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
285 const result = yield* service.cloneRepository({
286 provider: SourceControlProviderKind.make("github"),
287 repository: "octocat/t3code",
288 destinationPath,
289 protocol: "https",
290 });
291
292 assert.deepStrictEqual(result, {
293 cwd: destinationPath,
294 remoteUrl: CLONE_URLS.url,
295 repository: { provider: SourceControlProviderKind.make("github"), ...CLONE_URLS },
296 });
297 assert.deepStrictEqual(cloneCalls, [
298 {
299 cwd: parent,
300 args: ["clone", "--progress", "--", CLONE_URLS.url, "t3code"],
301 },
302 ]);
303 }).pipe(
304 Effect.provide(
305 layer({
306 git: {
307 execute: (input) =>
308 Effect.sync(() => {
309 cloneCalls.push({ cwd: input.cwd, args: input.args });
310 return processOutput();
311 }),
312 },
313 }),
314 ),
315 );
316 }).pipe(Effect.provide(NodeServices.layer)),
317);
318
319it.effect.each(["--bare", "source.git"])(
320 "clones a local repository named %s as a working tree",
321 (repositoryName) =>
322 Effect.gen(function* () {
323 const fs = yield* FileSystem.FileSystem;
324 const path = yield* Path.Path;
325 const git = yield* GitVcsDriver.GitVcsDriver;
326 const parent = yield* fs.makeTempDirectoryScoped({ prefix: "t3-clone-options-" });
327 yield* git.execute({
328 operation: "test.init",
329 cwd: parent,
330 args: ["init", "--bare", path.join(parent, repositoryName)],
331 });
332 const destinationPath = path.join(parent, "checkout");
333 yield* Effect.gen(function* () {
334 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
335 const result = yield* service.cloneRepository({
336 remoteUrl: repositoryName,
337 destinationPath,
338 });
339 assert.strictEqual(result.cwd, destinationPath);
340 assert.isTrue(yield* fs.exists(path.join(destinationPath, ".git")));
341 }).pipe(Effect.provide(layer({ git: { execute: git.execute } })));
342 }).pipe(
343 Effect.provide(
344 GitVcsDriver.layer.pipe(
345 Layer.provide(
346 ServerConfig.layerTest(process.cwd(), {
347 prefix: "t3-clone-options-config-",
348 }),
349 ),
350 Layer.provideMerge(NodeServices.layer),
351 ),
352 ),
353 ),
354);
355
356it.effect("reports clone progress from git's stderr and keeps its error text on failure", () =>
357 Effect.gen(function* () {
358 const fs = yield* FileSystem.FileSystem;
359 const path = yield* Path.Path;
360 const parent = yield* fs.makeTempDirectoryScoped({
361 prefix: "t3-source-control-clone-progress-",
362 });
363 const destinationPath = path.join(parent, "t3code");
364 const progress: Array<{ stage: string; percent: number | null; detail: string | null }> = [];
365
366 const stderrLines = [
367 "Cloning into 't3code'...",
368 "remote: Enumerating objects: 10, done.",
369 "Receiving objects: 40% (4/10), 1.00 MiB | 2.00 MiB/s",
370 "Receiving objects: 100% (10/10), 2.50 MiB | 2.00 MiB/s, done.",
371 "fatal: early EOF",
372 "fatal: unable to access 'https://user:s3c@ret@github.com/octocat/t3code.git/': could not resolve host",
373 ];
374 const error = yield* Effect.gen(function* () {
375 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
376 return yield* Effect.flip(
377 service.cloneRepository(
378 { remoteUrl: CLONE_URLS.sshUrl, destinationPath },
379 { onProgress: (line) => Effect.sync(() => void progress.push(line)) },
380 ),
381 );
382 }).pipe(
383 Effect.provide(
384 layer({
385 git: {
386 execute: (input) =>
387 Effect.gen(function* () {
388 for (const line of stderrLines) {
389 yield* input.progress?.onStderrLine?.(line) ?? Effect.void;
390 }
391 return yield* new GitCommandError({
392 operation: input.operation,
393 command: "git",
394 cwd: input.cwd,
395 detail: "Git command exited with a non-zero status.",
396 exitCode: 128,
397 });
398 }),
399 },
400 }),
401 ),
402 );
403
404 assert.deepStrictEqual(progress, [
405 { stage: "counting", percent: null, detail: null },
406 { stage: "receiving", percent: 40, detail: "1.00 MiB | 2.00 MiB/s" },
407 { stage: "receiving", percent: 100, detail: "2.50 MiB | 2.00 MiB/s" },
408 ]);
409 // Git echoes the remote in some failures; the credentials must not follow.
410 assert.strictEqual(
411 error.detail,
412 "fatal: early EOF fatal: unable to access 'https://github.com/octocat/t3code.git/': could not resolve host",
413 );
414 }).pipe(Effect.provide(NodeServices.layer)),
415);
416
417it.effect("strips embedded credentials from the remote URL it reports", () =>
418 Effect.gen(function* () {
419 const fs = yield* FileSystem.FileSystem;
420 const path = yield* Path.Path;
421 const parent = yield* fs.makeTempDirectoryScoped({ prefix: "t3-source-control-redact-" });
422 const destinationPath = path.join(parent, "t3code");
423 const cloneArgs: Array<ReadonlyArray<string>> = [];
424 const result = yield* Effect.gen(function* () {
425 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
426 return yield* service.prepareClone({
427 remoteUrl: "https://user:s3cret@github.com/octocat/t3code.git",
428 destinationPath,
429 });
430 }).pipe(
431 Effect.provide(
432 layer({
433 git: {
434 execute: (input) =>
435 Effect.sync(() => {
436 cloneArgs.push(input.args);
437 return processOutput();
438 }),
439 },
440 }),
441 ),
442 );
443 assert.equal(result.remoteUrl, "https://github.com/octocat/t3code.git");
444 // Git itself still receives the credentials.
445 assert.equal(result.cloneUrl, "https://user:s3cret@github.com/octocat/t3code.git");
446 }).pipe(Effect.provide(NodeServices.layer)),
447);
448
449it.effect("discards only a directory git wrote to", () =>
450 Effect.gen(function* () {
451 const fs = yield* FileSystem.FileSystem;
452 const path = yield* Path.Path;
453 const parent = yield* fs.makeTempDirectoryScoped({ prefix: "t3-source-control-discard-" });
454 const partial = path.join(parent, "partial");
455 yield* fs.makeDirectory(path.join(partial, ".git"), { recursive: true });
456 yield* fs.writeFileString(path.join(partial, "README.md"), "half");
457 const foreign = path.join(parent, "foreign");
458 yield* fs.makeDirectory(foreign);
459 yield* fs.writeFileString(path.join(foreign, "notes.txt"), "mine");
460
461 // A file where the directory should be must not be removed either.
462 const replaced = path.join(parent, "replaced");
463 yield* fs.writeFileString(replaced, "not a directory");
464
465 yield* Effect.gen(function* () {
466 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
467 yield* service.discardClone(partial);
468 const error = yield* Effect.flip(service.discardClone(foreign));
469 assert.include(error.detail, "not from the clone");
470 const replacedError = yield* Effect.flip(service.discardClone(replaced));
471 assert.include(replacedError.detail, "could not be inspected");
472 // A destination that never got created is nothing to discard.
473 yield* service.discardClone(path.join(parent, "missing"));
474 }).pipe(Effect.provide(layer({})));
475
476 // The partial clone is emptied but its directory (the workspace root) stays.
477 assert.deepStrictEqual(yield* fs.readDirectory(partial), []);
478 assert.deepStrictEqual(yield* fs.readDirectory(foreign), ["notes.txt"]);
479 assert.strictEqual(yield* fs.readFileString(replaced), "not a directory");
480 }).pipe(Effect.provide(NodeServices.layer)),
481);
482
483it.effect("redacts query tokens and userinfo containing '@' from reported URLs", () =>
484 Effect.gen(function* () {
485 const fs = yield* FileSystem.FileSystem;
486 const path = yield* Path.Path;
487 const parent = yield* fs.makeTempDirectoryScoped({ prefix: "t3-source-control-redact2-" });
488 yield* Effect.gen(function* () {
489 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
490 const query = yield* service.prepareClone({
491 remoteUrl: "https://github.com/octocat/t3code.git?access_token=s3cret",
492 destinationPath: path.join(parent, "a"),
493 });
494 assert.equal(query.remoteUrl, "https://github.com/octocat/t3code.git");
495 const nested = yield* service.prepareClone({
496 remoteUrl: "https://user:pa@rt@github.com/octocat/t3code.git",
497 destinationPath: path.join(parent, "b"),
498 });
499 assert.equal(nested.remoteUrl, "https://github.com/octocat/t3code.git");
500 }).pipe(Effect.provide(layer({})));
501 }).pipe(Effect.provide(NodeServices.layer)),
502);
503
504it.effect("preserves destination probe failures instead of treating them as missing paths", () => {
505 const fileSystemCause = PlatformError.systemError({
506 _tag: "PermissionDenied",
507 module: "FileSystem",
508 method: "exists",
509 pathOrDescriptor: "/restricted/t3code",
510 });
511
512 return Effect.gen(function* () {
513 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
514 const error = yield* Effect.flip(
515 service.cloneRepository({
516 remoteUrl: CLONE_URLS.sshUrl,
517 destinationPath: "/restricted/t3code",
518 }),
519 );
520
521 assert.strictEqual(error.provider, "unknown");
522 assert.strictEqual(error.operation, "cloneRepository");
523 assert.strictEqual(error.cause, fileSystemCause);
524 }).pipe(
525 Effect.provide(
526 layer({
527 fileSystem: FileSystem.makeNoop({
528 exists: () => Effect.fail(fileSystemCause),
529 makeDirectory: () => Effect.void,
530 }),
531 }),
532 ),
533 );
534});
535
536it.effect("publishes by creating the repository, adding a remote, and pushing upstream", () => {
537 const createCalls: Array<{ cwd: string; repository: string; visibility: string }> = [];
538 const remoteCalls: Array<{ cwd: string; preferredName: string; url: string }> = [];
539 const pushCalls: Array<{ cwd: string; remoteName: string | null | undefined }> = [];
540 const provider = makeProvider({
541 createRepository: (input) =>
542 Effect.sync(() => {
543 createCalls.push({
544 cwd: input.cwd,
545 repository: input.repository,
546 visibility: input.visibility,
547 });
548 return CLONE_URLS;
549 }),
550 });
551
552 return Effect.gen(function* () {
553 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
554 const result = yield* service.publishRepository({
555 cwd: "/workspace",
556 provider: SourceControlProviderKind.make("github"),
557 repository: "octocat/t3code",
558 visibility: "private",
559 remoteName: "origin",
560 protocol: "ssh",
561 });
562
563 assert.deepStrictEqual(result, {
564 repository: { provider: SourceControlProviderKind.make("github"), ...CLONE_URLS },
565 remoteName: "origin",
566 remoteUrl: CLONE_URLS.sshUrl,
567 branch: "feature/remote-v1",
568 upstreamBranch: "origin/feature/remote-v1",
569 status: "pushed",
570 });
571 assert.deepStrictEqual(createCalls, [
572 { cwd: "/workspace", repository: "octocat/t3code", visibility: "private" },
573 ]);
574 assert.deepStrictEqual(remoteCalls, [
575 { cwd: "/workspace", preferredName: "origin", url: CLONE_URLS.sshUrl },
576 ]);
577 assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin" }]);
578 }).pipe(
579 Effect.provide(
580 layer({
581 provider,
582 git: {
583 ensureRemote: (input) =>
584 Effect.sync(() => {
585 remoteCalls.push(input);
586 return "origin";
587 }),
588 pushCurrentBranch: (cwd, _fallbackBranch, options) =>
589 Effect.sync(() => {
590 pushCalls.push({ cwd, remoteName: options?.remoteName });
591 return {
592 status: "pushed" as const,
593 branch: "feature/remote-v1",
594 upstreamBranch: "origin/feature/remote-v1",
595 setUpstream: true,
596 };
597 }),
598 },
599 }),
600 ),
601 );
602});
603
604it.effect("publishes to the remote name returned by ensureRemote", () => {
605 const pushCalls: Array<{ cwd: string; remoteName: string | null | undefined }> = [];
606
607 return Effect.gen(function* () {
608 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
609 const result = yield* service.publishRepository({
610 cwd: "/workspace",
611 provider: SourceControlProviderKind.make("github"),
612 repository: "octocat/t3code",
613 visibility: "private",
614 remoteName: "origin",
615 protocol: "ssh",
616 });
617
618 assert.equal(result.remoteName, "origin-1");
619 assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin-1" }]);
620 }).pipe(
621 Effect.provide(
622 layer({
623 git: {
624 ensureRemote: () => Effect.succeed("origin-1"),
625 pushCurrentBranch: (cwd, _fallbackBranch, options) =>
626 Effect.sync(() => {
627 pushCalls.push({ cwd, remoteName: options?.remoteName });
628 return {
629 status: "pushed" as const,
630 branch: "feature/remote-v1",
631 upstreamBranch: `${options?.remoteName ?? "missing"}/feature/remote-v1`,
632 setUpstream: true,
633 };
634 }),
635 },
636 }),
637 ),
638 );
639});
640
641it.effect("publish succeeds with status remote_added when the local repo has no commits", () => {
642 let pushCalls = 0;
643 return Effect.gen(function* () {
644 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
645 const result = yield* service.publishRepository({
646 cwd: "/workspace",
647 provider: SourceControlProviderKind.make("github"),
648 repository: "octocat/t3code",
649 visibility: "private",
650 remoteName: "origin",
651 protocol: "ssh",
652 });
653
654 assert.deepStrictEqual(result, {
655 repository: { provider: SourceControlProviderKind.make("github"), ...CLONE_URLS },
656 remoteName: "origin",
657 remoteUrl: CLONE_URLS.sshUrl,
658 branch: "main",
659 status: "remote_added",
660 });
661 assert.strictEqual(pushCalls, 0);
662 }).pipe(
663 Effect.provide(
664 layer({
665 git: {
666 execute: (input) =>
667 input.args[0] === "rev-parse"
668 ? Effect.fail(
669 new GitCommandError({
670 operation: input.operation,
671 command: "git rev-parse --verify HEAD",
672 cwd: input.cwd,
673 detail: "fatal: Needed a single revision",
674 }),
675 )
676 : Effect.succeed(processOutput()),
677 statusDetails: () =>
678 Effect.succeed({
679 isRepo: true,
680 hasOriginRemote: true,
681 isDefaultBranch: true,
682 branch: "main",
683 upstreamRef: null,
684 hasWorkingTreeChanges: false,
685 workingTree: { files: [], insertions: 0, deletions: 0 },
686 hasUpstream: false,
687 aheadCount: 0,
688 behindCount: 0,
689 aheadOfDefaultCount: 0,
690 }),
691 pushCurrentBranch: () =>
692 Effect.sync(() => {
693 pushCalls += 1;
694 return {
695 status: "pushed" as const,
696 branch: "main",
697 upstreamBranch: "origin/main",
698 setUpstream: true,
699 };
700 }),
701 },
702 }),
703 ),
704 );
705});