mirror your GitHub repos to tangled.org automatically
1import { execFileSync } from 'node:child_process'
2import { afterEach, beforeEach, describe, expect, it } from 'vitest'
3import { RemoteRejectedError } from '../../server/utils/git-wire/errors'
4import { runSplice, runSpliceDelete } from '../../server/utils/splice'
5import { fakeGithubFetch, GitFixture, localReceivePackFactory } from '../utils/git-wire'
6
7describe('splice (end-to-end against real git binaries)', () => {
8 let fx: GitFixture
9 let realFetch: typeof globalThis.fetch
10
11 beforeEach(() => {
12 fx = new GitFixture()
13 realFetch = globalThis.fetch
14 })
15
16 afterEach(() => {
17 globalThis.fetch = realFetch
18 fx.cleanup()
19 })
20
21 function wireGithub(ghBare: string) {
22 globalThis.fetch = fakeGithubFetch(new Map([['owner/repo', ghBare]])) as unknown as typeof globalThis.fetch
23 }
24
25 it('mirrors a first push into an empty knot, transferring objects end to end', async () => {
26 const gh = fx.initBare('gh.git')
27 const work = fx.initWork('work')
28 const sha = fx.commit(work, 'a.txt', 'hello world')
29 fx.pushTo(work, gh, 'HEAD:refs/heads/main')
30 const knot = fx.initBare('knot.git')
31 wireGithub(gh)
32
33 const result = await runSplice(localReceivePackFactory(knot), {
34 repoFullName: 'owner/repo',
35 ref: 'refs/heads/main',
36 want: sha,
37 token: 'tok',
38 })
39
40 expect(result).toEqual({ status: 'synced', sha })
41 expect(fx.revParse(knot, 'refs/heads/main')).toBe(sha)
42 expect(execFileSync('git', ['cat-file', '-p', `${sha}:a.txt`], { cwd: knot, encoding: 'utf8' })).toBe('hello world')
43 })
44
45 it('streams only the delta on a follow-up push (thin pack via knot tips as haves)', async () => {
46 const gh = fx.initBare('gh.git')
47 const work = fx.initWork('work')
48 const first = fx.commit(work, 'a.txt', 'a'.repeat(8000))
49 fx.pushTo(work, gh, 'HEAD:refs/heads/main')
50 const knot = fx.initBare('knot.git')
51 wireGithub(gh)
52
53 await runSplice(localReceivePackFactory(knot), { repoFullName: 'owner/repo', ref: 'refs/heads/main', want: first, token: 'tok' })
54
55 const second = fx.commit(work, 'b.txt', 'b'.repeat(8000))
56 fx.pushTo(work, gh, 'HEAD:refs/heads/main')
57 const result = await runSplice(localReceivePackFactory(knot), { repoFullName: 'owner/repo', ref: 'refs/heads/main', want: second, token: 'tok' })
58
59 expect(result).toEqual({ status: 'synced', sha: second })
60 expect(fx.revParse(knot, 'refs/heads/main')).toBe(second)
61 })
62
63 it('no-ops when the knot tip already equals want', async () => {
64 const gh = fx.initBare('gh.git')
65 const work = fx.initWork('work')
66 const sha = fx.commit(work, 'a.txt', 'hello')
67 fx.pushTo(work, gh, 'HEAD:refs/heads/main')
68 const knot = fx.initBare('knot.git')
69 wireGithub(gh)
70
71 await runSplice(localReceivePackFactory(knot), { repoFullName: 'owner/repo', ref: 'refs/heads/main', want: sha, token: 'tok' })
72 const again = await runSplice(localReceivePackFactory(knot), { repoFullName: 'owner/repo', ref: 'refs/heads/main', want: sha, token: 'tok' })
73
74 expect(again).toEqual({ status: 'already-synced', sha })
75 })
76
77 it('aborts a push that exceeds the byte cap', async () => {
78 const gh = fx.initBare('gh.git')
79 const work = fx.initWork('work')
80 const sha = fx.commit(work, 'a.txt', 'x'.repeat(20_000))
81 fx.pushTo(work, gh, 'HEAD:refs/heads/main')
82 const knot = fx.initBare('knot.git')
83 wireGithub(gh)
84
85 const prev = process.env.NUXT_MAX_PACK_BYTES
86 process.env.NUXT_MAX_PACK_BYTES = '10'
87 try {
88 await expect(
89 runSplice(localReceivePackFactory(knot), { repoFullName: 'owner/repo', ref: 'refs/heads/main', want: sha, token: 'tok' }),
90 ).rejects.toMatchObject({ constructor: RemoteRejectedError, reason: 'too-big' })
91 }
92 finally {
93 if (prev === undefined) delete process.env.NUXT_MAX_PACK_BYTES
94 else process.env.NUXT_MAX_PACK_BYTES = prev
95 }
96 })
97
98 it('deletes a ref and treats an already-absent ref as success', async () => {
99 const gh = fx.initBare('gh.git')
100 const work = fx.initWork('work')
101 const sha = fx.commit(work, 'a.txt', 'hello')
102 fx.pushTo(work, gh, 'HEAD:refs/heads/main')
103 const knot = fx.initBare('knot.git')
104 wireGithub(gh)
105
106 await runSplice(localReceivePackFactory(knot), { repoFullName: 'owner/repo', ref: 'refs/heads/main', want: sha, token: 'tok' })
107
108 expect(await runSpliceDelete(localReceivePackFactory(knot), 'refs/heads/main')).toEqual({ status: 'synced' })
109 expect(() => fx.revParse(knot, 'refs/heads/main')).toThrow(/unknown revision|ambiguous argument|fatal/)
110
111 expect(await runSpliceDelete(localReceivePackFactory(knot), 'refs/heads/gone')).toEqual({ status: 'already-absent' })
112 })
113})