INTEGRITY Документация

isomorphic-git

Используйте isomorphic-git ↗ для выполнения Git операций в репозиториях Artifacts прямо из Cloudflare Worker.

Привязка Artifacts создаёт репозитории и управляет ими, но не может читать и записывать файлы внутри них. Для этого нужен Git. Поскольку в Workers нет бинарного файла git и локальной файловой системы, isomorphic-git закрывает этот пробел. Библиотека предоставляет операции Git, такие как init, commit и push, в виде вызовов функций JavaScript, используя файловую систему в памяти вместо настоящего диска.

Используйте этот способ, если вашему Worker нужно программно создавать файловые деревья и отправлять их в репозиторий Artifacts с помощью push. Например, это может быть ИИ-агент, который генерирует код и делает коммит, или автоматизация, которая клонирует репозиторий, изменяет файлы и отправляет изменения обратно.

Предварительные требования

Следуйте Руководство по настройке Artifacts для Workers для настройки Worker с привязкой Artifacts.

Установка зависимости

Установка isomorphic-git в вашем проекте Worker:

npm i isomorphic-git

Пример

Этот пример при каждом запросе создает новый репозиторий, записывает два файла, коммитит их и выполняет push main на новый удалённый репозиторий.

Используйте это как справочный материал по сквозному процессу. В рабочем Worker ищите или используйте существующий репозиторий, а не создавайте новый для каждого запроса.

src/index.js
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: "[email protected]",
			},
		});

		// 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.ts
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: "[email protected]",
			},
		});

		// 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>;

Вспомогательный модуль для файловой системы в памяти

Используйте этот вспомогательный метод с isomorphic-git в Workers, когда вам нужно кратковременное рабочее дерево в памяти.

src/memory-fs.js
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.ts
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);
	}
}