@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,71 @@
|
|
|
1
|
+
import type { CommitLocalChanges } from '../commit/commitLocalChangesController.js';
|
|
2
|
+
import type { IClock } from '../contracts/IClock.js';
|
|
3
|
+
import type { IGitReader } from '../contracts/IGitReader.js';
|
|
4
|
+
import type { IGitWriter } from '../contracts/IGitWriter.js';
|
|
5
|
+
import type { IHaltStore } from '../contracts/IHaltStore.js';
|
|
6
|
+
import type { ILogger } from '../contracts/ILogger.js';
|
|
7
|
+
import type { IPathQuarantine } from '../contracts/IPathQuarantine.js';
|
|
8
|
+
import type { SyncCycleInput, SyncCycleResult } from '../contracts/ISyncCycle.js';
|
|
9
|
+
import type { IntegrateUpstream } from './integrateUpstreamController.js';
|
|
10
|
+
/**
|
|
11
|
+
* Consecutive cycles whose rebase git refused to start before the cycle
|
|
12
|
+
* reports paused REBASE_REFUSED instead of busy (design 12.7, Decision 82).
|
|
13
|
+
*/
|
|
14
|
+
export declare const REBASE_REFUSED_AFTER = 3;
|
|
15
|
+
export type { SyncCycleInput, SyncCycleResult };
|
|
16
|
+
export interface SyncCycleDeps {
|
|
17
|
+
readonly git: IGitReader;
|
|
18
|
+
/** HaltGuardGitWriter in production. */
|
|
19
|
+
readonly writer: IGitWriter;
|
|
20
|
+
readonly halt: IHaltStore;
|
|
21
|
+
/** Only for step 4, a memento left pending by an earlier cycle; holding is integrate's. */
|
|
22
|
+
readonly quarantine: IPathQuarantine;
|
|
23
|
+
/** integrateUpstreamController with its deps bound (the same guarded writer, journal and quarantine). */
|
|
24
|
+
readonly integrate: IntegrateUpstream;
|
|
25
|
+
/** commitLocalChangesController with its deps bound; step 5 commits through it (design 12.7). */
|
|
26
|
+
readonly commit: CommitLocalChanges;
|
|
27
|
+
readonly logger: ILogger;
|
|
28
|
+
readonly clock: IClock;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Controller: one sync cycle, the same chain for every trigger (design 1.10).
|
|
32
|
+
* GIT_INDEX_LOCKED thrown by any step returns busy.
|
|
33
|
+
*
|
|
34
|
+
* 1. Halt gate: halt.read(); no git call precedes it (D3).
|
|
35
|
+
* 2. Repository gate: index.lock busy, foreign operation and detached HEAD pause.
|
|
36
|
+
* 3. Upstream of the checked-out branch, every cycle (D6): none pauses NO_UPSTREAM,
|
|
37
|
+
* a local one (remote '.') pauses UPSTREAM_NOT_REMOTE (Decision 28).
|
|
38
|
+
* 4. Pending quarantine memento: restore it; failed halts QUARANTINE_RESTORE_FAILED.
|
|
39
|
+
* 5. Commit local changes, skipped on a clean status (which also clears the
|
|
40
|
+
* memory's partialStagingSince): after the status read the repository
|
|
41
|
+
* state is read again (Decision 52), then deps.commit runs with the commit
|
|
42
|
+
* memory (design 12.7); busy and paused return, committed and nothing go
|
|
43
|
+
* on, waiting goes on and is the outcome unless a later step returns
|
|
44
|
+
* something else. The check right before the commit is the commit step's,
|
|
45
|
+
* under index.lock.
|
|
46
|
+
* 6. Remote check: ls-remote, tracking ref, verified ref (seeded from tracking when missing).
|
|
47
|
+
* 7. Verify and fetch: fetch into the incoming ref when the remote moved; the remote sha
|
|
48
|
+
* must descend from the verified sha, else halt UPSTREAM_REWRITTEN; then move the
|
|
49
|
+
* tracking and verified refs with compare-and-swap.
|
|
50
|
+
* 8. Divergence against the remote sha.
|
|
51
|
+
* 9. Integrate when behind (deps.integrate); conflict, busy, paused and halted are returned unchanged.
|
|
52
|
+
* When step 5 returned waiting PARTIAL_SELECTION_STAGED, or waiting and
|
|
53
|
+
* git.hasStagedChanges() says the real index differs from HEAD (git would
|
|
54
|
+
* refuse the rebase), integrate is skipped, UNPUSHED reported and waiting
|
|
55
|
+
* returned (Decision 84).
|
|
56
|
+
* not-started: the count of consecutive refusals (input.notStartedRebases)
|
|
57
|
+
* goes up by one; the first logs REBASE_NOT_STARTED with git's first stderr
|
|
58
|
+
* line; UNPUSHED is reported; the outcome is busy, or paused REBASE_REFUSED
|
|
59
|
+
* from REBASE_REFUSED_AFTER on (Decision 82). Any other integrate result, or
|
|
60
|
+
* a branch not behind, resets the count to 0; a cycle that returns before
|
|
61
|
+
* step 8 carries it unchanged.
|
|
62
|
+
* 10. Push when ahead: the branch's commit, resolved once, pushed by its sha;
|
|
63
|
+
* the verified ref moves to that sha (Decision 111). A branch gone since
|
|
64
|
+
* step 2 pauses BRANCH_CHANGED.
|
|
65
|
+
* 11. synced.
|
|
66
|
+
* UNPUSHED is logged with the count on every cycle that reaches the remote check and ends
|
|
67
|
+
* with local commits not on the upstream, halts aside (D2); the pauses and the busy index
|
|
68
|
+
* of steps 2, 3 and 5 return before it and are reported once (Decision 53).
|
|
69
|
+
* PAUSED, BUSY and OFFLINE are the runner's to log, once per change.
|
|
70
|
+
*/
|
|
71
|
+
export declare function syncCycleController(input: SyncCycleInput, deps: SyncCycleDeps): Promise<SyncCycleResult>;
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { EnmetuError } from '../contracts/errors.js';
|
|
2
|
+
import { EMPTY_COMMIT_MEMORY, } from '../contracts/syncTypes.js';
|
|
3
|
+
import { withLastMove } from '../core/commit/commitMemory.js';
|
|
4
|
+
import { withCycleId } from '../core/errors/withCycleId.js';
|
|
5
|
+
import { incomingRefFor, verifiedRefFor } from '../core/git/enmetuRefs.js';
|
|
6
|
+
import { logRestoreWarnings } from '../core/quarantine/restoreWarnings.js';
|
|
7
|
+
import { recordHalt } from '../halt/recordHalt.js';
|
|
8
|
+
/** The remote name git gives an upstream that is a branch of this repository. */
|
|
9
|
+
const LOCAL_REMOTE = '.';
|
|
10
|
+
const SHORT_SHA_LENGTH = 7;
|
|
11
|
+
/**
|
|
12
|
+
* Consecutive cycles whose rebase git refused to start before the cycle
|
|
13
|
+
* reports paused REBASE_REFUSED instead of busy (design 12.7, Decision 82).
|
|
14
|
+
*/
|
|
15
|
+
export const REBASE_REFUSED_AFTER = 3;
|
|
16
|
+
/**
|
|
17
|
+
* Controller: one sync cycle, the same chain for every trigger (design 1.10).
|
|
18
|
+
* GIT_INDEX_LOCKED thrown by any step returns busy.
|
|
19
|
+
*
|
|
20
|
+
* 1. Halt gate: halt.read(); no git call precedes it (D3).
|
|
21
|
+
* 2. Repository gate: index.lock busy, foreign operation and detached HEAD pause.
|
|
22
|
+
* 3. Upstream of the checked-out branch, every cycle (D6): none pauses NO_UPSTREAM,
|
|
23
|
+
* a local one (remote '.') pauses UPSTREAM_NOT_REMOTE (Decision 28).
|
|
24
|
+
* 4. Pending quarantine memento: restore it; failed halts QUARANTINE_RESTORE_FAILED.
|
|
25
|
+
* 5. Commit local changes, skipped on a clean status (which also clears the
|
|
26
|
+
* memory's partialStagingSince): after the status read the repository
|
|
27
|
+
* state is read again (Decision 52), then deps.commit runs with the commit
|
|
28
|
+
* memory (design 12.7); busy and paused return, committed and nothing go
|
|
29
|
+
* on, waiting goes on and is the outcome unless a later step returns
|
|
30
|
+
* something else. The check right before the commit is the commit step's,
|
|
31
|
+
* under index.lock.
|
|
32
|
+
* 6. Remote check: ls-remote, tracking ref, verified ref (seeded from tracking when missing).
|
|
33
|
+
* 7. Verify and fetch: fetch into the incoming ref when the remote moved; the remote sha
|
|
34
|
+
* must descend from the verified sha, else halt UPSTREAM_REWRITTEN; then move the
|
|
35
|
+
* tracking and verified refs with compare-and-swap.
|
|
36
|
+
* 8. Divergence against the remote sha.
|
|
37
|
+
* 9. Integrate when behind (deps.integrate); conflict, busy, paused and halted are returned unchanged.
|
|
38
|
+
* When step 5 returned waiting PARTIAL_SELECTION_STAGED, or waiting and
|
|
39
|
+
* git.hasStagedChanges() says the real index differs from HEAD (git would
|
|
40
|
+
* refuse the rebase), integrate is skipped, UNPUSHED reported and waiting
|
|
41
|
+
* returned (Decision 84).
|
|
42
|
+
* not-started: the count of consecutive refusals (input.notStartedRebases)
|
|
43
|
+
* goes up by one; the first logs REBASE_NOT_STARTED with git's first stderr
|
|
44
|
+
* line; UNPUSHED is reported; the outcome is busy, or paused REBASE_REFUSED
|
|
45
|
+
* from REBASE_REFUSED_AFTER on (Decision 82). Any other integrate result, or
|
|
46
|
+
* a branch not behind, resets the count to 0; a cycle that returns before
|
|
47
|
+
* step 8 carries it unchanged.
|
|
48
|
+
* 10. Push when ahead: the branch's commit, resolved once, pushed by its sha;
|
|
49
|
+
* the verified ref moves to that sha (Decision 111). A branch gone since
|
|
50
|
+
* step 2 pauses BRANCH_CHANGED.
|
|
51
|
+
* 11. synced.
|
|
52
|
+
* UNPUSHED is logged with the count on every cycle that reaches the remote check and ends
|
|
53
|
+
* with local commits not on the upstream, halts aside (D2); the pauses and the busy index
|
|
54
|
+
* of steps 2, 3 and 5 return before it and are reported once (Decision 53).
|
|
55
|
+
* PAUSED, BUSY and OFFLINE are the runner's to log, once per change.
|
|
56
|
+
*/
|
|
57
|
+
export async function syncCycleController(input, deps) {
|
|
58
|
+
const carried = {
|
|
59
|
+
commitMemory: input.commitMemory,
|
|
60
|
+
notStartedRebases: input.notStartedRebases,
|
|
61
|
+
};
|
|
62
|
+
const outcome = (cycleOutcome) => resultOf(cycleOutcome, null, carried);
|
|
63
|
+
try {
|
|
64
|
+
return await runCycle(input, deps, carried, outcome);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof EnmetuError && error.code === 'GIT_INDEX_LOCKED') {
|
|
68
|
+
return outcome({ kind: 'busy' });
|
|
69
|
+
}
|
|
70
|
+
throw withCycleId(error, input.cycleId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function runCycle(input, deps, carried, outcome) {
|
|
74
|
+
const { cycleId } = input;
|
|
75
|
+
// --- Step 1: halt gate, before any git call (D3) ---
|
|
76
|
+
const haltRecord = await deps.halt.read();
|
|
77
|
+
if (haltRecord !== null) {
|
|
78
|
+
return outcome({ kind: 'halted', code: haltRecord.code });
|
|
79
|
+
}
|
|
80
|
+
// --- Step 2: repository gate ---
|
|
81
|
+
const state = await deps.git.repositoryState();
|
|
82
|
+
if (state.indexLocked) {
|
|
83
|
+
return outcome({ kind: 'busy' });
|
|
84
|
+
}
|
|
85
|
+
if (state.operation !== null) {
|
|
86
|
+
// A person's rebase, merge, cherry-pick, revert or bisect is never touched.
|
|
87
|
+
return outcome({ kind: 'paused', reason: 'FOREIGN_OPERATION' });
|
|
88
|
+
}
|
|
89
|
+
if (state.branchRef === null) {
|
|
90
|
+
return outcome({ kind: 'paused', reason: 'DETACHED_HEAD' });
|
|
91
|
+
}
|
|
92
|
+
const branchRef = state.branchRef;
|
|
93
|
+
// --- Step 3: the upstream of whatever branch is checked out now (D6) ---
|
|
94
|
+
const upstream = await deps.git.upstream(branchRef);
|
|
95
|
+
if (upstream === null) {
|
|
96
|
+
return outcome({ kind: 'paused', reason: 'NO_UPSTREAM' });
|
|
97
|
+
}
|
|
98
|
+
if (upstream.remote === LOCAL_REMOTE) {
|
|
99
|
+
// A branch switched to mid-run whose upstream is another local branch: a
|
|
100
|
+
// push would fast-forward that branch, which nobody asked enmetu to touch.
|
|
101
|
+
return outcome({ kind: 'paused', reason: 'UPSTREAM_NOT_REMOTE' });
|
|
102
|
+
}
|
|
103
|
+
// --- Step 4: a memento an earlier cycle left pending is restored first ---
|
|
104
|
+
if (await deps.quarantine.hasPending()) {
|
|
105
|
+
const restore = await deps.quarantine.restore();
|
|
106
|
+
if (restore.kind === 'failed') {
|
|
107
|
+
return outcome(await haltWith('QUARANTINE_RESTORE_FAILED', `restoring a pending quarantine memento failed (${restore.detail}); the memento is kept`, cycleId, deps));
|
|
108
|
+
}
|
|
109
|
+
if (restore.kind === 'restored') {
|
|
110
|
+
logRestoreWarnings(restore, cycleId, deps.logger);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// --- Step 5: commit local changes; a clean status writes nothing to the index ---
|
|
114
|
+
let waiting = null;
|
|
115
|
+
// The commit's parent when this cycle committed: step 9 records the pull
|
|
116
|
+
// from there, so the repair covers the commit and the pull in one move.
|
|
117
|
+
let committedFrom = null;
|
|
118
|
+
if ((await deps.git.status()).length > 0) {
|
|
119
|
+
// A person's merge, cherry-pick or revert that stopped after step 2 is in
|
|
120
|
+
// that status; the commit step would only find it under index.lock (Decision 52).
|
|
121
|
+
if ((await deps.git.repositoryState()).operation !== null) {
|
|
122
|
+
return outcome({ kind: 'paused', reason: 'FOREIGN_OPERATION' });
|
|
123
|
+
}
|
|
124
|
+
const committed = await deps.commit({
|
|
125
|
+
cycleId,
|
|
126
|
+
memory: carried.commitMemory ?? EMPTY_COMMIT_MEMORY,
|
|
127
|
+
branchRef,
|
|
128
|
+
});
|
|
129
|
+
carried.commitMemory = committed.memory;
|
|
130
|
+
if (committed.kind === 'busy') {
|
|
131
|
+
return outcome({ kind: 'busy' });
|
|
132
|
+
}
|
|
133
|
+
if (committed.kind === 'paused') {
|
|
134
|
+
return outcome({ kind: 'paused', reason: committed.reason });
|
|
135
|
+
}
|
|
136
|
+
if (committed.kind === 'waiting') {
|
|
137
|
+
waiting = committed.reason;
|
|
138
|
+
}
|
|
139
|
+
if (committed.kind === 'committed') {
|
|
140
|
+
committedFrom = committed.memory.lastMove?.from ?? null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (carried.commitMemory !== undefined) {
|
|
144
|
+
// A clean tree has no staged selection, so a wait for one is over.
|
|
145
|
+
carried.commitMemory = {
|
|
146
|
+
partialStagingSince: null,
|
|
147
|
+
rejection: carried.commitMemory.rejection,
|
|
148
|
+
residueTree: carried.commitMemory.residueTree,
|
|
149
|
+
lastMove: carried.commitMemory.lastMove,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// --- Step 6: remote check ---
|
|
153
|
+
const verifiedRef = verifiedRefFor(branchRef);
|
|
154
|
+
const remoteHead = await deps.git.remoteHead(upstream.remote, upstream.remoteRef);
|
|
155
|
+
const trackingSha = await deps.git.resolveCommit(upstream.trackingRef);
|
|
156
|
+
let verifiedSha = await deps.git.resolveCommit(verifiedRef);
|
|
157
|
+
if (remoteHead.kind === 'unreachable') {
|
|
158
|
+
await reportUnpushedAgainstLastKnown(trackingSha ?? verifiedSha, upstream, cycleId, deps);
|
|
159
|
+
return outcome({ kind: 'offline' });
|
|
160
|
+
}
|
|
161
|
+
if (remoteHead.kind === 'absent') {
|
|
162
|
+
// A deleted remote branch is never recreated by enmetu, so commits pile
|
|
163
|
+
// up locally while it is gone; they are reported every cycle (D2).
|
|
164
|
+
await reportUnpushedAgainstLastKnown(trackingSha ?? verifiedSha, upstream, cycleId, deps);
|
|
165
|
+
return outcome({ kind: 'paused', reason: 'UPSTREAM_GONE' });
|
|
166
|
+
}
|
|
167
|
+
const remoteSha = remoteHead.sha;
|
|
168
|
+
if (verifiedSha === null && trackingSha !== null) {
|
|
169
|
+
// First start: the tracking ref is the best reference point there is.
|
|
170
|
+
await deps.writer.updateRef(verifiedRef, trackingSha, null);
|
|
171
|
+
verifiedSha = trackingSha;
|
|
172
|
+
}
|
|
173
|
+
// --- Step 7: verify and fetch; refs move only after the ancestry check ---
|
|
174
|
+
if (remoteSha !== trackingSha) {
|
|
175
|
+
const fetched = await deps.writer.fetch(upstream.remote, upstream.remoteRef, incomingRefFor(branchRef));
|
|
176
|
+
if (fetched.kind === 'failed') {
|
|
177
|
+
await reportUnpushedAgainstLastKnown(trackingSha ?? verifiedSha, upstream, cycleId, deps);
|
|
178
|
+
return outcome({ kind: 'offline' });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Checked even when the fetch was skipped: a person's own fetch may already
|
|
182
|
+
// have moved the tracking ref to a rewritten upstream.
|
|
183
|
+
if (verifiedSha !== null && verifiedSha !== remoteSha) {
|
|
184
|
+
if (!(await deps.git.isAncestor(verifiedSha, remoteSha))) {
|
|
185
|
+
return outcome(await haltWith('UPSTREAM_REWRITTEN', `${upstream.remote} ${upstream.remoteRef} now points at ${remoteSha}, which does not descend from ${verifiedSha}, the upstream enmetu last verified; the upstream was rewritten`, cycleId, deps));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (trackingSha !== remoteSha) {
|
|
189
|
+
await deps.writer.updateRef(upstream.trackingRef, remoteSha, trackingSha);
|
|
190
|
+
}
|
|
191
|
+
if (verifiedSha !== remoteSha) {
|
|
192
|
+
await deps.writer.updateRef(verifiedRef, remoteSha, verifiedSha);
|
|
193
|
+
}
|
|
194
|
+
// --- Step 8: divergence against the verified remote sha ---
|
|
195
|
+
const divergence = await deps.git.divergence(remoteSha);
|
|
196
|
+
const behind = divergence.behind;
|
|
197
|
+
let ahead = divergence.ahead;
|
|
198
|
+
// --- Step 9: integrate when behind ---
|
|
199
|
+
if (behind === 0) {
|
|
200
|
+
carried.notStartedRebases = resetCount(carried.notStartedRebases);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// git rebase refuses an index that differs from HEAD (Decision 84). A
|
|
204
|
+
// selection step 5 waited for holds integrate back even when the person
|
|
205
|
+
// committed it since: step 5 committed nothing, so the person's other
|
|
206
|
+
// edits are still uncommitted, and the quarantine would hold them while a
|
|
207
|
+
// rebase runs under the person's hands; the next cycle commits first.
|
|
208
|
+
if (waiting === 'PARTIAL_SELECTION_STAGED' ||
|
|
209
|
+
(waiting !== null && (await deps.git.hasStagedChanges()))) {
|
|
210
|
+
reportUnpushed(ahead, upstream, cycleId, deps);
|
|
211
|
+
return outcome({ kind: 'waiting', reason: waiting });
|
|
212
|
+
}
|
|
213
|
+
const pulledFrom = committedFrom ?? (await deps.git.headCommit());
|
|
214
|
+
const integrated = await deps.integrate({
|
|
215
|
+
cycleId,
|
|
216
|
+
branchRef,
|
|
217
|
+
ontoSha: remoteSha,
|
|
218
|
+
behind,
|
|
219
|
+
knownConflict: input.knownConflict,
|
|
220
|
+
});
|
|
221
|
+
if (integrated.kind === 'not-started') {
|
|
222
|
+
const refusals = (carried.notStartedRebases ?? 0) + 1;
|
|
223
|
+
carried.notStartedRebases = refusals;
|
|
224
|
+
if (refusals === 1) {
|
|
225
|
+
deps.logger.log({
|
|
226
|
+
level: 'WARN',
|
|
227
|
+
code: 'REBASE_NOT_STARTED',
|
|
228
|
+
message: `git did not start the rebase onto ${shortSha(remoteSha)} (${firstLine(integrated.detail)}); enmetu retries on the next cycle`,
|
|
229
|
+
cycleId,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
reportUnpushed(ahead, upstream, cycleId, deps);
|
|
233
|
+
return outcome(refusals >= REBASE_REFUSED_AFTER
|
|
234
|
+
? { kind: 'paused', reason: 'REBASE_REFUSED' }
|
|
235
|
+
: { kind: 'busy' });
|
|
236
|
+
}
|
|
237
|
+
carried.notStartedRebases = resetCount(carried.notStartedRebases);
|
|
238
|
+
if (integrated.kind === 'conflict') {
|
|
239
|
+
reportUnpushed(ahead, upstream, cycleId, deps);
|
|
240
|
+
return resultOf({ kind: 'conflict' }, integrated.conflict, carried);
|
|
241
|
+
}
|
|
242
|
+
if (integrated.kind === 'halted') {
|
|
243
|
+
// recordHalt already logged HALTED with the reason.
|
|
244
|
+
return outcome(integrated);
|
|
245
|
+
}
|
|
246
|
+
if (integrated.kind !== 'rebased') {
|
|
247
|
+
// Behind and not rebased, so nothing is pushed this cycle (D2).
|
|
248
|
+
reportUnpushed(ahead, upstream, cycleId, deps);
|
|
249
|
+
return outcome(integrated);
|
|
250
|
+
}
|
|
251
|
+
// A git command of the person's that read the index before the rebase
|
|
252
|
+
// and writes it afterwards puts the pre-pull entries back (Decision 98).
|
|
253
|
+
const pulledTo = await deps.git.headCommit();
|
|
254
|
+
if (pulledFrom !== null && pulledTo !== null && pulledTo !== pulledFrom) {
|
|
255
|
+
carried.commitMemory = withLastMove(carried.commitMemory ?? EMPTY_COMMIT_MEMORY, { from: pulledFrom, to: pulledTo });
|
|
256
|
+
}
|
|
257
|
+
ahead = (await deps.git.divergence(remoteSha)).ahead;
|
|
258
|
+
}
|
|
259
|
+
// --- Step 10: push when ahead ---
|
|
260
|
+
if (ahead > 0) {
|
|
261
|
+
// The branch's commit, read once and pushed by its sha: HEAD may be
|
|
262
|
+
// detached at a person's half-done replay by now, and the verified ref
|
|
263
|
+
// must name what the remote received (Decision 111).
|
|
264
|
+
const pushedSha = await deps.git.resolveCommit(branchRef);
|
|
265
|
+
if (pushedSha === null) {
|
|
266
|
+
// The branch step 2 resolved is gone; the next cycle reads it again.
|
|
267
|
+
reportUnpushed(ahead, upstream, cycleId, deps);
|
|
268
|
+
return outcome({ kind: 'paused', reason: 'BRANCH_CHANGED' });
|
|
269
|
+
}
|
|
270
|
+
const pushed = await deps.writer.push(upstream.remote, pushedSha, upstream.remoteRef);
|
|
271
|
+
if (pushed.kind !== 'pushed') {
|
|
272
|
+
deps.logger.log({
|
|
273
|
+
level: 'WARN',
|
|
274
|
+
code: pushed.kind === 'rejected' ? 'PUSH_REJECTED' : 'PUSH_FAILED',
|
|
275
|
+
message: pushed.detail,
|
|
276
|
+
cycleId,
|
|
277
|
+
});
|
|
278
|
+
reportUnpushed(ahead, upstream, cycleId, deps);
|
|
279
|
+
return outcome({ kind: 'push-failed' });
|
|
280
|
+
}
|
|
281
|
+
deps.logger.log({
|
|
282
|
+
level: 'INFO',
|
|
283
|
+
code: 'PUSHED',
|
|
284
|
+
message: ahead === 1 ? '1 commit' : `${ahead} commits`,
|
|
285
|
+
cycleId,
|
|
286
|
+
});
|
|
287
|
+
await deps.writer.updateRef(verifiedRef, pushedSha, remoteSha);
|
|
288
|
+
}
|
|
289
|
+
// --- Step 11: synced, or still waiting for what step 5 could not commit ---
|
|
290
|
+
return outcome(waiting === null ? { kind: 'synced' } : { kind: 'waiting', reason: waiting });
|
|
291
|
+
}
|
|
292
|
+
/** The result with what the cycle carries; every outcome but a conflict has no conflict key. */
|
|
293
|
+
function resultOf(cycleOutcome, conflict, carried) {
|
|
294
|
+
return {
|
|
295
|
+
outcome: cycleOutcome,
|
|
296
|
+
conflict,
|
|
297
|
+
commitMemory: carried.commitMemory,
|
|
298
|
+
notStartedRebases: carried.notStartedRebases,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/** A count the input carried goes back to 0; an absent one stays absent, which means 0. */
|
|
302
|
+
function resetCount(count) {
|
|
303
|
+
return count === undefined ? undefined : 0;
|
|
304
|
+
}
|
|
305
|
+
function shortSha(sha) {
|
|
306
|
+
return sha.slice(0, SHORT_SHA_LENGTH);
|
|
307
|
+
}
|
|
308
|
+
function firstLine(text) {
|
|
309
|
+
return (text.trim().split('\n')[0] ?? '').trim();
|
|
310
|
+
}
|
|
311
|
+
async function haltWith(code, message, cycleId, deps) {
|
|
312
|
+
await recordHalt({ code, message, cycleId }, deps);
|
|
313
|
+
return { kind: 'halted', code };
|
|
314
|
+
}
|
|
315
|
+
function reportUnpushed(ahead, upstream, cycleId, deps) {
|
|
316
|
+
if (ahead === 0) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
deps.logger.log({
|
|
320
|
+
level: 'WARN',
|
|
321
|
+
code: 'UNPUSHED',
|
|
322
|
+
message: `${ahead === 1 ? '1 local commit' : `${ahead} local commits`} not on ${upstream.remote} ${upstream.remoteRef}`,
|
|
323
|
+
cycleId,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Remote not readable or gone: counted against the last known upstream, the
|
|
328
|
+
* tracking ref or else enmetu's verified ref, uncounted only when neither exists.
|
|
329
|
+
* The tracking ref comes first because a person's fetch keeps it the newer
|
|
330
|
+
* observation of the remote; the verified ref covers a tracking ref that
|
|
331
|
+
* `git fetch --prune` removed after the remote branch was deleted.
|
|
332
|
+
*/
|
|
333
|
+
async function reportUnpushedAgainstLastKnown(lastKnownSha, upstream, cycleId, deps) {
|
|
334
|
+
if (lastKnownSha === null) {
|
|
335
|
+
deps.logger.log({
|
|
336
|
+
level: 'WARN',
|
|
337
|
+
code: 'UNPUSHED',
|
|
338
|
+
message: `local commits may not be on ${upstream.remote} ${upstream.remoteRef} (count unknown: no upstream was ever fetched or verified)`,
|
|
339
|
+
cycleId,
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const { ahead } = await deps.git.divergence(lastKnownSha);
|
|
344
|
+
reportUnpushed(ahead, upstream, cycleId, deps);
|
|
345
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ILogger } from '../contracts/ILogger.js';
|
|
2
|
+
import type { ISyncCycle } from '../contracts/ISyncCycle.js';
|
|
3
|
+
import type { ISyncCycleRunner } from '../contracts/ISyncCycleRunner.js';
|
|
4
|
+
import { type CycleOutcomeKind } from '../contracts/syncTypes.js';
|
|
5
|
+
/**
|
|
6
|
+
* Service: the memory that spans cycles. Numbers cycles c1, c2, ...; passes
|
|
7
|
+
* the previous ConflictKey in and replaces it with the result's, and does the
|
|
8
|
+
* same for the commit memory and the count of refused rebases (design 12.7);
|
|
9
|
+
* logs WAITING, PAUSED, BUSY and OFFLINE when they begin or change and RESUMED
|
|
10
|
+
* when they end; turns a thrown error into CYCLE_FAILED and outcome 'failed'.
|
|
11
|
+
*/
|
|
12
|
+
export declare class SyncCycleRunner implements ISyncCycleRunner {
|
|
13
|
+
private readonly cycle;
|
|
14
|
+
private readonly logger;
|
|
15
|
+
private cycleCount;
|
|
16
|
+
private knownConflict;
|
|
17
|
+
private quiet;
|
|
18
|
+
/** null until a cycle carried a commit memory or a refusal count; absent means empty and 0 (design 12.7). */
|
|
19
|
+
private carried;
|
|
20
|
+
constructor(cycle: ISyncCycle, logger: ILogger);
|
|
21
|
+
run(): Promise<CycleOutcomeKind>;
|
|
22
|
+
private inputFor;
|
|
23
|
+
private reportQuietTransition;
|
|
24
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { EMPTY_COMMIT_MEMORY, } from '../contracts/syncTypes.js';
|
|
2
|
+
import { errorMessage } from '../core/errors/errorMessage.js';
|
|
3
|
+
const WAITING_MESSAGES = {
|
|
4
|
+
PARTIAL_SELECTION_STAGED: 'waiting: a partial selection is staged; nothing is committed or pulled until you commit or unstage it, and after partialStagingWaitMs enmetu commits everything',
|
|
5
|
+
COMMIT_HOOK_REJECTED: 'waiting: a commit hook rejected the commit; enmetu tries again when the files change',
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Service: the memory that spans cycles. Numbers cycles c1, c2, ...; passes
|
|
9
|
+
* the previous ConflictKey in and replaces it with the result's, and does the
|
|
10
|
+
* same for the commit memory and the count of refused rebases (design 12.7);
|
|
11
|
+
* logs WAITING, PAUSED, BUSY and OFFLINE when they begin or change and RESUMED
|
|
12
|
+
* when they end; turns a thrown error into CYCLE_FAILED and outcome 'failed'.
|
|
13
|
+
*/
|
|
14
|
+
export class SyncCycleRunner {
|
|
15
|
+
cycle;
|
|
16
|
+
logger;
|
|
17
|
+
cycleCount = 0;
|
|
18
|
+
knownConflict = null;
|
|
19
|
+
quiet = null;
|
|
20
|
+
/** null until a cycle carried a commit memory or a refusal count; absent means empty and 0 (design 12.7). */
|
|
21
|
+
carried = null;
|
|
22
|
+
constructor(cycle, logger) {
|
|
23
|
+
this.cycle = cycle;
|
|
24
|
+
this.logger = logger;
|
|
25
|
+
}
|
|
26
|
+
async run() {
|
|
27
|
+
this.cycleCount += 1;
|
|
28
|
+
const cycleId = `c${this.cycleCount}`;
|
|
29
|
+
try {
|
|
30
|
+
const result = await this.cycle.run(this.inputFor(cycleId));
|
|
31
|
+
// A busy cycle carries no key, so the next cycle re-runs the rebase
|
|
32
|
+
// that the lock interrupted (Decision 11).
|
|
33
|
+
this.knownConflict = result.conflict;
|
|
34
|
+
if (this.carried !== null ||
|
|
35
|
+
result.commitMemory !== undefined ||
|
|
36
|
+
result.notStartedRebases !== undefined) {
|
|
37
|
+
this.carried = {
|
|
38
|
+
commitMemory: result.commitMemory ?? EMPTY_COMMIT_MEMORY,
|
|
39
|
+
notStartedRebases: result.notStartedRebases ?? 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
this.reportQuietTransition(result.outcome, cycleId);
|
|
43
|
+
return result.outcome.kind;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// No result, so neither the conflict key, the commit memory, the
|
|
47
|
+
// refusal count nor the quiet state is replaced: a throw says nothing
|
|
48
|
+
// about whether any of them has ended.
|
|
49
|
+
this.logger.log({
|
|
50
|
+
level: 'ERROR',
|
|
51
|
+
code: 'CYCLE_FAILED',
|
|
52
|
+
message: describeError(error),
|
|
53
|
+
cycleId,
|
|
54
|
+
});
|
|
55
|
+
return 'failed';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
inputFor(cycleId) {
|
|
59
|
+
if (this.carried === null) {
|
|
60
|
+
return { cycleId, knownConflict: this.knownConflict };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
cycleId,
|
|
64
|
+
knownConflict: this.knownConflict,
|
|
65
|
+
commitMemory: this.carried.commitMemory,
|
|
66
|
+
notStartedRebases: this.carried.notStartedRebases,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
reportQuietTransition(outcome, cycleId) {
|
|
70
|
+
const previous = this.quiet;
|
|
71
|
+
const current = asQuiet(outcome);
|
|
72
|
+
if (current !== null) {
|
|
73
|
+
this.quiet = current;
|
|
74
|
+
if (previous === null || !sameQuiet(previous, current)) {
|
|
75
|
+
this.logger.log(quietEntry(current, cycleId));
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// A halt is reported by the controller and ends the run; calling it a
|
|
80
|
+
// resumption would contradict the HALTED line that precedes it.
|
|
81
|
+
if (outcome.kind === 'halted') {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.quiet = null;
|
|
85
|
+
if (previous !== null) {
|
|
86
|
+
this.logger.log({
|
|
87
|
+
level: 'INFO',
|
|
88
|
+
code: 'RESUMED',
|
|
89
|
+
message: `sync resumed after ${describeQuiet(previous)}`,
|
|
90
|
+
cycleId,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function asQuiet(outcome) {
|
|
96
|
+
switch (outcome.kind) {
|
|
97
|
+
case 'paused':
|
|
98
|
+
case 'busy':
|
|
99
|
+
case 'offline':
|
|
100
|
+
case 'waiting':
|
|
101
|
+
return outcome;
|
|
102
|
+
default:
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function sameQuiet(left, right) {
|
|
107
|
+
if (left.kind === 'paused' && right.kind === 'paused') {
|
|
108
|
+
return left.reason === right.reason;
|
|
109
|
+
}
|
|
110
|
+
if (left.kind === 'waiting' && right.kind === 'waiting') {
|
|
111
|
+
return left.reason === right.reason;
|
|
112
|
+
}
|
|
113
|
+
return left.kind === right.kind;
|
|
114
|
+
}
|
|
115
|
+
function quietEntry(outcome, cycleId) {
|
|
116
|
+
switch (outcome.kind) {
|
|
117
|
+
case 'paused':
|
|
118
|
+
return {
|
|
119
|
+
level: 'WARN',
|
|
120
|
+
code: 'PAUSED',
|
|
121
|
+
message: `sync paused: ${outcome.reason}`,
|
|
122
|
+
cycleId,
|
|
123
|
+
};
|
|
124
|
+
case 'busy':
|
|
125
|
+
return {
|
|
126
|
+
level: 'WARN',
|
|
127
|
+
code: 'BUSY',
|
|
128
|
+
message: 'git is busy in this repository; retrying on the next cycle',
|
|
129
|
+
cycleId,
|
|
130
|
+
};
|
|
131
|
+
case 'offline':
|
|
132
|
+
return {
|
|
133
|
+
level: 'WARN',
|
|
134
|
+
code: 'OFFLINE',
|
|
135
|
+
message: 'remote unreachable; retrying on every poll',
|
|
136
|
+
cycleId,
|
|
137
|
+
};
|
|
138
|
+
case 'waiting':
|
|
139
|
+
return {
|
|
140
|
+
level: 'WARN',
|
|
141
|
+
code: 'WAITING',
|
|
142
|
+
message: WAITING_MESSAGES[outcome.reason],
|
|
143
|
+
cycleId,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function describeQuiet(outcome) {
|
|
148
|
+
switch (outcome.kind) {
|
|
149
|
+
case 'paused':
|
|
150
|
+
return `pause ${outcome.reason}`;
|
|
151
|
+
case 'busy':
|
|
152
|
+
return 'busy git';
|
|
153
|
+
case 'offline':
|
|
154
|
+
return 'offline';
|
|
155
|
+
case 'waiting':
|
|
156
|
+
return `waiting ${outcome.reason}`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** The message, prefixed with the structured code when the error carries one (EnmetuError, Node errno errors). */
|
|
160
|
+
function describeError(error) {
|
|
161
|
+
const message = errorMessage(error);
|
|
162
|
+
if (typeof error === 'object' &&
|
|
163
|
+
error !== null &&
|
|
164
|
+
'code' in error &&
|
|
165
|
+
typeof error.code === 'string') {
|
|
166
|
+
return `${error.code}: ${message}`;
|
|
167
|
+
}
|
|
168
|
+
return message;
|
|
169
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { IClock } from '../contracts/IClock.js';
|
|
2
|
+
import type { ISyncCycleRunner } from '../contracts/ISyncCycleRunner.js';
|
|
3
|
+
import type { ISyncScheduler } from '../contracts/ISyncScheduler.js';
|
|
4
|
+
import { type SchedulerTiming } from '../core/schedule/schedulerStep.js';
|
|
5
|
+
type EndReason = 'stopped' | 'halted';
|
|
6
|
+
/**
|
|
7
|
+
* Service, the imperative shell around schedulerStep: it holds the state,
|
|
8
|
+
* feeds events in, and executes the effects with IClock and the runner. It
|
|
9
|
+
* contains no timing rule of its own.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SyncScheduler implements ISyncScheduler {
|
|
12
|
+
private readonly runner;
|
|
13
|
+
private readonly clock;
|
|
14
|
+
private readonly timing;
|
|
15
|
+
private state;
|
|
16
|
+
private quietTimer;
|
|
17
|
+
private pollTimer;
|
|
18
|
+
private running;
|
|
19
|
+
private endReason;
|
|
20
|
+
private resolveEnd;
|
|
21
|
+
constructor(runner: ISyncCycleRunner, clock: IClock, timing: SchedulerTiming);
|
|
22
|
+
run(): Promise<EndReason>;
|
|
23
|
+
notifyChange(): void;
|
|
24
|
+
stop(): void;
|
|
25
|
+
private dispatch;
|
|
26
|
+
private execute;
|
|
27
|
+
private startCycle;
|
|
28
|
+
private finishCycle;
|
|
29
|
+
/** A deadline already passed (a late timer callback) fires at once. */
|
|
30
|
+
private delayUntil;
|
|
31
|
+
}
|
|
32
|
+
export {};
|