git-workspace-bare 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ import test from 'brittle'
2
+ import fs from 'bare-fs'
3
+ import path from 'bare-path'
4
+ import setup from './helpers.js'
5
+
6
+ function read(registry) {
7
+ return JSON.parse(fs.readFileSync(registry, 'utf8'))
8
+ }
9
+
10
+ test('stable IDs survive repeated adds, branch renames, and root owner changes', (t) => {
11
+ const { ok, git, registry } = setup(t)
12
+ t.alike(read(registry), { version: 1, repositories: [] })
13
+ ok('add', 'owner/repo@feature/jobs')
14
+ ok('add', 'owner/repo@other')
15
+ const initial = read(registry)
16
+ const root = initial.repositories[0]
17
+ t.ok(/^[0-9a-f-]{36}$/.test(root.id), 'repository UUID')
18
+ t.ok(
19
+ root.worktrees.every(({ id }) => /^[0-9a-f-]{36}$/.test(id)),
20
+ 'worktree UUIDs'
21
+ )
22
+ t.is(new Set([root.id, ...root.worktrees.map(({ id }) => id)]).size, 3)
23
+ const head = git('-C', path.dirname(registry), 'rev-parse', 'HEAD')
24
+ ok('add', 'owner/repo@feature/jobs')
25
+ t.alike(read(registry), initial, 'idempotent add keeps IDs')
26
+ t.is(git('-C', path.dirname(registry), 'rev-parse', 'HEAD'), head, 'no ID churn commit')
27
+ ok('rename', 'owner/repo@feature/jobs', 'owner/repo@fixes')
28
+ t.alike(read(registry).repositories[0], {
29
+ id: root.id,
30
+ name: root.name,
31
+ worktrees: [{ id: root.worktrees[0].id, branch: 'fixes' }, root.worktrees[1]]
32
+ })
33
+ ok('rename', 'owner/repo', 'other/new')
34
+ const renamed = read(registry).repositories[0]
35
+ t.is(renamed.id, root.id, 'root identity retained across owners')
36
+ t.is(renamed.name, 'other/new')
37
+ t.alike(
38
+ renamed.worktrees.map(({ id }) => id),
39
+ root.worktrees.map(({ id }) => id)
40
+ )
41
+ t.is(ok('list'), 'other/new\nother/new@fixes\nother/new@other\n')
42
+ })
43
+
44
+ test('old names can be reused without reusing repository or worktree identities', (t) => {
45
+ const { ok, registry } = setup(t)
46
+ ok('add', 'owner/repo@jobs')
47
+ const original = read(registry).repositories[0]
48
+ ok('rename', 'owner/repo@jobs', 'owner/repo@fixes')
49
+ ok('add', 'owner/repo@jobs')
50
+ const reused = read(registry).repositories[0]
51
+ t.not(reused.worktrees.find(({ branch }) => branch === 'jobs').id, original.worktrees[0].id)
52
+ const removedId = reused.worktrees.find(({ branch }) => branch === 'jobs').id
53
+ ok('remove', 'owner/repo@jobs')
54
+ ok('add', 'owner/repo@jobs')
55
+ t.not(
56
+ read(registry).repositories[0].worktrees.find(({ branch }) => branch === 'jobs').id,
57
+ removedId
58
+ )
59
+ ok('rename', 'owner/repo', 'owner/new')
60
+ ok('add', 'owner/repo')
61
+ const replacement = read(registry).repositories.find(({ name }) => name === 'owner/repo')
62
+ t.not(replacement.id, original.id, 'reusing a renamed root name gets a fresh ID')
63
+ ok('remove', 'owner/repo')
64
+ ok('add', 'owner/repo')
65
+ t.not(read(registry).repositories.find(({ name }) => name === 'owner/repo').id, replacement.id)
66
+ })
67
+
68
+ test('version-1 identities are shared unchanged with another machine', (t) => {
69
+ const { ok, cliAt, git, home, registry, upstream } = setup(t)
70
+ ok('add', 'owner/repo@feature/jobs')
71
+ const contents = fs.readFileSync(registry, 'utf8')
72
+ git('clone', '--bare', path.dirname(registry), path.join(upstream, 'owner/workspace.git'))
73
+ const second = path.join(home, 'second')
74
+ fs.mkdirSync(second)
75
+ t.is(cliAt(second, 'init', 'owner/workspace').status, 0)
76
+ t.is(
77
+ fs.readFileSync(path.join(second, 'workspace/owner/workspace/workspace.json'), 'utf8'),
78
+ contents
79
+ )
80
+ t.absent(
81
+ fs.existsSync(path.join(second, 'workspace/owner/repo')),
82
+ 'init does not apply the manifest'
83
+ )
84
+ })
85
+
86
+ test('IDs survive a failed commit and retry', (t) => {
87
+ const { cli, ok, git, registry } = setup(t)
88
+ const directory = path.dirname(registry)
89
+ const hooks = path.join(directory, '.git/hooks')
90
+ git('-C', directory, 'config', 'core.hooksPath', hooks)
91
+ const hook = path.join(hooks, 'pre-commit')
92
+ fs.writeFileSync(hook, '#!/bin/sh\nexit 1\n')
93
+ fs.chmodSync(hook, 0o755)
94
+ t.is(cli('add', 'owner/repo@jobs').status, 1)
95
+ const pending = read(registry)
96
+ t.is(pending.repositories.length, 1)
97
+ fs.unlinkSync(hook)
98
+ ok('add', 'owner/repo@jobs')
99
+ t.alike(read(registry), pending)
100
+ t.alike(JSON.parse(git('-C', directory, 'show', 'HEAD:workspace.json')), pending)
101
+ })
102
+
103
+ test('invalid manifests fail before filesystem or identity changes', (t) => {
104
+ const { cli, ok, git, registry, root } = setup(t)
105
+ ok('add', 'owner/repo@jobs')
106
+ const directory = path.dirname(registry)
107
+ const head = git('-C', directory, 'rev-parse', 'HEAD')
108
+ const good = read(registry)
109
+ const invalid = [
110
+ { ...good, version: 99 },
111
+ { version: 1, repositories: [{ ...good.repositories[0], id: '' }] },
112
+ {
113
+ version: 1,
114
+ repositories: [
115
+ { ...good.repositories[0], worktrees: [{ id: good.repositories[0].id, branch: 'jobs' }] }
116
+ ]
117
+ },
118
+ {
119
+ version: 1,
120
+ repositories: [good.repositories[0], { ...good.repositories[0], id: 'another' }]
121
+ },
122
+ {
123
+ version: 1,
124
+ repositories: [
125
+ {
126
+ ...good.repositories[0],
127
+ worktrees: [
128
+ { id: 'first', branch: 'jobs' },
129
+ { id: 'second', branch: 'jobs' }
130
+ ]
131
+ }
132
+ ]
133
+ },
134
+ { version: 1, repositories: [{ ...good.repositories[0], name: '../escape' }] },
135
+ {
136
+ version: 1,
137
+ repositories: [{ ...good.repositories[0], worktrees: [{ id: 'first', branch: '../escape' }] }]
138
+ },
139
+ [],
140
+ ['owner/repo'],
141
+ ['owner/repo', 'owner/repo@jobs'],
142
+ ['owner/repo', 'owner/repo'],
143
+ ['owner/repo@jobs']
144
+ ]
145
+ for (const manifest of invalid) {
146
+ const contents = JSON.stringify(manifest)
147
+ fs.writeFileSync(registry, contents)
148
+ for (const args of [
149
+ ['list'],
150
+ ['init', 'owner/workspace'],
151
+ ['add', 'owner/repo'],
152
+ ['remove', 'owner/repo@jobs'],
153
+ ['rename', 'owner/repo', 'owner/new']
154
+ ]) {
155
+ t.is(cli(...args).status, 1, args[0] + ' rejects invalid manifest')
156
+ }
157
+ t.is(fs.readFileSync(registry, 'utf8'), contents)
158
+ t.ok(fs.existsSync(root + '@jobs'))
159
+ }
160
+ t.is(git('-C', directory, 'rev-parse', 'HEAD'), head)
161
+ })
162
+
163
+ test('a missing root with registered worktrees cannot be removed from the new schema', (t) => {
164
+ const { cli, ok, registry, root } = setup(t)
165
+ ok('add', 'owner/repo@jobs')
166
+ const before = read(registry)
167
+ fs.renameSync(root, root + '-moved')
168
+ t.is(cli('remove', 'owner/repo').status, 1)
169
+ t.alike(read(registry), before)
170
+ })