~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
122 lines · 4.0 KB Raw History
/**
 * 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 };
}