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 CHANGED
@@ -2,24 +2,41 @@
2
2
 
3
3
  Manage Git worktrees as development workspaces.
4
4
 
5
- The CLI can clone GitHub repositories, create branch worktrees, list registered checkouts, and unregister them.
5
+ The CLI can clone GitHub repositories or initialize them locally, create branch worktrees, list registered checkouts, and unregister them.
6
6
 
7
- ## Local setup
7
+ ## Installation
8
8
 
9
- The CLI runs on [Bare](https://github.com/holepunchto/bare). Install Bare using npm:
9
+ The CLI runs on [Bare](https://github.com/holepunchto/bare). Install both globally using npm:
10
10
 
11
11
  ```sh
12
- npm install -g bare
12
+ npm install -g bare git-workspace-bare
13
13
  ```
14
14
 
15
- Then run from the repository directory:
15
+ This makes the `git-workspace` command available in your shell. Git also discovers it on your `PATH`, so you can run it as `git workspace`. Bare must be on your `PATH`; Node.js and npm are used for package installation and development tooling.
16
16
 
17
- ```sh
18
- npm install
19
- npm link
17
+ ### Bash completion
18
+
19
+ Add this line to `~/.bashrc` (or the startup file your interactive Bash shell loads):
20
+
21
+ ```bash
22
+ eval "$(git-workspace completion bash)"
20
23
  ```
21
24
 
22
- This makes the `git-workspace` command available in your shell. Git also discovers it on your `PATH`, so you can run it as `git workspace`. Bare must be on your `PATH`; Node.js and npm are used for package installation and development tooling.
25
+ For `git workspace` completion, also enable [Git's Bash completion](https://git-scm.com/book/en/v2/Appendix-A%3A-Git-in-Other-Environments-Git-in-Bash).
26
+ The workspace script extends it; existing Git completions are preserved. The direct
27
+ `git-workspace` command has completion even without Git's completion script. Open a new shell
28
+ or run that line in your current shell to enable it. Bash 3.2 and newer are supported.
29
+
30
+ Tab completes commands, flags, and registered repository/worktree names for `remove` and
31
+ the source of `rename`. For `add`, it suggests registered roots; after `owner/repo@`, it
32
+ suggests local branches and already-fetched origin branches. New repository and branch names
33
+ can still be typed freely. Completed repository names have no trailing space, so you can
34
+ press Enter or append `@` and complete a branch. For a branch rename's destination, it fills the `owner/repo@`
35
+ prefix so you can type the new branch name.
36
+
37
+ Suggestions use local data only, without fetching or changing the workspace. Missing or
38
+ invalid workspace data silently produces no repository suggestions; commands and flags
39
+ remain available before `init`. Currently only Bash is supported.
23
40
 
24
41
  ## Usage
25
42
 
@@ -29,6 +46,7 @@ git workspace --version
29
46
  git workspace init nicolasfont/workspace
30
47
  git workspace add holepunchto/brittle
31
48
  git workspace list
49
+ git workspace pull --dry-run
32
50
  git workspace rename holepunchto/brittle@jobs holepunchto/brittle@fixes
33
51
  git workspace remove holepunchto/brittle
34
52
  ```
@@ -50,35 +68,176 @@ git workspace init nicolasfont/workspace
50
68
 
51
69
  This tries to clone the repository over SSH into `~/workspace/nicolasfont/workspace`. If cloning fails and the destination is absent or empty, it initializes a local Git repository there and sets `origin` to the GitHub SSH URL. The clone failure is reported because a missing repository, authentication failure, and network failure can look similar. Existing nonempty destinations are never overwritten.
52
70
 
53
- Creating the repository on GitHub, committing, and pushing remain manual. Initialization saves `~/.config/git-workspace/config.json` with a `manifestRepository` field containing the absolute checkout path. The local repository uses `workspace.json` as the manifest, storing portable repository names rather than machine-specific absolute paths:
71
+ Creating the repository on GitHub remains manual. Manifest commits are pushed automatically when the remote is available. Initialization saves `~/.config/git-workspace/config.json` with a `manifestRepository` field containing the absolute checkout path. The local repository uses `workspace.json` as the manifest, storing portable repository names rather than machine-specific absolute paths:
54
72
 
55
73
  ```json
56
- ["holepunchto/brittle", "holepunchto/brittle@feature/jobs"]
74
+ {
75
+ "version": 1,
76
+ "repositories": [
77
+ {
78
+ "id": "8257b322-695c-4c97-a816-c6c2c6198e40",
79
+ "name": "holepunchto/brittle",
80
+ "worktrees": [
81
+ {
82
+ "id": "37326a40-98e7-461b-9dd3-fc6a57f0b856",
83
+ "branch": "feature/jobs"
84
+ }
85
+ ]
86
+ }
87
+ ]
88
+ }
57
89
  ```
58
90
 
59
- On every machine, those names resolve under `~/workspace`.
91
+ On every machine, those names resolve under `~/workspace`. `list` still displays
92
+ `owner/repo` and `owner/repo@branch`; IDs are stored in the manifest, not used in CLI arguments.
93
+
94
+ Each repository and linked worktree gets a UUID when first registered. Renaming a repository
95
+ (including changing its owner) preserves its ID and all its worktree IDs. Renaming a branch
96
+ worktree preserves its ID. Removing an entry deletes it from the current manifest; reusing
97
+ that name later creates a new ID. Old identities remain in Git history, so names are not
98
+ reserved forever. Treat IDs as permanent identifiers when editing the manifest manually.
99
+
100
+ IDs must be unique across the entire manifest, and repository names and worktree names must
101
+ not be duplicated. Worktrees belong to their root repository. Invalid entries and unsupported
102
+ manifest versions are rejected before changing checkouts. The manifest records desired state;
103
+ reading it does not apply changes. A local applied-commit checkpoint records verified state;
104
+ `git workspace pull` synchronizes the local layout, and `--dry-run` previews changes first.
105
+
106
+ If the manifest repository has no `workspace.json`, initialization writes an empty version-1 manifest and commits it with the message `Initialize workspace manifest`. Only `workspace.json` is included; unrelated staged changes remain staged. Git identity, hooks, and signing settings apply as usual. The new manifest commit is then pushed. Old local registry files are not read or migrated.
107
+
108
+ If the commit fails, `init` reports the Git error and leaves the new manifest on disk. Resolve the error, commit only `workspace.json` in the manifest repository, then rerun `init`.
109
+
110
+ If a version-1 `workspace.json` already exists, it is validated and preserved without creating a commit, even if it has uncommitted edits. Initialization does not clone or remove the repositories listed in the manifest. Repeating `init` with the same manifest repository preserves local edits and does not pull. A different manifest origin is refused.
111
+
112
+ Run `init` before using `add`, `remove`, `list`, `status`, or `pull`. All commands locate `workspace.json` through that config file; there is no legacy registry fallback or separate activation marker. `init` commits newly created manifests, and successful `add`, `remove`, and `rename` commands commit the current manifest and attempt to push it.
60
113
 
61
- If the manifest repository has no `workspace.json`, initialization writes an empty array. Old local registry files are not read or migrated.
114
+ Automatic pushes target the manifest branch's configured upstream, or the same branch name
115
+ on `origin` when no upstream is configured. A successful push establishes tracking. Only
116
+ that manifest branch is pushed, including its existing local commits; project branches and
117
+ tags are not published. Pushes never force, fetch, or merge remote history.
62
118
 
63
- If `workspace.json` already exists, it is validated and preserved. Initialization does not clone or remove the repositories listed in the manifest. Repeating `init` with the same manifest repository preserves local edits and does not pull. A different manifest origin is refused.
119
+ A push failure warns without undoing the checkout operation or local commit, changing the
120
+ command's success status, or preventing local applied tracking and post hooks. This allows
121
+ commands to work offline or before the manifest repository exists on GitHub. Once connected,
122
+ repeat `add` for an already registered checkout to retry the push without another commit,
123
+ or push from the manifest repository yourself (for a new upstream, `git push -u origin HEAD`).
124
+ For a non-fast-forward rejection, resolve the divergent manifest history before retrying.
64
125
 
65
- Run `init` before using `add`, `remove`, `list`, or `status`. All commands locate `workspace.json` through that config file; there is no legacy registry fallback or separate activation marker. Updates remain uncommitted changes in the manifest checkout: automatic commits, pushes, and a `git workspace pull` command will come in later commits. For now, the manifest checkout can be committed and pushed with ordinary Git commands.
126
+ Only the version-1 object format is supported. Old array manifests are rejected without being
127
+ rewritten. Keep IDs when editing entries; replacing an ID means replacing that entry's identity.
128
+
129
+ After successful `init`, `add`, `remove`, and `rename` commands, the CLI automatically
130
+ verifies the committed manifest against this machine's checkouts and records the applied
131
+ commit locally. This internal bookkeeping supports future synchronization; it does not
132
+ create commits or push anything. Dirty files and local commits in your repositories are allowed.
133
+ If verification finds a mismatch, the completed operation is preserved and a warning explains
134
+ why local tracking could not advance. Tracking is checked again on the next successful command
135
+ that initializes or changes the workspace. `list` and `status` do not change it.
136
+
137
+ ### Synchronize from the manifest
138
+
139
+ ```sh
140
+ git workspace pull --dry-run
141
+ git workspace pull
142
+ ```
143
+
144
+ Fetches the manifest branch from its configured upstream. If the local branch has no upstream,
145
+ it uses the same branch name on `origin`. The manifest repository must be on a branch with a
146
+ clean working tree and index. Divergent histories are refused; local commits ahead of the
147
+ remote are retained; pull itself does not push them.
148
+
149
+ The preview compares the latest manifest with this machine's last verified applied state.
150
+ Matching IDs identify renames, even across owners. A root rename includes its worktrees;
151
+ branch renames are listed separately only when the branch name changes. Removed children
152
+ are listed before their parents, and parents are added before children. Reusing a name with
153
+ a new ID means removing the old entry and adding the replacement.
154
+
155
+ For example:
156
+
157
+ ```text
158
+ Manifest: origin/main
159
+ Would fast-forward the manifest
160
+ rename owner/repo -> other/new
161
+ rename other/new@jobs -> other/new@fixes
162
+ add other/new@feature/new
163
+ Dry run: no manifest checkout or workspace changes applied
164
+ ```
165
+
166
+ The planner reports missing or mismatched checkouts, occupied destinations, existing branch
167
+ targets, locked worktrees, and dirty checkouts that would be removed. Dirty work is allowed
168
+ in retained or renamed checkouts. Blockers are printed with `blocked:` and exit status 1;
169
+ the preview still shows the requested changes. These checks describe local conditions now;
170
+ they cannot guarantee a future clone or filesystem operation will succeed.
171
+
172
+ On a new machine without an applied baseline, the preview plans additions and reports
173
+ matching existing checkouts as `adopt`. It does not infer old removals or renames from history
174
+ before that machine's first verified baseline. Unexpected local layout changes after a
175
+ baseline are reported for manual resolution.
176
+
177
+ Without `--dry-run`, pull checks for blockers before fast-forwarding the manifest and
178
+ applying the layout changes. It moves renamed checkouts, updates renamed roots' origin URLs,
179
+ removes clean retired checkouts, and clones or creates additions. GitHub renames and transfers
180
+ remain manual. Clean removals can delete local commits, just like `git workspace remove`;
181
+ preview the plan before applying removals. Existing managed repositories are never fetched
182
+ or pulled. Pull creates no manifest commits and pushes nothing. Unlike a direct `add`, pull
183
+ requires new repositories to clone successfully; it never substitutes an empty local repository
184
+ for an unavailable remote.
185
+
186
+ If an operation fails, completed changes remain in place. Resolve the reported error and
187
+ rerun `git workspace pull` to resume the saved target. The next pull fetches any newer remote
188
+ changes. Progress is stored locally in `.git/workspace-pull.json`, with the target commit
189
+ pinned at `refs/git-workspace/pull-target`. Other commands that modify the manifest are
190
+ blocked while a pull is pending. Local applied tracking advances only after the complete
191
+ layout has been verified; progress is then cleared. Do not edit or delete the progress file
192
+ to retry. A dry run during recovery checks the remaining changes without applying them;
193
+ a partially moved rename may need a real pull to repair its Git links before those checks
194
+ can finish.
195
+
196
+ A dry run fetches objects and updates Git's fetch metadata and remote-tracking refs in the
197
+ manifest repository. It never checks out the fetched manifest, applies layout changes,
198
+ advances local applied tracking, creates commits, or pushes.
66
199
 
67
200
  ### Add a repository
68
201
 
69
- With Git on your `PATH` and SSH access to GitHub configured:
202
+ With Git on your `PATH`:
70
203
 
71
204
  ```sh
72
205
  git workspace add holepunchto/brittle
73
206
  ```
74
207
 
75
- The required `owner/repo` identifies a GitHub repository. If `~/workspace/holepunchto/brittle` does not exist, the command clones `git@github.com:holepunchto/brittle.git` there, then registers the checkout. The default workspace directory is `~/workspace`, regardless of your current directory.
208
+ The required `owner/repo` identifies a GitHub repository, including one you have not created
209
+ on GitHub yet. If `~/workspace/holepunchto/brittle` is absent or an empty directory, the command
210
+ tries to clone `git@github.com:holepunchto/brittle.git` there. The default workspace directory
211
+ is `~/workspace`, regardless of your current directory.
212
+
213
+ If cloning fails and the destination is still absent or empty, `add` initializes an empty local
214
+ Git repository and configures that URL as `origin`. This works offline and when the remote
215
+ does not exist or is inaccessible, with no flag or confirmation required. The clone diagnostic
216
+ is preserved, followed by a notice explaining that a local repository was initialized. The
217
+ checkout is registered and committed to the manifest normally, and add hooks run normally.
218
+ No GitHub repository is created and no project commits are pushed. The workspace manifest commit is pushed separately.
219
+
220
+ The local repository follows Git's `init.defaultBranch` setting and has no initial commit.
221
+ Create your first commit when ready. Later remote synchronization is explicit: repeating `add`
222
+ reuses the local checkout, even once you are online. If the remote already has history, reconcile
223
+ that history with any commits you made locally before pushing.
76
224
 
77
- If the destination already exists, it must be a Git worktree root whose `origin` points to the requested GitHub repository. SSH and HTTPS origins are accepted. Repeating the command does not pull, switch branches, or add duplicate entries. Existing repositories elsewhere are not moved.
225
+ Default SSH clone attempts use a five-second connection timeout, one connection attempt, and
226
+ noninteractive authentication, using keys already available to SSH. The timeout covers the
227
+ [SSH connection and initial handshake](https://man.openbsd.org/ssh_config#ConnectTimeout),
228
+ not the entire clone or system DNS resolution. Custom `GIT_SSH`, `GIT_SSH_COMMAND`,
229
+ `core.sshCommand`, and non-OpenSSH variants keep their own transport settings; URL rewrites
230
+ to other transports also retain their own connection behavior.
78
231
 
79
- Registered names are stored in the initialized manifest. Cloning shows Git's progress; registering an existing checkout is silent. A failed clone or destination validation leaves the registry unchanged. If registration fails after cloning, the checkout remains on disk; fix the registry issue and rerun the same command.
232
+ A failed clone that leaves files behind is preserved and reported as an error. Initialization
233
+ never overwrites a nonempty destination or follows a destination symlink. Git invocation,
234
+ interruption, and local filesystem failures remain errors.
80
235
 
81
- Path-based `add` is no longer supported. No sessions are started.
236
+ If the destination already contains files, it must be a Git worktree root whose `origin` points to the requested GitHub repository. SSH and HTTPS origins are accepted. Repeating the command does not pull, switch branches, or add duplicate entries. Existing repositories elsewhere are not moved.
237
+
238
+ Registered names are stored in the initialized manifest. A successful `add` commits the current `workspace.json` with the message `Add owner/repo` or `Add owner/repo@branch`. Adding a branch and its root together makes one commit. If the manifest matches the last commit, no commit is created. The commit includes any pending edits to `workspace.json`, including manual edits; unrelated staged files remain staged. The manifest branch is then pushed; repeating add retries the push even if no new commit is needed. Cloning shows Git's progress; registering an existing checkout is silent. A destination validation failure leaves the registry unchanged. If registration fails after cloning or local initialization, the checkout remains on disk; fix the registry issue and rerun the same command. If committing fails, the checkouts and manifest are retained; resolve the reported Git error and rerun the same `add` command to retry the commit.
239
+
240
+ Path-based `add` is no longer supported. Session management can be provided by a command hook.
82
241
 
83
242
  ### Add a branch worktree
84
243
 
@@ -86,17 +245,39 @@ Path-based `add` is no longer supported. No sessions are started.
86
245
  git workspace add holepunchto/brittle@jobs
87
246
  ```
88
247
 
89
- First ensures `~/workspace/holepunchto/brittle` exists and is registered, cloning it if necessary. Then creates and registers `~/workspace/holepunchto/brittle@jobs`:
248
+ First ensures `~/workspace/holepunchto/brittle` exists and is registered, cloning it or initializing
249
+ it locally if necessary. Then creates and registers `~/workspace/holepunchto/brittle@jobs`:
90
250
 
91
251
  - Uses the local `jobs` branch if it exists.
92
252
  - Otherwise creates `jobs` tracking `origin/jobs`, if that remote-tracking branch exists locally.
93
253
  - Otherwise creates `jobs` from the root checkout's current `HEAD`, without an upstream.
94
254
 
255
+ An empty root needs its first commit before a branch worktree can be created. If the root was
256
+ just initialized locally, it stays registered and the command explains how to make the first
257
+ commit and retry. It does not fabricate a commit to create the worktree.
258
+
95
259
  Existing repositories are not fetched automatically. The root checkout's branch and local edits remain unchanged. If the destination already exists, it must belong to the same Git repository and be on the requested branch; it is reused without switching branches. Git refuses creation when the branch is checked out elsewhere.
96
260
 
97
261
  Branch names containing `/` use nested directories. For example, `git workspace add holepunchto/brittle@feature/jobs` creates branch `feature/jobs` at `~/workspace/holepunchto/brittle@feature/jobs`. Use the same name for `remove`; `list` preserves the slashes. Invalid branch names are rejected before cloning or registration.
98
262
 
99
- If worktree creation fails, the root repository remains registered. If registration fails after creation, the worktree remains on disk; fix the registry issue and retry the same command.
263
+ If worktree creation fails, the root repository remains registered and its manifest change remains uncommitted. If registration fails after creation, the worktree remains on disk; fix the registry issue and retry the same command.
264
+
265
+ ### Rename commits
266
+
267
+ After a successful root or branch rename, the current `workspace.json` is committed with the
268
+ message `Rename <from> to <to>`. A root rename and all its linked worktree entries are included
269
+ in one commit. Pending manifest edits are included; unrelated staged files remain staged.
270
+ No commit is created if the manifest matches the last commit. The manifest branch is then pushed.
271
+
272
+ If committing fails, the renamed checkouts, origin configuration, and updated manifest are
273
+ retained. Resolve the Git error and commit only `workspace.json` in the manifest repository,
274
+ for example:
275
+
276
+ ```sh
277
+ git commit --only -m "Rename holepunchto/brittle@jobs to holepunchto/brittle@fixes" -- workspace.json
278
+ ```
279
+
280
+ The rename is already complete, so do not rerun it using the old name to retry the commit.
100
281
 
101
282
  ### Rename a root repository
102
283
 
@@ -113,7 +294,7 @@ The local `origin` URL is updated to the new name, preserving SSH or HTTPS forma
113
294
  origin push URLs pointing to the old repository are updated too; URLs pointing elsewhere and
114
295
  other remotes are left alone. An origin already pointing to the new name is also accepted.
115
296
 
116
- This is entirely local: it does not rename anything on GitHub, fetch, commit, or push. Rename
297
+ The checkout rename does not rename anything on GitHub, fetch, or push project commits. Its workspace manifest commit is pushed. Rename
117
298
  the repository separately in GitHub **Settings → General → Repository name**, or run:
118
299
 
119
300
  ```sh
@@ -214,10 +395,44 @@ After successful worktree and branch removal, empty branch-parent directories ar
214
395
 
215
396
  If branch deletion fails after the directory is removed, the branch and existing registry entry remain. Resolve the branch issue and retry the same command to finish removal.
216
397
 
217
- Supply the same `owner/repo` or `owner/repo@branch` used by `add`. Paths resolve under `~/workspace`, regardless of the current directory. If a registered directory is already missing, branch deletion is still attempted; an already missing branch is accepted. No GitHub connection is needed, and sessions are not managed.
398
+ Supply the same `owner/repo` or `owner/repo@branch` used by `add`. Paths resolve under `~/workspace`, regardless of the current directory. If a registered directory is already missing, branch deletion is still attempted; an already missing branch is accepted. No GitHub connection is needed. Session cleanup can be provided by a command hook.
399
+
400
+ A successful `remove` commits the current `workspace.json` with the message `Remove owner/repo` or `Remove owner/repo@branch`. Like `add`, it includes pending manifest edits, preserves unrelated staged files, skips commits when the manifest matches the last commit, and pushes the manifest branch.
401
+
402
+ If committing fails, the checkout and registration are already removed. Resolve the reported Git error and commit only `workspace.json` in the manifest repository, for example:
403
+
404
+ ```sh
405
+ git commit --only -m "Remove holepunchto/brittle" -- workspace.json
406
+ ```
407
+
408
+ Rerunning `remove` is not the commit recovery step: a missing, unregistered checkout still reports an error.
218
409
 
219
410
  Successful removal is silent. Existing checkouts can also be removed if previously unregistered. A missing, unregistered repository reports an error. Path-based `remove` is not supported.
220
411
 
412
+ ### Command hooks
413
+
414
+ Executables on `PATH` named `git-workspace-hook-<command>-post-<name>` run after
415
+ `add`, `remove`, and `rename`, including those operations performed by `pull`. For example,
416
+ a separate tmux package could expose `git-workspace-hook-add-post-tmux` and
417
+ `git-workspace-hook-remove-post-tmux`. Installing the executables on `PATH` enables them;
418
+ there is no hook directory, registration command, or hook configuration in the manifest.
419
+
420
+ ```sh
421
+ git workspace hook list
422
+ git workspace add owner/repo --no-hooks
423
+ git workspace pull --no-hooks
424
+ ```
425
+
426
+ `hook list` shows available hooks and their resolved paths, even before workspace initialization.
427
+ Hooks receive the command and `post` as arguments and versioned JSON on stdin, including
428
+ stable IDs, checkout paths before and after the operation, and its outcome. Hooks run in order
429
+ and are awaited. Failures warn without rolling back the command or changing its exit status.
430
+ Dry runs, help, and completion never execute hooks.
431
+
432
+ See [Writing command hooks](docs/hooks.md) for the full protocol, npm packaging example,
433
+ partial failures, and pull recovery semantics. Hooks must tolerate repeated delivery after
434
+ an interrupted pull. There are no pre hooks, fetch hooks, or prune hooks in this version.
435
+
221
436
  ### Errors
222
437
 
223
438
  Failures print an `error:` message to stderr and exit with status 1. The label is red when stderr is a terminal; redirected output stays plain. When available, a separate `hint:` explains the next step:
@@ -229,6 +444,13 @@ hint: remove the linked worktrees first
229
444
 
230
445
  ## Development
231
446
 
447
+ With Bare installed, run from the repository directory to install dependencies and link the local CLI:
448
+
449
+ ```sh
450
+ npm install
451
+ npm link
452
+ ```
453
+
232
454
  Run the Brittle integration tests on Bare (Git must be installed):
233
455
 
234
456
  ```sh
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env bare
2
2
 
3
3
  import { arg, command, flag, summary } from 'paparam'
4
+ import fs from 'bare-fs'
4
5
  import add from '../lib/add.js'
5
6
  import init from '../lib/init.js'
6
7
  import remove from '../lib/remove.js'
7
8
  import rename from '../lib/rename.js'
8
9
  import list from '../lib/list.js'
9
10
  import status from '../lib/status.js'
11
+ import pull, { formatPlan } from '../lib/pull.js'
12
+ import completion from '../lib/completion.js'
10
13
  import reportError from '../lib/report-error.js'
14
+ import { discoverHooks } from '../lib/hooks.js'
11
15
  import pkg from '../package.json' with { type: 'json' }
12
16
 
13
17
  const cmd = command(
@@ -28,11 +32,12 @@ const cmd = command(
28
32
  ),
29
33
  command(
30
34
  'add',
31
- summary('Clone a repository or create a branch worktree'),
35
+ summary('Clone or initialize a repository, or create a branch worktree'),
32
36
  arg('<repository>', 'owner/repo or owner/repo@branch'),
33
- ({ args }) => {
37
+ flag('--no-hooks', 'Skip command hooks'),
38
+ ({ args, flags }) => {
34
39
  try {
35
- add(args.repository)
40
+ add(args.repository, { noHooks: flags.hooks === false })
36
41
  } catch (err) {
37
42
  reportError(err)
38
43
  }
@@ -42,9 +47,10 @@ const cmd = command(
42
47
  'remove',
43
48
  summary('Remove a repository or linked worktree'),
44
49
  arg('<repository>', 'owner/repo or owner/repo@branch'),
45
- ({ args }) => {
50
+ flag('--no-hooks', 'Skip command hooks'),
51
+ ({ args, flags }) => {
46
52
  try {
47
- remove(args.repository)
53
+ remove(args.repository, { noHooks: flags.hooks === false })
48
54
  } catch (err) {
49
55
  reportError(err)
50
56
  }
@@ -55,9 +61,10 @@ const cmd = command(
55
61
  summary('Rename a root repository or a branch worktree'),
56
62
  arg('<from>', 'owner/repo or owner/repo@branch to rename'),
57
63
  arg('<to>', 'New owner/repo or owner/repo@branch'),
58
- ({ args }) => {
64
+ flag('--no-hooks', 'Skip command hooks'),
65
+ ({ args, flags }) => {
59
66
  try {
60
- rename(args.from, args.to)
67
+ rename(args.from, args.to, { noHooks: flags.hooks === false })
61
68
  } catch (err) {
62
69
  reportError(err)
63
70
  }
@@ -70,6 +77,30 @@ const cmd = command(
70
77
  reportError(err)
71
78
  }
72
79
  }),
80
+ command(
81
+ 'pull',
82
+ summary('Apply changes from the latest workspace manifest'),
83
+ flag('--dry-run', 'Fetch the manifest and preview changes without applying them'),
84
+ flag('--no-hooks', 'Skip command hooks, including pending hooks from an interrupted pull'),
85
+ ({ flags }) => {
86
+ try {
87
+ const plan = pull({ dryRun: flags.dryRun, noHooks: flags.hooks === false })
88
+ console.log(formatPlan(plan))
89
+ if (plan.blockers.length) Bare.exitCode = 1
90
+ } catch (err) {
91
+ reportError(err)
92
+ }
93
+ }
94
+ ),
95
+ command(
96
+ 'hook',
97
+ summary('Inspect command hooks discovered on PATH'),
98
+ command('list', summary('List available post hooks and their executable paths'), () => {
99
+ for (const hook of discoverHooks()) {
100
+ console.log(`${hook.command}/post\t${hook.name}\t${hook.path}`)
101
+ }
102
+ })
103
+ ),
73
104
  command('status', summary('Show clean, modified, or missing checkouts'), async () => {
74
105
  try {
75
106
  const entries = await status()
@@ -81,6 +112,23 @@ const cmd = command(
81
112
  reportError(err)
82
113
  }
83
114
  }),
115
+ command(
116
+ 'completion',
117
+ summary('Print a shell completion script'),
118
+ arg('<shell>', 'Shell to generate completion for (bash)'),
119
+ ({ args }) => {
120
+ try {
121
+ if (args.shell !== 'bash') throw new Error('Supported completion shell: bash')
122
+ console.log(
123
+ fs
124
+ .readFileSync(new URL('../completions/git-workspace.bash', import.meta.url), 'utf8')
125
+ .trimEnd()
126
+ )
127
+ } catch (err) {
128
+ reportError(err)
129
+ }
130
+ }
131
+ ),
84
132
  ({ flags }) => {
85
133
  if (flags.version) {
86
134
  console.log(pkg.version)
@@ -89,8 +137,14 @@ const cmd = command(
89
137
  )
90
138
 
91
139
  try {
92
- const parsed = cmd.parse()
93
- if (parsed) await parsed.running
140
+ if (Bare.argv[2] === '--complete') {
141
+ // Internal completion protocol; flags in these words are input, never actions.
142
+ const candidates = completion(Bare.argv.slice(3), cmd.helpJSON())
143
+ if (candidates.length) console.log(candidates.join('\n'))
144
+ } else {
145
+ const parsed = cmd.parse()
146
+ if (parsed) await parsed.running
147
+ }
94
148
  } catch (err) {
95
149
  reportError(err)
96
150
  }
@@ -0,0 +1,26 @@
1
+ # For `git workspace`, load Git's Bash completion as well as this script.
2
+ # The direct `git-workspace` form also works without Git's completion script.
3
+
4
+ _git_workspace ()
5
+ {
6
+ local first=$(( ${__git_cmd_idx:-0} + 1 )) candidate
7
+ COMPREPLY=()
8
+ while IFS= read -r candidate; do
9
+ case "$candidate" in
10
+ *@) ;;
11
+ *@*) candidate="$candidate " ;;
12
+ */*) ;; # Keep repository names open for an @branch suffix.
13
+ *) candidate="$candidate " ;;
14
+ esac
15
+ COMPREPLY[${#COMPREPLY[@]}]="$candidate"
16
+ done < <(command git-workspace --complete "${words[@]:first:cword-first}" "$cur" 2>/dev/null)
17
+ }
18
+
19
+ _git_workspace_direct ()
20
+ {
21
+ local words=("${COMP_WORDS[@]}") cword=$COMP_CWORD
22
+ local cur="${COMP_WORDS[COMP_CWORD]}" __git_cmd_idx=0
23
+ _git_workspace
24
+ }
25
+
26
+ complete -o nospace -F _git_workspace_direct git-workspace