@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,150 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
/** At most this many repaired paths are named in the log line; the count is always exact. */
|
|
3
|
+
export const MAX_REPAIRED_PATHS_LOGGED = 20;
|
|
4
|
+
const SHORT_SHA_LENGTH = 7;
|
|
5
|
+
/**
|
|
6
|
+
* Controller: sets back index entries that a person's git command wrote back
|
|
7
|
+
* from before enmetu's last move of HEAD, its commit or its pull (design
|
|
8
|
+
* 12.7, Decision 98). A command that read the real index before the move and
|
|
9
|
+
* writes it after (measured for git merge against enmetu's commit section;
|
|
10
|
+
* seen in the stress scenario against enmetu's rebase) leaves HEAD at
|
|
11
|
+
* lastMove.to and lastMove.from's entries in the index, which would revert
|
|
12
|
+
* the move in the person's next commit.
|
|
13
|
+
*
|
|
14
|
+
* 1. HEAD differs from lastMove.to: moved.
|
|
15
|
+
* 2. Candidates: git.stagedPaths() (index against HEAD), in git's order. A
|
|
16
|
+
* path is stale when its index entry equals lastMove.from's (both absent
|
|
17
|
+
* for a path the move added) while the working tree does not (absent, or
|
|
18
|
+
* a regular file whose filtered blob is not from's; a link or a directory
|
|
19
|
+
* is never stale). A path the move did not change has from's entry in
|
|
20
|
+
* HEAD, so it differs from HEAD only with another entry and never matches.
|
|
21
|
+
* Reverted: index entry and working tree both from's (design 13.2,
|
|
22
|
+
* Decision 108). Neither: none. Only reverted: STAGED_REVERT once, nothing
|
|
23
|
+
* written, reverted.
|
|
24
|
+
* 3. Prepare: fs.copy(realIndex, commit index), so every other entry, the
|
|
25
|
+
* person's staged selection included, stays unchanged; commitIndex.writer
|
|
26
|
+
* .unstage(stale) sets exactly those entries to HEAD's.
|
|
27
|
+
* 4. Critical section, shaped like the commit's (Decision 81): indexLock.acquire
|
|
28
|
+
* (false: busy); an operation in progress or the real index's identity
|
|
29
|
+
* differing from before: release, busy; HEAD moved: release, moved;
|
|
30
|
+
* indexLock.install(commit index); release on every path.
|
|
31
|
+
* 5. INDEX_REPAIRED once, naming the paths, then STAGED_REVERT for reverted
|
|
32
|
+
* paths; repaired. Busy or moved names nothing, so the next cycle does.
|
|
33
|
+
*/
|
|
34
|
+
export async function repairStaleIndexController(input, deps) {
|
|
35
|
+
const { cycleId, lastMove, before } = input;
|
|
36
|
+
// --- Step 1: only while HEAD is still where enmetu moved it ---
|
|
37
|
+
if ((await deps.git.headCommit()) !== lastMove.to) {
|
|
38
|
+
return { kind: 'moved' };
|
|
39
|
+
}
|
|
40
|
+
// --- Step 2: staged paths with the stale or the reverted signature ---
|
|
41
|
+
const stale = [];
|
|
42
|
+
const reverted = [];
|
|
43
|
+
for (const path of await deps.git.stagedPaths()) {
|
|
44
|
+
const signature = await classify(path, lastMove, deps);
|
|
45
|
+
if (signature === 'stale') {
|
|
46
|
+
stale.push(path);
|
|
47
|
+
}
|
|
48
|
+
else if (signature === 'reverted') {
|
|
49
|
+
reverted.push(path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (stale.length === 0) {
|
|
53
|
+
if (reverted.length === 0) {
|
|
54
|
+
return { kind: 'none' };
|
|
55
|
+
}
|
|
56
|
+
logReverted(reverted, input, deps);
|
|
57
|
+
return { kind: 'reverted', paths: reverted };
|
|
58
|
+
}
|
|
59
|
+
// --- Step 3: the repaired index, prepared beside the real one ---
|
|
60
|
+
if (!(await deps.fs.copy(deps.settings.realIndex, deps.commitIndex.path))) {
|
|
61
|
+
// No real index: a stale write-back always leaves one, so something else happened.
|
|
62
|
+
return { kind: 'busy' };
|
|
63
|
+
}
|
|
64
|
+
await deps.commitIndex.writer.unstage(stale);
|
|
65
|
+
// --- Step 4: install it under git's own index.lock ---
|
|
66
|
+
const installed = await install(lastMove.to, before, deps);
|
|
67
|
+
if (installed !== 'installed') {
|
|
68
|
+
return { kind: installed };
|
|
69
|
+
}
|
|
70
|
+
// --- Step 5: one line per repair, then the reverted paths once it is installed ---
|
|
71
|
+
deps.logger.log({
|
|
72
|
+
level: 'WARN',
|
|
73
|
+
code: 'INDEX_REPAIRED',
|
|
74
|
+
message: `a git command wrote back the index from before enmetu moved HEAD to ${lastMove.to.slice(0, SHORT_SHA_LENGTH)}; ${stale.length === 1 ? '1 index entry' : `${stale.length} index entries`} set back to HEAD, which a commit would otherwise revert: ${namedPaths(stale)}`,
|
|
75
|
+
cycleId,
|
|
76
|
+
});
|
|
77
|
+
if (reverted.length > 0) {
|
|
78
|
+
logReverted(reverted, input, deps);
|
|
79
|
+
}
|
|
80
|
+
return { kind: 'repaired', paths: stale };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Each side is a blob id, or null where the path is absent; the index side
|
|
84
|
+
* must be from's. Stale: the working-tree side is not from's, so a person who
|
|
85
|
+
* keeps editing the file after the move still matches, which is why the
|
|
86
|
+
* working tree is compared with from, not with to (stress seed 11 of
|
|
87
|
+
* follow-up review 2). Reverted: the working tree is from's too, where both
|
|
88
|
+
* absent count equal (Decision 108): a merge's restore_state, or a deliberate
|
|
89
|
+
* revert (git checkout HEAD~ -- <path>), which is why it is named and never
|
|
90
|
+
* repaired. null: neither.
|
|
91
|
+
*/
|
|
92
|
+
async function classify(path, lastMove, deps) {
|
|
93
|
+
const fromBlob = await deps.git.entryBlobId(path, lastMove.from);
|
|
94
|
+
if ((await deps.git.entryBlobId(path, null)) !== fromBlob) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const facts = await deps.fs.facts(join(deps.settings.root, path));
|
|
98
|
+
if (facts === null) {
|
|
99
|
+
return fromBlob === null ? 'reverted' : 'stale';
|
|
100
|
+
}
|
|
101
|
+
if (facts.kind !== 'file') {
|
|
102
|
+
// A link, a directory or a special file: never judged, never touched.
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return (await deps.git.worktreeBlobId(path)) === fromBlob
|
|
106
|
+
? 'reverted'
|
|
107
|
+
: 'stale';
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* STAGED_REVERT (Decision 108): names enmetu's commit by its full id, so the
|
|
111
|
+
* remedy stays valid after a later commit of enmetu's records the revert.
|
|
112
|
+
*/
|
|
113
|
+
function logReverted(paths, input, deps) {
|
|
114
|
+
const { to } = input.lastMove;
|
|
115
|
+
deps.logger.log({
|
|
116
|
+
level: 'WARN',
|
|
117
|
+
code: 'STAGED_REVERT',
|
|
118
|
+
message: `staged and on disk as before enmetu moved HEAD to ${to.slice(0, SHORT_SHA_LENGTH)}, as a git merge refused with "would be overwritten by merge" leaves them: ${namedPaths(paths)}; unless you reverted them yourself, git restore --source=${to} --staged --worktree -- <path> brings back the version enmetu committed`,
|
|
119
|
+
cycleId: input.cycleId,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** At most MAX_REPAIRED_PATHS_LOGGED paths, then how many more. */
|
|
123
|
+
function namedPaths(paths) {
|
|
124
|
+
const named = paths.slice(0, MAX_REPAIRED_PATHS_LOGGED).join(', ');
|
|
125
|
+
const rest = paths.length - MAX_REPAIRED_PATHS_LOGGED;
|
|
126
|
+
return `${named}${rest > 0 ? ` and ${rest} more` : ''}`;
|
|
127
|
+
}
|
|
128
|
+
async function install(head, before, deps) {
|
|
129
|
+
// The lock names HEAD, so recovery after a crash in here keeps the
|
|
130
|
+
// installed index, which matches HEAD better than the stale one.
|
|
131
|
+
if (!(await deps.indexLock.acquire(head))) {
|
|
132
|
+
return 'busy';
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
if ((await deps.git.repositoryState()).operation !== null) {
|
|
136
|
+
return 'busy';
|
|
137
|
+
}
|
|
138
|
+
if ((await deps.git.headCommit()) !== head) {
|
|
139
|
+
return 'moved';
|
|
140
|
+
}
|
|
141
|
+
if ((await deps.fs.identity(deps.settings.realIndex)) !== before) {
|
|
142
|
+
return 'busy';
|
|
143
|
+
}
|
|
144
|
+
await deps.indexLock.install(deps.commitIndex.path);
|
|
145
|
+
return 'installed';
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await deps.indexLock.release();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IGitReader } from '../contracts/IGitReader.js';
|
|
2
|
+
import type { IGitWriter } from '../contracts/IGitWriter.js';
|
|
3
|
+
import type { ILogger } from '../contracts/ILogger.js';
|
|
4
|
+
export interface StageGuardedInput {
|
|
5
|
+
readonly cycleId: string;
|
|
6
|
+
}
|
|
7
|
+
export interface StageGuardedDeps {
|
|
8
|
+
readonly git: IGitReader;
|
|
9
|
+
readonly writer: IGitWriter;
|
|
10
|
+
readonly logger: ILogger;
|
|
11
|
+
readonly skipJsonCheck: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export interface StageGuardedResult {
|
|
14
|
+
readonly staged: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Controller: stage everything, inspect the staged blobs with the shared
|
|
18
|
+
* content rules, unstage what is not fit to commit (F3, F10, F11, D11).
|
|
19
|
+
*
|
|
20
|
+
* 1. writer.stageAll(); partial logs STAGE_PARTIAL.
|
|
21
|
+
* 2. For each git.stagedFiles(MAX_SCAN_BYTES): findContentViolation.
|
|
22
|
+
* 3. Violations: one writer.unstage(paths) call, EXCLUDED logged per path.
|
|
23
|
+
* 4. staged = git.hasStagedChanges().
|
|
24
|
+
*
|
|
25
|
+
* Inspecting the staged blobs rather than the working tree means what is
|
|
26
|
+
* inspected is exactly what the next commit records (Decision 5). The
|
|
27
|
+
* controller raises no error of its own; git failures propagate unchanged to
|
|
28
|
+
* the cycle, which owns the busy mapping and threads the cycle id.
|
|
29
|
+
*/
|
|
30
|
+
export declare function stageGuardedChanges(input: StageGuardedInput, deps: StageGuardedDeps): Promise<StageGuardedResult>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { findContentViolation, MAX_SCAN_BYTES, } from '../core/content/contentRules.js';
|
|
2
|
+
/**
|
|
3
|
+
* Controller: stage everything, inspect the staged blobs with the shared
|
|
4
|
+
* content rules, unstage what is not fit to commit (F3, F10, F11, D11).
|
|
5
|
+
*
|
|
6
|
+
* 1. writer.stageAll(); partial logs STAGE_PARTIAL.
|
|
7
|
+
* 2. For each git.stagedFiles(MAX_SCAN_BYTES): findContentViolation.
|
|
8
|
+
* 3. Violations: one writer.unstage(paths) call, EXCLUDED logged per path.
|
|
9
|
+
* 4. staged = git.hasStagedChanges().
|
|
10
|
+
*
|
|
11
|
+
* Inspecting the staged blobs rather than the working tree means what is
|
|
12
|
+
* inspected is exactly what the next commit records (Decision 5). The
|
|
13
|
+
* controller raises no error of its own; git failures propagate unchanged to
|
|
14
|
+
* the cycle, which owns the busy mapping and threads the cycle id.
|
|
15
|
+
*/
|
|
16
|
+
export async function stageGuardedChanges(input, deps) {
|
|
17
|
+
// --- Step 1: stage the whole tree ---
|
|
18
|
+
const stage = await deps.writer.stageAll();
|
|
19
|
+
if (stage.partial) {
|
|
20
|
+
// One unreadable file is reported and skipped instead of blocking every commit.
|
|
21
|
+
deps.logger.log({
|
|
22
|
+
level: 'WARN',
|
|
23
|
+
code: 'STAGE_PARTIAL',
|
|
24
|
+
message: stage.detail,
|
|
25
|
+
cycleId: input.cycleId,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
// --- Step 2: inspect one staged blob at a time ---
|
|
29
|
+
const violations = [];
|
|
30
|
+
for await (const file of deps.git.stagedFiles(MAX_SCAN_BYTES)) {
|
|
31
|
+
const violation = findContentViolation(file, deps.skipJsonCheck);
|
|
32
|
+
if (violation !== null) {
|
|
33
|
+
violations.push(violation);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// --- Step 3: unstage the violations in one call, in git's order ---
|
|
37
|
+
if (violations.length > 0) {
|
|
38
|
+
// reset also clears unmerged entries, which git add has already turned
|
|
39
|
+
// into stage-0 marker text; the bytes stay on disk for the person.
|
|
40
|
+
await deps.writer.unstage(violations.map((violation) => violation.path));
|
|
41
|
+
for (const violation of violations) {
|
|
42
|
+
deps.logger.log({
|
|
43
|
+
level: 'WARN',
|
|
44
|
+
code: 'EXCLUDED',
|
|
45
|
+
message: `${violation.path}: ${violation.rule} (${violation.detail})`,
|
|
46
|
+
cycleId: input.cycleId,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// --- Step 4: report whether anything is left to commit ---
|
|
51
|
+
return { staged: await deps.git.hasStagedChanges() };
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ExitCode } from './exitCode.js';
|
|
2
|
+
import type { Upstream } from './gitTypes.js';
|
|
3
|
+
import type { HaltCode } from './IHaltStore.js';
|
|
4
|
+
export type RecoveryAction = 'REBASE_ABORTED' | 'STALE_JOURNAL_CLEARED' | 'QUARANTINE_RESTORED'
|
|
5
|
+
/** An index.lock left by a crashed enmetu's commit (design 12.7). */
|
|
6
|
+
| 'INDEX_LOCK_RELEASED';
|
|
7
|
+
export type RecoveryResult = {
|
|
8
|
+
readonly kind: 'clean';
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'recovered';
|
|
11
|
+
readonly actions: readonly RecoveryAction[];
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'halted';
|
|
14
|
+
readonly code: HaltCode;
|
|
15
|
+
};
|
|
16
|
+
/** recoveryController with its deps bound (a closure built by the composition root). */
|
|
17
|
+
export interface IRecovery {
|
|
18
|
+
recover(): Promise<RecoveryResult>;
|
|
19
|
+
}
|
|
20
|
+
/** repositoryPreflight with its deps bound; throws PreflightError. */
|
|
21
|
+
export interface IPreflight {
|
|
22
|
+
check(): Promise<Upstream>;
|
|
23
|
+
}
|
|
24
|
+
export interface IDaemon {
|
|
25
|
+
run(): Promise<ExitCode>;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface FileFacts {
|
|
2
|
+
readonly kind: 'file' | 'directory' | 'symlink' | 'other';
|
|
3
|
+
readonly executable: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface WriteOptions {
|
|
6
|
+
readonly executable: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Where the temp file is created before the rename. A rename across file
|
|
9
|
+
* systems (EXDEV) retries with the temp file in the target's directory.
|
|
10
|
+
*/
|
|
11
|
+
readonly tempDir: string;
|
|
12
|
+
}
|
|
13
|
+
export interface IFileSystem {
|
|
14
|
+
/** null when absent. */
|
|
15
|
+
readText(path: string): Promise<string | null>;
|
|
16
|
+
/** Temp file, then rename over the target; creates parent directories. */
|
|
17
|
+
writeAtomic(path: string, content: string | Buffer, options: WriteOptions): Promise<void>;
|
|
18
|
+
/** Complete content or nothing: temp file, link(2) to the target, unlink temp; creates parent directories. false when the target exists. */
|
|
19
|
+
createExclusive(path: string, content: string): Promise<boolean>;
|
|
20
|
+
/** rename(2); false when the source does not exist; creates the target's parent directory when it is missing. Exactly one of two concurrent renames of one file succeeds. */
|
|
21
|
+
rename(from: string, to: string): Promise<boolean>;
|
|
22
|
+
/** No error when absent. */
|
|
23
|
+
remove(path: string): Promise<void>;
|
|
24
|
+
/** lstat; null when absent. */
|
|
25
|
+
facts(path: string): Promise<FileFacts | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Copies a file's bytes over the target in one open (copy-on-write where
|
|
28
|
+
* the file system offers it); false when the source does not exist. The
|
|
29
|
+
* copy's modification time is the source's, rounded down to the whole
|
|
30
|
+
* second and read before the copy, so never later than the copied bytes:
|
|
31
|
+
* git judges an index entry "racily clean" against its index file's own
|
|
32
|
+
* modification time, in whole seconds, and a fresh time on a copied index
|
|
33
|
+
* would hide a same-size edit made in the second the source was written.
|
|
34
|
+
*/
|
|
35
|
+
copy(from: string, to: string): Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* An opaque identity of the file's current version (stat: device, inode,
|
|
38
|
+
* size, modification and change time); null when absent. Two equal values
|
|
39
|
+
* mean no rename replaced the file and no write changed it in between (design 12.7).
|
|
40
|
+
*/
|
|
41
|
+
identity(path: string): Promise<string | null>;
|
|
42
|
+
/** link(2): a second name for the same file; false when the source does not exist. Fails when the target exists. */
|
|
43
|
+
link(existing: string, target: string): Promise<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Whether two existing paths are on one file system (stat's device), so
|
|
46
|
+
* link(2) and rename(2) between them can work; throws when either is
|
|
47
|
+
* absent (design 13.11, Decision 117).
|
|
48
|
+
*/
|
|
49
|
+
sameFileSystem(left: string, right: string): Promise<boolean>;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repository hooks enmetu runs around its own commit when commitHooks is
|
|
3
|
+
* true (design 12.7). The message hooks (prepare-commit-msg, commit-msg) do
|
|
4
|
+
* not run, so no hook can put a name into enmetu's message (Decision 80).
|
|
5
|
+
*/
|
|
6
|
+
export type CommitHookName = 'pre-commit' | 'post-commit' | 'post-index-change';
|
|
7
|
+
export type HookRun = {
|
|
8
|
+
readonly kind: 'passed';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The hook exited non-zero, outlived hookTimeoutMs or was ended by a signal;
|
|
12
|
+
* detail is its stderr, or the timeout or signal.
|
|
13
|
+
*/
|
|
14
|
+
| {
|
|
15
|
+
readonly kind: 'rejected';
|
|
16
|
+
readonly detail: string;
|
|
17
|
+
};
|
|
18
|
+
export interface CommitTreeInput {
|
|
19
|
+
readonly tree: string;
|
|
20
|
+
/** HEAD at the time the commit index was seeded: the only parent, never MERGE_HEAD. */
|
|
21
|
+
readonly parent: string;
|
|
22
|
+
/** The whole message, passed on stdin. */
|
|
23
|
+
readonly message: string;
|
|
24
|
+
/** -S when true, --no-gpg-sign when false: commit-tree does not read commit.gpgSign itself. */
|
|
25
|
+
readonly sign: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Runs one repository hook through `git hook run --ignore-missing`, which
|
|
29
|
+
* honours core.hooksPath; a missing hook passes. The hook gets its own
|
|
30
|
+
* timeout (hookTimeoutMs) and its output is truncated, never a failure. The
|
|
31
|
+
* facade it is called on decides GIT_INDEX_FILE: the commit index for
|
|
32
|
+
* pre-commit, the real index for post-commit and post-index-change.
|
|
33
|
+
*/
|
|
34
|
+
export interface IGitHookRunner {
|
|
35
|
+
runHook(name: CommitHookName, args: readonly string[]): Promise<HookRun>;
|
|
36
|
+
/**
|
|
37
|
+
* Whether `git hook run` would find the hook: the path git itself resolves
|
|
38
|
+
* (`rev-parse --git-path hooks/<name>`, which honours core.hooksPath) is an
|
|
39
|
+
* executable file, or a symbolic link, which counts too, so a doubt
|
|
40
|
+
* costs a redundant check rather than a missed hook.
|
|
41
|
+
*/
|
|
42
|
+
hookExists(name: CommitHookName): Promise<boolean>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Plumbing for enmetu's own commits on the commit index
|
|
46
|
+
* (<stateDir>/commit-index): nothing here touches the real index, HEAD or a
|
|
47
|
+
* ref, and nothing here runs a hook but runHook.
|
|
48
|
+
*/
|
|
49
|
+
export interface IGitCommitter extends IGitHookRunner {
|
|
50
|
+
/**
|
|
51
|
+
* git read-tree --reset <commit>, hookless: the commit index holds that
|
|
52
|
+
* commit's tree, stat data kept where content matches. --reset, not -m,
|
|
53
|
+
* because -m refuses an index with unmerged entries (a conflicted stash pop).
|
|
54
|
+
*/
|
|
55
|
+
seedFrom(commit: string): Promise<void>;
|
|
56
|
+
/** git write-tree of the commit index, hookless. */
|
|
57
|
+
writeTree(): Promise<string>;
|
|
58
|
+
/** git commit-tree; runs no hook, concludes nothing, moves no ref. */
|
|
59
|
+
commitTree(input: CommitTreeInput): Promise<string>;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** What startup found of an index.lock a crashed enmetu left (design 12.7). */
|
|
2
|
+
export type StaleIndexLock =
|
|
3
|
+
/** No index.lock, one that is not enmetu's, or one whose enmetu is still alive: nothing touched. */
|
|
4
|
+
{
|
|
5
|
+
readonly kind: 'none';
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Removed. kept: HEAD is the commit the lock names, so the installed index
|
|
9
|
+
* already matches it. restored: HEAD is not, so index.before went back over
|
|
10
|
+
* the real index.
|
|
11
|
+
*/
|
|
12
|
+
| {
|
|
13
|
+
readonly kind: 'released';
|
|
14
|
+
readonly index: 'kept' | 'restored';
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* git's own lockfile protocol on <gitDir>/index.lock, held by enmetu across
|
|
18
|
+
* the index install and the branch swap of its commit (design 12.7), so no
|
|
19
|
+
* git command can write the real index, start a merge or commit a stale index
|
|
20
|
+
* inside that window. The lock file's content names enmetu, its pid and the
|
|
21
|
+
* commit it is about to install, so a lock left by a crashed enmetu can be told
|
|
22
|
+
* from a person's and finished or undone at the next start.
|
|
23
|
+
*/
|
|
24
|
+
export interface IGitIndexLock {
|
|
25
|
+
/** createExclusive(index.lock, 'enmetu index lock, pid <pid>, commit <commit>\n'); false when another process holds it. */
|
|
26
|
+
acquire(commit: string): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Under the lock: hard-links <gitDir>/index to <stateDir>/index.before (the
|
|
29
|
+
* person's current index), then renames the prepared index over
|
|
30
|
+
* <gitDir>/index. A reader sees the old index or the new one, never none.
|
|
31
|
+
*/
|
|
32
|
+
install(preparedIndex: string): Promise<void>;
|
|
33
|
+
/** Under the lock, after a failed swap: renames index.before back over <gitDir>/index. */
|
|
34
|
+
restore(): Promise<void>;
|
|
35
|
+
/** Removes index.before, then the lock this instance holds; the index stays unchanged. */
|
|
36
|
+
release(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Startup only, with the current HEAD: an index.lock with enmetu's content
|
|
39
|
+
* and a dead pid is finished (HEAD is the named commit: index.before removed)
|
|
40
|
+
* or undone (index.before renamed back), then removed. Never touches any other
|
|
41
|
+
* index.lock.
|
|
42
|
+
*/
|
|
43
|
+
releaseStale(head: string | null): Promise<StaleIndexLock>;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Divergence, HeadMove, RebaseStart, RemoteHead, RepositoryState, StagedFile, StatusEntry, Upstream } from './gitTypes.js';
|
|
2
|
+
import type { IGitConfigReader } from './IGitConfigReader.js';
|
|
3
|
+
export interface IGitReader extends IGitConfigReader {
|
|
4
|
+
repositoryState(): Promise<RepositoryState>;
|
|
5
|
+
/** null when HEAD is unborn. */
|
|
6
|
+
headCommit(): Promise<string | null>;
|
|
7
|
+
hasCommitterIdentity(): Promise<boolean>;
|
|
8
|
+
/** null when the branch has no upstream. */
|
|
9
|
+
upstream(branchRef: string): Promise<Upstream | null>;
|
|
10
|
+
/** Whole tree, or only the given root-relative paths. */
|
|
11
|
+
status(paths?: readonly string[] | undefined): Promise<readonly StatusEntry[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Staged regular files, read one cat-file batch chunk at a time, so at most
|
|
14
|
+
* one chunk of content is held while the consumer inspects it.
|
|
15
|
+
*/
|
|
16
|
+
stagedFiles(maxBytes: number): AsyncIterable<StagedFile>;
|
|
17
|
+
hasStagedChanges(): Promise<boolean>;
|
|
18
|
+
/** Root-relative paths whose index entry differs from HEAD's, a rename being a delete plus an add (Decision 98). */
|
|
19
|
+
stagedPaths(): Promise<readonly string[]>;
|
|
20
|
+
remoteHead(remote: string, remoteRef: string): Promise<RemoteHead>;
|
|
21
|
+
/** null when the ref does not exist. */
|
|
22
|
+
resolveCommit(ref: string): Promise<string | null>;
|
|
23
|
+
isAncestor(ancestor: string, descendant: string): Promise<boolean>;
|
|
24
|
+
divergence(upstreamSha: string): Promise<Divergence>;
|
|
25
|
+
hasUnmergedPaths(): Promise<boolean>;
|
|
26
|
+
/** orig-head of a rebase in progress, null when none is in progress. */
|
|
27
|
+
rebaseOrigHead(): Promise<string | null>;
|
|
28
|
+
/** onto of a rebase in progress, null when none is in progress. */
|
|
29
|
+
rebaseOnto(): Promise<string | null>;
|
|
30
|
+
/**
|
|
31
|
+
* The branch a rebase in progress returns to: its head-name file
|
|
32
|
+
* (`refs/heads/main`), which `git rebase --abort` resets to orig-head with
|
|
33
|
+
* no old-value check. null when no rebase is in progress or it started on a
|
|
34
|
+
* detached HEAD (head-name `detached HEAD`). Design 14.4, Decision 122.
|
|
35
|
+
*/
|
|
36
|
+
rebaseHeadName(): Promise<string | null>;
|
|
37
|
+
/**
|
|
38
|
+
* HEAD's newest "(start)" reflog entry: `git reflog exists HEAD` (exit 1:
|
|
39
|
+
* unlogged), then `git log -g -1 --format=%gs --grep-reflog=" (start): "
|
|
40
|
+
* HEAD` (empty: none). rebaseOwner (src/core/git/rebaseOwnership.ts) turns
|
|
41
|
+
* it and the journal into the owner of the rebase in progress (design
|
|
42
|
+
* 13.2, Decision 104).
|
|
43
|
+
*/
|
|
44
|
+
rebaseStart(): Promise<RebaseStart>;
|
|
45
|
+
/**
|
|
46
|
+
* HEAD's move since the rebase run under reflogAction (its
|
|
47
|
+
* GIT_REFLOG_ACTION) last moved it, read from HEAD's newest reflog entries
|
|
48
|
+
* (`git log -g -n <bound> --format="%H %gs" HEAD`). null when the newest
|
|
49
|
+
* entry is the rebase's own (`<reflogAction> (`), when none of the read
|
|
50
|
+
* entries is, or when HEAD has no reflog (design 14.14, Decision 124).
|
|
51
|
+
*/
|
|
52
|
+
headMovedSince(reflogAction: string): Promise<HeadMove | null>;
|
|
53
|
+
/** The commit's subject line (`git log -1 --format=%s <commit>`), design 14.15. */
|
|
54
|
+
commitSubject(commit: string): Promise<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Whether an orphaned commit was dealt with (design 14.15): a local branch
|
|
57
|
+
* contains it (`git for-each-ref --count=1 --contains <commit> refs/heads/`,
|
|
58
|
+
* after a merge or a branch made at it), or a commit on a local branch
|
|
59
|
+
* committed since `since` records picking it (`git log --branches -F
|
|
60
|
+
* --grep="(cherry picked from commit <commit>)"`, which `git cherry-pick -x`
|
|
61
|
+
* writes, also after a resolved conflict). The commit must resolve.
|
|
62
|
+
*/
|
|
63
|
+
onLocalBranch(commit: string, since: string): Promise<boolean>;
|
|
64
|
+
readBlob(blobId: string): Promise<Buffer>;
|
|
65
|
+
/** Blob id the file's current bytes would get, without filters, without writing. */
|
|
66
|
+
hashFile(path: string): Promise<string>;
|
|
67
|
+
/** Blob id `git add` would stage for the file's current bytes, filters applied, without writing an object. */
|
|
68
|
+
worktreeBlobId(path: string): Promise<string>;
|
|
69
|
+
/** Blob id of the path in a commit's tree, or in the index's stage-0 entry when commit is null; null when there is none. */
|
|
70
|
+
entryBlobId(path: string, commit: string | null): Promise<string | null>;
|
|
71
|
+
/**
|
|
72
|
+
* git merge-file -p of three files given by absolute path: the merged bytes
|
|
73
|
+
* when the three-way merge is clean, null when it conflicts or git cannot
|
|
74
|
+
* merge them (binary). Writes nothing, no conflict marker ever leaves it.
|
|
75
|
+
*/
|
|
76
|
+
mergeFile(current: string, base: string, other: string): Promise<Buffer | null>;
|
|
77
|
+
/** The best common ancestor of two commits; null when they share none (design 12.3). */
|
|
78
|
+
mergeBase(left: string, right: string): Promise<string | null>;
|
|
79
|
+
/** Root-relative paths that differ between two commits, a rename being a delete plus an add, in git's order. */
|
|
80
|
+
changedPaths(from: string, to: string): Promise<readonly string[]>;
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface GitRunOptions {
|
|
2
|
+
/** Written to git's stdin, then stdin is closed. */
|
|
3
|
+
readonly stdin?: string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Replaces the adapter's per-command timeout for this call: a repository
|
|
6
|
+
* hook's own budget (design 12.7). The call runs git, leading its own
|
|
7
|
+
* process group and the timeout ends the whole group (SIGTERM, then SIGKILL
|
|
8
|
+
* after 2 s), so a hook git started never outlives it.
|
|
9
|
+
*/
|
|
10
|
+
readonly timeoutMs?: number | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Keeps at most this many bytes of stdout and of stderr and drops the rest
|
|
13
|
+
* instead of failing (a repository hook's output, design 12.7).
|
|
14
|
+
*/
|
|
15
|
+
readonly outputCapBytes?: number | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Ends the call like its timeout once aborted (the whole process group,
|
|
18
|
+
* SIGTERM, then SIGKILL after 2 s), and the call throws GIT_ABORTED; an
|
|
19
|
+
* already aborted signal spawns nothing. A stop request ends a running
|
|
20
|
+
* repository hook this way (design 12.7, Decision 92).
|
|
21
|
+
*/
|
|
22
|
+
readonly abort?: AbortSignal | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Sets GIT_REFLOG_ACTION for this call only, so the entries git writes to
|
|
25
|
+
* HEAD's reflog carry this action instead of the command's name: enmetu's
|
|
26
|
+
* rebase writes "enmetu <cycleId> <at> (start): checkout <sha>", the
|
|
27
|
+
* attempt's own token, which tells it from a person's rebase with the same
|
|
28
|
+
* orig-head and onto and from an earlier attempt of enmetu's (design 13.2,
|
|
29
|
+
* Decision 104).
|
|
30
|
+
*/
|
|
31
|
+
readonly reflogAction?: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
export interface GitRunResult {
|
|
34
|
+
readonly exitCode: number;
|
|
35
|
+
readonly stdout: Buffer;
|
|
36
|
+
readonly stderr: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runs the git binary with an argument array (execFile, never a shell).
|
|
40
|
+
* Never throws for a non-zero exit code: the caller interprets it.
|
|
41
|
+
* Throws GitCommandError GIT_NOT_FOUND when the binary cannot be spawned and
|
|
42
|
+
* GIT_TIMEOUT when the per-command timeout elapses (git is killed with SIGTERM),
|
|
43
|
+
* and GIT_ABORTED when options.abort ends the call.
|
|
44
|
+
*/
|
|
45
|
+
export interface IGitRunner {
|
|
46
|
+
run(args: readonly string[], options?: GitRunOptions): Promise<GitRunResult>;
|
|
47
|
+
}
|
|
48
|
+
/** Builds a runner for a working directory; the locator needs one before the root is known. */
|
|
49
|
+
export type GitRunnerFactory = (cwd: string) => IGitRunner;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FetchResult, PushResult, RebaseResult, StageResult } from './gitTypes.js';
|
|
2
|
+
export interface IGitWriter {
|
|
3
|
+
stageAll(): Promise<StageResult>;
|
|
4
|
+
unstage(paths: readonly string[]): Promise<void>;
|
|
5
|
+
/** Fetches remoteRef into intoRef only; no other ref moves. */
|
|
6
|
+
fetch(remote: string, remoteRef: string, intoRef: string): Promise<FetchResult>;
|
|
7
|
+
/**
|
|
8
|
+
* Compare-and-swap: moves ref to newSha only while it still points at oldSha
|
|
9
|
+
* (oldSha null: only while the ref does not exist). A mismatch throws GIT_FAILED.
|
|
10
|
+
*/
|
|
11
|
+
updateRef(ref: string, newSha: string, oldSha: string | null): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* reflogAction: GIT_REFLOG_ACTION for this rebase only, the attempt's own
|
|
14
|
+
* token (rebaseReflogAction of its journal entry), so HEAD's reflog names
|
|
15
|
+
* this attempt in its "(start)" entry (design 13.2, Decision 104).
|
|
16
|
+
*/
|
|
17
|
+
rebase(ontoSha: string, reflogAction: string): Promise<RebaseResult>;
|
|
18
|
+
/** Exit code ignored on purpose: the caller verifies the outcome. */
|
|
19
|
+
abortRebase(): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Ends enmetu's own rebase at the branch's current tip instead of at
|
|
22
|
+
* orig-head: `git rebase --quit` (removes the rebase state, writes no ref,
|
|
23
|
+
* index entry or file), then `git switch --discard-changes <branch>` (index
|
|
24
|
+
* and tree to the branch's tip, whatever they hold, as the abort's reset
|
|
25
|
+
* does). For a branch a commit moved during the rebase, which
|
|
26
|
+
* `git rebase --abort` would reset to orig-head and so drop that commit
|
|
27
|
+
* (design 14.4, Decision 122). Exit codes ignored: the caller verifies.
|
|
28
|
+
*/
|
|
29
|
+
endRebaseAtBranch(branchRef: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Pushes exactly this commit to remoteRef (`<sha>:<remoteRef>`), never a
|
|
32
|
+
* ref git resolves at push time, so the caller knows what it pushed
|
|
33
|
+
* (design 13.11, Decision 111).
|
|
34
|
+
*/
|
|
35
|
+
push(remote: string, sha: string, remoteRef: string): Promise<PushResult>;
|
|
36
|
+
/** Writes the file's bytes into the object store without filters; returns the blob id. */
|
|
37
|
+
storeBlob(path: string): Promise<string>;
|
|
38
|
+
restoreFromIndex(paths: readonly string[]): Promise<void>;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type HaltCode = 'REBASE_ABORT_UNVERIFIED' | 'UPSTREAM_REWRITTEN' | 'QUARANTINE_RESTORE_FAILED' | 'HALT_MARKER_UNREADABLE';
|
|
2
|
+
export interface HaltRecord {
|
|
3
|
+
readonly code: HaltCode;
|
|
4
|
+
readonly message: string;
|
|
5
|
+
readonly cycleId: string | null;
|
|
6
|
+
/** ISO-8601 UTC; 'unknown' for a marker that cannot be parsed (HALT_MARKER_UNREADABLE). */
|
|
7
|
+
readonly at: string;
|
|
8
|
+
}
|
|
9
|
+
export interface IHaltStore {
|
|
10
|
+
/** An unparseable marker means halted, with code HALT_MARKER_UNREADABLE (fail-safe). */
|
|
11
|
+
read(): Promise<HaltRecord | null>;
|
|
12
|
+
set(record: HaltRecord): Promise<void>;
|
|
13
|
+
clear(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|