@fluado/enmetu 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +168 -2
- package/dist/adapters/clock/systemClockAdapter.d.ts +6 -0
- package/dist/adapters/clock/systemClockAdapter.js +12 -0
- package/dist/adapters/fs/nodeFileSystemAdapter.d.ts +18 -0
- package/dist/adapters/fs/nodeFileSystemAdapter.js +230 -0
- package/dist/adapters/git/gitFacade.d.ts +148 -0
- package/dist/adapters/git/gitFacade.js +822 -0
- package/dist/adapters/git/gitLocatorFacade.d.ts +8 -0
- package/dist/adapters/git/gitLocatorFacade.js +72 -0
- package/dist/adapters/git/gitProcessAdapter.d.ts +46 -0
- package/dist/adapters/git/gitProcessAdapter.js +261 -0
- package/dist/adapters/log/nullLogger.d.ts +9 -0
- package/dist/adapters/log/nullLogger.js +10 -0
- package/dist/adapters/log/stderrLoggerAdapter.d.ts +10 -0
- package/dist/adapters/log/stderrLoggerAdapter.js +13 -0
- package/dist/adapters/process/nodeProcessAdapter.d.ts +9 -0
- package/dist/adapters/process/nodeProcessAdapter.js +56 -0
- package/dist/adapters/watch/fsWatchAdapter.d.ts +24 -0
- package/dist/adapters/watch/fsWatchAdapter.js +47 -0
- package/dist/cli/commands/hookCommand.d.ts +28 -0
- package/dist/cli/commands/hookCommand.js +90 -0
- package/dist/cli/commands/resumeCommand.d.ts +48 -0
- package/dist/cli/commands/resumeCommand.js +124 -0
- package/dist/cli/commands/runCommand.d.ts +33 -0
- package/dist/cli/commands/runCommand.js +69 -0
- package/dist/cli/commands/statusCommand.d.ts +71 -0
- package/dist/cli/commands/statusCommand.js +249 -0
- package/dist/cli/exitCodes.d.ts +7 -0
- package/dist/cli/exitCodes.js +39 -0
- package/dist/cli/helpText.d.ts +1 -0
- package/dist/cli/helpText.js +16 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +49 -0
- package/dist/cli/parseArgs.d.ts +19 -0
- package/dist/cli/parseArgs.js +106 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +32 -0
- package/dist/commit/commitLocalChangesController.d.ts +132 -0
- package/dist/commit/commitLocalChangesController.js +358 -0
- package/dist/commit/gitIndexLock.d.ts +36 -0
- package/dist/commit/gitIndexLock.js +90 -0
- package/dist/commit/repairStaleIndexController.d.ts +90 -0
- package/dist/commit/repairStaleIndexController.js +150 -0
- package/dist/commit/stageGuardedChanges.d.ts +30 -0
- package/dist/commit/stageGuardedChanges.js +52 -0
- package/dist/contracts/IClock.d.ts +7 -0
- package/dist/contracts/IClock.js +1 -0
- package/dist/contracts/IDaemon.d.ts +26 -0
- package/dist/contracts/IDaemon.js +1 -0
- package/dist/contracts/IFileSystem.d.ts +50 -0
- package/dist/contracts/IFileSystem.js +1 -0
- package/dist/contracts/IGitCommitter.d.ts +60 -0
- package/dist/contracts/IGitCommitter.js +1 -0
- package/dist/contracts/IGitConfigReader.d.ts +5 -0
- package/dist/contracts/IGitConfigReader.js +1 -0
- package/dist/contracts/IGitIndexLock.d.ts +44 -0
- package/dist/contracts/IGitIndexLock.js +1 -0
- package/dist/contracts/IGitReader.d.ts +81 -0
- package/dist/contracts/IGitReader.js +1 -0
- package/dist/contracts/IGitRunner.d.ts +49 -0
- package/dist/contracts/IGitRunner.js +1 -0
- package/dist/contracts/IGitWriter.d.ts +39 -0
- package/dist/contracts/IGitWriter.js +1 -0
- package/dist/contracts/IHaltStore.d.ts +14 -0
- package/dist/contracts/IHaltStore.js +1 -0
- package/dist/contracts/IHookInstaller.d.ts +14 -0
- package/dist/contracts/IHookInstaller.js +1 -0
- package/dist/contracts/IInstanceLock.d.ts +26 -0
- package/dist/contracts/IInstanceLock.js +1 -0
- package/dist/contracts/ILogger.d.ts +11 -0
- package/dist/contracts/ILogger.js +1 -0
- package/dist/contracts/IOperationJournal.d.ts +12 -0
- package/dist/contracts/IOperationJournal.js +1 -0
- package/dist/contracts/IPathQuarantine.d.ts +67 -0
- package/dist/contracts/IPathQuarantine.js +1 -0
- package/dist/contracts/IProcessHost.d.ts +11 -0
- package/dist/contracts/IProcessHost.js +1 -0
- package/dist/contracts/IRepositoryLocator.d.ts +28 -0
- package/dist/contracts/IRepositoryLocator.js +1 -0
- package/dist/contracts/IStateRecorder.d.ts +68 -0
- package/dist/contracts/IStateRecorder.js +1 -0
- package/dist/contracts/IStateStore.d.ts +88 -0
- package/dist/contracts/IStateStore.js +1 -0
- package/dist/contracts/ISyncCycle.d.ts +23 -0
- package/dist/contracts/ISyncCycle.js +1 -0
- package/dist/contracts/ISyncCycleRunner.d.ts +4 -0
- package/dist/contracts/ISyncCycleRunner.js +1 -0
- package/dist/contracts/ISyncScheduler.d.ts +6 -0
- package/dist/contracts/ISyncScheduler.js +1 -0
- package/dist/contracts/ITextSink.d.ts +3 -0
- package/dist/contracts/ITextSink.js +1 -0
- package/dist/contracts/IWatcher.d.ts +9 -0
- package/dist/contracts/IWatcher.js +1 -0
- package/dist/contracts/config.d.ts +20 -0
- package/dist/contracts/config.js +1 -0
- package/dist/contracts/errors.d.ts +39 -0
- package/dist/contracts/errors.js +64 -0
- package/dist/contracts/exitCode.d.ts +10 -0
- package/dist/contracts/exitCode.js +17 -0
- package/dist/contracts/gitTypes.d.ts +113 -0
- package/dist/contracts/gitTypes.js +1 -0
- package/dist/contracts/library.d.ts +35 -0
- package/dist/contracts/library.js +1 -0
- package/dist/contracts/logCodes.d.ts +1 -0
- package/dist/contracts/logCodes.js +1 -0
- package/dist/contracts/runtime.d.ts +16 -0
- package/dist/contracts/runtime.js +1 -0
- package/dist/contracts/syncTypes.d.ts +75 -0
- package/dist/contracts/syncTypes.js +6 -0
- package/dist/core/commit/commitMemory.d.ts +3 -0
- package/dist/core/commit/commitMemory.js +9 -0
- package/dist/core/commit/commitMessage.d.ts +6 -0
- package/dist/core/commit/commitMessage.js +12 -0
- package/dist/core/config/parseConfig.d.ts +5 -0
- package/dist/core/config/parseConfig.js +196 -0
- package/dist/core/content/contentRules.d.ts +11 -0
- package/dist/core/content/contentRules.js +50 -0
- package/dist/core/errors/errorMessage.d.ts +6 -0
- package/dist/core/errors/errorMessage.js +14 -0
- package/dist/core/errors/withCycleId.d.ts +7 -0
- package/dist/core/errors/withCycleId.js +38 -0
- package/dist/core/git/enmetuRefs.d.ts +9 -0
- package/dist/core/git/enmetuRefs.js +22 -0
- package/dist/core/git/porcelain.d.ts +51 -0
- package/dist/core/git/porcelain.js +219 -0
- package/dist/core/git/rebaseOwnership.d.ts +28 -0
- package/dist/core/git/rebaseOwnership.js +29 -0
- package/dist/core/git/stoppedRebaseWrites.d.ts +10 -0
- package/dist/core/git/stoppedRebaseWrites.js +22 -0
- package/dist/core/halt/resumeHint.d.ts +6 -0
- package/dist/core/halt/resumeHint.js +9 -0
- package/dist/core/log/formatLogLine.d.ts +3 -0
- package/dist/core/log/formatLogLine.js +11 -0
- package/dist/core/quarantine/keptBlobMessage.d.ts +5 -0
- package/dist/core/quarantine/keptBlobMessage.js +8 -0
- package/dist/core/quarantine/restoreWarnings.d.ts +16 -0
- package/dist/core/quarantine/restoreWarnings.js +36 -0
- package/dist/core/schedule/schedulerStep.d.ts +63 -0
- package/dist/core/schedule/schedulerStep.js +144 -0
- package/dist/core/state/stateGuards.d.ts +21 -0
- package/dist/core/state/stateGuards.js +295 -0
- package/dist/core/state/stateLiveness.d.ts +10 -0
- package/dist/core/state/stateLiveness.js +21 -0
- package/dist/core/state/stateRecordStep.d.ts +28 -0
- package/dist/core/state/stateRecordStep.js +203 -0
- package/dist/core/status/conflictGuide.d.ts +30 -0
- package/dist/core/status/conflictGuide.js +86 -0
- package/dist/core/status/orphanedCommits.d.ts +15 -0
- package/dist/core/status/orphanedCommits.js +24 -0
- package/dist/core/status/statusHeadline.d.ts +46 -0
- package/dist/core/status/statusHeadline.js +130 -0
- package/dist/core/watch/watchFilter.d.ts +2 -0
- package/dist/core/watch/watchFilter.js +34 -0
- package/dist/daemon/daemon.d.ts +55 -0
- package/dist/daemon/daemon.js +277 -0
- package/dist/enmetuInstances.d.ts +65 -0
- package/dist/enmetuInstances.js +333 -0
- package/dist/halt/haltGuardGitWriter.d.ts +23 -0
- package/dist/halt/haltGuardGitWriter.js +61 -0
- package/dist/halt/haltStore.d.ts +13 -0
- package/dist/halt/haltStore.js +49 -0
- package/dist/halt/recordHalt.d.ts +19 -0
- package/dist/halt/recordHalt.js +20 -0
- package/dist/hooks/hookScript.d.ts +22 -0
- package/dist/hooks/hookScript.js +52 -0
- package/dist/hooks/nullHookInstaller.d.ts +5 -0
- package/dist/hooks/nullHookInstaller.js +6 -0
- package/dist/hooks/preCommitCheckController.d.ts +23 -0
- package/dist/hooks/preCommitCheckController.js +22 -0
- package/dist/hooks/preCommitHookInstaller.d.ts +32 -0
- package/dist/hooks/preCommitHookInstaller.js +113 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +38 -0
- package/dist/library/enmetuLibrary.d.ts +16 -0
- package/dist/library/enmetuLibrary.js +62 -0
- package/dist/library/readStateController.d.ts +28 -0
- package/dist/library/readStateController.js +32 -0
- package/dist/library/requestedStopProcessHost.d.ts +28 -0
- package/dist/library/requestedStopProcessHost.js +58 -0
- package/dist/lock/instanceLock.d.ts +26 -0
- package/dist/lock/instanceLock.js +125 -0
- package/dist/preflight/repositoryPreflight.d.ts +20 -0
- package/dist/preflight/repositoryPreflight.js +42 -0
- package/dist/quarantine/pathQuarantine.d.ts +113 -0
- package/dist/quarantine/pathQuarantine.js +504 -0
- package/dist/recovery/enmetuRebaseAbort.d.ts +108 -0
- package/dist/recovery/enmetuRebaseAbort.js +302 -0
- package/dist/recovery/operationJournal.d.ts +13 -0
- package/dist/recovery/operationJournal.js +37 -0
- package/dist/recovery/recoveryController.d.ts +48 -0
- package/dist/recovery/recoveryController.js +112 -0
- package/dist/state/stateFileStore.d.ts +18 -0
- package/dist/state/stateFileStore.js +73 -0
- package/dist/state/stateRecorder.d.ts +26 -0
- package/dist/state/stateRecorder.js +69 -0
- package/dist/state/stateRecordingSyncCycle.d.ts +24 -0
- package/dist/state/stateRecordingSyncCycle.js +71 -0
- package/dist/sync/hookAbortingSyncScheduler.d.ts +16 -0
- package/dist/sync/hookAbortingSyncScheduler.js +27 -0
- package/dist/sync/integrateUpstreamController.d.ts +95 -0
- package/dist/sync/integrateUpstreamController.js +353 -0
- package/dist/sync/syncCycleController.d.ts +71 -0
- package/dist/sync/syncCycleController.js +345 -0
- package/dist/sync/syncCycleRunner.d.ts +24 -0
- package/dist/sync/syncCycleRunner.js +169 -0
- package/dist/sync/syncScheduler.d.ts +32 -0
- package/dist/sync/syncScheduler.js +97 -0
- package/package.json +42 -4
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { EnmetuError, GitCommandError } from '../contracts/errors.js';
|
|
2
|
+
import { withLastMove } from '../core/commit/commitMemory.js';
|
|
3
|
+
import { gitBoolean } from '../core/commit/commitMessage.js';
|
|
4
|
+
import { repairStaleIndexController } from './repairStaleIndexController.js';
|
|
5
|
+
import { stageGuardedChanges } from './stageGuardedChanges.js';
|
|
6
|
+
/** An unchanged tree a hook rejected is offered to the hooks again after this long (design 12.7). */
|
|
7
|
+
export const HOOK_RETRY_MS = 600000;
|
|
8
|
+
/**
|
|
9
|
+
* Controller: enmetu's commit without touching the person's index (design
|
|
10
|
+
* 12.7, Decisions 72 to 74, 80, 81).
|
|
11
|
+
*
|
|
12
|
+
* 1. before = fs.identity(realIndex), read first, so the staged-selection
|
|
13
|
+
* verdict below still holds when the identity is unchanged under the lock.
|
|
14
|
+
* Staged selection: git.hasStagedChanges() on the real index. When it
|
|
15
|
+
* differs and memory.lastMove is set, repairStaleIndexController first
|
|
16
|
+
* (Decision 98): moved, repaired or reverted (Decision 108) forgets lastMove; repaired reads
|
|
17
|
+
* before and the verdict again, so a stale index is never taken for a
|
|
18
|
+
* person's selection. "wait"
|
|
19
|
+
* without a reached cap returns waiting PARTIAL_SELECTION_STAGED; a reached
|
|
20
|
+
* cap logs SELECTION_WAIT_ENDED and goes on; "commit" goes on.
|
|
21
|
+
* 2. Build: parent = git.headCommit(); fs.copy(realIndex, commit index);
|
|
22
|
+
* committer.seedFrom(parent); stageGuardedChanges on the commit index.
|
|
23
|
+
* Nothing staged returns nothing.
|
|
24
|
+
* 3. tree = committer.writeTree(); equal to memory.residueTree returns nothing;
|
|
25
|
+
* equal to memory.rejection.tree within HOOK_RETRY_MS returns waiting
|
|
26
|
+
* COMMIT_HOOK_REJECTED without running a hook.
|
|
27
|
+
* 4. commitHooks: pre-commit on the commit index (rejected, timed out or
|
|
28
|
+
* killed: HOOK_REJECTED, waiting, rejection remembered); then re-stage and
|
|
29
|
+
* write the tree again, so a formatter's rewrite is in this commit. The
|
|
30
|
+
* message is commitSubject plus a newline, with or without hooks; the
|
|
31
|
+
* message hooks do not run (Decision 80). The re-stage logs through
|
|
32
|
+
* residueLogger: its EXCLUDED lines repeat the first build's.
|
|
33
|
+
* 5. commit = committer.commitTree({ tree, parent, message, sign: gitBoolean(commit.gpgSign) }).
|
|
34
|
+
* 6. Critical section: indexLock.acquire(commit) (false: busy);
|
|
35
|
+
* 6.1 repositoryState() with an operation: release, paused; HEAD naming
|
|
36
|
+
* anything but input.branchRef: release, busy (Decision 121);
|
|
37
|
+
* 6.2 fs.identity(realIndex) differing from before: release, busy;
|
|
38
|
+
* 6.3 indexLock.install(commit index);
|
|
39
|
+
* 6.4 writer.updateRef(input.branchRef, commit, parent) (a mismatch:
|
|
40
|
+
* restore, release, COMMIT_DISCARDED, busy; any other throw: restore,
|
|
41
|
+
* release, rethrow);
|
|
42
|
+
* 6.4b HEAD naming input.branchRef, or else resolving to the commit, keeps
|
|
43
|
+
* it; otherwise writer.updateRef(input.branchRef, parent, commit),
|
|
44
|
+
* restore, COMMIT_DISCARDED, busy (a roll-back failing with GIT_FAILED
|
|
45
|
+
* restores and returns busy the same way; any other throw restores and
|
|
46
|
+
* rethrows); release.
|
|
47
|
+
* 7. COMMITTED; with hooks: when realHooks.hookExists says post-index-change
|
|
48
|
+
* or post-commit exists, a build with residueLogger before the hooks;
|
|
49
|
+
* post-index-change "0 0" and post-commit on the real index (git ignores
|
|
50
|
+
* both exit codes, so does enmetu); then, only when such a hook exists and
|
|
51
|
+
* the build before found nothing uncommitted, a fresh build whose tree,
|
|
52
|
+
* when it differs from the commit's, becomes residueTree (Decision 90).
|
|
53
|
+
*/
|
|
54
|
+
export async function commitLocalChangesController(input, deps) {
|
|
55
|
+
const { cycleId } = input;
|
|
56
|
+
const { settings } = deps;
|
|
57
|
+
const now = deps.clock.now();
|
|
58
|
+
// --- Step 1: identity first, then the person's staged selection ---
|
|
59
|
+
let memory = input.memory;
|
|
60
|
+
let before = await deps.fs.identity(settings.realIndex);
|
|
61
|
+
let staged = await deps.git.hasStagedChanges();
|
|
62
|
+
if (staged && memory.lastMove !== null) {
|
|
63
|
+
const repair = await repairStaleIndexController({ cycleId, lastMove: memory.lastMove, before }, deps);
|
|
64
|
+
// reverted is forgotten too, so its STAGED_REVERT line comes once (Decision 108).
|
|
65
|
+
if (repair.kind === 'moved' ||
|
|
66
|
+
repair.kind === 'repaired' ||
|
|
67
|
+
repair.kind === 'reverted') {
|
|
68
|
+
memory = withLastMove(memory, null);
|
|
69
|
+
}
|
|
70
|
+
if (repair.kind === 'repaired') {
|
|
71
|
+
before = await deps.fs.identity(settings.realIndex);
|
|
72
|
+
staged = await deps.git.hasStagedChanges();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let since = null;
|
|
76
|
+
if (staged) {
|
|
77
|
+
if (settings.stagedSelection === 'wait') {
|
|
78
|
+
since = memory.stagedSelectionSince ?? now;
|
|
79
|
+
const cap = settings.stagedSelectionMaxWaitMs;
|
|
80
|
+
if (cap === null || now - since < cap) {
|
|
81
|
+
return {
|
|
82
|
+
kind: 'waiting',
|
|
83
|
+
reason: 'PARTIAL_SELECTION_STAGED',
|
|
84
|
+
memory: rememberSince(memory, since),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
deps.logger.log({
|
|
88
|
+
level: 'WARN',
|
|
89
|
+
code: 'SELECTION_WAIT_ENDED',
|
|
90
|
+
message: `a staged selection waited ${cap} ms, the configured cap; committing everything, the selection included`,
|
|
91
|
+
cycleId,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const unchanged = rememberSince(memory, since);
|
|
96
|
+
// --- Step 2: build on the commit index, seeded from HEAD ---
|
|
97
|
+
const parent = await deps.git.headCommit();
|
|
98
|
+
if (parent === null) {
|
|
99
|
+
// The preflight refuses an unborn HEAD; this guards the type.
|
|
100
|
+
throw new EnmetuError('HEAD is unborn; there is nothing to commit onto', 'HEAD_UNBORN', cycleId);
|
|
101
|
+
}
|
|
102
|
+
if (!(await build(parent, cycleId, deps.logger, deps))) {
|
|
103
|
+
return { kind: 'nothing', memory: unchanged };
|
|
104
|
+
}
|
|
105
|
+
// --- Step 3: trees enmetu already knows ---
|
|
106
|
+
let tree = await deps.commitIndex.committer.writeTree();
|
|
107
|
+
if (tree === memory.residueTree) {
|
|
108
|
+
return { kind: 'nothing', memory: unchanged };
|
|
109
|
+
}
|
|
110
|
+
const rejection = memory.rejection;
|
|
111
|
+
if (rejection !== null &&
|
|
112
|
+
tree === rejection.tree &&
|
|
113
|
+
now - rejection.at < HOOK_RETRY_MS) {
|
|
114
|
+
return {
|
|
115
|
+
kind: 'waiting',
|
|
116
|
+
reason: 'COMMIT_HOOK_REJECTED',
|
|
117
|
+
memory: unchanged,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// --- Step 4: pre-commit on the commit index, then re-stage its rewrites ---
|
|
121
|
+
if (settings.commitHooks) {
|
|
122
|
+
const preCommit = await deps.commitIndex.committer.runHook('pre-commit', []);
|
|
123
|
+
if (preCommit.kind === 'rejected') {
|
|
124
|
+
deps.logger.log({
|
|
125
|
+
level: 'WARN',
|
|
126
|
+
code: 'HOOK_REJECTED',
|
|
127
|
+
message: `pre-commit rejected the commit (${firstLine(preCommit.detail)}); enmetu offers it again when the files change, or after ${HOOK_RETRY_MS} ms`,
|
|
128
|
+
cycleId,
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
kind: 'waiting',
|
|
132
|
+
reason: 'COMMIT_HOOK_REJECTED',
|
|
133
|
+
memory: {
|
|
134
|
+
stagedSelectionSince: since,
|
|
135
|
+
rejection: { tree, at: now },
|
|
136
|
+
residueTree: memory.residueTree,
|
|
137
|
+
lastMove: memory.lastMove,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const restaged = await stageGuardedChanges({ cycleId }, {
|
|
142
|
+
git: deps.commitIndex.reader,
|
|
143
|
+
writer: deps.commitIndex.writer,
|
|
144
|
+
logger: deps.residueLogger,
|
|
145
|
+
skipJsonCheck: settings.skipJsonCheck,
|
|
146
|
+
});
|
|
147
|
+
if (!restaged.staged) {
|
|
148
|
+
return { kind: 'nothing', memory: unchanged };
|
|
149
|
+
}
|
|
150
|
+
tree = await deps.commitIndex.committer.writeTree();
|
|
151
|
+
}
|
|
152
|
+
// --- Step 5: the commit object; HEAD is its only parent, never MERGE_HEAD ---
|
|
153
|
+
// The subject is the whole message: no path ever reaches it (D1).
|
|
154
|
+
const commit = await deps.commitIndex.committer.commitTree({
|
|
155
|
+
tree,
|
|
156
|
+
parent,
|
|
157
|
+
message: `${settings.commitSubject}\n`,
|
|
158
|
+
sign: gitBoolean(await deps.git.configValue('commit.gpgSign')),
|
|
159
|
+
});
|
|
160
|
+
// --- Step 6: critical section under git's own index.lock ---
|
|
161
|
+
const swap = await installAndSwap(commit, parent, before, input, deps);
|
|
162
|
+
if (swap !== 'committed') {
|
|
163
|
+
return swap === 'busy'
|
|
164
|
+
? { kind: 'busy', memory: unchanged }
|
|
165
|
+
: { kind: 'paused', reason: 'FOREIGN_OPERATION', memory: unchanged };
|
|
166
|
+
}
|
|
167
|
+
// --- Step 7: after the lock ---
|
|
168
|
+
deps.logger.log({
|
|
169
|
+
level: 'INFO',
|
|
170
|
+
code: 'COMMITTED',
|
|
171
|
+
message: 'local changes',
|
|
172
|
+
cycleId,
|
|
173
|
+
});
|
|
174
|
+
let residueTree = null;
|
|
175
|
+
if (settings.commitHooks) {
|
|
176
|
+
residueTree = await runPostCommitHooks(commit, tree, cycleId, deps);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
kind: 'committed',
|
|
180
|
+
memory: {
|
|
181
|
+
stagedSelectionSince: null,
|
|
182
|
+
rejection: null,
|
|
183
|
+
residueTree,
|
|
184
|
+
lastMove: { from: parent, to: commit },
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Step 7 with hooks: post-index-change and post-commit on the real index, and
|
|
190
|
+
* the residue they leave (Decision 90). A residue is built only when one of
|
|
191
|
+
* them exists, so a repository without such hooks never mistakes a person's
|
|
192
|
+
* write for hook output. A build before the
|
|
193
|
+
* hooks tells what was already uncommitted: anything there (a person's write
|
|
194
|
+
* since the commit's build) means no residue at all, so the next cycle
|
|
195
|
+
* commits that write together with whatever the hooks wrote, and the cycle
|
|
196
|
+
* after it finds the residue alone.
|
|
197
|
+
*/
|
|
198
|
+
async function runPostCommitHooks(commit, tree, cycleId, deps) {
|
|
199
|
+
const mayWrite = (await deps.realHooks.hookExists('post-index-change')) ||
|
|
200
|
+
(await deps.realHooks.hookExists('post-commit'));
|
|
201
|
+
const pendingBefore = mayWrite && (await build(commit, cycleId, deps.residueLogger, deps));
|
|
202
|
+
// Both run through `git hook run --ignore-missing` whatever hookExists
|
|
203
|
+
// said, so what runs is git's decision alone; git ignores both exit codes,
|
|
204
|
+
// and so does enmetu.
|
|
205
|
+
await deps.realHooks.runHook('post-index-change', ['0', '0']);
|
|
206
|
+
await deps.realHooks.runHook('post-commit', []);
|
|
207
|
+
if (!mayWrite || pendingBefore) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
if (!(await build(commit, cycleId, deps.residueLogger, deps))) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const residue = await deps.commitIndex.committer.writeTree();
|
|
214
|
+
return residue === tree ? null : residue;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Step 2 of the chain: copy the real index (git's stat data and skip-worktree
|
|
218
|
+
* bits, so add -A does not re-hash every file), seed it with the commit's
|
|
219
|
+
* tree, stage the guarded working tree. true when anything is staged.
|
|
220
|
+
*/
|
|
221
|
+
async function build(seed, cycleId, logger, deps) {
|
|
222
|
+
const { commitIndex } = deps;
|
|
223
|
+
if (!(await deps.fs.copy(deps.settings.realIndex, commitIndex.path))) {
|
|
224
|
+
// No real index (git reads a missing index like an empty one): a leftover commit index must
|
|
225
|
+
// not lend its stat data.
|
|
226
|
+
await deps.fs.remove(commitIndex.path);
|
|
227
|
+
}
|
|
228
|
+
await commitIndex.committer.seedFrom(seed);
|
|
229
|
+
const { staged } = await stageGuardedChanges({ cycleId }, {
|
|
230
|
+
git: commitIndex.reader,
|
|
231
|
+
writer: commitIndex.writer,
|
|
232
|
+
logger,
|
|
233
|
+
skipJsonCheck: deps.settings.skipJsonCheck,
|
|
234
|
+
});
|
|
235
|
+
return staged;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Step 6: under index.lock, in this order (Decisions 72, 81, 121):
|
|
239
|
+
*
|
|
240
|
+
* 6.1 repositoryState(): an operation pauses; HEAD naming anything but
|
|
241
|
+
* branchRef (a person's git switch -c or --detach, which git lets through
|
|
242
|
+
* a held index.lock) is busy, nothing installed.
|
|
243
|
+
* 6.2 The real index's identity still equals before, else busy.
|
|
244
|
+
* 6.3 Install the commit index, so a git commit that reads enmetu's branch
|
|
245
|
+
* also reads enmetu's index.
|
|
246
|
+
* 6.4 Compare-and-swap of branchRef, not HEAD: a swap through HEAD would
|
|
247
|
+
* follow a switch made after 6.1 onto the person's branch.
|
|
248
|
+
* 6.4b HEAD still names branchRef, or resolves to the commit (a branch
|
|
249
|
+
* created or a detach made after the swap carries the commit along);
|
|
250
|
+
* otherwise the swap is undone by compare-and-swap and the index restored.
|
|
251
|
+
*
|
|
252
|
+
* The lock is released on every path.
|
|
253
|
+
*/
|
|
254
|
+
async function installAndSwap(commit, parent, before, input, deps) {
|
|
255
|
+
const { cycleId, branchRef } = input;
|
|
256
|
+
if (!(await deps.indexLock.acquire(commit))) {
|
|
257
|
+
return 'busy';
|
|
258
|
+
}
|
|
259
|
+
let installing = false;
|
|
260
|
+
let settled = false;
|
|
261
|
+
try {
|
|
262
|
+
// --- 6.1 ---
|
|
263
|
+
const state = await deps.git.repositoryState();
|
|
264
|
+
if (state.operation !== null) {
|
|
265
|
+
return 'paused';
|
|
266
|
+
}
|
|
267
|
+
if (state.branchRef !== branchRef) {
|
|
268
|
+
return 'busy';
|
|
269
|
+
}
|
|
270
|
+
// --- 6.2: a write of a person's git to the real index since step 1
|
|
271
|
+
// replaced the file; installing over it would discard what they staged ---
|
|
272
|
+
if ((await deps.fs.identity(deps.settings.realIndex)) !== before) {
|
|
273
|
+
return 'busy';
|
|
274
|
+
}
|
|
275
|
+
// --- 6.3 ---
|
|
276
|
+
installing = true;
|
|
277
|
+
await deps.indexLock.install(deps.commitIndex.path);
|
|
278
|
+
// --- 6.4 ---
|
|
279
|
+
try {
|
|
280
|
+
await deps.writer.updateRef(branchRef, commit, parent);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
settled = true;
|
|
284
|
+
await deps.indexLock.restore();
|
|
285
|
+
if (error instanceof GitCommandError && error.code === 'GIT_FAILED') {
|
|
286
|
+
deps.logger.log({
|
|
287
|
+
level: 'INFO',
|
|
288
|
+
code: 'COMMIT_DISCARDED',
|
|
289
|
+
message: 'a concurrent commit moved HEAD first; enmetu discards its commit and commits again on a later cycle',
|
|
290
|
+
cycleId,
|
|
291
|
+
});
|
|
292
|
+
return 'busy';
|
|
293
|
+
}
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
// From here the branch holds the commit and the installed index matches
|
|
297
|
+
// it; only a roll-back puts the person's index back, so a failed read
|
|
298
|
+
// below never leaves the index behind the branch.
|
|
299
|
+
settled = true;
|
|
300
|
+
// --- 6.4b ---
|
|
301
|
+
if (await headStillOnCommit(branchRef, commit, deps.git)) {
|
|
302
|
+
return 'committed';
|
|
303
|
+
}
|
|
304
|
+
await rollBack(branchRef, commit, parent, cycleId, deps);
|
|
305
|
+
return 'busy';
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (installing && !settled) {
|
|
309
|
+
await deps.indexLock.restore();
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
await deps.indexLock.release();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/** 6.4b: HEAD names branchRef, or, asked only when it does not, resolves to the commit. */
|
|
318
|
+
async function headStillOnCommit(branchRef, commit, git) {
|
|
319
|
+
if ((await git.repositoryState()).branchRef === branchRef) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
return (await git.headCommit()) === commit;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* 6.4b's compensation: branchRef back from the commit to its parent by
|
|
326
|
+
* compare-and-swap, then the person's index. A roll-back git refuses
|
|
327
|
+
* (GIT_FAILED: another command moved the branch after the swap) leaves the
|
|
328
|
+
* branch where that command put it; any other throw restores and rethrows.
|
|
329
|
+
*/
|
|
330
|
+
async function rollBack(branchRef, commit, parent, cycleId, deps) {
|
|
331
|
+
try {
|
|
332
|
+
await deps.writer.updateRef(branchRef, parent, commit);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (!(error instanceof GitCommandError && error.code === 'GIT_FAILED')) {
|
|
336
|
+
await deps.indexLock.restore();
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
await deps.indexLock.restore();
|
|
341
|
+
deps.logger.log({
|
|
342
|
+
level: 'INFO',
|
|
343
|
+
code: 'COMMIT_DISCARDED',
|
|
344
|
+
message: `HEAD left ${branchRef} while enmetu moved it to its commit (a git switch of yours, most likely); enmetu put ${branchRef} and the index back and commits again on a later cycle`,
|
|
345
|
+
cycleId,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function rememberSince(memory, since) {
|
|
349
|
+
return {
|
|
350
|
+
stagedSelectionSince: since,
|
|
351
|
+
rejection: memory.rejection,
|
|
352
|
+
residueTree: memory.residueTree,
|
|
353
|
+
lastMove: memory.lastMove,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function firstLine(text) {
|
|
357
|
+
return (text.trim().split('\n')[0] ?? '').trim();
|
|
358
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { IFileSystem } from '../contracts/IFileSystem.js';
|
|
2
|
+
import type { IGitIndexLock, StaleIndexLock } from '../contracts/IGitIndexLock.js';
|
|
3
|
+
import type { IProcessHost } from '../contracts/IProcessHost.js';
|
|
4
|
+
/** index.lock's content while enmetu holds it: `${INDEX_LOCK_MARKER}<pid>, commit <commit>\n`. */
|
|
5
|
+
export declare const INDEX_LOCK_MARKER = "enmetu index lock, pid ";
|
|
6
|
+
/** The person's index, kept under a second name while enmetu's commit index is installed. */
|
|
7
|
+
export declare const INDEX_BEFORE_FILE = "index.before";
|
|
8
|
+
/**
|
|
9
|
+
* Service: <gitDir>/index.lock (design 12.7, Decision 81).
|
|
10
|
+
* acquire(commit) is createExclusive(index.lock, the marker with pid and
|
|
11
|
+
* commit). install(prepared) removes a leftover <stateDir>/index.before, links
|
|
12
|
+
* <gitDir>/index to it (no link when there is no real index), then renames the
|
|
13
|
+
* prepared index over <gitDir>/index. restore() renames index.before back over
|
|
14
|
+
* <gitDir>/index. release() removes index.before, then the lock, and only when
|
|
15
|
+
* this instance holds it. releaseStale(head) reads the lock: not the marker, or
|
|
16
|
+
* a live pid: none. Otherwise HEAD equal to the marker's commit means the
|
|
17
|
+
* install happened before the crash (it precedes the swap): index.before is
|
|
18
|
+
* removed, kept. Any other HEAD: index.before, when present, is renamed back,
|
|
19
|
+
* restored. Then the lock is removed.
|
|
20
|
+
*/
|
|
21
|
+
export declare class GitIndexLock implements IGitIndexLock {
|
|
22
|
+
private readonly fs;
|
|
23
|
+
private readonly processHost;
|
|
24
|
+
private readonly gitDir;
|
|
25
|
+
private readonly stateDir;
|
|
26
|
+
private held;
|
|
27
|
+
constructor(fs: IFileSystem, processHost: IProcessHost, gitDir: string, stateDir: string);
|
|
28
|
+
acquire(commit: string): Promise<boolean>;
|
|
29
|
+
install(preparedIndex: string): Promise<void>;
|
|
30
|
+
restore(): Promise<void>;
|
|
31
|
+
release(): Promise<void>;
|
|
32
|
+
releaseStale(head: string | null): Promise<StaleIndexLock>;
|
|
33
|
+
private lockPath;
|
|
34
|
+
private indexPath;
|
|
35
|
+
private beforePath;
|
|
36
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
/** index.lock's content while enmetu holds it: `${INDEX_LOCK_MARKER}<pid>, commit <commit>\n`. */
|
|
3
|
+
export const INDEX_LOCK_MARKER = 'enmetu index lock, pid ';
|
|
4
|
+
/** The person's index, kept under a second name while enmetu's commit index is installed. */
|
|
5
|
+
export const INDEX_BEFORE_FILE = 'index.before';
|
|
6
|
+
const MARKER_LINE = /^enmetu index lock, pid ([0-9]+), commit (\S+)\n?$/;
|
|
7
|
+
/**
|
|
8
|
+
* Service: <gitDir>/index.lock (design 12.7, Decision 81).
|
|
9
|
+
* acquire(commit) is createExclusive(index.lock, the marker with pid and
|
|
10
|
+
* commit). install(prepared) removes a leftover <stateDir>/index.before, links
|
|
11
|
+
* <gitDir>/index to it (no link when there is no real index), then renames the
|
|
12
|
+
* prepared index over <gitDir>/index. restore() renames index.before back over
|
|
13
|
+
* <gitDir>/index. release() removes index.before, then the lock, and only when
|
|
14
|
+
* this instance holds it. releaseStale(head) reads the lock: not the marker, or
|
|
15
|
+
* a live pid: none. Otherwise HEAD equal to the marker's commit means the
|
|
16
|
+
* install happened before the crash (it precedes the swap): index.before is
|
|
17
|
+
* removed, kept. Any other HEAD: index.before, when present, is renamed back,
|
|
18
|
+
* restored. Then the lock is removed.
|
|
19
|
+
*/
|
|
20
|
+
export class GitIndexLock {
|
|
21
|
+
fs;
|
|
22
|
+
processHost;
|
|
23
|
+
gitDir;
|
|
24
|
+
stateDir;
|
|
25
|
+
held = false;
|
|
26
|
+
constructor(fs, processHost, gitDir, stateDir) {
|
|
27
|
+
this.fs = fs;
|
|
28
|
+
this.processHost = processHost;
|
|
29
|
+
this.gitDir = gitDir;
|
|
30
|
+
this.stateDir = stateDir;
|
|
31
|
+
}
|
|
32
|
+
async acquire(commit) {
|
|
33
|
+
const created = await this.fs.createExclusive(this.lockPath(), `${INDEX_LOCK_MARKER}${this.processHost.pid}, commit ${commit}\n`);
|
|
34
|
+
this.held = created;
|
|
35
|
+
return created;
|
|
36
|
+
}
|
|
37
|
+
async install(preparedIndex) {
|
|
38
|
+
// A leftover from a section that ended without release would make link(2)
|
|
39
|
+
// fail with EEXIST.
|
|
40
|
+
await this.fs.remove(this.beforePath());
|
|
41
|
+
// false: the repository has no index yet (git reads a missing index like an empty one), so
|
|
42
|
+
// there is nothing of the person's to keep.
|
|
43
|
+
await this.fs.link(this.indexPath(), this.beforePath());
|
|
44
|
+
await this.fs.rename(preparedIndex, this.indexPath());
|
|
45
|
+
}
|
|
46
|
+
async restore() {
|
|
47
|
+
await this.fs.rename(this.beforePath(), this.indexPath());
|
|
48
|
+
}
|
|
49
|
+
async release() {
|
|
50
|
+
if (!this.held) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await this.fs.remove(this.beforePath());
|
|
54
|
+
await this.fs.remove(this.lockPath());
|
|
55
|
+
this.held = false;
|
|
56
|
+
}
|
|
57
|
+
async releaseStale(head) {
|
|
58
|
+
const content = await this.fs.readText(this.lockPath());
|
|
59
|
+
if (content === null) {
|
|
60
|
+
return { kind: 'none' };
|
|
61
|
+
}
|
|
62
|
+
const match = MARKER_LINE.exec(content);
|
|
63
|
+
const pid = match?.[1];
|
|
64
|
+
const commit = match?.[2];
|
|
65
|
+
if (pid === undefined || commit === undefined) {
|
|
66
|
+
return { kind: 'none' };
|
|
67
|
+
}
|
|
68
|
+
if (this.processHost.isAlive(Number(pid))) {
|
|
69
|
+
return { kind: 'none' };
|
|
70
|
+
}
|
|
71
|
+
if (head === commit) {
|
|
72
|
+
await this.fs.remove(this.beforePath());
|
|
73
|
+
await this.fs.remove(this.lockPath());
|
|
74
|
+
return { kind: 'released', index: 'kept' };
|
|
75
|
+
}
|
|
76
|
+
// false when the crash came before the link: nothing was installed.
|
|
77
|
+
await this.fs.rename(this.beforePath(), this.indexPath());
|
|
78
|
+
await this.fs.remove(this.lockPath());
|
|
79
|
+
return { kind: 'released', index: 'restored' };
|
|
80
|
+
}
|
|
81
|
+
lockPath() {
|
|
82
|
+
return join(this.gitDir, 'index.lock');
|
|
83
|
+
}
|
|
84
|
+
indexPath() {
|
|
85
|
+
return join(this.gitDir, 'index');
|
|
86
|
+
}
|
|
87
|
+
beforePath() {
|
|
88
|
+
return join(this.stateDir, INDEX_BEFORE_FILE);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { IFileSystem } from '../contracts/IFileSystem.js';
|
|
2
|
+
import type { IGitIndexLock } from '../contracts/IGitIndexLock.js';
|
|
3
|
+
import type { IGitReader } from '../contracts/IGitReader.js';
|
|
4
|
+
import type { IGitWriter } from '../contracts/IGitWriter.js';
|
|
5
|
+
import type { ILogger } from '../contracts/ILogger.js';
|
|
6
|
+
import type { HeadMove } from '../contracts/syncTypes.js';
|
|
7
|
+
/** At most this many repaired paths are named in the log line; the count is always exact. */
|
|
8
|
+
export declare const MAX_REPAIRED_PATHS_LOGGED = 20;
|
|
9
|
+
export interface RepairStaleIndexInput {
|
|
10
|
+
readonly cycleId: string;
|
|
11
|
+
readonly lastMove: HeadMove;
|
|
12
|
+
/** fs.identity(realIndex), read before the staged paths, so an unchanged identity under the lock means they still hold. */
|
|
13
|
+
readonly before: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface RepairStaleIndexDeps {
|
|
16
|
+
/** The real index and HEAD. */
|
|
17
|
+
readonly git: IGitReader;
|
|
18
|
+
readonly commitIndex: {
|
|
19
|
+
/** Behind HaltGuardGitWriter; its unstage sets entries back to HEAD's on the commit index. */
|
|
20
|
+
readonly writer: IGitWriter;
|
|
21
|
+
/** <stateDir>/commit-index */
|
|
22
|
+
readonly path: string;
|
|
23
|
+
};
|
|
24
|
+
readonly indexLock: IGitIndexLock;
|
|
25
|
+
readonly fs: IFileSystem;
|
|
26
|
+
readonly logger: ILogger;
|
|
27
|
+
readonly settings: {
|
|
28
|
+
/** The working tree root, to read a path's working-tree side. */
|
|
29
|
+
readonly root: string;
|
|
30
|
+
/** <gitDir>/index */
|
|
31
|
+
readonly realIndex: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export type RepairStaleIndexResult =
|
|
35
|
+
/** HEAD is no longer lastMove.to: the memory has nothing left to repair against. */
|
|
36
|
+
{
|
|
37
|
+
readonly kind: 'moved';
|
|
38
|
+
}
|
|
39
|
+
/** No staged path carries the stale signature: nothing touched, the memory stays. */
|
|
40
|
+
| {
|
|
41
|
+
readonly kind: 'none';
|
|
42
|
+
}
|
|
43
|
+
/** index.lock held, an operation started or the real index changed meanwhile: nothing touched, tried again next cycle. */
|
|
44
|
+
| {
|
|
45
|
+
readonly kind: 'busy';
|
|
46
|
+
} | {
|
|
47
|
+
readonly kind: 'repaired';
|
|
48
|
+
readonly paths: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* No stale path, but staged paths whose index entry AND working tree are
|
|
52
|
+
* lastMove.from's: a failed git merge's restore_state left them (design
|
|
53
|
+
* 13.2, Decision 108), or the person reverted them. Named once in
|
|
54
|
+
* STAGED_REVERT, nothing written; the caller forgets lastMove, so the line
|
|
55
|
+
* is not repeated. A cycle that also repairs returns repaired and logs both.
|
|
56
|
+
*/
|
|
57
|
+
| {
|
|
58
|
+
readonly kind: 'reverted';
|
|
59
|
+
readonly paths: readonly string[];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Controller: sets back index entries that a person's git command wrote back
|
|
63
|
+
* from before enmetu's last move of HEAD, its commit or its pull (design
|
|
64
|
+
* 12.7, Decision 98). A command that read the real index before the move and
|
|
65
|
+
* writes it after (measured for git merge against enmetu's commit section;
|
|
66
|
+
* seen in the stress scenario against enmetu's rebase) leaves HEAD at
|
|
67
|
+
* lastMove.to and lastMove.from's entries in the index, which would revert
|
|
68
|
+
* the move in the person's next commit.
|
|
69
|
+
*
|
|
70
|
+
* 1. HEAD differs from lastMove.to: moved.
|
|
71
|
+
* 2. Candidates: git.stagedPaths() (index against HEAD), in git's order. A
|
|
72
|
+
* path is stale when its index entry equals lastMove.from's (both absent
|
|
73
|
+
* for a path the move added) while the working tree does not (absent, or
|
|
74
|
+
* a regular file whose filtered blob is not from's; a link or a directory
|
|
75
|
+
* is never stale). A path the move did not change has from's entry in
|
|
76
|
+
* HEAD, so it differs from HEAD only with another entry and never matches.
|
|
77
|
+
* Reverted: index entry and working tree both from's (design 13.2,
|
|
78
|
+
* Decision 108). Neither: none. Only reverted: STAGED_REVERT once, nothing
|
|
79
|
+
* written, reverted.
|
|
80
|
+
* 3. Prepare: fs.copy(realIndex, commit index), so every other entry, the
|
|
81
|
+
* person's staged selection included, stays unchanged; commitIndex.writer
|
|
82
|
+
* .unstage(stale) sets exactly those entries to HEAD's.
|
|
83
|
+
* 4. Critical section, shaped like the commit's (Decision 81): indexLock.acquire
|
|
84
|
+
* (false: busy); an operation in progress or the real index's identity
|
|
85
|
+
* differing from before: release, busy; HEAD moved: release, moved;
|
|
86
|
+
* indexLock.install(commit index); release on every path.
|
|
87
|
+
* 5. INDEX_REPAIRED once, naming the paths, then STAGED_REVERT for reverted
|
|
88
|
+
* paths; repaired. Busy or moved names nothing, so the next cycle does.
|
|
89
|
+
*/
|
|
90
|
+
export declare function repairStaleIndexController(input: RepairStaleIndexInput, deps: RepairStaleIndexDeps): Promise<RepairStaleIndexResult>;
|