@solaqua/gji 0.10.0 → 0.12.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 +83 -426
- package/dist/back.d.ts +3 -0
- package/dist/back.js +19 -7
- package/dist/bootstrap-output.d.ts +3 -0
- package/dist/bootstrap-output.js +27 -0
- package/dist/bootstrap-preview.d.ts +10 -0
- package/dist/bootstrap-preview.js +21 -0
- package/dist/clean.js +27 -3
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +140 -22
- package/dist/clone-failure-store.d.ts +18 -0
- package/dist/clone-failure-store.js +230 -0
- package/dist/command-runner.d.ts +6 -0
- package/dist/command-runner.js +38 -0
- package/dist/completion.js +7 -2
- package/dist/config-command.d.ts +1 -0
- package/dist/config-command.js +7 -1
- package/dist/config.d.ts +26 -1
- package/dist/config.js +110 -6
- package/dist/context-card.d.ts +1 -0
- package/dist/context-card.js +25 -0
- package/dist/dependency-bootstrap-prompt.d.ts +24 -0
- package/dist/dependency-bootstrap-prompt.js +103 -0
- package/dist/dependency-bootstrap.d.ts +129 -0
- package/dist/dependency-bootstrap.js +566 -0
- package/dist/dir-clone.d.ts +33 -0
- package/dist/dir-clone.js +505 -0
- package/dist/doctor.js +21 -0
- package/dist/done.d.ts +10 -0
- package/dist/done.js +210 -0
- package/dist/file-sync.js +71 -22
- package/dist/format-bytes.d.ts +1 -0
- package/dist/format-bytes.js +14 -0
- package/dist/fs-utils.d.ts +4 -0
- package/dist/fs-utils.js +26 -0
- package/dist/git.d.ts +5 -0
- package/dist/git.js +50 -0
- package/dist/gji-bundle.mjs +6189 -1313
- package/dist/go-resolver.d.ts +35 -0
- package/dist/go-resolver.js +185 -0
- package/dist/go.d.ts +6 -2
- package/dist/go.js +270 -62
- package/dist/hooks.d.ts +2 -1
- package/dist/hooks.js +20 -8
- package/dist/hub.d.ts +31 -0
- package/dist/hub.js +287 -0
- package/dist/init.js +30 -22
- package/dist/install-prompt.d.ts +1 -0
- package/dist/install-prompt.js +3 -25
- package/dist/ls.js +9 -1
- package/dist/navigation-output.d.ts +11 -0
- package/dist/navigation-output.js +6 -0
- package/dist/new.d.ts +14 -2
- package/dist/new.js +422 -38
- package/dist/pr-open.js +2 -10
- package/dist/pr.d.ts +6 -2
- package/dist/pr.js +135 -30
- package/dist/pull-requests.d.ts +1 -0
- package/dist/pull-requests.js +32 -3
- package/dist/remove.js +22 -0
- package/dist/repo.d.ts +4 -0
- package/dist/repo.js +61 -6
- package/dist/run-hook.js +2 -0
- package/dist/safe-destination.d.ts +15 -0
- package/dist/safe-destination.js +166 -0
- package/dist/shell-completion.js +52 -13
- package/dist/slots.d.ts +8 -0
- package/dist/slots.js +61 -0
- package/dist/status.d.ts +2 -0
- package/dist/status.js +9 -1
- package/dist/sync-directories.d.ts +33 -0
- package/dist/sync-directories.js +97 -0
- package/dist/sync-plan.d.ts +16 -0
- package/dist/sync-plan.js +128 -0
- package/dist/sync.js +7 -13
- package/dist/task-command.d.ts +9 -0
- package/dist/task-command.js +29 -0
- package/dist/task.d.ts +9 -0
- package/dist/task.js +37 -0
- package/dist/terminal-text.d.ts +4 -0
- package/dist/terminal-text.js +72 -0
- package/dist/undo.d.ts +36 -0
- package/dist/undo.js +392 -0
- package/dist/warp.d.ts +3 -1
- package/dist/warp.js +32 -125
- package/dist/worktree-bootstrap.d.ts +35 -0
- package/dist/worktree-bootstrap.js +96 -0
- package/dist/worktree-info.d.ts +6 -1
- package/dist/worktree-info.js +14 -4
- package/dist/worktree-management.d.ts +1 -0
- package/dist/worktree-management.js +23 -4
- package/dist/worktree-picker.d.ts +24 -11
- package/dist/worktree-picker.js +176 -95
- package/dist/worktree-prompts.js +1 -1
- package/dist/worktree-source.d.ts +6 -0
- package/dist/worktree-source.js +1 -0
- package/dist/worktree-sources.d.ts +4 -0
- package/dist/worktree-sources.js +77 -0
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-doctor.1 +1 -1
- package/man/man1/gji-done.1 +19 -0
- package/man/man1/gji-go.1 +12 -3
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +21 -3
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +7 -1
- package/man/man1/gji-remove.1 +3 -3
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-task.1 +16 -0
- package/man/man1/gji-undo.1 +16 -0
- package/man/man1/gji-warp.1 +3 -6
- package/man/man1/gji.1 +17 -5
- package/package.json +3 -1
package/dist/pr.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
-
import {
|
|
5
|
+
import { createBootstrapReporter } from "./bootstrap-output.js";
|
|
6
|
+
import { createDependencyBootstrapPreview, formatDependencyBootstrapPreview, } from "./bootstrap-preview.js";
|
|
7
|
+
import { loadEffectiveConfigResult, resolveConfigString, } from "./config.js";
|
|
6
8
|
import { pathExists, promptForPathConflict, } from "./conflict.js";
|
|
7
|
-
import {
|
|
9
|
+
import { resolveDependencyBootstrapPolicy, } from "./dependency-bootstrap-prompt.js";
|
|
10
|
+
import { formatBytes } from "./format-bytes.js";
|
|
8
11
|
import { isHeadless } from "./headless.js";
|
|
9
12
|
import { recordWorktreeUsage } from "./history.js";
|
|
10
|
-
import {
|
|
11
|
-
import { maybeRunInstallPrompt, } from "./install-prompt.js";
|
|
13
|
+
import { createNavigationRepository, createNavigationTarget, } from "./navigation-output.js";
|
|
12
14
|
import { detectRepository, resolveWorktreePath } from "./repo.js";
|
|
13
15
|
import { writeShellOutput } from "./shell-handoff.js";
|
|
16
|
+
import { estimateSyncDirectories } from "./sync-plan.js";
|
|
17
|
+
import { bootstrapWorktree } from "./worktree-bootstrap.js";
|
|
14
18
|
const execFileAsync = promisify(execFile);
|
|
15
19
|
const PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
|
|
16
20
|
export function parsePrInput(input) {
|
|
@@ -39,7 +43,23 @@ export function createPrCommand(dependencies = {}) {
|
|
|
39
43
|
return 1;
|
|
40
44
|
}
|
|
41
45
|
const repository = await detectRepository(options.cwd);
|
|
42
|
-
|
|
46
|
+
let config;
|
|
47
|
+
let dependencyBootstrapExplicit;
|
|
48
|
+
try {
|
|
49
|
+
const loaded = await loadEffectiveConfigResult(repository.repoRoot, undefined, options.json ? undefined : options.stderr);
|
|
50
|
+
config = loaded.config;
|
|
51
|
+
dependencyBootstrapExplicit = loaded.dependencyBootstrapExplicit;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
if (options.json) {
|
|
56
|
+
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
options.stderr(`gji pr: ${message}\n`);
|
|
60
|
+
}
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
43
63
|
const branchName = `pr/${prNumber}`;
|
|
44
64
|
const remoteRef = `refs/remotes/origin/pull/${prNumber}/head`;
|
|
45
65
|
const rawBasePath = resolveConfigString(config, "worktreePath");
|
|
@@ -55,25 +75,66 @@ export function createPrCommand(dependencies = {}) {
|
|
|
55
75
|
}
|
|
56
76
|
else {
|
|
57
77
|
options.stderr(`gji pr: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
|
|
58
|
-
options.stderr(`Hint: Use 'gji
|
|
78
|
+
options.stderr(`Hint: Use 'gji done pr/${prNumber}' or 'gji clean' to remove the existing worktree\n`);
|
|
59
79
|
}
|
|
60
80
|
return 1;
|
|
61
81
|
}
|
|
62
82
|
const choice = await prompt(worktreePath);
|
|
63
83
|
if (choice === "reuse") {
|
|
64
84
|
await recordWorktreeUsage(worktreePath, branchName);
|
|
65
|
-
await writeOutput(worktreePath, options.stdout);
|
|
85
|
+
await writeOutput(worktreePath, options.stdout, options.outputEnv);
|
|
66
86
|
return 0;
|
|
67
87
|
}
|
|
68
88
|
options.stderr(`Aborted because target worktree path already exists: ${worktreePath}\n`);
|
|
69
89
|
return 1;
|
|
70
90
|
}
|
|
91
|
+
let dependencyPolicy = {
|
|
92
|
+
mode: config.dependencyBootstrap ?? "off",
|
|
93
|
+
prompted: false,
|
|
94
|
+
source: dependencyBootstrapExplicit ? "explicit" : "default",
|
|
95
|
+
};
|
|
96
|
+
if (options.dryRun) {
|
|
97
|
+
dependencyPolicy = await resolveDependencyBootstrapPolicy({
|
|
98
|
+
currentRoot: repository.currentRoot,
|
|
99
|
+
repoRoot: repository.repoRoot,
|
|
100
|
+
worktreePath,
|
|
101
|
+
}, config, dependencyBootstrapExplicit, {
|
|
102
|
+
dependencies,
|
|
103
|
+
dryRun: true,
|
|
104
|
+
legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== undefined,
|
|
105
|
+
nonInteractive: !!options.json,
|
|
106
|
+
stderr: options.stderr,
|
|
107
|
+
});
|
|
108
|
+
config = {
|
|
109
|
+
...config,
|
|
110
|
+
dependencyBootstrap: dependencyPolicy.mode,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const dryRunSyncDirs = options.dryRun
|
|
114
|
+
? await estimateSyncDirectories(repository.repoRoot, worktreePath, config.syncDirs ?? [])
|
|
115
|
+
: [];
|
|
116
|
+
const dryRunDependencyBootstrap = options.dryRun
|
|
117
|
+
? await createDependencyBootstrapPreview(config.dependencyBootstrap ?? "off", {
|
|
118
|
+
currentRoot: repository.currentRoot,
|
|
119
|
+
repoRoot: repository.repoRoot,
|
|
120
|
+
cargoBuildCommand: config.dependencyBuildCommand,
|
|
121
|
+
worktreePath,
|
|
122
|
+
})
|
|
123
|
+
: undefined;
|
|
71
124
|
if (options.dryRun) {
|
|
72
125
|
if (options.json) {
|
|
73
|
-
|
|
126
|
+
const output = {
|
|
127
|
+
...createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), worktreePath, branchName),
|
|
128
|
+
dryRun: true,
|
|
129
|
+
};
|
|
130
|
+
if (dryRunSyncDirs.length > 0)
|
|
131
|
+
output.syncDirs = dryRunSyncDirs;
|
|
132
|
+
if (dryRunDependencyBootstrap?.targets.length)
|
|
133
|
+
output.dependencyBootstrap = dryRunDependencyBootstrap;
|
|
134
|
+
options.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
74
135
|
}
|
|
75
136
|
else {
|
|
76
|
-
options.stdout(`Would create worktree at ${worktreePath} (branch: ${branchName})\n`);
|
|
137
|
+
options.stdout(`Would create worktree at ${worktreePath} (branch: ${branchName})\n${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(bytes)})\n`).join("")}${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`);
|
|
77
138
|
}
|
|
78
139
|
return 0;
|
|
79
140
|
}
|
|
@@ -97,31 +158,75 @@ export function createPrCommand(dependencies = {}) {
|
|
|
97
158
|
? ["worktree", "add", worktreePath, branchName]
|
|
98
159
|
: ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
|
|
99
160
|
await execFileAsync("git", worktreeArgs, { cwd: repository.repoRoot });
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
161
|
+
if (!dependencyBootstrapExplicit) {
|
|
162
|
+
dependencyPolicy = await resolveDependencyBootstrapPolicy({
|
|
163
|
+
currentRoot: repository.currentRoot,
|
|
164
|
+
detectionRoot: worktreePath,
|
|
165
|
+
repoRoot: repository.repoRoot,
|
|
166
|
+
worktreePath,
|
|
167
|
+
}, config, false, {
|
|
168
|
+
dependencies,
|
|
169
|
+
legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== undefined,
|
|
170
|
+
nonInteractive: !!options.json,
|
|
171
|
+
stderr: options.stderr,
|
|
172
|
+
});
|
|
173
|
+
config = {
|
|
174
|
+
...config,
|
|
175
|
+
dependencyBootstrap: dependencyPolicy.mode,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const bootstrap = await bootstrapWorktree({
|
|
179
|
+
branch: branchName,
|
|
180
|
+
cloneDirectory: dependencies.cloneDir,
|
|
181
|
+
config,
|
|
182
|
+
currentRoot: repository.currentRoot,
|
|
183
|
+
dependencyDetectionRoot: worktreePath,
|
|
184
|
+
nonInteractive: !!options.json,
|
|
185
|
+
repoRoot: repository.repoRoot,
|
|
186
|
+
reporter: createBootstrapReporter(options.stderr, !!options.json),
|
|
187
|
+
runCommand: dependencies.runInstallCommand,
|
|
188
|
+
commandStdout: options.json ? () => undefined : options.stdout,
|
|
189
|
+
commandStderr: options.json ? () => undefined : options.stderr,
|
|
190
|
+
json: options.json,
|
|
191
|
+
worktreePath,
|
|
192
|
+
installDependencies: dependencies,
|
|
193
|
+
dependencyBootstrapPolicy: dependencyPolicy,
|
|
194
|
+
});
|
|
195
|
+
if (!bootstrap.ready) {
|
|
196
|
+
const details = {
|
|
197
|
+
dependencyBootstrap: bootstrap.dependencyBootstrap,
|
|
198
|
+
path: worktreePath,
|
|
199
|
+
skipped: bootstrap.skippedDirs,
|
|
200
|
+
syncFiles: bootstrap.syncFileFailures,
|
|
201
|
+
};
|
|
202
|
+
if (options.json) {
|
|
203
|
+
options.stderr(`${JSON.stringify({ error: "worktree bootstrap failed", ...details }, null, 2)}\n`);
|
|
107
204
|
}
|
|
108
|
-
|
|
109
|
-
options.stderr(`
|
|
205
|
+
else {
|
|
206
|
+
options.stderr(`gji pr: worktree bootstrap failed at ${worktreePath}\n`);
|
|
207
|
+
options.stderr(`Hint: inspect the worktree or remove it with 'gji done ${worktreePath}' before retrying\n`);
|
|
110
208
|
}
|
|
209
|
+
return 1;
|
|
111
210
|
}
|
|
112
|
-
await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
|
|
113
|
-
const hooks = extractHooks(config);
|
|
114
|
-
await runHook(hooks["after-create"], worktreePath, {
|
|
115
|
-
branch: branchName,
|
|
116
|
-
path: worktreePath,
|
|
117
|
-
repo: basename(repository.repoRoot),
|
|
118
|
-
}, options.stderr);
|
|
119
211
|
if (options.json) {
|
|
120
|
-
|
|
212
|
+
const output = {
|
|
213
|
+
...createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), worktreePath, branchName),
|
|
214
|
+
};
|
|
215
|
+
if (bootstrap.clonedDirs.length > 0) {
|
|
216
|
+
output.cloned = bootstrap.clonedDirs.map(({ dir, ms }) => ({
|
|
217
|
+
dir,
|
|
218
|
+
ms,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
if (bootstrap.skippedDirs.length > 0)
|
|
222
|
+
output.skipped = bootstrap.skippedDirs;
|
|
223
|
+
if (bootstrap.dependencyBootstrap.mode !== "off")
|
|
224
|
+
output.dependencyBootstrap = bootstrap.dependencyBootstrap;
|
|
225
|
+
options.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
121
226
|
}
|
|
122
227
|
else {
|
|
123
228
|
await recordWorktreeUsage(worktreePath, branchName);
|
|
124
|
-
await writeOutput(worktreePath, options.stdout);
|
|
229
|
+
await writeOutput(worktreePath, options.stdout, options.outputEnv);
|
|
125
230
|
}
|
|
126
231
|
return 0;
|
|
127
232
|
};
|
|
@@ -185,6 +290,6 @@ function sourceRefForForge(forge, prNumber) {
|
|
|
185
290
|
return `refs/merge-requests/${prNumber}/head`;
|
|
186
291
|
}
|
|
187
292
|
}
|
|
188
|
-
async function writeOutput(worktreePath, stdout) {
|
|
189
|
-
await writeShellOutput(PR_OUTPUT_FILE_ENV, worktreePath, stdout);
|
|
293
|
+
async function writeOutput(worktreePath, stdout, outputEnv) {
|
|
294
|
+
await writeShellOutput(outputEnv ?? PR_OUTPUT_FILE_ENV, worktreePath, stdout);
|
|
190
295
|
}
|
package/dist/pull-requests.d.ts
CHANGED
package/dist/pull-requests.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
const execFileAsync = promisify(execFile);
|
|
4
4
|
const PULL_REQUEST_QUERY_TIMEOUT_MS = 2500;
|
|
5
|
+
const PULL_REQUEST_CACHE_TTL_MS = 30_000;
|
|
5
6
|
export function parsePullRequestRemote(remoteUrl) {
|
|
6
7
|
const parsed = parseRemoteUrl(remoteUrl.trim());
|
|
7
8
|
if (parsed === null)
|
|
@@ -46,10 +47,36 @@ export function parsePullRequestRemote(remoteUrl) {
|
|
|
46
47
|
export function createPullRequestQuery(dependencies = {}) {
|
|
47
48
|
const runCommand = dependencies.runCommand ?? defaultRunCommand;
|
|
48
49
|
const fetcher = dependencies.fetch ?? fetch;
|
|
50
|
+
const repositoryCache = new Map();
|
|
51
|
+
const repositoryInFlight = new Map();
|
|
52
|
+
const listRepositoryPullRequests = (repoRoot) => {
|
|
53
|
+
const cached = repositoryCache.get(repoRoot);
|
|
54
|
+
if (cached !== undefined && cached.expiresAt > Date.now()) {
|
|
55
|
+
return Promise.resolve(cached.value);
|
|
56
|
+
}
|
|
57
|
+
const inFlight = repositoryInFlight.get(repoRoot);
|
|
58
|
+
if (inFlight !== undefined)
|
|
59
|
+
return inFlight;
|
|
60
|
+
const request = listOpenPullRequestsForRepository(repoRoot, runCommand, fetcher)
|
|
61
|
+
.then((value) => {
|
|
62
|
+
repositoryCache.set(repoRoot, {
|
|
63
|
+
expiresAt: Date.now() + PULL_REQUEST_CACHE_TTL_MS,
|
|
64
|
+
value,
|
|
65
|
+
});
|
|
66
|
+
repositoryInFlight.delete(repoRoot);
|
|
67
|
+
return value;
|
|
68
|
+
})
|
|
69
|
+
.catch((error) => {
|
|
70
|
+
repositoryInFlight.delete(repoRoot);
|
|
71
|
+
throw error;
|
|
72
|
+
});
|
|
73
|
+
repositoryInFlight.set(repoRoot, request);
|
|
74
|
+
return request;
|
|
75
|
+
};
|
|
49
76
|
return {
|
|
50
77
|
findOpenPullRequest: (repoRoot, number) => findOpenPullRequest(repoRoot, number, runCommand, fetcher),
|
|
51
78
|
listOpenPullRequests: (repoRoot, sourceBranch) => listOpenPullRequests(repoRoot, sourceBranch, runCommand, fetcher),
|
|
52
|
-
listOpenPullRequestsForRepository:
|
|
79
|
+
listOpenPullRequestsForRepository: listRepositoryPullRequests,
|
|
53
80
|
};
|
|
54
81
|
}
|
|
55
82
|
async function listOpenPullRequests(repoRoot, sourceBranch, runCommand, fetcher) {
|
|
@@ -153,7 +180,7 @@ function providerCliCommand(remote, query) {
|
|
|
153
180
|
"view",
|
|
154
181
|
String(query.number),
|
|
155
182
|
"--json",
|
|
156
|
-
"number,url,headRefName,state",
|
|
183
|
+
"number,url,headRefName,title,state",
|
|
157
184
|
"--repo",
|
|
158
185
|
coordinate,
|
|
159
186
|
],
|
|
@@ -168,7 +195,7 @@ function providerCliCommand(remote, query) {
|
|
|
168
195
|
"open",
|
|
169
196
|
...(query.kind === "branch" ? ["--head", sourceBranch] : []),
|
|
170
197
|
"--json",
|
|
171
|
-
"number,url,headRefName,state",
|
|
198
|
+
"number,url,headRefName,title,state",
|
|
172
199
|
"--limit",
|
|
173
200
|
"100",
|
|
174
201
|
"--repo",
|
|
@@ -302,6 +329,7 @@ function normalizePullRequest(value, query) {
|
|
|
302
329
|
(isRecord(value.links) && isRecord(value.links.html)
|
|
303
330
|
? value.links.html.href
|
|
304
331
|
: undefined));
|
|
332
|
+
const title = stringValue(value.title);
|
|
305
333
|
const state = stringValue(value.state)?.toLowerCase();
|
|
306
334
|
if (number === null || url === null)
|
|
307
335
|
return null;
|
|
@@ -314,6 +342,7 @@ function normalizePullRequest(value, query) {
|
|
|
314
342
|
return {
|
|
315
343
|
number,
|
|
316
344
|
sourceBranch: sourceBranch ?? "",
|
|
345
|
+
...(title === null ? {} : { title }),
|
|
317
346
|
url,
|
|
318
347
|
};
|
|
319
348
|
}
|
package/dist/remove.js
CHANGED
|
@@ -4,6 +4,8 @@ import { loadEffectiveConfig } from "./config.js";
|
|
|
4
4
|
import { isHeadless } from "./headless.js";
|
|
5
5
|
import { extractHooks, runHook } from "./hooks.js";
|
|
6
6
|
import { writeShellOutput } from "./shell-handoff.js";
|
|
7
|
+
import { releaseWorktreeSlot } from "./slots.js";
|
|
8
|
+
import { finalizeUndoOperation, recordUndoOperation } from "./undo.js";
|
|
7
9
|
import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeForceRemovalError, loadLinkedWorktrees, removeWorktree, } from "./worktree-management.js";
|
|
8
10
|
import { buildWorktreePromptEntries, promptForSingleWorktree, } from "./worktree-picker.js";
|
|
9
11
|
import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree, } from "./worktree-prompts.js";
|
|
@@ -74,6 +76,20 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
74
76
|
}
|
|
75
77
|
const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
|
|
76
78
|
const hooks = extractHooks(config);
|
|
79
|
+
let journal;
|
|
80
|
+
try {
|
|
81
|
+
journal = await recordUndoOperation("remove", repository.repoRoot, [
|
|
82
|
+
worktree,
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
emitError(options, `could not write undo journal; no worktree was removed: ${toMessage(error)}`);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
if (!journal) {
|
|
90
|
+
emitError(options, "could not capture undo state; no worktree was removed");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
77
93
|
await runHook(hooks["before-remove"], worktree.path, {
|
|
78
94
|
branch: worktree.branch ?? undefined,
|
|
79
95
|
path: worktree.path,
|
|
@@ -84,10 +100,12 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
84
100
|
}
|
|
85
101
|
catch (error) {
|
|
86
102
|
if (!isWorktreeForceRemovalError(error)) {
|
|
103
|
+
await finalizeUndoOperation(journal, []);
|
|
87
104
|
throw error;
|
|
88
105
|
}
|
|
89
106
|
if (!options.force &&
|
|
90
107
|
!(await confirmForceRemoveWorktree(worktree.path))) {
|
|
108
|
+
await finalizeUndoOperation(journal, []);
|
|
91
109
|
options.stderr("Aborted\n");
|
|
92
110
|
return 1;
|
|
93
111
|
}
|
|
@@ -95,10 +113,13 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
95
113
|
await forceRemoveWorktree(repository.repoRoot, worktree.path);
|
|
96
114
|
}
|
|
97
115
|
catch (forceError) {
|
|
116
|
+
await finalizeUndoOperation(journal, []);
|
|
98
117
|
emitError(options, `Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}`);
|
|
99
118
|
return 1;
|
|
100
119
|
}
|
|
101
120
|
}
|
|
121
|
+
await finalizeUndoOperation(journal, [worktree]);
|
|
122
|
+
await releaseWorktreeSlot(worktree.path);
|
|
102
123
|
if (worktree.branch) {
|
|
103
124
|
try {
|
|
104
125
|
await deleteBranch(repository.repoRoot, worktree.branch);
|
|
@@ -125,6 +146,7 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
125
146
|
options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}\n`);
|
|
126
147
|
}
|
|
127
148
|
else {
|
|
149
|
+
options.stderr("undo: gji undo\n");
|
|
128
150
|
await writeOutput(repository.repoRoot, options.stdout);
|
|
129
151
|
}
|
|
130
152
|
return 0;
|
package/dist/repo.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export interface WorktreeEntry {
|
|
|
11
11
|
path: string;
|
|
12
12
|
}
|
|
13
13
|
export declare function detectRepository(cwd: string): Promise<RepositoryContext>;
|
|
14
|
+
export declare function worktreeGitDir(worktreePath: string): Promise<string>;
|
|
14
15
|
export declare function resolveWorktreePath(repoRoot: string, branch: string, basePath?: string): string;
|
|
15
16
|
export declare function validateBranchName(name: string): string | null;
|
|
17
|
+
export declare function hasLocalBranch(repoRoot: string, branch: string): Promise<boolean>;
|
|
18
|
+
export declare function hasRemoteBranch(repoRoot: string, remote: string, branch: string): Promise<boolean>;
|
|
19
|
+
export declare function getRepositoryRemoteUrl(repoRoot: string, remote: string): Promise<string | null>;
|
|
16
20
|
export declare function listWorktrees(cwd: string): Promise<WorktreeEntry[]>;
|
package/dist/repo.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
4
|
import { runGit } from "./git.js";
|
|
@@ -16,6 +17,10 @@ export async function detectRepository(cwd) {
|
|
|
16
17
|
repoRoot,
|
|
17
18
|
};
|
|
18
19
|
}
|
|
20
|
+
export async function worktreeGitDir(worktreePath) {
|
|
21
|
+
const rawGitDir = await runGit(worktreePath, ["rev-parse", "--git-dir"]);
|
|
22
|
+
return isAbsolute(rawGitDir) ? rawGitDir : resolve(worktreePath, rawGitDir);
|
|
23
|
+
}
|
|
19
24
|
export function resolveWorktreePath(repoRoot, branch, basePath) {
|
|
20
25
|
const segments = branch.split("/").filter(Boolean);
|
|
21
26
|
if (segments.length === 0) {
|
|
@@ -65,6 +70,42 @@ export function validateBranchName(name) {
|
|
|
65
70
|
}
|
|
66
71
|
return null;
|
|
67
72
|
}
|
|
73
|
+
export async function hasLocalBranch(repoRoot, branch) {
|
|
74
|
+
try {
|
|
75
|
+
await runGit(repoRoot, [
|
|
76
|
+
"show-ref",
|
|
77
|
+
"--verify",
|
|
78
|
+
"--quiet",
|
|
79
|
+
`refs/heads/${branch}`,
|
|
80
|
+
]);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export async function hasRemoteBranch(repoRoot, remote, branch) {
|
|
88
|
+
try {
|
|
89
|
+
await runGit(repoRoot, [
|
|
90
|
+
"show-ref",
|
|
91
|
+
"--verify",
|
|
92
|
+
"--quiet",
|
|
93
|
+
`refs/remotes/${remote}/${branch}`,
|
|
94
|
+
]);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export async function getRepositoryRemoteUrl(repoRoot, remote) {
|
|
102
|
+
try {
|
|
103
|
+
return await runGit(repoRoot, ["remote", "get-url", remote]);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
68
109
|
function expandTildeInPath(p) {
|
|
69
110
|
if (p === "~")
|
|
70
111
|
return homedir();
|
|
@@ -78,15 +119,29 @@ export async function listWorktrees(cwd) {
|
|
|
78
119
|
runGit(cwd, ["rev-parse", "--show-toplevel"]),
|
|
79
120
|
]);
|
|
80
121
|
const entries = output.split("\n\n").filter(Boolean);
|
|
81
|
-
|
|
122
|
+
const worktrees = entries.flatMap((entry) => {
|
|
123
|
+
if (findOptionalPorcelainValue(entry, "prunable") !== null)
|
|
124
|
+
return [];
|
|
82
125
|
const path = findPorcelainValue(entry, "worktree");
|
|
83
126
|
const branchRef = findOptionalPorcelainValue(entry, "branch");
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
branch: branchRef ? branchRef.replace("refs/heads/", "") : null,
|
|
130
|
+
isCurrent: path === currentRoot,
|
|
131
|
+
path,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
89
134
|
});
|
|
135
|
+
const accessible = await Promise.all(worktrees.map(async (worktree) => {
|
|
136
|
+
try {
|
|
137
|
+
await access(worktree.path);
|
|
138
|
+
return worktree;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
144
|
+
return accessible.filter((worktree) => worktree !== null);
|
|
90
145
|
}
|
|
91
146
|
function findPorcelainValue(block, key) {
|
|
92
147
|
const value = findOptionalPorcelainValue(block, key);
|
package/dist/run-hook.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadEffectiveConfig } from "./config.js";
|
|
2
2
|
import { extractHooks, runHook } from "./hooks.js";
|
|
3
3
|
import { detectRepository, listWorktrees } from "./repo.js";
|
|
4
|
+
import { getWorktreeSlot } from "./slots.js";
|
|
4
5
|
const VALID_HOOKS = [
|
|
5
6
|
"after-create",
|
|
6
7
|
"after-enter",
|
|
@@ -31,6 +32,7 @@ export async function runHookCommand(options) {
|
|
|
31
32
|
branch: currentWorktree?.branch ?? undefined,
|
|
32
33
|
path: repository.currentRoot,
|
|
33
34
|
repo: repository.repoName,
|
|
35
|
+
slot: await getWorktreeSlot(repository.currentRoot),
|
|
34
36
|
}, options.stderr);
|
|
35
37
|
return 0;
|
|
36
38
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type DestinationInspection = {
|
|
2
|
+
kind: "missing";
|
|
3
|
+
} | {
|
|
4
|
+
kind: "exists";
|
|
5
|
+
} | {
|
|
6
|
+
kind: "unsafe";
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
export interface OpenDestinationDirectory {
|
|
10
|
+
path: string;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function openDestinationDirectory(root: string, path: string): Promise<OpenDestinationDirectory>;
|
|
14
|
+
export declare function inspectDestination(root: string, path: string): Promise<DestinationInspection>;
|
|
15
|
+
export declare function ensureDestinationDirectory(root: string, path: string): Promise<void>;
|