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