@hyperdrive.bot/gut 0.2.1 → 0.2.3-alpha.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
CHANGED
|
@@ -48,7 +48,7 @@ $ npm install -g @hyperdrive.bot/gut
|
|
|
48
48
|
$ gut COMMAND
|
|
49
49
|
running command...
|
|
50
50
|
$ gut (--version)
|
|
51
|
-
@hyperdrive.bot/gut/0.2.
|
|
51
|
+
@hyperdrive.bot/gut/0.2.2 linux-x64 node-v22.22.2
|
|
52
52
|
$ gut --help [COMMAND]
|
|
53
53
|
USAGE
|
|
54
54
|
$ gut COMMAND
|
|
@@ -88,6 +88,25 @@ export default class WorktreeCreate extends BaseCommand {
|
|
|
88
88
|
this.error('No entities focused and no --entity flag provided. Use "gut focus <entity>" first, or pass --entity <name> (repeatable).');
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
// Pre-flight: each entity must be its OWN git repo. A plain directory
|
|
92
|
+
// tracked by the super-repo (no .git of its own) would make per-entity
|
|
93
|
+
// `git worktree add` walk up to the super-repo's .git and collide with
|
|
94
|
+
// the super-repo branch we'd create one step earlier — the failure
|
|
95
|
+
// happens AFTER the super-repo branch is created, leaving an orphan
|
|
96
|
+
// `<name>` branch behind. Catching it here errors cleanly and never
|
|
97
|
+
// touches super-repo state.
|
|
98
|
+
const nonRepoEntities = selectedEntities.filter((entity) => {
|
|
99
|
+
const entityMainPath = path.join(workspaceRoot, entity.path.replace(/^\.\//, ''));
|
|
100
|
+
return !this.gitService.isStandaloneRepository(entityMainPath);
|
|
101
|
+
});
|
|
102
|
+
if (nonRepoEntities.length > 0) {
|
|
103
|
+
const details = nonRepoEntities.map((e) => ` • ${e.name} @ ${e.path}`).join('\n');
|
|
104
|
+
this.error(`Cannot create worktree — these entities are not standalone git repositories:\n${details}\n\n` +
|
|
105
|
+
'They are plain directories tracked by the super-repo (no .git of their own), so each\n' +
|
|
106
|
+
'per-entity `git worktree add` would collide with the super-repo\'s branch namespace.\n' +
|
|
107
|
+
'Pass --entity pointing at entities that have their own .git, or remove the offenders\n' +
|
|
108
|
+
'from your focus set. Run `gut entity list` to see all entities + their paths.');
|
|
109
|
+
}
|
|
91
110
|
// --- Create worktrees with atomic rollback (AC: 1, 2, 9) ---
|
|
92
111
|
const createdWorktrees = [];
|
|
93
112
|
const entityRecords = [];
|
|
@@ -136,6 +155,14 @@ export default class WorktreeCreate extends BaseCommand {
|
|
|
136
155
|
const entityMainPath = path.join(workspaceRoot, entityRelativePath);
|
|
137
156
|
await this.gitService.worktreePrune(entityMainPath).catch(() => { });
|
|
138
157
|
}
|
|
158
|
+
// Delete the super-repo branch we created at step 1 (otherwise it
|
|
159
|
+
// lingers as an orphan after every partial failure — confusing trail
|
|
160
|
+
// that grows on each retry). Derived from createdWorktrees so the flag
|
|
161
|
+
// and the list of created worktrees can't drift.
|
|
162
|
+
const superRepoBranchCreated = createdWorktrees.some((w) => w.repoPath === workspaceRoot);
|
|
163
|
+
if (superRepoBranchCreated) {
|
|
164
|
+
await this.gitService.branchDelete(workspaceRoot, args.name, true).catch(() => { });
|
|
165
|
+
}
|
|
139
166
|
this.log(chalk.red('✗ Worktree creation failed. All partial worktrees cleaned up.'));
|
|
140
167
|
const message = error instanceof Error ? error.message : String(error);
|
|
141
168
|
this.error(message);
|
|
@@ -40,6 +40,26 @@ export declare class GitService {
|
|
|
40
40
|
hasRemote(repoPath: string, remote?: string): Promise<boolean>;
|
|
41
41
|
init(repoPath: string): Promise<void>;
|
|
42
42
|
isRepository(repoPath: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* True if `repoPath` is its OWN git repo (has a `.git` directory or file
|
|
45
|
+
* at the exact path) — distinct from `isRepository` which walks up via
|
|
46
|
+
* `git rev-parse --git-dir` and would falsely succeed for a plain dir
|
|
47
|
+
* tracked by an ancestor repo.
|
|
48
|
+
*
|
|
49
|
+
* Used by `worktree create` to reject entities that are plain directories
|
|
50
|
+
* in the super-repo (no .git of their own) — those would make per-entity
|
|
51
|
+
* `git worktree add` walk up to the super-repo's .git and collide with
|
|
52
|
+
* the super-repo branch.
|
|
53
|
+
*/
|
|
54
|
+
isStandaloneRepository(repoPath: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Delete a local branch. With `force=true` deletes even if not fully merged
|
|
57
|
+
* (`-D`); otherwise refuses if the branch has unmerged commits (`-d`).
|
|
58
|
+
* Used by `worktree create`'s rollback to remove the super-repo branch
|
|
59
|
+
* created in step 1 when a later per-entity step fails — without this, every
|
|
60
|
+
* partial failure leaves an orphan `<name>` branch on the super-repo.
|
|
61
|
+
*/
|
|
62
|
+
branchDelete(repoPath: string, branch: string, force?: boolean): Promise<void>;
|
|
43
63
|
pull(repoPath: string, options?: PullOptions): Promise<void>;
|
|
44
64
|
push(repoPath: string, options?: PushOptions): Promise<void>;
|
|
45
65
|
worktreeAdd(repoPath: string, worktreePath: string, branch: string, baseBranch?: string): Promise<void>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
export class GitService {
|
|
4
5
|
async add(repoPath, files) {
|
|
@@ -167,6 +168,30 @@ export class GitService {
|
|
|
167
168
|
return false;
|
|
168
169
|
}
|
|
169
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* True if `repoPath` is its OWN git repo (has a `.git` directory or file
|
|
173
|
+
* at the exact path) — distinct from `isRepository` which walks up via
|
|
174
|
+
* `git rev-parse --git-dir` and would falsely succeed for a plain dir
|
|
175
|
+
* tracked by an ancestor repo.
|
|
176
|
+
*
|
|
177
|
+
* Used by `worktree create` to reject entities that are plain directories
|
|
178
|
+
* in the super-repo (no .git of their own) — those would make per-entity
|
|
179
|
+
* `git worktree add` walk up to the super-repo's .git and collide with
|
|
180
|
+
* the super-repo branch.
|
|
181
|
+
*/
|
|
182
|
+
isStandaloneRepository(repoPath) {
|
|
183
|
+
return fs.existsSync(path.join(repoPath, '.git'));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Delete a local branch. With `force=true` deletes even if not fully merged
|
|
187
|
+
* (`-D`); otherwise refuses if the branch has unmerged commits (`-d`).
|
|
188
|
+
* Used by `worktree create`'s rollback to remove the super-repo branch
|
|
189
|
+
* created in step 1 when a later per-entity step fails — without this, every
|
|
190
|
+
* partial failure leaves an orphan `<name>` branch on the super-repo.
|
|
191
|
+
*/
|
|
192
|
+
async branchDelete(repoPath, branch, force = false) {
|
|
193
|
+
await this.exec(['branch', force ? '-D' : '-d', branch], { cwd: repoPath });
|
|
194
|
+
}
|
|
170
195
|
async pull(repoPath, options) {
|
|
171
196
|
const args = ['pull'];
|
|
172
197
|
if (options?.rebase) {
|
|
@@ -22,6 +22,21 @@ describe('GitService worktree methods', () => {
|
|
|
22
22
|
.rejects.toThrow('Git command failed');
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
|
+
describe('branchDelete', () => {
|
|
26
|
+
it('calls exec with -d when force is false (or omitted)', async () => {
|
|
27
|
+
await gitService.branchDelete('/repo', 'feat/x');
|
|
28
|
+
expect(execSpy).toHaveBeenCalledWith(['branch', '-d', 'feat/x'], { cwd: '/repo' });
|
|
29
|
+
});
|
|
30
|
+
it('calls exec with -D when force is true', async () => {
|
|
31
|
+
await gitService.branchDelete('/repo', 'feat/x', true);
|
|
32
|
+
expect(execSpy).toHaveBeenCalledWith(['branch', '-D', 'feat/x'], { cwd: '/repo' });
|
|
33
|
+
});
|
|
34
|
+
it('rejects when git exits non-zero (e.g. branch is checked out)', async () => {
|
|
35
|
+
execSpy.mockRejectedValue(new Error("Git command failed: error: Cannot delete branch 'feat/x' checked out at '/wt'"));
|
|
36
|
+
await expect(gitService.branchDelete('/repo', 'feat/x', true))
|
|
37
|
+
.rejects.toThrow('Git command failed');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
25
40
|
describe('worktreeList', () => {
|
|
26
41
|
it('parses multi-entry porcelain output', async () => {
|
|
27
42
|
const porcelainOutput = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperdrive.bot/gut",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3-alpha.0",
|
|
4
4
|
"description": "Git Unified Tooling - Enhanced git with workspace intelligence for entity-based organization",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@hyperdrive.bot/cli-auth": "^1.1.1",
|
|
41
|
-
"@hyperdrive.bot/plugin-telemetry": "
|
|
41
|
+
"@hyperdrive.bot/plugin-telemetry": "^0.1.2",
|
|
42
42
|
"@oclif/core": "^4.5.2",
|
|
43
43
|
"@oclif/plugin-help": "^6.2.32",
|
|
44
44
|
"axios": "^1.7.1",
|