@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/new.js
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import {
|
|
2
|
+
import { access, mkdir } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
-
import { isCancel, text } from "@clack/prompts";
|
|
6
|
-
import {
|
|
5
|
+
import { confirm, isCancel, text } from "@clack/prompts";
|
|
6
|
+
import { createBootstrapReporter } from "./bootstrap-output.js";
|
|
7
|
+
import { createDependencyBootstrapPreview, formatDependencyBootstrapPreview, } from "./bootstrap-preview.js";
|
|
8
|
+
import { loadEffectiveConfigResult, resolveConfigString, } from "./config.js";
|
|
7
9
|
import { pathExists, promptForPathConflict, } from "./conflict.js";
|
|
10
|
+
import { resolveDependencyBootstrapPolicy, } from "./dependency-bootstrap-prompt.js";
|
|
11
|
+
import { cloneDir } from "./dir-clone.js";
|
|
8
12
|
import { defaultSpawnEditor, EDITORS } from "./editor.js";
|
|
9
|
-
import {
|
|
13
|
+
import { formatBytes } from "./format-bytes.js";
|
|
14
|
+
import { resolveRemoteBase, runGit } from "./git.js";
|
|
10
15
|
import { isHeadless } from "./headless.js";
|
|
11
16
|
import { recordWorktreeUsage } from "./history.js";
|
|
12
|
-
import {
|
|
13
|
-
import { maybeRunInstallPrompt, } from "./install-prompt.js";
|
|
17
|
+
import { createNavigationRepository, createNavigationTarget, } from "./navigation-output.js";
|
|
14
18
|
import { detectRepository, resolveWorktreePath, validateBranchName, } from "./repo.js";
|
|
15
19
|
import { writeShellOutput } from "./shell-handoff.js";
|
|
20
|
+
import { estimateSyncDirectories } from "./sync-plan.js";
|
|
21
|
+
import { writeTask } from "./task.js";
|
|
22
|
+
import { bootstrapWorktree } from "./worktree-bootstrap.js";
|
|
16
23
|
const execFileAsync = promisify(execFile);
|
|
17
24
|
const NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
|
|
18
25
|
export function createNewCommand(dependencies = {}) {
|
|
19
26
|
const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
|
|
27
|
+
const cloneDirectory = dependencies.cloneDir ?? cloneDir;
|
|
20
28
|
const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
|
|
29
|
+
const promptForFetchFailure = dependencies.promptForFetchFailure ?? defaultPromptForFetchFailure;
|
|
21
30
|
const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
|
|
22
31
|
const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
|
|
23
32
|
return async function runNewCommand(options) {
|
|
33
|
+
if (options.copy && !options.take)
|
|
34
|
+
return emitNewError(options, "--copy requires --take");
|
|
24
35
|
if (options.detached && options.fromCurrent) {
|
|
25
36
|
const message = "--from-current cannot be used with --detached";
|
|
26
37
|
if (options.json) {
|
|
@@ -32,7 +43,16 @@ export function createNewCommand(dependencies = {}) {
|
|
|
32
43
|
return 1;
|
|
33
44
|
}
|
|
34
45
|
const repository = await detectRepository(options.cwd);
|
|
35
|
-
|
|
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
|
+
return emitNewError(options, error instanceof Error ? error.message : String(error));
|
|
55
|
+
}
|
|
36
56
|
const usesGeneratedDetachedName = options.detached && options.branch === undefined;
|
|
37
57
|
if (options.editor && !options.open) {
|
|
38
58
|
options.stderr("gji new: --editor has no effect without --open\n");
|
|
@@ -77,9 +97,11 @@ export function createNewCommand(dependencies = {}) {
|
|
|
77
97
|
const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~")
|
|
78
98
|
? rawBasePath
|
|
79
99
|
: undefined;
|
|
80
|
-
const worktreeName = options.
|
|
100
|
+
const worktreeName = options.mode
|
|
81
101
|
? rawBranch
|
|
82
|
-
:
|
|
102
|
+
: options.detached
|
|
103
|
+
? rawBranch
|
|
104
|
+
: applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
|
|
83
105
|
const worktreePath = usesGeneratedDetachedName
|
|
84
106
|
? await resolveUniqueDetachedWorktreePath(repository.repoRoot, worktreeName, configuredBasePath)
|
|
85
107
|
: resolveWorktreePath(repository.repoRoot, worktreeName, configuredBasePath);
|
|
@@ -119,7 +141,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
119
141
|
}
|
|
120
142
|
else {
|
|
121
143
|
options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
|
|
122
|
-
options.stderr(`Hint: Use 'gji
|
|
144
|
+
options.stderr(`Hint: Use 'gji done ${worktreeName}' or 'gji clean' to remove the existing worktree\n`);
|
|
123
145
|
options.stderr(`Hint: Use 'gji run-hook after-create' inside the worktree to re-run setup hooks\n`);
|
|
124
146
|
}
|
|
125
147
|
return 1;
|
|
@@ -128,16 +150,64 @@ export function createNewCommand(dependencies = {}) {
|
|
|
128
150
|
const choice = await prompt(worktreePath);
|
|
129
151
|
if (choice === "reuse") {
|
|
130
152
|
await recordWorktreeUsage(worktreePath, worktreeName);
|
|
131
|
-
await writeOutput(worktreePath, options.stdout);
|
|
153
|
+
await writeOutput(worktreePath, options.stdout, options.outputEnv);
|
|
132
154
|
return 0;
|
|
133
155
|
}
|
|
134
156
|
options.stderr(`Aborted because target worktree path already exists: ${worktreePath}\n`);
|
|
135
157
|
return 1;
|
|
136
158
|
}
|
|
137
159
|
}
|
|
160
|
+
const dependencyPolicy = await resolveDependencyBootstrapPolicy({
|
|
161
|
+
currentRoot: repository.currentRoot,
|
|
162
|
+
repoRoot: repository.repoRoot,
|
|
163
|
+
worktreePath,
|
|
164
|
+
}, config, dependencyBootstrapExplicit, {
|
|
165
|
+
dependencies,
|
|
166
|
+
dryRun: options.dryRun,
|
|
167
|
+
legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== undefined,
|
|
168
|
+
nonInteractive: !!options.json,
|
|
169
|
+
stderr: options.stderr,
|
|
170
|
+
});
|
|
171
|
+
config = {
|
|
172
|
+
...config,
|
|
173
|
+
dependencyBootstrap: dependencyPolicy.mode,
|
|
174
|
+
};
|
|
138
175
|
if (options.dryRun) {
|
|
176
|
+
if (options.take) {
|
|
177
|
+
const changedFiles = await listTakeFiles(options.cwd);
|
|
178
|
+
const submoduleFiles = await listSubmoduleFiles(options.cwd, changedFiles);
|
|
179
|
+
const transferableFiles = changedFiles.filter((file) => !submoduleFiles.includes(file));
|
|
180
|
+
const ignoredFiles = await listIgnoredFiles(options.cwd);
|
|
181
|
+
if (transferableFiles.length === 0)
|
|
182
|
+
return emitNewError(options, submoduleFiles.length > 0
|
|
183
|
+
? "nothing to take: submodule changes are not supported"
|
|
184
|
+
: ignoredFiles.length > 0
|
|
185
|
+
? "nothing to take: ignored files are not moved"
|
|
186
|
+
: "nothing to take: working tree is clean");
|
|
187
|
+
if (options.json)
|
|
188
|
+
options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true, take: transferableFiles, ignored: ignoredFiles, submodules: submoduleFiles }, null, 2)}\n`);
|
|
189
|
+
else {
|
|
190
|
+
options.stdout(`Would take ${transferableFiles.length} changed file${transferableFiles.length === 1 ? "" : "s"} into ${worktreeName}${ignoredFiles.length > 0 ? ` (ignored files not moved: ${ignoredFiles.length})` : ""}${submoduleFiles.length > 0 ? ` (submodules not moved: ${submoduleFiles.length})` : ""}\n${transferableFiles.map((file) => ` ${file}\n`).join("")}`);
|
|
191
|
+
}
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
const dryRunSyncDirs = await estimateSyncDirectories(repository.repoRoot, worktreePath, config.syncDirs ?? []);
|
|
195
|
+
const dryRunDependencyBootstrap = await createDependencyBootstrapPreview(config.dependencyBootstrap ?? "off", {
|
|
196
|
+
currentRoot: repository.currentRoot,
|
|
197
|
+
repoRoot: repository.repoRoot,
|
|
198
|
+
cargoBuildCommand: config.dependencyBuildCommand,
|
|
199
|
+
worktreePath,
|
|
200
|
+
});
|
|
139
201
|
if (options.json) {
|
|
140
|
-
|
|
202
|
+
const output = {
|
|
203
|
+
...createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), worktreePath, worktreeName),
|
|
204
|
+
dryRun: true,
|
|
205
|
+
};
|
|
206
|
+
if (dryRunSyncDirs.length > 0)
|
|
207
|
+
output.syncDirs = dryRunSyncDirs;
|
|
208
|
+
if (dryRunDependencyBootstrap.targets.length > 0)
|
|
209
|
+
output.dependencyBootstrap = dryRunDependencyBootstrap;
|
|
210
|
+
options.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
141
211
|
}
|
|
142
212
|
else {
|
|
143
213
|
const resolvedEditor = options.open
|
|
@@ -146,7 +216,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
146
216
|
const openNote = resolvedEditor
|
|
147
217
|
? `, then open in ${resolvedEditor}`
|
|
148
218
|
: "";
|
|
149
|
-
options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})\n`);
|
|
219
|
+
options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})\n${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(bytes)})\n`).join("")}${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`);
|
|
150
220
|
}
|
|
151
221
|
return 0;
|
|
152
222
|
}
|
|
@@ -154,44 +224,163 @@ export function createNewCommand(dependencies = {}) {
|
|
|
154
224
|
const startPoint = options.fromCurrent && !options.detached
|
|
155
225
|
? await resolveCurrentWorktreeHead(options.cwd)
|
|
156
226
|
: undefined;
|
|
227
|
+
const takeStartPoint = options.take
|
|
228
|
+
? await resolveCurrentWorktreeHead(options.cwd)
|
|
229
|
+
: undefined;
|
|
230
|
+
const changedTakeFiles = options.take
|
|
231
|
+
? await listTakeFiles(options.cwd)
|
|
232
|
+
: [];
|
|
233
|
+
const submoduleFiles = options.take
|
|
234
|
+
? await listSubmoduleFiles(options.cwd, changedTakeFiles)
|
|
235
|
+
: [];
|
|
236
|
+
const takeFiles = changedTakeFiles.filter((file) => !submoduleFiles.includes(file));
|
|
237
|
+
const takeUntracked = options.take
|
|
238
|
+
? await countUntrackedFiles(options.cwd)
|
|
239
|
+
: 0;
|
|
240
|
+
if (options.take && takeFiles.length === 0)
|
|
241
|
+
return emitNewError(options, submoduleFiles.length > 0
|
|
242
|
+
? "nothing to take: submodule changes are not supported"
|
|
243
|
+
: "nothing to take: working tree is clean");
|
|
244
|
+
if (options.take && submoduleFiles.length > 0)
|
|
245
|
+
options.stderr(`Warning: submodule changes are not transferred: ${submoduleFiles.join(", ")}\n`);
|
|
246
|
+
const existingLocalBranch = await localBranchExists(repository.repoRoot, worktreeName);
|
|
247
|
+
const shouldRefreshBase = !options.noFetch &&
|
|
248
|
+
!options.detached &&
|
|
249
|
+
!options.fromCurrent &&
|
|
250
|
+
!options.take &&
|
|
251
|
+
options.mode !== "track" &&
|
|
252
|
+
options.mode !== "checkout" &&
|
|
253
|
+
!existingLocalBranch;
|
|
254
|
+
const freshBaseRef = shouldRefreshBase
|
|
255
|
+
? await resolveFreshBaseRef(repository.repoRoot, config, options, promptForFetchFailure)
|
|
256
|
+
: undefined;
|
|
257
|
+
if (freshBaseRef === null)
|
|
258
|
+
return 1;
|
|
259
|
+
let stashSha = null;
|
|
260
|
+
if (options.take) {
|
|
261
|
+
try {
|
|
262
|
+
const inProgressState = await detectInProgressGitState(options.cwd);
|
|
263
|
+
if (inProgressState)
|
|
264
|
+
return emitNewError(options, `cannot take changes while Git is in progress (${inProgressState})`);
|
|
265
|
+
stashSha = await createTakeStash(options.cwd, worktreeName);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
return emitNewError(options, `could not stash changes safely: ${toExecMessage(error)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
157
271
|
const gitArgs = options.detached
|
|
158
|
-
? [
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
272
|
+
? [
|
|
273
|
+
"worktree",
|
|
274
|
+
"add",
|
|
275
|
+
"--detach",
|
|
276
|
+
worktreePath,
|
|
277
|
+
...(takeStartPoint ? [takeStartPoint] : []),
|
|
278
|
+
]
|
|
279
|
+
: options.mode === "track"
|
|
280
|
+
? [
|
|
162
281
|
"worktree",
|
|
163
282
|
"add",
|
|
283
|
+
"--track",
|
|
164
284
|
"-b",
|
|
165
285
|
worktreeName,
|
|
166
286
|
worktreePath,
|
|
167
|
-
|
|
168
|
-
]
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
287
|
+
`${options.remote ?? "origin"}/${worktreeName}`,
|
|
288
|
+
]
|
|
289
|
+
: options.mode === "checkout" || existingLocalBranch
|
|
290
|
+
? ["worktree", "add", worktreePath, worktreeName]
|
|
291
|
+
: [
|
|
292
|
+
"worktree",
|
|
293
|
+
"add",
|
|
294
|
+
"-b",
|
|
295
|
+
worktreeName,
|
|
296
|
+
worktreePath,
|
|
297
|
+
...(takeStartPoint
|
|
298
|
+
? [takeStartPoint]
|
|
299
|
+
: startPoint
|
|
300
|
+
? [startPoint]
|
|
301
|
+
: freshBaseRef
|
|
302
|
+
? [freshBaseRef]
|
|
303
|
+
: []),
|
|
304
|
+
];
|
|
305
|
+
try {
|
|
306
|
+
await execFileAsync("git", gitArgs, { cwd: repository.repoRoot });
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
if (stashSha)
|
|
310
|
+
await restoreTakeStash(options.cwd, stashSha, options.stderr);
|
|
311
|
+
return emitNewError(options, `failed to create worktree: ${toExecMessage(error)}`);
|
|
312
|
+
}
|
|
313
|
+
if (stashSha &&
|
|
314
|
+
!(await applyTakeStash(worktreePath, options.cwd, stashSha, !!options.copy, options.stderr))) {
|
|
315
|
+
try {
|
|
316
|
+
await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], { cwd: repository.repoRoot });
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
/* rollback best effort */
|
|
320
|
+
}
|
|
321
|
+
return emitNewError(options, `could not apply taken changes; your changes are safe in stash: ${stashSha}`);
|
|
322
|
+
}
|
|
323
|
+
if (options.task !== undefined) {
|
|
175
324
|
try {
|
|
176
|
-
await
|
|
325
|
+
await writeTask(worktreePath, options.task);
|
|
177
326
|
}
|
|
178
327
|
catch (error) {
|
|
179
|
-
options.stderr(`Warning:
|
|
328
|
+
options.stderr(`Warning: could not save task metadata: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
180
329
|
}
|
|
181
330
|
}
|
|
182
|
-
|
|
183
|
-
const hooks = extractHooks(config);
|
|
184
|
-
await runHook(hooks["after-create"], worktreePath, {
|
|
331
|
+
const bootstrap = await bootstrapWorktree({
|
|
185
332
|
branch: worktreeName,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
333
|
+
cloneDirectory,
|
|
334
|
+
config,
|
|
335
|
+
currentRoot: repository.currentRoot,
|
|
336
|
+
nonInteractive: !!options.json,
|
|
337
|
+
repoRoot: repository.repoRoot,
|
|
338
|
+
reporter: createBootstrapReporter(options.stderr, !!options.json),
|
|
339
|
+
runCommand: dependencies.runInstallCommand,
|
|
340
|
+
commandStdout: options.json ? () => undefined : options.stdout,
|
|
341
|
+
commandStderr: options.json ? () => undefined : options.stderr,
|
|
342
|
+
json: options.json,
|
|
343
|
+
worktreePath,
|
|
344
|
+
installDependencies: dependencies,
|
|
345
|
+
dependencyBootstrapPolicy: dependencyPolicy,
|
|
346
|
+
});
|
|
347
|
+
if (!bootstrap.ready) {
|
|
348
|
+
return emitNewError(options, "worktree bootstrap failed", {
|
|
349
|
+
dependencyBootstrap: bootstrap.dependencyBootstrap,
|
|
350
|
+
path: worktreePath,
|
|
351
|
+
skipped: bootstrap.skippedDirs,
|
|
352
|
+
syncFiles: bootstrap.syncFileFailures,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
189
355
|
if (options.json) {
|
|
190
|
-
|
|
356
|
+
const navigation = createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), worktreePath, worktreeName);
|
|
357
|
+
const output = options.take
|
|
358
|
+
? {
|
|
359
|
+
...navigation,
|
|
360
|
+
taken: {
|
|
361
|
+
files: takeFiles.length,
|
|
362
|
+
untracked: takeUntracked,
|
|
363
|
+
submodules: submoduleFiles,
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
: { ...navigation };
|
|
367
|
+
if (bootstrap.clonedDirs.length > 0) {
|
|
368
|
+
output.cloned = bootstrap.clonedDirs.map(({ dir, ms }) => ({
|
|
369
|
+
dir,
|
|
370
|
+
ms,
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
if (bootstrap.skippedDirs.length > 0)
|
|
374
|
+
output.skipped = bootstrap.skippedDirs;
|
|
375
|
+
if (bootstrap.dependencyBootstrap.mode !== "off")
|
|
376
|
+
output.dependencyBootstrap = bootstrap.dependencyBootstrap;
|
|
377
|
+
options.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
191
378
|
}
|
|
192
379
|
else {
|
|
380
|
+
if (options.take)
|
|
381
|
+
options.stderr(`✓ took ${takeFiles.length} changed file${takeFiles.length === 1 ? "" : "s"} (${takeUntracked} untracked) → ${worktreeName}\n`);
|
|
193
382
|
await recordWorktreeUsage(worktreePath, worktreeName);
|
|
194
|
-
await writeOutput(worktreePath, options.stdout);
|
|
383
|
+
await writeOutput(worktreePath, options.stdout, options.outputEnv);
|
|
195
384
|
}
|
|
196
385
|
if (options.open) {
|
|
197
386
|
const resolvedEditor = options.editor ?? resolveConfigString(config, "editor");
|
|
@@ -201,6 +390,60 @@ export function createNewCommand(dependencies = {}) {
|
|
|
201
390
|
};
|
|
202
391
|
}
|
|
203
392
|
export const runNewCommand = createNewCommand();
|
|
393
|
+
async function resolveFreshBaseRef(repoRoot, config, options, promptForFetchFailure) {
|
|
394
|
+
const remote = resolveConfigString(config, "syncRemote") ?? "origin";
|
|
395
|
+
const configuredRemote = resolveConfigString(config, "syncRemote");
|
|
396
|
+
let baseBranch = resolveConfigString(config, "syncDefaultBranch");
|
|
397
|
+
try {
|
|
398
|
+
const remoteBase = await resolveRemoteBase(repoRoot, remote, baseBranch);
|
|
399
|
+
if (!remoteBase) {
|
|
400
|
+
if (!configuredRemote)
|
|
401
|
+
return "HEAD";
|
|
402
|
+
throw new Error("the remote default branch is unknown");
|
|
403
|
+
}
|
|
404
|
+
baseBranch = remoteBase.branch;
|
|
405
|
+
await runGit(repoRoot, ["fetch", "--prune", remote, remoteBase.branch]);
|
|
406
|
+
return remoteBase.ref;
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
410
|
+
const message = `Could not refresh ${remote}: ${detail}`;
|
|
411
|
+
if (options.json || isHeadless()) {
|
|
412
|
+
emitNewError(options, `${message}. Use --no-fetch to create from the local repository HEAD.`);
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const continueWithoutRefresh = await promptForFetchFailure(message);
|
|
416
|
+
if (!continueWithoutRefresh) {
|
|
417
|
+
options.stderr("Aborted\n");
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
if (baseBranch &&
|
|
421
|
+
(await localRefExists(repoRoot, `refs/remotes/${remote}/${baseBranch}`))) {
|
|
422
|
+
options.stderr(`Continuing with the cached ${remote}/${baseBranch}.\n`);
|
|
423
|
+
return `${remote}/${baseBranch}`;
|
|
424
|
+
}
|
|
425
|
+
options.stderr("Continuing from the local repository HEAD.\n");
|
|
426
|
+
return "HEAD";
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function defaultPromptForFetchFailure(message) {
|
|
430
|
+
const choice = await confirm({
|
|
431
|
+
active: "Continue",
|
|
432
|
+
inactive: "Abort",
|
|
433
|
+
initialValue: false,
|
|
434
|
+
message: `${message}. Continue without refreshing the remote base?`,
|
|
435
|
+
});
|
|
436
|
+
return !isCancel(choice) && choice;
|
|
437
|
+
}
|
|
438
|
+
async function localRefExists(repoRoot, ref) {
|
|
439
|
+
try {
|
|
440
|
+
await runGit(repoRoot, ["rev-parse", "--verify", "--quiet", ref]);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
204
447
|
export function generateBranchPlaceholder(random = Math.random) {
|
|
205
448
|
const roots = [
|
|
206
449
|
"socrates",
|
|
@@ -356,8 +599,41 @@ async function localBranchExists(repoRoot, branchName) {
|
|
|
356
599
|
async function resolveCurrentWorktreeHead(cwd) {
|
|
357
600
|
return execFileAsync("git", ["rev-parse", "--verify", "HEAD"], { cwd }).then(({ stdout }) => stdout.trim());
|
|
358
601
|
}
|
|
359
|
-
async function
|
|
360
|
-
|
|
602
|
+
async function createTakeStash(cwd, worktreeName) {
|
|
603
|
+
const message = `gji-take: ${worktreeName}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
604
|
+
await execFileAsync("git", ["stash", "push", "--include-untracked", "-m", message], { cwd });
|
|
605
|
+
const { stdout } = await execFileAsync("git", ["stash", "list", "--format=%H%x00%s"], { cwd });
|
|
606
|
+
const line = stdout
|
|
607
|
+
.split("\n")
|
|
608
|
+
.map((candidate) => candidate.split("\0"))
|
|
609
|
+
.find(([, subject]) => subject?.endsWith(message));
|
|
610
|
+
if (!line?.[0])
|
|
611
|
+
throw new Error("could not identify the stash created for --take");
|
|
612
|
+
return line[0];
|
|
613
|
+
}
|
|
614
|
+
async function detectInProgressGitState(cwd) {
|
|
615
|
+
const markers = [
|
|
616
|
+
["MERGE_HEAD", "merge"],
|
|
617
|
+
["rebase-merge", "rebase"],
|
|
618
|
+
["rebase-apply", "rebase"],
|
|
619
|
+
["CHERRY_PICK_HEAD", "cherry-pick"],
|
|
620
|
+
["REVERT_HEAD", "revert"],
|
|
621
|
+
["BISECT_LOG", "bisect"],
|
|
622
|
+
];
|
|
623
|
+
for (const [marker, name] of markers) {
|
|
624
|
+
try {
|
|
625
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", marker], { cwd });
|
|
626
|
+
await access(resolve(cwd, stdout.trim()));
|
|
627
|
+
return name;
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
/* marker is absent */
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
async function writeOutput(worktreePath, stdout, outputEnv) {
|
|
636
|
+
await writeShellOutput(outputEnv ?? NEW_OUTPUT_FILE_ENV, worktreePath, stdout);
|
|
361
637
|
}
|
|
362
638
|
function isNotRegisteredWorktreeError(error) {
|
|
363
639
|
const stderr = hasExecStderr(error) ? error.stderr : String(error);
|
|
@@ -391,3 +667,111 @@ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
|
|
|
391
667
|
stderr(`gji new: failed to open editor: ${message}\n`);
|
|
392
668
|
}
|
|
393
669
|
}
|
|
670
|
+
async function listTakeFiles(cwd) {
|
|
671
|
+
try {
|
|
672
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1"], { cwd });
|
|
673
|
+
return stdout
|
|
674
|
+
.split("\n")
|
|
675
|
+
.filter((line) => line.length > 3)
|
|
676
|
+
.map((line) => line.slice(3).replace(/^"|"$/g, ""));
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
return [];
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function listIgnoredFiles(cwd) {
|
|
683
|
+
try {
|
|
684
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1", "--ignored", "--untracked-files=all"], { cwd });
|
|
685
|
+
return stdout
|
|
686
|
+
.split("\n")
|
|
687
|
+
.filter((line) => line.startsWith("!! "))
|
|
688
|
+
.map((line) => line.slice(3).replace(/^"|"$/g, ""));
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return [];
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async function listSubmoduleFiles(cwd, changedFiles) {
|
|
695
|
+
if (changedFiles.length === 0)
|
|
696
|
+
return [];
|
|
697
|
+
try {
|
|
698
|
+
const { stdout } = await execFileAsync("git", ["ls-files", "--stage"], {
|
|
699
|
+
cwd,
|
|
700
|
+
});
|
|
701
|
+
const submodules = new Set(stdout
|
|
702
|
+
.split("\n")
|
|
703
|
+
.filter((line) => line.startsWith("160000 "))
|
|
704
|
+
.map((line) => line.slice(line.indexOf("\t") + 1)));
|
|
705
|
+
return changedFiles.filter((file) => submodules.has(file));
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return [];
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function countUntrackedFiles(cwd) {
|
|
712
|
+
try {
|
|
713
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd });
|
|
714
|
+
return stdout.split("\n").filter((line) => line.startsWith("?? ")).length;
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
return 0;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr) {
|
|
721
|
+
try {
|
|
722
|
+
await execFileAsync("git", ["stash", "apply", stashSha], {
|
|
723
|
+
cwd: worktreePath,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
stderr(`Warning: stash apply failed: ${toExecMessage(error)}\n`);
|
|
728
|
+
await restoreTakeStash(sourcePath, stashSha, stderr);
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
if (copy) {
|
|
732
|
+
try {
|
|
733
|
+
await execFileAsync("git", ["stash", "apply", stashSha], {
|
|
734
|
+
cwd: sourcePath,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
stderr(`Warning: could not restore copied changes to the source: ${toExecMessage(error)}\n`);
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
const { stdout } = await execFileAsync("git", ["stash", "list", "--format=%H %gd"], { cwd: sourcePath });
|
|
744
|
+
const line = stdout
|
|
745
|
+
.split("\n")
|
|
746
|
+
.find((candidate) => candidate.startsWith(stashSha));
|
|
747
|
+
const ref = line?.trim().split(/\s+/).at(1);
|
|
748
|
+
if (ref)
|
|
749
|
+
await execFileAsync("git", ["stash", "drop", ref], { cwd: sourcePath });
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
/* preserving the stash is safer */
|
|
753
|
+
}
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
async function restoreTakeStash(sourcePath, stashSha, stderr) {
|
|
757
|
+
try {
|
|
758
|
+
await execFileAsync("git", ["stash", "apply", stashSha], {
|
|
759
|
+
cwd: sourcePath,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
stderr(`Your changes are safe in stash: ${stashSha} — run "git stash apply ${stashSha}" to restore\n`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
function emitNewError(options, message, details) {
|
|
767
|
+
if (options.json)
|
|
768
|
+
options.stderr(`${JSON.stringify({ error: message, ...details }, null, 2)}\n`);
|
|
769
|
+
else {
|
|
770
|
+
const path = typeof details?.path === "string" ? details.path : undefined;
|
|
771
|
+
options.stderr(`gji new: ${message}${path ? ` at ${path}` : ""}\n`);
|
|
772
|
+
if (path) {
|
|
773
|
+
options.stderr(`Hint: inspect the worktree or remove it with 'gji done ${path}' before retrying\n`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return 1;
|
|
777
|
+
}
|
package/dist/pr-open.js
CHANGED
|
@@ -130,7 +130,7 @@ async function openFromWorktreeSelector(repoRoot, repoName, options, promptForWo
|
|
|
130
130
|
const entries = await buildWorktreePromptEntries(sources, {
|
|
131
131
|
queryPullRequests: async (_root, branch) => pullRequestsByBranch.get(branch) ?? [],
|
|
132
132
|
});
|
|
133
|
-
const connectedEntries = entries.filter((entry) => (entry.
|
|
133
|
+
const connectedEntries = entries.filter((entry) => (entry.pullRequests?.length ?? 0) > 0);
|
|
134
134
|
if (connectedEntries.length === 0) {
|
|
135
135
|
if (lookupError !== null) {
|
|
136
136
|
options.stderr(`gji pr open: failed to look up open PRs: ${formatError(lookupError)}\n`);
|
|
@@ -225,15 +225,7 @@ async function openForBranch(repoRoot, branch, options, queryPullRequests, promp
|
|
|
225
225
|
return openPullRequests(pullRequests, options, promptForPullRequest, openInBrowser);
|
|
226
226
|
}
|
|
227
227
|
async function openSelectedEntry(entry, options, promptForPullRequest, openInBrowser) {
|
|
228
|
-
const
|
|
229
|
-
const urls = entry.pullRequestUrls ?? [];
|
|
230
|
-
const pullRequests = numbers
|
|
231
|
-
.map((number, index) => ({
|
|
232
|
-
number,
|
|
233
|
-
sourceBranch: entry.branch ?? "",
|
|
234
|
-
url: urls[index] ?? "",
|
|
235
|
-
}))
|
|
236
|
-
.filter((pullRequest) => pullRequest.url.length > 0);
|
|
228
|
+
const pullRequests = entry.pullRequests ?? [];
|
|
237
229
|
return openPullRequests(pullRequests, options, promptForPullRequest, openInBrowser);
|
|
238
230
|
}
|
|
239
231
|
async function openPullRequests(pullRequests, options, promptForPullRequest, openInBrowser) {
|
package/dist/pr.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { type PathConflictChoice } from "./conflict.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type DependencyBootstrapPromptDependencies } from "./dependency-bootstrap-prompt.js";
|
|
3
|
+
import type { CloneDirectory } from "./dir-clone.js";
|
|
4
|
+
import type { InstallPromptDependencies } from "./install-prompt.js";
|
|
3
5
|
export type { PathConflictChoice };
|
|
4
6
|
export interface PrCommandOptions {
|
|
5
7
|
cwd: string;
|
|
6
8
|
dryRun?: boolean;
|
|
7
9
|
json?: boolean;
|
|
8
10
|
number: string;
|
|
11
|
+
outputEnv?: string;
|
|
9
12
|
stderr: (chunk: string) => void;
|
|
10
13
|
stdout: (chunk: string) => void;
|
|
11
14
|
}
|
|
12
|
-
export interface PrCommandDependencies extends InstallPromptDependencies {
|
|
15
|
+
export interface PrCommandDependencies extends InstallPromptDependencies, DependencyBootstrapPromptDependencies {
|
|
16
|
+
cloneDir?: CloneDirectory;
|
|
13
17
|
promptForPathConflict: (path: string) => Promise<PathConflictChoice>;
|
|
14
18
|
}
|
|
15
19
|
export declare function parsePrInput(input: string): string | null;
|