@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,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enmetu's own refs. They live outside refs/heads, refs/remotes and refs/tags,
|
|
3
|
+
* so no fetch refspec, push or person's command moves them, and a push that
|
|
4
|
+
* names branchRef never sends them.
|
|
5
|
+
*/
|
|
6
|
+
const BRANCH_PREFIX = 'refs/heads/';
|
|
7
|
+
/** The last upstream sha enmetu verified to be a fast-forward or pushed itself: refs/enmetu/verified/<branch>. */
|
|
8
|
+
export function verifiedRefFor(branchRef) {
|
|
9
|
+
return `refs/enmetu/verified/${branchName(branchRef)}`;
|
|
10
|
+
}
|
|
11
|
+
/** Where enmetu fetches before verifying: refs/enmetu/incoming/<branch>. */
|
|
12
|
+
export function incomingRefFor(branchRef) {
|
|
13
|
+
return `refs/enmetu/incoming/${branchName(branchRef)}`;
|
|
14
|
+
}
|
|
15
|
+
// Callers pass the branchRef of `git symbolic-ref HEAD`, always under
|
|
16
|
+
// refs/heads/. Anything else keeps its full name, so two inputs never map
|
|
17
|
+
// to the same enmetu ref.
|
|
18
|
+
function branchName(branchRef) {
|
|
19
|
+
return branchRef.startsWith(BRANCH_PREFIX)
|
|
20
|
+
? branchRef.slice(BRANCH_PREFIX.length)
|
|
21
|
+
: branchRef;
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Divergence, StatusEntry, Upstream } from '../../contracts/gitTypes.js';
|
|
2
|
+
import type { GitVersion } from '../../contracts/IRepositoryLocator.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parsers and guards for every git output enmetu reads. Malformed output
|
|
5
|
+
* throws GitOutputError GIT_OUTPUT_UNPARSEABLE; an answer meaning "absent"
|
|
6
|
+
* (no upstream, ref not listed, unknown version) returns null.
|
|
7
|
+
*/
|
|
8
|
+
export interface RawDiffEntry {
|
|
9
|
+
/** New mode, e.g. 100644, 100755, 120000, 160000. */
|
|
10
|
+
readonly mode: string;
|
|
11
|
+
/** New blob id. */
|
|
12
|
+
readonly blobId: string;
|
|
13
|
+
readonly path: string;
|
|
14
|
+
}
|
|
15
|
+
export type BatchCheckEntry = {
|
|
16
|
+
readonly kind: 'found';
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly size: number;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'missing';
|
|
21
|
+
readonly id: string;
|
|
22
|
+
};
|
|
23
|
+
export interface BatchBlob {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly content: Buffer;
|
|
26
|
+
}
|
|
27
|
+
export interface RevParsePaths {
|
|
28
|
+
readonly root: string;
|
|
29
|
+
readonly gitDir: string;
|
|
30
|
+
readonly hooksDir: string;
|
|
31
|
+
}
|
|
32
|
+
/** `git status --porcelain=v1 -z` records; a rename or copy record consumes its source path field. */
|
|
33
|
+
export declare function parseStatus(stdout: Buffer): readonly StatusEntry[];
|
|
34
|
+
/** `git diff --cached --raw -z --no-abbrev` records. */
|
|
35
|
+
export declare function parseRawDiff(stdout: Buffer): readonly RawDiffEntry[];
|
|
36
|
+
/** The sha of exactly `ref` in `git ls-remote` output, null when the ref is not listed. */
|
|
37
|
+
export declare function parseLsRemote(stdout: string, ref: string): string | null;
|
|
38
|
+
/** `git rev-list --left-right --count HEAD...<sha>` output. */
|
|
39
|
+
export declare function parseDivergence(stdout: string): Divergence;
|
|
40
|
+
/** `git version` output, null when the format is unknown. */
|
|
41
|
+
export declare function parseGitVersion(stdout: string): GitVersion | null;
|
|
42
|
+
/** `git cat-file --batch-check` output, one entry per requested id. */
|
|
43
|
+
export declare function parseBatchCheck(stdout: string): readonly BatchCheckEntry[];
|
|
44
|
+
/** `git cat-file --batch` output split by each header's declared size. */
|
|
45
|
+
export declare function parseBatch(stdout: Buffer): readonly BatchBlob[];
|
|
46
|
+
/** for-each-ref `%(upstream:remotename)%00%(upstream:remoteref)%00%(upstream)` output; null without an upstream. */
|
|
47
|
+
export declare function parseUpstream(stdout: string, branchRef: string): Upstream | null;
|
|
48
|
+
/** The three absolute paths of rev-parse; null unless exactly three absolute lines (a path with a newline). */
|
|
49
|
+
export declare function parseRevParsePaths(stdout: string): RevParsePaths | null;
|
|
50
|
+
/** true when `git push --porcelain` output carries a ref line starting with `!`. */
|
|
51
|
+
export declare function hasRejectedRef(stdout: string): boolean;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { GitOutputError } from '../../contracts/errors.js';
|
|
2
|
+
const STATUS_CODES = [
|
|
3
|
+
' ',
|
|
4
|
+
'M',
|
|
5
|
+
'T',
|
|
6
|
+
'A',
|
|
7
|
+
'D',
|
|
8
|
+
'R',
|
|
9
|
+
'C',
|
|
10
|
+
'U',
|
|
11
|
+
'?',
|
|
12
|
+
'!',
|
|
13
|
+
];
|
|
14
|
+
// SHA-1 ids are 40 hex digits, SHA-256 ids 64.
|
|
15
|
+
const OBJECT_ID = '[0-9a-f]{40}(?:[0-9a-f]{24})?';
|
|
16
|
+
const RAW_DIFF_HEADER = new RegExp(`^:[0-7]{6} ([0-7]{6}) ${OBJECT_ID} (${OBJECT_ID}) ([A-Z])[0-9]*$`);
|
|
17
|
+
const LS_REMOTE_LINE = new RegExp(`^(${OBJECT_ID})\t(.+)$`);
|
|
18
|
+
const BATCH_CHECK_FOUND = new RegExp(`^(${OBJECT_ID}) blob ([0-9]+)$`);
|
|
19
|
+
const BATCH_CHECK_MISSING = new RegExp(`^(${OBJECT_ID}) missing$`);
|
|
20
|
+
const BATCH_HEADER = BATCH_CHECK_FOUND;
|
|
21
|
+
const DIVERGENCE_LINE = /^([0-9]+)\t([0-9]+)$/;
|
|
22
|
+
const GIT_VERSION = /^git version ([0-9]+)\.([0-9]+)\.([0-9]+)/;
|
|
23
|
+
const NEWLINE = 0x0a;
|
|
24
|
+
/** `git status --porcelain=v1 -z` records; a rename or copy record consumes its source path field. */
|
|
25
|
+
export function parseStatus(stdout) {
|
|
26
|
+
const records = nulTerminatedFields(stdout, 'git status');
|
|
27
|
+
const entries = [];
|
|
28
|
+
for (let i = 0; i < records.length; i += 1) {
|
|
29
|
+
const record = records[i] ?? '';
|
|
30
|
+
if (record.length < 4 || record[2] !== ' ') {
|
|
31
|
+
throw unparseable('git status', record);
|
|
32
|
+
}
|
|
33
|
+
const index = statusCode(record[0], record);
|
|
34
|
+
const worktree = statusCode(record[1], record);
|
|
35
|
+
entries.push({ index, worktree, path: record.slice(3) });
|
|
36
|
+
if (isRenameOrCopy(index) || isRenameOrCopy(worktree)) {
|
|
37
|
+
const source = records[i + 1];
|
|
38
|
+
if (source === undefined || source === '') {
|
|
39
|
+
throw unparseable('git status', record);
|
|
40
|
+
}
|
|
41
|
+
i += 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return entries;
|
|
45
|
+
}
|
|
46
|
+
/** `git diff --cached --raw -z --no-abbrev` records. */
|
|
47
|
+
export function parseRawDiff(stdout) {
|
|
48
|
+
const fields = nulTerminatedFields(stdout, 'git diff --raw');
|
|
49
|
+
const entries = [];
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < fields.length) {
|
|
52
|
+
const header = fields[i] ?? '';
|
|
53
|
+
const match = RAW_DIFF_HEADER.exec(header);
|
|
54
|
+
const mode = match?.[1];
|
|
55
|
+
const blobId = match?.[2];
|
|
56
|
+
const status = match?.[3];
|
|
57
|
+
if (mode === undefined || blobId === undefined || status === undefined) {
|
|
58
|
+
throw unparseable('git diff --raw', header);
|
|
59
|
+
}
|
|
60
|
+
// A rename or copy carries its source path before the destination.
|
|
61
|
+
const pathCount = isRenameOrCopy(status) ? 2 : 1;
|
|
62
|
+
const path = fields[i + pathCount];
|
|
63
|
+
if (path === undefined || path === '') {
|
|
64
|
+
throw unparseable('git diff --raw', header);
|
|
65
|
+
}
|
|
66
|
+
entries.push({ mode, blobId, path });
|
|
67
|
+
i += 1 + pathCount;
|
|
68
|
+
}
|
|
69
|
+
return entries;
|
|
70
|
+
}
|
|
71
|
+
/** The sha of exactly `ref` in `git ls-remote` output, null when the ref is not listed. */
|
|
72
|
+
export function parseLsRemote(stdout, ref) {
|
|
73
|
+
for (const line of nonEmptyLines(stdout)) {
|
|
74
|
+
const match = LS_REMOTE_LINE.exec(line);
|
|
75
|
+
const sha = match?.[1];
|
|
76
|
+
const listedRef = match?.[2];
|
|
77
|
+
if (sha === undefined || listedRef === undefined) {
|
|
78
|
+
throw unparseable('git ls-remote', line);
|
|
79
|
+
}
|
|
80
|
+
if (listedRef === ref) {
|
|
81
|
+
return sha;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/** `git rev-list --left-right --count HEAD...<sha>` output. */
|
|
87
|
+
export function parseDivergence(stdout) {
|
|
88
|
+
const line = stdout.trimEnd();
|
|
89
|
+
const match = DIVERGENCE_LINE.exec(line);
|
|
90
|
+
const ahead = match?.[1];
|
|
91
|
+
const behind = match?.[2];
|
|
92
|
+
if (ahead === undefined || behind === undefined) {
|
|
93
|
+
throw unparseable('git rev-list --count', line);
|
|
94
|
+
}
|
|
95
|
+
return { ahead: Number(ahead), behind: Number(behind) };
|
|
96
|
+
}
|
|
97
|
+
/** `git version` output, null when the format is unknown. */
|
|
98
|
+
export function parseGitVersion(stdout) {
|
|
99
|
+
const match = GIT_VERSION.exec(stdout);
|
|
100
|
+
const major = match?.[1];
|
|
101
|
+
const minor = match?.[2];
|
|
102
|
+
const patch = match?.[3];
|
|
103
|
+
if (major === undefined || minor === undefined || patch === undefined) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return { major: Number(major), minor: Number(minor), patch: Number(patch) };
|
|
107
|
+
}
|
|
108
|
+
/** `git cat-file --batch-check` output, one entry per requested id. */
|
|
109
|
+
export function parseBatchCheck(stdout) {
|
|
110
|
+
return nonEmptyLines(stdout).map((line) => {
|
|
111
|
+
const found = BATCH_CHECK_FOUND.exec(line);
|
|
112
|
+
const foundId = found?.[1];
|
|
113
|
+
const size = found?.[2];
|
|
114
|
+
if (foundId !== undefined && size !== undefined) {
|
|
115
|
+
return { kind: 'found', id: foundId, size: Number(size) };
|
|
116
|
+
}
|
|
117
|
+
const missingId = BATCH_CHECK_MISSING.exec(line)?.[1];
|
|
118
|
+
if (missingId !== undefined) {
|
|
119
|
+
return { kind: 'missing', id: missingId };
|
|
120
|
+
}
|
|
121
|
+
throw unparseable('git cat-file --batch-check', line);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/** `git cat-file --batch` output split by each header's declared size. */
|
|
125
|
+
export function parseBatch(stdout) {
|
|
126
|
+
const blobs = [];
|
|
127
|
+
let offset = 0;
|
|
128
|
+
while (offset < stdout.length) {
|
|
129
|
+
const headerEnd = stdout.indexOf(NEWLINE, offset);
|
|
130
|
+
if (headerEnd === -1) {
|
|
131
|
+
throw unparseable('git cat-file --batch', stdout.toString('utf8', offset));
|
|
132
|
+
}
|
|
133
|
+
const header = stdout.toString('utf8', offset, headerEnd);
|
|
134
|
+
const match = BATCH_HEADER.exec(header);
|
|
135
|
+
const id = match?.[1];
|
|
136
|
+
const size = match?.[2];
|
|
137
|
+
if (id === undefined || size === undefined) {
|
|
138
|
+
throw unparseable('git cat-file --batch', header);
|
|
139
|
+
}
|
|
140
|
+
const contentStart = headerEnd + 1;
|
|
141
|
+
const contentEnd = contentStart + Number(size);
|
|
142
|
+
if (contentEnd >= stdout.length || stdout[contentEnd] !== NEWLINE) {
|
|
143
|
+
throw unparseable('git cat-file --batch', header);
|
|
144
|
+
}
|
|
145
|
+
// A view, not a copy: the chunk is alive while its files are consumed anyway.
|
|
146
|
+
blobs.push({ id, content: stdout.subarray(contentStart, contentEnd) });
|
|
147
|
+
offset = contentEnd + 1;
|
|
148
|
+
}
|
|
149
|
+
return blobs;
|
|
150
|
+
}
|
|
151
|
+
/** for-each-ref `%(upstream:remotename)%00%(upstream:remoteref)%00%(upstream)` output; null without an upstream. */
|
|
152
|
+
export function parseUpstream(stdout, branchRef) {
|
|
153
|
+
const line = stripOneTrailingNewline(stdout);
|
|
154
|
+
// No output: the branch ref does not exist (an unborn HEAD).
|
|
155
|
+
if (line === '') {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const fields = line.split('\0');
|
|
159
|
+
if (fields.length !== 3) {
|
|
160
|
+
throw unparseable('git for-each-ref', line);
|
|
161
|
+
}
|
|
162
|
+
const [remote, remoteRef, trackingRef] = fields;
|
|
163
|
+
// An empty field is an upstream git cannot resolve to a remote-tracking
|
|
164
|
+
// ref; the cycle treats it like no upstream at all.
|
|
165
|
+
if (!remote || !remoteRef || !trackingRef) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return { branchRef, remote, remoteRef, trackingRef };
|
|
169
|
+
}
|
|
170
|
+
/** The three absolute paths of rev-parse; null unless exactly three absolute lines (a path with a newline). */
|
|
171
|
+
export function parseRevParsePaths(stdout) {
|
|
172
|
+
const lines = stripOneTrailingNewline(stdout).split('\n');
|
|
173
|
+
if (lines.length !== 3 || !lines.every((line) => line.startsWith('/'))) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const [root, gitDir, hooksDir] = lines;
|
|
177
|
+
if (root === undefined || gitDir === undefined || hooksDir === undefined) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return { root, gitDir, hooksDir };
|
|
181
|
+
}
|
|
182
|
+
/** true when `git push --porcelain` output carries a ref line starting with `!`. */
|
|
183
|
+
export function hasRejectedRef(stdout) {
|
|
184
|
+
return stdout.split('\n').some((line) => line.startsWith('!'));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Splits `-z` output into its fields. Every field is NUL-terminated, so
|
|
188
|
+
* output that does not end in NUL was cut off.
|
|
189
|
+
*/
|
|
190
|
+
function nulTerminatedFields(stdout, command) {
|
|
191
|
+
const text = stdout.toString('utf8');
|
|
192
|
+
if (text === '') {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
if (!text.endsWith('\0')) {
|
|
196
|
+
throw unparseable(command, text);
|
|
197
|
+
}
|
|
198
|
+
return text.slice(0, -1).split('\0');
|
|
199
|
+
}
|
|
200
|
+
function nonEmptyLines(stdout) {
|
|
201
|
+
return stdout.split('\n').filter((line) => line !== '');
|
|
202
|
+
}
|
|
203
|
+
function stripOneTrailingNewline(stdout) {
|
|
204
|
+
return stdout.endsWith('\n') ? stdout.slice(0, -1) : stdout;
|
|
205
|
+
}
|
|
206
|
+
function statusCode(char, record) {
|
|
207
|
+
const code = STATUS_CODES.find((candidate) => candidate === char);
|
|
208
|
+
if (code === undefined) {
|
|
209
|
+
throw unparseable('git status', record);
|
|
210
|
+
}
|
|
211
|
+
return code;
|
|
212
|
+
}
|
|
213
|
+
function isRenameOrCopy(code) {
|
|
214
|
+
return code === 'R' || code === 'C';
|
|
215
|
+
}
|
|
216
|
+
const EXCERPT_LENGTH = 120;
|
|
217
|
+
function unparseable(command, excerpt) {
|
|
218
|
+
return new GitOutputError(`unexpected ${command} output: ${JSON.stringify(excerpt.slice(0, EXCERPT_LENGTH))}`);
|
|
219
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RebaseStart } from '../../contracts/gitTypes.js';
|
|
2
|
+
import type { RebaseJournal } from '../../contracts/IOperationJournal.js';
|
|
3
|
+
/**
|
|
4
|
+
* Who started the rebase in progress (design 13.2, Decision 104):
|
|
5
|
+
* - enmetu: HEAD's newest start entry carries this attempt's token;
|
|
6
|
+
* - other: every other start entry (a person's command, an earlier attempt of
|
|
7
|
+
* enmetu's) or none at all while HEAD has a reflog;
|
|
8
|
+
* - unknown: HEAD has no reflog, so only orig-head and onto can decide.
|
|
9
|
+
*/
|
|
10
|
+
export type RebaseOwner = 'enmetu' | 'other' | 'unknown';
|
|
11
|
+
/**
|
|
12
|
+
* The attempt's token, GIT_REFLOG_ACTION for its `git rebase`. Built from the
|
|
13
|
+
* journal entry begun for the attempt, so startup recovery rebuilds the same
|
|
14
|
+
* token from the journal it reads. Cycle ids restart with every process; the
|
|
15
|
+
* entry's time keeps two processes' tokens apart (one instance per repository
|
|
16
|
+
* at a time, and a restart takes longer than a millisecond).
|
|
17
|
+
*/
|
|
18
|
+
export declare function rebaseReflogAction(journal: Pick<RebaseJournal, 'cycleId' | 'at'>): string;
|
|
19
|
+
/**
|
|
20
|
+
* enmetu only when the newest start entry's action equals
|
|
21
|
+
* rebaseReflogAction(journal); git writes that entry when the rebase checks
|
|
22
|
+
* out onto, before any pick can stop it, so a stopped rebase of this attempt
|
|
23
|
+
* always has it (probe W of design 13.10). Positive on purpose: a person's
|
|
24
|
+
* rebase held between creating its directory and its checkout has not
|
|
25
|
+
* written its entry yet, so "the newest entry is someone else's" would miss
|
|
26
|
+
* it, while "the newest entry is not this attempt's" does not.
|
|
27
|
+
*/
|
|
28
|
+
export declare function rebaseOwner(start: RebaseStart, journal: RebaseJournal): RebaseOwner;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attempt's token, GIT_REFLOG_ACTION for its `git rebase`. Built from the
|
|
3
|
+
* journal entry begun for the attempt, so startup recovery rebuilds the same
|
|
4
|
+
* token from the journal it reads. Cycle ids restart with every process; the
|
|
5
|
+
* entry's time keeps two processes' tokens apart (one instance per repository
|
|
6
|
+
* at a time, and a restart takes longer than a millisecond).
|
|
7
|
+
*/
|
|
8
|
+
export function rebaseReflogAction(journal) {
|
|
9
|
+
return `enmetu ${journal.cycleId} ${journal.at}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* enmetu only when the newest start entry's action equals
|
|
13
|
+
* rebaseReflogAction(journal); git writes that entry when the rebase checks
|
|
14
|
+
* out onto, before any pick can stop it, so a stopped rebase of this attempt
|
|
15
|
+
* always has it (probe W of design 13.10). Positive on purpose: a person's
|
|
16
|
+
* rebase held between creating its directory and its checkout has not
|
|
17
|
+
* written its entry yet, so "the newest entry is someone else's" would miss
|
|
18
|
+
* it, while "the newest entry is not this attempt's" does not.
|
|
19
|
+
*/
|
|
20
|
+
export function rebaseOwner(start, journal) {
|
|
21
|
+
switch (start.kind) {
|
|
22
|
+
case 'unlogged':
|
|
23
|
+
return 'unknown';
|
|
24
|
+
case 'none':
|
|
25
|
+
return 'other';
|
|
26
|
+
case 'entry':
|
|
27
|
+
return start.action === rebaseReflogAction(journal) ? 'enmetu' : 'other';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StatusEntry } from '../../contracts/gitTypes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Paths written while a rebase was stopped, which `git rebase --abort` would
|
|
4
|
+
* reset to orig-head (Decision 45): a worktree modification, deletion or type
|
|
5
|
+
* change on an entry that is not unmerged. Paths the rebase itself staged show
|
|
6
|
+
* a clean worktree column. Unmerged entries (either column U, or AA and DD)
|
|
7
|
+
* carry git's conflict content, which the abort is meant to discard; a write
|
|
8
|
+
* into one cannot be told apart from it, the named residual.
|
|
9
|
+
*/
|
|
10
|
+
export declare function writesDuringStoppedRebase(entries: readonly StatusEntry[]): readonly string[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths written while a rebase was stopped, which `git rebase --abort` would
|
|
3
|
+
* reset to orig-head (Decision 45): a worktree modification, deletion or type
|
|
4
|
+
* change on an entry that is not unmerged. Paths the rebase itself staged show
|
|
5
|
+
* a clean worktree column. Unmerged entries (either column U, or AA and DD)
|
|
6
|
+
* carry git's conflict content, which the abort is meant to discard; a write
|
|
7
|
+
* into one cannot be told apart from it, the named residual.
|
|
8
|
+
*/
|
|
9
|
+
export function writesDuringStoppedRebase(entries) {
|
|
10
|
+
return entries
|
|
11
|
+
.filter((entry) => isWorktreeWrite(entry) && !isUnmerged(entry))
|
|
12
|
+
.map((entry) => entry.path);
|
|
13
|
+
}
|
|
14
|
+
function isWorktreeWrite(entry) {
|
|
15
|
+
return (entry.worktree === 'M' || entry.worktree === 'D' || entry.worktree === 'T');
|
|
16
|
+
}
|
|
17
|
+
function isUnmerged(entry) {
|
|
18
|
+
return (entry.index === 'U' ||
|
|
19
|
+
entry.worktree === 'U' ||
|
|
20
|
+
(entry.index === 'A' && entry.worktree === 'A') ||
|
|
21
|
+
(entry.index === 'D' && entry.worktree === 'D'));
|
|
22
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HaltCode } from '../../contracts/IHaltStore.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a person does to clear a halt; the daemon's HALTED lines and status print
|
|
4
|
+
* it. Null: the code is not known (the marker was cleared before it was read).
|
|
5
|
+
*/
|
|
6
|
+
export declare function resumeHint(code: HaltCode | null): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a person does to clear a halt; the daemon's HALTED lines and status print
|
|
3
|
+
* it. Null: the code is not known (the marker was cleared before it was read).
|
|
4
|
+
*/
|
|
5
|
+
export function resumeHint(code) {
|
|
6
|
+
return code === 'UPSTREAM_REWRITTEN'
|
|
7
|
+
? 'Reconcile the branch with the rewritten upstream, then run: enmetu resume --accept-upstream'
|
|
8
|
+
: 'Reconcile the branch, then run: enmetu resume';
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** `<ISO-8601 UTC with ms> <LEVEL padded to 5> <cycleId or -> <CODE> <message>`, newlines in the message replaced by ` | `. */
|
|
2
|
+
export function formatLogLine(entry, epochMs) {
|
|
3
|
+
const timestamp = new Date(epochMs).toISOString();
|
|
4
|
+
const level = entry.level.padEnd(5);
|
|
5
|
+
const cycleId = (entry.cycleId ?? '-').padEnd(3);
|
|
6
|
+
// git stderr ends in a newline and spans several lines; a log entry is one line.
|
|
7
|
+
const message = entry.message
|
|
8
|
+
.replace(/(?:\r?\n)+$/, '')
|
|
9
|
+
.replace(/\r?\n/g, ' | ');
|
|
10
|
+
return `${timestamp} ${level} ${cycleId} ${entry.code} ${message}`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { KeptBlob } from '../../contracts/IPathQuarantine.js';
|
|
2
|
+
/** The QUARANTINE_KEPT message, the same wherever a restore keeps a blob (1.13). */
|
|
3
|
+
export declare function keptBlobMessage(kept: KeptBlob): string;
|
|
4
|
+
/** The QUARANTINE_KEPT message for a write the restore replaced after its last check (Decision 117). */
|
|
5
|
+
export declare function overwrittenBlobMessage(kept: KeptBlob): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The QUARANTINE_KEPT message, the same wherever a restore keeps a blob (1.13). */
|
|
2
|
+
export function keptBlobMessage(kept) {
|
|
3
|
+
return `${kept.path} changed while it was held; the held version is blob ${kept.blobId} (git cat-file -p ${kept.blobId}), kept until git gc prunes it`;
|
|
4
|
+
}
|
|
5
|
+
/** The QUARANTINE_KEPT message for a write the restore replaced after its last check (Decision 117). */
|
|
6
|
+
export function overwrittenBlobMessage(kept) {
|
|
7
|
+
return `${kept.path} was written while enmetu put its held version back, after enmetu's last check of the file; that write is blob ${kept.blobId} (git cat-file -p ${kept.blobId}), kept until git gc prunes it, and the file holds the held version`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ILogger } from '../../contracts/ILogger.js';
|
|
2
|
+
import type { KeptBlob } from '../../contracts/IPathQuarantine.js';
|
|
3
|
+
/** What a restore reports that a person should see; the fields of RestoreResult's restored. */
|
|
4
|
+
export interface RestoreWarnings {
|
|
5
|
+
readonly kept: readonly KeptBlob[];
|
|
6
|
+
readonly changedUnderHold: readonly string[];
|
|
7
|
+
readonly overwritten: readonly KeptBlob[];
|
|
8
|
+
}
|
|
9
|
+
/** The QUARANTINE_UPSTREAM_CHANGED message (Decision 99). */
|
|
10
|
+
export declare function changedUnderHoldMessage(path: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* One WARN per kept blob and per path restored over a change, the same at
|
|
13
|
+
* every restore: recovery, a pending memento, and integrate (written three
|
|
14
|
+
* times before it was extracted).
|
|
15
|
+
*/
|
|
16
|
+
export declare function logRestoreWarnings(warnings: RestoreWarnings, cycleId: string, logger: ILogger): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { keptBlobMessage, overwrittenBlobMessage } from './keptBlobMessage.js';
|
|
2
|
+
/** The QUARANTINE_UPSTREAM_CHANGED message (Decision 99). */
|
|
3
|
+
export function changedUnderHoldMessage(path) {
|
|
4
|
+
return `${path} changed in HEAD while it was held (a pulled change) and the held edits do not merge cleanly onto it; the held version is back in the working tree, so committing it undoes that change (compare with git diff HEAD -- ${path})`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* One WARN per kept blob and per path restored over a change, the same at
|
|
8
|
+
* every restore: recovery, a pending memento, and integrate (written three
|
|
9
|
+
* times before it was extracted).
|
|
10
|
+
*/
|
|
11
|
+
export function logRestoreWarnings(warnings, cycleId, logger) {
|
|
12
|
+
for (const kept of warnings.kept) {
|
|
13
|
+
logger.log({
|
|
14
|
+
level: 'WARN',
|
|
15
|
+
code: 'QUARANTINE_KEPT',
|
|
16
|
+
message: keptBlobMessage(kept),
|
|
17
|
+
cycleId,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
for (const overwritten of warnings.overwritten) {
|
|
21
|
+
logger.log({
|
|
22
|
+
level: 'WARN',
|
|
23
|
+
code: 'QUARANTINE_KEPT',
|
|
24
|
+
message: overwrittenBlobMessage(overwritten),
|
|
25
|
+
cycleId,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
for (const path of warnings.changedUnderHold) {
|
|
29
|
+
logger.log({
|
|
30
|
+
level: 'WARN',
|
|
31
|
+
code: 'QUARANTINE_UPSTREAM_CHANGED',
|
|
32
|
+
message: changedUnderHoldMessage(path),
|
|
33
|
+
cycleId,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CycleOutcomeKind } from '../../contracts/syncTypes.js';
|
|
2
|
+
export type SchedulerState = {
|
|
3
|
+
readonly kind: 'idle';
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: 'waiting';
|
|
6
|
+
readonly firstChangeAt: number;
|
|
7
|
+
readonly lastChangeAt: number;
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: 'syncing';
|
|
10
|
+
readonly changedDuringSync: boolean;
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'stopping';
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'stopped';
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'halted';
|
|
17
|
+
};
|
|
18
|
+
export type SchedulerEvent = {
|
|
19
|
+
readonly type: 'started';
|
|
20
|
+
} | {
|
|
21
|
+
readonly type: 'changed';
|
|
22
|
+
readonly at: number;
|
|
23
|
+
} | {
|
|
24
|
+
readonly type: 'quietElapsed';
|
|
25
|
+
} | {
|
|
26
|
+
readonly type: 'pollDue';
|
|
27
|
+
readonly at: number;
|
|
28
|
+
} | {
|
|
29
|
+
readonly type: 'cycleFinished';
|
|
30
|
+
readonly at: number;
|
|
31
|
+
readonly outcome: CycleOutcomeKind;
|
|
32
|
+
} | {
|
|
33
|
+
readonly type: 'stopRequested';
|
|
34
|
+
};
|
|
35
|
+
export type SchedulerEffect = {
|
|
36
|
+
readonly type: 'runCycle';
|
|
37
|
+
} | {
|
|
38
|
+
readonly type: 'armQuietTimer';
|
|
39
|
+
readonly fireAt: number;
|
|
40
|
+
} | {
|
|
41
|
+
readonly type: 'armPollTimer';
|
|
42
|
+
readonly fireAt: number;
|
|
43
|
+
} | {
|
|
44
|
+
readonly type: 'cancelTimers';
|
|
45
|
+
} | {
|
|
46
|
+
readonly type: 'end';
|
|
47
|
+
readonly reason: 'stopped' | 'halted';
|
|
48
|
+
};
|
|
49
|
+
export interface SchedulerTiming {
|
|
50
|
+
readonly quietMs: number;
|
|
51
|
+
readonly maxDelayMs: number;
|
|
52
|
+
readonly pollMs: number;
|
|
53
|
+
}
|
|
54
|
+
export interface SchedulerStepResult {
|
|
55
|
+
readonly state: SchedulerState;
|
|
56
|
+
readonly effects: readonly SchedulerEffect[];
|
|
57
|
+
}
|
|
58
|
+
export declare const INITIAL_SCHEDULER_STATE: SchedulerState;
|
|
59
|
+
/**
|
|
60
|
+
* The transitions of design 1.15, one function per state. Every event a
|
|
61
|
+
* state does not draw returns that state unchanged with no effects.
|
|
62
|
+
*/
|
|
63
|
+
export declare function schedulerStep(state: SchedulerState, event: SchedulerEvent, timing: SchedulerTiming): SchedulerStepResult;
|