diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts index 01b0f6dc3..f5ae83a95 100644 --- a/packages/core/src/git.ts +++ b/packages/core/src/git.ts @@ -6,11 +6,13 @@ import { GIT_DIFF_MAX_TOKENS, GIT_IGNORE_GENAI } from "./constants" import { llmifyDiff } from "./diff" import { resolveFileContents } from "./file" import { readText } from "./fs" -import { runtimeHost } from "./host" +import { host, runtimeHost } from "./host" import { shellParse } from "./shell" -import { arrayify } from "./util" +import { arrayify, dotGenaiscriptPath } from "./util" import { estimateTokens, truncateTextToTokens } from "./tokens" import { resolveTokenEncoder } from "./encoders" +import { underscore } from "inflection" +import { existsSync } from "node:fs" /** * GitClient class provides an interface to interact with Git. @@ -93,6 +95,7 @@ export class GitClient implements Git { Array.isArray(args) ? args : shellParse(args), opts ) + if (res.exitCode !== 0) throw new Error(res.stderr) return res.stdout } @@ -328,6 +331,54 @@ ${await this.diff({ ...options, nameOnly: true })} return res } + /** + * Create a shallow git clone + * @param repository URL of the remote repository + * @param options various clone options + */ + async shallowClone( + repository: string, + options?: { + /** + * Brnach to clone + */ + branch?: string + } + ): Promise { + let { branch, ...rest } = options || {} + + // normalize short github url + if (/^\w+\/\w+$/.test(repository)) { + repository = `https://github.com/${repository}` + } + const url = new URL(repository) + const sha = ( + await this.exec(["ls-remote", repository, branch || "HEAD"]) + ).split(/\s+/)[0] + let directory = dotGenaiscriptPath( + "git", + url.pathname + .split(/\//g) + .filter((s) => !!s) + .join("-"), + branch || `HEAD`, + sha + ) + if (branch) directory = host.path.join(directory, branch) + if (existsSync(directory)) return directory + + const args = ["clone", "--depth=1"] + if (branch) args.push("--branch", branch) + Object.entries(rest).forEach(([k, v]) => + args.push( + v === true ? `--${underscore(k)}` : `--${underscore(k)}=${v}` + ) + ) + args.push(repository, directory) + await this.exec(args) + return directory + } + client(cwd: string) { return new GitClient(cwd) } diff --git a/packages/core/src/types/prompt_template.d.ts b/packages/core/src/types/prompt_template.d.ts index 57754c1a6..bdaa0f0b2 100644 --- a/packages/core/src/types/prompt_template.d.ts +++ b/packages/core/src/types/prompt_template.d.ts @@ -1841,6 +1841,22 @@ interface Git { excludedPaths?: ElementOrArray }): Promise + /** + * Create a shallow git clone + * @param repository URL of the remote repository + * @param options various clone options + * @returns the path to the cloned repository + */ + async shallowClone( + repository: string, + options?: { + /** + * Brnach to clone + */ + branch?: string + } + ): Promise + /** * Open a git client on a different directory * @param cwd working directory diff --git a/packages/sample/genaisrc/git.genai.mts b/packages/sample/genaisrc/git.genai.mts index 7789127f2..57a516eb2 100644 --- a/packages/sample/genaisrc/git.genai.mts +++ b/packages/sample/genaisrc/git.genai.mts @@ -32,3 +32,8 @@ for (const commit of log.slice(0, 10)) { const client = git.client(".") console.log({ log: await client.log() }) + +const clone = await git.shallowClone("microsoft/genaiscript") +const cachedClone = await git.shallowClone("microsoft/genaiscript") +console.log({ clone, cachedClone }) +if (clone !== cachedClone) throw new Error("Clones should be cached")