UNPKG

alinea

Version:
327 lines (325 loc) 9.53 kB
import "../../chunks/chunk-NZLE2WMY.js"; // src/core/source/Tree.ts import { assert } from "../util/Assert.js"; import { hashTree, serializeTreeEntries } from "./GitUtils.js"; import { ShaMismatchError } from "./ShaMismatchError.js"; import { compareStrings, splitPath } from "./Utils.js"; var Leaf = class { type = "blob"; sha; mode; constructor({ sha, mode }) { if (mode !== "100644" && mode !== "100755") throw new Error(`Invalid mode for leaf: ${mode}`); this.sha = sha; this.mode = mode; } clone() { return this; } toJSON() { return { ...this }; } }; var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; var TreeBase = class _TreeBase { type = "tree"; mode = "040000"; sha; nodes = /* @__PURE__ */ new Map(); constructor(sha) { this.sha = sha; } get(path) { const [name, rest] = splitPath(path); const named = this.nodes.get(name); if (!rest) return named; if (named && !(named instanceof Leaf)) return named.get(rest); } getNode(path) { const entry = this.get(path); if (!entry) throw new Error(`Node not found: ${path}`); if (entry instanceof Leaf) throw new Error(`Expected node, found leaf: ${path}`); return entry; } getLeaf(path) { const entry = this.get(path); if (!entry) throw new Error(`Leaf not found: ${path}`); if (!(entry instanceof Leaf)) throw new Error(`Expected leaf, found node: ${path}`); return entry; } has(path) { const [name, rest] = splitPath(path); if (rest) { const target = this.nodes.get(name); if (!target || target instanceof Leaf) return false; return target.has(rest); } return this.nodes.has(name); } *[Symbol.iterator]() { for (const [name, entry] of this.nodes) { yield [name, entry]; if (entry instanceof _TreeBase) for (const [childName, child] of entry) yield [`${name}/${childName}`, child]; } } *paths() { for (const [name, entry] of this.nodes) { yield name; if (entry instanceof _TreeBase) for (const path of entry.paths()) yield `${name}/${path}`; } } index() { return new Map(this.fileIndex("")); } fileIndex(prefix) { return Array.from(this.nodes, ([key, entry]) => { if (entry instanceof _TreeBase) return entry.fileIndex(`${prefix}${key}/`); return [[prefix + key, entry.sha]]; }).flat(); } equals(other) { if (other instanceof ReadonlyTree) return this.sha === other.sha; const canCompare = this.sha && other.sha; if (canCompare && this.sha === other.sha) return true; if (this.nodes.size !== other.nodes.size) return false; for (const [name, entry] of this.nodes) { const otherEntry = other.nodes.get(name); if (!otherEntry) return false; if (entry instanceof Leaf) { if (!(otherEntry instanceof Leaf)) return false; if (entry.sha !== otherEntry.sha) return false; } else { if (!(otherEntry instanceof _TreeBase)) return false; if (!entry.equals(otherEntry)) return false; } } return true; } }; var ReadonlyTree = class _ReadonlyTree extends TreeBase { sha; static EMPTY = new _ReadonlyTree({ sha: EMPTY_TREE_SHA, entries: [] }); #shas = /* @__PURE__ */ new Set(); constructor({ sha, entries }) { super(sha); this.sha = sha; for (const entry of entries) { const node = entry.entries ? new _ReadonlyTree(entry) : new Leaf(entry); if (node instanceof Leaf) this.#shas.add(node.sha); else for (const sha2 of node.#shas) this.#shas.add(sha2); this.nodes.set(entry.name, node); } } get isEmpty() { return this.sha === EMPTY_TREE_SHA; } get entries() { return [...this.nodes.entries()].map(([name, entry]) => ({ name, ...entry.toJSON() })); } get shas() { return this.#shas; } hasSha(sha) { return this.#shas.has(sha); } clone() { return new WriteableTree({ sha: this.sha, entries: this.entries }); } toJSON() { return { sha: this.sha, mode: this.mode, entries: this.entries }; } flat() { return { sha: this.sha, tree: this.#flatEntries("") }; } withChanges(batch) { const result = this.clone(); result.applyChanges(batch); return result.compile(); } #flatEntries(prefix) { return Array.from(this.nodes, ([key, entry]) => { const self = { type: entry.type, path: prefix + key, mode: entry.mode, sha: entry.sha }; if (entry instanceof TreeBase) return [self].concat(entry.#flatEntries(`${prefix}${key}/`)); return [self]; }).flat(); } static fromFlat(tree) { const entries = Array(); const nodes = /* @__PURE__ */ new Map(); for (const { path, mode, sha } of tree.tree) { const lastSlash = path.lastIndexOf("/"); const dir = lastSlash === -1 ? "" : path.slice(0, lastSlash); const name = lastSlash === -1 ? path : path.slice(lastSlash + 1); const node = { name, mode, sha }; nodes.set(path, node); if (dir) { const parent = nodes.get(dir); assert(parent, `Parent not found: ${dir}`); if (!parent.entries) parent.entries = []; parent.entries.push(node); } else { entries.push(node); } } return new _ReadonlyTree({ sha: tree.sha, entries }); } // Todo: check modes diff(that) { const local = this.index(); const remote = that.index(); const changes = []; const paths = new Set( [...local.keys(), ...remote.keys()].sort(compareStrings) ); for (const path of paths) { const localValue = local.get(path); const remoteValue = remote.get(path); if (localValue === remoteValue) continue; if (remoteValue === void 0) { changes.unshift({ op: "delete", path, sha: localValue }); } else { changes.push({ op: "add", path, sha: remoteValue }); } } return { fromSha: this.sha, changes }; } }; var WriteableTree = class _WriteableTree extends TreeBase { constructor({ sha, entries } = { sha: EMPTY_TREE_SHA, entries: [] }) { super(sha); for (const entry of entries) { this.nodes.set( entry.name, entry.entries ? new _WriteableTree(entry) : new Leaf(entry) ); } } add(path, input) { this.sha = void 0; const [name, rest] = splitPath(path); if (rest) { const target = this.#makeNode(name); target.add(rest, input); } else { const node = typeof input === "string" ? new Leaf({ sha: input, mode: "100644" }) : input.clone(); this.nodes.set(name, node); } } #makeNode(segment) { this.sha = void 0; if (!this.nodes.has(segment)) this.nodes.set(segment, new _WriteableTree()); return this.getNode(segment); } #getNode(segment) { return this.nodes.get(segment); } remove(path) { this.sha = void 0; const [name, rest] = splitPath(path); if (!rest) return this.nodes.delete(name); const target = this.#getNode(name); if (!target) return false; const result = target.remove(rest); if (target.nodes.size === 0) this.nodes.delete(name); return result; } rename(from, to) { const entry = this.get(from); if (!entry) return; this.remove(from); this.add(to, entry.clone()); } applyChanges(batch) { const { fromSha, changes } = batch; if (this.sha && this.sha !== fromSha) throw new ShaMismatchError(fromSha, this.sha); for (const change of changes) { switch (change.op) { case "delete": { const existing = this.get(change.path); if (!existing) continue; assert(existing instanceof Leaf, `Cannot delete: ${change.path}`); assert( existing.sha === change.sha, `SHA mismatch: ${existing.sha} !== ${change.sha} for ${change.path}` ); this.remove(change.path); continue; } case "add": { const existing = this.get(change.path); if (existing && existing.sha === change.sha) continue; this.add(change.path, change.sha); continue; } } } } async #getTree(previous) { if (previous?.equals(this)) return previous.toJSON(); const entries = await this.#treeEntries(previous); if (this.sha) return { sha: this.sha, entries }; const serialized = serializeTreeEntries(entries); this.sha = await hashTree(serialized); return { sha: this.sha, entries }; } async #treeEntries(previous) { const entries = Array(); for (const [name, node] of this.nodes.entries()) { if (node instanceof TreeBase) { const previousNode = previous?.get(name); const entry = await node.#getTree( previousNode instanceof ReadonlyTree ? previousNode : void 0 ); if (entry.entries.length > 0) entries.push({ name, ...node, ...entry }); } else { entries.push({ name, ...node }); } } return entries; } async getSha() { return (await this.#getTree()).sha; } async compile(previous) { return new ReadonlyTree(await this.#getTree(previous)); } clone() { const result = new _WriteableTree(); for (const [name, entry] of this.nodes) result.add(name, entry.clone()); result.sha = this.sha; return result; } }; export { Leaf, ReadonlyTree, WriteableTree };