跳转到内容
搜索文档

isomorphic-git

最后更新 查看 MarkdownAgent 设置

使用 isomorphic-git 直接在 Cloudflare Worker 中对 Artifacts 仓库运行 Git 操作。

Artifacts 绑定(binding)用于创建和管理仓库,但它无法读取或写入其中的文件——为此,您需要 Git。由于 Workers 没有 git 二进制文件或本地文件系统,isomorphic-git 填补了这一空白。它提供类似于 init、commit 和 push 的 Git 操作作为 JavaScript 函数调用,使用内存文件系统代替真实的磁盘。

当您的 Worker 需要以编程方式构建文件树并将其推送至 Artifacts 仓库时,请使用此方法。例如,生成代码并提交的 AI 智能体(agent),或者克隆仓库、修改文件并将更改推回的自动化程序。

前提条件

按照 Artifacts Workers 设置指南,使用 Artifacts 绑定(binding)设置 Worker。

安装依赖项

在您的 Worker 项目中安装 isomorphic-git

npm i isomorphic-git

示例

此演示在每次请求时创建一个新仓库、写入两个文件、提交它们,并将 main 分支推送至新的远程仓库。

将此用作端到端流程的参考。在生产环境的 Worker 中,请查找或重用现有仓库,而不是针对每次请求都创建一个新仓库。

src/index.jsjs
import git from "isomorphic-git";
import http from "isomorphic-git/http/web";
import { MemoryFS } from "./memory-fs";

export default {
	async fetch(_request, env) {
		// Create a new Artifacts repo
		const repoName = `worker-demo-${crypto.randomUUID().slice(0, 8)}`;
		const created = await env.ARTIFACTS.create(repoName);

		// Artifacts tokens include expiry metadata: art_v1_<secret>?expires=<unix_seconds>
		// Git Basic auth expects only the token secret as the password
		const tokenSecret = created.token.split("?expires=")[0];

		// Set up an in-memory filesystem and initialize a Git repo
		const dir = "/workspace";
		const fs = new MemoryFS();
		await git.init({ fs, dir, defaultBranch: "main" });

		// Write files
		await fs.promises.writeFile(
			`${dir}/README.md`,
			"# Artifacts repo created from a Worker\n",
		);
		await fs.promises.writeFile(
			`${dir}/src/index.ts`,
			'export const message = "hello from Artifacts";\n',
		);

		// Stage and commit
		await git.add({ fs, dir, filepath: "README.md" });
		await git.add({ fs, dir, filepath: "src/index.ts" });

		const commit = await git.commit({
			fs,
			dir,
			message: "Create starter files",
			author: {
				name: "Artifacts example",
				email: "artifacts@example.com",
			},
		});

		// Push to the Artifacts remote
		const push = await git.push({
			fs,
			http,
			dir,
			url: created.remote,
			ref: "main",
			onAuth: () => ({
				username: "x",
				password: tokenSecret,
			}),
		});

		return Response.json({
			repo: created.name,
			remote: created.remote,
			commit,
			refs: push.refs,
		});
	},
};
src/index.tsts
import git from "isomorphic-git";
import http from "isomorphic-git/http/web";
import { MemoryFS } from "./memory-fs";

export interface Env {
	ARTIFACTS: Artifacts;
}

export default {
	async fetch(_request: Request, env: Env) {
		// Create a new Artifacts repo
		const repoName = `worker-demo-${crypto.randomUUID().slice(0, 8)}`;
		const created = await env.ARTIFACTS.create(repoName);

		// Artifacts tokens include expiry metadata: art_v1_<secret>?expires=<unix_seconds>
		// Git Basic auth expects only the token secret as the password
		const tokenSecret = created.token.split("?expires=")[0];

		// Set up an in-memory filesystem and initialize a Git repo
		const dir = "/workspace";
		const fs = new MemoryFS();
		await git.init({ fs, dir, defaultBranch: "main" });

		// Write files
		await fs.promises.writeFile(
			`${dir}/README.md`,
			"# Artifacts repo created from a Worker\n",
		);
		await fs.promises.writeFile(
			`${dir}/src/index.ts`,
			'export const message = "hello from Artifacts";\n',
		);

		// Stage and commit
		await git.add({ fs, dir, filepath: "README.md" });
		await git.add({ fs, dir, filepath: "src/index.ts" });

		const commit = await git.commit({
			fs,
			dir,
			message: "Create starter files",
			author: {
				name: "Artifacts example",
				email: "artifacts@example.com",
			},
		});

		// Push to the Artifacts remote
		const push = await git.push({
			fs,
			http,
			dir,
			url: created.remote,
			ref: "main",
			onAuth: () => ({
				username: "x",
				password: tokenSecret,
			}),
		});

		return Response.json({
			repo: created.name,
			remote: created.remote,
			commit,
			refs: push.refs,
		});
	},
} satisfies ExportedHandler<Env>;

内存文件系统辅助工具

