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 CHANGED
@@ -2,7 +2,7 @@
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
7
  ## Installation
8
8
 
@@ -14,6 +14,57 @@ npm install -g bare git-workspace-bare
14
14
 
15
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
+ For optional integrations such as tmux session management, see [available hooks](#available-hooks).
18
+
19
+ ### Manual page
20
+
21
+ The package includes a `git-workspace(1)` manual covering commands, options,
22
+ manifest synchronization, and hooks:
23
+
24
+ ```sh
25
+ git workspace --help
26
+ git help workspace
27
+ man git-workspace
28
+ ```
29
+
30
+ Global installation (or `npm link` during development) registers the page with npm
31
+ versions that support man-page linking. If your system cannot find it, add this line
32
+ to your shell startup file to include the packaged manual in its search path:
33
+
34
+ ```sh
35
+ export MANPATH="$(npm root -g)/git-workspace-bare/man:${MANPATH:-}"
36
+ ```
37
+
38
+ The trailing empty entry preserves your system's default manual paths. This also
39
+ works with npm 12, which no longer registers man pages automatically.
40
+
41
+ If a shell wrapper intercepts `git workspace --help` and shows CLI usage instead,
42
+ use `git help workspace` or `man git-workspace` to open the manual directly.
43
+
44
+ ### Bash completion
45
+
46
+ Add this line to `~/.bashrc` (or the startup file your interactive Bash shell loads):
47
+
48
+ ```bash
49
+ eval "$(git-workspace completion bash)"
50
+ ```
51
+
52
+ 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).
53
+ The workspace script extends it; existing Git completions are preserved. The direct
54
+ `git-workspace` command has completion even without Git's completion script. Open a new shell
55
+ or run that line in your current shell to enable it. Bash 3.2 and newer are supported.
56
+
57
+ Tab completes commands, flags, and registered repository/worktree names for `remove` and
58
+ the source of `rename`. For `add`, it suggests registered roots; after `owner/repo@`, it
59
+ suggests local branches and already-fetched origin branches. New repository and branch names
60
+ can still be typed freely. Completed repository names have no trailing space, so you can
61
+ press Enter or append `@` and complete a branch. For a branch rename's destination, it fills the `owner/repo@`
62
+ prefix so you can type the new branch name.
63
+
64
+ Suggestions use local data only, without fetching or changing the workspace. Missing or
65
+ invalid workspace data silently produces no repository suggestions; commands and flags
66
+ remain available before `init`. Currently only Bash is supported.
67
+
17
68
  ## Usage
18
69
 
19
70
  ```sh
@@ -22,11 +73,12 @@ git workspace --version
22
73
  git workspace init nicolasfont/workspace
23
74
  git workspace add holepunchto/brittle
24
75
  git workspace list
76
+ git workspace pull --dry-run
25
77
  git workspace rename holepunchto/brittle@jobs holepunchto/brittle@fixes
26
78
  git workspace remove holepunchto/brittle
27
79
  ```
28
80
 
29
- - `--help` (or `-h`) shows the available options.
81
+ - `-h` shows the available options; `git workspace --help` opens the manual page.
30
82
  - `--version` prints the package version.
31
83
 
32
84
  You can also invoke the executable directly, for example `git-workspace list`. Use `git workspace -h` or `git-workspace --help` for CLI help; Git handles `git workspace --help` as a manual-page request.
@@ -43,35 +95,176 @@ git workspace init nicolasfont/workspace
43
95
 
44
96
  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.
45
97
 
46
- 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:
98
+ 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:
47
99
 
