git-workspace-bare 0.1.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/.lunterc.json +5 -0
- package/.prettierrc +1 -0
- package/LICENSE +201 -0
- package/README.md +252 -0
- package/bin/git-workspace.js +96 -0
- package/lib/add.js +105 -0
- package/lib/checked-out-branch.js +12 -0
- package/lib/init.js +85 -0
- package/lib/list.js +6 -0
- package/lib/map-concurrent.js +22 -0
- package/lib/registry.js +70 -0
- package/lib/remove-empty-parents.js +14 -0
- package/lib/remove.js +126 -0
- package/lib/rename-root.js +173 -0
- package/lib/rename.js +97 -0
- package/lib/report-error.js +9 -0
- package/lib/repository-name.js +14 -0
- package/lib/repository-path.js +18 -0
- package/lib/status.js +52 -0
- package/package.json +30 -0
- package/test/fixtures/cli.js +5 -0
- package/test/helpers.js +83 -0
- package/test/lifecycle.js +479 -0
package/.lunterc.json
ADDED
package/.prettierrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prettier-config-holepunch
|
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# git-workspace-bare
|
|
2
|
+
|
|
3
|
+
Manage Git worktrees as development workspaces.
|
|
4
|
+
|
|
5
|
+
The CLI can clone GitHub repositories, create branch worktrees, list registered checkouts, and unregister them.
|
|
6
|
+
|
|
7
|
+
## Local setup
|
|
8
|
+
|
|
9
|
+
The CLI runs on [Bare](https://github.com/holepunchto/bare). Install Bare using npm:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install -g bare
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run from the repository directory:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install
|
|
19
|
+
npm link
|
|
20
|
+
```
|
|
21
|
+
|
|
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.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
git workspace -h
|
|
28
|
+
git workspace --version
|
|
29
|
+
git workspace init nicolasfont/workspace
|
|
30
|
+
git workspace add holepunchto/brittle
|
|
31
|
+
git workspace list
|
|
32
|
+
git workspace rename holepunchto/brittle@jobs holepunchto/brittle@fixes
|
|
33
|
+
git workspace remove holepunchto/brittle
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `--help` (or `-h`) shows the available options.
|
|
37
|
+
- `--version` prints the package version.
|
|
38
|
+
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
Running `git workspace` without arguments exits successfully without printing anything.
|
|
42
|
+
|
|
43
|
+
### Initialize a manifest repository
|
|
44
|
+
|
|
45
|
+
Initialize your manifest repository:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
git workspace init nicolasfont/workspace
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
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
|
+
|
|
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:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
["holepunchto/brittle", "holepunchto/brittle@feature/jobs"]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
On every machine, those names resolve under `~/workspace`.
|
|
60
|
+
|
|
61
|
+
If the manifest repository has no `workspace.json`, initialization writes an empty array. Old local registry files are not read or migrated.
|
|
62
|
+
|
|
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.
|
|
64
|
+
|
|
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.
|
|
66
|
+
|
|
67
|
+
### Add a repository
|
|
68
|
+
|
|
69
|
+
With Git on your `PATH` and SSH access to GitHub configured:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
git workspace add holepunchto/brittle
|
|
73
|
+
```
|
|
74
|
+
|
|
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.
|
|
76
|
+
|
|
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.
|
|
78
|
+
|
|
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.
|
|
80
|
+
|
|
81
|
+
Path-based `add` is no longer supported. No sessions are started.
|
|
82
|
+
|
|
83
|
+
### Add a branch worktree
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
git workspace add holepunchto/brittle@jobs
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
First ensures `~/workspace/holepunchto/brittle` exists and is registered, cloning it if necessary. Then creates and registers `~/workspace/holepunchto/brittle@jobs`:
|
|
90
|
+
|
|
91
|
+
- Uses the local `jobs` branch if it exists.
|
|
92
|
+
- Otherwise creates `jobs` tracking `origin/jobs`, if that remote-tracking branch exists locally.
|
|
93
|
+
- Otherwise creates `jobs` from the root checkout's current `HEAD`, without an upstream.
|
|
94
|
+
|
|
95
|
+
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
|
+
|
|
97
|
+
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
|
+
|
|
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.
|
|
100
|
+
|
|
101
|
+
### Rename a root repository
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
git workspace rename holepunchto/git-workspace holepunchto/git-workspace-bare
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Moves the root checkout and its linked `owner/repo@branch` directories to the new repository
|
|
108
|
+
name, repairs Git's worktree links, and updates their manifest entries. Branch names, commits,
|
|
109
|
+
and uncommitted files are preserved. Linked worktrees outside this naming layout stay in place
|
|
110
|
+
and their Git links are repaired too.
|
|
111
|
+
|
|
112
|
+
The local `origin` URL is updated to the new name, preserving SSH or HTTPS format. Explicit
|
|
113
|
+
origin push URLs pointing to the old repository are updated too; URLs pointing elsewhere and
|
|
114
|
+
other remotes are left alone. An origin already pointing to the new name is also accepted.
|
|
115
|
+
|
|
116
|
+
This is entirely local: it does not rename anything on GitHub, fetch, commit, or push. Rename
|
|
117
|
+
the repository separately in GitHub **Settings → General → Repository name**, or run:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
gh repo rename git-workspace-bare --repo github.com/holepunchto/git-workspace
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
That separate command changes the GitHub repository's name without pushing commits. It requires
|
|
124
|
+
an authenticated [GitHub CLI](https://cli.github.com/manual/gh_repo_rename) and repository admin
|
|
125
|
+
permissions. You can do the GitHub rename before or after the local rename; the new origin URL
|
|
126
|
+
will only work once the GitHub repository has the new name.
|
|
127
|
+
|
|
128
|
+
Both arguments must use `owner/repo`. The owner can change too: the root and linked worktrees
|
|
129
|
+
move under the new owner's workspace directory, and origin URLs follow the new `owner/repo`.
|
|
130
|
+
Transfer ownership on GitHub separately; the local command does not perform that transfer.
|
|
131
|
+
The source must be a registered standalone root checkout with the expected GitHub origin.
|
|
132
|
+
Existing destination paths or manifest entries, missing or invalid linked worktrees, linked
|
|
133
|
+
worktrees containing submodules, locked
|
|
134
|
+
worktrees that would move, and symbolic-link checkouts are refused. The active manifest
|
|
135
|
+
repository itself cannot be renamed. If a local move, Git repair, or manifest write fails, the
|
|
136
|
+
command attempts to restore the original paths and origin configuration and reports any
|
|
137
|
+
failure to restore them.
|
|
138
|
+
|
|
139
|
+
### Rename a branch worktree
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
git workspace rename holepunchto/brittle@jobs holepunchto/brittle@fixes
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Renames a branch worktree and the branch it holds together, so the directory name and the
|
|
146
|
+
checked-out branch stay in sync. It moves `~/workspace/holepunchto/brittle@jobs` to
|
|
147
|
+
`~/workspace/holepunchto/brittle@fixes`, renames the local branch with `git branch -m`, and
|
|
148
|
+
updates the manifest entry. Uncommitted work moves with the directory; the worktree does not
|
|
149
|
+
need to be clean.
|
|
150
|
+
|
|
151
|
+
Both arguments are full `owner/repo@branch` names, and the `owner/repo` part must match: a
|
|
152
|
+
worktree cannot be renamed into a different repository.
|
|
153
|
+
|
|
154
|
+
The branch actually checked out does not have to match the `@branch` in the source name. If you
|
|
155
|
+
switched branches inside a worktree with ordinary Git, rename it to the branch it now holds to
|
|
156
|
+
line the directory back up:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
git workspace rename holepunchto/brittle@jobs holepunchto/brittle@wandered
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
That moves the directory only. Renaming never touches a branch other than the one the worktree
|
|
163
|
+
has checked out, so a branch left behind by `git switch -c` survives.
|
|
164
|
+
|
|
165
|
+
If the branch has an upstream, it is retargeted to the matching branch on the same remote, so
|
|
166
|
+
renaming `jobs` to `fixes` leaves it tracking `origin/fixes` rather than `origin/jobs`. That
|
|
167
|
+
remote branch may not exist yet; push it before pulling. Branches without an upstream stay
|
|
168
|
+
without one, and remote branches are never renamed or deleted.
|
|
169
|
+
|
|
170
|
+
Renaming is refused if the destination directory exists, if the new branch name is already taken
|
|
171
|
+
by another branch, if the source is not registered or not checked out, or if the worktree is on a
|
|
172
|
+
detached HEAD. Nested branch names use nested directories, as with `add`, and empty parent
|
|
173
|
+
directories left behind are removed.
|
|
174
|
+
|
|
175
|
+
### List repositories
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
git workspace list
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Prints registered repositories as `owner/repo` and linked worktrees as `owner/repo@branch`, one per line sorted alphabetically, regardless of the current directory:
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
holepunchto/brittle
|
|
185
|
+
holepunchto/brittle@jobs
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
An empty manifest produces no output. Missing or invalid manifest data reports an error.
|
|
189
|
+
|
|
190
|
+
Repositories remain listed even if their directories no longer exist. Listing does not modify the registry.
|
|
191
|
+
|
|
192
|
+
### Check workspace status
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
git workspace status
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Checks up to four checkouts concurrently and shows results in alphabetical order, followed by `clean`, `modified`, or `missing`. Modified includes staged changes, unstaged changes, and untracked files; ignored files do not count. Missing means the checkout path no longer exists.
|
|
199
|
+
|
|
200
|
+
An empty registry produces no output. Existing paths that are not valid Git worktree roots report an error. Status does not fetch, modify the registry, or refresh Git's index on disk. It does not report whether branches are ahead of or behind their remotes.
|
|
201
|
+
|
|
202
|
+
### Remove a repository or worktree
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
git workspace remove holepunchto/brittle
|
|
206
|
+
git workspace remove holepunchto/brittle@jobs
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Without `@branch`, deletes the root checkout, including its local Git history and branches, and removes its registry entry. The destination must be a standalone root repository with the expected GitHub origin. The active manifest repository cannot be removed. Removal refuses local changes, untracked files, symbolic-link destinations, or any linked worktrees (including stale worktree records). Remove linked worktrees first. Ignored files are deleted with the checkout, and remote repositories are untouched. Local commits are not checked for whether they have been pushed.
|
|
210
|
+
|
|
211
|
+
With `@branch`, verifies the linked worktree belongs to the root repository, removes the worktree directory, deletes the branch it actually had checked out with `git branch -D`, and unregisters it. The checked-out branch does not have to match the `@branch` in the name: a worktree you switched to another branch is still removed, and that branch is the one deleted. A worktree left on a detached HEAD is removed without deleting any branch. Branch deletion skips Git's merged-status check, including for squash-merged PRs or unmerged local commits. Worktree removal still refuses dirty or locked worktrees, and remote branches are untouched. A worktree previously unregistered by an older version can also be removed this way.
|
|
212
|
+
|
|
213
|
+
After successful worktree and branch removal, empty branch-parent directories are removed up to, but not including, the owner directory. Parents containing sibling worktrees or other files remain.
|
|
214
|
+
|
|
215
|
+
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
|
+
|
|
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.
|
|
218
|
+
|
|
219
|
+
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
|
+
|
|
221
|
+
### Errors
|
|
222
|
+
|
|
223
|
+
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:
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
error: cannot remove holepunchto/brittle: linked worktrees exist
|
|
227
|
+
hint: remove the linked worktrees first
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
Run the Brittle integration tests on Bare (Git must be installed):
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
npm test
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Tests use isolated temporary workspaces and local Git repositories, disable network protocols, and clean up afterward. They do not access your real workspace or Git configuration.
|
|
239
|
+
|
|
240
|
+
Format files with Prettier and the Holepunch config:
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
npm run format
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Lint files with Lunte:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
npm run lint
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The `require-await` rule is disabled, so async functions may omit `await`.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bare
|
|
2
|
+
|
|
3
|
+
import { arg, command, flag, summary } from 'paparam'
|
|
4
|
+
import add from '../lib/add.js'
|
|
5
|
+
import init from '../lib/init.js'
|
|
6
|
+
import remove from '../lib/remove.js'
|
|
7
|
+
import rename from '../lib/rename.js'
|
|
8
|
+
import list from '../lib/list.js'
|
|
9
|
+
import status from '../lib/status.js'
|
|
10
|
+
import reportError from '../lib/report-error.js'
|
|
11
|
+
import pkg from '../package.json' with { type: 'json' }
|
|
12
|
+
|
|
13
|
+
const cmd = command(
|
|
14
|
+
'git-workspace',
|
|
15
|
+
summary('Manage Git worktrees as development workspaces.'),
|
|
16
|
+
flag('--version', 'Show the version'),
|
|
17
|
+
command(
|
|
18
|
+
'init',
|
|
19
|
+
summary('Initialize a GitHub repository for the workspace manifest'),
|
|
20
|
+
arg('<repository>', 'Manifest repository in owner/repo form'),
|
|
21
|
+
({ args }) => {
|
|
22
|
+
try {
|
|
23
|
+
init(args.repository)
|
|
24
|
+
} catch (err) {
|
|
25
|
+
reportError(err)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
),
|
|
29
|
+
command(
|
|
30
|
+
'add',
|
|
31
|
+
summary('Clone a repository or create a branch worktree'),
|
|
32
|
+
arg('<repository>', 'owner/repo or owner/repo@branch'),
|
|
33
|
+
({ args }) => {
|
|
34
|
+
try {
|
|
35
|
+
add(args.repository)
|
|
36
|
+
} catch (err) {
|
|
37
|
+
reportError(err)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
),
|
|
41
|
+
command(
|
|
42
|
+
'remove',
|
|
43
|
+
summary('Remove a repository or linked worktree'),
|
|
44
|
+
arg('<repository>', 'owner/repo or owner/repo@branch'),
|
|
45
|
+
({ args }) => {
|
|
46
|
+
try {
|
|
47
|
+
remove(args.repository)
|
|
48
|
+
} catch (err) {
|
|
49
|
+
reportError(err)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
),
|
|
53
|
+
command(
|
|
54
|
+
'rename',
|
|
55
|
+
summary('Rename a root repository or a branch worktree'),
|
|
56
|
+
arg('<from>', 'owner/repo or owner/repo@branch to rename'),
|
|
57
|
+
arg('<to>', 'New owner/repo or owner/repo@branch'),
|
|
58
|
+
({ args }) => {
|
|
59
|
+
try {
|
|
60
|
+
rename(args.from, args.to)
|
|
61
|
+
} catch (err) {
|
|
62
|
+
reportError(err)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
),
|
|
66
|
+
command('list', summary('List registered repositories and worktrees'), () => {
|
|
67
|
+
try {
|
|
68
|
+
for (const repository of list()) console.log(repository)
|
|
69
|
+
} catch (err) {
|
|
70
|
+
reportError(err)
|
|
71
|
+
}
|
|
72
|
+
}),
|
|
73
|
+
command('status', summary('Show clean, modified, or missing checkouts'), async () => {
|
|
74
|
+
try {
|
|
75
|
+
const entries = await status()
|
|
76
|
+
const width = entries.reduce((max, entry) => Math.max(max, entry.repository.length), 0)
|
|
77
|
+
for (const { repository, state } of entries) {
|
|
78
|
+
console.log(`${repository.padEnd(width)} ${state}`)
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
reportError(err)
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
({ flags }) => {
|
|
85
|
+
if (flags.version) {
|
|
86
|
+
console.log(pkg.version)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const parsed = cmd.parse()
|
|
93
|
+
if (parsed) await parsed.running
|
|
94
|
+
} catch (err) {
|
|
95
|
+
reportError(err)
|
|
96
|
+
}
|
package/lib/add.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'bare-fs'
|
|
2
|
+
import path from 'bare-path'
|
|
3
|
+
import { spawnSync } from 'bare-subprocess'
|
|
4
|
+
import { readRegistry, updateRegistry } from './registry.js'
|
|
5
|
+
import repositoryPath from './repository-path.js'
|
|
6
|
+
|
|
7
|
+
export default function add(repository) {
|
|
8
|
+
const directory = repositoryPath(repository)
|
|
9
|
+
readRegistry()
|
|
10
|
+
const [base, branch] = repository.split('@')
|
|
11
|
+
if (branch) {
|
|
12
|
+
git(['check-ref-format', '--branch', branch])
|
|
13
|
+
add(base)
|
|
14
|
+
const root = repositoryPath(base)
|
|
15
|
+
if (!exists(directory)) {
|
|
16
|
+
const args = ['-C', root, 'worktree', 'add']
|
|
17
|
+
if (hasRef(root, `refs/heads/${branch}`)) {
|
|
18
|
+
git([...args, directory, branch], { stdio: 'inherit' })
|
|
19
|
+
} else if (hasRef(root, `refs/remotes/origin/${branch}`)) {
|
|
20
|
+
git([...args, '--track', '-b', branch, directory, `refs/remotes/origin/${branch}`], {
|
|
21
|
+
stdio: 'inherit'
|
|
22
|
+
})
|
|
23
|
+
} else {
|
|
24
|
+
git([...args, '--no-track', '-b', branch, directory, 'HEAD'], { stdio: 'inherit' })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const worktree = fs.realpathSync(directory)
|
|
29
|
+
if (fs.realpathSync(git(['-C', directory, 'rev-parse', '--show-toplevel'])) !== worktree) {
|
|
30
|
+
throw new Error('Destination is not a Git worktree root')
|
|
31
|
+
}
|
|
32
|
+
const common = (cwd) =>
|
|
33
|
+
fs.realpathSync(git(['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir']))
|
|
34
|
+
if (
|
|
35
|
+
common(directory) !== common(root) ||
|
|
36
|
+
git(['-C', directory, 'symbolic-ref', 'HEAD']) !== `refs/heads/${branch}`
|
|
37
|
+
) {
|
|
38
|
+
throw new Error('Destination is not a worktree of the requested repository and branch')
|
|
39
|
+
}
|
|
40
|
+
register(repository, directory, worktree)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!exists(directory)) {
|
|
45
|
+
fs.mkdirSync(path.dirname(directory), { recursive: true })
|
|
46
|
+
git(['clone', `git@github.com:${repository}.git`, directory], { stdio: 'inherit' })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const worktree = fs.realpathSync(directory)
|
|
50
|
+
const root = git(['-C', directory, 'rev-parse', '--show-toplevel'])
|
|
51
|
+
if (fs.realpathSync(root) !== worktree) throw new Error('Destination is not a Git worktree root')
|
|
52
|
+
|
|
53
|
+
const origin = git(['-C', directory, 'config', '--get', 'remote.origin.url'])
|
|
54
|
+
const match =
|
|
55
|
+
/^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/github\.com\/)([^/]+\/[^/]+?)\/?$/i.exec(
|
|
56
|
+
origin
|
|
57
|
+
)
|
|
58
|
+
if (!match || match[1].replace(/\.git$/i, '').toLowerCase() !== repository.toLowerCase()) {
|
|
59
|
+
throw new Error(`Destination origin does not match ${repository}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
register(repository, directory, worktree)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function exists(directory) {
|
|
66
|
+
try {
|
|
67
|
+
fs.lstatSync(directory)
|
|
68
|
+
return true
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err.code !== 'ENOENT') throw err
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasRef(directory, ref) {
|
|
76
|
+
const result = spawnSync('git', ['-C', directory, 'show-ref', '--verify', '--quiet', ref])
|
|
77
|
+
if (result.error) throw result.error
|
|
78
|
+
if (result.status === 1) return false
|
|
79
|
+
if (result.status !== 0) {
|
|
80
|
+
throw new Error(result.stderr?.toString().trim() || 'Could not inspect Git ref')
|
|
81
|
+
}
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function register(repository, directory, worktree) {
|
|
86
|
+
try {
|
|
87
|
+
updateRegistry((worktrees) => {
|
|
88
|
+
if (!worktrees.includes(worktree)) worktrees.push(worktree)
|
|
89
|
+
return worktrees
|
|
90
|
+
})
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Could not register ${directory}: ${err.message}. Retry git workspace add ${repository} after fixing the registry.`
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function git(args, options) {
|
|
99
|
+
const result = spawnSync('git', args, options)
|
|
100
|
+
if (result.error) throw result.error
|
|
101
|
+
if (result.status !== 0) {
|
|
102
|
+
throw new Error(result.stderr?.toString().trim() || 'Git command failed')
|
|
103
|
+
}
|
|
104
|
+
return result.stdout?.toString().replace(/\r?\n$/, '')
|
|
105
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { spawnSync } from 'bare-subprocess'
|
|
2
|
+
|
|
3
|
+
export default function checkedOutBranch(directory) {
|
|
4
|
+
const result = spawnSync('git', ['-C', directory, 'symbolic-ref', '--quiet', 'HEAD'])
|
|
5
|
+
if (result.error) throw result.error
|
|
6
|
+
if (result.status === 1) return null
|
|
7
|
+
if (result.status !== 0) {
|
|
8
|
+
throw new Error(result.stderr?.toString().trim() || 'Could not inspect Git HEAD')
|
|
9
|
+
}
|
|
10
|
+
const ref = result.stdout?.toString().replace(/\r?\n$/, '')
|
|
11
|
+
return ref?.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : null
|
|
12
|
+
}
|