git-workspace-bare 0.1.1 → 0.3.1
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 +279 -17
- 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/man/man1/git-workspace.1 +240 -0
- package/package.json +6 -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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import test from 'brittle'
|
|
2
|
+
import fs from 'bare-fs'
|
|
3
|
+
import os from 'bare-os'
|
|
4
|
+
import path from 'bare-path'
|
|
5
|
+
import setup from './helpers.js'
|
|
6
|
+
|
|
7
|
+
test('completion works before init and only offers flags accepted at that position', (t) => {
|
|
8
|
+
const { cli, ok } = setup(t, { initialize: false })
|
|
9
|
+
t.is(ok('--complete', ''), 'add\ncompletion\nhook\ninit\nlist\npull\nremove\nrename\nstatus\n')
|
|
10
|
+
t.is(ok('--complete', 're'), 'remove\nrename\n')
|
|
11
|
+
t.is(ok('--complete', '--'), '--help\n--version\n')
|
|
12
|
+
t.is(ok('--complete', 'remove', '-'), '--help\n--no-hooks\n-h\n')
|
|
13
|
+
t.is(ok('--complete', 'pull', '--'), '--dry-run\n--help\n--no-hooks\n')
|
|
14
|
+
t.is(ok('--complete', 'hook', ''), 'list\n')
|
|
15
|
+
t.is(ok('--complete', 'hook', 'list', ''), '')
|
|
16
|
+
t.is(ok('--complete', 'pull', '--dry-run', '-h'), '-h\n')
|
|
17
|
+
t.is(ok('--complete', 'completion', ''), 'bash\n')
|
|
18
|
+
t.is(ok('--complete', 'completion', 'ba'), 'bash\n')
|
|
19
|
+
for (const words of [
|
|
20
|
+
['remove', ''],
|
|
21
|
+
['add', ''],
|
|
22
|
+
['init', ''],
|
|
23
|
+
['list', ''],
|
|
24
|
+
['status', ''],
|
|
25
|
+
['unknown', ''],
|
|
26
|
+
['remove', '--help', ''],
|
|
27
|
+
['--version', ''],
|
|
28
|
+
['add', '--', '-'],
|
|
29
|
+
['completion', 'bash', '']
|
|
30
|
+
]) {
|
|
31
|
+
t.alike(cli('--complete', ...words), { status: 0, stdout: '', stderr: '' }, words.join(' '))
|
|
32
|
+
}
|
|
33
|
+
t.is(cli('completion', 'zsh').status, 1, 'unsupported shell explained')
|
|
34
|
+
t.ok(cli('completion', 'zsh').stderr.includes('Supported completion shell: bash'))
|
|
35
|
+
t.absent(ok('-h').includes('--complete'), 'internal protocol hidden from help')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('completion reads registered names, including missing checkouts, without changing state', (t) => {
|
|
39
|
+
const { cli, ok, git, registry, root, home } = setup(t)
|
|
40
|
+
ok('add', 'owner/repo@feature/jobs')
|
|
41
|
+
fs.writeFileSync(path.join(root, 'tracked.txt'), 'local changes\n')
|
|
42
|
+
git('-C', root, 'add', 'tracked.txt')
|
|
43
|
+
const contents = fs.readFileSync(registry, 'utf8')
|
|
44
|
+
const config = fs.readFileSync(path.join(home, '.config/git-workspace/config.json'), 'utf8')
|
|
45
|
+
const index = fs.readFileSync(path.join(root, '.git/index'))
|
|
46
|
+
const directory = path.dirname(registry)
|
|
47
|
+
const head = git('-C', directory, 'rev-parse', 'HEAD')
|
|
48
|
+
const applied = git('-C', directory, 'rev-parse', 'refs/git-workspace/applied')
|
|
49
|
+
t.is(ok('--complete', 'remove', ''), 'owner/repo\nowner/repo@feature/jobs\n')
|
|
50
|
+
t.is(ok('--complete', 'rename', 'owner/repo@fea'), 'owner/repo@feature/jobs\n')
|
|
51
|
+
t.is(ok('--complete', 'add', 'owner/'), 'owner/repo\n')
|
|
52
|
+
t.is(ok('--complete', 'rename', 'owner/repo@feature/jobs', ''), 'owner/repo@\n')
|
|
53
|
+
t.is(ok('--complete', 'rename', 'owner/repo@feature/jobs', 'owner/repo@n'), '')
|
|
54
|
+
t.is(ok('--complete', 'rename', 'owner/repo', ''), '', 'root target is free-form')
|
|
55
|
+
t.is(ok('--complete', 'remove', 'owner/repo', ''), '', 'no extra arguments suggested')
|
|
56
|
+
t.is(ok('--complete', 'remove', 'owner/repo', '-h'), '-h\n')
|
|
57
|
+
t.is(fs.readFileSync(registry, 'utf8'), contents)
|
|
58
|
+
t.is(fs.readFileSync(path.join(home, '.config/git-workspace/config.json'), 'utf8'), config)
|
|
59
|
+
t.alike(fs.readFileSync(path.join(root, '.git/index')), index)
|
|
60
|
+
t.is(fs.readFileSync(path.join(root, 'tracked.txt'), 'utf8'), 'local changes\n')
|
|
61
|
+
t.is(git('-C', directory, 'rev-parse', 'HEAD'), head)
|
|
62
|
+
t.is(git('-C', directory, 'rev-parse', 'refs/git-workspace/applied'), applied)
|
|
63
|
+
fs.renameSync(root + '@feature', root + '@saved')
|
|
64
|
+
t.is(ok('--complete', 'remove', 'owner/repo@'), 'owner/repo@feature/jobs\n')
|
|
65
|
+
fs.writeFileSync(registry, '{invalid')
|
|
66
|
+
t.alike(cli('--complete', 'remove', ''), { status: 0, stdout: '', stderr: '' })
|
|
67
|
+
t.is(
|
|
68
|
+
ok('--complete', 'remove', '-'),
|
|
69
|
+
'--help\n--no-hooks\n-h\n',
|
|
70
|
+
'flags still work with invalid manifest'
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('add completes local and cached origin branches without fetching or accepting unsupported names', (t) => {
|
|
75
|
+
const { cli, ok, git, root } = setup(t)
|
|
76
|
+
ok('add', 'owner/repo')
|
|
77
|
+
const head = git('-C', root, 'rev-parse', 'HEAD')
|
|
78
|
+
git('-C', root, 'branch', 'feature/local')
|
|
79
|
+
git('-C', root, 'branch', 'feature/duplicate')
|
|
80
|
+
git('-C', root, 'branch', 'unsupported+name')
|
|
81
|
+
git('-C', root, 'update-ref', 'refs/remotes/origin/feature/remote', head)
|
|
82
|
+
git('-C', root, 'update-ref', 'refs/remotes/origin/feature/duplicate', head)
|
|
83
|
+
git('-C', root, 'update-ref', 'refs/remotes/other/feature/foreign', head)
|
|
84
|
+
git('-C', root, 'tag', 'feature/tag')
|
|
85
|
+
t.is(
|
|
86
|
+
ok('--complete', 'add', 'owner/repo@fea'),
|
|
87
|
+
'owner/repo@feature/duplicate\nowner/repo@feature/local\nowner/repo@feature/remote\n'
|
|
88
|
+
)
|
|
89
|
+
t.is(
|
|
90
|
+
ok('--complete', 'add', 'owner/repo@'),
|
|
91
|
+
'owner/repo@feature/duplicate\nowner/repo@feature/local\nowner/repo@feature/remote\nowner/repo@main\n',
|
|
92
|
+
'origin HEAD excluded and local/remote duplicates combined'
|
|
93
|
+
)
|
|
94
|
+
t.is(ok('--complete', 'add', 'owner/repo@feature/l'), 'owner/repo@feature/local\n')
|
|
95
|
+
t.is(ok('--complete', 'add', 'other/repo@'), '')
|
|
96
|
+
t.absent(fs.existsSync(path.join(root, '.git/FETCH_HEAD')), 'no fetch performed')
|
|
97
|
+
fs.renameSync(root, root + '-saved')
|
|
98
|
+
t.alike(cli('--complete', 'add', 'owner/repo@'), { status: 0, stdout: '', stderr: '' })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
function shellCompletion(t, context) {
|
|
102
|
+
const { run, home } = context
|
|
103
|
+
// A PATH containing spaces also verifies installation paths are quoted.
|
|
104
|
+
const bin = path.join(home, 'completion bin')
|
|
105
|
+
fs.mkdirSync(bin)
|
|
106
|
+
const runner = fs.realpathSync(new URL('./fixtures/cli.js', import.meta.url))
|
|
107
|
+
const fixture = fs.realpathSync(new URL('./fixtures/completion.bash', import.meta.url))
|
|
108
|
+
const quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'"
|
|
109
|
+
const shim = path.join(bin, 'git-workspace')
|
|
110
|
+
fs.writeFileSync(
|
|
111
|
+
shim,
|
|
112
|
+
`#!/bin/sh\nexec ${quote(os.execPath())} ${quote(runner)} ${quote(home)} "$@"\n`
|
|
113
|
+
)
|
|
114
|
+
fs.chmodSync(shim, 0o755)
|
|
115
|
+
return (words, gitCompletion = '') => {
|
|
116
|
+
const result = run('bash', ['--noprofile', '--norc', fixture, bin, gitCompletion, ...words])
|
|
117
|
+
t.is(result.status, 0, result.stderr || 'Bash completion succeeded')
|
|
118
|
+
t.is(result.stderr, '', 'completion is quiet')
|
|
119
|
+
return result.stdout
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
test('generated Bash completion works standalone, preserves branch prefixes, and supports Bash 3.2', (t) => {
|
|
124
|
+
const context = setup(t)
|
|
125
|
+
const { ok, root, git } = context
|
|
126
|
+
ok('add', 'owner/repo@feature/jobs')
|
|
127
|
+
git('-C', root, 'branch', 'feature/new')
|
|
128
|
+
const complete = shellCompletion(t, context)
|
|
129
|
+
t.is(complete(['git-workspace', 're']), 'remove \nrename \n')
|
|
130
|
+
t.is(complete(['git-workspace', 'add', 'owner/r']), 'owner/repo\n', 'root has no trailing space')
|
|
131
|
+
t.is(complete(['git-workspace', 'remove', 'owner/repo@fea']), 'owner/repo@feature/jobs \n')
|
|
132
|
+
t.is(complete(['git-workspace', 'add', 'owner/repo@feature/n']), 'owner/repo@feature/new \n')
|
|
133
|
+
t.is(complete(['git-workspace', 'rename', 'owner/repo@feature/jobs', '']), 'owner/repo@\n')
|
|
134
|
+
t.is(complete(['git-workspace', 'completion', '']), 'bash \n')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('Bash integrates with real Git completion, including Git global options', (t) => {
|
|
138
|
+
const context = setup(t)
|
|
139
|
+
const { git, ok, root } = context
|
|
140
|
+
const exec = git('--exec-path')
|
|
141
|
+
const gitCompletion = [
|
|
142
|
+
path.resolve(exec, '../../share/git-core/git-completion.bash'),
|
|
143
|
+
path.resolve(exec, '../../share/bash-completion/completions/git'),
|
|
144
|
+
'/usr/share/bash-completion/completions/git',
|
|
145
|
+
'/usr/share/git-core/git-completion.bash'
|
|
146
|
+
].find((file) => fs.existsSync(file))
|
|
147
|
+
if (!gitCompletion) {
|
|
148
|
+
t.comment('Git Bash completion is not installed; standalone adapter is tested separately')
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
ok('add', 'owner/repo@feature/jobs')
|
|
152
|
+
git('-C', root, 'branch', 'feature/new')
|
|
153
|
+
const complete = shellCompletion(t, context)
|
|
154
|
+
t.is(complete(['git', 'workspace', 're'], gitCompletion), 'remove \nrename \n')
|
|
155
|
+
t.is(
|
|
156
|
+
complete(['git', 'workspace', 'add', 'owner/r'], gitCompletion),
|
|
157
|
+
'owner/repo\n',
|
|
158
|
+
'root has no trailing space'
|
|
159
|
+
)
|
|
160
|
+
t.is(
|
|
161
|
+
complete(['git', 'workspace', 'remove', 'owner/repo@fea'], gitCompletion),
|
|
162
|
+
'owner/repo@feature/jobs \n'
|
|
163
|
+
)
|
|
164
|
+
t.is(
|
|
165
|
+
complete(['git', '-C', '/', 'workspace', 'add', 'owner/repo@feature/n'], gitCompletion),
|
|
166
|
+
'owner/repo@feature/new \n'
|
|
167
|
+
)
|
|
168
|
+
t.is(
|
|
169
|
+
complete(
|
|
170
|
+
['git', '-c', 'color.ui=false', 'workspace', 'rename', 'owner/repo@feature/jobs', ''],
|
|
171
|
+
gitCompletion
|
|
172
|
+
),
|
|
173
|
+
'owner/repo@\n'
|
|
174
|
+
)
|
|
175
|
+
t.ok(
|
|
176
|
+
complete(['git', 'sta'], gitCompletion).includes('status'),
|
|
177
|
+
'standard Git completion preserved'
|
|
178
|
+
)
|
|
179
|
+
t.ok(complete(['git', 'work'], gitCompletion).includes('workspace'), 'Git discovers the command')
|
|
180
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os from 'bare-os'
|
|
2
|
+
import process from 'bare-process'
|
|
3
|
+
|
|
4
|
+
const home = Bare.argv.splice(2, 1)[0]
|
|
5
|
+
os.homedir = () => home
|
|
6
|
+
const { default: checkpoint, readCheckpoint } = await import('../../lib/checkpoint.js')
|
|
7
|
+
try {
|
|
8
|
+
console.log(
|
|
9
|
+
Bare.argv.includes('--show') ? readCheckpoint() || 'No applied checkpoint' : checkpoint()
|
|
10
|
+
)
|
|
11
|
+
} catch (err) {
|
|
12
|
+
console.error(err.message)
|
|
13
|
+
process.exitCode = 1
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Drive the same completion functions that Bash invokes on Tab.
|
|
2
|
+
completion_bin=$1
|
|
3
|
+
git_completion=$2
|
|
4
|
+
shift 2
|
|
5
|
+
export PATH="$completion_bin:$PATH"
|
|
6
|
+
|
|
7
|
+
if [ -n "$git_completion" ]; then
|
|
8
|
+
source "$git_completion" || exit 1
|
|
9
|
+
git_before=$(complete -p git)
|
|
10
|
+
fi
|
|
11
|
+
completion_script=$(command git-workspace completion bash) || exit 1
|
|
12
|
+
eval "$completion_script" || exit 1
|
|
13
|
+
|
|
14
|
+
COMP_WORDS=("$@")
|
|
15
|
+
COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 ))
|
|
16
|
+
COMP_LINE="${COMP_WORDS[*]}"
|
|
17
|
+
COMP_POINT=${#COMP_LINE}
|
|
18
|
+
COMP_TYPE=9
|
|
19
|
+
COMPREPLY=()
|
|
20
|
+
if [ "${COMP_WORDS[0]}" = git ]; then
|
|
21
|
+
[ "$(complete -p git)" = "$git_before" ] || exit 1
|
|
22
|
+
__git_func_wrap __git_main
|
|
23
|
+
else
|
|
24
|
+
_git_workspace_direct
|
|
25
|
+
fi
|
|
26
|
+
for candidate in "${COMPREPLY[@]}"; do
|
|
27
|
+
printf '%s\n' "$candidate"
|
|
28
|
+
done
|
package/test/helpers.js
CHANGED
|
@@ -15,19 +15,58 @@ export default function setup(t, { initialize = true } = {}) {
|
|
|
15
15
|
fs.mkdirSync(path.join(temporary, 'hooks'))
|
|
16
16
|
fs.writeFileSync(config, '')
|
|
17
17
|
|
|
18
|
+
// A private PATH prevents tests from running hooks installed on the developer's machine.
|
|
19
|
+
const bin = path.join(temporary, 'bin')
|
|
20
|
+
fs.mkdirSync(bin)
|
|
21
|
+
const gitPath = spawnSync('which', ['git']).stdout.toString().trim()
|
|
22
|
+
fs.symlinkSync(gitPath, path.join(bin, 'git'))
|
|
23
|
+
fs.symlinkSync(os.execPath(), path.join(bin, 'bare'))
|
|
24
|
+
for (const name of [
|
|
25
|
+
'bash',
|
|
26
|
+
'sh',
|
|
27
|
+
'env',
|
|
28
|
+
'which',
|
|
29
|
+
'cat',
|
|
30
|
+
'dirname',
|
|
31
|
+
'basename',
|
|
32
|
+
'sed',
|
|
33
|
+
'tr',
|
|
34
|
+
'sort',
|
|
35
|
+
'cut',
|
|
36
|
+
'grep',
|
|
37
|
+
'head',
|
|
38
|
+
'mkdir',
|
|
39
|
+
'rm',
|
|
40
|
+
'uname',
|
|
41
|
+
'pwd',
|
|
42
|
+
'perl',
|
|
43
|
+
'sleep',
|
|
44
|
+
'touch',
|
|
45
|
+
'wc',
|
|
46
|
+
'awk',
|
|
47
|
+
'ln'
|
|
48
|
+
]) {
|
|
49
|
+
const executable = ['/bin', '/usr/bin']
|
|
50
|
+
.map((directory) => path.join(directory, name))
|
|
51
|
+
.find((file) => fs.existsSync(file))
|
|
52
|
+
if (executable) fs.symlinkSync(executable, path.join(bin, name))
|
|
53
|
+
}
|
|
54
|
+
|
|
18
55
|
const env = { ...process.env }
|
|
19
56
|
for (const key of Object.keys(env)) {
|
|
20
57
|
if (key.startsWith('GIT_')) delete env[key]
|
|
21
58
|
}
|
|
22
59
|
Object.assign(env, {
|
|
60
|
+
PATH: bin,
|
|
61
|
+
GIT_WORKSPACE_HOOK: '',
|
|
23
62
|
GIT_CONFIG_NOSYSTEM: '1',
|
|
24
63
|
GIT_CONFIG_GLOBAL: config,
|
|
25
64
|
GIT_ALLOW_PROTOCOL: 'file',
|
|
26
65
|
GIT_TERMINAL_PROMPT: '0'
|
|
27
66
|
})
|
|
28
67
|
|
|
29
|
-
function run(file, args) {
|
|
30
|
-
const result = spawnSync(file, args, { cwd: temporary, env })
|
|
68
|
+
function run(file, args, { env: overrides = {} } = {}) {
|
|
69
|
+
const result = spawnSync(file, args, { cwd: temporary, env: { ...env, ...overrides } })
|
|
31
70
|
if (result.error) throw result.error
|
|
32
71
|
return {
|
|
33
72
|
status: result.status,
|
|
@@ -68,11 +107,30 @@ export default function setup(t, { initialize = true } = {}) {
|
|
|
68
107
|
return result.stdout
|
|
69
108
|
}
|
|
70
109
|
|
|
110
|
+
const checkpointRunner = fs.realpathSync(new URL('./fixtures/checkpoint.js', import.meta.url))
|
|
111
|
+
function checkAt(directory, ...args) {
|
|
112
|
+
return run(os.execPath(), [checkpointRunner, directory, ...args])
|
|
113
|
+
}
|
|
114
|
+
function check(...args) {
|
|
115
|
+
return checkAt(home, ...args)
|
|
116
|
+
}
|
|
117
|
+
function checked(...args) {
|
|
118
|
+
const result = check(...args)
|
|
119
|
+
if (result.status !== 0) throw new Error(result.stderr)
|
|
120
|
+
return result.stdout
|
|
121
|
+
}
|
|
122
|
+
|
|
71
123
|
if (initialize) ok('init', 'owner/workspace')
|
|
72
124
|
|
|
73
125
|
return {
|
|
126
|
+
bin,
|
|
127
|
+
env,
|
|
128
|
+
run,
|
|
74
129
|
cli,
|
|
75
130
|
cliAt,
|
|
131
|
+
check,
|
|
132
|
+
checkAt,
|
|
133
|
+
checked,
|
|
76
134
|
ok,
|
|
77
135
|
git,
|
|
78
136
|
home,
|