chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// Assembling the push that merges a pull request, over two object stores that
// hold what two servers would. The bundle it answers with is read back by the
// parsers that read git's own, so a merge that indexes here indexes there.
import { describe, expect, it } from 'vitest';
import { parseBundle } from '../src/bundle.js';
import { buildMerge } from '../src/merge.js';
import {
indexPacks,
objectId,
parseCommit,
parseTree,
} from '../src/objects.js';
const encoder = new TextEncoder();
/** @param {string} type @param {Uint8Array} data */
const object = (type, data) => ({ type, data, sha: objectId(type, data) });
/** @param {string} text */
const blob = (text) => object('blob', encoder.encode(text));
/**
* A tree of files, in git's own entry format.
* @param {Record<string, {sha: string, type?: string}>} files
*/
function tree(files) {
/** @type {Uint8Array[]} */
const parts = [];
for (const [name, entry] of Object.entries(files)) {
const mode = entry.type === 'tree' ? '40000' : '100644';
const prefix = encoder.encode(`${mode} ${name}\0`);
const raw = Uint8Array.from(
(entry.sha.match(/../g) ?? []).map((/** @type {string} */ pair) =>
Number.parseInt(pair, 16),
),
);
const row = new Uint8Array(prefix.length + raw.length);
row.set(prefix, 0);
row.set(raw, prefix.length);
parts.push(row);
}
const size = parts.reduce((total, part) => total + part.length, 0);
const data = new Uint8Array(size);
let at = 0;
for (const part of parts) {
data.set(part, at);
at += part.length;
}
return object('tree', data);
}
/** @param {string} treeSha @param {string[]} parents @param {string} message */
function commit(treeSha, parents, message) {
return object(
'commit',
encoder.encode(
[
`tree ${treeSha}`,
...parents.map((parent) => `parent ${parent}`),
'author Bob <bob@example.com> 1756000000 +0000',
'committer Bob <bob@example.com> 1756000000 +0000',
'',
`${message}\n`,
].join('\n'),
),
);
}
/**
* An object store over a plain map, the shape a bundle chain presents.
* @param {Array<{sha: string, type: string, data: Uint8Array}>} objects
*/
function storeOf(objects) {
const held = new Map(objects.map((one) => [one.sha, one]));
return {
/** @param {string} sha */
meta: (sha) => {
const one = held.get(sha);
return one ? { type: one.type, size: one.data.length } : null;
},
/** @param {string} sha */
get: async (sha) => held.get(sha) ?? null,
};
}
// One root commit both copies share, then one commit only bob has.
const readme = blob('# demo\n');
const rootTree = tree({ 'README.md': readme });
const root = commit(rootTree.sha, [], 'the first commit');
const added = blob('export const farewell = 1;\n');
const workTree = tree({ 'README.md': readme, 'index.js': added });
const work = commit(workTree.sha, [root.sha], 'add farewell()');
// A second commit on the branch, so the branch has a commit whose tree is not
// the tip's. A merge has to carry that tree too: main's history reaches the
// commit, and git opens every tree it can reach.
const midway = blob('half done\n');
const midTree = tree({ 'README.md': readme, 'notes.md': midway });
const shared = [readme, rootTree, root];
const main = storeOf(shared);
const source = storeOf([...shared, added, workTree, work]);
// bob's branch as two commits: the notes, then farewell() on top.
const midRooted = commit(midTree.sha, [root.sha], 'start the notes');
const stackedTip = commit(workTree.sha, [midRooted.sha], 'add farewell()');
const stackedSource = storeOf([
...shared,
added,
workTree,
midway,
midTree,
midRooted,
stackedTip,
]);
const from = {
did: 'did:plc:bob',
repo: 'demo',
ref: 'refs/heads/agent/farewell',
};
const by = { name: 'Alice', email: 'alice@example.com' };
/** @param {{mode: 'merge'|'squash'}} how */
const merge = ({ mode }) =>
buildMerge({
main,
source,
mainTip: root.sha,
branchTip: work.sha,
canonicalRef: 'refs/heads/main',
mode,
from,
by,
at: Date.UTC(2026, 7, 24),
subject: 'add farewell()',
});
describe('buildMerge', () => {
it('bundles what the canonical copy lacks, and nothing it has', async () => {
const built = await merge({ mode: 'merge' });
const objects = await indexPacks([parseBundle(built.bundle).pack]);
// The commit, its tree, the file it added, and the merge itself. The
// README and the root tree are already there and are not sent again.
expect([...objects.keys()].sort()).toEqual(
[work.sha, workTree.sha, added.sha, built.sha].sort(),
);
expect(objects.has(readme.sha)).toBe(false);
expect(objects.has(rootTree.sha)).toBe(false);
});
it('carries the tree of every commit a merge makes reachable', async () => {
const built = await buildMerge({
main,
source: stackedSource,
mainTip: root.sha,
branchTip: stackedTip.sha,
canonicalRef: 'refs/heads/main',
mode: 'merge',
from,
by,
at: 0,
subject: 'add farewell()',
});
const objects = await indexPacks([parseBundle(built.bundle).pack]);
// The middle commit rides along, so the tree it points at has to as well:
// git reads every commit the merge reaches and opens each one's tree.
expect(objects.has(midRooted.sha)).toBe(true);
expect(objects.has(midTree.sha)).toBe(true);
expect(objects.has(midway.sha)).toBe(true);
});
it('gives a merge the branch tip as a parent and its tree as the result', async () => {
const built = await merge({ mode: 'merge' });
const objects = await indexPacks([parseBundle(built.bundle).pack]);
const merged = parseCommit(
/** @type {{data: Uint8Array}} */ (objects.get(built.sha)).data,
);
expect(merged.parents).toEqual([root.sha, work.sha]);
expect(merged.tree).toBe(workTree.sha);
});
it('leaves a squash the commits it squashed', async () => {
const built = await merge({ mode: 'squash' });
const objects = await indexPacks([parseBundle(built.bundle).pack]);
expect(objects.has(work.sha)).toBe(false);
const squashed = parseCommit(
/** @type {{data: Uint8Array}} */ (objects.get(built.sha)).data,
);
expect(squashed.parents).toEqual([root.sha]);
// The graph says nothing, so the message carries the answer.
expect(
new TextDecoder().decode(
/** @type {{data: Uint8Array}} */ (objects.get(built.sha)).data,
),
).toContain('Merged-ref: refs/heads/agent/farewell');
});
it('names the ref it moves, and what a reader must already hold', async () => {
const built = await merge({ mode: 'merge' });
const parsed = parseBundle(built.bundle);
expect(parsed.refs).toEqual([{ sha: built.sha, name: 'refs/heads/main' }]);
expect(parsed.prerequisites).toEqual([root.sha]);
expect(built.index.length).toBeGreaterThan(0);
});
it('merges a branch main has moved under, when the two sides are apart', async () => {
// Main added notes; the branch added index.js. Both land.
const notes = blob('notes\n');
const movedTree = tree({ 'README.md': readme, 'NOTES.md': notes });
const moved = commit(movedTree.sha, [root.sha], 'main moved on');
const movedMain = storeOf([...shared, notes, movedTree, moved]);
const answer = await buildMerge({
main: movedMain,
source,
mainTip: moved.sha,
branchTip: work.sha,
canonicalRef: 'refs/heads/main',
from,
by,
at: 0,
subject: 'add farewell()',
mode: 'merge',
});
const objects = await indexPacks([parseBundle(answer.bundle).pack]);
const result = parseCommit(
/** @type {{data: Uint8Array}} */ (objects.get(answer.sha)).data,
);
expect(result.parents).toEqual([moved.sha, work.sha]);
const merged = objects.get(result.tree);
expect(merged).toBeDefined();
const names = parseTree(
/** @type {{data: Uint8Array}} */ (merged).data,
).map((entry) => entry.name);
expect(names).toEqual(['NOTES.md', 'README.md', 'index.js']);
});
it('refuses a branch that conflicts with where main has moved, naming the path', async () => {
const theirs = blob('# demo\nbranch\n');
const ours = blob('# demo\nmain\n');
const clashTree = tree({ 'README.md': theirs });
const clash = commit(clashTree.sha, [root.sha], 'rewrite the readme');
const movedTree = tree({ 'README.md': ours });
const moved = commit(movedTree.sha, [root.sha], 'main rewrites it too');
await expect(
buildMerge({
main: storeOf([...shared, ours, movedTree, moved]),
source: storeOf([...shared, theirs, clashTree, clash]),
mainTip: moved.sha,
branchTip: clash.sha,
canonicalRef: 'refs/heads/main',
from,
by,
at: 0,
subject: 'rewrite the readme',
mode: 'merge',
}),
).rejects.toThrow(/conflicts with main in README\.md/);
});
it('refuses a ref that does not end in a commit', async () => {
await expect(
buildMerge({
main,
source,
mainTip: root.sha,
branchTip: workTree.sha,
canonicalRef: 'refs/heads/main',
mode: 'merge',
from,
by,
at: 0,
subject: 'x',
}),
).rejects.toThrow(/does not end in a commit/);
});
it('refuses a branch sharing no history with main', async () => {
const alone = commit(rootTree.sha, [], 'another history');
await expect(
buildMerge({
main,
source: storeOf([...shared, alone]),
mainTip: root.sha,
branchTip: alone.sha,
canonicalRef: 'refs/heads/main',
from,
by,
at: 0,
subject: 'x',
mode: 'merge',
}),
).rejects.toThrow(/no history/);
});
});