当您在 Workers 中需要内存中的短期工作树时,请与 isomorphic-git 配合使用此辅助工具。

src/memory-fs.jsjs
class MemoryStats {
	entry;

	constructor(entry) {
		this.entry = entry;
	}

	get size() {
		return this.entry.kind === "file" ? this.entry.data.byteLength : 0;
	}

	get mtimeMs() {
		return this.entry.mtimeMs;
	}

	get ctimeMs() {
		return this.entry.mtimeMs;
	}

	get mode() {
		return this.entry.kind === "file" ? 0o100644 : 0o040000;
	}

	isFile() {
		return this.entry.kind === "file";
	}

	isDirectory() {
		return this.entry.kind === "dir";
	}

	isSymbolicLink() {
		return false;
	}
}

export class MemoryFS {
	encoder = new TextEncoder();
	decoder = new TextDecoder();
	entries = new Map([
		["/", { kind: "dir", children: new Set(), mtimeMs: Date.now() }],
	]);

	promises = {
		readFile: this.readFile.bind(this),
		writeFile: this.writeFile.bind(this),
		unlink: this.unlink.bind(this),
		readdir: this.readdir.bind(this),
		mkdir: this.mkdir.bind(this),
		rmdir: this.rmdir.bind(this),
		stat: this.stat.bind(this),
		lstat: this.lstat.bind(this),
	};

	normalize(input) {
		const segments = [];

		for (const part of input.split("/")) {
			if (!part || part === ".") {
				continue;
			}

			if (part === "..") {
				segments.pop();
				continue;
			}

			segments.push(part);
		}

		return `/${segments.join("/")}` || "/";
	}

	parent(path) {
		const normalized = this.normalize(path);
		if (normalized === "/") {
			return "/";
		}

		const parts = normalized.split("/").filter(Boolean);
		parts.pop();
		return parts.length ? `/${parts.join("/")}` : "/";
	}

	basename(path) {
		return this.normalize(path).split("/").filter(Boolean).pop() ?? "";
	}

	getEntry(path) {
		return this.entries.get(this.normalize(path));
	}

	requireEntry(path) {
		const entry = this.getEntry(path);
		if (!entry) {
			throw new Error(`ENOENT: ${path}`);
		}

		return entry;
	}

	requireDir(path) {
		const entry = this.requireEntry(path);
		if (entry.kind !== "dir") {
			throw new Error(`ENOTDIR: ${path}`);
		}

		return entry;
	}

	async mkdir(path, options) {
		const target = this.normalize(path);
		if (target === "/") {
			return;
		}

		const recursive =
			typeof options === "object" && options !== null && options.recursive;
		const parent = this.parent(target);

		if (!this.entries.has(parent)) {
			if (!recursive) {
				throw new Error(`ENOENT: ${parent}`);
			}

			await this.mkdir(parent, { recursive: true });
		}

		if (this.entries.has(target)) {
			return;
		}

		this.entries.set(target, {
			kind: "dir",
			children: new Set(),
			mtimeMs: Date.now(),
		});

		this.requireDir(parent).children.add(this.basename(target));
	}

	async writeFile(path, data) {
		const target = this.normalize(path);
		await this.mkdir(this.parent(target), { recursive: true });

		const bytes =
			typeof data === "string"
				? this.encoder.encode(data)
				: data instanceof Uint8Array
					? data
					: new Uint8Array(data);

		this.entries.set(target, {
			kind: "file",
			data: bytes,
			mtimeMs: Date.now(),
		});

		this.requireDir(this.parent(target)).children.add(this.basename(target));
	}

	async readFile(path, options) {
		const entry = this.requireEntry(path);
		if (entry.kind !== "file") {
			throw new Error(`EISDIR: ${path}`);
		}

		const encoding = typeof options === "string" ? options : options?.encoding;
		return encoding ? this.decoder.decode(entry.data) : entry.data;
	}

	async readdir(path) {
		return [...this.requireDir(path).children].sort();
	}

	async unlink(path) {
		const target = this.normalize(path);
		const entry = this.requireEntry(target);
		if (entry.kind !== "file") {
			throw new Error(`EISDIR: ${path}`);
		}

		this.entries.delete(target);
		this.requireDir(this.parent(target)).children.delete(this.basename(target));
	}

	async rmdir(path) {
		const target = this.normalize(path);
		const entry = this.requireDir(target);
		if (entry.children.size > 0) {
			throw new Error(`ENOTEMPTY: ${path}`);
		}

		this.entries.delete(target);
		this.requireDir(this.parent(target)).children.delete(this.basename(target));
	}

	async stat(path) {
		return new MemoryStats(this.requireEntry(path));
	}

	async lstat(path) {
		return this.stat(path);
	}
}
src/memory-fs.tsts
type Entry =
	| {
			kind: "dir";
			children: Set<string>;
			mtimeMs: number;
	  }
	| {
			kind: "file";
			data: Uint8Array;
			mtimeMs: number;
	  };

