chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* Merging a pull request: the commit that puts a branch's work on main.
*
* Two modes, and the difference is what main's history keeps.
*
* A merge commit names the branch tip as its second parent, so the graph
* itself says where the work came from and a reader recovers the whole pull
* request from it later. A squash names only main, so the graph says nothing,
* and the commit carries trailers naming the copy and the ref instead. That
* is the decentralised form of a forge's "(#123)": a forge recovers a squashed
* pull request from a number that a central authority minted, and no such
* authority exists here.
*
* Both modes require the branch to be up to date with main. Then the merge is
* trivial and the result tree is the branch's own, with no three-way content
* merge to do and nothing to get wrong. A branch behind main is refused, and
* rebasing it is the author's move.
*
* Workers-clean: no node imports.
*/
import { objectId } from './objects.js';
const encoder = new TextEncoder();
/**
* Whether a branch has anything to merge. What the merge comes to when main
* has moved is the three-way merge's answer, not this one's.
*
* `base` is the merge base of the branch and main. A base that is the
* branch's own tip means main already reaches all of it.
* @param {{base: string|null, sha: string}} pull
* @param {string} mainTip
* @returns {{can: boolean, why: string}}
*/
export function mergeable(pull, mainTip) {
if (pull.sha === mainTip) {
return { can: false, why: 'This is already the tip of main.' };
}
if (!pull.base) {
return { can: false, why: 'This branch shares no history with main.' };
}
if (pull.base === pull.sha) {
return { can: false, why: 'Main already reaches this branch.' };
}
return { can: true, why: '' };
}
/**
* The trailers a merged commit carries, naming the work it came from.
*
* A merge commit carries them too. The graph already says where the work came
* from, and the trailers say it in the vocabulary a reader of the record
* speaks: an account and a ref rather than a commit id.
* @param {Object} from
* @param {string} from.did - the account whose copy held the work
* @param {string} from.repo - the record key of their copy
* @param {string} from.ref - full ref name of their branch
* @returns {string[]}
*/
export function mergeTrailers({ did, repo, ref }) {
return [
`Merged-from: at://${did}/dev.pdsjs.git.repo/${repo}`,
`Merged-ref: ${ref}`,
];
}
/**
* One git identity line's worth of a person, as a commit records them.
* @param {{name: string, email: string}} who
* @param {number} at - epoch milliseconds
* @returns {string}
*/
function stamp(who, at) {
return `${who.name} <${who.email}> ${Math.floor(at / 1000)} +0000`;
}
/**
* The commit that merges a pull request onto main.
*
* @param {Object} merge
* @param {'merge'|'squash'} merge.mode
* @param {string} merge.tree - the branch tip's tree, which is the result
* @param {string} merge.mainTip
* @param {string} merge.branchTip
* @param {{name: string, email: string}} merge.by - who is merging it
* @param {number} merge.at - epoch milliseconds
* @param {string} merge.subject - the first line
* @param {string} [merge.body]
* @param {string[]} [merge.trailers]
* @returns {{sha: string, type: string, data: Uint8Array}}
*/
export function buildMergeCommit({
mode,
tree,
mainTip,
branchTip,
by,
at,
subject,
body = '',
trailers = [],
}) {
const parents = mode === 'merge' ? [mainTip, branchTip] : [mainTip];
const who = stamp(by, at);
// Subject, body and trailer block, each divided from the next by one blank
// line. A section with nothing in it leaves no blank line behind it.
const message = [subject, body.trim(), trailers.join('\n')]
.filter((part) => part !== '')
.join('\n\n');
const data = encoder.encode(
[
`tree ${tree}`,
...parents.map((parent) => `parent ${parent}`),
`author ${who}`,
`committer ${who}`,
'',
`${message}\n`,
].join('\n'),
);
return { sha: objectId('commit', data), type: 'commit', data };
}