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.
@@ -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,