alinea
Version:
Headless git-based CMS
327 lines (325 loc) • 9.53 kB
JavaScript
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
};