@solaqua/gji 0.12.0 → 0.12.2
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/dist/bootstrap-output.js +0 -1
- package/dist/dependency-bootstrap.d.ts +10 -17
- package/dist/dependency-bootstrap.js +343 -108
- package/dist/dir-clone.d.ts +1 -2
- package/dist/dir-clone.js +303 -167
- package/dist/gji-bundle.mjs +985 -799
- package/dist/hub.js +13 -1
- package/dist/safe-destination.js +10 -0
- package/dist/sync-directories.d.ts +0 -4
- package/dist/sync-directories.js +6 -26
- package/dist/worktree-bootstrap.js +0 -2
- package/dist/worktree-picker.js +13 -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 +1 -1
- package/man/man1/gji-go.1 +1 -1
- 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 +1 -1
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +1 -1
- package/man/man1/gji-remove.1 +1 -1
- 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 +1 -1
- package/man/man1/gji-undo.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +1 -1
- package/package.json +1 -1
- package/dist/clone-failure-store.d.ts +0 -18
- package/dist/clone-failure-store.js +0 -230
package/dist/hub.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
import { spinner } from "@clack/prompts";
|
|
2
4
|
import { loadHistory } from "./history.js";
|
|
3
5
|
import { createPullRequestQuery, } from "./pull-requests.js";
|
|
4
6
|
import { detectRepository } from "./repo.js";
|
|
@@ -65,7 +67,17 @@ export async function buildHubData(cwd, dependencies = {}) {
|
|
|
65
67
|
};
|
|
66
68
|
}
|
|
67
69
|
export async function runHubCommand(options, dependencies = {}) {
|
|
68
|
-
const
|
|
70
|
+
const loading = !options.json && stdin.isTTY === true && stdout.isTTY === true
|
|
71
|
+
? spinner({ indicator: "timer" })
|
|
72
|
+
: null;
|
|
73
|
+
loading?.start("Loading worktrees");
|
|
74
|
+
let data;
|
|
75
|
+
try {
|
|
76
|
+
data = await buildHubData(options.cwd, dependencies);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
loading?.stop();
|
|
80
|
+
}
|
|
69
81
|
if (options.json) {
|
|
70
82
|
options.stdout(`${JSON.stringify(data, null, 2)}\n`);
|
|
71
83
|
return 0;
|
package/dist/safe-destination.js
CHANGED
|
@@ -4,8 +4,18 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
import { isAlreadyExistsError, isNotDirectoryError, isNotFoundError, } from "./fs-utils.js";
|
|
5
5
|
export async function openDestinationDirectory(root, path) {
|
|
6
6
|
const segments = destinationSegments(root, path);
|
|
7
|
+
if (process.platform === "darwin") {
|
|
8
|
+
await ensureDestinationDirectory(root, path);
|
|
9
|
+
const handle = await open(path, destinationDirectoryFlags());
|
|
10
|
+
return {
|
|
11
|
+
path: resolve(path),
|
|
12
|
+
close: async () => handle.close(),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
7
15
|
if (process.platform !== "linux") {
|
|
8
16
|
await ensureDestinationDirectory(root, path);
|
|
17
|
+
const handle = await open(path, destinationDirectoryFlags());
|
|
18
|
+
await handle.close();
|
|
9
19
|
return { path: resolve(path), close: async () => undefined };
|
|
10
20
|
}
|
|
11
21
|
let handle = await open(root, destinationDirectoryFlags());
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type CloneFailureStore } from "./clone-failure-store.js";
|
|
2
1
|
import type { CloneDirectory } from "./dir-clone.js";
|
|
3
2
|
import type { SyncDirectoryPlan } from "./sync-plan.js";
|
|
4
3
|
export interface ClonedDirectory {
|
|
@@ -15,7 +14,6 @@ export type SyncDirectoryOutcome = {
|
|
|
15
14
|
reason: string;
|
|
16
15
|
};
|
|
17
16
|
export interface SyncDirectoryReporter {
|
|
18
|
-
readonly emitCachedFailureWarnings: boolean;
|
|
19
17
|
readonly measureCloneSize: boolean;
|
|
20
18
|
write(message: string): void;
|
|
21
19
|
cloned(directory: ClonedDirectory): void;
|
|
@@ -26,8 +24,6 @@ export interface SyncDirectoryReporter {
|
|
|
26
24
|
}
|
|
27
25
|
export interface SyncDirectoryExecutionOptions {
|
|
28
26
|
cloneDirectory: CloneDirectory;
|
|
29
|
-
failureStore?: CloneFailureStore;
|
|
30
|
-
repoRoot: string;
|
|
31
27
|
reporter: SyncDirectoryReporter;
|
|
32
28
|
}
|
|
33
29
|
export declare function executeSyncDirectoryPlan(plan: readonly SyncDirectoryPlan[], options: SyncDirectoryExecutionOptions): Promise<SyncDirectoryOutcome[]>;
|
package/dist/sync-directories.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isCloneDestinationExistsError, isCloneInProgressError, isCloneUnsupportedError, } from "./dir-clone.js";
|
|
1
|
+
import { isCloneDestinationExistsError, isCloneInProgressError, } from "./dir-clone.js";
|
|
3
2
|
import { inspectDestination } from "./safe-destination.js";
|
|
4
3
|
export async function executeSyncDirectoryPlan(plan, options) {
|
|
5
|
-
const failureStore = options.failureStore ?? defaultCloneFailureStore;
|
|
6
4
|
const outcomes = [];
|
|
7
5
|
for (const entry of plan) {
|
|
8
6
|
if (entry.destinationWarning) {
|
|
@@ -27,18 +25,6 @@ export async function executeSyncDirectoryPlan(plan, options) {
|
|
|
27
25
|
recordSkipped(outcomes, options.reporter, entry.directory, "source does not exist");
|
|
28
26
|
continue;
|
|
29
27
|
}
|
|
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
28
|
const refreshedDestinationState = await inspectDestination(entry.worktreePath, entry.destination);
|
|
43
29
|
if (refreshedDestinationState.kind !== "missing") {
|
|
44
30
|
recordSkipped(outcomes, options.reporter, entry.directory, refreshedDestinationState.kind === "unsafe"
|
|
@@ -65,13 +51,9 @@ export async function executeSyncDirectoryPlan(plan, options) {
|
|
|
65
51
|
continue;
|
|
66
52
|
}
|
|
67
53
|
const reason = toErrorMessage(error);
|
|
68
|
-
if (isCloneUnsupportedError(error)) {
|
|
69
|
-
await failureStore.cache(options.repoRoot, entry.directory, reason, failureScope);
|
|
70
|
-
}
|
|
71
54
|
recordSkipped(outcomes, options.reporter, entry.directory, reason);
|
|
72
55
|
continue;
|
|
73
56
|
}
|
|
74
|
-
await failureStore.clear(options.repoRoot, entry.directory, failureScope);
|
|
75
57
|
const clonedDirectory = {
|
|
76
58
|
bytes: result.bytes,
|
|
77
59
|
dir: entry.directory,
|
|
@@ -83,14 +65,12 @@ export async function executeSyncDirectoryPlan(plan, options) {
|
|
|
83
65
|
}
|
|
84
66
|
return outcomes;
|
|
85
67
|
}
|
|
86
|
-
function recordSkipped(outcomes, reporter, dir, reason
|
|
68
|
+
function recordSkipped(outcomes, reporter, dir, reason) {
|
|
87
69
|
outcomes.push({ kind: "skipped", dir, reason });
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
reporter.write(`syncDirs: ${reason}, skipped ${dir}\n`);
|
|
93
|
-
}
|
|
70
|
+
if (reporter.skipped)
|
|
71
|
+
reporter.skipped({ dir, reason });
|
|
72
|
+
else
|
|
73
|
+
reporter.write(`syncDirs: ${reason}, skipped ${dir}\n`);
|
|
94
74
|
}
|
|
95
75
|
function toErrorMessage(error) {
|
|
96
76
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -19,7 +19,6 @@ export async function bootstrapWorktree(options) {
|
|
|
19
19
|
const syncPlan = await prepareSyncDirectoryPlan(options.repoRoot, options.worktreePath, options.config.syncDirs ?? []);
|
|
20
20
|
const outcomes = await executeSyncDirectoryPlan(syncPlan, {
|
|
21
21
|
cloneDirectory: options.cloneDirectory ?? cloneDir,
|
|
22
|
-
repoRoot: options.repoRoot,
|
|
23
22
|
reporter: options.reporter,
|
|
24
23
|
});
|
|
25
24
|
const clonedDirs = outcomes.flatMap((outcome) => outcome.kind === "cloned" ? [outcome.directory] : []);
|
|
@@ -53,7 +52,6 @@ export async function bootstrapWorktree(options) {
|
|
|
53
52
|
? { mode: dependencyMode, ready: false, events: [] }
|
|
54
53
|
: await executeDependencyBootstrap(dependencyPlan, {
|
|
55
54
|
cloneDirectory: options.cloneDirectory,
|
|
56
|
-
repoRoot: options.repoRoot,
|
|
57
55
|
reporter: options.reporter,
|
|
58
56
|
stderr: options.commandStderr ?? options.reporter.write,
|
|
59
57
|
stdout: options.commandStdout,
|
package/dist/worktree-picker.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { env, platform, stdin, stdout } from "node:process";
|
|
2
2
|
import { isCancel, Prompt } from "@clack/core";
|
|
3
|
+
import { spinner } from "@clack/prompts";
|
|
3
4
|
import { loadHistory } from "./history.js";
|
|
4
5
|
import { createPullRequestQuery, } from "./pull-requests.js";
|
|
5
6
|
import { readTask } from "./task.js";
|
|
@@ -8,6 +9,18 @@ import { readWorktreeInfos, } from "./worktree-info.js";
|
|
|
8
9
|
const MAX_PULL_REQUEST_REPOSITORY_QUERY_CONCURRENCY = 4;
|
|
9
10
|
const MAX_TASK_READ_CONCURRENCY = 8;
|
|
10
11
|
export async function buildWorktreePromptEntries(sources, dependencies = {}) {
|
|
12
|
+
const loading = stdin.isTTY === true && stdout.isTTY === true
|
|
13
|
+
? spinner({ indicator: "timer" })
|
|
14
|
+
: null;
|
|
15
|
+
loading?.start("Loading worktrees");
|
|
16
|
+
try {
|
|
17
|
+
return await buildWorktreePromptEntriesWithoutLoading(sources, dependencies);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
loading?.stop();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function buildWorktreePromptEntriesWithoutLoading(sources, dependencies) {
|
|
11
24
|
const metadataMode = dependencies.metadata ?? "full";
|
|
12
25
|
const includeMetadata = metadataMode === "full";
|
|
13
26
|
const pullRequestQuery = createPullRequestQuery();
|
package/man/man1/gji-back.1
CHANGED
package/man/man1/gji-clean.1
CHANGED
package/man/man1/gji-config.1
CHANGED
package/man/man1/gji-doctor.1
CHANGED
package/man/man1/gji-done.1
CHANGED
package/man/man1/gji-go.1
CHANGED
package/man/man1/gji-history.1
CHANGED
package/man/man1/gji-init.1
CHANGED
package/man/man1/gji-ls.1
CHANGED
package/man/man1/gji-new.1
CHANGED
package/man/man1/gji-open.1
CHANGED
package/man/man1/gji-pr.1
CHANGED
package/man/man1/gji-remove.1
CHANGED
package/man/man1/gji-root.1
CHANGED
package/man/man1/gji-run-hook.1
CHANGED
package/man/man1/gji-status.1
CHANGED
package/man/man1/gji-sync.1
CHANGED
package/man/man1/gji-task.1
CHANGED
package/man/man1/gji-undo.1
CHANGED
package/man/man1/gji-warp.1
CHANGED
package/man/man1/gji.1
CHANGED
package/package.json
CHANGED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface CloneFailureStore {
|
|
2
|
-
isCached(repoRoot: string, directory: string, scope?: string): Promise<boolean>;
|
|
3
|
-
cache(repoRoot: string, directory: string, reason: string, scope?: string): Promise<void>;
|
|
4
|
-
clear(repoRoot: string, directory: string, scope?: string): Promise<void>;
|
|
5
|
-
}
|
|
6
|
-
export declare class FileCloneFailureStore implements CloneFailureStore {
|
|
7
|
-
private updateQueue;
|
|
8
|
-
isCached(repoRoot: string, directory: string, scope?: string): Promise<boolean>;
|
|
9
|
-
cache(repoRoot: string, directory: string, reason: string, scope?: string): Promise<void>;
|
|
10
|
-
clear(repoRoot: string, directory: string, scope?: string): Promise<void>;
|
|
11
|
-
private update;
|
|
12
|
-
private withStateLock;
|
|
13
|
-
private readState;
|
|
14
|
-
private writeState;
|
|
15
|
-
private stateFilePath;
|
|
16
|
-
}
|
|
17
|
-
export declare const defaultCloneFailureStore: CloneFailureStore;
|
|
18
|
-
export declare function cloneFailureScope(source: string, destination: string): Promise<string>;
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, mkdtemp, readdir, readFile, rename, rm, rmdir, stat, unlink, utimes, writeFile, } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { GLOBAL_CONFIG_DIRECTORY } from "./config.js";
|
|
6
|
-
const STATE_FILE_NAME = "state.json";
|
|
7
|
-
const STATE_LOCK_SUFFIX = ".lock";
|
|
8
|
-
const CLONE_FAILURE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
-
const STATE_LOCK_TTL_MS = 30 * 1000;
|
|
10
|
-
export class FileCloneFailureStore {
|
|
11
|
-
updateQueue = Promise.resolve();
|
|
12
|
-
async isCached(repoRoot, directory, scope) {
|
|
13
|
-
const state = await this.readState();
|
|
14
|
-
const repoState = state.syncDirs?.[repoRoot];
|
|
15
|
-
const key = failureKey(directory, scope);
|
|
16
|
-
if (!repoState || !Object.hasOwn(repoState, key))
|
|
17
|
-
return false;
|
|
18
|
-
const failure = repoState[key];
|
|
19
|
-
return (isPlainObject(failure) &&
|
|
20
|
-
typeof failure.failedAt === "number" &&
|
|
21
|
-
Date.now() - failure.failedAt < CLONE_FAILURE_TTL_MS);
|
|
22
|
-
}
|
|
23
|
-
async cache(repoRoot, directory, reason, scope) {
|
|
24
|
-
await this.update(async () => {
|
|
25
|
-
const state = await this.readState();
|
|
26
|
-
const syncDirs = state.syncDirs ?? {};
|
|
27
|
-
const repoState = syncDirs[repoRoot] ?? {};
|
|
28
|
-
const key = failureKey(directory, scope);
|
|
29
|
-
syncDirs[repoRoot] = {
|
|
30
|
-
...repoState,
|
|
31
|
-
[key]: { failedAt: Date.now(), reason },
|
|
32
|
-
};
|
|
33
|
-
await this.writeState({ ...state, syncDirs });
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
async clear(repoRoot, directory, scope) {
|
|
37
|
-
await this.update(async () => {
|
|
38
|
-
const state = await this.readState();
|
|
39
|
-
const repoState = state.syncDirs?.[repoRoot];
|
|
40
|
-
const key = failureKey(directory, scope);
|
|
41
|
-
if (!repoState || !Object.hasOwn(repoState, key))
|
|
42
|
-
return;
|
|
43
|
-
const nextRepoState = { ...repoState };
|
|
44
|
-
delete nextRepoState[key];
|
|
45
|
-
const syncDirs = { ...state.syncDirs };
|
|
46
|
-
if (Object.keys(nextRepoState).length === 0)
|
|
47
|
-
delete syncDirs[repoRoot];
|
|
48
|
-
else
|
|
49
|
-
syncDirs[repoRoot] = nextRepoState;
|
|
50
|
-
await this.writeState({ ...state, syncDirs });
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
async update(operation) {
|
|
54
|
-
const next = this.updateQueue.then(() => this.withStateLock(operation), () => this.withStateLock(operation));
|
|
55
|
-
this.updateQueue = next.then(() => undefined, () => undefined);
|
|
56
|
-
await next;
|
|
57
|
-
}
|
|
58
|
-
async withStateLock(operation) {
|
|
59
|
-
const lockPath = `${this.stateFilePath()}${STATE_LOCK_SUFFIX}`;
|
|
60
|
-
const lockToken = await acquireStateLock(lockPath);
|
|
61
|
-
if (lockToken === undefined)
|
|
62
|
-
return;
|
|
63
|
-
const stopHeartbeat = startStateLockHeartbeat(lockPath, lockToken);
|
|
64
|
-
try {
|
|
65
|
-
await operation();
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
stopHeartbeat();
|
|
69
|
-
await releaseStateLock(lockPath, lockToken);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
async readState() {
|
|
73
|
-
try {
|
|
74
|
-
const raw = await readFile(this.stateFilePath(), "utf8");
|
|
75
|
-
const parsed = JSON.parse(raw);
|
|
76
|
-
if (!isPlainObject(parsed))
|
|
77
|
-
return {};
|
|
78
|
-
return isPlainObject(parsed.syncDirs)
|
|
79
|
-
? parsed
|
|
80
|
-
: { ...parsed, syncDirs: {} };
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
return {};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
async writeState(state) {
|
|
87
|
-
try {
|
|
88
|
-
const path = this.stateFilePath();
|
|
89
|
-
const directory = dirname(path);
|
|
90
|
-
await mkdir(directory, { recursive: true });
|
|
91
|
-
const temporaryDirectory = await mkdtemp(join(directory, `.gji-state-${randomUUID()}-`));
|
|
92
|
-
const temporaryPath = join(temporaryDirectory, STATE_FILE_NAME);
|
|
93
|
-
try {
|
|
94
|
-
await writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
95
|
-
await rename(temporaryPath, path);
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
await rm(temporaryDirectory, { force: true, recursive: true });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
// The cache is advisory and must never block worktree creation.
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
stateFilePath(home = homedir()) {
|
|
106
|
-
const configuredDirectory = process.env.GJI_CONFIG_DIR;
|
|
107
|
-
const directory = configuredDirectory
|
|
108
|
-
? resolve(configuredDirectory)
|
|
109
|
-
: join(home, GLOBAL_CONFIG_DIRECTORY);
|
|
110
|
-
return join(directory, STATE_FILE_NAME);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
async function acquireStateLock(lockPath) {
|
|
114
|
-
const lockToken = randomUUID();
|
|
115
|
-
await mkdir(dirname(lockPath), { recursive: true }).catch(() => undefined);
|
|
116
|
-
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
117
|
-
try {
|
|
118
|
-
await mkdir(lockPath);
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
if (!isErrorCode(error, "EEXIST"))
|
|
122
|
-
return undefined;
|
|
123
|
-
try {
|
|
124
|
-
const freshnessPath = await lockFreshnessPath(lockPath);
|
|
125
|
-
const lockStats = await stat(freshnessPath);
|
|
126
|
-
if (Date.now() - lockStats.mtimeMs >= STATE_LOCK_TTL_MS) {
|
|
127
|
-
const stalePath = `${lockPath}.stale-${randomUUID()}`;
|
|
128
|
-
try {
|
|
129
|
-
await rename(lockPath, stalePath);
|
|
130
|
-
}
|
|
131
|
-
catch (renameError) {
|
|
132
|
-
if (isErrorCode(renameError, "ENOENT"))
|
|
133
|
-
continue;
|
|
134
|
-
return undefined;
|
|
135
|
-
}
|
|
136
|
-
await rm(stalePath, { force: true, recursive: true });
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch (lockError) {
|
|
141
|
-
if (!isErrorCode(lockError, "ENOENT"))
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
await writeFile(join(lockPath, ownerFileName(lockToken)), `${lockToken}\n`, {
|
|
149
|
-
flag: "wx",
|
|
150
|
-
});
|
|
151
|
-
return lockToken;
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
await rm(lockPath, { force: true, recursive: true }).catch(() => undefined);
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return undefined;
|
|
159
|
-
}
|
|
160
|
-
async function lockFreshnessPath(lockPath) {
|
|
161
|
-
try {
|
|
162
|
-
const owner = (await readdir(lockPath)).find((entry) => entry.startsWith("owner-"));
|
|
163
|
-
return owner ? join(lockPath, owner) : lockPath;
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
if (isErrorCode(error, "ENOENT"))
|
|
167
|
-
throw error;
|
|
168
|
-
return lockPath;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
function startStateLockHeartbeat(lockPath, lockToken) {
|
|
172
|
-
const timer = setInterval(() => {
|
|
173
|
-
void refreshStateLock(lockPath, lockToken);
|
|
174
|
-
}, STATE_LOCK_TTL_MS / 3);
|
|
175
|
-
timer.unref?.();
|
|
176
|
-
return () => clearInterval(timer);
|
|
177
|
-
}
|
|
178
|
-
async function refreshStateLock(lockPath, lockToken) {
|
|
179
|
-
try {
|
|
180
|
-
const ownerPath = join(lockPath, ownerFileName(lockToken));
|
|
181
|
-
await readFile(ownerPath, "utf8");
|
|
182
|
-
const now = new Date();
|
|
183
|
-
await utimes(ownerPath, now, now);
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
// The cache is advisory; a failed heartbeat must not block worktree creation.
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
async function releaseStateLock(lockPath, lockToken) {
|
|
190
|
-
try {
|
|
191
|
-
const ownerPath = join(lockPath, ownerFileName(lockToken));
|
|
192
|
-
await unlink(ownerPath);
|
|
193
|
-
await rmdir(lockPath);
|
|
194
|
-
}
|
|
195
|
-
catch (error) {
|
|
196
|
-
if (!isErrorCode(error, "ENOENT") && !isErrorCode(error, "ENOTEMPTY")) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function ownerFileName(lockToken) {
|
|
202
|
-
return `owner-${lockToken}`;
|
|
203
|
-
}
|
|
204
|
-
export const defaultCloneFailureStore = new FileCloneFailureStore();
|
|
205
|
-
export async function cloneFailureScope(source, destination) {
|
|
206
|
-
const sourcePath = resolve(source);
|
|
207
|
-
const destinationParent = resolve(dirname(destination));
|
|
208
|
-
const [sourceDevice, destinationDevice] = await Promise.all([
|
|
209
|
-
readDevice(sourcePath),
|
|
210
|
-
readDevice(destinationParent),
|
|
211
|
-
]);
|
|
212
|
-
return JSON.stringify([sourcePath, sourceDevice, destinationDevice]);
|
|
213
|
-
}
|
|
214
|
-
async function readDevice(path) {
|
|
215
|
-
try {
|
|
216
|
-
return (await stat(path)).dev;
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
return undefined;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
function failureKey(directory, scope) {
|
|
223
|
-
return scope === undefined ? directory : JSON.stringify([scope, directory]);
|
|
224
|
-
}
|
|
225
|
-
function isPlainObject(value) {
|
|
226
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
227
|
-
}
|
|
228
|
-
function isErrorCode(error, code) {
|
|
229
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
230
|
-
}
|