392 lines · 13.9 KB · TypeScript
RawDownload
import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as Schema from "effect/Schema"; import {  SourceControlRepositoryError,  type SourceControlCloneRepositoryInput,  type SourceControlCloneRepositoryResult,  type SourceControlCloneProtocol,  type SourceControlProviderKind,  type SourceControlPublishRepositoryInput,  type SourceControlPublishRepositoryResult,  type SourceControlRepositoryCloneUrls,  type SourceControlRepositoryInfo,  type SourceControlRepositoryLookupInput,} from "@t3tools/contracts";import { ServerConfig } from "../config.ts";import { expandHomePathWith } from "../pathExpansion.ts";import { makeKeyedSerialExecutor } from "../orchestration-v2/KeyedSerialExecutor.ts";import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); export class SourceControlRepositoryService extends Context.Service<  SourceControlRepositoryService,  {    readonly lookupRepository: (      input: SourceControlRepositoryLookupInput,    ) => Effect.Effect<SourceControlRepositoryInfo, SourceControlRepositoryError>;    readonly cloneRepository: (      input: SourceControlCloneRepositoryInput,    ) => Effect.Effect<SourceControlCloneRepositoryResult, SourceControlRepositoryError>;    readonly publishRepository: (      input: SourceControlPublishRepositoryInput,    ) => Effect.Effect<SourceControlPublishRepositoryResult, SourceControlRepositoryError>;  }>()("t3/sourceControl/SourceControlRepositoryService") {} function mapRepositoryError(operation: string, provider: SourceControlProviderKind) {  return Effect.mapError((cause: unknown) =>    isSourceControlRepositoryError(cause)      ? cause      : new SourceControlRepositoryError({          operation,          provider,          detail: "The source control operation could not be completed.",          cause,        }),  );} function toRepositoryInfo(  provider: SourceControlProviderKind,  urls: SourceControlRepositoryCloneUrls,): SourceControlRepositoryInfo {  return {    provider,    nameWithOwner: urls.nameWithOwner,    url: urls.url,    sshUrl: urls.sshUrl,  };} function selectRemoteUrl(  urls: SourceControlRepositoryCloneUrls,  protocol: SourceControlCloneProtocol | undefined,): string {  switch (protocol ?? "auto") {    case "https":      return urls.url;    case "ssh":    case "auto":      return urls.sshUrl;  }} export const make = Effect.gen(function* () {  const config = yield* ServerConfig;  const fileSystem = yield* FileSystem.FileSystem;  const git = yield* GitVcsDriver.GitVcsDriver;  const path = yield* Path.Path;  const providers = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;  const cloneOperations = yield* makeKeyedSerialExecutor<string>();   const ensureConcreteProvider = (input: {    readonly operation: string;    readonly provider: SourceControlProviderKind;  }) => {    if (input.provider !== "unknown") {      return Effect.succeed(input.provider);    }     return Effect.fail(      new SourceControlRepositoryError({        operation: input.operation,        provider: input.provider,        detail: "Choose a source control provider before continuing.",      }),    );  };   const lookupRepository = Effect.fn("SourceControlRepositoryService.lookupRepository")(function* (    input: SourceControlRepositoryLookupInput,  ) {    const providerKind = yield* ensureConcreteProvider({      operation: "lookupRepository",      provider: input.provider,    });    const provider = yield* providers.get(providerKind);    const urls = yield* provider.getRepositoryCloneUrls({      cwd: input.cwd ?? config.cwd,      repository: input.repository.trim(),    });    return toRepositoryInfo(providerKind, urls);  });   const normalizeDestinationPath = Effect.fn("SourceControlRepositoryService.normalizeDestination")(    function* (destinationPath: string) {      const trimmed = destinationPath.trim();      if (trimmed.length === 0) {        return yield* new SourceControlRepositoryError({          operation: "cloneRepository",          provider: "unknown",          detail: "Choose a destination path before cloning.",        });      }       return path.resolve(expandHomePathWith(trimmed, path));    },  );   const canonicalDestinationKey = Effect.fn(    "SourceControlRepositoryService.canonicalDestinationKey",  )(function* (destinationPath: string) {    if (yield* fileSystem.exists(destinationPath)) {      return yield* fileSystem.realPath(destinationPath);    }     const suffix = [path.basename(destinationPath)];    let ancestor = path.dirname(destinationPath);    while (!(yield* fileSystem.exists(ancestor))) {      const parent = path.dirname(ancestor);      if (parent === ancestor) break;      suffix.unshift(path.basename(ancestor));      ancestor = parent;    }    return path.join(yield* fileSystem.realPath(ancestor), ...suffix);  });   const prepareDestination = Effect.fn("SourceControlRepositoryService.prepareDestination")(    function* (normalizedDestination: string) {      if (yield* fileSystem.exists(normalizedDestination)) {        const entries = yield* fileSystem          .readDirectory(normalizedDestination, { recursive: false })          .pipe(            Effect.mapError(              (cause) =>                new SourceControlRepositoryError({                  operation: "cloneRepository",                  provider: "unknown",                  detail: "Destination path already exists and is not a directory.",                  cause,                }),            ),          );        return {          destinationPath: normalizedDestination,          parentPath: path.dirname(normalizedDestination),          directoryName: path.basename(normalizedDestination),          occupied: entries.length > 0,        } as const;      } else {        yield* fileSystem.makeDirectory(path.dirname(normalizedDestination), { recursive: true });      }       return {        destinationPath: normalizedDestination,        parentPath: path.dirname(normalizedDestination),        directoryName: path.basename(normalizedDestination),        occupied: false,      } as const;    },  );   const isMatchingClone = Effect.fn("SourceControlRepositoryService.isMatchingClone")(function* (    destinationPath: string,    remoteUrl: string,  ) {    const repositoryRoot = yield* git.execute({      operation: "SourceControlRepositoryService.cloneRepository.verifyRoot",      cwd: destinationPath,      args: ["rev-parse", "--show-toplevel"],      timeoutMs: 5_000,      maxOutputBytes: 16 * 1024,    });    const reportedRoot = repositoryRoot.stdout.trim();    if (reportedRoot.length === 0) return false;    const [canonicalDestination, canonicalRoot] = yield* Effect.all([      fileSystem.realPath(destinationPath),      fileSystem.realPath(reportedRoot),    ]);    if (canonicalDestination !== canonicalRoot) return false;    const existingRemote = yield* git.readConfigValue(destinationPath, "remote.origin.url");    if (existingRemote === null || existingRemote.trim() !== remoteUrl.trim()) {      return false;    }     // A repository shell with only an origin, or with fetched refs but an    // unfinished checkout, can be left behind by an interrupted clone.    const headCommit = yield* git      .execute({        operation: "SourceControlRepositoryService.cloneRepository.verifyHead",        cwd: destinationPath,        args: ["rev-parse", "--verify", "HEAD^{commit}"],        timeoutMs: 5_000,        maxOutputBytes: 16 * 1024,      })      .pipe(        Effect.map((result) => result.stdout.trim() || null),        Effect.orElseSucceed(() => null),      );    if (headCommit === null) return false;     const remoteCommits = yield* git      .execute({        operation: "SourceControlRepositoryService.cloneRepository.verifyRemoteRefs",        cwd: destinationPath,        args: ["for-each-ref", "--format=%(objectname)", "refs/remotes/origin"],        timeoutMs: 5_000,        maxOutputBytes: 64 * 1024,      })      .pipe(        Effect.map((result) => new Set(result.stdout.split("\n").map((line) => line.trim()))),        Effect.orElseSucceed(() => new Set<string>()),      );    if (!remoteCommits.has(headCommit)) return false;     return yield* git      .execute({        operation: "SourceControlRepositoryService.cloneRepository.verifyCheckout",        cwd: destinationPath,        args: ["status", "--porcelain=v1", "--untracked-files=no"],        timeoutMs: 5_000,        maxOutputBytes: 16 * 1024,      })      .pipe(        Effect.map((result) => result.stdout.trim().length === 0),        Effect.orElseSucceed(() => false),      );  });   const cloneRepository = Effect.fn("SourceControlRepositoryService.cloneRepository")(function* (    input: SourceControlCloneRepositoryInput,  ) {    const destinationPath = yield* normalizeDestinationPath(input.destinationPath);    const destinationKey = yield* canonicalDestinationKey(destinationPath);    return yield* cloneOperations.withLock(      destinationKey,      Effect.gen(function* () {        let repository: SourceControlRepositoryInfo | null = null;        let remoteUrl = input.remoteUrl?.trim() ?? null;        let provider: SourceControlProviderKind = input.provider ?? "unknown";         if (!remoteUrl && !(input.provider && input.repository)) {          return yield* new SourceControlRepositoryError({            operation: "cloneRepository",            provider,            detail: "Enter a repository path or clone URL before cloning.",          });        }         const preparedDestination = yield* prepareDestination(destinationPath);         if (input.provider && input.repository) {          repository = yield* lookupRepository({            provider: input.provider,            repository: input.repository,            cwd: preparedDestination.parentPath,          });          remoteUrl = selectRemoteUrl(repository, input.protocol);          provider = input.provider;        }         if (!remoteUrl) {          return yield* new SourceControlRepositoryError({            operation: "cloneRepository",            provider,            detail: "Enter a repository path or clone URL before cloning.",          });        }         if (preparedDestination.occupied) {          const matches = yield* isMatchingClone(destinationPath, remoteUrl).pipe(            Effect.orElseSucceed(() => false),          );          if (!matches) {            return yield* new SourceControlRepositoryError({              operation: "cloneRepository",              provider,              detail:                "Destination path already exists and is not a clone of the requested repository.",            });          }          return { cwd: destinationPath, remoteUrl, repository };        }         yield* git.execute({          operation: "SourceControlRepositoryService.cloneRepository",          cwd: preparedDestination.parentPath,          args: ["clone", "--origin", "origin", remoteUrl, preparedDestination.directoryName],          timeoutMs: 120_000,          maxOutputBytes: 256 * 1024,        });         return { cwd: destinationPath, remoteUrl, repository };      }),    );  });   const publishRepository = Effect.fn("SourceControlRepositoryService.publishRepository")(    function* (input: SourceControlPublishRepositoryInput) {      const providerKind = yield* ensureConcreteProvider({        operation: "publishRepository",        provider: input.provider,      });      const provider = yield* providers.get(providerKind);      const urls = yield* provider.createRepository({        cwd: input.cwd,        repository: input.repository.trim(),        visibility: input.visibility,      });      const remoteUrl = selectRemoteUrl(urls, input.protocol);      const remoteName = yield* git.ensureRemote({        cwd: input.cwd,        preferredName: input.remoteName?.trim() || "origin",        url: remoteUrl,      });       // An empty local repo (no commits) would make `git push HEAD:...` fail      // with an opaque "src refspec HEAD does not match any". Treat this as a      // partial success: the remote was created and wired up, but there is      // nothing to push yet.      const hasCommits = yield* git        .execute({          operation: "SourceControlRepositoryService.publishRepository.headCheck",          cwd: input.cwd,          args: ["rev-parse", "--verify", "HEAD"],        })        .pipe(          Effect.map(() => true),          Effect.orElseSucceed(() => false),        );      if (!hasCommits) {        const details = yield* git.statusDetails(input.cwd).pipe(Effect.orElseSucceed(() => null));        return {          repository: toRepositoryInfo(providerKind, urls),          remoteName,          remoteUrl,          branch: details?.branch ?? "main",          status: "remote_added" as const,        };      }       const pushResult = yield* git.pushCurrentBranch(input.cwd, null, { remoteName });       return {        repository: toRepositoryInfo(providerKind, urls),        remoteName,        remoteUrl,        branch: pushResult.branch,        ...(pushResult.upstreamBranch ? { upstreamBranch: pushResult.upstreamBranch } : {}),        status: "pushed" as const,      };    },  );   return SourceControlRepositoryService.of({    lookupRepository: (input) =>      lookupRepository(input).pipe(mapRepositoryError("lookupRepository", input.provider)),    cloneRepository: (input) =>      cloneRepository(input).pipe(        mapRepositoryError("cloneRepository", input.provider ?? "unknown"),      ),    publishRepository: (input) =>      publishRepository(input).pipe(mapRepositoryError("publishRepository", input.provider)),  });}); export const layer = Layer.effect(SourceControlRepositoryService, make);