48
100
  ```json
49
- ["holepunchto/brittle", "holepunchto/brittle@feature/jobs"]
101
+ {
102
+ "version": 1,
103
+ "repositories": [
104
+ {
105
+ "id": "8257b322-695c-4c97-a816-c6c2c6198e40",
106
+ "name": "holepunchto/brittle",
107
+ "worktrees": [
108
+ {
109
+ "id": "37326a40-98e7-461b-9dd3-fc6a57f0b856",
110
+ "branch": "feature/jobs"
111
+ }
112
+ ]
113
+ }
114
+ ]
115
+ }
116
+ ```
117
+
118
+ On every machine, those names resolve under `~/workspace`. `list` still displays
119
+ `owner/repo` and `owner/repo@branch`; IDs are stored in the manifest, not used in CLI arguments.
120
+
121
+ Each repository and linked worktree gets a UUID when first registered. Renaming a repository
122
+ (including changing its owner) preserves its ID and all its worktree IDs. Renaming a branch
123
+ worktree preserves its ID. Removing an entry deletes it from the current manifest; reusing
124
+ that name later creates a new ID. Old identities remain in Git history, so names are not
125
+ reserved forever. Treat IDs as permanent identifiers when editing the manifest manually.
126
+
127
+ IDs must be unique across the entire manifest, and repository names and worktree names must
128
+ not be duplicated. Worktrees belong to their root repository. Invalid entries and unsupported
129
+ manifest versions are rejected before changing checkouts. The manifest records desired state;
130
+ reading it does not apply changes. A local applied-commit checkpoint records verified state;
131
+ `git workspace pull` synchronizes the local layout, and `--dry-run` previews changes first.
132
+
133
+ 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.
134
+
135
+ 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`.
136
+
137
+ 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.
138
+
139
+ 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.
140
+
141
+ Automatic pushes target the manifest branch's configured upstream, or the same branch name
142
+ on `origin` when no upstream is configured. A successful push establishes tracking. Only
143
+ that manifest branch is pushed, including its existing local commits; project branches and
144
+ tags are not published. Pushes never force, fetch, or merge remote history.
145
+
146
+ A push failure warns without undoing the checkout operation or local commit, changing the
147
+ command's success status, or preventing local applied tracking and post hooks. This allows
148
+ commands to work offline or before the manifest repository exists on GitHub. Once connected,
149
+ repeat `add` for an already registered checkout to retry the push without another commit,
150
+ or push from the manifest repository yourself (for a new upstream, `git push -u origin HEAD`).
151
+ For a non-fast-forward rejection, resolve the divergent manifest history before retrying.
152
+
153
+ Only the version-1 object format is supported. Old array manifests are rejected without being
154
+ rewritten. Keep IDs when editing entries; replacing an ID means replacing that entry's identity.
155
+
156
+ After successful `init`, `add`, `remove`, and `rename` commands, the CLI automatically
157
+ verifies the committed manifest against this machine's checkouts and records the applied
158
+ commit locally. This internal bookkeeping supports future synchronization; it does not
159
+ create commits or push anything. Dirty files and local commits in your repositories are allowed.
160
+ If verification finds a mismatch, the completed operation is preserved and a warning explains
161
+ why local tracking could not advance. Tracking is checked again on the next successful command
162
+ that initializes or changes the workspace. `list` and `status` do not change it.
163
+
164
+ ### Synchronize from the manifest
165
+
166
+ ```sh
167
+ git workspace pull --dry-run
168
+ git workspace pull
50
169
  ```
51
170
 
52
- On every machine, those names resolve under `~/workspace`.
171
+ Fetches the manifest branch from its configured upstream. If the local branch has no upstream,
172
+ it uses the same branch name on `origin`. The manifest repository must be on a branch with a
173
+ clean working tree and index. Divergent histories are refused; local commits ahead of the
174
+ remote are retained; pull itself does not push them.
53
175
 
54
- If the manifest repository has no `workspace.json`, initialization writes an empty array. Old local registry files are not read or migrated.
176
+ The preview compares the latest manifest with this machine's last verified applied state.
177
+ Matching IDs identify renames, even across owners. A root rename includes its worktrees;
178
+ branch renames are listed separately only when the branch name changes. Removed children
179
+ are listed before their parents, and parents are added before children. Reusing a name with
180
+ a new ID means removing the old entry and adding the replacement.
55
181
 
56
- 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.
182
+ For example:
57
183
 
58
- 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.
184
+ ```text
185
+ Manifest: origin/main
186
+ Would fast-forward the manifest
187
+ rename owner/repo -> other/new
188
+ rename other/new@jobs -> other/new@fixes
189
+ add other/new@feature/new
190
+ Dry run: no manifest checkout or workspace changes applied
191
+ ```
192
+
193
+ The planner reports missing or mismatched checkouts, occupied destinations, existing branch
194
+ targets, locked worktrees, and dirty checkouts that would be removed. Dirty work is allowed
195
+ in retained or renamed checkouts. Blockers are printed with `blocked:` and exit status 1;
196
+ the preview still shows the requested changes. These checks describe local conditions now;
197
+ they cannot guarantee a future clone or filesystem operation will succeed.
198
+
199
+ On a new machine without an applied baseline, the preview plans additions and reports
200
+ matching existing checkouts as `adopt`. It does not infer old removals or renames from history
201
+ before that machine's first verified baseline. Unexpected local layout changes after a
202
+ baseline are reported for manual resolution.
203
+
204
+ Without `--dry-run`, pull checks for blockers before fast-forwarding the manifest and
205
+ applying the layout changes. It moves renamed checkouts, updates renamed roots' origin URLs,
206
+ removes clean retired checkouts, and clones or creates additions. GitHub renames and transfers
207
+ remain manual. Clean removals can delete local commits, just like `git workspace remove`;
208
+ preview the plan before applying removals. Existing managed repositories are never fetched
209
+ or pulled. Pull creates no manifest commits and pushes nothing. Unlike a direct `add`, pull
210
+ requires new repositories to clone successfully; it never substitutes an empty local repository
211
+ for an unavailable remote.
212
+
213
+ If an operation fails, completed changes remain in place. Resolve the reported error and
214
+ rerun `git workspace pull` to resume the saved target. The next pull fetches any newer remote
215
+ changes. Progress is stored locally in `.git/workspace-pull.json`, with the target commit
216
+ pinned at `refs/git-workspace/pull-target`. Other commands that modify the manifest are
217
+ blocked while a pull is pending. Local applied tracking advances only after the complete
218
+ layout has been verified; progress is then cleared. Do not edit or delete the progress file
219
+ to retry. A dry run during recovery checks the remaining changes without applying them;
220
+ a partially moved rename may need a real pull to repair its Git links before those checks
221
+ can finish.
222
+
223
+ A dry run fetches objects and updates Git's fetch metadata and remote-tracking refs in the
224
+ manifest repository. It never checks out the fetched manifest, applies layout changes,
225
+ advances local applied tracking, creates commits, or pushes.
59
226
 
60
227
  ### Add a repository
61
228
 
62
- With Git on your `PATH` and SSH access to GitHub configured:
229
+ With Git on your `PATH`:
63
230
 
64
231
  ```sh
