git-workspace-bare 0.1.0 → 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 +246 -24
- 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/rename-root.js
CHANGED
|
@@ -5,7 +5,7 @@ import { manifestDirectory, updateRegistry } from './registry.js'
|
|
|
5
5
|
import repositoryPath from './repository-path.js'
|
|
6
6
|
import removeEmptyParents from './remove-empty-parents.js'
|
|
7
7
|
|
|
8
|
-
export default function renameRoot(from, to) {
|
|
8
|
+
export default function renameRoot(from, to, { update = updateRegistry } = {}) {
|
|
9
9
|
const source = repositoryPath(from)
|
|
10
10
|
const destination = repositoryPath(to)
|
|
11
11
|
if (from === to) throw new Error(`${to} is already the registered name`)
|
|
@@ -13,101 +13,100 @@ export default function renameRoot(from, to) {
|
|
|
13
13
|
const moved = []
|
|
14
14
|
let originalConfig
|
|
15
15
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const gitDirectory = path.join(canonical, '.git')
|
|
24
|
-
if (
|
|
25
|
-
fs.lstatSync(source).isSymbolicLink() ||
|
|
26
|
-
!fs.lstatSync(gitDirectory).isDirectory() ||
|
|
27
|
-
fs.realpathSync(git(source, 'rev-parse', '--show-toplevel')) !== canonical ||
|
|
28
|
-
fs.realpathSync(git(source, 'rev-parse', '--path-format=absolute', '--git-common-dir')) !==
|
|
29
|
-
gitDirectory
|
|
30
|
-
) {
|
|
31
|
-
throw new Error('Source is not a standalone root repository')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const urls = configValues(source, 'remote.origin.url')
|
|
35
|
-
if (urls.length !== 1 || !matches(urls[0], from, to)) {
|
|
36
|
-
throw new Error(`Source origin does not match ${from} or ${to}`)
|
|
37
|
-
}
|
|
38
|
-
const pushurls = configValues(source, 'remote.origin.pushurl')
|
|
39
|
-
const entries = git(source, 'worktree', 'list', '--porcelain', '-z')
|
|
40
|
-
.split('\0\0')
|
|
41
|
-
.filter(Boolean)
|
|
42
|
-
.map((entry) => entry.split('\0'))
|
|
43
|
-
const moves = [[source, destination]]
|
|
44
|
-
for (const entry of entries.slice(1)) {
|
|
45
|
-
const directory = entry[0].slice('worktree '.length)
|
|
46
|
-
if (!exists(directory)) {
|
|
47
|
-
throw new Error(`Missing linked worktree: ${directory}; repair or prune it first`)
|
|
16
|
+
update(
|
|
17
|
+
(worktrees) => {
|
|
18
|
+
if (!worktrees.includes(source)) throw new Error(`${from} is not registered`)
|
|
19
|
+
if (!exists(source)) throw new Error(`${from} is not checked out`)
|
|
20
|
+
const canonical = fs.realpathSync(source)
|
|
21
|
+
if (canonical === fs.realpathSync(manifestDirectory())) {
|
|
22
|
+
throw new Error('Cannot rename the active manifest repository')
|
|
48
23
|
}
|
|
24
|
+
const gitDirectory = path.join(canonical, '.git')
|
|
49
25
|
if (
|
|
50
|
-
fs.
|
|
51
|
-
|
|
26
|
+
fs.lstatSync(source).isSymbolicLink() ||
|
|
27
|
+
!fs.lstatSync(gitDirectory).isDirectory() ||
|
|
28
|
+
fs.realpathSync(git(source, 'rev-parse', '--show-toplevel')) !== canonical ||
|
|
52
29
|
fs.realpathSync(
|
|
53
|
-
git(
|
|
30
|
+
git(source, 'rev-parse', '--path-format=absolute', '--git-common-dir')
|
|
54
31
|
) !== gitDirectory
|
|
55
32
|
) {
|
|
56
|
-
throw new Error(
|
|
33
|
+
throw new Error('Source is not a standalone root repository')
|
|
57
34
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.some((entry) => entry.startsWith('160000 '))
|
|
63
|
-
) {
|
|
64
|
-
throw new Error(`Linked worktree contains submodules: ${directory}`)
|
|
65
|
-
}
|
|
66
|
-
if (!directory.startsWith(source + '@')) continue
|
|
67
|
-
if (entry.some((field) => field === 'locked' || field.startsWith('locked '))) {
|
|
68
|
-
throw new Error(`Linked worktree is locked: ${directory}`)
|
|
35
|
+
|
|
36
|
+
const urls = configValues(source, 'remote.origin.url')
|
|
37
|
+
if (urls.length !== 1 || !matches(urls[0], from, to)) {
|
|
38
|
+
throw new Error(`Source origin does not match ${from} or ${to}`)
|
|
69
39
|
}
|
|
70
|
-
|
|
71
|
-
|
|
40
|
+
const pushurls = configValues(source, 'remote.origin.pushurl')
|
|
41
|
+
const entries = git(source, 'worktree', 'list', '--porcelain', '-z')
|
|
42
|
+
.split('\0\0')
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.map((entry) => entry.split('\0'))
|
|
45
|
+
const moves = [[source, destination]]
|
|
46
|
+
for (const entry of entries.slice(1)) {
|
|
47
|
+
const directory = entry[0].slice('worktree '.length)
|
|
48
|
+
if (!exists(directory)) {
|
|
49
|
+
throw new Error(`Missing linked worktree: ${directory}; repair or prune it first`)
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
fs.realpathSync(git(directory, 'rev-parse', '--show-toplevel')) !==
|
|
53
|
+
fs.realpathSync(directory) ||
|
|
54
|
+
fs.realpathSync(
|
|
55
|
+
git(directory, 'rev-parse', '--path-format=absolute', '--git-common-dir')
|
|
56
|
+
) !== gitDirectory
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(`Linked worktree needs repair: ${directory}`)
|
|
59
|
+
}
|
|
60
|
+
// Worktree repair does not repair the separate Git links inside submodules.
|
|
61
|
+
if (
|
|
62
|
+
git(directory, 'ls-files', '--stage', '-z')
|
|
63
|
+
.split('\0')
|
|
64
|
+
.some((entry) => entry.startsWith('160000 '))
|
|
65
|
+
) {
|
|
66
|
+
throw new Error(`Linked worktree contains submodules: ${directory}`)
|
|
67
|
+
}
|
|
68
|
+
if (!directory.startsWith(source + '@')) continue
|
|
69
|
+
if (entry.some((field) => field === 'locked' || field.startsWith('locked '))) {
|
|
70
|
+
throw new Error(`Linked worktree is locked: ${directory}`)
|
|
71
|
+
}
|
|
72
|
+
if (fs.lstatSync(directory).isSymbolicLink()) {
|
|
73
|
+
throw new Error(`Linked worktree is a symbolic link: ${directory}`)
|
|
74
|
+
}
|
|
75
|
+
moves.push([directory, destination + directory.slice(source.length)])
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
77
|
+
const mapping = new Map(moves)
|
|
78
|
+
for (const directory of worktrees) {
|
|
79
|
+
if (directory.startsWith(source + '@') && !mapping.has(directory)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Registered worktree is missing or does not belong to ${from}: ${directory}`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
if (directory === destination || directory.startsWith(destination + '@')) {
|
|
85
|
+
throw new Error(`${to} already has registered checkouts`)
|
|
86
|
+
}
|
|
81
87
|
}
|
|
82
|
-
|
|
83
|
-
throw new Error(`${
|
|
88
|
+
for (const [, target] of moves) {
|
|
89
|
+
if (exists(target)) throw new Error(`${target} already exists`)
|
|
90
|
+
// Resolve parents before any moves, including nested branch destinations.
|
|
91
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
84
92
|
}
|
|
85
|
-
}
|
|
86
|
-
for (const [, target] of moves) {
|
|
87
|
-
if (exists(target)) throw new Error(`${target} already exists`)
|
|
88
|
-
// Resolve parents before any moves, including nested branch destinations.
|
|
89
|
-
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
90
|
-
}
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
const linked = entries.slice(1).map((entry) => {
|
|
98
|
-
const directory = entry[0].slice('worktree '.length)
|
|
99
|
-
return mapping.get(directory) || directory
|
|
100
|
-
})
|
|
101
|
-
git(destination, 'worktree', 'repair', ...linked)
|
|
102
|
-
git(destination, 'config', 'remote.origin.url', replaceURL(urls[0], from, to))
|
|
103
|
-
if (pushurls.length) {
|
|
104
|
-
git(destination, 'config', '--unset-all', 'remote.origin.pushurl')
|
|
105
|
-
for (const url of pushurls) {
|
|
106
|
-
git(destination, 'config', '--add', 'remote.origin.pushurl', replaceURL(url, from, to))
|
|
94
|
+
originalConfig = fs.readFileSync(path.join(source, '.git', 'config'))
|
|
95
|
+
for (const [oldPath, newPath] of moves) {
|
|
96
|
+
fs.renameSync(oldPath, newPath)
|
|
97
|
+
moved.push([oldPath, newPath])
|
|
107
98
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
const linked = entries.slice(1).map((entry) => {
|
|
100
|
+
const directory = entry[0].slice('worktree '.length)
|
|
101
|
+
return mapping.get(directory) || directory
|
|
102
|
+
})
|
|
103
|
+
git(destination, 'worktree', 'repair', ...linked)
|
|
104
|
+
git(destination, 'config', 'remote.origin.url', replaceURL(urls[0], from, to))
|
|
105
|
+
replacePushURLs(destination, pushurls, from, to)
|
|
106
|
+
return worktrees.map((directory) => mapping.get(directory) || directory)
|
|
107
|
+
},
|
|
108
|
+
{ rename: { from, to } }
|
|
109
|
+
)
|
|
111
110
|
} catch (err) {
|
|
112
111
|
if (moved.length) {
|
|
113
112
|
try {
|
|
@@ -126,6 +125,105 @@ export default function renameRoot(from, to) {
|
|
|
126
125
|
)
|
|
127
126
|
}
|
|
128
127
|
|
|
128
|
+
// Finish a root move interrupted before Git links or origin were repaired.
|
|
129
|
+
// Validate the on-disk Git pointers before moving or repairing any worktree.
|
|
130
|
+
export function finishRootRename(from, to, registered) {
|
|
131
|
+
const source = repositoryPath(from)
|
|
132
|
+
const destination = repositoryPath(to)
|
|
133
|
+
if (exists(source)) throw new Error('Original root still exists; cannot finish its move')
|
|
134
|
+
if (fs.lstatSync(destination).isSymbolicLink()) throw new Error('Renamed root is a symbolic link')
|
|
135
|
+
const common = path.join(destination, '.git')
|
|
136
|
+
if (
|
|
137
|
+
!fs.lstatSync(common).isDirectory() ||
|
|
138
|
+
fs.realpathSync(git(destination, 'rev-parse', '--path-format=absolute', '--git-common-dir')) !==
|
|
139
|
+
fs.realpathSync(common) ||
|
|
140
|
+
fs.realpathSync(git(destination, 'rev-parse', '--show-toplevel')) !==
|
|
141
|
+
fs.realpathSync(destination)
|
|
142
|
+
) {
|
|
143
|
+
throw new Error('Renamed destination is not a standalone root repository')
|
|
144
|
+
}
|
|
145
|
+
if (fs.realpathSync(destination) === fs.realpathSync(manifestDirectory())) {
|
|
146
|
+
throw new Error('Cannot rename the active manifest repository')
|
|
147
|
+
}
|
|
148
|
+
const urls = configValues(destination, 'remote.origin.url')
|
|
149
|
+
if (urls.length !== 1 || !matches(urls[0], from, to)) {
|
|
150
|
+
throw new Error('Renamed root has an unexpected origin')
|
|
151
|
+
}
|
|
152
|
+
const pushurls = configValues(destination, 'remote.origin.pushurl')
|
|
153
|
+
const entries = git(destination, 'worktree', 'list', '--porcelain', '-z')
|
|
154
|
+
.split('\0\0')
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.slice(1)
|
|
157
|
+
.map((entry) => entry.split('\0'))
|
|
158
|
+
const moves = entries.map((entry) => {
|
|
159
|
+
const oldPath = entry[0].slice('worktree '.length)
|
|
160
|
+
const owned = oldPath.startsWith(source + '@') || oldPath.startsWith(destination + '@')
|
|
161
|
+
const target = oldPath.startsWith(source + '@')
|
|
162
|
+
? destination + oldPath.slice(source.length)
|
|
163
|
+
: oldPath
|
|
164
|
+
if (owned && entry.some((field) => field === 'locked' || field.startsWith('locked '))) {
|
|
165
|
+
throw new Error(`Linked worktree is locked: ${oldPath}`)
|
|
166
|
+
}
|
|
167
|
+
if (oldPath !== target && exists(oldPath) && exists(target)) {
|
|
168
|
+
throw new Error(`Both worktree paths exist: ${oldPath} and ${target}`)
|
|
169
|
+
}
|
|
170
|
+
const actual = exists(target) ? target : oldPath
|
|
171
|
+
if (!exists(actual) || fs.lstatSync(actual).isSymbolicLink()) {
|
|
172
|
+
throw new Error(`Missing or symbolic linked worktree: ${actual}`)
|
|
173
|
+
}
|
|
174
|
+
const pointerFile = path.join(actual, '.git')
|
|
175
|
+
if (!fs.lstatSync(pointerFile).isFile()) throw new Error(`Invalid Git pointer: ${actual}`)
|
|
176
|
+
const pointer = /^gitdir: (.+)\r?\n?$/.exec(fs.readFileSync(pointerFile, 'utf8'))
|
|
177
|
+
if (!pointer) throw new Error(`Invalid Git pointer: ${actual}`)
|
|
178
|
+
let metadata = path.resolve(actual, pointer[1])
|
|
179
|
+
if (metadata.startsWith(path.join(source, '.git/worktrees') + path.sep)) {
|
|
180
|
+
metadata = common + metadata.slice(path.join(source, '.git').length)
|
|
181
|
+
}
|
|
182
|
+
if (path.dirname(metadata) !== path.join(common, 'worktrees')) {
|
|
183
|
+
throw new Error(`Worktree belongs to another repository: ${actual}`)
|
|
184
|
+
}
|
|
185
|
+
const backlink = fs.readFileSync(path.join(metadata, 'gitdir'), 'utf8').trim()
|
|
186
|
+
if (![path.join(oldPath, '.git'), path.join(target, '.git')].includes(backlink)) {
|
|
187
|
+
throw new Error(`Unexpected worktree backlink: ${actual}`)
|
|
188
|
+
}
|
|
189
|
+
return [actual, target]
|
|
190
|
+
})
|
|
191
|
+
for (const directory of registered.filter((name) => name.startsWith(source + '@'))) {
|
|
192
|
+
if (!moves.some(([, target]) => target === destination + directory.slice(source.length))) {
|
|
193
|
+
throw new Error(`Missing registered worktree: ${directory}`)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const [actual, target] of moves) {
|
|
197
|
+
if (actual === target) continue
|
|
198
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
199
|
+
fs.renameSync(actual, target)
|
|
200
|
+
}
|
|
201
|
+
git(destination, 'worktree', 'repair', ...moves.map(([, target]) => target))
|
|
202
|
+
git(destination, 'config', 'remote.origin.url', replaceURL(urls[0], from, to))
|
|
203
|
+
replacePushURLs(destination, pushurls, from, to)
|
|
204
|
+
for (const directory of registered.filter((name) => name.startsWith(source + '@'))) {
|
|
205
|
+
removeEmptyParents(directory, path.dirname(source))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Set each push URL in place, so an interruption cannot lose the remaining URLs.
|
|
210
|
+
function replacePushURLs(destination, pushurls, from, to) {
|
|
211
|
+
for (const url of pushurls) {
|
|
212
|
+
const replacement = replaceURL(url, from, to)
|
|
213
|
+
if (replacement !== url) {
|
|
214
|
+
git(
|
|
215
|
+
destination,
|
|
216
|
+
'config',
|
|
217
|
+
'--fixed-value',
|
|
218
|
+
'--replace-all',
|
|
219
|
+
'remote.origin.pushurl',
|
|
220
|
+
replacement,
|
|
221
|
+
url
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
129
227
|
function matches(url, from, to) {
|
|
130
228
|
const match = parseURL(url)
|
|
131
229
|
return match && [from.toLowerCase(), to.toLowerCase()].includes(match[2].toLowerCase())
|
package/lib/rename.js
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
import fs from 'bare-fs'
|
|
2
2
|
import path from 'bare-path'
|
|
3
3
|
import { spawnSync } from 'bare-subprocess'
|
|
4
|
-
import { updateRegistry } from './registry.js'
|
|
4
|
+
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
8
|
import renameRoot from './rename-root.js'
|
|
9
|
+
import commitManifest from './commit-manifest.js'
|
|
10
|
+
import { refreshCheckpoint } from './checkpoint.js'
|
|
11
|
+
import { assertNoPendingPull } from './pull-state.js'
|
|
12
|
+
import { withPostHooks } from './hook-context.js'
|
|
9
13
|
|
|
10
|
-
export default function rename(from, to) {
|
|
14
|
+
export default function rename(from, to, options = {}) {
|
|
15
|
+
assertNoPendingPull()
|
|
16
|
+
return withPostHooks('rename', { from, to }, options, () => {
|
|
17
|
+
renameCheckout(from, to)
|
|
18
|
+
try {
|
|
19
|
+
commitManifest(manifestDirectory(), `Rename ${from} to ${to}`)
|
|
20
|
+
} catch (err) {
|
|
21
|
+
err.message = `Renamed ${from} to ${to}, but could not commit the manifest: ${err.message}`
|
|
22
|
+
err.hint =
|
|
23
|
+
'the rename is already complete; resolve the Git error and commit only workspace.json in the manifest repository'
|
|
24
|
+
throw err
|
|
25
|
+
}
|
|
26
|
+
refreshCheckpoint()
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renameCheckout(from, to, { update = updateRegistry } = {}) {
|
|
11
31
|
const source = repositoryPath(from)
|
|
12
32
|
const destination = repositoryPath(to)
|
|
13
33
|
const [base, requested] = from.split('@')
|
|
14
34
|
const [target, branch] = to.split('@')
|
|
15
|
-
if (!requested && !branch) return renameRoot(from, to)
|
|
35
|
+
if (!requested && !branch) return renameRoot(from, to, { update })
|
|
16
36
|
if (!requested || !branch) {
|
|
17
37
|
throw new Error('Use owner/repo on both sides, or owner/repo@branch on both sides')
|
|
18
38
|
}
|
|
@@ -55,20 +75,76 @@ export default function rename(from, to) {
|
|
|
55
75
|
throw new Error(`${destination} already exists`)
|
|
56
76
|
}
|
|
57
77
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
git(['-C', root, '
|
|
67
|
-
|
|
78
|
+
update(
|
|
79
|
+
(worktrees) => {
|
|
80
|
+
const registered = worktrees.includes(source) ? source : canonical
|
|
81
|
+
if (!worktrees.includes(registered)) {
|
|
82
|
+
throw new Error(`${from} is not registered`)
|
|
83
|
+
}
|
|
84
|
+
if (worktrees.includes(destination)) throw new Error(`${to} is already registered`)
|
|
85
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
|
86
|
+
git(['-C', root, 'worktree', 'move', canonical, destination])
|
|
87
|
+
if (current !== branch) {
|
|
88
|
+
git(['-C', root, 'branch', '-m', current, branch])
|
|
89
|
+
retargetUpstream(root, branch)
|
|
90
|
+
}
|
|
91
|
+
removeEmptyParents(source, path.dirname(root))
|
|
92
|
+
return worktrees.map((worktree) => (worktree === registered ? destination : worktree))
|
|
93
|
+
},
|
|
94
|
+
{ rename: { from, to } }
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function finishBranchRename(from, to) {
|
|
99
|
+
const [base, original] = from.split('@')
|
|
100
|
+
const [target, branch] = to.split('@')
|
|
101
|
+
if (!original || !branch || base !== target) throw new Error('Invalid interrupted branch rename')
|
|
102
|
+
const root = repositoryPath(base)
|
|
103
|
+
const destination = repositoryPath(to)
|
|
104
|
+
if (fs.lstatSync(destination).isSymbolicLink()) {
|
|
105
|
+
throw new Error('Renamed worktree is a symbolic link')
|
|
106
|
+
}
|
|
107
|
+
const common = (directory) =>
|
|
108
|
+
fs.realpathSync(
|
|
109
|
+
git(['-C', directory, 'rev-parse', '--path-format=absolute', '--git-common-dir'])
|
|
110
|
+
)
|
|
111
|
+
if (
|
|
112
|
+
fs.realpathSync(git(['-C', destination, 'rev-parse', '--show-toplevel'])) !==
|
|
113
|
+
fs.realpathSync(destination) ||
|
|
114
|
+
common(root) !== common(destination)
|
|
115
|
+
) {
|
|
116
|
+
throw new Error('Renamed destination is not a worktree of the requested repository')
|
|
117
|
+
}
|
|
118
|
+
const current = checkedOutBranch(destination)
|
|
119
|
+
if (current === original) {
|
|
120
|
+
if (hasRef(root, `refs/heads/${branch}`)) throw new Error(`Branch ${branch} already exists`)
|
|
121
|
+
git(['-C', root, 'branch', '-m', original, branch])
|
|
122
|
+
} else if (current !== branch) {
|
|
123
|
+
throw new Error(`Renamed worktree is on an unexpected branch: ${current || 'detached HEAD'}`)
|
|
124
|
+
}
|
|
125
|
+
if (hasRef(root, `refs/heads/${original}`)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Original branch ${original} still exists after the rename; inspect it before retrying`
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
// Git can rename the ref before failing to rename the config section.
|
|
131
|
+
if (!hasRef(root, `refs/heads/${original}`)) {
|
|
132
|
+
const section = spawnSync('git', [
|
|
133
|
+
'-C',
|
|
134
|
+
root,
|
|
135
|
+
'config',
|
|
136
|
+
'--get-regexp',
|
|
137
|
+
`^branch\\.${original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.`
|
|
138
|
+
])
|
|
139
|
+
if (section.error) throw section.error
|
|
140
|
+
if (section.status === 0) {
|
|
141
|
+
git(['-C', root, 'config', '--rename-section', `branch.${original}`, `branch.${branch}`])
|
|
142
|
+
} else if (section.status !== 1) {
|
|
143
|
+
throw new Error(section.stderr.toString().trim() || 'Could not inspect branch config')
|
|
68
144
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
145
|
+
}
|
|
146
|
+
retargetUpstream(root, branch)
|
|
147
|
+
removeEmptyParents(repositoryPath(from), path.dirname(root))
|
|
72
148
|
}
|
|
73
149
|
|
|
74
150
|
function retargetUpstream(root, branch) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-workspace-bare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Manage Git worktrees as development workspaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"license": "Apache-2.0",
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"bare-crypto": "^1.15.3",
|
|
17
|
+
"bare-env": "^3.0.1",
|
|
16
18
|
"bare-fs": "^4.8.1",
|
|
17
19
|
"bare-os": "^3.9.3",
|
|
18
20
|
"bare-path": "^3.1.2",
|