@fluado/enmetu 0.0.0-stage → 0.2.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 +209 -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 +75 -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 +29 -0
- package/dist/cli/commands/hookCommand.js +92 -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 +34 -0
- package/dist/cli/commands/runCommand.js +79 -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 +38 -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 +357 -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 +32 -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 +26 -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 +8 -0
- package/dist/core/config/parseConfig.js +176 -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 +52 -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 +332 -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 +123 -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,504 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { errorMessage } from '../core/errors/errorMessage.js';
|
|
3
|
+
import { parseQuarantineMemento } from '../core/state/stateGuards.js';
|
|
4
|
+
export const QUARANTINE_FILE_NAME = 'quarantine.json';
|
|
5
|
+
/** Temp files of restored worktree paths, outside the tree so a crash cannot leave one to be committed. */
|
|
6
|
+
export const QUARANTINE_TEMP_DIR_NAME = 'tmp';
|
|
7
|
+
/** The inputs of a merge at restore (Decisions 94, 99), in the temp directory; one restore runs at a time, under the instance lock. */
|
|
8
|
+
const MERGE_BASE_FILE_NAME = 'quarantine-merge-base';
|
|
9
|
+
const MERGE_HELD_FILE_NAME = 'quarantine-merge-held';
|
|
10
|
+
const MERGE_HEAD_FILE_NAME = 'quarantine-merge-head';
|
|
11
|
+
/** The version a restore writes, flushed before the swap, and the second name of the file it replaces (Decision 117). */
|
|
12
|
+
const STAGED_FILE_NAME = 'quarantine-staged';
|
|
13
|
+
const PREVIOUS_FILE_NAME = 'quarantine-previous';
|
|
14
|
+
/** How often a restore judges an entry again when the file changed between its decision and its write. */
|
|
15
|
+
export const MAX_RESTORE_REJUDGES = 2;
|
|
16
|
+
/** What observe reads for an absent path, and for one that is not a regular file; never a blob id. */
|
|
17
|
+
const OBSERVED_ABSENT = 'absent';
|
|
18
|
+
const OBSERVED_OTHER = 'not a regular file';
|
|
19
|
+
function isRegularFile(observed) {
|
|
20
|
+
return observed !== OBSERVED_ABSENT && observed !== OBSERVED_OTHER;
|
|
21
|
+
}
|
|
22
|
+
const RESTORED = { kind: 'restored', overwritten: null };
|
|
23
|
+
/** The last entry per path: a later capture (extend) supersedes an earlier hold. */
|
|
24
|
+
function latestEntryByPath(entries) {
|
|
25
|
+
const latest = new Map();
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
latest.set(entry.path, entry);
|
|
28
|
+
}
|
|
29
|
+
return latest;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Service and Memento caretaker (D4): holds tracked paths that are still
|
|
33
|
+
* dirty after the commit step aside for the duration of a rebase, without a
|
|
34
|
+
* stash, and captures writes that land while enmetu's rebase is stopped
|
|
35
|
+
* before the abort resets them (Decision 45). The memento
|
|
36
|
+
* (<stateDir>/quarantine.json) is persisted before the tree changes and
|
|
37
|
+
* removed only after every entry was handled.
|
|
38
|
+
*/
|
|
39
|
+
export class PathQuarantine {
|
|
40
|
+
git;
|
|
41
|
+
writer;
|
|
42
|
+
fs;
|
|
43
|
+
paths;
|
|
44
|
+
constructor(git, writer, fs, paths) {
|
|
45
|
+
this.git = git;
|
|
46
|
+
this.writer = writer;
|
|
47
|
+
this.fs = fs;
|
|
48
|
+
this.paths = paths;
|
|
49
|
+
}
|
|
50
|
+
async hold(paths) {
|
|
51
|
+
// --- Step 1: never overwrite an earlier hold ---
|
|
52
|
+
if (await this.hasPending()) {
|
|
53
|
+
return { kind: 'pending' };
|
|
54
|
+
}
|
|
55
|
+
// An empty pathspec list must never reach restoreFromIndex: there is
|
|
56
|
+
// nothing to hold, so nothing is written either.
|
|
57
|
+
if (paths.length === 0) {
|
|
58
|
+
return { kind: 'held', count: 0 };
|
|
59
|
+
}
|
|
60
|
+
// --- Step 2: refuse what cannot be held byte for byte, before touching anything ---
|
|
61
|
+
const inspected = await this.inspect(paths);
|
|
62
|
+
if (inspected.kind === 'unsupported') {
|
|
63
|
+
return inspected;
|
|
64
|
+
}
|
|
65
|
+
// --- Step 3: store every held file's raw bytes in the object store ---
|
|
66
|
+
const entries = await this.storeEntries(inspected.files);
|
|
67
|
+
// --- Step 4: persist the memento before the tree changes ---
|
|
68
|
+
// A crash after this line leaves a record the next cycle restores from.
|
|
69
|
+
await this.writeMemento({ version: 1, entries });
|
|
70
|
+
// --- Step 5: a file written since step 3 is stored again (Decision 114) ---
|
|
71
|
+
await this.storeAgainIfWritten(entries, []);
|
|
72
|
+
// --- Step 6: the held paths now match the index, so the rebase may run ---
|
|
73
|
+
await this.writer.restoreFromIndex(paths);
|
|
74
|
+
return { kind: 'held', count: entries.length };
|
|
75
|
+
}
|
|
76
|
+
async extend(paths) {
|
|
77
|
+
// --- Step 1: nothing captured, nothing written ---
|
|
78
|
+
if (paths.length === 0) {
|
|
79
|
+
return { kind: 'held', count: 0 };
|
|
80
|
+
}
|
|
81
|
+
// --- Step 2: the entries already held; an unreadable memento is never overwritten ---
|
|
82
|
+
const text = await this.fs.readText(this.mementoPath());
|
|
83
|
+
let earlier = [];
|
|
84
|
+
if (text !== null) {
|
|
85
|
+
const memento = parseQuarantineMemento(text);
|
|
86
|
+
if (memento === null) {
|
|
87
|
+
return { kind: 'pending' };
|
|
88
|
+
}
|
|
89
|
+
earlier = memento.entries;
|
|
90
|
+
}
|
|
91
|
+
// --- Step 3: refuse what cannot be held byte for byte, before touching anything ---
|
|
92
|
+
const inspected = await this.inspect(paths);
|
|
93
|
+
if (inspected.kind === 'unsupported') {
|
|
94
|
+
return inspected;
|
|
95
|
+
}
|
|
96
|
+
// --- Step 4: store every captured file's raw bytes in the object store ---
|
|
97
|
+
const entries = await this.storeEntries(inspected.files);
|
|
98
|
+
// --- Step 5: append and persist before the tree changes ---
|
|
99
|
+
// A path held earlier keeps its older entry: restore writes the later one
|
|
100
|
+
// and lists the older blob in kept, so neither version goes unrecorded.
|
|
101
|
+
await this.writeMemento({ version: 1, entries: [...earlier, ...entries] });
|
|
102
|
+
// --- Step 6: a file written since step 4 is stored again (Decision 114) ---
|
|
103
|
+
await this.storeAgainIfWritten(entries, earlier);
|
|
104
|
+
// --- Step 7: the captured paths now match the index ---
|
|
105
|
+
await this.writer.restoreFromIndex(paths);
|
|
106
|
+
return { kind: 'held', count: entries.length };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* git's checkout writes the index's version over a file whatever the file
|
|
110
|
+
* holds, so a write that landed after storeEntries read it would be gone
|
|
111
|
+
* without a record. Each file is read once more right before the checkout;
|
|
112
|
+
* one whose bytes changed (or that appeared or went) is stored again and the
|
|
113
|
+
* memento rewritten first (design 13.11, Decision 114). What stays open is
|
|
114
|
+
* the time between this read and git's own write, the few milliseconds of
|
|
115
|
+
* one spawn. A path that is no longer a regular file keeps its entry.
|
|
116
|
+
*/
|
|
117
|
+
async storeAgainIfWritten(entries, earlier) {
|
|
118
|
+
const written = [];
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const target = join(this.paths.root, entry.path);
|
|
121
|
+
const seen = await this.observe(entry.path, target);
|
|
122
|
+
if (seen === OBSERVED_OTHER ||
|
|
123
|
+
seen === (entry.blobId ?? OBSERVED_ABSENT)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const facts = await this.fs.facts(target);
|
|
127
|
+
if (facts === null || facts.kind === 'file') {
|
|
128
|
+
written.push({ path: entry.path, facts });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (written.length === 0) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const stored = new Map((await this.storeEntries(written)).map((entry) => [entry.path, entry]));
|
|
135
|
+
await this.writeMemento({
|
|
136
|
+
version: 1,
|
|
137
|
+
entries: [
|
|
138
|
+
...earlier,
|
|
139
|
+
...entries.map((entry) => stored.get(entry.path) ?? entry),
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async restore() {
|
|
144
|
+
try {
|
|
145
|
+
// --- Step 1: nothing pending ---
|
|
146
|
+
const text = await this.fs.readText(this.mementoPath());
|
|
147
|
+
if (text === null) {
|
|
148
|
+
return { kind: 'nothing' };
|
|
149
|
+
}
|
|
150
|
+
const memento = parseQuarantineMemento(text);
|
|
151
|
+
if (memento === null) {
|
|
152
|
+
// Kept in place: the blobs it names are the only pointer to the held bytes.
|
|
153
|
+
return {
|
|
154
|
+
kind: 'failed',
|
|
155
|
+
detail: `the quarantine memento ${this.mementoPath()} cannot be parsed`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// --- Step 2: every entry, one at a time ---
|
|
159
|
+
let count = 0;
|
|
160
|
+
const kept = [];
|
|
161
|
+
const changedUnderHold = [];
|
|
162
|
+
const overwritten = [];
|
|
163
|
+
const latest = latestEntryByPath(memento.entries);
|
|
164
|
+
const reported = new Set();
|
|
165
|
+
for (const entry of memento.entries) {
|
|
166
|
+
const last = latest.get(entry.path);
|
|
167
|
+
if (last !== entry) {
|
|
168
|
+
// Superseded by a later capture of the same path: the later bytes
|
|
169
|
+
// win, and an older version that differs is reported by its blob.
|
|
170
|
+
const key = `${entry.path}\0${entry.blobId ?? ''}`;
|
|
171
|
+
if (entry.blobId !== null &&
|
|
172
|
+
entry.blobId !== last?.blobId &&
|
|
173
|
+
!reported.has(key)) {
|
|
174
|
+
reported.add(key);
|
|
175
|
+
kept.push({ path: entry.path, blobId: entry.blobId });
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const outcome = await this.restoreEntry(entry);
|
|
180
|
+
if (outcome.kind === 'kept') {
|
|
181
|
+
kept.push(outcome.blob);
|
|
182
|
+
}
|
|
183
|
+
else if (outcome.kind !== 'left') {
|
|
184
|
+
count += 1;
|
|
185
|
+
if (outcome.kind === 'restored-over-change') {
|
|
186
|
+
changedUnderHold.push(entry.path);
|
|
187
|
+
}
|
|
188
|
+
if (outcome.overwritten !== null) {
|
|
189
|
+
overwritten.push(outcome.overwritten);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// --- Step 3: only now is the memento no longer needed ---
|
|
194
|
+
await this.fs.remove(this.mementoPath());
|
|
195
|
+
return { kind: 'restored', count, kept, changedUnderHold, overwritten };
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
// --- Step 4: the memento stays, so a later restore can finish the job ---
|
|
199
|
+
return {
|
|
200
|
+
kind: 'failed',
|
|
201
|
+
detail: errorMessage(error),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async hasPending() {
|
|
206
|
+
// An unparseable memento still counts: ignoring it would let a new hold
|
|
207
|
+
// overwrite the only record of held bytes.
|
|
208
|
+
return (await this.fs.readText(this.mementoPath())) !== null;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* "Written during the hold" is judged by the working tree alone: the held
|
|
212
|
+
* version goes back when the file still holds what enmetu left there. The
|
|
213
|
+
* index is never the judge, because a person may stage the held path during
|
|
214
|
+
* the hold (git add, git apply --cached) without touching the file, and
|
|
215
|
+
* keeping the held bytes aside then would take the unstaged part off disk.
|
|
216
|
+
*/
|
|
217
|
+
async restoreEntry(entry, attempt = 0) {
|
|
218
|
+
const target = join(this.paths.root, entry.path);
|
|
219
|
+
// The file is read before the status that decides, so every write-back
|
|
220
|
+
// checks against bytes the decision covers. Read after a clean status, a
|
|
221
|
+
// person's write landing in between (a partial commit's two hunks, the
|
|
222
|
+
// first staged) would pass for what enmetu left and be overwritten with
|
|
223
|
+
// the held version, the unstaged hunk lost.
|
|
224
|
+
const facts = await this.fs.facts(target);
|
|
225
|
+
const raw = facts?.kind === 'file' ? await this.git.hashFile(entry.path) : null;
|
|
226
|
+
const seen = facts === null ? OBSERVED_ABSENT : (raw ?? OBSERVED_OTHER);
|
|
227
|
+
// A clean status means the file equals HEAD's version through git's own
|
|
228
|
+
// filters, which is what the rebase or its abort left: the common case.
|
|
229
|
+
if ((await this.git.status([entry.path])).length === 0) {
|
|
230
|
+
return this.putBackOnHead(entry, target, seen, attempt);
|
|
231
|
+
}
|
|
232
|
+
if (facts?.kind !== 'file' || raw === null) {
|
|
233
|
+
// Absent while HEAD or the index has the path, or no longer a file: a
|
|
234
|
+
// deletion or a replacement during the window wins, and an entry with
|
|
235
|
+
// bytes keeps them in a blob.
|
|
236
|
+
return entry.blobId === null
|
|
237
|
+
? facts === null
|
|
238
|
+
? RESTORED
|
|
239
|
+
: { kind: 'left' }
|
|
240
|
+
: { kind: 'kept', blob: { path: entry.path, blobId: entry.blobId } };
|
|
241
|
+
}
|
|
242
|
+
if (entry.blobId !== null && raw === entry.blobId) {
|
|
243
|
+
// A previous restore wrote this file and stopped before the memento went.
|
|
244
|
+
return RESTORED;
|
|
245
|
+
}
|
|
246
|
+
const filtered = await this.git.worktreeBlobId(entry.path);
|
|
247
|
+
if (await this.stillAsLeft(entry, filtered)) {
|
|
248
|
+
return this.putBackOnHead(entry, target, raw, attempt);
|
|
249
|
+
}
|
|
250
|
+
if (entry.blobId === null) {
|
|
251
|
+
// Written during the window while held for a deletion: the write wins.
|
|
252
|
+
return { kind: 'left' };
|
|
253
|
+
}
|
|
254
|
+
// Written during the window (an editor that reloaded the held file and
|
|
255
|
+
// saved on top of it): both edits survive when they merge cleanly onto
|
|
256
|
+
// what the hold left, the common ancestor of both (Decision 94). Only
|
|
257
|
+
// where no filter applies, so the three inputs are the same kind of bytes.
|
|
258
|
+
if (entry.heldOver !== null && raw === filtered) {
|
|
259
|
+
const merged = await this.mergeHeld(target, entry.heldOver, entry.blobId);
|
|
260
|
+
// A write after the merge read the file wins: it is never overwritten.
|
|
261
|
+
if (merged !== null) {
|
|
262
|
+
const written = await this.writeOver(entry, target, raw, merged, facts.executable);
|
|
263
|
+
if (written.kind === 'written') {
|
|
264
|
+
return { kind: 'restored', overwritten: written.overwritten };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { kind: 'kept', blob: { path: entry.path, blobId: entry.blobId } };
|
|
269
|
+
}
|
|
270
|
+
/** The file holds what the hold's checkout wrote, or HEAD's version (what a rebase or its abort wrote). */
|
|
271
|
+
async stillAsLeft(entry, current) {
|
|
272
|
+
if (entry.heldOver !== null && current === entry.heldOver) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
const head = await this.git.headCommit();
|
|
276
|
+
return (head !== null &&
|
|
277
|
+
current === (await this.git.entryBlobId(entry.path, head)));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The file still holds what enmetu left (Decision 99). When HEAD's version
|
|
281
|
+
* of the path is no longer heldOver, a rebase changed the path under the
|
|
282
|
+
* hold and the held bytes are based on the old version: written back as
|
|
283
|
+
* they are, enmetu's next commit would undo that change. Then the clean
|
|
284
|
+
* three-way merge of HEAD's version and the held bytes, base heldOver, goes
|
|
285
|
+
* back, only where no filter applies (raw hash equals filtered hash) and
|
|
286
|
+
* only while the file is still unchanged; a newer write wins and the held
|
|
287
|
+
* bytes are kept in a blob. Without a clean merge (a conflict, binary
|
|
288
|
+
* input, a filter, a deletion on either side) the held version goes back
|
|
289
|
+
* and the path is reported, so the undo is never silent. seen is the file
|
|
290
|
+
* the way the decision read it (observe); every write-back is skipped when the
|
|
291
|
+
* file changed since.
|
|
292
|
+
*/
|
|
293
|
+
async putBackOnHead(entry, target, seen, attempt) {
|
|
294
|
+
if (entry.heldOver === null) {
|
|
295
|
+
// A memento from before heldOver existed: nothing to compare HEAD with.
|
|
296
|
+
return this.putBackUnlessChanged(entry, target, seen, attempt, 'restored');
|
|
297
|
+
}
|
|
298
|
+
const head = await this.git.headCommit();
|
|
299
|
+
const headBlob = head === null ? null : await this.git.entryBlobId(entry.path, head);
|
|
300
|
+
if (headBlob === entry.heldOver) {
|
|
301
|
+
return this.putBackUnlessChanged(entry, target, seen, attempt, 'restored');
|
|
302
|
+
}
|
|
303
|
+
if (entry.blobId !== null &&
|
|
304
|
+
headBlob !== null &&
|
|
305
|
+
isRegularFile(seen) &&
|
|
306
|
+
seen === (await this.git.worktreeBlobId(entry.path))) {
|
|
307
|
+
const merged = await this.mergeOntoHead(headBlob, entry.heldOver, entry.blobId);
|
|
308
|
+
if (merged !== null) {
|
|
309
|
+
const written = await this.writeOver(entry, target, seen, merged, entry.executable);
|
|
310
|
+
return written.kind === 'written'
|
|
311
|
+
? { kind: 'restored', overwritten: written.overwritten }
|
|
312
|
+
: { kind: 'kept', blob: { path: entry.path, blobId: entry.blobId } };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return this.putBackUnlessChanged(entry, target, seen, attempt, 'restored-over-change');
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Writes the held version back only while the file is still what the
|
|
319
|
+
* decision saw. A write that landed since (a person's editor saving
|
|
320
|
+
* during the restore's own spawns) is judged again from the top, at most
|
|
321
|
+
* MAX_RESTORE_REJUDGES times; after that the write wins and the held bytes
|
|
322
|
+
* stay a blob, so a restore never overwrites what it did not judge. The
|
|
323
|
+
* held bytes are read before the check, so no spawn runs between the check
|
|
324
|
+
* and the write (Decision 117).
|
|
325
|
+
*/
|
|
326
|
+
async putBackUnlessChanged(entry, target, seen, attempt, kind) {
|
|
327
|
+
const content = entry.blobId === null ? null : await this.git.readBlob(entry.blobId);
|
|
328
|
+
const written = await this.writeOver(entry, target, seen, content, entry.executable);
|
|
329
|
+
if (written.kind === 'written') {
|
|
330
|
+
return { kind, overwritten: written.overwritten };
|
|
331
|
+
}
|
|
332
|
+
if (attempt < MAX_RESTORE_REJUDGES) {
|
|
333
|
+
return this.restoreEntry(entry, attempt + 1);
|
|
334
|
+
}
|
|
335
|
+
return entry.blobId === null
|
|
336
|
+
? { kind: 'left' }
|
|
337
|
+
: { kind: 'kept', blob: { path: entry.path, blobId: entry.blobId } };
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Writes content over the file (null: removes it) only while the file is
|
|
341
|
+
* what the decision saw (seen), and names a write that lands after that
|
|
342
|
+
* check (design 13.11, Decision 117). No git command can write a file only
|
|
343
|
+
* while it is unchanged, so the version being replaced is kept under a
|
|
344
|
+
* second name in the same step that replaces it:
|
|
345
|
+
*
|
|
346
|
+
* 1. content is written and flushed to a staged file first;
|
|
347
|
+
* 2. the file is checked against seen (one hashFile spawn);
|
|
348
|
+
* 3. link(2) gives the file's current version a second name, then rename(2)
|
|
349
|
+
* puts the staged file in its place (a removal is one rename(2) to the
|
|
350
|
+
* second name);
|
|
351
|
+
* 4. the second name is read: a version other than seen was written after
|
|
352
|
+
* step 2, and is stored in a blob and reported, never lost unrecorded.
|
|
353
|
+
*
|
|
354
|
+
* What stays open is enmetu's own: a save that replaces the file by
|
|
355
|
+
* rename(2) between the link and the rename of step 3 (two consecutive
|
|
356
|
+
* system calls), and a write into the replaced version through a file
|
|
357
|
+
* descriptor opened before step 3 that lands after step 4's read. Where the
|
|
358
|
+
* state directory is on another file system than the working tree (a
|
|
359
|
+
* linked worktree of a repository on another volume), or the file is not a
|
|
360
|
+
* regular file, link(2) cannot keep it and the plain write runs, with the
|
|
361
|
+
* window of steps 2 and 3.
|
|
362
|
+
*/
|
|
363
|
+
async writeOver(entry, target, seen, content, executable) {
|
|
364
|
+
const tempDir = this.tempDir();
|
|
365
|
+
if (seen === OBSERVED_OTHER ||
|
|
366
|
+
!(await this.fs.sameFileSystem(this.paths.root, this.paths.stateDir))) {
|
|
367
|
+
if ((await this.observe(entry.path, target)) !== seen) {
|
|
368
|
+
return { kind: 'changed' };
|
|
369
|
+
}
|
|
370
|
+
if (content === null) {
|
|
371
|
+
await this.fs.remove(target);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
await this.fs.writeAtomic(target, content, { executable, tempDir });
|
|
375
|
+
}
|
|
376
|
+
return { kind: 'written', overwritten: null };
|
|
377
|
+
}
|
|
378
|
+
const staged = join(tempDir, STAGED_FILE_NAME);
|
|
379
|
+
const previous = join(tempDir, PREVIOUS_FILE_NAME);
|
|
380
|
+
try {
|
|
381
|
+
// --- Step 1: the version to write, flushed before the check ---
|
|
382
|
+
if (content !== null) {
|
|
383
|
+
await this.fs.writeAtomic(staged, content, { executable, tempDir });
|
|
384
|
+
}
|
|
385
|
+
// --- Step 2: the decision's file, still there ---
|
|
386
|
+
if ((await this.observe(entry.path, target)) !== seen) {
|
|
387
|
+
return { kind: 'changed' };
|
|
388
|
+
}
|
|
389
|
+
// --- Step 3: replace, keeping what is replaced under a second name ---
|
|
390
|
+
await this.fs.remove(previous);
|
|
391
|
+
if (content === null) {
|
|
392
|
+
await this.fs.rename(target, previous);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
const linked = await this.fs.link(target, previous);
|
|
396
|
+
if (!linked && isRegularFile(seen)) {
|
|
397
|
+
// Deleted since step 2: the deletion is newer than the decision.
|
|
398
|
+
return { kind: 'changed' };
|
|
399
|
+
}
|
|
400
|
+
await this.fs.rename(staged, target);
|
|
401
|
+
}
|
|
402
|
+
// --- Step 4: a version written after step 2 is named ---
|
|
403
|
+
const replaced = await this.observe(previous, previous);
|
|
404
|
+
if (replaced === seen || !isRegularFile(replaced)) {
|
|
405
|
+
return { kind: 'written', overwritten: null };
|
|
406
|
+
}
|
|
407
|
+
const blobId = await this.writer.storeBlob(previous);
|
|
408
|
+
return { kind: 'written', overwritten: { path: entry.path, blobId } };
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
await this.fs.remove(staged);
|
|
412
|
+
await this.fs.remove(previous);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** The file's raw blob id, or a marker for an absent path or one that is not a regular file. */
|
|
416
|
+
async observe(path, target) {
|
|
417
|
+
const facts = await this.fs.facts(target);
|
|
418
|
+
if (facts === null) {
|
|
419
|
+
return OBSERVED_ABSENT;
|
|
420
|
+
}
|
|
421
|
+
if (facts?.kind !== 'file') {
|
|
422
|
+
return OBSERVED_OTHER;
|
|
423
|
+
}
|
|
424
|
+
return this.git.hashFile(path);
|
|
425
|
+
}
|
|
426
|
+
/** Three-way merge of HEAD's blob with the held bytes on the base the hold left; null unless clean. */
|
|
427
|
+
async mergeOntoHead(headBlob, base, held) {
|
|
428
|
+
const headPath = join(this.tempDir(), MERGE_HEAD_FILE_NAME);
|
|
429
|
+
try {
|
|
430
|
+
await this.fs.writeAtomic(headPath, await this.git.readBlob(headBlob), {
|
|
431
|
+
executable: false,
|
|
432
|
+
tempDir: this.tempDir(),
|
|
433
|
+
});
|
|
434
|
+
return await this.mergeHeld(headPath, base, held);
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
await this.fs.remove(headPath);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/** Three-way merge of a file with the held bytes on the base the hold left; null unless clean. */
|
|
441
|
+
async mergeHeld(current, base, held) {
|
|
442
|
+
const basePath = join(this.tempDir(), MERGE_BASE_FILE_NAME);
|
|
443
|
+
const heldPath = join(this.tempDir(), MERGE_HELD_FILE_NAME);
|
|
444
|
+
const options = { executable: false, tempDir: this.tempDir() };
|
|
445
|
+
try {
|
|
446
|
+
await this.fs.writeAtomic(basePath, await this.git.readBlob(base), options);
|
|
447
|
+
await this.fs.writeAtomic(heldPath, await this.git.readBlob(held), options);
|
|
448
|
+
return await this.git.mergeFile(current, basePath, heldPath);
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
await this.fs.remove(basePath);
|
|
452
|
+
await this.fs.remove(heldPath);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
tempDir() {
|
|
456
|
+
return join(this.paths.stateDir, QUARANTINE_TEMP_DIR_NAME);
|
|
457
|
+
}
|
|
458
|
+
async inspect(paths) {
|
|
459
|
+
const files = [];
|
|
460
|
+
for (const path of paths) {
|
|
461
|
+
const facts = await this.fs.facts(join(this.paths.root, path));
|
|
462
|
+
// Only a regular file round-trips through a blob and writeAtomic; a
|
|
463
|
+
// symlink, a directory or a special file would not come back unchanged.
|
|
464
|
+
if (facts !== null && facts.kind !== 'file') {
|
|
465
|
+
return { kind: 'unsupported', path };
|
|
466
|
+
}
|
|
467
|
+
files.push({ path, facts });
|
|
468
|
+
}
|
|
469
|
+
return { kind: 'files', files };
|
|
470
|
+
}
|
|
471
|
+
async storeEntries(files) {
|
|
472
|
+
const entries = [];
|
|
473
|
+
for (const { path, facts } of files) {
|
|
474
|
+
// Read before the checkout that writes it, so restore can tell the
|
|
475
|
+
// checkout's bytes from a person's write.
|
|
476
|
+
const heldOver = await this.git.entryBlobId(path, null);
|
|
477
|
+
entries.push(facts === null
|
|
478
|
+
? { path, blobId: null, executable: false, heldOver }
|
|
479
|
+
: {
|
|
480
|
+
path,
|
|
481
|
+
blobId: await this.writer.storeBlob(path),
|
|
482
|
+
executable: facts.executable,
|
|
483
|
+
heldOver,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return entries;
|
|
487
|
+
}
|
|
488
|
+
async writeMemento(memento) {
|
|
489
|
+
// Built field by field in interface order, so the file is byte-stable.
|
|
490
|
+
const persisted = {
|
|
491
|
+
version: memento.version,
|
|
492
|
+
entries: memento.entries.map((entry) => ({
|
|
493
|
+
path: entry.path,
|
|
494
|
+
blobId: entry.blobId,
|
|
495
|
+
executable: entry.executable,
|
|
496
|
+
heldOver: entry.heldOver,
|
|
497
|
+
})),
|
|
498
|
+
};
|
|
499
|
+
await this.fs.writeAtomic(this.mementoPath(), `${JSON.stringify(persisted)}\n`, { executable: false, tempDir: this.paths.stateDir });
|
|
500
|
+
}
|
|
501
|
+
mementoPath() {
|
|
502
|
+
return join(this.paths.stateDir, QUARANTINE_FILE_NAME);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { IClock } from '../contracts/IClock.js';
|
|
2
|
+
import type { IGitReader } from '../contracts/IGitReader.js';
|
|
3
|
+
import type { IGitWriter } from '../contracts/IGitWriter.js';
|
|
4
|
+
import type { IHaltStore } from '../contracts/IHaltStore.js';
|
|
5
|
+
import type { ILogger } from '../contracts/ILogger.js';
|
|
6
|
+
import type { IOperationJournal } from '../contracts/IOperationJournal.js';
|
|
7
|
+
import type { IPathQuarantine } from '../contracts/IPathQuarantine.js';
|
|
8
|
+
import type { IOrphanedCommitRecorder } from '../contracts/IStateRecorder.js';
|
|
9
|
+
import { type RecordHaltDeps } from '../halt/recordHalt.js';
|
|
10
|
+
export interface EnmetuRebaseAbortInput {
|
|
11
|
+
readonly cycleId: string;
|
|
12
|
+
/**
|
|
13
|
+
* The HEAD enmetu's rebase started from; the verification expects HEAD back
|
|
14
|
+
* there, unless a commit moved the branch off it (branchMovedTo).
|
|
15
|
+
*/
|
|
16
|
+
readonly origHead: string;
|
|
17
|
+
readonly onto: string;
|
|
18
|
+
/**
|
|
19
|
+
* The attempt's GIT_REFLOG_ACTION (rebaseReflogAction of its journal entry),
|
|
20
|
+
* which tells the rebase's own HEAD moves from a commit made on its detached
|
|
21
|
+
* HEAD (Decision 124); null when no attempt token is known.
|
|
22
|
+
*/
|
|
23
|
+
readonly reflogAction: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface EnmetuRebaseAbortDeps {
|
|
26
|
+
readonly git: IGitReader;
|
|
27
|
+
readonly writer: IGitWriter;
|
|
28
|
+
readonly journal: IOperationJournal;
|
|
29
|
+
readonly quarantine: IPathQuarantine;
|
|
30
|
+
readonly halt: IHaltStore;
|
|
31
|
+
readonly logger: ILogger;
|
|
32
|
+
readonly clock: IClock;
|
|
33
|
+
/** Told of each commit step 1a names, for the state file and `enmetu status` (design 14.15). */
|
|
34
|
+
readonly orphans?: IOrphanedCommitRecorder | undefined;
|
|
35
|
+
}
|
|
36
|
+
export type AbortVerdict = 'verified' | 'unverified';
|
|
37
|
+
export interface EnmetuRebaseAbortResult {
|
|
38
|
+
readonly verdict: AbortVerdict;
|
|
39
|
+
/** Writes made during the stop went into the quarantine; the caller restores them. */
|
|
40
|
+
readonly captured: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The branch's tip when a commit moved the branch the rebase returns to off
|
|
43
|
+
* orig-head while the rebase ran: the rebase was ended at that tip
|
|
44
|
+
* (endRebaseAtBranch), never aborted to orig-head, and the verdict checks
|
|
45
|
+
* HEAD names the branch at that tip. null otherwise (design 14.4, Decision 122).
|
|
46
|
+
*/
|
|
47
|
+
readonly branchMovedTo: string | null;
|
|
48
|
+
}
|
|
49
|
+
/** What the verification reads; the deps of the check alone, so it halts nothing. */
|
|
50
|
+
export interface RebaseAbortCheckDeps {
|
|
51
|
+
readonly git: IGitReader;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Capture, abort and verify (1.11 abort-and-verify, 1.14 step 2): the one
|
|
55
|
+
* implementation for the live cycle and for startup recovery. The caller has
|
|
56
|
+
* already proven the rebase in progress is enmetu's (HEAD's reflog carries the
|
|
57
|
+
* attempt's token, or, without a reflog, orig-head and onto match; Decision 104).
|
|
58
|
+
*
|
|
59
|
+
* 0a. branchRef = git.rebaseHeadName(): the branch the rebase returns to,
|
|
60
|
+
* null for a rebase started on a detached HEAD (Decision 122).
|
|
61
|
+
* 0. Read the writes that landed during the stop (Decision 45).
|
|
62
|
+
* 0b. Name: every staged path the abort resets that is not written, not
|
|
63
|
+
* changed by the stopped commit and not orig-head's version is logged
|
|
64
|
+
* STAGED_RESET with its stage-0 blob (Decisions 105, 115); nothing is
|
|
65
|
+
* written for it.
|
|
66
|
+
* 0c. Capture, the last step before the abort: the status is read again, the
|
|
67
|
+
* writes go through quarantine.extend, and the status is read again after
|
|
68
|
+
* each capture, up to MAX_CAPTURE_ROUNDS captures, so a write that landed
|
|
69
|
+
* while enmetu was naming or capturing is held too (Decision 113). A
|
|
70
|
+
* written path the stopped commit changes that holds git's conflict
|
|
71
|
+
* markers is not captured but named, and the abort resets it (Decision
|
|
72
|
+
* 118). A refused capture halts REBASE_ABORT_UNVERIFIED with the rebase
|
|
73
|
+
* left in progress.
|
|
74
|
+
* 1a. A commit made on the rebase's detached HEAD (HEAD's newest reflog
|
|
75
|
+
* entry is not the attempt's, and the attempt's last HEAD is an ancestor
|
|
76
|
+
* of HEAD) is left on no branch by step 1 and its changes leave the disk:
|
|
77
|
+
* COMMIT_ORPHANED names it with the remedy (Decision 124), and deps.orphans
|
|
78
|
+
* records it for `enmetu status` (design 14.15).
|
|
79
|
+
* 1. tip = git.resolveCommit(branchRef), the last read before git's write. A
|
|
80
|
+
* tip other than origHead (a commit moved the branch while the rebase
|
|
81
|
+
* ran): writer.endRebaseAtBranch(branchRef) and REBASE_BRANCH_MOVED;
|
|
82
|
+
* otherwise writer.abortRebase() (Decision 122).
|
|
83
|
+
* 2. Verify: no operation, nothing unmerged, HEAD at the moved tip or at
|
|
84
|
+
* origHead and, with a branchRef, HEAD naming it; a failure halts
|
|
85
|
+
* REBASE_ABORT_UNVERIFIED, none clears the journal. branchMovedTo carries
|
|
86
|
+
* the tip step 1 ended at.
|
|
87
|
+
*/
|
|
88
|
+
export declare function abortEnmetuRebase(input: EnmetuRebaseAbortInput, deps: EnmetuRebaseAbortDeps): Promise<EnmetuRebaseAbortResult>;
|
|
89
|
+
/**
|
|
90
|
+
* How many captures step 0c runs at most before the abort. Each capture takes
|
|
91
|
+
* a few git spawns; a write that lands during one is found by the next status
|
|
92
|
+
* read. The bound keeps a file saved over and over from holding the abort
|
|
93
|
+
* back for ever; what stays open is the time between the last status read and
|
|
94
|
+
* the reset inside `git rebase --abort` (design 13.11).
|
|
95
|
+
*/
|
|
96
|
+
export declare const MAX_CAPTURE_ROUNDS = 3;
|
|
97
|
+
/**
|
|
98
|
+
* The verification's checks, read without halting: no operation in progress, nothing
|
|
99
|
+
* unmerged, HEAD back at origHead. Returns the failure's detail, or null when
|
|
100
|
+
* all three hold. It records nothing, so a caller can ask who started the
|
|
101
|
+
* rebase before it decides on a halt (design 13.2).
|
|
102
|
+
*/
|
|
103
|
+
export declare function rebaseAbortFailure(input: EnmetuRebaseAbortInput, deps: RebaseAbortCheckDeps): Promise<string | null>;
|
|
104
|
+
/**
|
|
105
|
+
* Halts REBASE_ABORT_UNVERIFIED and keeps the journal and any memento, which
|
|
106
|
+
* are what a person needs to finish by hand.
|
|
107
|
+
*/
|
|
108
|
+
export declare function recordAbortUnverified(input: EnmetuRebaseAbortInput, failure: string, deps: RecordHaltDeps): Promise<void>;
|