65
232
  git workspace add holepunchto/brittle
66
233
  ```
67
234
 
68
- 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.
235
+ The required `owner/repo` identifies a GitHub repository, including one you have not created
236
+ on GitHub yet. If `~/workspace/holepunchto/brittle` is absent or an empty directory, the command
237
+ tries to clone `git@github.com:holepunchto/brittle.git` there. The default workspace directory
238
+ is `~/workspace`, regardless of your current directory.
239
+
240
+ If cloning fails and the destination is still absent or empty, `add` initializes an empty local
241
+ Git repository and configures that URL as `origin`. This works offline and when the remote
242
+ does not exist or is inaccessible, with no flag or confirmation required. The clone diagnostic
243
+ is preserved, followed by a notice explaining that a local repository was initialized. The
244
+ checkout is registered and committed to the manifest normally, and add hooks run normally.
245
+ No GitHub repository is created and no project commits are pushed. The workspace manifest commit is pushed separately.
246
+
247
+ The local repository follows Git's `init.defaultBranch` setting and has no initial commit.
248
+ Create your first commit when ready. Later remote synchronization is explicit: repeating `add`
249
+ reuses the local checkout, even once you are online. If the remote already has history, reconcile
250
+ that history with any commits you made locally before pushing.
69
251
 
70
- 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.
252
+ Default SSH clone attempts use a five-second connection timeout, one connection attempt, and
253
+ noninteractive authentication, using keys already available to SSH. The timeout covers the
254
+ [SSH connection and initial handshake](https://man.openbsd.org/ssh_config#ConnectTimeout),
255
+ not the entire clone or system DNS resolution. Custom `GIT_SSH`, `GIT_SSH_COMMAND`,
256
+ `core.sshCommand`, and non-OpenSSH variants keep their own transport settings; URL rewrites
257
+ to other transports also retain their own connection behavior.
71
258
 
72
- 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.
259
+ A failed clone that leaves files behind is preserved and reported as an error. Initialization
260
+ never overwrites a nonempty destination or follows a destination symlink. Git invocation,
261
+ interruption, and local filesystem failures remain errors.
73
262
 
74
- Path-based `add` is no longer supported. No sessions are started.
263
+ 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.
264
+
265
+ 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.
266
+
267
+ Path-based `add` is no longer supported. Session management can be provided by a command hook.
75
268
 
76
269
  ### Add a branch worktree
77
270
 
@@ -79,17 +272,39 @@ Path-based `add` is no longer supported. No sessions are started.
79
272
  git workspace add holepunchto/brittle@jobs
80
273
  ```
81
274
 
82
- First ensures `~/workspace/holepunchto/brittle` exists and is registered, cloning it if necessary. Then creates and registers `~/workspace/holepunchto/brittle@jobs`:
275
+ First ensures `~/workspace/holepunchto/brittle` exists and is registered, cloning it or initializing
276
+ it locally if necessary. Then creates and registers `~/workspace/holepunchto/brittle@jobs`:
83
277
 
84
278
  - Uses the local `jobs` branch if it exists.
