git-workspace-bare 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -24
- package/bin/git-workspace.js +63 -9
- package/completions/git-workspace.bash +26 -0
- package/docs/hooks.md +214 -0
- package/lib/add.js +55 -7
- package/lib/apply-pull.js +140 -0
- package/lib/checkpoint.js +208 -0
- package/lib/clone-repository.js +51 -0
- package/lib/commit-manifest.js +29 -0
- package/lib/completion.js +93 -0
- package/lib/git-network-environment.js +31 -0
- package/lib/hook-context.js +239 -0
- package/lib/hooks.js +87 -0
- package/lib/init.js +17 -4
- package/lib/manifest.js +102 -0
- package/lib/plan-pull.js +332 -0
- package/lib/pull-state.js +113 -0
- package/lib/pull.js +269 -0
- package/lib/push-manifest.js +53 -0
- package/lib/registry.js +23 -12
- package/lib/remove.js +27 -2
- package/lib/rename-root.js +182 -84
- package/lib/rename.js +92 -16
- package/package.json +3 -1
- package/test/add-offline.js +240 -0
- package/test/checkpoint.js +243 -0
- package/test/completion.js +180 -0
- package/test/fixtures/checkpoint.js +14 -0
- package/test/fixtures/completion.bash +28 -0
- package/test/helpers.js +60 -2
- package/test/hooks.js +451 -0
- package/test/lifecycle.js +393 -13
- package/test/manifest-push.js +135 -0
- package/test/manifest.js +170 -0
- package/test/pull.js +696 -0
package/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
|
-
##
|
|
7
|
+
## Installation
|
|
8
8
|
|
|
9
|
-
The CLI runs on [Bare](https://github.com/holepunchto/bare). Install
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
package/bin/git-workspace.js
CHANGED
|
@@ -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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
|
|
93
|
-
|
|
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
|