@shivanshshrivas/gwit 0.1.3 → 0.2.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 +82 -2
- package/dist/index.cjs +880 -61
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -26,9 +26,16 @@ gwit -b fix/login-page # create a new branch from HEAD
|
|
|
26
26
|
|
|
27
27
|
# Day-to-day
|
|
28
28
|
gwit list # show all active worktrees
|
|
29
|
+
gwit status # see ahead/behind, dirty state, PR info
|
|
29
30
|
gwit sync feature/auth # re-copy files after .env changes
|
|
31
|
+
gwit sync --back feature/auth # three-way merge .gwitinclude files back to main
|
|
30
32
|
gwit open feature/auth # re-open editor for an existing worktree
|
|
33
|
+
|
|
34
|
+
# Merge and clean up
|
|
35
|
+
gwit merge feature/auth # merge branch back into main
|
|
31
36
|
gwit remove feature/auth # run cleanup hooks and remove worktree
|
|
37
|
+
gwit sweep # bulk-remove all merged worktrees
|
|
38
|
+
gwit rename old/name new/name # rename a worktree branch
|
|
32
39
|
```
|
|
33
40
|
|
|
34
41
|
## How it works
|
|
@@ -102,13 +109,26 @@ gwit remove feature/auth --force # skip uncommitted-changes check
|
|
|
102
109
|
|
|
103
110
|
### `gwit sync [branch]`
|
|
104
111
|
|
|
105
|
-
|
|
112
|
+
By default, re-copy `.gwitinclude` files from main into an existing worktree.
|
|
113
|
+
With `--back`, sync in the reverse direction using snapshot-aware three-way merge.
|
|
106
114
|
|
|
107
115
|
```sh
|
|
108
116
|
gwit sync feature/auth # sync a specific branch
|
|
109
117
|
gwit sync # auto-detect from current directory (when inside a worktree)
|
|
118
|
+
gwit sync --back feature/auth # three-way merge worktree -> main
|
|
119
|
+
gwit sync --back # auto-detect branch, then merge back
|
|
110
120
|
```
|
|
111
121
|
|
|
122
|
+
`--back` compares three versions of each snapshot-tracked file:
|
|
123
|
+
|
|
124
|
+
1. **base** - file content captured when the worktree was created
|
|
125
|
+
2. **main** - current file in the main worktree
|
|
126
|
+
3. **worktree** - current file in the linked worktree
|
|
127
|
+
|
|
128
|
+
If both sides changed different text regions, gwit applies a clean merge.
|
|
129
|
+
If both sides changed the same region, gwit writes git-style conflict markers
|
|
130
|
+
(`<<<<<<<`, `=======`, `>>>>>>>`). Binary conflicts are skipped with a warning.
|
|
131
|
+
|
|
112
132
|
### `gwit open <branch>`
|
|
113
133
|
|
|
114
134
|
Re-open the editor for an existing worktree. Useful when the editor window was closed.
|
|
@@ -118,6 +138,62 @@ gwit open feature/auth
|
|
|
118
138
|
gwit open feature/auth --editor cursor
|
|
119
139
|
```
|
|
120
140
|
|
|
141
|
+
### `gwit merge <branch>`
|
|
142
|
+
|
|
143
|
+
Merge a worktree branch back into the target branch. Combines git-tracked changes (via real git merge) with gitignored files (via reverse `.gwitinclude` sync) in a single command.
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
gwit merge feature/auth # standard merge into default branch
|
|
147
|
+
gwit merge feature/auth --into dev # merge into a specific branch
|
|
148
|
+
gwit merge feature/auth --squash # squash all commits into one
|
|
149
|
+
gwit merge feature/auth --rebase # rebase onto target, then fast-forward
|
|
150
|
+
gwit merge feature/auth --no-ff # force a merge commit
|
|
151
|
+
gwit merge feature/auth --cleanup # remove worktree after successful merge
|
|
152
|
+
gwit merge feature/auth --no-sync-back # skip reverse .gwitinclude copy
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
What it does, in order:
|
|
156
|
+
|
|
157
|
+
1. Validates the worktree exists in the registry
|
|
158
|
+
2. Resolves the target branch (default: repo's default branch)
|
|
159
|
+
3. Three-way syncs `.gwitinclude` files back to main (falls back to direct copy if no snapshot; unless `--no-sync-back`)
|
|
160
|
+
4. Checks out the target branch in the main worktree
|
|
161
|
+
5. Merges the feature branch using the chosen strategy
|
|
162
|
+
6. Optionally removes the worktree (`--cleanup`)
|
|
163
|
+
|
|
164
|
+
If the merge fails with conflicts, gwit exits with a helpful message so you can resolve manually.
|
|
165
|
+
|
|
166
|
+
### `gwit status`
|
|
167
|
+
|
|
168
|
+
Show detailed status of all active gwit worktrees.
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
gwit status # rich table output
|
|
172
|
+
gwit status --json # machine-readable JSON
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Displays per-worktree: branch, path, port, ahead/behind counts, dirty-file count, and PR info (if [`gh` CLI](https://cli.github.com) is installed).
|
|
176
|
+
|
|
177
|
+
### `gwit sweep`
|
|
178
|
+
|
|
179
|
+
Bulk-remove worktrees whose branches are fully merged into the default branch or whose GitHub PRs are merged/closed.
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
gwit sweep # interactive confirmation
|
|
183
|
+
gwit sweep --dry-run # show what would be removed
|
|
184
|
+
gwit sweep --force # skip confirmation
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Runs cleanup hooks for each removed worktree, just like `gwit remove`.
|
|
188
|
+
|
|
189
|
+
### `gwit rename <old-branch> <new-branch>`
|
|
190
|
+
|
|
191
|
+
Rename a worktree's git branch, move the directory (if the slug changes), and update the registry atomically.
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
gwit rename feature/auth feature/authentication
|
|
195
|
+
```
|
|
196
|
+
|
|
121
197
|
### `gwit config`
|
|
122
198
|
|
|
123
199
|
Show or update global configuration stored in `~/.gwitrc`.
|
|
@@ -145,15 +221,19 @@ gwit config set basePort 4000 # starting port for auto-assignment
|
|
|
145
221
|
|
|
146
222
|
### `.gwitinclude`
|
|
147
223
|
|
|
148
|
-
List of gitignored files and directories to copy into every new worktree. One entry per line, comments with `#` ignored.
|
|
224
|
+
List of gitignored files and directories to copy into every new worktree. One entry per line, comments with `#` ignored. Supports glob patterns via [minimatch](https://github.com/isaacs/minimatch).
|
|
149
225
|
|
|
150
226
|
```
|
|
151
227
|
# .gwitinclude - files to copy into each new worktree
|
|
152
228
|
.env
|
|
153
229
|
.env.local
|
|
154
230
|
certs/
|
|
231
|
+
*.pem
|
|
232
|
+
secrets/**
|
|
155
233
|
```
|
|
156
234
|
|
|
235
|
+
Glob patterns (`*`, `?`, `[...]`, `**`) are expanded against the set of gitignored files in the repo. Literal entries are copied directly.
|
|
236
|
+
|
|
157
237
|
Only gitignored files are eligible to copy. Tracked files are silently skipped - gwit is an allowlist for files that must be present but cannot be committed.
|
|
158
238
|
|
|
159
239
|
### `.gwitcommand`
|
package/dist/index.cjs
CHANGED
|
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
27
|
+
var fs13 = __toESM(require("fs"));
|
|
28
|
+
var path8 = __toESM(require("path"));
|
|
29
29
|
var import_commander = require("commander");
|
|
30
30
|
|
|
31
31
|
// src/types.ts
|
|
@@ -54,21 +54,21 @@ function colorize(text, code) {
|
|
|
54
54
|
return `${code}${text}${ANSI.reset}`;
|
|
55
55
|
}
|
|
56
56
|
var ui = {
|
|
57
|
-
|
|
57
|
+
// Prints a green success line prefixed with ✓.
|
|
58
58
|
success: (msg) => console.log(colorize(`\u2713 ${msg}`, ANSI.green)),
|
|
59
|
-
|
|
59
|
+
// Prints a red error line to stderr prefixed with ✗.
|
|
60
60
|
error: (msg) => console.error(colorize(`\u2717 ${msg}`, ANSI.red)),
|
|
61
|
-
|
|
61
|
+
// Prints a yellow warning line to stderr prefixed with ⚠.
|
|
62
62
|
warn: (msg) => console.warn(colorize(`\u26A0 ${msg}`, ANSI.yellow)),
|
|
63
|
-
|
|
63
|
+
// Prints a cyan informational line.
|
|
64
64
|
info: (msg) => console.log(colorize(` ${msg}`, ANSI.cyan)),
|
|
65
|
-
|
|
65
|
+
// Prints a cyan step line prefixed with →.
|
|
66
66
|
step: (msg) => console.log(colorize(`\u2192 ${msg}`, ANSI.cyan)),
|
|
67
|
-
|
|
67
|
+
// Prints a dimmed line (secondary information).
|
|
68
68
|
dim: (msg) => console.log(colorize(msg, ANSI.dim)),
|
|
69
|
-
|
|
69
|
+
// Returns bold-wrapped text (for embedding in other strings).
|
|
70
70
|
bold: (text) => colorize(text, ANSI.bold),
|
|
71
|
-
|
|
71
|
+
// Returns gray-wrapped text (for embedding in other strings).
|
|
72
72
|
gray: (text) => colorize(text, ANSI.gray)
|
|
73
73
|
};
|
|
74
74
|
function formatError(err) {
|
|
@@ -108,6 +108,12 @@ function getGwitDir() {
|
|
|
108
108
|
function getRegistryPath() {
|
|
109
109
|
return path.join(getGwitDir(), "worktrees.json");
|
|
110
110
|
}
|
|
111
|
+
function getSnapshotsDir() {
|
|
112
|
+
return path.join(getGwitDir(), "snapshots");
|
|
113
|
+
}
|
|
114
|
+
function getSnapshotDir(slug) {
|
|
115
|
+
return path.join(getSnapshotsDir(), slug);
|
|
116
|
+
}
|
|
111
117
|
function getConfigPath() {
|
|
112
118
|
return path.join(os.homedir(), ".gwitrc");
|
|
113
119
|
}
|
|
@@ -154,6 +160,18 @@ function runArgsSafe(cmd, args, options) {
|
|
|
154
160
|
const stdout = (result.stdout ?? "").trim();
|
|
155
161
|
return { stdout, success: result.status === 0 };
|
|
156
162
|
}
|
|
163
|
+
function runArgsWithExitCode(cmd, args, options) {
|
|
164
|
+
const result = (0, import_child_process.spawnSync)(cmd, args, {
|
|
165
|
+
encoding: "utf-8",
|
|
166
|
+
cwd: options?.cwd,
|
|
167
|
+
env: options?.env ? { ...process.env, ...options.env } : process.env,
|
|
168
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
stdout: (result.stdout ?? "").trim(),
|
|
172
|
+
exitCode: result.status ?? 1
|
|
173
|
+
};
|
|
174
|
+
}
|
|
157
175
|
function runSafe(command, options) {
|
|
158
176
|
try {
|
|
159
177
|
const stdout = (0, import_child_process.execSync)(command, {
|
|
@@ -173,11 +191,11 @@ function _parseWorktreePorcelain(output) {
|
|
|
173
191
|
const blocks = output.trim().split(/\n\n+/);
|
|
174
192
|
return blocks.filter(Boolean).map((block) => {
|
|
175
193
|
const lines = block.split("\n");
|
|
176
|
-
const
|
|
194
|
+
const path9 = lines.find((l) => l.startsWith("worktree "))?.slice(9) ?? "";
|
|
177
195
|
const head = lines.find((l) => l.startsWith("HEAD "))?.slice(5) ?? "";
|
|
178
196
|
const branchLine = lines.find((l) => l.startsWith("branch "));
|
|
179
197
|
const branch = branchLine ? branchLine.slice(7).replace("refs/heads/", "") : null;
|
|
180
|
-
return { path:
|
|
198
|
+
return { path: path9, head, branch };
|
|
181
199
|
}).filter((w) => w.path.length > 0);
|
|
182
200
|
}
|
|
183
201
|
function isGitRepo() {
|
|
@@ -226,6 +244,56 @@ function hasUncommittedChanges(worktreePath) {
|
|
|
226
244
|
const result = runArgsSafe("git", ["-C", worktreePath, "status", "--porcelain"]);
|
|
227
245
|
return result.success && result.stdout.length > 0;
|
|
228
246
|
}
|
|
247
|
+
function isBranchMerged(branch, into) {
|
|
248
|
+
const result = runArgsSafe("git", ["branch", "--merged", into]);
|
|
249
|
+
if (!result.success) return false;
|
|
250
|
+
return result.stdout.split("\n").some((line) => line.trim().replace(/^\*\s*/, "") === branch);
|
|
251
|
+
}
|
|
252
|
+
function getDefaultBranch() {
|
|
253
|
+
const symref = runArgsSafe("git", ["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
254
|
+
if (symref.success && symref.stdout.length > 0) {
|
|
255
|
+
return symref.stdout.replace("refs/remotes/origin/", "");
|
|
256
|
+
}
|
|
257
|
+
if (branchExistsLocal("main")) return "main";
|
|
258
|
+
if (branchExistsLocal("master")) return "master";
|
|
259
|
+
throw new GwitError(
|
|
260
|
+
"Could not determine the default branch.",
|
|
261
|
+
"Specify the target explicitly: gwit merge <branch> --into main"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
function getAheadBehind(branch, base, cwd) {
|
|
265
|
+
const args = cwd ? ["-C", cwd, "rev-list", "--left-right", "--count", `${base}...${branch}`] : ["rev-list", "--left-right", "--count", `${base}...${branch}`];
|
|
266
|
+
const result = runArgsSafe("git", args);
|
|
267
|
+
if (!result.success) return { ahead: 0, behind: 0 };
|
|
268
|
+
const parts = result.stdout.split(" ");
|
|
269
|
+
return {
|
|
270
|
+
behind: parseInt(parts[0] ?? "0", 10) || 0,
|
|
271
|
+
ahead: parseInt(parts[1] ?? "0", 10) || 0
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function mergeBranch(cwd, branch, noFf = false) {
|
|
275
|
+
const args = ["-C", cwd, "merge", branch];
|
|
276
|
+
if (noFf) args.splice(3, 0, "--no-ff");
|
|
277
|
+
runArgsInherited("git", args);
|
|
278
|
+
}
|
|
279
|
+
function squashMergeBranch(cwd, branch) {
|
|
280
|
+
runArgsInherited("git", ["-C", cwd, "merge", "--squash", branch]);
|
|
281
|
+
}
|
|
282
|
+
function rebaseBranch(cwd, onto) {
|
|
283
|
+
runArgsInherited("git", ["-C", cwd, "rebase", onto]);
|
|
284
|
+
}
|
|
285
|
+
function ffMergeBranch(cwd, branch) {
|
|
286
|
+
runArgsInherited("git", ["-C", cwd, "merge", "--ff-only", branch]);
|
|
287
|
+
}
|
|
288
|
+
function commitMerge(cwd, message) {
|
|
289
|
+
runArgsInherited("git", ["-C", cwd, "commit", "-m", message]);
|
|
290
|
+
}
|
|
291
|
+
function renameBranch(oldName, newName) {
|
|
292
|
+
runArgsInherited("git", ["branch", "-m", oldName, newName]);
|
|
293
|
+
}
|
|
294
|
+
function moveWorktree(oldPath, newPath) {
|
|
295
|
+
runArgsInherited("git", ["worktree", "move", oldPath, newPath]);
|
|
296
|
+
}
|
|
229
297
|
|
|
230
298
|
// src/core/config.ts
|
|
231
299
|
var fs = __toESM(require("fs"));
|
|
@@ -336,7 +404,9 @@ async function ensureConfig() {
|
|
|
336
404
|
// src/core/files.ts
|
|
337
405
|
var fs2 = __toESM(require("fs"));
|
|
338
406
|
var path2 = __toESM(require("path"));
|
|
407
|
+
var import_minimatch = require("minimatch");
|
|
339
408
|
var GWITINCLUDE_FILE = ".gwitinclude";
|
|
409
|
+
var GLOB_CHARS = /[*?[]/;
|
|
340
410
|
function _parseLines(content) {
|
|
341
411
|
return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
342
412
|
}
|
|
@@ -356,14 +426,33 @@ function copyEntry(src, dest) {
|
|
|
356
426
|
fs2.copyFileSync(src, dest);
|
|
357
427
|
}
|
|
358
428
|
}
|
|
429
|
+
function isGlobPattern(entry) {
|
|
430
|
+
return GLOB_CHARS.test(entry);
|
|
431
|
+
}
|
|
432
|
+
function expandGlob(pattern, cwd) {
|
|
433
|
+
const result = runArgsSafe("git", ["ls-files", "--others", "--ignored", "--exclude-standard"], {
|
|
434
|
+
cwd
|
|
435
|
+
});
|
|
436
|
+
if (!result.success) return [];
|
|
437
|
+
const files = result.stdout.split("\n").filter((f) => f.length > 0);
|
|
438
|
+
return files.filter((f) => (0, import_minimatch.minimatch)(f, pattern, { dot: true }));
|
|
439
|
+
}
|
|
359
440
|
function parseGwitInclude(mainPath) {
|
|
360
441
|
const includePath = path2.join(mainPath, GWITINCLUDE_FILE);
|
|
361
442
|
if (!fs2.existsSync(includePath)) return [];
|
|
362
443
|
return _parseLines(fs2.readFileSync(includePath, "utf-8"));
|
|
363
444
|
}
|
|
364
445
|
function copyIncludedFiles(mainPath, worktreePath) {
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
446
|
+
const rawEntries = parseGwitInclude(mainPath);
|
|
447
|
+
if (rawEntries.length === 0) return [];
|
|
448
|
+
const entries = [];
|
|
449
|
+
for (const entry of rawEntries) {
|
|
450
|
+
if (isGlobPattern(entry)) {
|
|
451
|
+
entries.push(...expandGlob(entry, mainPath));
|
|
452
|
+
} else {
|
|
453
|
+
entries.push(entry);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
367
456
|
const copied = [];
|
|
368
457
|
for (const entry of entries) {
|
|
369
458
|
const entryPath = entry.replace(/\/$/, "");
|
|
@@ -397,56 +486,247 @@ function copyIncludedFiles(mainPath, worktreePath) {
|
|
|
397
486
|
}
|
|
398
487
|
return copied;
|
|
399
488
|
}
|
|
489
|
+
function reverseCopyIncludedFiles(worktreePath, mainPath) {
|
|
490
|
+
const rawEntries = parseGwitInclude(mainPath);
|
|
491
|
+
if (rawEntries.length === 0) return [];
|
|
492
|
+
const entries = [];
|
|
493
|
+
for (const entry of rawEntries) {
|
|
494
|
+
if (isGlobPattern(entry)) {
|
|
495
|
+
entries.push(...expandGlob(entry, worktreePath));
|
|
496
|
+
} else {
|
|
497
|
+
entries.push(entry);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const copied = [];
|
|
501
|
+
for (const entry of entries) {
|
|
502
|
+
const entryPath = entry.replace(/\/$/, "");
|
|
503
|
+
if (path2.isAbsolute(entryPath)) {
|
|
504
|
+
ui.dim(` skip ${entry} (absolute path not allowed)`);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const worktreeResolved = path2.resolve(worktreePath);
|
|
508
|
+
const resolvedSrc = path2.resolve(worktreePath, entryPath);
|
|
509
|
+
const rel = path2.relative(worktreeResolved, resolvedSrc);
|
|
510
|
+
if (rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
511
|
+
ui.dim(` skip ${entry} (path escapes repo)`);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const src = resolvedSrc;
|
|
515
|
+
const dest = path2.resolve(mainPath, entryPath);
|
|
516
|
+
if (!fs2.existsSync(src)) {
|
|
517
|
+
ui.dim(` skip ${entry} (not found in worktree)`);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (!isGitIgnored(entryPath, mainPath)) {
|
|
521
|
+
ui.dim(` skip ${entry} (not gitignored)`);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (isGitTracked(entryPath, mainPath)) {
|
|
525
|
+
ui.dim(` skip ${entry} (tracked by git)`);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
copyEntry(src, dest);
|
|
529
|
+
copied.push(entry);
|
|
530
|
+
}
|
|
531
|
+
return copied;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/core/snapshot.ts
|
|
535
|
+
var fs3 = __toESM(require("fs"));
|
|
536
|
+
var path3 = __toESM(require("path"));
|
|
537
|
+
var import_crypto = require("crypto");
|
|
538
|
+
var FILES_DIR_NAME = "files";
|
|
539
|
+
var MANIFEST_FILE_NAME = "manifest.json";
|
|
540
|
+
function normalizeRelPath(relPath) {
|
|
541
|
+
return relPath.replace(/\\/g, "/");
|
|
542
|
+
}
|
|
543
|
+
function tryChmod(filePath, mode) {
|
|
544
|
+
try {
|
|
545
|
+
fs3.chmodSync(filePath, mode);
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function ensureDir(dirPath) {
|
|
550
|
+
fs3.mkdirSync(dirPath, { recursive: true, mode: 448 });
|
|
551
|
+
tryChmod(dirPath, 448);
|
|
552
|
+
}
|
|
553
|
+
function getManifestPath(slug) {
|
|
554
|
+
return path3.join(getSnapshotDir(slug), MANIFEST_FILE_NAME);
|
|
555
|
+
}
|
|
556
|
+
function getSnapshotFilesDir(slug) {
|
|
557
|
+
return path3.join(getSnapshotDir(slug), FILES_DIR_NAME);
|
|
558
|
+
}
|
|
559
|
+
function collectEntryFiles(entryPath, mainPath) {
|
|
560
|
+
const resolvedMain = path3.resolve(mainPath);
|
|
561
|
+
const resolvedEntry = path3.resolve(entryPath);
|
|
562
|
+
const relToMain = path3.relative(resolvedMain, resolvedEntry);
|
|
563
|
+
if (relToMain.startsWith("..") || path3.isAbsolute(relToMain)) {
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
const stat = fs3.statSync(resolvedEntry);
|
|
567
|
+
if (stat.isFile()) {
|
|
568
|
+
return [normalizeRelPath(relToMain)];
|
|
569
|
+
}
|
|
570
|
+
const files = [];
|
|
571
|
+
const stack = [resolvedEntry];
|
|
572
|
+
while (stack.length > 0) {
|
|
573
|
+
const current = stack.pop();
|
|
574
|
+
if (!current) continue;
|
|
575
|
+
const entries = fs3.readdirSync(current, { withFileTypes: true });
|
|
576
|
+
for (const entry of entries) {
|
|
577
|
+
const childPath = path3.join(current, entry.name);
|
|
578
|
+
if (entry.isDirectory()) {
|
|
579
|
+
stack.push(childPath);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (entry.isFile()) {
|
|
583
|
+
const rel = path3.relative(resolvedMain, childPath);
|
|
584
|
+
if (!rel.startsWith("..") && !path3.isAbsolute(rel)) {
|
|
585
|
+
files.push(normalizeRelPath(rel));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return files;
|
|
591
|
+
}
|
|
592
|
+
function writeManifestAtomic(slug, manifest) {
|
|
593
|
+
const snapshotDir = getSnapshotDir(slug);
|
|
594
|
+
const manifestPath = getManifestPath(slug);
|
|
595
|
+
const tmpPath = path3.join(
|
|
596
|
+
snapshotDir,
|
|
597
|
+
`${MANIFEST_FILE_NAME}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`
|
|
598
|
+
);
|
|
599
|
+
fs3.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
600
|
+
tryChmod(tmpPath, 384);
|
|
601
|
+
fs3.renameSync(tmpPath, manifestPath);
|
|
602
|
+
}
|
|
603
|
+
function hashFile(filePath) {
|
|
604
|
+
const data = fs3.readFileSync(filePath);
|
|
605
|
+
return (0, import_crypto.createHash)("sha256").update(data).digest("hex");
|
|
606
|
+
}
|
|
607
|
+
function createSnapshot(slug, branch, mainPath, copiedFiles) {
|
|
608
|
+
const snapshotsDir = getSnapshotsDir();
|
|
609
|
+
const snapshotDir = getSnapshotDir(slug);
|
|
610
|
+
const filesDir = getSnapshotFilesDir(slug);
|
|
611
|
+
ensureDir(snapshotsDir);
|
|
612
|
+
if (fs3.existsSync(snapshotDir)) {
|
|
613
|
+
fs3.rmSync(snapshotDir, { recursive: true, force: true });
|
|
614
|
+
}
|
|
615
|
+
ensureDir(snapshotDir);
|
|
616
|
+
ensureDir(filesDir);
|
|
617
|
+
const relPaths = /* @__PURE__ */ new Set();
|
|
618
|
+
for (const entry of copiedFiles) {
|
|
619
|
+
const entryPath = entry.replace(/\/$/, "");
|
|
620
|
+
if (entryPath.length === 0) continue;
|
|
621
|
+
const sourcePath = path3.resolve(mainPath, entryPath);
|
|
622
|
+
if (!fs3.existsSync(sourcePath)) continue;
|
|
623
|
+
for (const relPath of collectEntryFiles(sourcePath, mainPath)) {
|
|
624
|
+
relPaths.add(relPath);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const manifest = {
|
|
628
|
+
branch,
|
|
629
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
630
|
+
files: {}
|
|
631
|
+
};
|
|
632
|
+
for (const relPath of [...relPaths].sort()) {
|
|
633
|
+
const sourcePath = path3.resolve(mainPath, relPath);
|
|
634
|
+
if (!fs3.existsSync(sourcePath)) continue;
|
|
635
|
+
const destPath = path3.join(filesDir, relPath);
|
|
636
|
+
ensureDir(path3.dirname(destPath));
|
|
637
|
+
fs3.copyFileSync(sourcePath, destPath);
|
|
638
|
+
tryChmod(destPath, 384);
|
|
639
|
+
const stat = fs3.statSync(sourcePath);
|
|
640
|
+
manifest.files[relPath] = {
|
|
641
|
+
hash: hashFile(sourcePath),
|
|
642
|
+
size: stat.size
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
writeManifestAtomic(slug, manifest);
|
|
646
|
+
}
|
|
647
|
+
function readSnapshot(slug) {
|
|
648
|
+
const manifestPath = getManifestPath(slug);
|
|
649
|
+
if (!fs3.existsSync(manifestPath)) return void 0;
|
|
650
|
+
try {
|
|
651
|
+
return JSON.parse(fs3.readFileSync(manifestPath, "utf-8"));
|
|
652
|
+
} catch {
|
|
653
|
+
return void 0;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function getSnapshotFilePath(slug, relPath) {
|
|
657
|
+
const filesDir = path3.resolve(getSnapshotFilesDir(slug));
|
|
658
|
+
const targetPath = path3.resolve(filesDir, relPath);
|
|
659
|
+
const relToRoot = path3.relative(filesDir, targetPath);
|
|
660
|
+
if (relToRoot.startsWith("..") || path3.isAbsolute(relToRoot)) {
|
|
661
|
+
return void 0;
|
|
662
|
+
}
|
|
663
|
+
return fs3.existsSync(targetPath) ? targetPath : void 0;
|
|
664
|
+
}
|
|
665
|
+
function deleteSnapshot(slug) {
|
|
666
|
+
const snapshotDir = getSnapshotDir(slug);
|
|
667
|
+
if (!fs3.existsSync(snapshotDir)) return;
|
|
668
|
+
fs3.rmSync(snapshotDir, { recursive: true, force: true });
|
|
669
|
+
}
|
|
670
|
+
function renameSnapshot(oldSlug, newSlug) {
|
|
671
|
+
const oldDir = getSnapshotDir(oldSlug);
|
|
672
|
+
const newDir = getSnapshotDir(newSlug);
|
|
673
|
+
if (!fs3.existsSync(oldDir)) return;
|
|
674
|
+
ensureDir(getSnapshotsDir());
|
|
675
|
+
if (fs3.existsSync(newDir)) {
|
|
676
|
+
fs3.rmSync(newDir, { recursive: true, force: true });
|
|
677
|
+
}
|
|
678
|
+
fs3.renameSync(oldDir, newDir);
|
|
679
|
+
}
|
|
400
680
|
|
|
401
681
|
// src/core/ports.ts
|
|
402
682
|
var net = __toESM(require("net"));
|
|
403
683
|
|
|
404
684
|
// src/core/registry.ts
|
|
405
|
-
var
|
|
406
|
-
var
|
|
685
|
+
var fs4 = __toESM(require("fs"));
|
|
686
|
+
var path4 = __toESM(require("path"));
|
|
407
687
|
var MAX_RETRIES = 3;
|
|
408
688
|
var RETRY_DELAY_MS = 100;
|
|
409
689
|
function sleep(ms) {
|
|
410
|
-
return new Promise((
|
|
690
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
411
691
|
}
|
|
412
692
|
async function writeRegistry(registry) {
|
|
413
693
|
const registryPath = getRegistryPath();
|
|
414
694
|
const gwitDir = getGwitDir();
|
|
415
|
-
if (!
|
|
416
|
-
|
|
695
|
+
if (!fs4.existsSync(gwitDir)) {
|
|
696
|
+
fs4.mkdirSync(gwitDir, { recursive: true, mode: 448 });
|
|
417
697
|
}
|
|
418
|
-
const tmpPath =
|
|
698
|
+
const tmpPath = path4.join(
|
|
419
699
|
gwitDir,
|
|
420
700
|
`worktrees.json.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`
|
|
421
701
|
);
|
|
422
|
-
|
|
702
|
+
fs4.writeFileSync(tmpPath, JSON.stringify(registry, null, 2), "utf-8");
|
|
423
703
|
try {
|
|
424
|
-
|
|
704
|
+
fs4.chmodSync(tmpPath, 384);
|
|
425
705
|
} catch {
|
|
426
706
|
}
|
|
427
707
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
428
708
|
try {
|
|
429
|
-
|
|
709
|
+
fs4.renameSync(tmpPath, registryPath);
|
|
430
710
|
return;
|
|
431
711
|
} catch {
|
|
432
712
|
if (attempt < MAX_RETRIES) await sleep(RETRY_DELAY_MS);
|
|
433
713
|
}
|
|
434
714
|
}
|
|
435
715
|
try {
|
|
436
|
-
|
|
716
|
+
fs4.unlinkSync(tmpPath);
|
|
437
717
|
} catch {
|
|
438
718
|
}
|
|
439
719
|
throw new GwitError("Failed to write worktree registry after multiple retries.");
|
|
440
720
|
}
|
|
441
721
|
function readRegistry() {
|
|
442
722
|
const registryPath = getRegistryPath();
|
|
443
|
-
if (!
|
|
723
|
+
if (!fs4.existsSync(registryPath)) return {};
|
|
444
724
|
try {
|
|
445
|
-
return JSON.parse(
|
|
725
|
+
return JSON.parse(fs4.readFileSync(registryPath, "utf-8"));
|
|
446
726
|
} catch {
|
|
447
727
|
const backupPath = `${registryPath}.bak`;
|
|
448
728
|
try {
|
|
449
|
-
|
|
729
|
+
fs4.copyFileSync(registryPath, backupPath);
|
|
450
730
|
} catch {
|
|
451
731
|
}
|
|
452
732
|
console.warn(`\u26A0 Corrupted registry backed up to ${backupPath}. Starting fresh.`);
|
|
@@ -487,10 +767,10 @@ function listWorktreeEntries(mainPath) {
|
|
|
487
767
|
// src/core/ports.ts
|
|
488
768
|
var MAX_PORT_SCAN = 100;
|
|
489
769
|
function _isPortFree(port) {
|
|
490
|
-
return new Promise((
|
|
770
|
+
return new Promise((resolve4) => {
|
|
491
771
|
const server = net.createServer();
|
|
492
|
-
server.once("error", () => server.close(() =>
|
|
493
|
-
server.once("listening", () => server.close(() =>
|
|
772
|
+
server.once("error", () => server.close(() => resolve4(false)));
|
|
773
|
+
server.once("listening", () => server.close(() => resolve4(true)));
|
|
494
774
|
server.listen(port, "127.0.0.1");
|
|
495
775
|
});
|
|
496
776
|
}
|
|
@@ -526,8 +806,8 @@ function buildEnvironment(branch, slug, port, worktreePath, mainPath, index) {
|
|
|
526
806
|
}
|
|
527
807
|
|
|
528
808
|
// src/core/hooks.ts
|
|
529
|
-
var
|
|
530
|
-
var
|
|
809
|
+
var fs5 = __toESM(require("fs"));
|
|
810
|
+
var path5 = __toESM(require("path"));
|
|
531
811
|
var import_child_process2 = require("child_process");
|
|
532
812
|
var GWITCOMMAND_FILE = ".gwitcommand";
|
|
533
813
|
var GWITCLEANUP_FILE = ".gwitcleanup";
|
|
@@ -535,9 +815,9 @@ function _parseHookLines(content) {
|
|
|
535
815
|
return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
536
816
|
}
|
|
537
817
|
function readHookFile(mainPath, filename) {
|
|
538
|
-
const filePath =
|
|
539
|
-
if (!
|
|
540
|
-
return _parseHookLines(
|
|
818
|
+
const filePath = path5.join(mainPath, filename);
|
|
819
|
+
if (!fs5.existsSync(filePath)) return [];
|
|
820
|
+
return _parseHookLines(fs5.readFileSync(filePath, "utf-8"));
|
|
541
821
|
}
|
|
542
822
|
function executeCommands(commands, worktreePath, env, stopOnError, label) {
|
|
543
823
|
const mergedEnv = { ...process.env, ...env };
|
|
@@ -640,6 +920,7 @@ async function createCommand(branch, options) {
|
|
|
640
920
|
ui.success(`Worktree ready at ${ui.bold(worktreePath)}`);
|
|
641
921
|
const copied = copyIncludedFiles(mainPath, worktreePath);
|
|
642
922
|
if (copied.length > 0) {
|
|
923
|
+
createSnapshot(slug, branch, mainPath, copied);
|
|
643
924
|
ui.success(`Copied ${copied.length} file${copied.length === 1 ? "" : "s"} from .gwitinclude`);
|
|
644
925
|
copied.forEach((f) => ui.dim(` ${f}`));
|
|
645
926
|
}
|
|
@@ -703,7 +984,7 @@ function listCommand() {
|
|
|
703
984
|
}
|
|
704
985
|
|
|
705
986
|
// src/commands/remove.ts
|
|
706
|
-
var
|
|
987
|
+
var fs6 = __toESM(require("fs"));
|
|
707
988
|
function removeUnregisteredWorktree(branch, force) {
|
|
708
989
|
const worktrees = listWorktrees();
|
|
709
990
|
const match = worktrees.find((w) => w.branch === branch);
|
|
@@ -733,7 +1014,7 @@ async function removeCommand(branch, options) {
|
|
|
733
1014
|
return;
|
|
734
1015
|
}
|
|
735
1016
|
const worktreePath = entry.path;
|
|
736
|
-
if (!force &&
|
|
1017
|
+
if (!force && fs6.existsSync(worktreePath) && hasUncommittedChanges(worktreePath)) {
|
|
737
1018
|
throw new GwitError(
|
|
738
1019
|
`Worktree '${branch}' has uncommitted changes.`,
|
|
739
1020
|
`Use 'gwit remove ${branch} --force' to remove anyway, or commit first.`
|
|
@@ -749,13 +1030,14 @@ async function removeCommand(branch, options) {
|
|
|
749
1030
|
);
|
|
750
1031
|
runCleanupHooks(mainPath, worktreePath, env);
|
|
751
1032
|
ui.step(`Removing worktree at ${worktreePath}\u2026`);
|
|
752
|
-
if (
|
|
1033
|
+
if (fs6.existsSync(worktreePath)) {
|
|
753
1034
|
removeWorktree(worktreePath, force);
|
|
754
1035
|
} else {
|
|
755
1036
|
runArgs("git", ["worktree", "prune"]);
|
|
756
1037
|
ui.dim(" (directory not found \u2014 pruned git bookkeeping)");
|
|
757
1038
|
}
|
|
758
1039
|
await removeWorktreeEntry(mainPath, branch);
|
|
1040
|
+
deleteSnapshot(entry.slug);
|
|
759
1041
|
ui.success(`Removed worktree for '${branch}' (port ${entry.port} freed)`);
|
|
760
1042
|
}
|
|
761
1043
|
|
|
@@ -816,8 +1098,8 @@ function configSetCommand(key, value) {
|
|
|
816
1098
|
}
|
|
817
1099
|
|
|
818
1100
|
// src/commands/init.ts
|
|
819
|
-
var
|
|
820
|
-
var
|
|
1101
|
+
var fs7 = __toESM(require("fs"));
|
|
1102
|
+
var path6 = __toESM(require("path"));
|
|
821
1103
|
var import_inquirer2 = __toESM(require("inquirer"));
|
|
822
1104
|
var GWITINCLUDE_FILE2 = ".gwitinclude";
|
|
823
1105
|
var GWITCOMMAND_FILE2 = ".gwitcommand";
|
|
@@ -846,23 +1128,23 @@ var CLEANUP_HEADER = [
|
|
|
846
1128
|
"# All $GWIT_* variables are available."
|
|
847
1129
|
].join("\n");
|
|
848
1130
|
function _detectAppName(mainPath) {
|
|
849
|
-
const pkgPath =
|
|
850
|
-
if (
|
|
1131
|
+
const pkgPath = path6.join(mainPath, "package.json");
|
|
1132
|
+
if (fs7.existsSync(pkgPath)) {
|
|
851
1133
|
try {
|
|
852
|
-
const pkg2 = JSON.parse(
|
|
1134
|
+
const pkg2 = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
853
1135
|
if (pkg2.name && pkg2.name.trim().length > 0) {
|
|
854
1136
|
return pkg2.name.replace(/^@[^/]+\//, "").trim();
|
|
855
1137
|
}
|
|
856
1138
|
} catch {
|
|
857
1139
|
}
|
|
858
1140
|
}
|
|
859
|
-
return
|
|
1141
|
+
return path6.basename(mainPath);
|
|
860
1142
|
}
|
|
861
1143
|
function _detectPackageManager(mainPath) {
|
|
862
|
-
if (
|
|
863
|
-
if (
|
|
864
|
-
if (
|
|
865
|
-
if (
|
|
1144
|
+
if (fs7.existsSync(path6.join(mainPath, "bun.lockb"))) return "bun";
|
|
1145
|
+
if (fs7.existsSync(path6.join(mainPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
1146
|
+
if (fs7.existsSync(path6.join(mainPath, "yarn.lock"))) return "yarn";
|
|
1147
|
+
if (fs7.existsSync(path6.join(mainPath, "package.json"))) return "npm";
|
|
866
1148
|
return null;
|
|
867
1149
|
}
|
|
868
1150
|
function scanGitignored(mainPath) {
|
|
@@ -875,8 +1157,8 @@ function scanGitignored(mainPath) {
|
|
|
875
1157
|
return result.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
876
1158
|
}
|
|
877
1159
|
function writeHookFile(filePath, content, force) {
|
|
878
|
-
if (
|
|
879
|
-
|
|
1160
|
+
if (fs7.existsSync(filePath) && !force) return "skipped";
|
|
1161
|
+
fs7.writeFileSync(filePath, content, "utf-8");
|
|
880
1162
|
return "written";
|
|
881
1163
|
}
|
|
882
1164
|
async function collectCommands(firstMessage, continueMessage) {
|
|
@@ -1081,7 +1363,7 @@ async function initCommand(options) {
|
|
|
1081
1363
|
const detectedAppName = _detectAppName(mainPath);
|
|
1082
1364
|
const detectedPm = _detectPackageManager(mainPath);
|
|
1083
1365
|
console.log();
|
|
1084
|
-
ui.info(`Setting up gwit for ${ui.bold(
|
|
1366
|
+
ui.info(`Setting up gwit for ${ui.bold(path6.basename(mainPath))}`);
|
|
1085
1367
|
if (!options.force) {
|
|
1086
1368
|
ui.dim(" Existing files are skipped \u2014 use --force to overwrite.");
|
|
1087
1369
|
}
|
|
@@ -1098,7 +1380,7 @@ async function initCommand(options) {
|
|
|
1098
1380
|
{ name: GWITCLEANUP_FILE2, content: _buildCleanupContent(sel.appName, sel) }
|
|
1099
1381
|
];
|
|
1100
1382
|
for (const { name, content } of files) {
|
|
1101
|
-
const result = writeHookFile(
|
|
1383
|
+
const result = writeHookFile(path6.join(mainPath, name), content, options.force);
|
|
1102
1384
|
if (result === "written") {
|
|
1103
1385
|
ui.success(` ${name}`);
|
|
1104
1386
|
} else {
|
|
@@ -1134,7 +1416,117 @@ function openCommand(branch, options) {
|
|
|
1134
1416
|
}
|
|
1135
1417
|
|
|
1136
1418
|
// src/commands/sync.ts
|
|
1137
|
-
var
|
|
1419
|
+
var fs9 = __toESM(require("fs"));
|
|
1420
|
+
|
|
1421
|
+
// src/core/merge.ts
|
|
1422
|
+
var fs8 = __toESM(require("fs"));
|
|
1423
|
+
var path7 = __toESM(require("path"));
|
|
1424
|
+
function normalizeRelPath2(relPath) {
|
|
1425
|
+
return relPath.replace(/\\/g, "/");
|
|
1426
|
+
}
|
|
1427
|
+
function copyFileToMain(worktreeFile, mainFile) {
|
|
1428
|
+
fs8.mkdirSync(path7.dirname(mainFile), { recursive: true });
|
|
1429
|
+
fs8.copyFileSync(worktreeFile, mainFile);
|
|
1430
|
+
}
|
|
1431
|
+
function isExistingFile(filePath) {
|
|
1432
|
+
return fs8.existsSync(filePath) && fs8.statSync(filePath).isFile();
|
|
1433
|
+
}
|
|
1434
|
+
function isBinaryFile(filePath) {
|
|
1435
|
+
const fd = fs8.openSync(filePath, "r");
|
|
1436
|
+
const buffer = Buffer.alloc(8192);
|
|
1437
|
+
try {
|
|
1438
|
+
const bytesRead = fs8.readSync(fd, buffer, 0, buffer.length, 0);
|
|
1439
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
1440
|
+
if (buffer[i] === 0) return true;
|
|
1441
|
+
}
|
|
1442
|
+
return false;
|
|
1443
|
+
} finally {
|
|
1444
|
+
fs8.closeSync(fd);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function runGitMergeFile(mainFile, snapshotFile, worktreeFile, branch) {
|
|
1448
|
+
const result = runArgsWithExitCode("git", [
|
|
1449
|
+
"merge-file",
|
|
1450
|
+
"-L",
|
|
1451
|
+
"main",
|
|
1452
|
+
"-L",
|
|
1453
|
+
"base",
|
|
1454
|
+
"-L",
|
|
1455
|
+
branch,
|
|
1456
|
+
mainFile,
|
|
1457
|
+
snapshotFile,
|
|
1458
|
+
worktreeFile
|
|
1459
|
+
]);
|
|
1460
|
+
return result.exitCode > 0;
|
|
1461
|
+
}
|
|
1462
|
+
function mergeBackIncludedFiles(worktreePath, mainPath, slug) {
|
|
1463
|
+
const result = {
|
|
1464
|
+
copied: [],
|
|
1465
|
+
merged: [],
|
|
1466
|
+
conflicts: [],
|
|
1467
|
+
skipped: [],
|
|
1468
|
+
binarySkipped: []
|
|
1469
|
+
};
|
|
1470
|
+
const manifest = readSnapshot(slug);
|
|
1471
|
+
if (!manifest) return result;
|
|
1472
|
+
const relPaths = Object.keys(manifest.files).sort();
|
|
1473
|
+
for (const rawRelPath of relPaths) {
|
|
1474
|
+
const relPath = normalizeRelPath2(rawRelPath);
|
|
1475
|
+
const base = manifest.files[relPath];
|
|
1476
|
+
if (!base) {
|
|
1477
|
+
result.skipped.push(relPath);
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
if (!isGitIgnored(relPath, mainPath) || isGitTracked(relPath, mainPath)) {
|
|
1481
|
+
result.skipped.push(relPath);
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
const mainFile = path7.resolve(mainPath, relPath);
|
|
1485
|
+
const worktreeFile = path7.resolve(worktreePath, relPath);
|
|
1486
|
+
const snapshotFile = getSnapshotFilePath(slug, relPath);
|
|
1487
|
+
if (!isExistingFile(mainFile)) {
|
|
1488
|
+
result.skipped.push(relPath);
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (!isExistingFile(worktreeFile)) {
|
|
1492
|
+
result.skipped.push(relPath);
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
if (!snapshotFile || !isExistingFile(snapshotFile)) {
|
|
1496
|
+
result.skipped.push(relPath);
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
const baseHash = base.hash;
|
|
1500
|
+
const mainHash = hashFile(mainFile);
|
|
1501
|
+
const worktreeHash = hashFile(worktreeFile);
|
|
1502
|
+
if (baseHash === mainHash && baseHash === worktreeHash) {
|
|
1503
|
+
result.skipped.push(relPath);
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (baseHash === mainHash) {
|
|
1507
|
+
copyFileToMain(worktreeFile, mainFile);
|
|
1508
|
+
result.copied.push(relPath);
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
if (baseHash === worktreeHash || mainHash === worktreeHash) {
|
|
1512
|
+
result.skipped.push(relPath);
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
if (isBinaryFile(mainFile) || isBinaryFile(worktreeFile) || isBinaryFile(snapshotFile)) {
|
|
1516
|
+
result.binarySkipped.push(relPath);
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
const hasConflicts = runGitMergeFile(mainFile, snapshotFile, worktreeFile, manifest.branch);
|
|
1520
|
+
if (hasConflicts) {
|
|
1521
|
+
result.conflicts.push(relPath);
|
|
1522
|
+
} else {
|
|
1523
|
+
result.merged.push(relPath);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return result;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// src/commands/sync.ts
|
|
1138
1530
|
function detectCurrentBranch(mainPath) {
|
|
1139
1531
|
const currentRoot = getRepoRoot();
|
|
1140
1532
|
if (currentRoot === mainPath) {
|
|
@@ -1156,7 +1548,45 @@ function detectCurrentBranch(mainPath) {
|
|
|
1156
1548
|
}
|
|
1157
1549
|
return current.branch;
|
|
1158
1550
|
}
|
|
1159
|
-
function
|
|
1551
|
+
function logFileList(files) {
|
|
1552
|
+
files.forEach((f) => ui.dim(` ${f}`));
|
|
1553
|
+
}
|
|
1554
|
+
function reportBackResult(result) {
|
|
1555
|
+
if (result.copied.length > 0) {
|
|
1556
|
+
ui.success(
|
|
1557
|
+
`Copied ${result.copied.length} worktree-only file${result.copied.length === 1 ? "" : "s"}`
|
|
1558
|
+
);
|
|
1559
|
+
logFileList(result.copied);
|
|
1560
|
+
}
|
|
1561
|
+
if (result.merged.length > 0) {
|
|
1562
|
+
ui.success(
|
|
1563
|
+
`Merged ${result.merged.length} file${result.merged.length === 1 ? "" : "s"} cleanly`
|
|
1564
|
+
);
|
|
1565
|
+
logFileList(result.merged);
|
|
1566
|
+
}
|
|
1567
|
+
if (result.conflicts.length > 0) {
|
|
1568
|
+
ui.warn(
|
|
1569
|
+
`Conflicts in ${result.conflicts.length} file${result.conflicts.length === 1 ? "" : "s"} (markers written to main)`
|
|
1570
|
+
);
|
|
1571
|
+
logFileList(result.conflicts);
|
|
1572
|
+
}
|
|
1573
|
+
if (result.binarySkipped.length > 0) {
|
|
1574
|
+
ui.warn(
|
|
1575
|
+
`Skipped ${result.binarySkipped.length} binary file${result.binarySkipped.length === 1 ? "" : "s"} (manual resolution required)`
|
|
1576
|
+
);
|
|
1577
|
+
logFileList(result.binarySkipped);
|
|
1578
|
+
}
|
|
1579
|
+
if (result.skipped.length > 0) {
|
|
1580
|
+
ui.dim(
|
|
1581
|
+
` skipped ${result.skipped.length} unchanged/main-only file${result.skipped.length === 1 ? "" : "s"}`
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
const changed = result.copied.length + result.merged.length + result.conflicts.length;
|
|
1585
|
+
if (changed === 0 && result.binarySkipped.length === 0) {
|
|
1586
|
+
ui.info("Nothing to sync back \u2014 no snapshot-tracked files changed.");
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function syncCommand(branch, options = {}) {
|
|
1160
1590
|
if (!isGitRepo()) {
|
|
1161
1591
|
throw new GwitError(
|
|
1162
1592
|
"Not a git repository.",
|
|
@@ -1172,26 +1602,403 @@ function syncCommand(branch) {
|
|
|
1172
1602
|
`Run 'gwit list' to see active worktrees.`
|
|
1173
1603
|
);
|
|
1174
1604
|
}
|
|
1175
|
-
if (!
|
|
1605
|
+
if (!fs9.existsSync(entry.path)) {
|
|
1176
1606
|
throw new GwitError(
|
|
1177
1607
|
`Worktree path no longer exists: ${entry.path}`,
|
|
1178
1608
|
`Run 'gwit remove ${targetBranch}' to clean up the stale registry entry.`
|
|
1179
1609
|
);
|
|
1180
1610
|
}
|
|
1611
|
+
if (options.back) {
|
|
1612
|
+
ui.step(`Syncing .gwitinclude back to main from ${entry.path}\u2026`);
|
|
1613
|
+
const snapshot = readSnapshot(entry.slug);
|
|
1614
|
+
if (!snapshot) {
|
|
1615
|
+
ui.warn(`No snapshot found for '${targetBranch}' \u2014 falling back to direct copy.`);
|
|
1616
|
+
const copiedBack = reverseCopyIncludedFiles(entry.path, mainPath);
|
|
1617
|
+
if (copiedBack.length > 0) {
|
|
1618
|
+
ui.success(`Synced ${copiedBack.length} file${copiedBack.length === 1 ? "" : "s"} back`);
|
|
1619
|
+
logFileList(copiedBack);
|
|
1620
|
+
} else {
|
|
1621
|
+
ui.info("Nothing to sync back \u2014 no .gwitinclude entries were copied.");
|
|
1622
|
+
}
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
const result = mergeBackIncludedFiles(entry.path, mainPath, entry.slug);
|
|
1626
|
+
reportBackResult(result);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1181
1629
|
ui.step(`Syncing .gwitinclude into ${entry.path}\u2026`);
|
|
1182
1630
|
const copied = copyIncludedFiles(mainPath, entry.path);
|
|
1183
1631
|
if (copied.length > 0) {
|
|
1184
1632
|
ui.success(`Synced ${copied.length} file${copied.length === 1 ? "" : "s"}`);
|
|
1185
|
-
copied
|
|
1633
|
+
logFileList(copied);
|
|
1186
1634
|
} else {
|
|
1187
1635
|
ui.info("Nothing to sync \u2014 no .gwitinclude entries were copied.");
|
|
1188
1636
|
}
|
|
1189
1637
|
}
|
|
1190
1638
|
|
|
1639
|
+
// src/commands/merge.ts
|
|
1640
|
+
var fs10 = __toESM(require("fs"));
|
|
1641
|
+
function executeMerge(mainPath, worktreePath, branch, target, options) {
|
|
1642
|
+
try {
|
|
1643
|
+
if (options.squash) {
|
|
1644
|
+
ui.step(`Squash merging '${branch}' into '${target}'\u2026`);
|
|
1645
|
+
squashMergeBranch(mainPath, branch);
|
|
1646
|
+
commitMerge(mainPath, `Squash merge branch '${branch}'`);
|
|
1647
|
+
} else if (options.rebase) {
|
|
1648
|
+
ui.step(`Rebasing '${branch}' onto '${target}'\u2026`);
|
|
1649
|
+
rebaseBranch(worktreePath, target);
|
|
1650
|
+
ui.step(`Fast-forwarding '${target}' to '${branch}'\u2026`);
|
|
1651
|
+
ffMergeBranch(mainPath, branch);
|
|
1652
|
+
} else {
|
|
1653
|
+
ui.step(`Merging '${branch}' into '${target}'\u2026`);
|
|
1654
|
+
mergeBranch(mainPath, branch, options.noFf ?? false);
|
|
1655
|
+
}
|
|
1656
|
+
} catch {
|
|
1657
|
+
throw new GwitError(
|
|
1658
|
+
`Merge conflict while merging '${branch}' into '${target}'.`,
|
|
1659
|
+
`Resolve conflicts in ${mainPath}, then run: git merge --continue`
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
async function cleanupWorktree(mainPath, worktreePath, branch, port) {
|
|
1664
|
+
const entry = getWorktreeEntry(mainPath, branch);
|
|
1665
|
+
if (entry) {
|
|
1666
|
+
const env = buildEnvironment(
|
|
1667
|
+
entry.branch,
|
|
1668
|
+
entry.slug,
|
|
1669
|
+
entry.port,
|
|
1670
|
+
worktreePath,
|
|
1671
|
+
mainPath,
|
|
1672
|
+
entry.index
|
|
1673
|
+
);
|
|
1674
|
+
runCleanupHooks(mainPath, worktreePath, env);
|
|
1675
|
+
}
|
|
1676
|
+
ui.step(`Removing worktree at ${worktreePath}\u2026`);
|
|
1677
|
+
if (fs10.existsSync(worktreePath)) {
|
|
1678
|
+
removeWorktree(worktreePath, true);
|
|
1679
|
+
} else {
|
|
1680
|
+
runArgs("git", ["worktree", "prune"]);
|
|
1681
|
+
}
|
|
1682
|
+
await removeWorktreeEntry(mainPath, branch);
|
|
1683
|
+
ui.success(`Cleaned up worktree for '${branch}' (port ${port} freed)`);
|
|
1684
|
+
}
|
|
1685
|
+
function syncBackFiles(worktreePath, mainPath, slug, branch) {
|
|
1686
|
+
const snapshot = readSnapshot(slug);
|
|
1687
|
+
if (!snapshot) {
|
|
1688
|
+
ui.warn(`No snapshot found for '${branch}' \u2014 falling back to direct copy.`);
|
|
1689
|
+
const copied = reverseCopyIncludedFiles(worktreePath, mainPath);
|
|
1690
|
+
if (copied.length > 0) {
|
|
1691
|
+
ui.success(`Synced ${copied.length} file${copied.length === 1 ? "" : "s"} back`);
|
|
1692
|
+
copied.forEach((f) => ui.dim(` ${f}`));
|
|
1693
|
+
} else {
|
|
1694
|
+
ui.dim(" No .gwitinclude files to sync back");
|
|
1695
|
+
}
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
const result = mergeBackIncludedFiles(worktreePath, mainPath, slug);
|
|
1699
|
+
const changed = result.copied.length + result.merged.length + result.conflicts.length;
|
|
1700
|
+
if (result.copied.length > 0) {
|
|
1701
|
+
ui.success(
|
|
1702
|
+
`Copied ${result.copied.length} worktree-only file${result.copied.length === 1 ? "" : "s"}`
|
|
1703
|
+
);
|
|
1704
|
+
result.copied.forEach((f) => ui.dim(` ${f}`));
|
|
1705
|
+
}
|
|
1706
|
+
if (result.merged.length > 0) {
|
|
1707
|
+
ui.success(
|
|
1708
|
+
`Merged ${result.merged.length} file${result.merged.length === 1 ? "" : "s"} cleanly`
|
|
1709
|
+
);
|
|
1710
|
+
result.merged.forEach((f) => ui.dim(` ${f}`));
|
|
1711
|
+
}
|
|
1712
|
+
if (result.conflicts.length > 0) {
|
|
1713
|
+
ui.warn(
|
|
1714
|
+
`Conflicts in ${result.conflicts.length} file${result.conflicts.length === 1 ? "" : "s"} (markers written to main)`
|
|
1715
|
+
);
|
|
1716
|
+
result.conflicts.forEach((f) => ui.dim(` ${f}`));
|
|
1717
|
+
}
|
|
1718
|
+
if (result.binarySkipped.length > 0) {
|
|
1719
|
+
ui.warn(
|
|
1720
|
+
`Skipped ${result.binarySkipped.length} binary file${result.binarySkipped.length === 1 ? "" : "s"} (manual resolution required)`
|
|
1721
|
+
);
|
|
1722
|
+
result.binarySkipped.forEach((f) => ui.dim(` ${f}`));
|
|
1723
|
+
}
|
|
1724
|
+
if (changed === 0 && result.binarySkipped.length === 0) {
|
|
1725
|
+
ui.dim(" No .gwitinclude files to sync back");
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
async function mergeCommand(branch, options) {
|
|
1729
|
+
if (!isGitRepo()) {
|
|
1730
|
+
throw new GwitError(
|
|
1731
|
+
"Not a git repository.",
|
|
1732
|
+
"Run gwit merge from inside a git repo (or any of its worktrees)."
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
const mainPath = getMainWorktreePath();
|
|
1736
|
+
const entry = getWorktreeEntry(mainPath, branch);
|
|
1737
|
+
if (!entry) {
|
|
1738
|
+
throw new GwitError(
|
|
1739
|
+
`No gwit worktree found for '${branch}'.`,
|
|
1740
|
+
`Run 'gwit list' to see active worktrees.`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
if (!fs10.existsSync(entry.path)) {
|
|
1744
|
+
throw new GwitError(
|
|
1745
|
+
`Worktree path no longer exists: ${entry.path}`,
|
|
1746
|
+
`Run 'gwit remove ${branch}' to clean up the stale registry entry.`
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
if (hasUncommittedChanges(entry.path)) {
|
|
1750
|
+
throw new GwitError(
|
|
1751
|
+
`Worktree for '${branch}' has uncommitted changes.`,
|
|
1752
|
+
"Commit or stash changes in the worktree first, then retry."
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
const target = options.into ?? getDefaultBranch();
|
|
1756
|
+
ui.info(`Target branch: ${ui.bold(target)}`);
|
|
1757
|
+
if (options.syncBack !== false) {
|
|
1758
|
+
ui.step("Syncing .gwitinclude files back to main\u2026");
|
|
1759
|
+
syncBackFiles(entry.path, mainPath, entry.slug, entry.branch);
|
|
1760
|
+
}
|
|
1761
|
+
executeMerge(mainPath, entry.path, branch, target, options);
|
|
1762
|
+
ui.success(`Merged '${branch}' into '${target}'`);
|
|
1763
|
+
if (options.cleanup) {
|
|
1764
|
+
await cleanupWorktree(mainPath, entry.path, branch, entry.port);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// src/commands/status.ts
|
|
1769
|
+
var fs11 = __toESM(require("fs"));
|
|
1770
|
+
|
|
1771
|
+
// src/core/gh.ts
|
|
1772
|
+
function aggregateChecks(rollup) {
|
|
1773
|
+
if (!rollup || rollup.length === 0) return null;
|
|
1774
|
+
let hasPending = false;
|
|
1775
|
+
for (const check of rollup) {
|
|
1776
|
+
const conclusion = check.conclusion ?? check.state;
|
|
1777
|
+
if (conclusion === "FAILURE" || conclusion === "ERROR" || conclusion === "ACTION_REQUIRED") {
|
|
1778
|
+
return "fail";
|
|
1779
|
+
}
|
|
1780
|
+
if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED") {
|
|
1781
|
+
hasPending = true;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return hasPending ? "pending" : "pass";
|
|
1785
|
+
}
|
|
1786
|
+
function isGhAvailable() {
|
|
1787
|
+
return runArgsSafe("gh", ["--version"]).success;
|
|
1788
|
+
}
|
|
1789
|
+
function getPrInfo(branch) {
|
|
1790
|
+
const result = runArgsSafe("gh", [
|
|
1791
|
+
"pr",
|
|
1792
|
+
"view",
|
|
1793
|
+
branch,
|
|
1794
|
+
"--json",
|
|
1795
|
+
"number,state,isDraft,statusCheckRollup"
|
|
1796
|
+
]);
|
|
1797
|
+
if (!result.success || result.stdout.length === 0) return null;
|
|
1798
|
+
try {
|
|
1799
|
+
const data = JSON.parse(result.stdout);
|
|
1800
|
+
if (typeof data.number !== "number" || typeof data.state !== "string") return null;
|
|
1801
|
+
return {
|
|
1802
|
+
number: data.number,
|
|
1803
|
+
state: data.state,
|
|
1804
|
+
isDraft: data.isDraft ?? false,
|
|
1805
|
+
checksStatus: aggregateChecks(data.statusCheckRollup)
|
|
1806
|
+
};
|
|
1807
|
+
} catch {
|
|
1808
|
+
return null;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// src/commands/status.ts
|
|
1813
|
+
function buildRow(branch, entryPath, port, defaultBranch, ghAvailable) {
|
|
1814
|
+
const exists = fs11.existsSync(entryPath);
|
|
1815
|
+
const { ahead, behind } = exists ? getAheadBehind(branch, defaultBranch, entryPath) : { ahead: 0, behind: 0 };
|
|
1816
|
+
const dirty = exists ? hasUncommittedChanges(entryPath) : false;
|
|
1817
|
+
let pr = "\u2014";
|
|
1818
|
+
if (ghAvailable) {
|
|
1819
|
+
const info = getPrInfo(branch);
|
|
1820
|
+
if (info) {
|
|
1821
|
+
const draft = info.isDraft ? " draft" : "";
|
|
1822
|
+
const checks = info.checksStatus ? ` (checks: ${info.checksStatus})` : "";
|
|
1823
|
+
pr = `#${info.number} ${info.state.toLowerCase()}${draft}${checks}`;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return { branch, path: entryPath, port, ahead, behind, dirty, pr };
|
|
1827
|
+
}
|
|
1828
|
+
function printTable2(rows) {
|
|
1829
|
+
const headers = ["Branch", "Port", "Ahead/Behind", "Changes", "PR"];
|
|
1830
|
+
const formatted = rows.map((r) => [
|
|
1831
|
+
r.branch,
|
|
1832
|
+
String(r.port),
|
|
1833
|
+
`+${r.ahead} / -${r.behind}`,
|
|
1834
|
+
r.dirty ? "dirty" : "clean",
|
|
1835
|
+
r.pr
|
|
1836
|
+
]);
|
|
1837
|
+
const widths = headers.map(
|
|
1838
|
+
(h, i) => Math.max(h.length, ...formatted.map((row) => (row[i] ?? "").length))
|
|
1839
|
+
);
|
|
1840
|
+
const headerLine = headers.map((h, i) => h.padEnd(widths[i] ?? 0)).join(" ");
|
|
1841
|
+
console.log(` ${ui.bold(headerLine)}`);
|
|
1842
|
+
for (const row of formatted) {
|
|
1843
|
+
const line = row.map((cell, i) => (cell ?? "").padEnd(widths[i] ?? 0)).join(" ");
|
|
1844
|
+
console.log(` ${line}`);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
function statusCommand(options) {
|
|
1848
|
+
if (!isGitRepo()) {
|
|
1849
|
+
throw new GwitError(
|
|
1850
|
+
"Not a git repository.",
|
|
1851
|
+
"Run gwit status from inside a git repo (or any of its worktrees)."
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
const mainPath = getMainWorktreePath();
|
|
1855
|
+
const entries = listWorktreeEntries(mainPath);
|
|
1856
|
+
if (entries.length === 0) {
|
|
1857
|
+
ui.info("No active gwit worktrees.");
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const defaultBranch = getDefaultBranch();
|
|
1861
|
+
const ghAvail = isGhAvailable();
|
|
1862
|
+
const rows = entries.map((e) => buildRow(e.branch, e.path, e.port, defaultBranch, ghAvail));
|
|
1863
|
+
if (options.json) {
|
|
1864
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
console.log();
|
|
1868
|
+
printTable2(rows);
|
|
1869
|
+
console.log();
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/commands/sweep.ts
|
|
1873
|
+
var fs12 = __toESM(require("fs"));
|
|
1874
|
+
var import_inquirer3 = __toESM(require("inquirer"));
|
|
1875
|
+
function isSweepable(branch, defaultBranch, ghAvailable) {
|
|
1876
|
+
if (isBranchMerged(branch, defaultBranch)) return true;
|
|
1877
|
+
if (ghAvailable) {
|
|
1878
|
+
const pr = getPrInfo(branch);
|
|
1879
|
+
if (pr && (pr.state === "MERGED" || pr.state === "CLOSED")) return true;
|
|
1880
|
+
}
|
|
1881
|
+
return false;
|
|
1882
|
+
}
|
|
1883
|
+
async function sweepEntry(entry, mainPath) {
|
|
1884
|
+
const env = buildEnvironment(
|
|
1885
|
+
entry.branch,
|
|
1886
|
+
entry.slug,
|
|
1887
|
+
entry.port,
|
|
1888
|
+
entry.path,
|
|
1889
|
+
mainPath,
|
|
1890
|
+
entry.index
|
|
1891
|
+
);
|
|
1892
|
+
runCleanupHooks(mainPath, entry.path, env);
|
|
1893
|
+
if (fs12.existsSync(entry.path)) {
|
|
1894
|
+
removeWorktree(entry.path, true);
|
|
1895
|
+
} else {
|
|
1896
|
+
runArgs("git", ["worktree", "prune"]);
|
|
1897
|
+
}
|
|
1898
|
+
await removeWorktreeEntry(mainPath, entry.branch);
|
|
1899
|
+
ui.success(`Removed '${entry.branch}' (port ${entry.port} freed)`);
|
|
1900
|
+
}
|
|
1901
|
+
async function sweepCommand(options) {
|
|
1902
|
+
if (!isGitRepo()) {
|
|
1903
|
+
throw new GwitError(
|
|
1904
|
+
"Not a git repository.",
|
|
1905
|
+
"Run gwit sweep from inside a git repo (or any of its worktrees)."
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
const mainPath = getMainWorktreePath();
|
|
1909
|
+
const entries = listWorktreeEntries(mainPath);
|
|
1910
|
+
if (entries.length === 0) {
|
|
1911
|
+
ui.info("No active gwit worktrees to sweep.");
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
const defaultBranch = getDefaultBranch();
|
|
1915
|
+
const ghAvail = isGhAvailable();
|
|
1916
|
+
const sweepable = entries.filter((e) => isSweepable(e.branch, defaultBranch, ghAvail));
|
|
1917
|
+
if (sweepable.length === 0) {
|
|
1918
|
+
ui.info("No merged worktrees found to sweep.");
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
console.log();
|
|
1922
|
+
ui.info(`Found ${sweepable.length} merged worktree${sweepable.length === 1 ? "" : "s"}:`);
|
|
1923
|
+
for (const entry of sweepable) {
|
|
1924
|
+
ui.dim(` ${entry.branch} (port ${entry.port})`);
|
|
1925
|
+
}
|
|
1926
|
+
console.log();
|
|
1927
|
+
if (options.dryRun) {
|
|
1928
|
+
ui.info("Dry run \u2014 no worktrees were removed.");
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
if (!options.force) {
|
|
1932
|
+
const { confirm } = await import_inquirer3.default.prompt([
|
|
1933
|
+
{
|
|
1934
|
+
type: "confirm",
|
|
1935
|
+
name: "confirm",
|
|
1936
|
+
message: `Remove ${sweepable.length} worktree${sweepable.length === 1 ? "" : "s"}?`,
|
|
1937
|
+
default: false
|
|
1938
|
+
}
|
|
1939
|
+
]);
|
|
1940
|
+
if (!confirm) {
|
|
1941
|
+
ui.info("Sweep cancelled.");
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
for (const entry of sweepable) {
|
|
1946
|
+
await sweepEntry(entry, mainPath);
|
|
1947
|
+
}
|
|
1948
|
+
ui.success(`Swept ${sweepable.length} worktree${sweepable.length === 1 ? "" : "s"}`);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// src/commands/rename.ts
|
|
1952
|
+
async function renameCommand(oldBranch, newBranch) {
|
|
1953
|
+
if (!isGitRepo()) {
|
|
1954
|
+
throw new GwitError(
|
|
1955
|
+
"Not a git repository.",
|
|
1956
|
+
"Run gwit rename from inside a git repo (or any of its worktrees)."
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
const mainPath = getMainWorktreePath();
|
|
1960
|
+
const entry = getWorktreeEntry(mainPath, oldBranch);
|
|
1961
|
+
if (!entry) {
|
|
1962
|
+
throw new GwitError(
|
|
1963
|
+
`No gwit worktree found for '${oldBranch}'.`,
|
|
1964
|
+
`Run 'gwit list' to see active worktrees.`
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
const existingNew = getWorktreeEntry(mainPath, newBranch);
|
|
1968
|
+
if (existingNew) {
|
|
1969
|
+
throw new GwitError(
|
|
1970
|
+
`A worktree already exists for '${newBranch}'.`,
|
|
1971
|
+
`Remove it first: gwit remove ${newBranch}`
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
ui.step(`Renaming branch '${oldBranch}' \u2192 '${newBranch}'\u2026`);
|
|
1975
|
+
renameBranch(oldBranch, newBranch);
|
|
1976
|
+
const oldSlug = entry.slug;
|
|
1977
|
+
const newSlug = toSlug(newBranch);
|
|
1978
|
+
let newPath = entry.path;
|
|
1979
|
+
if (oldSlug !== newSlug) {
|
|
1980
|
+
const config = loadConfig();
|
|
1981
|
+
newPath = getWorktreePath(mainPath, config.location, newSlug);
|
|
1982
|
+
ui.step(`Moving worktree to ${newPath}\u2026`);
|
|
1983
|
+
moveWorktree(entry.path, newPath);
|
|
1984
|
+
renameSnapshot(oldSlug, newSlug);
|
|
1985
|
+
}
|
|
1986
|
+
await removeWorktreeEntry(mainPath, oldBranch);
|
|
1987
|
+
await addWorktreeEntry(mainPath, {
|
|
1988
|
+
...entry,
|
|
1989
|
+
branch: newBranch,
|
|
1990
|
+
slug: newSlug,
|
|
1991
|
+
path: newPath
|
|
1992
|
+
});
|
|
1993
|
+
ui.success(`Renamed '${oldBranch}' \u2192 '${newBranch}'`);
|
|
1994
|
+
if (oldSlug !== newSlug) {
|
|
1995
|
+
ui.info(`Slug ${ui.bold(oldSlug)} \u2192 ${ui.bold(newSlug)}`);
|
|
1996
|
+
ui.info(`Path ${ui.bold(newPath)}`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1191
2000
|
// src/index.ts
|
|
1192
|
-
var pkg = JSON.parse(
|
|
1193
|
-
fs8.readFileSync(path6.join(__dirname, "..", "package.json"), "utf-8")
|
|
1194
|
-
);
|
|
2001
|
+
var pkg = JSON.parse(fs13.readFileSync(path8.join(__dirname, "..", "package.json"), "utf-8"));
|
|
1195
2002
|
var program = new import_commander.Command();
|
|
1196
2003
|
program.name("gwit").description("Fully isolated git worktrees for parallel development").version(pkg.version);
|
|
1197
2004
|
program.argument("[branch]", "Branch name (local or remote)").option("-b", "Create a new branch from HEAD").option("--editor <name>", "Override editor for this invocation").option("--no-editor", "Skip opening editor").option("--no-commands", "Skip .gwitcommand execution").action(async (branch, options) => {
|
|
@@ -1223,8 +2030,20 @@ configCmd.action(() => {
|
|
|
1223
2030
|
program.command("open <branch>").description("Re-open the editor for an existing gwit worktree").option("--editor <name>", "Override editor for this invocation").action((branch, options) => {
|
|
1224
2031
|
openCommand(branch, options);
|
|
1225
2032
|
});
|
|
1226
|
-
program.command("sync [branch]").description("
|
|
1227
|
-
syncCommand(branch);
|
|
2033
|
+
program.command("sync [branch]").description("Sync .gwitinclude files into a worktree or back to main").option("--back", "Three-way merge .gwitinclude files back to main").action((branch, options) => {
|
|
2034
|
+
syncCommand(branch, options);
|
|
2035
|
+
});
|
|
2036
|
+
program.command("merge <branch>").description("Merge a worktree branch back into the target branch").option("--into <target>", "Target branch (default: repo default branch)").option("--squash", "Squash all commits into one before merging").option("--rebase", "Rebase feature onto target, then fast-forward").option("--no-ff", "Force a merge commit even when fast-forward is possible").option("--cleanup", "Remove worktree after successful merge").option("--no-sync-back", "Skip reverse-copying .gwitinclude files").action(async (branch, options) => {
|
|
2037
|
+
await mergeCommand(branch, options);
|
|
2038
|
+
});
|
|
2039
|
+
program.command("status").description("Show status of all active gwit worktrees").option("--json", "Output as JSON").action((options) => {
|
|
2040
|
+
statusCommand(options);
|
|
2041
|
+
});
|
|
2042
|
+
program.command("sweep").description("Remove worktrees whose branches are merged").option("--dry-run", "Show what would be removed without removing").option("--force", "Skip confirmation prompt").action(async (options) => {
|
|
2043
|
+
await sweepCommand(options);
|
|
2044
|
+
});
|
|
2045
|
+
program.command("rename <old-branch> <new-branch>").description("Rename a worktree branch").action(async (oldBranch, newBranch) => {
|
|
2046
|
+
await renameCommand(oldBranch, newBranch);
|
|
1228
2047
|
});
|
|
1229
2048
|
program.parseAsync(process.argv).catch((err) => {
|
|
1230
2049
|
if (err instanceof GwitError) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivanshshrivas/gwit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Fully isolated git worktrees for parallel development",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
@@ -21,22 +21,30 @@
|
|
|
21
21
|
"test:unit": "vitest run tests/unit",
|
|
22
22
|
"test:integration": "vitest run tests/integration",
|
|
23
23
|
"lint": "eslint src tests --ext .ts",
|
|
24
|
-
"
|
|
24
|
+
"lint:maintainability": "eslint -c eslint.maintainability.cjs src/",
|
|
25
|
+
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
26
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\""
|
|
25
27
|
},
|
|
26
28
|
"dependencies": {
|
|
27
29
|
"commander": "^12.1.0",
|
|
28
|
-
"inquirer": "^8.2.6"
|
|
30
|
+
"inquirer": "^8.2.6",
|
|
31
|
+
"minimatch": "^10.2.4"
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
34
|
+
"@eslint/js": "^9.39.3",
|
|
31
35
|
"@types/inquirer": "^8.2.10",
|
|
36
|
+
"@types/minimatch": "^5.1.2",
|
|
32
37
|
"@types/node": "^20.0.0",
|
|
33
38
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
34
39
|
"@typescript-eslint/parser": "^7.0.0",
|
|
35
40
|
"eslint": "^8.57.0",
|
|
41
|
+
"eslint-config-prettier": "^10.1.8",
|
|
42
|
+
"eslint-plugin-jsdoc": "^61.7.1",
|
|
36
43
|
"prettier": "^3.2.0",
|
|
37
44
|
"tsup": "^8.0.0",
|
|
38
45
|
"tsx": "^4.7.0",
|
|
39
46
|
"typescript": "^5.4.0",
|
|
47
|
+
"typescript-eslint": "^8.56.1",
|
|
40
48
|
"vitest": "^1.6.0"
|
|
41
49
|
}
|
|
42
50
|
}
|