171 lines · 5.9 KB · TypeScript
RawDownload
import type { RepositoryIdentity } from "@t3tools/contracts";import * as Cache from "effect/Cache";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import {  detectSourceControlProviderFromGitRemoteUrl,  normalizeGitRemoteUrl,} from "@t3tools/shared/git"; import * as ProcessRunner from "../../processRunner.ts";import {  RepositoryIdentityResolver,  type RepositoryIdentityResolverShape,} from "../Services/RepositoryIdentityResolver.ts"; function parseRemoteFetchUrls(stdout: string): Map<string, string> {  const remotes = new Map<string, string>();  for (const line of stdout.split("\n")) {    const trimmed = line.trim();    if (trimmed.length === 0) continue;    const match = /^(\S+)\s+(\S+)\s+\((fetch|push)\)$/.exec(trimmed);    if (!match) continue;    const [, remoteName = "", remoteUrl = "", direction = ""] = match;    if (direction !== "fetch" || remoteName.length === 0 || remoteUrl.length === 0) {      continue;    }    remotes.set(remoteName, remoteUrl);  }  return remotes;} function pickPrimaryRemote(  remotes: ReadonlyMap<string, string>,): { readonly remoteName: string; readonly remoteUrl: string } | null {  for (const preferredRemoteName of ["upstream", "origin"] as const) {    const remoteUrl = remotes.get(preferredRemoteName);    if (remoteUrl) {      return { remoteName: preferredRemoteName, remoteUrl };    }  }   const [remoteName, remoteUrl] =    [...remotes.entries()].toSorted(([left], [right]) => left.localeCompare(right))[0] ?? [];  return remoteName && remoteUrl ? { remoteName, remoteUrl } : null;} function buildRepositoryIdentity(input: {  readonly remoteName: string;  readonly remoteUrl: string;  readonly rootPath: string;}): RepositoryIdentity {  const canonicalKey = normalizeGitRemoteUrl(input.remoteUrl);  const sourceControlProvider = detectSourceControlProviderFromGitRemoteUrl(input.remoteUrl);  const repositoryPath = canonicalKey.split("/").slice(1).join("/");  const repositoryPathSegments = repositoryPath.split("/").filter((segment) => segment.length > 0);  const [owner] = repositoryPathSegments;  const repositoryName = repositoryPathSegments.at(-1);   return {    canonicalKey,    locator: {      source: "git-remote",      remoteName: input.remoteName,      remoteUrl: input.remoteUrl,    },    rootPath: input.rootPath,    ...(repositoryPath ? { displayName: repositoryPath } : {}),    ...(sourceControlProvider ? { provider: sourceControlProvider.kind } : {}),    ...(owner ? { owner } : {}),    ...(repositoryName ? { name: repositoryName } : {}),  };} const DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY = 512;const DEFAULT_POSITIVE_CACHE_TTL = Duration.minutes(1);const DEFAULT_NEGATIVE_CACHE_TTL = Duration.minutes(1); interface RepositoryIdentityResolverOptions {  readonly cacheCapacity?: number;  readonly positiveCacheTtl?: Duration.Input;  readonly negativeCacheTtl?: Duration.Input;} const resolveRepositoryIdentityCacheKey = Effect.fn("resolveRepositoryIdentityCacheKey")(function* (  cwd: string,) {  const processRunner = yield* ProcessRunner.ProcessRunner;  let cacheKey = cwd;   const topLevelResult = yield* processRunner    .run({      command: "git",      args: ["-C", cwd, "rev-parse", "--show-toplevel"],      timeoutBehavior: "timedOutResult",    })    .pipe(Effect.option);  if (topLevelResult._tag === "None" || topLevelResult.value.code !== 0) {    return cacheKey;  }   const candidate = topLevelResult.value.stdout.trim();  if (candidate.length > 0) {    cacheKey = candidate;  }   return cacheKey;}); const resolveRepositoryIdentityFromCacheKey = Effect.fn("resolveRepositoryIdentityFromCacheKey")(  function* (    cacheKey: string,  ): Effect.fn.Return<RepositoryIdentity | null, never, ProcessRunner.ProcessRunner> {    const processRunner = yield* ProcessRunner.ProcessRunner;    const remoteResult = yield* processRunner      .run({        command: "git",        args: ["-C", cacheKey, "remote", "-v"],        timeoutBehavior: "timedOutResult",      })      .pipe(Effect.option);    if (remoteResult._tag === "None" || remoteResult.value.code !== 0) {      return null;    }     const remote = pickPrimaryRemote(parseRemoteFetchUrls(remoteResult.value.stdout));    return remote ? buildRepositoryIdentity({ ...remote, rootPath: cacheKey }) : null;  },); export const makeRepositoryIdentityResolver = Effect.fn("makeRepositoryIdentityResolver")(  function* (options: RepositoryIdentityResolverOptions = {}) {    const processRunner = yield* ProcessRunner.ProcessRunner;     const repositoryIdentityCache = yield* Cache.makeWith<string, RepositoryIdentity | null>(      (cacheKey) =>        resolveRepositoryIdentityFromCacheKey(cacheKey).pipe(          Effect.provideService(ProcessRunner.ProcessRunner, processRunner),        ),      {        capacity: options.cacheCapacity ?? DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY,        timeToLive: Exit.match({          onSuccess: (value) =>            value === null              ? (options.negativeCacheTtl ?? DEFAULT_NEGATIVE_CACHE_TTL)              : (options.positiveCacheTtl ?? DEFAULT_POSITIVE_CACHE_TTL),          onFailure: () => Duration.zero,        }),      },    );     const resolve: RepositoryIdentityResolverShape["resolve"] = Effect.fn(      "RepositoryIdentityResolver.resolve",    )(function* (cwd) {      const cacheKey = yield* resolveRepositoryIdentityCacheKey(cwd).pipe(        Effect.provideService(ProcessRunner.ProcessRunner, processRunner),      );      return yield* Cache.get(repositoryIdentityCache, cacheKey);    });     return {      resolve,    } satisfies RepositoryIdentityResolverShape;  },); export const RepositoryIdentityResolverLive = Layer.effect(  RepositoryIdentityResolver,  makeRepositoryIdentityResolver(),).pipe(Layer.provide(ProcessRunner.layer));