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.
- package/README.md +238 -16
- package/bin/git-workspace.js +63 -9
- package/completions/git-workspace.bash +26 -0
- package/docs/hooks.md +214 -0
- package/lib/add.js +55 -7
- package/lib/apply-pull.js +140 -0
- package/lib/checkpoint.js +208 -0
- package/lib/clone-repository.js +51 -0
- package/lib/commit-manifest.js +29 -0
- package/lib/completion.js +93 -0
- package/lib/git-network-environment.js +31 -0
- package/lib/hook-context.js +239 -0
- package/lib/hooks.js +87 -0
- package/lib/init.js +17 -4
- package/lib/manifest.js +102 -0
- package/lib/plan-pull.js +332 -0
- package/lib/pull-state.js +113 -0
- package/lib/pull.js +269 -0
- package/lib/push-manifest.js +53 -0
- package/lib/registry.js +23 -12
- package/lib/remove.js +27 -2
- package/lib/rename-root.js +182 -84
- package/lib/rename.js +92 -16
- package/package.json +3 -1
- package/test/add-offline.js +240 -0
- package/test/checkpoint.js +243 -0
- package/test/completion.js +180 -0
- package/test/fixtures/checkpoint.js +14 -0
- package/test/fixtures/completion.bash +28 -0
- package/test/helpers.js +60 -2
- package/test/hooks.js +451 -0
- package/test/lifecycle.js +393 -13
- package/test/manifest-push.js +135 -0
- package/test/manifest.js +170 -0
- package/test/pull.js +696 -0
package/lib/pull.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import fs from 'bare-fs'
|
|
2
|
+
import path from 'bare-path'
|
|
3
|
+
import { spawnSync } from 'bare-subprocess'
|
|
4
|
+
import { manifestDirectory, readManifest } from './registry.js'
|
|
5
|
+
import { readCheckpoint, readCommitRef, recordCheckpoint, snapshot } from './checkpoint.js'
|
|
6
|
+
import { emptyManifest, manifestNames } from './manifest.js'
|
|
7
|
+
import planPull, { changes } from './plan-pull.js'
|
|
8
|
+
import applyPull, { layoutAfter, pendingRename } from './apply-pull.js'
|
|
9
|
+
import { readPullState, writePullState, statePath, targetRef, lockPull } from './pull-state.js'
|
|
10
|
+
import repositoryPath from './repository-path.js'
|
|
11
|
+
import Hooks from './hooks.js'
|
|
12
|
+
import { validateHookProgress } from './hook-context.js'
|
|
13
|
+
|
|
14
|
+
export default function pull({ dryRun = false, noHooks = false } = {}) {
|
|
15
|
+
const directory = manifestDirectory()
|
|
16
|
+
const hooks = new Hooks({ noHooks: noHooks || dryRun })
|
|
17
|
+
if (
|
|
18
|
+
fs.lstatSync(directory).isSymbolicLink() ||
|
|
19
|
+
!fs.lstatSync(path.join(directory, '.git')).isDirectory()
|
|
20
|
+
) {
|
|
21
|
+
throw new Error('Manifest must be a standalone Git repository')
|
|
22
|
+
}
|
|
23
|
+
const unlock = lockPull(directory)
|
|
24
|
+
try {
|
|
25
|
+
const manifest = path.join(directory, 'workspace.json')
|
|
26
|
+
const local = readManifest(manifest)
|
|
27
|
+
const contents = fs.readFileSync(manifest, 'utf8')
|
|
28
|
+
if (
|
|
29
|
+
fs.realpathSync(git(directory, ['rev-parse', '--show-toplevel'])) !==
|
|
30
|
+
fs.realpathSync(directory)
|
|
31
|
+
) {
|
|
32
|
+
throw new Error('Manifest must be a standalone Git repository')
|
|
33
|
+
}
|
|
34
|
+
const head = git(directory, ['rev-parse', '--verify', 'HEAD^{commit}'])
|
|
35
|
+
if (git(directory, ['status', '--porcelain', '--untracked-files=all'])) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'Manifest repository has uncommitted changes; commit or stash them before pulling'
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
if (JSON.stringify(local) !== JSON.stringify(snapshot(directory, head))) {
|
|
41
|
+
throw new Error('workspace.json differs from HEAD')
|
|
42
|
+
}
|
|
43
|
+
const symbolic = run(directory, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
|
|
44
|
+
if (symbolic.status === 1) {
|
|
45
|
+
throw new Error('Manifest repository has a detached HEAD; check out a branch before pulling')
|
|
46
|
+
}
|
|
47
|
+
if (symbolic.status !== 0) fail(symbolic)
|
|
48
|
+
const branch = symbolic.stdout.toString().trim()
|
|
49
|
+
const progress = readPullState(directory)
|
|
50
|
+
if (progress) return resume(directory, progress, head, branch, dryRun, hooks)
|
|
51
|
+
const remote = config(directory, `branch.${branch}.remote`) || 'origin'
|
|
52
|
+
const merge = config(directory, `branch.${branch}.merge`) || `refs/heads/${branch}`
|
|
53
|
+
if (remote === '.' || !merge.startsWith('refs/heads/')) {
|
|
54
|
+
throw new Error('Manifest branch must track a remote branch')
|
|
55
|
+
}
|
|
56
|
+
git(directory, ['check-ref-format', merge])
|
|
57
|
+
git(directory, ['remote', 'get-url', remote])
|
|
58
|
+
const applied = readCheckpoint(directory)
|
|
59
|
+
if (applied && !ancestor(directory, applied, head)) {
|
|
60
|
+
throw new Error('Manifest HEAD does not descend from the locally applied commit')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
git(directory, [
|
|
64
|
+
'fetch',
|
|
65
|
+
'--no-tags',
|
|
66
|
+
'--no-recurse-submodules',
|
|
67
|
+
'--no-auto-maintenance',
|
|
68
|
+
'--',
|
|
69
|
+
remote,
|
|
70
|
+
merge
|
|
71
|
+
])
|
|
72
|
+
const fetched = git(directory, ['rev-parse', '--verify', 'FETCH_HEAD^{commit}'])
|
|
73
|
+
let target = fetched
|
|
74
|
+
if (ancestor(directory, fetched, head)) target = head
|
|
75
|
+
else if (!ancestor(directory, head, fetched)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'Local and remote manifest histories have diverged; resolve them before pulling'
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
const previous = applied ? snapshot(directory, applied) : emptyManifest()
|
|
81
|
+
const desired = snapshot(directory, target)
|
|
82
|
+
const plan = planPull(previous, desired, directory)
|
|
83
|
+
if (
|
|
84
|
+
git(directory, ['rev-parse', '--verify', 'HEAD^{commit}']) !== head ||
|
|
85
|
+
readCheckpoint(directory) !== applied ||
|
|
86
|
+
fs.readFileSync(manifest, 'utf8') !== contents ||
|
|
87
|
+
git(directory, ['status', '--porcelain', '--untracked-files=all'])
|
|
88
|
+
) {
|
|
89
|
+
throw new Error('Local manifest state changed during planning; retry')
|
|
90
|
+
}
|
|
91
|
+
const result = {
|
|
92
|
+
...plan,
|
|
93
|
+
head,
|
|
94
|
+
fetched,
|
|
95
|
+
target,
|
|
96
|
+
applied,
|
|
97
|
+
remote,
|
|
98
|
+
branch: merge.slice('refs/heads/'.length),
|
|
99
|
+
dryRun
|
|
100
|
+
}
|
|
101
|
+
if (dryRun || plan.blockers.length) return result
|
|
102
|
+
const state = {
|
|
103
|
+
version: 1,
|
|
104
|
+
head,
|
|
105
|
+
target,
|
|
106
|
+
applied,
|
|
107
|
+
remote,
|
|
108
|
+
merge,
|
|
109
|
+
localBranch: branch,
|
|
110
|
+
completed: 0,
|
|
111
|
+
pending: false
|
|
112
|
+
}
|
|
113
|
+
const pinned = readCommitRef(directory, targetRef)
|
|
114
|
+
git(directory, [
|
|
115
|
+
'update-ref',
|
|
116
|
+
'--no-deref',
|
|
117
|
+
targetRef,
|
|
118
|
+
target,
|
|
119
|
+
pinned || '0'.repeat(target.length)
|
|
120
|
+
])
|
|
121
|
+
writePullState(directory, state)
|
|
122
|
+
execute(directory, state, previous, desired, hooks)
|
|
123
|
+
return result
|
|
124
|
+
} finally {
|
|
125
|
+
unlock()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resume(directory, state, head, branch, dryRun, hooks) {
|
|
130
|
+
const previous = state.applied ? snapshot(directory, state.applied) : emptyManifest()
|
|
131
|
+
const desired = snapshot(directory, state.target)
|
|
132
|
+
const original = changes(previous, desired)
|
|
133
|
+
validateHookProgress(state, original.actions)
|
|
134
|
+
const applied = readCheckpoint(directory)
|
|
135
|
+
const finished = state.completed === original.actions.length && !state.pending
|
|
136
|
+
if (
|
|
137
|
+
original.blockers.length ||
|
|
138
|
+
state.completed > original.actions.length ||
|
|
139
|
+
(state.pending && state.completed === original.actions.length) ||
|
|
140
|
+
branch !== state.localBranch ||
|
|
141
|
+
![state.head, state.target].includes(head) ||
|
|
142
|
+
((state.completed > 0 || state.pending) && head !== state.target) ||
|
|
143
|
+
!ancestor(directory, state.head, state.target) ||
|
|
144
|
+
(state.applied && !ancestor(directory, state.applied, state.head)) ||
|
|
145
|
+
(applied !== state.applied && !(finished && applied === state.target)) ||
|
|
146
|
+
readCommitRef(directory, targetRef) !== state.target
|
|
147
|
+
) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
'Manifest or local pull progress changed unexpectedly; inspect .git/workspace-pull.json'
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
const layout = layoutAfter(previous, desired, original.actions.slice(0, state.completed))
|
|
153
|
+
const interrupted = state.pending ? original.actions[state.completed] : null
|
|
154
|
+
let plan
|
|
155
|
+
if (interrupted && pendingRename(interrupted)) {
|
|
156
|
+
plan = { actions: original.actions.slice(state.completed), blockers: [] }
|
|
157
|
+
if (dryRun) {
|
|
158
|
+
plan.blockers.push(
|
|
159
|
+
'Interrupted rename needs repair before remaining changes can be checked; rerun git workspace pull'
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
} else plan = planPull(layout, desired, directory)
|
|
163
|
+
const result = {
|
|
164
|
+
...plan,
|
|
165
|
+
head,
|
|
166
|
+
fetched: state.target,
|
|
167
|
+
target: state.target,
|
|
168
|
+
applied: state.applied,
|
|
169
|
+
remote: state.remote,
|
|
170
|
+
branch: state.merge.slice('refs/heads/'.length),
|
|
171
|
+
dryRun,
|
|
172
|
+
resumed: true,
|
|
173
|
+
completed: state.completed
|
|
174
|
+
}
|
|
175
|
+
if (dryRun) return result
|
|
176
|
+
// execute repeats preflight after recovering the pending operation.
|
|
177
|
+
execute(directory, state, previous, desired, hooks, true)
|
|
178
|
+
return result
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function execute(directory, state, previous, desired, hooks, resumed = false) {
|
|
182
|
+
if (git(directory, ['rev-parse', '--verify', 'HEAD^{commit}']) !== state.target) {
|
|
183
|
+
git(directory, [
|
|
184
|
+
'-c',
|
|
185
|
+
'submodule.recurse=false',
|
|
186
|
+
'merge',
|
|
187
|
+
'--ff-only',
|
|
188
|
+
'--no-edit',
|
|
189
|
+
'--no-stat',
|
|
190
|
+
state.target
|
|
191
|
+
])
|
|
192
|
+
}
|
|
193
|
+
applyPull(directory, state, previous, desired, hooks, resumed)
|
|
194
|
+
if (
|
|
195
|
+
git(directory, ['rev-parse', '--verify', 'HEAD^{commit}']) !== state.target ||
|
|
196
|
+
![state.applied, state.target].includes(readCheckpoint(directory)) ||
|
|
197
|
+
git(directory, ['status', '--porcelain', '--untracked-files=all'])
|
|
198
|
+
) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
'Manifest state changed during pull; resolve the changes and rerun git workspace pull'
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
recordCheckpoint(directory, {
|
|
204
|
+
replacedPaths: new Set(manifestNames(desired).map(repositoryPath))
|
|
205
|
+
})
|
|
206
|
+
// Keep the target pinned until progress is removed, including a crash after verification.
|
|
207
|
+
fs.unlinkSync(statePath(directory))
|
|
208
|
+
git(directory, ['update-ref', '--no-deref', '-d', targetRef, state.target])
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function formatPlan(plan) {
|
|
212
|
+
const lines = [`Manifest: ${plan.remote}/${plan.branch}`]
|
|
213
|
+
if (plan.resumed) lines.push(`Resuming saved pull (${plan.completed} changes already completed)`)
|
|
214
|
+
const preview = plan.dryRun || plan.blockers.length
|
|
215
|
+
if (plan.target !== plan.head) {
|
|
216
|
+
lines.push(preview ? 'Would fast-forward the manifest' : 'Fast-forwarded the manifest')
|
|
217
|
+
} else if (plan.fetched !== plan.head) {
|
|
218
|
+
lines.push('Manifest is ahead of the remote; pushing remains manual')
|
|
219
|
+
} else {
|
|
220
|
+
lines.push('Manifest is up to date')
|
|
221
|
+
}
|
|
222
|
+
if (!plan.applied) {
|
|
223
|
+
lines.push(
|
|
224
|
+
preview
|
|
225
|
+
? 'First setup: existing matching checkouts would be adopted'
|
|
226
|
+
: 'First setup: existing matching checkouts adopted'
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
for (const action of plan.actions) {
|
|
230
|
+
if (action.type === 'rename') lines.push(`rename ${action.from} -> ${action.to}`)
|
|
231
|
+
else lines.push(`${action.present ? 'adopt' : action.type} ${action.from || action.to}`)
|
|
232
|
+
}
|
|
233
|
+
if (!plan.actions.length) lines.push('No layout changes planned')
|
|
234
|
+
for (const blocker of plan.blockers) lines.push(`blocked: ${blocker}`)
|
|
235
|
+
if (plan.dryRun) lines.push('Dry run: no manifest checkout or workspace changes applied')
|
|
236
|
+
else if (plan.blockers.length) lines.push('No manifest checkout or workspace changes applied')
|
|
237
|
+
else lines.push('Workspace applied; managed repositories were not fetched or pulled')
|
|
238
|
+
return lines.join('\n')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function config(directory, key) {
|
|
242
|
+
const result = run(directory, ['config', '--get', key])
|
|
243
|
+
if (result.status === 1) return null
|
|
244
|
+
if (result.status !== 0) fail(result)
|
|
245
|
+
return result.stdout.toString().trim()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function ancestor(directory, from, to) {
|
|
249
|
+
const result = run(directory, ['merge-base', '--is-ancestor', from, to])
|
|
250
|
+
if (result.status === 1) return false
|
|
251
|
+
if (result.status !== 0) fail(result)
|
|
252
|
+
return true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function run(directory, args) {
|
|
256
|
+
const result = spawnSync('git', ['--no-optional-locks', '-C', directory, ...args])
|
|
257
|
+
if (result.error) throw result.error
|
|
258
|
+
return result
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function fail(result) {
|
|
262
|
+
throw new Error(result.stderr.toString().trim() || 'Git command failed')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function git(directory, args) {
|
|
266
|
+
const result = run(directory, args)
|
|
267
|
+
if (result.status !== 0) fail(result)
|
|
268
|
+
return result.stdout.toString().replace(/\r?\n$/, '')
|
|
269
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawnSync } from 'bare-subprocess'
|
|
2
|
+
import networkEnvironment from './git-network-environment.js'
|
|
3
|
+
|
|
4
|
+
export default function pushManifest(directory) {
|
|
5
|
+
try {
|
|
6
|
+
const symbolic = spawnSync('git', [
|
|
7
|
+
'-C',
|
|
8
|
+
directory,
|
|
9
|
+
'symbolic-ref',
|
|
10
|
+
'--quiet',
|
|
11
|
+
'--short',
|
|
12
|
+
'HEAD'
|
|
13
|
+
])
|
|
14
|
+
if (!symbolic.error && !symbolic.signal && symbolic.status === 1) {
|
|
15
|
+
throw new Error('Manifest repository has a detached HEAD; check out a branch before pushing')
|
|
16
|
+
}
|
|
17
|
+
const branch = output(symbolic)
|
|
18
|
+
const remote = config(directory, `branch.${branch}.remote`) || 'origin'
|
|
19
|
+
const merge = config(directory, `branch.${branch}.merge`) || `refs/heads/${branch}`
|
|
20
|
+
if (remote === '.' || !merge.startsWith('refs/heads/')) {
|
|
21
|
+
throw new Error('Manifest branch must track a remote branch')
|
|
22
|
+
}
|
|
23
|
+
git(directory, ['push', '--set-upstream', '--no-follow-tags', '--', remote, `HEAD:${merge}`], {
|
|
24
|
+
env: networkEnvironment(directory)
|
|
25
|
+
})
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error(`warning: could not push workspace manifest: ${err.message}`)
|
|
28
|
+
console.error(
|
|
29
|
+
'hint: changes are saved locally; repeat add to retry, or push the manifest branch manually'
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function config(directory, key) {
|
|
35
|
+
const result = spawnSync('git', ['-C', directory, 'config', '--get', key])
|
|
36
|
+
if (result.error) throw result.error
|
|
37
|
+
if (!result.signal && result.status === 1) return null
|
|
38
|
+
return output(result)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function git(directory, args, options) {
|
|
42
|
+
return output(spawnSync('git', ['-C', directory, ...args], options))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function output(result) {
|
|
46
|
+
if (result.error) throw result.error
|
|
47
|
+
if (result.signal || result.status !== 0) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
result.stderr?.toString().trim() || `Git command failed (${result.signal || result.status})`
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
return result.stdout.toString().trim()
|
|
53
|
+
}
|
package/lib/registry.js
CHANGED
|
@@ -3,6 +3,7 @@ import os from 'bare-os'
|
|
|
3
3
|
import path from 'bare-path'
|
|
4
4
|
import repositoryPath from './repository-path.js'
|
|
5
5
|
import repositoryName from './repository-name.js'
|
|
6
|
+
import { manifestNames, updateManifest, validateManifest } from './manifest.js'
|
|
6
7
|
|
|
7
8
|
export function configPath() {
|
|
8
9
|
return path.join(os.homedir(), '.config', 'git-workspace', 'config.json')
|
|
@@ -33,10 +34,14 @@ export function registryPath() {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function readRegistry(registry = registryPath()) {
|
|
36
|
-
|
|
37
|
+
return manifestNames(readManifest(registry)).map(repositoryPath)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function readManifest(registry = registryPath()) {
|
|
41
|
+
let manifest
|
|
37
42
|
try {
|
|
38
43
|
if (!fs.lstatSync(registry).isFile()) throw new Error('Manifest must be a regular file')
|
|
39
|
-
|
|
44
|
+
manifest = JSON.parse(fs.readFileSync(registry, 'utf8'))
|
|
40
45
|
} catch (err) {
|
|
41
46
|
if (err.code === 'ENOENT') {
|
|
42
47
|
throw new Error('Workspace is not initialized; run git workspace init owner/repo')
|
|
@@ -44,24 +49,30 @@ export function readRegistry(registry = registryPath()) {
|
|
|
44
49
|
throw err
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return worktrees.map(repositoryPath)
|
|
52
|
+
validateManifest(manifest)
|
|
53
|
+
return manifest
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
export function updateRegistry(
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
export function updateRegistry(
|
|
57
|
+
update,
|
|
58
|
+
{
|
|
59
|
+
directory = manifestDirectory(),
|
|
60
|
+
registry = path.join(directory, 'workspace.json'),
|
|
61
|
+
rename
|
|
62
|
+
} = {}
|
|
63
|
+
) {
|
|
64
|
+
const lock = path.join(directory, '.git', 'workspace.lock')
|
|
65
|
+
if (!fs.existsSync(path.join(directory, '.git'))) {
|
|
57
66
|
throw new Error('Workspace is not initialized; run git workspace init owner/repo')
|
|
58
67
|
}
|
|
59
68
|
fs.mkdirSync(lock)
|
|
60
69
|
const pending = path.join(lock, 'worktrees.json')
|
|
61
70
|
try {
|
|
62
|
-
const
|
|
71
|
+
const previous = readManifest(registry)
|
|
72
|
+
const worktrees = update(manifestNames(previous).map(repositoryPath))
|
|
63
73
|
const entries = worktrees.map(repositoryName)
|
|
64
|
-
|
|
74
|
+
const manifest = updateManifest(previous, entries, rename)
|
|
75
|
+
fs.writeFileSync(pending, JSON.stringify(manifest, null, 2) + '\n')
|
|
65
76
|
fs.renameSync(pending, registry)
|
|
66
77
|
} finally {
|
|
67
78
|
if (fs.existsSync(pending)) fs.unlinkSync(pending)
|
package/lib/remove.js
CHANGED
|
@@ -5,8 +5,28 @@ import { manifestDirectory, updateRegistry } from './registry.js'
|
|
|
5
5
|
import repositoryPath from './repository-path.js'
|
|
6
6
|
import checkedOutBranch from './checked-out-branch.js'
|
|
7
7
|
import removeEmptyParents from './remove-empty-parents.js'
|
|
8
|
+
import commitManifest from './commit-manifest.js'
|
|
9
|
+
import { refreshCheckpoint } from './checkpoint.js'
|
|
10
|
+
import { assertNoPendingPull } from './pull-state.js'
|
|
11
|
+
import { withPostHooks } from './hook-context.js'
|
|
8
12
|
|
|
9
|
-
export default function remove(repository) {
|
|
13
|
+
export default function remove(repository, options = {}) {
|
|
14
|
+
assertNoPendingPull()
|
|
15
|
+
return withPostHooks('remove', { repository }, options, () => {
|
|
16
|
+
removeCheckout(repository)
|
|
17
|
+
try {
|
|
18
|
+
commitManifest(manifestDirectory(), `Remove ${repository}`)
|
|
19
|
+
} catch (err) {
|
|
20
|
+
err.message = `Removed ${repository}, but could not commit the manifest: ${err.message}`
|
|
21
|
+
err.hint =
|
|
22
|
+
'the checkout is already removed; resolve the Git error and commit only workspace.json in the manifest repository'
|
|
23
|
+
throw err
|
|
24
|
+
}
|
|
25
|
+
refreshCheckpoint()
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function removeCheckout(repository, { update = updateRegistry } = {}) {
|
|
10
30
|
const absolute = repositoryPath(repository)
|
|
11
31
|
let canonical = absolute
|
|
12
32
|
let exists = true
|
|
@@ -17,9 +37,14 @@ export default function remove(repository) {
|
|
|
17
37
|
exists = false
|
|
18
38
|
}
|
|
19
39
|
|
|
20
|
-
|
|
40
|
+
update((worktrees) => {
|
|
21
41
|
const target = worktrees.includes(absolute) ? absolute : canonical
|
|
22
42
|
const [base, requested] = repository.split('@')
|
|
43
|
+
if (!requested && worktrees.some((worktree) => worktree.startsWith(absolute + '@'))) {
|
|
44
|
+
const err = new Error(`cannot remove ${repository}: linked worktrees exist`)
|
|
45
|
+
err.hint = 'remove the linked worktrees first'
|
|
46
|
+
throw err
|
|
47
|
+
}
|
|
23
48
|
let branch = requested
|
|
24
49
|
if (requested && exists) {
|
|
25
50
|
const root = repositoryPath(base)
|