// The commit that merges a pull request, and what it is allowed to merge. import { describe, expect, it } from 'vitest'; import { buildMergeCommit, mergeable, mergeTrailers, } from '../src/merge-commit.js'; import { indexPacks, parseCommit } from '../src/objects.js'; import { writePack } from '../src/pack-write.js'; /** @param {string} name */ const sha = (name) => name.repeat(40).slice(0, 40); const MAIN = sha('m'); const TIP = sha('t'); const TREE = sha('r'); const by = { name: 'Alice', email: 'alice@example.com' }; // A fixed instant, so the commit id a test asserts on does not move. const AT = Date.UTC(2026, 7, 24, 12, 0, 0); describe('mergeable', () => { it('accepts a branch whose merge base is main itself', () => { expect(mergeable({ base: MAIN, sha: TIP }, MAIN).can).toBe(true); }); it('accepts a branch main has moved under; the merge itself decides', () => { expect(mergeable({ base: sha('o'), sha: TIP }, MAIN).can).toBe(true); }); it('refuses a branch main already reaches', () => { const answer = mergeable({ base: TIP, sha: TIP }, MAIN); expect(answer.can).toBe(false); expect(answer.why).toMatch(/already reaches/); }); it('refuses a branch sharing no history with main', () => { expect(mergeable({ base: null, sha: TIP }, MAIN).can).toBe(false); }); it('refuses main itself', () => { expect(mergeable({ base: MAIN, sha: MAIN }, MAIN).can).toBe(false); }); }); describe('mergeTrailers', () => { it('names the copy and the ref the work came from', () => { expect( mergeTrailers({ did: 'did:plc:bob', repo: 'project', ref: 'refs/heads/fix', }), ).toEqual([ 'Merged-from: at://did:plc:bob/dev.pdsjs.git.repo/project', 'Merged-ref: refs/heads/fix', ]); }); }); describe('buildMergeCommit', () => { /** * @param {'merge'|'squash'} mode * @param {{body?: string, trailers?: string[]}} [extra] */ const build = (mode, extra = {}) => buildMergeCommit({ mode, tree: TREE, mainTip: MAIN, branchTip: TIP, by, at: AT, subject: 'add farewell()', ...extra, }); it('gives a merge two parents, main first', async () => { const commit = build('merge'); const objects = await indexPacks([writePack([commit])]); const parsed = parseCommit( /** @type {{data: Uint8Array}} */ (objects.get(commit.sha)).data, ); expect(parsed.parents).toEqual([MAIN, TIP]); expect(parsed.tree).toBe(TREE); }); it('gives a squash main alone, so the graph says nothing', async () => { const commit = build('squash'); const objects = await indexPacks([writePack([commit])]); const parsed = parseCommit( /** @type {{data: Uint8Array}} */ (objects.get(commit.sha)).data, ); expect(parsed.parents).toEqual([MAIN]); }); it('carries the trailers in their own block', () => { const commit = build('squash', { body: 'Two greetings, one register.', trailers: mergeTrailers({ did: 'did:plc:bob', repo: 'project', ref: 'refs/heads/fix', }), }); const message = new TextDecoder().decode(commit.data); expect(message).toContain( '\nadd farewell()\n\nTwo greetings, one register.\n\nMerged-from: at://did:plc:bob/dev.pdsjs.git.repo/project\nMerged-ref: refs/heads/fix\n', ); }); it('leaves no blank lines behind a subject with nothing after it', () => { const message = new TextDecoder().decode(build('merge').data); expect(message.endsWith('\nadd farewell()\n')).toBe(true); }); it('is the same commit for the same inputs', () => { expect(build('merge').sha).toBe(build('merge').sha); expect(build('merge').sha).not.toBe(build('squash').sha); }); });