@solaqua/gji 0.9.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 +93 -20
- 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 +105 -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 +5489 -1206
- 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 +14 -2
- package/dist/new.js +434 -36
- package/dist/open.d.ts +1 -0
- package/dist/open.js +14 -2
- 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 +47 -14
- 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 +21 -3
- package/man/man1/gji-open.1 +4 -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/shell-completion.js
CHANGED
|
@@ -2,7 +2,15 @@ import { KNOWN_GLOBAL_CONFIG_KEYS } from "./config.js";
|
|
|
2
2
|
const TOP_LEVEL_COMMANDS = [
|
|
3
3
|
{
|
|
4
4
|
name: "new",
|
|
5
|
-
description: "create a new branch or detached linked worktree",
|
|
5
|
+
description: "create a new branch or detached linked worktree and CoW-bootstrap configured directories",
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
name: "done",
|
|
9
|
+
description: "finish the current linked worktree and return safely",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: "undo",
|
|
13
|
+
description: "restore the most recent destructive worktree operation",
|
|
6
14
|
},
|
|
7
15
|
{
|
|
8
16
|
name: "init",
|
|
@@ -17,7 +25,7 @@ const TOP_LEVEL_COMMANDS = [
|
|
|
17
25
|
{ name: "back", description: "navigate to the previously visited worktree" },
|
|
18
26
|
{ name: "history", description: "show navigation history" },
|
|
19
27
|
{ name: "open", description: "open the worktree in an editor" },
|
|
20
|
-
{ name: "go", description: "
|
|
28
|
+
{ name: "go", description: "resolve and jump to a worktree path" },
|
|
21
29
|
{ name: "jump", description: "alias of go" },
|
|
22
30
|
{ name: "root", description: "print the main repository root path" },
|
|
23
31
|
{ name: "status", description: "summarize repository and worktree health" },
|
|
@@ -30,14 +38,17 @@ const TOP_LEVEL_COMMANDS = [
|
|
|
30
38
|
{ name: "clean", description: "interactively prune linked worktrees" },
|
|
31
39
|
{
|
|
32
40
|
name: "remove",
|
|
33
|
-
description: "
|
|
41
|
+
description: "deprecated: use done for one worktree or clean for bulk cleanup",
|
|
34
42
|
},
|
|
35
|
-
{ name: "rm", description: "alias of remove" },
|
|
43
|
+
{ name: "rm", description: "deprecated alias of remove" },
|
|
36
44
|
{
|
|
37
45
|
name: "run-hook",
|
|
38
46
|
description: "run a named hook in the current worktree",
|
|
39
47
|
},
|
|
40
|
-
{
|
|
48
|
+
{
|
|
49
|
+
name: "warp",
|
|
50
|
+
description: "deprecated: use go (press Tab for all known repos)",
|
|
51
|
+
},
|
|
41
52
|
{ name: "config", description: "manage global config defaults" },
|
|
42
53
|
];
|
|
43
54
|
const SHELL_NAMES = ["bash", "fish", "zsh"];
|
|
@@ -137,7 +148,13 @@ _gji_completion() {
|
|
|
137
148
|
|
|
138
149
|
case "$command_name" in
|
|
139
150
|
new)
|
|
140
|
-
COMPREPLY=( $(compgen -W "--detached --force --open --editor --dry-run --json --help" -- "$cur") )
|
|
151
|
+
COMPREPLY=( $(compgen -W "--detached --from-current --no-fetch --take --copy --force --open --editor --dry-run --json --help" -- "$cur") )
|
|
152
|
+
;;
|
|
153
|
+
done)
|
|
154
|
+
COMPREPLY=( $(compgen -W "--force --keep-branch --json --help" -- "$cur") )
|
|
155
|
+
;;
|
|
156
|
+
undo)
|
|
157
|
+
COMPREPLY=( $(compgen -W "--list --json --help" -- "$cur") )
|
|
141
158
|
;;
|
|
142
159
|
init)
|
|
143
160
|
COMPREPLY=( $(compgen -W "${shells} --write --json --help" -- "$cur") )
|
|
@@ -179,10 +196,10 @@ _gji_completion() {
|
|
|
179
196
|
COMPREPLY=( $(compgen -W "--json --help" -- "$cur") )
|
|
180
197
|
;;
|
|
181
198
|
open)
|
|
182
|
-
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --editor --save --workspace --help" -- "$cur") )
|
|
199
|
+
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --editor --select --save --workspace --help" -- "$cur") )
|
|
183
200
|
;;
|
|
184
201
|
go|jump)
|
|
185
|
-
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --print --help" -- "$cur") )
|
|
202
|
+
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --root --print --json --help" -- "$cur") )
|
|
186
203
|
;;
|
|
187
204
|
root)
|
|
188
205
|
COMPREPLY=( $(compgen -W "--print --help" -- "$cur") )
|
|
@@ -212,7 +229,7 @@ _gji_completion() {
|
|
|
212
229
|
COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
|
|
213
230
|
;;
|
|
214
231
|
warp)
|
|
215
|
-
COMPREPLY=( $(compgen -W "
|
|
232
|
+
COMPREPLY=( $(compgen -W "--print --json --help" -- "$cur") )
|
|
216
233
|
;;
|
|
217
234
|
config)
|
|
218
235
|
if [ "$COMP_CWORD" -eq 2 ]; then
|
|
@@ -361,9 +378,17 @@ function __gji_should_complete_config_key
|
|
|
361
378
|
end
|
|
362
379
|
|
|
363
380
|
complete -c gji -f
|
|
381
|
+
complete -c gji -n '__fish_seen_subcommand_from done' -l force -d 'remove dirty or unmerged worktrees without prompting'
|
|
382
|
+
complete -c gji -n '__fish_seen_subcommand_from done' -l keep-branch -d 'remove the worktree but preserve its branch'
|
|
383
|
+
complete -c gji -n '__fish_seen_subcommand_from done' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
384
|
+
complete -c gji -n '__fish_seen_subcommand_from undo' -l list -d 'list recoverable operations'
|
|
385
|
+
complete -c gji -n '__fish_seen_subcommand_from undo' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
364
386
|
${commandLines}
|
|
365
387
|
|
|
366
388
|
complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a detached worktree without a branch'
|
|
389
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l from-current -d 'base the new branch on the current worktree instead of the main worktree'
|
|
390
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l take -d 'move current uncommitted changes into the new worktree'
|
|
391
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l copy -d 'copy current uncommitted changes instead of moving them (requires --take)'
|
|
367
392
|
complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and recreate the worktree if the target path already exists'
|
|
368
393
|
complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
|
|
369
394
|
complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, …)'
|
|
@@ -393,11 +418,14 @@ complete -c gji -n '__fish_seen_subcommand_from back' -l print -d 'print the res
|
|
|
393
418
|
complete -c gji -n '__fish_seen_subcommand_from history' -l json -d 'print history as JSON'
|
|
394
419
|
|
|
395
420
|
complete -c gji -n '__fish_seen_subcommand_from open' -l editor -r -d 'editor CLI to use (code, cursor, zed, windsurf, subl, …)'
|
|
421
|
+
complete -c gji -n '__fish_seen_subcommand_from open' -l select -d 'choose a worktree with the interactive selector'
|
|
396
422
|
complete -c gji -n '__fish_seen_subcommand_from open' -l save -d 'save the chosen editor to global config'
|
|
397
423
|
complete -c gji -n '__fish_seen_subcommand_from open' -l workspace -d 'generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)'
|
|
398
424
|
complete -c gji -n '__fish_seen_subcommand_from open' -a '(__gji_worktree_branches)' -d 'worktree branch'
|
|
399
425
|
|
|
400
426
|
complete -c gji -n '__fish_seen_subcommand_from go jump' -l print -d 'print the resolved worktree path explicitly'
|
|
427
|
+
complete -c gji -n '__fish_seen_subcommand_from go jump' -l root -d 'navigate to the main repository root'
|
|
428
|
+
complete -c gji -n '__fish_seen_subcommand_from go jump' -l json -d 'emit JSON for an existing worktree destination'
|
|
401
429
|
complete -c gji -n '__fish_seen_subcommand_from go jump' -a '(__gji_worktree_branches)' -d 'worktree branch'
|
|
402
430
|
|
|
403
431
|
complete -c gji -n '__fish_seen_subcommand_from root' -l print -d 'print the resolved repository root path explicitly'
|
|
@@ -428,7 +456,6 @@ complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_b
|
|
|
428
456
|
|
|
429
457
|
${hookLines}
|
|
430
458
|
|
|
431
|
-
complete -c gji -n '__fish_seen_subcommand_from warp' -s n -l new -d 'create a new worktree in a registered repo'
|
|
432
459
|
complete -c gji -n '__fish_seen_subcommand_from warp' -l print -d 'print the resolved worktree path without changing directory'
|
|
433
460
|
complete -c gji -n '__fish_seen_subcommand_from warp' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
434
461
|
|
|
@@ -516,7 +543,13 @@ fi
|
|
|
516
543
|
|
|
517
544
|
case "\${words[2]}" in
|
|
518
545
|
new)
|
|
519
|
-
_arguments '--detached[create a detached worktree without a branch]' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
546
|
+
_arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--no-fetch[skip refreshing the remote default branch before creating the new branch]' '--take[move current uncommitted changes into the new worktree]' '--copy[copy current uncommitted changes instead of moving them (requires --take)]' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
547
|
+
;;
|
|
548
|
+
done)
|
|
549
|
+
_arguments '--force[remove dirty or unmerged worktrees without prompting]' '--keep-branch[remove the worktree but preserve its branch]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
550
|
+
;;
|
|
551
|
+
undo)
|
|
552
|
+
_arguments '--list[list recoverable operations]' '--json[emit JSON on success or error instead of human-readable output]' '2:id: '
|
|
520
553
|
;;
|
|
521
554
|
init)
|
|
522
555
|
_arguments '--write[write the integration to the shell config file]' '--json[emit a JSON error in non-interactive mode]' '2:shell:(${shells})'
|
|
@@ -547,10 +580,10 @@ case "\${words[2]}" in
|
|
|
547
580
|
_arguments '--json[print history as JSON]'
|
|
548
581
|
;;
|
|
549
582
|
open)
|
|
550
|
-
_arguments '--editor[editor CLI to use (code, cursor, zed, windsurf, subl, …)]:editor:' '--save[save the chosen editor to global config]' '--workspace[generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)]' '2:branch:->worktrees'
|
|
583
|
+
_arguments '--editor[editor CLI to use (code, cursor, zed, windsurf, subl, …)]:editor:' '--select[choose a worktree with the interactive selector]' '--save[save the chosen editor to global config]' '--workspace[generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)]' '2:branch:->worktrees'
|
|
551
584
|
;;
|
|
552
585
|
go|jump)
|
|
553
|
-
_arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
|
|
586
|
+
_arguments '--root[navigate to the main repository root]' '--print[print the resolved worktree path explicitly]' '--json[emit JSON for an existing worktree destination]' '2:branch:->worktrees'
|
|
554
587
|
;;
|
|
555
588
|
root)
|
|
556
589
|
_arguments '--print[print the resolved repository root path explicitly]'
|
|
@@ -577,7 +610,7 @@ case "\${words[2]}" in
|
|
|
577
610
|
_arguments "2:hook:(${hooks})"
|
|
578
611
|
;;
|
|
579
612
|
warp)
|
|
580
|
-
_arguments '
|
|
613
|
+
_arguments '--print[print the resolved worktree path without changing directory]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
581
614
|
;;
|
|
582
615
|
config)
|
|
583
616
|
if (( CURRENT == 3 )); then
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type CloneFailureStore } from "./clone-failure-store.js";
|
|
2
|
+
import type { CloneDirectory } from "./dir-clone.js";
|
|
3
|
+
import type { SyncDirectoryPlan } from "./sync-plan.js";
|
|
4
|
+
export interface ClonedDirectory {
|
|
5
|
+
bytes?: number;
|
|
6
|
+
dir: string;
|
|
7
|
+
ms: number;
|
|
8
|
+
}
|
|
9
|
+
export type SyncDirectoryOutcome = {
|
|
10
|
+
kind: "cloned";
|
|
11
|
+
directory: ClonedDirectory;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "skipped";
|
|
14
|
+
dir: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
};
|
|
17
|
+
export interface SyncDirectoryReporter {
|
|
18
|
+
readonly emitCachedFailureWarnings: boolean;
|
|
19
|
+
readonly measureCloneSize: boolean;
|
|
20
|
+
write(message: string): void;
|
|
21
|
+
cloned(directory: ClonedDirectory): void;
|
|
22
|
+
skipped?(directory: {
|
|
23
|
+
dir: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
}): void;
|
|
26
|
+
}
|
|
27
|
+
export interface SyncDirectoryExecutionOptions {
|
|
28
|
+
cloneDirectory: CloneDirectory;
|
|
29
|
+
failureStore?: CloneFailureStore;
|
|
30
|
+
repoRoot: string;
|
|
31
|
+
reporter: SyncDirectoryReporter;
|
|
32
|
+
}
|
|
33
|
+
export declare function executeSyncDirectoryPlan(plan: readonly SyncDirectoryPlan[], options: SyncDirectoryExecutionOptions): Promise<SyncDirectoryOutcome[]>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { cloneFailureScope, defaultCloneFailureStore, } from "./clone-failure-store.js";
|
|
2
|
+
import { isCloneDestinationExistsError, isCloneInProgressError, isCloneUnsupportedError, } from "./dir-clone.js";
|
|
3
|
+
import { inspectDestination } from "./safe-destination.js";
|
|
4
|
+
export async function executeSyncDirectoryPlan(plan, options) {
|
|
5
|
+
const failureStore = options.failureStore ?? defaultCloneFailureStore;
|
|
6
|
+
const outcomes = [];
|
|
7
|
+
for (const entry of plan) {
|
|
8
|
+
if (entry.destinationWarning) {
|
|
9
|
+
recordSkipped(outcomes, options.reporter, entry.directory, entry.destinationWarning);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const destinationState = await inspectDestination(entry.worktreePath, entry.destination);
|
|
13
|
+
if (destinationState.kind === "exists") {
|
|
14
|
+
recordSkipped(outcomes, options.reporter, entry.directory, "destination already exists");
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (destinationState.kind === "unsafe") {
|
|
18
|
+
const reason = destinationState.reason;
|
|
19
|
+
recordSkipped(outcomes, options.reporter, entry.directory, reason);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (entry.warning) {
|
|
23
|
+
recordSkipped(outcomes, options.reporter, entry.directory, entry.warning);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!entry.source) {
|
|
27
|
+
recordSkipped(outcomes, options.reporter, entry.directory, "source does not exist");
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const failureScope = await cloneFailureScope(entry.source, entry.destination);
|
|
31
|
+
if (await failureStore.isCached(options.repoRoot, entry.directory, failureScope)) {
|
|
32
|
+
if (options.reporter.emitCachedFailureWarnings ||
|
|
33
|
+
options.reporter.skipped) {
|
|
34
|
+
recordSkipped(outcomes, options.reporter, entry.directory, "copy-on-write failure cached");
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
options.reporter.write(`syncDirs: previous copy-on-write failure cached, skipped ${entry.directory}\n`);
|
|
38
|
+
recordSkipped(outcomes, options.reporter, entry.directory, "copy-on-write failure cached", false);
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const refreshedDestinationState = await inspectDestination(entry.worktreePath, entry.destination);
|
|
43
|
+
if (refreshedDestinationState.kind !== "missing") {
|
|
44
|
+
recordSkipped(outcomes, options.reporter, entry.directory, refreshedDestinationState.kind === "unsafe"
|
|
45
|
+
? refreshedDestinationState.reason
|
|
46
|
+
: "destination already exists");
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
let result;
|
|
50
|
+
try {
|
|
51
|
+
const cloneOptions = {
|
|
52
|
+
destinationRoot: entry.worktreePath,
|
|
53
|
+
measureBytes: options.reporter.measureCloneSize,
|
|
54
|
+
};
|
|
55
|
+
result = await options.cloneDirectory(entry.source, entry.destination, cloneOptions);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (isCloneDestinationExistsError(error)) {
|
|
59
|
+
recordSkipped(outcomes, options.reporter, entry.directory, "destination already exists");
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (isCloneInProgressError(error)) {
|
|
63
|
+
const reason = "copy-on-write clone already in progress";
|
|
64
|
+
recordSkipped(outcomes, options.reporter, entry.directory, reason);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const reason = toErrorMessage(error);
|
|
68
|
+
if (isCloneUnsupportedError(error)) {
|
|
69
|
+
await failureStore.cache(options.repoRoot, entry.directory, reason, failureScope);
|
|
70
|
+
}
|
|
71
|
+
recordSkipped(outcomes, options.reporter, entry.directory, reason);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
await failureStore.clear(options.repoRoot, entry.directory, failureScope);
|
|
75
|
+
const clonedDirectory = {
|
|
76
|
+
bytes: result.bytes,
|
|
77
|
+
dir: entry.directory,
|
|
78
|
+
ms: result.ms,
|
|
79
|
+
};
|
|
80
|
+
const outcome = { kind: "cloned", directory: clonedDirectory };
|
|
81
|
+
outcomes.push(outcome);
|
|
82
|
+
options.reporter.cloned(clonedDirectory);
|
|
83
|
+
}
|
|
84
|
+
return outcomes;
|
|
85
|
+
}
|
|
86
|
+
function recordSkipped(outcomes, reporter, dir, reason, notify = true) {
|
|
87
|
+
outcomes.push({ kind: "skipped", dir, reason });
|
|
88
|
+
if (notify) {
|
|
89
|
+
if (reporter.skipped)
|
|
90
|
+
reporter.skipped({ dir, reason });
|
|
91
|
+
else
|
|
92
|
+
reporter.write(`syncDirs: ${reason}, skipped ${dir}\n`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function toErrorMessage(error) {
|
|
96
|
+
return error instanceof Error ? error.message : String(error);
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SyncDirectoryPlan {
|
|
2
|
+
directory: string;
|
|
3
|
+
destination: string;
|
|
4
|
+
worktreePath: string;
|
|
5
|
+
destinationWasPresent: boolean;
|
|
6
|
+
destinationWarning?: string;
|
|
7
|
+
source?: string;
|
|
8
|
+
warning?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SyncDirectoryEstimate {
|
|
11
|
+
bytes: number;
|
|
12
|
+
dir: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function prepareSyncDirectoryPlan(repoRoot: string, worktreePath: string, directories: readonly string[]): Promise<SyncDirectoryPlan[]>;
|
|
15
|
+
export declare function estimateSyncDirectoryPlan(plan: readonly SyncDirectoryPlan[]): Promise<SyncDirectoryEstimate[]>;
|
|
16
|
+
export declare function estimateSyncDirectories(repoRoot: string, worktreePath: string, directories: readonly string[]): Promise<SyncDirectoryEstimate[]>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { validateSyncDirPattern } from "./config.js";
|
|
4
|
+
import { directorySize } from "./dir-clone.js";
|
|
5
|
+
import { isNotFoundError } from "./fs-utils.js";
|
|
6
|
+
import { inspectDestination } from "./safe-destination.js";
|
|
7
|
+
export async function prepareSyncDirectoryPlan(repoRoot, worktreePath, directories) {
|
|
8
|
+
const normalizedDirectories = directories
|
|
9
|
+
.map(validateSyncDirPattern)
|
|
10
|
+
.sort(directoryDepthAscending);
|
|
11
|
+
const plan = [];
|
|
12
|
+
for (const directory of normalizedDirectories) {
|
|
13
|
+
const destination = join(worktreePath, directory);
|
|
14
|
+
const destinationState = await inspectDestination(worktreePath, destination);
|
|
15
|
+
const destinationWasPresent = destinationState.kind === "exists";
|
|
16
|
+
const destinationWarning = destinationState.kind === "unsafe" ? destinationState.reason : undefined;
|
|
17
|
+
try {
|
|
18
|
+
const source = await resolveSyncDirectorySource(repoRoot, directory);
|
|
19
|
+
if (!source) {
|
|
20
|
+
plan.push({
|
|
21
|
+
directory,
|
|
22
|
+
destination,
|
|
23
|
+
worktreePath,
|
|
24
|
+
destinationWasPresent,
|
|
25
|
+
destinationWarning,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else if ("warning" in source) {
|
|
29
|
+
plan.push({
|
|
30
|
+
directory,
|
|
31
|
+
destination,
|
|
32
|
+
worktreePath,
|
|
33
|
+
destinationWasPresent,
|
|
34
|
+
destinationWarning,
|
|
35
|
+
warning: source.warning,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
plan.push({
|
|
40
|
+
directory,
|
|
41
|
+
destination,
|
|
42
|
+
worktreePath,
|
|
43
|
+
destinationWasPresent,
|
|
44
|
+
destinationWarning,
|
|
45
|
+
source: source.path,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
plan.push({
|
|
51
|
+
directory,
|
|
52
|
+
destination,
|
|
53
|
+
worktreePath,
|
|
54
|
+
destinationWasPresent,
|
|
55
|
+
destinationWarning,
|
|
56
|
+
warning: `could not inspect ${directory}: ${toErrorMessage(error)}`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return plan;
|
|
61
|
+
}
|
|
62
|
+
export async function estimateSyncDirectoryPlan(plan) {
|
|
63
|
+
const estimates = [];
|
|
64
|
+
for (const entry of plan) {
|
|
65
|
+
if (entry.destinationWasPresent ||
|
|
66
|
+
entry.destinationWarning ||
|
|
67
|
+
!entry.source ||
|
|
68
|
+
entry.warning ||
|
|
69
|
+
isCoveredByCloneAncestor(entry, plan)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
estimates.push({
|
|
74
|
+
bytes: await directorySize(entry.source),
|
|
75
|
+
dir: entry.directory,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// A dry-run is informational; an unreadable source is omitted.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return estimates;
|
|
83
|
+
}
|
|
84
|
+
export async function estimateSyncDirectories(repoRoot, worktreePath, directories) {
|
|
85
|
+
const plan = await prepareSyncDirectoryPlan(repoRoot, worktreePath, directories);
|
|
86
|
+
return estimateSyncDirectoryPlan(plan);
|
|
87
|
+
}
|
|
88
|
+
async function resolveSyncDirectorySource(repoRoot, directory) {
|
|
89
|
+
const source = join(repoRoot, directory);
|
|
90
|
+
let resolvedSource;
|
|
91
|
+
try {
|
|
92
|
+
resolvedSource = await realpath(source);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (isNotFoundError(error))
|
|
96
|
+
return null;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
if (!isPathInside(repoRoot, resolvedSource)) {
|
|
100
|
+
return {
|
|
101
|
+
warning: `source symlink resolves outside the repository (${resolvedSource})`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const stats = await lstat(resolvedSource);
|
|
105
|
+
if (!stats.isDirectory())
|
|
106
|
+
return { warning: "source is not a directory" };
|
|
107
|
+
return { path: resolvedSource };
|
|
108
|
+
}
|
|
109
|
+
function isCoveredByCloneAncestor(entry, plan) {
|
|
110
|
+
return plan.some((candidate) => candidate !== entry &&
|
|
111
|
+
!candidate.destinationWasPresent &&
|
|
112
|
+
!candidate.destinationWarning &&
|
|
113
|
+
!!candidate.source &&
|
|
114
|
+
isPathInside(candidate.directory, entry.directory));
|
|
115
|
+
}
|
|
116
|
+
function directoryDepthAscending(left, right) {
|
|
117
|
+
return left.split(/[\\/]+/u).length - right.split(/[\\/]+/u).length;
|
|
118
|
+
}
|
|
119
|
+
function isPathInside(parent, child) {
|
|
120
|
+
const relativePath = relative(resolve(parent), resolve(child));
|
|
121
|
+
return (relativePath !== "" &&
|
|
122
|
+
!isAbsolute(relativePath) &&
|
|
123
|
+
relativePath !== ".." &&
|
|
124
|
+
!relativePath.startsWith(`..${sep}`));
|
|
125
|
+
}
|
|
126
|
+
function toErrorMessage(error) {
|
|
127
|
+
return error instanceof Error ? error.message : String(error);
|
|
128
|
+
}
|
package/dist/sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadEffectiveConfig } from "./config.js";
|
|
2
|
-
import { isDirtyWorktree,
|
|
2
|
+
import { isDirtyWorktree, resolveRemoteBase, runGit, } from "./git.js";
|
|
3
3
|
import { comparePaths } from "./paths.js";
|
|
4
4
|
import { detectRepository, listWorktrees } from "./repo.js";
|
|
5
5
|
export async function runSyncCommand(options) {
|
|
@@ -7,11 +7,9 @@ export async function runSyncCommand(options) {
|
|
|
7
7
|
const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
|
|
8
8
|
const worktrees = await listWorktrees(options.cwd);
|
|
9
9
|
const remote = resolveConfiguredString(config.syncRemote) ?? "origin";
|
|
10
|
-
let
|
|
10
|
+
let remoteBase;
|
|
11
11
|
try {
|
|
12
|
-
|
|
13
|
-
resolveConfiguredString(config.syncDefaultBranch) ??
|
|
14
|
-
(await resolveRemoteDefaultBranch(repository.repoRoot, remote));
|
|
12
|
+
remoteBase = await resolveRemoteBase(repository.repoRoot, remote, resolveConfiguredString(config.syncDefaultBranch) ?? undefined);
|
|
15
13
|
}
|
|
16
14
|
catch {
|
|
17
15
|
emitError(options, `Unable to reach remote '${remote}'`);
|
|
@@ -20,7 +18,7 @@ export async function runSyncCommand(options) {
|
|
|
20
18
|
}
|
|
21
19
|
return 1;
|
|
22
20
|
}
|
|
23
|
-
if (!
|
|
21
|
+
if (!remoteBase) {
|
|
24
22
|
emitError(options, "Unable to determine the default branch for sync.");
|
|
25
23
|
if (!options.json) {
|
|
26
24
|
options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>\n`);
|
|
@@ -50,15 +48,11 @@ export async function runSyncCommand(options) {
|
|
|
50
48
|
}
|
|
51
49
|
const updatedWorktrees = [];
|
|
52
50
|
for (const worktree of targetWorktrees) {
|
|
53
|
-
if (worktree.branch ===
|
|
54
|
-
await runGit(worktree.path, [
|
|
55
|
-
"merge",
|
|
56
|
-
"--ff-only",
|
|
57
|
-
`${remote}/${defaultBranch}`,
|
|
58
|
-
]);
|
|
51
|
+
if (worktree.branch === remoteBase.branch) {
|
|
52
|
+
await runGit(worktree.path, ["merge", "--ff-only", remoteBase.ref]);
|
|
59
53
|
}
|
|
60
54
|
else {
|
|
61
|
-
await runGit(worktree.path, ["rebase",
|
|
55
|
+
await runGit(worktree.path, ["rebase", remoteBase.ref]);
|
|
62
56
|
}
|
|
63
57
|
updatedWorktrees.push(worktree);
|
|
64
58
|
if (!options.json) {
|
package/dist/undo.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type WorktreeEntry } from "./repo.js";
|
|
2
|
+
export type UndoOperation = "remove" | "clean" | "done";
|
|
3
|
+
export interface UndoEntry {
|
|
4
|
+
branch: string | null;
|
|
5
|
+
headSha: string;
|
|
6
|
+
path: string;
|
|
7
|
+
upstream: string | null;
|
|
8
|
+
wasDirty: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface UndoRecord {
|
|
11
|
+
id: string;
|
|
12
|
+
op: UndoOperation;
|
|
13
|
+
repoRoot: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
entries: UndoEntry[];
|
|
16
|
+
}
|
|
17
|
+
export interface UndoRestoreResult {
|
|
18
|
+
restored: UndoEntry[];
|
|
19
|
+
failed: Array<UndoEntry & {
|
|
20
|
+
message: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export interface UndoCommandOptions {
|
|
24
|
+
cwd: string;
|
|
25
|
+
id?: string;
|
|
26
|
+
list?: boolean;
|
|
27
|
+
json?: boolean;
|
|
28
|
+
stderr: (chunk: string) => void;
|
|
29
|
+
stdout: (chunk: string) => void;
|
|
30
|
+
}
|
|
31
|
+
export declare function undoLogPath(home?: string): string;
|
|
32
|
+
export declare function loadUndoRecords(home?: string): Promise<UndoRecord[]>;
|
|
33
|
+
export declare function recordUndoOperation(op: UndoOperation, repoRoot: string, worktrees: WorktreeEntry[], home?: string): Promise<UndoRecord | null>;
|
|
34
|
+
export declare function finalizeUndoOperation(record: UndoRecord, removedWorktrees: WorktreeEntry[], home?: string): Promise<void>;
|
|
35
|
+
export declare function restoreUndoRecord(record: UndoRecord, home?: string): Promise<UndoRestoreResult>;
|
|
36
|
+
export declare function runUndoCommand(options: UndoCommandOptions): Promise<number>;
|