class MemoryStats {
	entry: Entry;

	constructor(entry: Entry) {
		this.entry = entry;
	}

	get size() {
		return this.entry.kind === "file" ? this.entry.data.byteLength : 0;
	}

	get mtimeMs() {
		return this.entry.mtimeMs;
	}

	get ctimeMs() {
		return this.entry.mtimeMs;
	}

	get mode() {
		return this.entry.kind === "file" ? 0o100644 : 0o040000;
	}

	isFile() {
		return this.entry.kind === "file";
	}

	isDirectory() {
		return this.entry.kind === "dir";
	}

	isSymbolicLink() {
		return false;
	}
}

export class MemoryFS {
	encoder = new TextEncoder();
	decoder = new TextDecoder();
	entries = new Map<string, Entry>([
		["/", { kind: "dir", children: new Set(), mtimeMs: Date.now() }],
	]);

	promises = {
		readFile: this.readFile.bind(this),
		writeFile: this.writeFile.bind(this),
		unlink: this.unlink.bind(this),
		readdir: this.readdir.bind(this),
		mkdir: this.mkdir.bind(this),
		rmdir: this.rmdir.bind(this),
		stat: this.stat.bind(this),
		lstat: this.lstat.bind(this),
	};

	normalize(input: string) {
		const segments: string[] = [];

		for (const part of input.split("/")) {
			if (!part || part === ".") {
				continue;
			}

			if (part === "..") {
				segments.pop();
				continue;
			}

			segments.push(part);
		}

		return `/${segments.join("/")}` || "/";
	}

	parent(path: string) {
		const normalized = this.normalize(path);
		if (normalized === "/") {
			return "/";
		}

		const parts = normalized.split("/").filter(Boolean);
		parts.pop();
		return parts.length ? `/${parts.join("/")}` : "/";
	}

	basename(path: string) {
		return this.normalize(path).split("/").filter(Boolean).pop() ?? "";
	}

	getEntry(path: string) {
		return this.entries.get(this.normalize(path));
	}

	requireEntry(path: string) {
		const entry = this.getEntry(path);
		if (!entry) {
			throw new Error(`ENOENT: ${path}`);
		}

		return entry;
	}

	requireDir(path: string) {
		const entry = this.requireEntry(path);
		if (entry.kind !== "dir") {
			throw new Error(`ENOTDIR: ${path}`);
		}

		return entry;
	}

	async mkdir(path: string, options?: { recursive?: boolean } | number) {
		const target = this.normalize(path);
		if (target === "/") {
			return;
		}

		const recursive =
			typeof options === "object" && options !== null && options.recursive;
		const parent = this.parent(target);

		if (!this.entries.has(parent)) {
			if (!recursive) {
				throw new Error(`ENOENT: ${parent}`);
			}

			await this.mkdir(parent, { recursive: true });
		}

		if (this.entries.has(target)) {
			return;
		}

		this.entries.set(target, {
			kind: "dir",
			children: new Set(),
			mtimeMs: Date.now(),
		});

		this.requireDir(parent).children.add(this.basename(target));
	}

	async writeFile(path: string, data: string | Uint8Array | ArrayBuffer) {
		const target = this.normalize(path);
		await this.mkdir(this.parent(target), { recursive: true });

		const bytes =
			typeof data === "string"
				? this.encoder.encode(data)
				: data instanceof Uint8Array
					? data
					: new Uint8Array(data);

		this.entries.set(target, {
			kind: "file",
			data: bytes,
			mtimeMs: Date.now(),
		});

		this.requireDir(this.parent(target)).children.add(this.basename(target));
	}

	async readFile(path: string, options?: string | { encoding?: string }) {
		const entry = this.requireEntry(path);
		if (entry.kind !== "file") {
			throw new Error(`EISDIR: ${path}`);
		}

		const encoding = typeof options === "string" ? options : options?.encoding;
		return encoding ? this.decoder.decode(entry.data) : entry.data;
	}

	async readdir(path: string) {
		return [...this.requireDir(path).children].sort();
	}

	async unlink(path: string) {
		const target = this.normalize(path);
		const entry = this.requireEntry(target);
		if (entry.kind !== "file") {
			throw new Error(`EISDIR: ${path}`);
		}

		this.entries.delete(target);
		this.requireDir(this.parent(target)).children.delete(this.basename(target));
	}

	async rmdir(path: string) {
		const target = this.normalize(path);
		const entry = this.requireDir(target);
		if (entry.children.size > 0) {
			throw new Error(`ENOTEMPTY: ${path}`);
		}

		this.entries.delete(target);
		this.requireDir(this.parent(target)).children.delete(this.basename(target));
	}

	async stat(path: string) {
		return new MemoryStats(this.requireEntry(path));
	}

	async lstat(path: string) {
		return this.stat(path);
	}
}

这篇文档对您有帮助吗?