git-worktree-organize 1.1.0 → 1.1.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 +200 -78
- package/dist/cli.js +188 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
# git-worktree-organize
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Convert any git repository into a bare-hub worktree layout where every branch lives in its own directory. No more stashing, no more switching.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **Use at your own risk.** This tool modifies your git repository structure. Always ensure you have a copy of important repositories before running it.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
> **Opinionated layout.** This tool enforces a specific structure where every branch is a sibling directory under a single hub root. If you prefer a different worktree arrangement, this tool may not be for you. See [Why this layout?](#why-this-layout) for the rationale.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## The target layout
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
|
|
12
|
+
myrepo/
|
|
13
13
|
├── .bare/ ← bare git repo (the actual git database)
|
|
14
14
|
├── .git ← plain file: "gitdir: ./.bare"
|
|
15
15
|
├── main/ ← worktree for the main branch
|
|
16
16
|
└── feature-x/ ← worktree for each other branch
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Each branch directory is a fully functional working tree. Open them in separate terminals or IDE windows simultaneously
|
|
19
|
+
Each branch directory is a fully functional working tree. Open them in separate terminals or IDE windows simultaneously.
|
|
20
20
|
|
|
21
|
-
The `.git` file at the hub root
|
|
21
|
+
The `.git` file at the hub root means `git worktree list` (and any git command) works from **anywhere** -- the hub root, any worktree directory, or even nested subdirectories. IDEs, linters, and other tools that walk up looking for `.git` also find the repo correctly.
|
|
22
22
|
|
|
23
23
|
## Installation
|
|
24
24
|
|
|
@@ -39,145 +39,267 @@ git-worktree-organize <source> [destination]
|
|
|
39
39
|
git-worktree-organize <source> [destination]
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
| Argument | Description
|
|
43
|
-
|
|
44
|
-
| `source` | Path to the existing git repository to migrate
|
|
45
|
-
| `destination` | Target hub directory (omit for in-place migration
|
|
46
|
-
|
|
47
|
-
**Without a destination**, the tool prompts for in-place migration:
|
|
48
|
-
- Renames `<source>` to `<source>.old`
|
|
49
|
-
- Creates the hub at the original `<source>` path
|
|
50
|
-
|
|
51
|
-
**With a destination**, the tool migrates to the specified path.
|
|
42
|
+
| Argument | Description |
|
|
43
|
+
|---------------|------------------------------------------------------------|
|
|
44
|
+
| `source` | Path to the existing git repository to migrate |
|
|
45
|
+
| `destination` | Target hub directory (omit for in-place migration) |
|
|
52
46
|
|
|
53
47
|
The tool shows a preview of what will be created and asks for confirmation before making any changes.
|
|
54
48
|
|
|
55
|
-
|
|
49
|
+
### In-place migration
|
|
56
50
|
|
|
57
|
-
|
|
51
|
+
When no destination is provided, the tool prompts to migrate in place:
|
|
52
|
+
|
|
53
|
+
1. Renames `<source>` to `<source>.old`
|
|
54
|
+
2. Creates the hub at the original `<source>` path
|
|
58
55
|
|
|
59
56
|
```sh
|
|
60
57
|
git-worktree-organize /projects/myrepo
|
|
61
58
|
```
|
|
62
59
|
|
|
63
|
-
|
|
60
|
+
Result:
|
|
64
61
|
|
|
65
62
|
```
|
|
66
|
-
/projects/myrepo/
|
|
63
|
+
/projects/myrepo/ ← new hub
|
|
67
64
|
├── .bare/
|
|
68
65
|
├── .git
|
|
69
66
|
├── main/
|
|
70
67
|
└── feature-x/
|
|
71
68
|
|
|
72
|
-
/projects/myrepo.old/
|
|
69
|
+
/projects/myrepo.old/ ← original repo (renamed)
|
|
73
70
|
```
|
|
74
71
|
|
|
75
|
-
|
|
72
|
+
The `.old` directory is kept so you can verify the migration before deleting it manually.
|
|
73
|
+
|
|
74
|
+
### Migration to a new location
|
|
75
|
+
|
|
76
|
+
When a destination is provided, the source repo is moved into the new hub as the main branch worktree:
|
|
76
77
|
|
|
77
78
|
```sh
|
|
78
|
-
git-worktree-organize /projects/myrepo /projects/myrepo-
|
|
79
|
+
git-worktree-organize /projects/myrepo /projects/myrepo-hub
|
|
79
80
|
```
|
|
80
81
|
|
|
81
82
|
Result:
|
|
82
83
|
|
|
83
84
|
```
|
|
84
|
-
/projects/myrepo-
|
|
85
|
+
/projects/myrepo-hub/
|
|
85
86
|
├── .bare/
|
|
86
87
|
├── .git
|
|
87
|
-
├── main/
|
|
88
|
+
├── main/ ← original /projects/myrepo moved here
|
|
88
89
|
├── feature-x/
|
|
89
90
|
└── hotfix/
|
|
90
91
|
```
|
|
91
92
|
|
|
92
|
-
The original
|
|
93
|
+
The original source directory becomes the `main/` worktree. No data is lost.
|
|
94
|
+
|
|
95
|
+
## Supported repository types
|
|
96
|
+
|
|
97
|
+
The tool detects and migrates five different git repository layouts:
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
| Type | Description |
|
|
100
|
+
|------|-------------|
|
|
101
|
+
| **Standard** | Ordinary repo with a `.git` directory |
|
|
102
|
+
| **Bare root** | Bare repo with git internals at the root (`HEAD`, `refs/`, `objects/`) |
|
|
103
|
+
| **Bare dotgit** | Repo where `.git` is a bare directory (`core.bare = true`) |
|
|
104
|
+
| **Bare external** | Repo where `.git` is a file pointing to a gitdir elsewhere |
|
|
105
|
+
| **Bare hub** | Already in the bare-hub layout (re-organizes worktrees into canonical structure) |
|
|
95
106
|
|
|
96
|
-
|
|
107
|
+
See [Layout conversions](#layout-conversions) for detailed before/after diagrams of each type.
|
|
97
108
|
|
|
98
|
-
|
|
109
|
+
## Recovery
|
|
99
110
|
|
|
100
|
-
|
|
101
|
-
- **Bare-root** — bare repo with git internals at the root (`HEAD`, `refs/`, `objects/`)
|
|
102
|
-
- **Bare-dotgit** — repo where `.git` is a bare git directory (`core.bare = true`)
|
|
103
|
-
- **Bare-external** — repo where `.git` is a file pointing to a gitdir elsewhere
|
|
104
|
-
- **Bare-hub** — already in the bare-hub layout (re-organizes worktrees into the canonical structure)
|
|
111
|
+
Running the tool on an existing hub detects problems and offers to fix them:
|
|
105
112
|
|
|
106
|
-
|
|
113
|
+
- **Partial migrations** -- Resumes moving worktrees that weren't fully processed (e.g. after an interruption).
|
|
114
|
+
- **Stale `.git` pointers** -- Repairs worktrees with broken connections to the bare repo.
|
|
115
|
+
- **Missing worktrees** -- Searches for worktrees that were moved outside the hub (up to 3 directory levels deep).
|
|
116
|
+
- **Parent directory renames** -- Detects and repairs when a hub's parent directory was renamed.
|
|
107
117
|
|
|
108
|
-
|
|
118
|
+
```sh
|
|
119
|
+
# Resume a partial migration or repair an existing hub
|
|
120
|
+
git-worktree-organize /path/to/hub
|
|
109
121
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
4. **Fix parent directory renames** — Automatically detect and repair when a hub's parent directory was renamed
|
|
122
|
+
# Repair after renaming a parent directory (can point to any worktree inside the hub)
|
|
123
|
+
git-worktree-organize /new/path/to/hub/some-worktree
|
|
124
|
+
```
|
|
114
125
|
|
|
115
|
-
|
|
126
|
+
## Safety
|
|
116
127
|
|
|
117
|
-
- **Interactive confirmation**
|
|
118
|
-
- **Branch name sanitization**
|
|
119
|
-
- **Collision detection**
|
|
120
|
-
- **
|
|
128
|
+
- **Interactive confirmation** -- Preview all changes before execution
|
|
129
|
+
- **Branch name sanitization** -- Slashes become hyphens (e.g. `feature/auth` becomes `feature-auth`)
|
|
130
|
+
- **Collision detection** -- Warns if sanitized names would conflict
|
|
131
|
+
- **AI agent documentation** -- Creates an `AGENTS.md` at the hub root for AI coding agents (never overwrites existing files)
|
|
132
|
+
- **Zero runtime dependencies** -- Only requires Node.js and git
|
|
121
133
|
|
|
122
|
-
##
|
|
134
|
+
## Why this layout?
|
|
123
135
|
|
|
124
|
-
|
|
136
|
+
Every branch as a sibling directory means you can work on multiple branches simultaneously. Run branch-specific builds side by side. No stashing, no context switching.
|
|
125
137
|
|
|
126
|
-
|
|
138
|
+
The `.git` file at the hub root is the key detail. Because it points to `.bare/`, git resolves the repository from any location in the tree:
|
|
127
139
|
|
|
128
140
|
```sh
|
|
129
|
-
|
|
141
|
+
# All of these work -- no -C flag or cd needed:
|
|
142
|
+
~/projects/myrepo $ git worktree list
|
|
143
|
+
~/projects/myrepo/main $ git worktree list
|
|
144
|
+
~/projects/myrepo/feature-x $ git worktree list
|
|
145
|
+
~/projects/myrepo/main/src/deep/nested $ git worktree list
|
|
130
146
|
```
|
|
131
147
|
|
|
132
|
-
|
|
148
|
+
IDEs, linters, pre-commit hooks, and AI coding agents all discover the repo automatically regardless of which worktree directory they're opened in.
|
|
149
|
+
|
|
150
|
+
## Requirements
|
|
133
151
|
|
|
134
|
-
|
|
152
|
+
- Node.js 18+
|
|
153
|
+
- Git 2.5+ (for worktree support)
|
|
135
154
|
|
|
136
|
-
|
|
155
|
+
## Development
|
|
137
156
|
|
|
138
157
|
```sh
|
|
139
|
-
git-worktree-organize
|
|
158
|
+
git clone https://github.com/drmikecrowe/git-worktree-organize.git
|
|
159
|
+
cd git-worktree-organize
|
|
160
|
+
npm install
|
|
161
|
+
|
|
162
|
+
npm test # Run tests
|
|
163
|
+
npm run build # Build
|
|
164
|
+
node dist/cli.js /path/to/test/repo # Test locally
|
|
140
165
|
```
|
|
141
166
|
|
|
142
|
-
|
|
167
|
+
## Layout conversions
|
|
143
168
|
|
|
144
|
-
|
|
169
|
+
Detailed before/after diagrams for each supported repository type.
|
|
145
170
|
|
|
146
|
-
|
|
171
|
+
### Standard repository
|
|
147
172
|
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
The most common case -- a normal repo with a `.git` directory and optionally some linked worktrees.
|
|
174
|
+
|
|
175
|
+
**Before:**
|
|
176
|
+
```
|
|
177
|
+
myrepo/
|
|
178
|
+
├── .git/ ← standard git directory
|
|
179
|
+
├── src/
|
|
180
|
+
└── package.json
|
|
181
|
+
|
|
182
|
+
myrepo-feature/ ← linked worktree (created by git worktree add)
|
|
183
|
+
├── .git ← file pointing back to myrepo/.git/worktrees/feature
|
|
184
|
+
├── src/
|
|
185
|
+
└── package.json
|
|
150
186
|
```
|
|
151
187
|
|
|
152
|
-
|
|
188
|
+
**After:**
|
|
189
|
+
```
|
|
190
|
+
myrepo/
|
|
191
|
+
├── .bare/ ← bare git repo (contents of old .git/)
|
|
192
|
+
├── .git ← file: "gitdir: ./.bare"
|
|
193
|
+
├── AGENTS.md
|
|
194
|
+
├── main/ ← main branch worktree (was myrepo/)
|
|
195
|
+
│ ├── src/
|
|
196
|
+
│ └── package.json
|
|
197
|
+
└── feature/ ← feature worktree (was myrepo-feature/)
|
|
198
|
+
├── .git ← file pointing to .bare/worktrees/feature
|
|
199
|
+
├── src/
|
|
200
|
+
└── package.json
|
|
201
|
+
```
|
|
153
202
|
|
|
154
|
-
|
|
203
|
+
### Bare root
|
|
155
204
|
|
|
156
|
-
|
|
205
|
+
A bare repository where git internals sit directly at the root. Often created by `git clone --bare` or `git init --bare`.
|
|
157
206
|
|
|
158
|
-
|
|
207
|
+
**Before:**
|
|
208
|
+
```
|
|
209
|
+
myrepo.git/
|
|
210
|
+
├── HEAD
|
|
211
|
+
├── config
|
|
212
|
+
├── objects/
|
|
213
|
+
└── refs/
|
|
214
|
+
```
|
|
159
215
|
|
|
160
|
-
|
|
161
|
-
|
|
216
|
+
**After:**
|
|
217
|
+
```
|
|
218
|
+
myrepo-hub/
|
|
219
|
+
├── .bare/ ← git internals moved here
|
|
220
|
+
│ ├── HEAD
|
|
221
|
+
│ ├── config
|
|
222
|
+
│ ├── objects/
|
|
223
|
+
│ └── refs/
|
|
224
|
+
├── .git ← file: "gitdir: ./.bare"
|
|
225
|
+
└── AGENTS.md
|
|
226
|
+
```
|
|
162
227
|
|
|
163
|
-
|
|
228
|
+
No worktrees are created since a bare repo has no checked-out branches. Use `git worktree add <branch>` from the hub to start working.
|
|
164
229
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
git
|
|
168
|
-
|
|
169
|
-
|
|
230
|
+
### Bare dotgit
|
|
231
|
+
|
|
232
|
+
A repository where `.git` is a directory but `core.bare = true`.
|
|
233
|
+
|
|
234
|
+
**Before:**
|
|
235
|
+
```
|
|
236
|
+
myrepo/
|
|
237
|
+
└── .git/ ← directory, but core.bare = true
|
|
238
|
+
├── HEAD
|
|
239
|
+
├── config ← contains core.bare = true
|
|
240
|
+
├── objects/
|
|
241
|
+
└── refs/
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**After:**
|
|
245
|
+
```
|
|
246
|
+
myrepo-hub/
|
|
247
|
+
├── .bare/ ← contents of old .git/
|
|
248
|
+
├── .git ← file: "gitdir: ./.bare"
|
|
249
|
+
└── AGENTS.md
|
|
250
|
+
```
|
|
170
251
|
|
|
171
|
-
|
|
172
|
-
npm test
|
|
252
|
+
### Bare external
|
|
173
253
|
|
|
174
|
-
|
|
175
|
-
npm run build
|
|
254
|
+
A repository where `.git` is a file pointing to a gitdir stored elsewhere on the filesystem.
|
|
176
255
|
|
|
177
|
-
|
|
178
|
-
|
|
256
|
+
**Before:**
|
|
257
|
+
```
|
|
258
|
+
myrepo/
|
|
259
|
+
├── .git ← file: "gitdir: /somewhere/else/myrepo.git"
|
|
260
|
+
├── src/
|
|
261
|
+
└── package.json
|
|
262
|
+
|
|
263
|
+
/somewhere/else/myrepo.git/
|
|
264
|
+
├── HEAD
|
|
265
|
+
├── objects/
|
|
266
|
+
└── refs/
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**After:**
|
|
270
|
+
```
|
|
271
|
+
myrepo-hub/
|
|
272
|
+
├── .bare/ ← copy of /somewhere/else/myrepo.git/
|
|
273
|
+
├── .git ← file: "gitdir: ./.bare"
|
|
274
|
+
└── AGENTS.md
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Bare hub (re-organize)
|
|
278
|
+
|
|
279
|
+
Already in the bare-hub layout but with worktrees scattered in non-standard locations.
|
|
280
|
+
|
|
281
|
+
**Before:**
|
|
282
|
+
```
|
|
283
|
+
myrepo/
|
|
284
|
+
├── .bare/
|
|
285
|
+
├── .git ← file: "gitdir: ./.bare"
|
|
286
|
+
└── main/
|
|
287
|
+
|
|
288
|
+
/other/path/feature/ ← worktree outside the hub
|
|
289
|
+
└── .git
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**After:**
|
|
293
|
+
```
|
|
294
|
+
myrepo/
|
|
295
|
+
├── .bare/
|
|
296
|
+
├── .git ← file: "gitdir: ./.bare"
|
|
297
|
+
├── AGENTS.md
|
|
298
|
+
├── main/
|
|
299
|
+
└── feature/ ← moved into the hub
|
|
300
|
+
└── .git ← repaired to point to .bare/worktrees/feature
|
|
179
301
|
```
|
|
180
302
|
|
|
181
303
|
## License
|
|
182
304
|
|
|
183
|
-
MIT
|
|
305
|
+
MIT -- see [github.com/drmikecrowe/git-worktree-organize](https://github.com/drmikecrowe/git-worktree-organize).
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { resolve as resolve3, join as join5, dirname as dirname3, basename as basename3 } from "node:path";
|
|
5
|
-
import { existsSync as existsSync5, statSync as statSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
5
|
+
import { existsSync as existsSync5, statSync as statSync5, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
|
|
6
6
|
|
|
7
7
|
// src/run.ts
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
@@ -144,6 +144,103 @@ function samefs(a, b) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// src/migrate.ts
|
|
147
|
+
var AGENTS_MD_TEMPLATE = `# Git Worktree Layout
|
|
148
|
+
|
|
149
|
+
This repository uses **git worktrees** with a bare repository pattern for parallel development across multiple branches.
|
|
150
|
+
|
|
151
|
+
## Directory Structure
|
|
152
|
+
|
|
153
|
+
\`\`\`
|
|
154
|
+
<project>/ # Root project directory
|
|
155
|
+
\u251C\u2500\u2500 .bare/ # Bare git repository (shared git data)
|
|
156
|
+
\u2502 \u251C\u2500\u2500 worktrees/ # Worktree metadata
|
|
157
|
+
\u2502 \u2502 \u251C\u2500\u2500 main/ # Main branch metadata
|
|
158
|
+
\u2502 \u2502 \u2514\u2500\u2500 <branch-name>/ # Per-branch worktree metadata
|
|
159
|
+
\u2502 \u251C\u2500\u2500 objects/ # Git objects (shared)
|
|
160
|
+
\u2502 \u251C\u2500\u2500 refs/ # Git refs (shared)
|
|
161
|
+
\u2502 \u2514\u2500\u2500 config # Repository config
|
|
162
|
+
\u251C\u2500\u2500 .git # Points to .bare (gitdir: ./.bare)
|
|
163
|
+
\u251C\u2500\u2500 main/ # Main branch worktree (primary)
|
|
164
|
+
\u251C\u2500\u2500 <branch-name>/ # Feature/fix branch worktrees
|
|
165
|
+
\u2514\u2500\u2500 *.code-workspace # VS Code multi-root workspace
|
|
166
|
+
\`\`\`
|
|
167
|
+
|
|
168
|
+
## How It Works
|
|
169
|
+
|
|
170
|
+
- **Bare Repository**: \`.bare/\` contains all git data (objects, refs, config)
|
|
171
|
+
- **Worktrees**: Each branch checkout is a separate directory at the root level
|
|
172
|
+
- **Shared History**: All worktrees share the same git history from \`.bare/\`
|
|
173
|
+
|
|
174
|
+
## Working with Worktrees
|
|
175
|
+
|
|
176
|
+
### Create a new worktree
|
|
177
|
+
|
|
178
|
+
\`\`\`bash
|
|
179
|
+
# From any worktree or the root
|
|
180
|
+
git worktree add <branch-name>
|
|
181
|
+
|
|
182
|
+
# Create new branch and worktree
|
|
183
|
+
git worktree add -b <new-branch> <directory-name>
|
|
184
|
+
\`\`\`
|
|
185
|
+
|
|
186
|
+
### List worktrees
|
|
187
|
+
|
|
188
|
+
\`\`\`bash
|
|
189
|
+
git worktree list
|
|
190
|
+
\`\`\`
|
|
191
|
+
|
|
192
|
+
### Remove a worktree
|
|
193
|
+
|
|
194
|
+
\`\`\`bash
|
|
195
|
+
# After merging/deleting the branch
|
|
196
|
+
git worktree remove <branch-name>
|
|
197
|
+
|
|
198
|
+
# Force removal (if untracked files exist)
|
|
199
|
+
git worktree remove --force <branch-name>
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
### Prune stale worktree references
|
|
203
|
+
|
|
204
|
+
\`\`\`bash
|
|
205
|
+
git worktree prune
|
|
206
|
+
\`\`\`
|
|
207
|
+
|
|
208
|
+
## Conventions
|
|
209
|
+
|
|
210
|
+
1. **Naming**: Worktree directories match the branch name (e.g., \`feature-auth\`, \`fix-login-bug\`)
|
|
211
|
+
2. **Main worktree**: \`main/\` is the primary worktree for the main branch
|
|
212
|
+
3. **Workspace file**: Open \`*.code-workspace\` in VS Code to work with multiple worktrees
|
|
213
|
+
|
|
214
|
+
## Tips
|
|
215
|
+
|
|
216
|
+
- Each worktree has its own \`.git\` file pointing back to \`.bare/\`
|
|
217
|
+
- You can run different branches simultaneously without stashing
|
|
218
|
+
- IDEs can open multiple worktrees as separate folders in one workspace
|
|
219
|
+
- Run \`git worktree prune\` periodically to clean up deleted worktree references
|
|
220
|
+
`;
|
|
221
|
+
function writeAgentsMd(dest) {
|
|
222
|
+
const agentsPath = join2(dest, "AGENTS.md");
|
|
223
|
+
if (!existsSync3(agentsPath)) {
|
|
224
|
+
writeFileSync(agentsPath, AGENTS_MD_TEMPLATE);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function worktreeConfigEnabled(bareDir) {
|
|
228
|
+
const configFile = join2(bareDir, "config");
|
|
229
|
+
if (!existsSync3(configFile)) return false;
|
|
230
|
+
const content = readFileSync2(configFile, "utf8");
|
|
231
|
+
const extensionsMatch = content.match(/\[extensions\]([\s\S]*?)(?=\[|$)/i);
|
|
232
|
+
if (!extensionsMatch) return false;
|
|
233
|
+
return /worktreeconfig\s*=\s*true/i.test(extensionsMatch[1]);
|
|
234
|
+
}
|
|
235
|
+
var WORKTREE_CONFIG_CONTENT = "[core]\n bare = false\n";
|
|
236
|
+
function ensureWorktreeConfig(adminDir, bareDir, log) {
|
|
237
|
+
if (!worktreeConfigEnabled(bareDir)) return;
|
|
238
|
+
const configWtFile = join2(adminDir, "config.worktree");
|
|
239
|
+
if (!existsSync3(configWtFile) || readFileSync2(configWtFile, "utf8") !== WORKTREE_CONFIG_CONTENT) {
|
|
240
|
+
log?.(`Writing config.worktree for [${basename(adminDir)}]`);
|
|
241
|
+
writeFileSync(configWtFile, WORKTREE_CONFIG_CONTENT);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
147
244
|
function sanitizeBranch(branch) {
|
|
148
245
|
return branch.replace(/\//g, "-");
|
|
149
246
|
}
|
|
@@ -180,6 +277,13 @@ async function repairHub(dest, log = console.log) {
|
|
|
180
277
|
const worktreePath = dirname2(registeredGitFile);
|
|
181
278
|
if (!worktreePath.startsWith(dest + "/")) continue;
|
|
182
279
|
if (!existsSync3(registeredGitFile) || !statSync3(registeredGitFile).isFile()) continue;
|
|
280
|
+
const commondirFile = join2(adminDir, "commondir");
|
|
281
|
+
const expectedCommondir = "../../\n";
|
|
282
|
+
if (!existsSync3(commondirFile) || readFileSync2(commondirFile, "utf8") !== expectedCommondir) {
|
|
283
|
+
log(`Repairing commondir for [${basename(worktreePath)}]`);
|
|
284
|
+
writeFileSync(commondirFile, expectedCommondir);
|
|
285
|
+
}
|
|
286
|
+
ensureWorktreeConfig(adminDir, join2(dest, ".bare"), log);
|
|
183
287
|
const content = readFileSync2(registeredGitFile, "utf8");
|
|
184
288
|
const match = content.match(/^gitdir:\s*(.+)/m);
|
|
185
289
|
if (!match) continue;
|
|
@@ -266,6 +370,7 @@ async function migrateInPlace(source, log = console.log, warn) {
|
|
|
266
370
|
mkdirSync(mainAdminDir, { recursive: true });
|
|
267
371
|
writeFileSync(join2(mainAdminDir, "gitdir"), mainDest + "/.git\n");
|
|
268
372
|
writeFileSync(join2(mainAdminDir, "commondir"), "../../\n");
|
|
373
|
+
ensureWorktreeConfig(mainAdminDir, destBare);
|
|
269
374
|
const headToWrite = mainHeadContent.endsWith("\n") ? mainHeadContent : mainHeadContent + "\n";
|
|
270
375
|
writeFileSync(join2(mainAdminDir, "HEAD"), headToWrite);
|
|
271
376
|
const bareIndex = join2(destBare, "index");
|
|
@@ -279,6 +384,7 @@ async function migrateInPlace(source, log = console.log, warn) {
|
|
|
279
384
|
await processLinkedWorktree(worktrees[i], resolvedSource, destBare, log, warn);
|
|
280
385
|
}
|
|
281
386
|
log(`Original repo backed up at: ${oldPath}`);
|
|
387
|
+
writeAgentsMd(resolvedSource);
|
|
282
388
|
return resolvedSource;
|
|
283
389
|
}
|
|
284
390
|
function bold(s) {
|
|
@@ -322,6 +428,7 @@ async function migrate(config, options, log, warn) {
|
|
|
322
428
|
mkdirSync(mainAdminDir, { recursive: true });
|
|
323
429
|
writeFileSync(join2(mainAdminDir, "gitdir"), mainDest + "/.git\n");
|
|
324
430
|
writeFileSync(join2(mainAdminDir, "commondir"), "../../\n");
|
|
431
|
+
ensureWorktreeConfig(mainAdminDir, destBare);
|
|
325
432
|
const headToWrite = mainHeadContent.endsWith("\n") ? mainHeadContent : mainHeadContent + "\n";
|
|
326
433
|
writeFileSync(join2(mainAdminDir, "HEAD"), headToWrite);
|
|
327
434
|
const bareIndex = join2(destBare, "index");
|
|
@@ -338,6 +445,7 @@ async function migrate(config, options, log, warn) {
|
|
|
338
445
|
await processLinkedWorktree(wt, dest, destBare, log, warn);
|
|
339
446
|
}
|
|
340
447
|
}
|
|
448
|
+
writeAgentsMd(dest);
|
|
341
449
|
return dest;
|
|
342
450
|
}
|
|
343
451
|
async function processLinkedWorktree(wt, dest, destBare, log, warn) {
|
|
@@ -359,6 +467,8 @@ async function processLinkedWorktree(wt, dest, destBare, log, warn) {
|
|
|
359
467
|
`);
|
|
360
468
|
if (existsSync3(newAdmin)) {
|
|
361
469
|
writeFileSync(join2(newAdmin, "gitdir"), wtDest + "/.git\n");
|
|
470
|
+
writeFileSync(join2(newAdmin, "commondir"), "../../\n");
|
|
471
|
+
ensureWorktreeConfig(newAdmin, destBare);
|
|
362
472
|
} else {
|
|
363
473
|
warn?.(`Admin dir ${newAdmin} does not exist for worktree ${wtDest}`);
|
|
364
474
|
}
|
|
@@ -481,12 +591,78 @@ function isGitPointerValid(worktreePath, hubPath) {
|
|
|
481
591
|
if (!match) {
|
|
482
592
|
return false;
|
|
483
593
|
}
|
|
484
|
-
const
|
|
594
|
+
const adminDir = match[1].trim();
|
|
485
595
|
const bareDir = join5(hubPath, ".bare");
|
|
486
|
-
|
|
596
|
+
if (!adminDir.includes(bareDir) || !adminDir.includes("/worktrees/")) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
if (!existsSync5(adminDir)) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
const adminGitdirFile = join5(adminDir, "gitdir");
|
|
603
|
+
if (!existsSync5(adminGitdirFile)) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
const adminGitdir = readFileSync5(adminGitdirFile, "utf8").trim();
|
|
607
|
+
if (adminGitdir !== gitFile) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
const commondirFile = join5(adminDir, "commondir");
|
|
611
|
+
if (!existsSync5(commondirFile) || readFileSync5(commondirFile, "utf8").trim() !== "../..") {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
const sharedConfig = join5(hubPath, ".bare", "config");
|
|
615
|
+
if (existsSync5(sharedConfig)) {
|
|
616
|
+
const cfg = readFileSync5(sharedConfig, "utf8");
|
|
617
|
+
const extMatch = cfg.match(/\[extensions\]([\s\S]*?)(?=\[|$)/i);
|
|
618
|
+
if (extMatch && /worktreeconfig\s*=\s*true/i.test(extMatch[1])) {
|
|
619
|
+
const configWt = join5(adminDir, "config.worktree");
|
|
620
|
+
if (!existsSync5(configWt) || !readFileSync5(configWt, "utf8").includes("bare = false")) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
run("git", ["-C", worktreePath, "rev-parse", "HEAD"]);
|
|
627
|
+
return true;
|
|
628
|
+
} catch {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function listWorktreesFromAdminDirs(hubPath) {
|
|
633
|
+
const adminBase = join5(hubPath, ".bare", "worktrees");
|
|
634
|
+
if (!existsSync5(adminBase)) return [];
|
|
635
|
+
const worktrees = [];
|
|
636
|
+
for (const adminName of readdirSync3(adminBase)) {
|
|
637
|
+
const adminDir = join5(adminBase, adminName);
|
|
638
|
+
if (!statSync5(adminDir).isDirectory()) continue;
|
|
639
|
+
const gitdirFile = join5(adminDir, "gitdir");
|
|
640
|
+
if (!existsSync5(gitdirFile)) continue;
|
|
641
|
+
const wtGitFile = readFileSync5(gitdirFile, "utf8").trim();
|
|
642
|
+
const wtPath = dirname3(wtGitFile);
|
|
643
|
+
const headFile = join5(adminDir, "HEAD");
|
|
644
|
+
let branch = null;
|
|
645
|
+
let head = "";
|
|
646
|
+
if (existsSync5(headFile)) {
|
|
647
|
+
const headContent = readFileSync5(headFile, "utf8").trim();
|
|
648
|
+
if (headContent.startsWith("ref: refs/heads/")) {
|
|
649
|
+
branch = headContent.slice("ref: refs/heads/".length);
|
|
650
|
+
} else {
|
|
651
|
+
head = headContent;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
worktrees.push({ path: wtPath, head, branch, isBare: false });
|
|
655
|
+
}
|
|
656
|
+
return worktrees;
|
|
487
657
|
}
|
|
488
658
|
async function runValidationMode(hubPath) {
|
|
489
|
-
|
|
659
|
+
let worktrees;
|
|
660
|
+
try {
|
|
661
|
+
worktrees = await listWorktrees(hubPath);
|
|
662
|
+
} catch {
|
|
663
|
+
console.log(`${yellow("warn:")} git worktree list failed; scanning admin dirs to enumerate worktrees`);
|
|
664
|
+
worktrees = listWorktreesFromAdminDirs(hubPath);
|
|
665
|
+
}
|
|
490
666
|
const validated = [];
|
|
491
667
|
for (const wt of worktrees) {
|
|
492
668
|
if (wt.isBare) continue;
|
|
@@ -539,12 +715,18 @@ async function runValidationMode(hubPath) {
|
|
|
539
715
|
if (counts.missing > 0) summaryParts.push(`${counts.missing} missing`);
|
|
540
716
|
if (counts.stale > 0) summaryParts.push(`${counts.stale} stale`);
|
|
541
717
|
console.log(`Summary: ${summaryParts.join(", ")}`);
|
|
542
|
-
const
|
|
718
|
+
const staleWorktrees = validated.filter((v) => v.status === "stale");
|
|
719
|
+
if (staleWorktrees.length > 0) {
|
|
720
|
+
console.log();
|
|
721
|
+
console.log(`${green("==>")} Auto-repairing ${staleWorktrees.length} stale worktree(s)...`);
|
|
722
|
+
await repairHub(hubPath, (msg) => console.log(` ${msg}`));
|
|
723
|
+
}
|
|
724
|
+
const needsRepair = validated.filter((v) => v.status === "missing");
|
|
543
725
|
if (needsRepair.length === 0) {
|
|
544
726
|
return;
|
|
545
727
|
}
|
|
546
728
|
console.log();
|
|
547
|
-
console.log(`${yellow("warn:")} ${needsRepair.length} worktree(s) need repair.`);
|
|
729
|
+
console.log(`${yellow("warn:")} ${needsRepair.length} missing worktree(s) need repair.`);
|
|
548
730
|
const searchDirs = [dirname3(hubPath)];
|
|
549
731
|
console.log(`${green("==>")} Searching for missing worktrees...`);
|
|
550
732
|
const results = await findMissingWorktrees(
|