85
279
  - Otherwise creates `jobs` tracking `origin/jobs`, if that remote-tracking branch exists locally.
86
280
  - Otherwise creates `jobs` from the root checkout's current `HEAD`, without an upstream.
87
281
 
282
+ An empty root needs its first commit before a branch worktree can be created. If the root was
283
+ just initialized locally, it stays registered and the command explains how to make the first
284
+ commit and retry. It does not fabricate a commit to create the worktree.
285
+
88
286
  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.
89
287
 
90
288
  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.
91
289
 
92
- 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.
290
+ 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.
291
+
292
+ ### Rename commits
293
+
294
+ After a successful root or branch rename, the current `workspace.json` is committed with the
295
+ message `Rename <from> to <to>`. A root rename and all its linked worktree entries are included
296
+ in one commit. Pending manifest edits are included; unrelated staged files remain staged.
297
+ No commit is created if the manifest matches the last commit. The manifest branch is then pushed.
298
+
299
+ If committing fails, the renamed checkouts, origin configuration, and updated manifest are
300
+ retained. Resolve the Git error and commit only `workspace.json` in the manifest repository,
301
+ for example:
302
+
303
+ ```sh
304
+ git commit --only -m "Rename holepunchto/brittle@jobs to holepunchto/brittle@fixes" -- workspace.json
305
+ ```
306
+
307
+ The rename is already complete, so do not rerun it using the old name to retry the commit.
93
308
 
94
309
  ### Rename a root repository
95
310
 
@@ -106,7 +321,7 @@ The local `origin` URL is updated to the new name, preserving SSH or HTTPS forma
106
321
  origin push URLs pointing to the old repository are updated too; URLs pointing elsewhere and
107
322
  other remotes are left alone. An origin already pointing to the new name is also accepted.
108
323
 
109
- This is entirely local: it does not rename anything on GitHub, fetch, commit, or push. Rename
324
+ The checkout rename does not rename anything on GitHub, fetch, or push project commits. Its workspace manifest commit is pushed. Rename
110
325
  the repository separately in GitHub **Settings → General → Repository name**, or run:
111
326
 
112
327
  ```sh
@@ -207,10 +422,57 @@ After successful worktree and branch removal, empty branch-parent directories ar
207
422
 
208
423
  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.
209
424
 
210
- 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.
425
+ 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.
426
+
427
+ 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.
428
+
429
+ 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:
430
+
431
+ ```sh
432
+ git commit --only -m "Remove holepunchto/brittle" -- workspace.json
433
+ ```
434
+
435
+ Rerunning `remove` is not the commit recovery step: a missing, unregistered checkout still reports an error.
211
436
 
212
437
  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.
213
438
 
439
+ ### Command hooks
440
+
441
+ Executables on `PATH` named `git-workspace-hook-<command>-post-<name>` run after
442
+ `add`, `remove`, and `rename`, including those operations performed by `pull`.
443
+ Installing the executables on `PATH` enables them;
444
+ there is no hook directory, registration command, or hook configuration in the manifest.
445
+
446
+ ```sh
447
+ git workspace hook list
448
+ git workspace add owner/repo --no-hooks
449
+ git workspace pull --no-hooks
450
+ ```
451
+
452
+ `hook list` shows available hooks and their resolved paths, even before workspace initialization.
453
+ Hooks receive the command and `post` as arguments and versioned JSON on stdin, including
454
+ stable IDs, checkout paths before and after the operation, and its outcome. Hooks run in order
455
+ and are awaited. Failures warn without rolling back the command or changing its exit status.
456
+ Dry runs, help, and completion never execute hooks.
457
+
458
+ See [Writing command hooks](docs/hooks.md) for the full protocol, npm packaging example,
459
+ partial failures, and pull recovery semantics. Hooks must tolerate repeated delivery after
460
+ an interrupted pull. There are no pre hooks, fetch hooks, or prune hooks in this version.
461
+
462
+ #### Available hooks
463
+
464
+ [git-workspace-hook-tmux](https://github.com/nicolasfont/git-workspace-hook-tmux) creates
465
+ detached tmux sessions when checkouts are added, preserves running panes across renames,
466
+ and closes managed sessions when checkouts are removed. Requires tmux on `PATH`.
467
+
468
+ ```sh
469
+ npm install -g git-workspace-hook-tmux
470
+ ```
471
+
472
+ Installation enables its add, remove, and rename hooks automatically. Sessions are never
473
+ attached or switched automatically. Removing a checkout terminates its managed session
474
+ and all programs running in its panes.
475
+
214
476
  ### Errors
215
477
 
216
478
  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:
@@ -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