@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,249 @@
|
|
|
1
|
+
import { EXIT } from '../../contracts/exitCode.js';
|
|
2
|
+
import { verifiedRefFor } from '../../core/git/enmetuRefs.js';
|
|
3
|
+
import { resumeHint } from '../../core/halt/resumeHint.js';
|
|
4
|
+
import { stateLiveness } from '../../core/state/stateLiveness.js';
|
|
5
|
+
import { conflictGuide, pathsOnBothSides, } from '../../core/status/conflictGuide.js';
|
|
6
|
+
import { orphanedCommitLines } from '../../core/status/orphanedCommits.js';
|
|
7
|
+
import { statusHeadline, } from '../../core/status/statusHeadline.js';
|
|
8
|
+
/** Labels padded to one column, so the values line up (design 1.18). */
|
|
9
|
+
const LABEL_WIDTH = 12;
|
|
10
|
+
const NO_DIVERGENCE = '-';
|
|
11
|
+
/** The uncommitted line names this many paths, then "and <n> more" (design 12.2). */
|
|
12
|
+
export const MAX_UNCOMMITTED_PATHS = 20;
|
|
13
|
+
/**
|
|
14
|
+
* Controller:
|
|
15
|
+
* 1. locator.locate(startDir); not found or bare: exit 5.
|
|
16
|
+
* 2. Gather the facts, all local (no network, no write): state.read(),
|
|
17
|
+
* lock.inspect(), state.read() again, liveness judged with the second
|
|
18
|
+
* record and the holder (Decision 86); halt.read(), journal.read(),
|
|
19
|
+
* quarantine.hasPending(); branch and upstream, divergence against
|
|
20
|
+
* trackingSha ?? verifiedSha (Decision 66); uncommitted paths from
|
|
21
|
+
* git.status(), which runs with GIT_OPTIONAL_LOCKS=0, so no index write
|
|
22
|
+
* (Decision 85). While a conflict is held at HEAD, the conflict guide
|
|
23
|
+
* (design 12.3) from merge-base and two name-only diffs, which write no
|
|
24
|
+
* object (Decision 67). Each recorded orphaned commit asked of git
|
|
25
|
+
* (design 14.15).
|
|
26
|
+
* 3. Print the headline, then one labelled line per fact (design 1.18, 12.2:
|
|
27
|
+
* state after instance, uncommitted after divergence, then the pending
|
|
28
|
+
* orphaned commits, then the guide);
|
|
29
|
+
* halted exits 3, else 0. The exit codes answer scripts and are unchanged
|
|
30
|
+
* (Decision 66).
|
|
31
|
+
*/
|
|
32
|
+
export async function statusCommand(input, deps) {
|
|
33
|
+
// --- Step 1: the repository root through git ---
|
|
34
|
+
const location = await deps.locator.locate(input.startDir);
|
|
35
|
+
if (location.kind !== 'found') {
|
|
36
|
+
const reason = location.kind === 'bare'
|
|
37
|
+
? 'is a bare repository'
|
|
38
|
+
: 'is not inside a git work tree';
|
|
39
|
+
deps.stdout.write(line('repository', `${input.startDir} ${reason}`));
|
|
40
|
+
return { exitCode: EXIT.NOT_ELIGIBLE, report: null };
|
|
41
|
+
}
|
|
42
|
+
const sources = deps.openSources(location.paths);
|
|
43
|
+
// --- Step 2: local facts only ---
|
|
44
|
+
// The first read only orders the lock between two reads: a stop or a start
|
|
45
|
+
// between them then yields the second record, which is judged (Decision 86).
|
|
46
|
+
await sources.state.read();
|
|
47
|
+
const holder = await sources.lock.inspect();
|
|
48
|
+
const record = await sources.state.read();
|
|
49
|
+
const halt = await sources.halt.read();
|
|
50
|
+
const branch = await describeBranch(sources.git);
|
|
51
|
+
const uncommitted = await sources.git.status();
|
|
52
|
+
const orphaned = await pendingOrphans(record, sources.git);
|
|
53
|
+
const report = {
|
|
54
|
+
headline: statusHeadline({
|
|
55
|
+
halt,
|
|
56
|
+
liveness: stateLiveness(record, holder),
|
|
57
|
+
record,
|
|
58
|
+
unpushed: branch.unpushed,
|
|
59
|
+
uncommitted: uncommitted.length,
|
|
60
|
+
orphaned,
|
|
61
|
+
}),
|
|
62
|
+
repository: location.paths.root,
|
|
63
|
+
instance: describeInstance(holder),
|
|
64
|
+
state: describeState(record),
|
|
65
|
+
halt: describeHalt(halt),
|
|
66
|
+
recovery: describeRecovery((await sources.journal.read()) !== null, await sources.quarantine.hasPending()),
|
|
67
|
+
branch: branch.branch,
|
|
68
|
+
divergence: branch.divergence,
|
|
69
|
+
uncommitted: describeUncommitted(uncommitted),
|
|
70
|
+
orphaned: orphanedCommitLines(orphaned),
|
|
71
|
+
conflictGuide: await guideFor(record, halt, branch, sources.git),
|
|
72
|
+
};
|
|
73
|
+
// --- Step 3: print, the headline first, then one fact per line ---
|
|
74
|
+
deps.stdout.write([
|
|
75
|
+
`${report.headline}\n`,
|
|
76
|
+
line('repository', report.repository),
|
|
77
|
+
line('instance', report.instance),
|
|
78
|
+
line('state', report.state),
|
|
79
|
+
line('halt', report.halt),
|
|
80
|
+
line('recovery', report.recovery),
|
|
81
|
+
line('branch', report.branch),
|
|
82
|
+
line('divergence', report.divergence),
|
|
83
|
+
line('uncommitted', report.uncommitted),
|
|
84
|
+
...report.orphaned.map((orphan) => line(orphan.label, orphan.value)),
|
|
85
|
+
...report.conflictGuide.map((guide) => line(guide.label, guide.value)),
|
|
86
|
+
].join(''));
|
|
87
|
+
return { exitCode: halt === null ? EXIT.OK : EXIT.HALTED, report };
|
|
88
|
+
}
|
|
89
|
+
function line(label, value) {
|
|
90
|
+
return `${label.padEnd(LABEL_WIDTH)}${value}\n`;
|
|
91
|
+
}
|
|
92
|
+
function describeInstance(holder) {
|
|
93
|
+
if (holder === null) {
|
|
94
|
+
return 'not running';
|
|
95
|
+
}
|
|
96
|
+
return `running, pid ${holder.pid} on ${holder.hostname} since ${holder.startedAt}`;
|
|
97
|
+
}
|
|
98
|
+
function describeState(record) {
|
|
99
|
+
if (record === null) {
|
|
100
|
+
return 'none recorded';
|
|
101
|
+
}
|
|
102
|
+
const reason = record.reason === null ? '' : ` ${record.reason}`;
|
|
103
|
+
const cycle = record.lastCycle === null
|
|
104
|
+
? 'no cycle finished yet'
|
|
105
|
+
: `last cycle ${record.lastCycle.cycleId} ${record.lastCycle.outcome} at ${record.lastCycle.at}`;
|
|
106
|
+
return `${record.state}${reason} since ${record.since}; ${cycle}`;
|
|
107
|
+
}
|
|
108
|
+
function describeHalt(record) {
|
|
109
|
+
if (record === null) {
|
|
110
|
+
return 'none';
|
|
111
|
+
}
|
|
112
|
+
return `${record.code} at ${record.at} (${record.cycleId ?? '-'}): ${record.message}. ${resumeHint(record.code)}`;
|
|
113
|
+
}
|
|
114
|
+
function describeRecovery(journal, memento) {
|
|
115
|
+
if (journal && memento) {
|
|
116
|
+
return 'rebase journal and quarantine memento pending';
|
|
117
|
+
}
|
|
118
|
+
if (journal) {
|
|
119
|
+
return 'rebase journal pending';
|
|
120
|
+
}
|
|
121
|
+
if (memento) {
|
|
122
|
+
return 'quarantine memento pending';
|
|
123
|
+
}
|
|
124
|
+
return 'none';
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Names the paths only; why one is uncommitted (quiet window, content guard,
|
|
128
|
+
* hook) would need every changed file read and the content rules run, and
|
|
129
|
+
* the EXCLUDED and HOOK_REJECTED log lines already say it (Decision 85).
|
|
130
|
+
*/
|
|
131
|
+
function describeUncommitted(entries) {
|
|
132
|
+
if (entries.length === 0) {
|
|
133
|
+
return 'none';
|
|
134
|
+
}
|
|
135
|
+
const named = entries
|
|
136
|
+
.slice(0, MAX_UNCOMMITTED_PATHS)
|
|
137
|
+
.map((entry) => entry.path)
|
|
138
|
+
.join(', ');
|
|
139
|
+
if (entries.length === 1) {
|
|
140
|
+
return `1 path: ${named}`;
|
|
141
|
+
}
|
|
142
|
+
const rest = entries.length - MAX_UNCOMMITTED_PATHS;
|
|
143
|
+
const more = rest > 0 ? ` and ${rest} more` : '';
|
|
144
|
+
return `${entries.length} paths: ${named}${more}`;
|
|
145
|
+
}
|
|
146
|
+
async function describeBranch(git) {
|
|
147
|
+
const { branchRef } = await git.repositoryState();
|
|
148
|
+
if (branchRef === null) {
|
|
149
|
+
return {
|
|
150
|
+
branch: 'detached HEAD',
|
|
151
|
+
divergence: NO_DIVERGENCE,
|
|
152
|
+
unpushed: { kind: 'no-upstream' },
|
|
153
|
+
upstream: null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const upstream = await git.upstream(branchRef);
|
|
157
|
+
if (upstream === null) {
|
|
158
|
+
return {
|
|
159
|
+
branch: `${branchRef} has no upstream`,
|
|
160
|
+
divergence: NO_DIVERGENCE,
|
|
161
|
+
unpushed: { kind: 'no-upstream' },
|
|
162
|
+
upstream: null,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const branch = `${branchRef} -> ${upstream.remote} ${upstream.remoteRef}`;
|
|
166
|
+
// The tracking ref, never ls-remote: status makes no network call. A prune
|
|
167
|
+
// can remove it from a fully pushed clone; the verified ref is the base the
|
|
168
|
+
// cycle's UNPUSHED line falls back to (Decision 59), so status uses it too.
|
|
169
|
+
const trackingSha = await git.resolveCommit(upstream.trackingRef);
|
|
170
|
+
if (trackingSha !== null) {
|
|
171
|
+
const { ahead, behind } = await git.divergence(trackingSha);
|
|
172
|
+
return {
|
|
173
|
+
branch,
|
|
174
|
+
divergence: `${ahead} ahead, ${behind} behind (against the last fetched upstream)`,
|
|
175
|
+
unpushed: { kind: 'counted', ahead },
|
|
176
|
+
upstream,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const verifiedSha = await git.resolveCommit(verifiedRefFor(branchRef));
|
|
180
|
+
if (verifiedSha !== null) {
|
|
181
|
+
const { ahead, behind } = await git.divergence(verifiedSha);
|
|
182
|
+
return {
|
|
183
|
+
branch,
|
|
184
|
+
divergence: `${ahead} ahead, ${behind} behind (against the upstream enmetu last verified; the tracking ref is gone)`,
|
|
185
|
+
unpushed: { kind: 'counted', ahead },
|
|
186
|
+
upstream,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
branch,
|
|
191
|
+
divergence: 'unknown (the upstream was never fetched or verified)',
|
|
192
|
+
unpushed: { kind: 'unknown' },
|
|
193
|
+
upstream,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The recorded orphaned commits git still finds on no local branch (design
|
|
198
|
+
* 14.15). Asked here, at every status, not at cycle end: status is the one
|
|
199
|
+
* reader that shows them, a stopped daemon's record must lose an orphan the
|
|
200
|
+
* person picked meanwhile, and a cycle-end check would spend spawns every
|
|
201
|
+
* poll for a list nobody reads then. An orphan git can no longer resolve
|
|
202
|
+
* (gc pruned it) is gone; reading is all this does, the file keeps the entry
|
|
203
|
+
* until newer orphans push it out.
|
|
204
|
+
*/
|
|
205
|
+
async function pendingOrphans(record, git) {
|
|
206
|
+
const pending = [];
|
|
207
|
+
for (const orphan of record?.orphaned ?? []) {
|
|
208
|
+
if ((await git.resolveCommit(orphan.commit)) !== null &&
|
|
209
|
+
!(await git.onLocalBranch(orphan.commit, orphan.at))) {
|
|
210
|
+
pending.push(orphan);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return pending;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The guide only while the record holds a conflict, no halt marker exists and
|
|
217
|
+
* HEAD is still the conflict's head: a person who already rebased, merged or
|
|
218
|
+
* reset would otherwise be told to resolve a conflict that is gone. The paths
|
|
219
|
+
* come from merge-base and two name-only diffs, which write nothing; merge-tree
|
|
220
|
+
* --write-tree would name exactly the conflicting paths but writes objects
|
|
221
|
+
* (Decision 67). Without an upstream there is no command to give.
|
|
222
|
+
*/
|
|
223
|
+
async function guideFor(record, halt, branch, git) {
|
|
224
|
+
if (record === null ||
|
|
225
|
+
record.state !== 'conflict' ||
|
|
226
|
+
record.conflict === null ||
|
|
227
|
+
halt !== null ||
|
|
228
|
+
branch.upstream === null) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
const { conflict } = record;
|
|
232
|
+
if ((await git.headCommit()) !== conflict.head) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
const base = await git.mergeBase(conflict.head, conflict.upstream);
|
|
236
|
+
// Unrelated histories have no base; the guide then says none were found
|
|
237
|
+
// and names the diff a person can read instead.
|
|
238
|
+
const paths = base === null
|
|
239
|
+
? []
|
|
240
|
+
: pathsOnBothSides(await git.changedPaths(base, conflict.head), await git.changedPaths(base, conflict.upstream));
|
|
241
|
+
return conflictGuide({
|
|
242
|
+
since: record.since,
|
|
243
|
+
conflict,
|
|
244
|
+
branchRef: branch.upstream.branchRef,
|
|
245
|
+
upstream: branch.upstream,
|
|
246
|
+
unpushed: branch.unpushed.kind === 'counted' ? branch.unpushed.ahead : null,
|
|
247
|
+
paths,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ExitCode } from '../contracts/exitCode.js';
|
|
2
|
+
/**
|
|
3
|
+
* The one mapping from errors to exit codes: USAGE and CONFIG_* to 2, HALTED
|
|
4
|
+
* to 3, LOCK_* to 4, repository and git eligibility codes to 5, anything else
|
|
5
|
+
* (unknown values included) to 1.
|
|
6
|
+
*/
|
|
7
|
+
export declare function exitCodeFor(error: unknown): ExitCode;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EnmetuError } from '../contracts/errors.js';
|
|
2
|
+
import { EXIT } from '../contracts/exitCode.js';
|
|
3
|
+
/** Every code named, so a new error code fails the type check until it is mapped. */
|
|
4
|
+
const EXIT_CODE_BY_ERROR = {
|
|
5
|
+
USAGE: EXIT.USAGE,
|
|
6
|
+
CONFIG_INVALID_JSON: EXIT.USAGE,
|
|
7
|
+
CONFIG_UNKNOWN_KEY: EXIT.USAGE,
|
|
8
|
+
CONFIG_INVALID_VALUE: EXIT.USAGE,
|
|
9
|
+
HALTED: EXIT.HALTED,
|
|
10
|
+
LOCK_HELD: EXIT.LOCKED,
|
|
11
|
+
LOCK_UNREADABLE: EXIT.LOCKED,
|
|
12
|
+
GIT_NOT_FOUND: EXIT.NOT_ELIGIBLE,
|
|
13
|
+
GIT_TOO_OLD: EXIT.NOT_ELIGIBLE,
|
|
14
|
+
NOT_A_REPOSITORY: EXIT.NOT_ELIGIBLE,
|
|
15
|
+
BARE_REPOSITORY: EXIT.NOT_ELIGIBLE,
|
|
16
|
+
PATH_UNSUPPORTED: EXIT.NOT_ELIGIBLE,
|
|
17
|
+
OPERATION_IN_PROGRESS: EXIT.NOT_ELIGIBLE,
|
|
18
|
+
DETACHED_HEAD: EXIT.NOT_ELIGIBLE,
|
|
19
|
+
HEAD_UNBORN: EXIT.NOT_ELIGIBLE,
|
|
20
|
+
NO_UPSTREAM: EXIT.NOT_ELIGIBLE,
|
|
21
|
+
UPSTREAM_NOT_REMOTE: EXIT.NOT_ELIGIBLE,
|
|
22
|
+
IDENTITY_MISSING: EXIT.NOT_ELIGIBLE,
|
|
23
|
+
GIT_TIMEOUT: EXIT.FAILURE,
|
|
24
|
+
GIT_ABORTED: EXIT.FAILURE,
|
|
25
|
+
GIT_FAILED: EXIT.FAILURE,
|
|
26
|
+
GIT_INDEX_LOCKED: EXIT.FAILURE,
|
|
27
|
+
GIT_OUTPUT_UNPARSEABLE: EXIT.FAILURE,
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The one mapping from errors to exit codes: USAGE and CONFIG_* to 2, HALTED
|
|
31
|
+
* to 3, LOCK_* to 4, repository and git eligibility codes to 5, anything else
|
|
32
|
+
* (unknown values included) to 1.
|
|
33
|
+
*/
|
|
34
|
+
export function exitCodeFor(error) {
|
|
35
|
+
if (error instanceof EnmetuError) {
|
|
36
|
+
return EXIT_CODE_BY_ERROR[error.code];
|
|
37
|
+
}
|
|
38
|
+
return EXIT.FAILURE;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HELP_TEXT = "Usage: enmetu <command> [options]\n\nKeeps the git working tree of the current repository committed and in sync\nwith the upstream of its checked-out branch.\n\nCommands:\n run [--repo <path>] Start the daemon in the foreground\n status [--repo <path>] Print instance, halt, recovery, branch and divergence\n resume [--repo <path>] [--accept-upstream]\n Clear a halt after verifying the repository\n hook pre-commit The pre-commit check (called by the installed hook)\n\nOptions:\n -h, --help Print this text\n -v, --version Print the version\n";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const HELP_TEXT = `Usage: enmetu <command> [options]
|
|
2
|
+
|
|
3
|
+
Keeps the git working tree of the current repository committed and in sync
|
|
4
|
+
with the upstream of its checked-out branch.
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
run [--repo <path>] Start the daemon in the foreground
|
|
8
|
+
status [--repo <path>] Print instance, halt, recovery, branch and divergence
|
|
9
|
+
resume [--repo <path>] [--accept-upstream]
|
|
10
|
+
Clear a halt after verifying the repository
|
|
11
|
+
hook pre-commit The pre-commit check (called by the installed hook)
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-h, --help Print this text
|
|
15
|
+
-v, --version Print the version
|
|
16
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ExitCode } from '../contracts/exitCode.js';
|
|
2
|
+
import type { Runtime } from '../contracts/runtime.js';
|
|
3
|
+
/**
|
|
4
|
+
* Entry dispatch: parseCommandLine, then the command's controller with deps
|
|
5
|
+
* from the composition root; help and version print to stdout; any error is
|
|
6
|
+
* mapped by exitCodeFor and printed to stderr in the form `enmetu: <message>`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function main(runtime: Runtime): Promise<ExitCode>;
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { EXIT } from '../contracts/exitCode.js';
|
|
3
|
+
import { errorMessage } from '../core/errors/errorMessage.js';
|
|
4
|
+
import { createHookCheckDeps, createResumeDeps, createRunDeps, createStatusDeps, } from '../enmetuInstances.js';
|
|
5
|
+
import { hookCommand } from './commands/hookCommand.js';
|
|
6
|
+
import { resumeCommand } from './commands/resumeCommand.js';
|
|
7
|
+
import { runCommand } from './commands/runCommand.js';
|
|
8
|
+
import { statusCommand } from './commands/statusCommand.js';
|
|
9
|
+
import { exitCodeFor } from './exitCodes.js';
|
|
10
|
+
import { HELP_TEXT } from './helpText.js';
|
|
11
|
+
import { parseCommandLine } from './parseArgs.js';
|
|
12
|
+
/**
|
|
13
|
+
* Entry dispatch: parseCommandLine, then the command's controller with deps
|
|
14
|
+
* from the composition root; help and version print to stdout; any error is
|
|
15
|
+
* mapped by exitCodeFor and printed to stderr in the form `enmetu: <message>`.
|
|
16
|
+
*/
|
|
17
|
+
export async function main(runtime) {
|
|
18
|
+
try {
|
|
19
|
+
const command = parseCommandLine(runtime.argv);
|
|
20
|
+
switch (command.kind) {
|
|
21
|
+
case 'help':
|
|
22
|
+
runtime.stdout.write(HELP_TEXT);
|
|
23
|
+
return EXIT.OK;
|
|
24
|
+
case 'version':
|
|
25
|
+
runtime.stdout.write(`${runtime.version}\n`);
|
|
26
|
+
return EXIT.OK;
|
|
27
|
+
case 'run':
|
|
28
|
+
return (await runCommand({ startDir: startDirOf(command.repo, runtime) }, createRunDeps(runtime))).exitCode;
|
|
29
|
+
case 'status':
|
|
30
|
+
return (await statusCommand({ startDir: startDirOf(command.repo, runtime) }, createStatusDeps(runtime))).exitCode;
|
|
31
|
+
case 'resume':
|
|
32
|
+
return (await resumeCommand({
|
|
33
|
+
startDir: startDirOf(command.repo, runtime),
|
|
34
|
+
acceptUpstream: command.acceptUpstream,
|
|
35
|
+
}, createResumeDeps(runtime))).exitCode;
|
|
36
|
+
case 'hook-pre-commit':
|
|
37
|
+
// Git runs hooks from the root of the work tree being committed.
|
|
38
|
+
return (await hookCommand({ startDir: runtime.cwd }, createHookCheckDeps(runtime))).exitCode;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
runtime.stderr.write(`enmetu: ${errorMessage(error)}\n`);
|
|
43
|
+
return exitCodeFor(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** --repo relative to the working directory, else the working directory. */
|
|
47
|
+
function startDirOf(repo, runtime) {
|
|
48
|
+
return repo === null ? runtime.cwd : resolve(runtime.cwd, repo);
|
|
49
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ParsedCommand = {
|
|
2
|
+
readonly kind: 'run';
|
|
3
|
+
readonly repo: string | null;
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: 'status';
|
|
6
|
+
readonly repo: string | null;
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: 'resume';
|
|
9
|
+
readonly repo: string | null;
|
|
10
|
+
readonly acceptUpstream: boolean;
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'hook-pre-commit';
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'help';
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'version';
|
|
17
|
+
};
|
|
18
|
+
/** node:util parseArgs with strict: true. Throws UsageError. No arguments is help. */
|
|
19
|
+
export declare function parseCommandLine(argv: readonly string[]): ParsedCommand;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { UsageError } from '../contracts/errors.js';
|
|
3
|
+
/** node:util parseArgs with strict: true. Throws UsageError. No arguments is help. */
|
|
4
|
+
export function parseCommandLine(argv) {
|
|
5
|
+
const line = parseLine(argv);
|
|
6
|
+
if (line.help) {
|
|
7
|
+
return { kind: 'help' };
|
|
8
|
+
}
|
|
9
|
+
if (line.version) {
|
|
10
|
+
return { kind: 'version' };
|
|
11
|
+
}
|
|
12
|
+
const [name, ...rest] = line.positionals;
|
|
13
|
+
if (name === undefined) {
|
|
14
|
+
if (line.repo !== null || line.acceptUpstream) {
|
|
15
|
+
throw new UsageError('missing command');
|
|
16
|
+
}
|
|
17
|
+
return { kind: 'help' };
|
|
18
|
+
}
|
|
19
|
+
switch (name) {
|
|
20
|
+
case 'run':
|
|
21
|
+
case 'status':
|
|
22
|
+
refuseExtraArguments(name, rest);
|
|
23
|
+
refuseAcceptUpstream(name, line);
|
|
24
|
+
return { kind: name, repo: line.repo };
|
|
25
|
+
case 'resume':
|
|
26
|
+
refuseExtraArguments(name, rest);
|
|
27
|
+
return {
|
|
28
|
+
kind: 'resume',
|
|
29
|
+
repo: line.repo,
|
|
30
|
+
acceptUpstream: line.acceptUpstream,
|
|
31
|
+
};
|
|
32
|
+
case 'hook':
|
|
33
|
+
return parseHook(rest, line);
|
|
34
|
+
default:
|
|
35
|
+
throw new UsageError(`unknown command '${name}'`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function parseLine(argv) {
|
|
39
|
+
try {
|
|
40
|
+
const { values, positionals } = parseArgs({
|
|
41
|
+
args: [...argv],
|
|
42
|
+
strict: true,
|
|
43
|
+
allowPositionals: true,
|
|
44
|
+
options: {
|
|
45
|
+
repo: { type: 'string' },
|
|
46
|
+
'accept-upstream': { type: 'boolean' },
|
|
47
|
+
help: { type: 'boolean', short: 'h' },
|
|
48
|
+
version: { type: 'boolean', short: 'v' },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
positionals,
|
|
53
|
+
repo: values.repo ?? null,
|
|
54
|
+
acceptUpstream: values['accept-upstream'] ?? false,
|
|
55
|
+
help: values.help ?? false,
|
|
56
|
+
version: values.version ?? false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = parseArgsMessage(error);
|
|
61
|
+
if (message === null) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
throw new UsageError(message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parseHook(rest, line) {
|
|
68
|
+
const [hookName, ...extra] = rest;
|
|
69
|
+
if (hookName !== 'pre-commit') {
|
|
70
|
+
throw new UsageError(hookName === undefined
|
|
71
|
+
? "hook needs a hook name: 'enmetu hook pre-commit'"
|
|
72
|
+
: `unknown hook '${hookName}'`);
|
|
73
|
+
}
|
|
74
|
+
refuseExtraArguments('hook pre-commit', extra);
|
|
75
|
+
if (line.repo !== null) {
|
|
76
|
+
// The hook runs where git runs it, the root of the work tree being committed.
|
|
77
|
+
throw new UsageError("'hook pre-commit' takes no --repo");
|
|
78
|
+
}
|
|
79
|
+
refuseAcceptUpstream('hook pre-commit', line);
|
|
80
|
+
return { kind: 'hook-pre-commit' };
|
|
81
|
+
}
|
|
82
|
+
function refuseExtraArguments(command, extra) {
|
|
83
|
+
const [first] = extra;
|
|
84
|
+
if (first !== undefined) {
|
|
85
|
+
throw new UsageError(`unexpected argument '${first}' after '${command}'`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function refuseAcceptUpstream(command, line) {
|
|
89
|
+
if (line.acceptUpstream) {
|
|
90
|
+
throw new UsageError(`--accept-upstream belongs to 'resume', not to '${command}'`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Structural, not instanceof: parseArgs throws a TypeError from Node's core
|
|
94
|
+
// realm, which differs from the caller's realm under Jest.
|
|
95
|
+
function parseArgsMessage(error) {
|
|
96
|
+
if (typeof error === 'object' &&
|
|
97
|
+
error !== null &&
|
|
98
|
+
'code' in error &&
|
|
99
|
+
typeof error.code === 'string' &&
|
|
100
|
+
error.code.startsWith('ERR_PARSE_ARGS_') &&
|
|
101
|
+
'message' in error &&
|
|
102
|
+
typeof error.message === 'string') {
|
|
103
|
+
return error.message;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { main } from './cli/main.js';
|
|
4
|
+
/**
|
|
5
|
+
* Entry: the only file that reads process.argv, process.env, process.cwd(),
|
|
6
|
+
* process.execPath, process.stdout and process.stderr and sets
|
|
7
|
+
* process.exitCode. It gathers them into a Runtime and calls main.
|
|
8
|
+
*/
|
|
9
|
+
/** package.json sits next to dist/ and src/, one level up from this file in both. */
|
|
10
|
+
function packageVersion() {
|
|
11
|
+
const manifest = createRequire(import.meta.url)('../package.json');
|
|
12
|
+
if (typeof manifest === 'object' &&
|
|
13
|
+
manifest !== null &&
|
|
14
|
+
'version' in manifest &&
|
|
15
|
+
typeof manifest.version === 'string') {
|
|
16
|
+
return manifest.version;
|
|
17
|
+
}
|
|
18
|
+
return 'unknown';
|
|
19
|
+
}
|
|
20
|
+
const runtime = {
|
|
21
|
+
argv: process.argv.slice(2),
|
|
22
|
+
env: process.env,
|
|
23
|
+
cwd: process.cwd(),
|
|
24
|
+
execPath: process.execPath,
|
|
25
|
+
// Node makes argv[1] absolute. Should it ever be missing, the hook records
|
|
26
|
+
// an empty path, finds no CLI there and lets commits through (fail open).
|
|
27
|
+
cliPath: process.argv[1] ?? '',
|
|
28
|
+
version: packageVersion(),
|
|
29
|
+
stdout: process.stdout,
|
|
30
|
+
stderr: process.stderr,
|
|
31
|
+
};
|
|
32
|
+
process.exitCode = await main(runtime);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { StagedSelectionMode } from '../contracts/config.js';
|
|
2
|
+
import type { IClock } from '../contracts/IClock.js';
|
|
3
|
+
import type { IFileSystem } from '../contracts/IFileSystem.js';
|
|
4
|
+
import type { IGitCommitter, IGitHookRunner } from '../contracts/IGitCommitter.js';
|
|
5
|
+
import type { IGitIndexLock } from '../contracts/IGitIndexLock.js';
|
|
6
|
+
import type { IGitReader } from '../contracts/IGitReader.js';
|
|
7
|
+
import type { IGitWriter } from '../contracts/IGitWriter.js';
|
|
8
|
+
import type { ILogger } from '../contracts/ILogger.js';
|
|
9
|
+
import type { CommitMemory, WaitReason } from '../contracts/syncTypes.js';
|
|
10
|
+
/** An unchanged tree a hook rejected is offered to the hooks again after this long (design 12.7). */
|
|
11
|
+
export declare const HOOK_RETRY_MS = 600000;
|
|
12
|
+
export interface CommitLocalChangesInput {
|
|
13
|
+
readonly cycleId: string;
|
|
14
|
+
readonly memory: CommitMemory;
|
|
15
|
+
/**
|
|
16
|
+
* The branch the cycle checked out and syncs (refs/heads/main). Under
|
|
17
|
+
* index.lock HEAD must still name it; the swap moves this ref, not HEAD,
|
|
18
|
+
* and a HEAD that then names neither it nor the commit rolls it back
|
|
19
|
+
* (design 14.3, Decision 121).
|
|
20
|
+
*/
|
|
21
|
+
readonly branchRef: string;
|
|
22
|
+
}
|
|
23
|
+
/** git bound to the commit index (GIT_INDEX_FILE=<stateDir>/commit-index). */
|
|
24
|
+
export interface CommitIndexGit {
|
|
25
|
+
/** For stageGuardedChanges: staged files and staged changes of the commit index. */
|
|
26
|
+
readonly reader: IGitReader;
|
|
27
|
+
/** Its stageAll and unstage, behind HaltGuardGitWriter. */
|
|
28
|
+
readonly writer: IGitWriter;
|
|
29
|
+
readonly committer: IGitCommitter;
|
|
30
|
+
/** <stateDir>/commit-index */
|
|
31
|
+
readonly path: string;
|
|
32
|
+
}
|
|
33
|
+
export interface CommitSettings {
|
|
34
|
+
readonly commitSubject: string;
|
|
35
|
+
readonly skipJsonCheck: readonly string[];
|
|
36
|
+
readonly stagedSelection: StagedSelectionMode;
|
|
37
|
+
readonly stagedSelectionMaxWaitMs: number | null;
|
|
38
|
+
readonly commitHooks: boolean;
|
|
39
|
+
/** <gitDir>/index, copied to seed the commit index with its stat data. */
|
|
40
|
+
readonly realIndex: string;
|
|
41
|
+
/** The working tree root; the stale-index repair reads a path's working-tree side (Decision 98). */
|
|
42
|
+
readonly root: string;
|
|
43
|
+
}
|
|
44
|
+
export interface CommitLocalChangesDeps {
|
|
45
|
+
/** The real index: HEAD, repository state, the person's staged selection, commit.gpgSign. */
|
|
46
|
+
readonly git: IGitReader;
|
|
47
|
+
/** Behind HaltGuardGitWriter; moves the synced branch by compare-and-swap. */
|
|
48
|
+
readonly writer: IGitWriter;
|
|
49
|
+
readonly commitIndex: CommitIndexGit;
|
|
50
|
+
/** Hooks on the real index: post-commit and post-index-change run after the branch moved. */
|
|
51
|
+
readonly realHooks: IGitHookRunner;
|
|
52
|
+
readonly indexLock: IGitIndexLock;
|
|
53
|
+
readonly fs: IFileSystem;
|
|
54
|
+
readonly logger: ILogger;
|
|
55
|
+
/** NullLogger from the root: the residue build must not log EXCLUDED a second time. */
|
|
56
|
+
readonly residueLogger: ILogger;
|
|
57
|
+
readonly clock: IClock;
|
|
58
|
+
readonly settings: CommitSettings;
|
|
59
|
+
}
|
|
60
|
+
export type CommitLocalChangesResult = {
|
|
61
|
+
readonly kind: 'nothing';
|
|
62
|
+
readonly memory: CommitMemory;
|
|
63
|
+
} | {
|
|
64
|
+
readonly kind: 'committed';
|
|
65
|
+
readonly memory: CommitMemory;
|
|
66
|
+
} | {
|
|
67
|
+
readonly kind: 'waiting';
|
|
68
|
+
readonly reason: WaitReason;
|
|
69
|
+
readonly memory: CommitMemory;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* index.lock held by another process, the real index changed since the
|
|
73
|
+
* build, a concurrent commit moved the branch first, or HEAD left the
|
|
74
|
+
* branch: nothing committed.
|
|
75
|
+
*/
|
|
76
|
+
| {
|
|
77
|
+
readonly kind: 'busy';
|
|
78
|
+
readonly memory: CommitMemory;
|
|
79
|
+
} | {
|
|
80
|
+
readonly kind: 'paused';
|
|
81
|
+
readonly reason: 'FOREIGN_OPERATION';
|
|
82
|
+
readonly memory: CommitMemory;
|
|
83
|
+
};
|
|
84
|
+
/** The step with its deps bound, the type of SyncCycleDeps.commit; the composition root builds it. */
|
|
85
|
+
export type CommitLocalChanges = (input: CommitLocalChangesInput) => Promise<CommitLocalChangesResult>;
|
|
86
|
+
/**
|
|
87
|
+
* Controller: enmetu's commit without touching the person's index (design
|
|
88
|
+
* 12.7, Decisions 72 to 74, 80, 81).
|
|
89
|
+
*
|
|
90
|
+
* 1. before = fs.identity(realIndex), read first, so the staged-selection
|
|
91
|
+
* verdict below still holds when the identity is unchanged under the lock.
|
|
92
|
+
* Staged selection: git.hasStagedChanges() on the real index. When it
|
|
93
|
+
* differs and memory.lastMove is set, repairStaleIndexController first
|
|
94
|
+
* (Decision 98): moved, repaired or reverted (Decision 108) forgets lastMove; repaired reads
|
|
95
|
+
* before and the verdict again, so a stale index is never taken for a
|
|
96
|
+
* person's selection. "wait"
|
|
97
|
+
* without a reached cap returns waiting PARTIAL_SELECTION_STAGED; a reached
|
|
98
|
+
* cap logs SELECTION_WAIT_ENDED and goes on; "commit" goes on.
|
|
99
|
+
* 2. Build: parent = git.headCommit(); fs.copy(realIndex, commit index);
|
|
100
|
+
* committer.seedFrom(parent); stageGuardedChanges on the commit index.
|
|
101
|
+
* Nothing staged returns nothing.
|
|
102
|
+
* 3. tree = committer.writeTree(); equal to memory.residueTree returns nothing;
|
|
103
|
+
* equal to memory.rejection.tree within HOOK_RETRY_MS returns waiting
|
|
104
|
+
* COMMIT_HOOK_REJECTED without running a hook.
|
|
105
|
+
* 4. commitHooks: pre-commit on the commit index (rejected, timed out or
|
|
106
|
+
* killed: HOOK_REJECTED, waiting, rejection remembered); then re-stage and
|
|
107
|
+
* write the tree again, so a formatter's rewrite is in this commit. The
|
|
108
|
+
* message is commitSubject plus a newline, with or without hooks; the
|
|
109
|
+
* message hooks do not run (Decision 80). The re-stage logs through
|
|
110
|
+
* residueLogger: its EXCLUDED lines repeat the first build's.
|
|
111
|
+
* 5. commit = committer.commitTree({ tree, parent, message, sign: gitBoolean(commit.gpgSign) }).
|
|
112
|
+
* 6. Critical section: indexLock.acquire(commit) (false: busy);
|
|
113
|
+
* 6.1 repositoryState() with an operation: release, paused; HEAD naming
|
|
114
|
+
* anything but input.branchRef: release, busy (Decision 121);
|
|
115
|
+
* 6.2 fs.identity(realIndex) differing from before: release, busy;
|
|
116
|
+
* 6.3 indexLock.install(commit index);
|
|
117
|
+
* 6.4 writer.updateRef(input.branchRef, commit, parent) (a mismatch:
|
|
118
|
+
* restore, release, COMMIT_DISCARDED, busy; any other throw: restore,
|
|
119
|
+
* release, rethrow);
|
|
120
|
+
* 6.4b HEAD naming input.branchRef, or else resolving to the commit, keeps
|
|
121
|
+
* it; otherwise writer.updateRef(input.branchRef, parent, commit),
|
|
122
|
+
* restore, COMMIT_DISCARDED, busy (a roll-back failing with GIT_FAILED
|
|
123
|
+
* restores and returns busy the same way; any other throw restores and
|
|
124
|
+
* rethrows); release.
|
|
125
|
+
* 7. COMMITTED; with hooks: when realHooks.hookExists says post-index-change
|
|
126
|
+
* or post-commit exists, a build with residueLogger before the hooks;
|
|
127
|
+
* post-index-change "0 0" and post-commit on the real index (git ignores
|
|
128
|
+
* both exit codes, so does enmetu); then, only when such a hook exists and
|
|
129
|
+
* the build before found nothing uncommitted, a fresh build whose tree,
|
|
130
|
+
* when it differs from the commit's, becomes residueTree (Decision 90).
|
|
131
|
+
*/
|
|
132
|
+
export declare function commitLocalChangesController(input: CommitLocalChangesInput, deps: CommitLocalChangesDeps): Promise<CommitLocalChangesResult>;
|