@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,8 @@
|
|
|
1
|
+
import type { GitRunnerFactory } from '../../contracts/IGitRunner.js';
|
|
2
|
+
import type { IRepositoryLocator, RepositoryLocation } from '../../contracts/IRepositoryLocator.js';
|
|
3
|
+
/** Facade: resolves the repository from a start directory through git, before the root is known (D9). */
|
|
4
|
+
export declare class GitLocatorFacade implements IRepositoryLocator {
|
|
5
|
+
private readonly createRunner;
|
|
6
|
+
constructor(createRunner: GitRunnerFactory);
|
|
7
|
+
locate(startDir: string): Promise<RepositoryLocation>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { GitCommandError, GitOutputError, PreflightError, } from '../../contracts/errors.js';
|
|
3
|
+
import { parseGitVersion, parseRevParsePaths, } from '../../core/git/porcelain.js';
|
|
4
|
+
const NOT_A_REPOSITORY_EXIT = 128;
|
|
5
|
+
/** Facade: resolves the repository from a start directory through git, before the root is known (D9). */
|
|
6
|
+
export class GitLocatorFacade {
|
|
7
|
+
createRunner;
|
|
8
|
+
constructor(createRunner) {
|
|
9
|
+
this.createRunner = createRunner;
|
|
10
|
+
}
|
|
11
|
+
async locate(startDir) {
|
|
12
|
+
const runner = this.createRunner(startDir);
|
|
13
|
+
const versionText = (await runOk(runner, ['version'])).stdout.toString('utf8');
|
|
14
|
+
const version = parseGitVersion(versionText);
|
|
15
|
+
if (version === null) {
|
|
16
|
+
throw new GitOutputError(`unexpected git version output: ${JSON.stringify(versionText.trim().slice(0, 120))}`);
|
|
17
|
+
}
|
|
18
|
+
const kindArgs = [
|
|
19
|
+
'rev-parse',
|
|
20
|
+
'--is-bare-repository',
|
|
21
|
+
'--is-inside-work-tree',
|
|
22
|
+
];
|
|
23
|
+
const kind = await runner.run(kindArgs);
|
|
24
|
+
if (kind.exitCode === NOT_A_REPOSITORY_EXIT) {
|
|
25
|
+
return { kind: 'not-a-repository' };
|
|
26
|
+
}
|
|
27
|
+
if (kind.exitCode !== 0) {
|
|
28
|
+
throw gitFailed(kindArgs, kind);
|
|
29
|
+
}
|
|
30
|
+
const [bare, insideWorkTree] = kind.stdout.toString('utf8').split('\n');
|
|
31
|
+
if (bare === 'true') {
|
|
32
|
+
return { kind: 'bare' };
|
|
33
|
+
}
|
|
34
|
+
// Inside the .git directory of a repository with a work tree: there is
|
|
35
|
+
// no tree to keep committed from here.
|
|
36
|
+
if (insideWorkTree !== 'true') {
|
|
37
|
+
return { kind: 'not-a-repository' };
|
|
38
|
+
}
|
|
39
|
+
// Not --git-path hooks: it honours core.hooksPath, and git refuses an
|
|
40
|
+
// empty one as a path (exit 128) although it runs hooks with it; the
|
|
41
|
+
// installer writes only to the repository's own hooks directory, and only
|
|
42
|
+
// while core.hooksPath is unset (design 14.17).
|
|
43
|
+
const paths = parseRevParsePaths((await runOk(runner, [
|
|
44
|
+
'rev-parse',
|
|
45
|
+
'--path-format=absolute',
|
|
46
|
+
'--show-toplevel',
|
|
47
|
+
'--git-dir',
|
|
48
|
+
'--git-common-dir',
|
|
49
|
+
])).stdout.toString('utf8'));
|
|
50
|
+
if (paths === null) {
|
|
51
|
+
throw new PreflightError('a repository path contains a newline, which enmetu does not support', 'PATH_UNSUPPORTED');
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
kind: 'found',
|
|
55
|
+
paths: {
|
|
56
|
+
root: paths.root,
|
|
57
|
+
gitDir: paths.gitDir,
|
|
58
|
+
hooksDir: join(paths.commonDir, 'hooks'),
|
|
59
|
+
stateDir: join(paths.gitDir, 'enmetu'),
|
|
60
|
+
},
|
|
61
|
+
version,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function runOk(runner, args) {
|
|
66
|
+
const result = await runner.run(args);
|
|
67
|
+
if (result.exitCode !== 0) {
|
|
68
|
+
throw gitFailed(args, result);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
function gitFailed(args, result) {
|
|
73
|
+
const firstLine = result.stderr.split('\n')[0] ?? '';
|
|
74
|
+
return new GitCommandError(`git ${args.join(' ')} exited ${result.exitCode}: ${firstLine}`, 'GIT_FAILED', args, result.exitCode, result.stderr);
|
|
75
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { GitRunOptions, GitRunResult, IGitRunner } from '../../contracts/IGitRunner.js';
|
|
2
|
+
/** 128 MiB: a cat-file batch chunk (64 MiB of content) plus headers fits with room. */
|
|
3
|
+
export declare const GIT_MAX_BUFFER_BYTES = 134217728;
|
|
4
|
+
export interface GitProcessAdapterOptions {
|
|
5
|
+
readonly cwd: string;
|
|
6
|
+
/** The environment git children start from (Runtime.env); the adapter reads no global environment. */
|
|
7
|
+
readonly baseEnv: Readonly<Record<string, string | undefined>>;
|
|
8
|
+
readonly timeoutMs: number;
|
|
9
|
+
readonly gitBinary: string;
|
|
10
|
+
/**
|
|
11
|
+
* The commit index (design 12.7): every child gets GIT_INDEX_FILE=<indexFile>
|
|
12
|
+
* and the one-invocation options -c core.splitIndex=false -c
|
|
13
|
+
* core.fsmonitor=false before the facade's arguments, so the commit index is
|
|
14
|
+
* one self-contained file and no fsmonitor hook runs for it.
|
|
15
|
+
*/
|
|
16
|
+
readonly indexFile?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Adapter: the only file that spawns git (execFile with an argument array and
|
|
20
|
+
* encoding 'buffer'). Adds GIT_TERMINAL_PROMPT=0, GIT_OPTIONAL_LOCKS=0 and
|
|
21
|
+
* LC_ALL=C to the base environment of every child.
|
|
22
|
+
*/
|
|
23
|
+
export declare class GitProcessAdapter implements IGitRunner {
|
|
24
|
+
private readonly options;
|
|
25
|
+
private readonly env;
|
|
26
|
+
constructor(options: GitProcessAdapterOptions);
|
|
27
|
+
/**
|
|
28
|
+
* options.timeoutMs replaces the adapter's timeout for this call;
|
|
29
|
+
* options.outputCapBytes keeps the first bytes of stdout and of stderr and
|
|
30
|
+
* drops the rest without failing (design 12.7, Decision 83), where without it
|
|
31
|
+
* output beyond GIT_MAX_BUFFER_BYTES fails the call. options.reflogAction
|
|
32
|
+
* sets GIT_REFLOG_ACTION for this one child only (design 13.2).
|
|
33
|
+
*/
|
|
34
|
+
run(args: readonly string[], options?: GitRunOptions): Promise<GitRunResult>;
|
|
35
|
+
/**
|
|
36
|
+
* A call with its own budget (a repository hook, design 12.7, Decision 83):
|
|
37
|
+
* git leads a new process group, so the timeout ends git and everything it
|
|
38
|
+
* started (SIGTERM, then SIGKILL after GROUP_KILL_GRACE_MS); killing `git
|
|
39
|
+
* hook run` alone would leave the hook running. Output beyond the cap is
|
|
40
|
+
* read and dropped, so a loud hook neither blocks on a full pipe nor fails.
|
|
41
|
+
* The timers are AbortSignal timers of this one child, like execFile's own
|
|
42
|
+
* timeout option on the default path: process plumbing, not scheduling.
|
|
43
|
+
*/
|
|
44
|
+
private runInGroup;
|
|
45
|
+
private failure;
|
|
46
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { kill } from 'node:process';
|
|
3
|
+
import { GitCommandError } from '../../contracts/errors.js';
|
|
4
|
+
/** 128 MiB: a cat-file batch chunk (64 MiB of content) plus headers fits with room. */
|
|
5
|
+
export const GIT_MAX_BUFFER_BYTES = 134217728;
|
|
6
|
+
const KILL_SIGNAL = 'SIGTERM';
|
|
7
|
+
const MAX_BUFFER_EXCEEDED = 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
|
|
8
|
+
/** After a group's SIGTERM, how long its processes get before SIGKILL (Decision 83). */
|
|
9
|
+
const GROUP_KILL_GRACE_MS = 2000;
|
|
10
|
+
/** The commit index is one self-contained file and runs no fsmonitor hook (design 12.7). */
|
|
11
|
+
const COMMIT_INDEX_ARGS = [
|
|
12
|
+
'-c',
|
|
13
|
+
'core.splitIndex=false',
|
|
14
|
+
'-c',
|
|
15
|
+
'core.fsmonitor=false',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Adapter: the only file that spawns git (execFile with an argument array and
|
|
19
|
+
* encoding 'buffer'). Adds GIT_TERMINAL_PROMPT=0, GIT_OPTIONAL_LOCKS=0 and
|
|
20
|
+
* LC_ALL=C to the base environment of every child.
|
|
21
|
+
*/
|
|
22
|
+
export class GitProcessAdapter {
|
|
23
|
+
options;
|
|
24
|
+
env;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.env = childEnvironment(options.baseEnv, options.indexFile);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* options.timeoutMs replaces the adapter's timeout for this call;
|
|
31
|
+
* options.outputCapBytes keeps the first bytes of stdout and of stderr and
|
|
32
|
+
* drops the rest without failing (design 12.7, Decision 83), where without it
|
|
33
|
+
* output beyond GIT_MAX_BUFFER_BYTES fails the call. options.reflogAction
|
|
34
|
+
* sets GIT_REFLOG_ACTION for this one child only (design 13.2).
|
|
35
|
+
*/
|
|
36
|
+
run(args, options) {
|
|
37
|
+
const argv = this.options.indexFile === undefined
|
|
38
|
+
? args
|
|
39
|
+
: [...COMMIT_INDEX_ARGS, ...args];
|
|
40
|
+
const env = callEnvironment(this.env, options?.reflogAction);
|
|
41
|
+
if (options?.timeoutMs !== undefined ||
|
|
42
|
+
options?.outputCapBytes !== undefined ||
|
|
43
|
+
options?.abort !== undefined) {
|
|
44
|
+
return this.runInGroup(argv, env, options);
|
|
45
|
+
}
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const child = execFile(this.options.gitBinary, argv, {
|
|
48
|
+
cwd: this.options.cwd,
|
|
49
|
+
env,
|
|
50
|
+
encoding: 'buffer',
|
|
51
|
+
maxBuffer: GIT_MAX_BUFFER_BYTES,
|
|
52
|
+
timeout: this.options.timeoutMs,
|
|
53
|
+
killSignal: KILL_SIGNAL,
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
}, (error, stdout, stderr) => {
|
|
56
|
+
const stderrText = stderr.toString('utf8');
|
|
57
|
+
if (error === null) {
|
|
58
|
+
resolve({ exitCode: 0, stdout, stderr: stderrText });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (typeof error.code === 'number') {
|
|
62
|
+
resolve({ exitCode: error.code, stdout, stderr: stderrText });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
reject(this.failure(error, argv, stderrText));
|
|
66
|
+
});
|
|
67
|
+
// git may exit before reading all of stdin (a fatal error on the first
|
|
68
|
+
// line); the resulting EPIPE must not become an uncaught exception.
|
|
69
|
+
child.stdin?.on('error', () => undefined);
|
|
70
|
+
// Closing stdin even without input, so a command that reads it
|
|
71
|
+
// (cat-file --batch, --pathspec-from-file=-) sees EOF instead of hanging.
|
|
72
|
+
child.stdin?.end(options?.stdin ?? '');
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A call with its own budget (a repository hook, design 12.7, Decision 83):
|
|
77
|
+
* git leads a new process group, so the timeout ends git and everything it
|
|
78
|
+
* started (SIGTERM, then SIGKILL after GROUP_KILL_GRACE_MS); killing `git
|
|
79
|
+
* hook run` alone would leave the hook running. Output beyond the cap is
|
|
80
|
+
* read and dropped, so a loud hook neither blocks on a full pipe nor fails.
|
|
81
|
+
* The timers are AbortSignal timers of this one child, like execFile's own
|
|
82
|
+
* timeout option on the default path: process plumbing, not scheduling.
|
|
83
|
+
*/
|
|
84
|
+
runInGroup(argv, env, options) {
|
|
85
|
+
const timeoutMs = options.timeoutMs ?? this.options.timeoutMs;
|
|
86
|
+
const cap = options.outputCapBytes;
|
|
87
|
+
const abort = options.abort;
|
|
88
|
+
const command = `git ${argv.join(' ')}`;
|
|
89
|
+
const abortedError = (stderrText) => new GitCommandError(`${command} was ended because enmetu is stopping`, 'GIT_ABORTED', argv, null, stderrText);
|
|
90
|
+
if (abort?.aborted === true) {
|
|
91
|
+
return Promise.reject(abortedError(''));
|
|
92
|
+
}
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const child = spawn(this.options.gitBinary, argv, {
|
|
95
|
+
cwd: this.options.cwd,
|
|
96
|
+
env,
|
|
97
|
+
detached: true,
|
|
98
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
99
|
+
windowsHide: true,
|
|
100
|
+
});
|
|
101
|
+
const stdout = new CappedOutput(cap ?? GIT_MAX_BUFFER_BYTES);
|
|
102
|
+
const stderr = new CappedOutput(cap ?? GIT_MAX_BUFFER_BYTES);
|
|
103
|
+
let settled = false;
|
|
104
|
+
let ended = null;
|
|
105
|
+
const deadline = AbortSignal.timeout(timeoutMs);
|
|
106
|
+
let grace = null;
|
|
107
|
+
const settle = (outcome) => {
|
|
108
|
+
if (settled) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
settled = true;
|
|
112
|
+
deadline.removeEventListener('abort', onDeadline);
|
|
113
|
+
abort?.removeEventListener('abort', onAbort);
|
|
114
|
+
grace?.removeEventListener('abort', onGrace);
|
|
115
|
+
outcome();
|
|
116
|
+
};
|
|
117
|
+
const endedError = () => ended === 'aborted'
|
|
118
|
+
? abortedError(stderr.text())
|
|
119
|
+
: new GitCommandError(`${command} timed out after ${timeoutMs} ms`, 'GIT_TIMEOUT', argv, null, stderr.text());
|
|
120
|
+
const onGrace = () => {
|
|
121
|
+
killGroup(child.pid, 'SIGKILL');
|
|
122
|
+
// A process that left the group (setsid) may still hold the pipes
|
|
123
|
+
// open; the call ends here regardless.
|
|
124
|
+
child.stdout.destroy();
|
|
125
|
+
child.stderr.destroy();
|
|
126
|
+
settle(() => reject(endedError()));
|
|
127
|
+
};
|
|
128
|
+
const endGroup = (reason) => {
|
|
129
|
+
if (ended !== null) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
ended = reason;
|
|
133
|
+
killGroup(child.pid, KILL_SIGNAL);
|
|
134
|
+
grace = AbortSignal.timeout(GROUP_KILL_GRACE_MS);
|
|
135
|
+
grace.addEventListener('abort', onGrace, { once: true });
|
|
136
|
+
};
|
|
137
|
+
const onDeadline = () => endGroup('timeout');
|
|
138
|
+
const onAbort = () => endGroup('aborted');
|
|
139
|
+
deadline.addEventListener('abort', onDeadline, { once: true });
|
|
140
|
+
abort?.addEventListener('abort', onAbort, { once: true });
|
|
141
|
+
child.stdout.on('data', (chunk) => stdout.add(chunk));
|
|
142
|
+
child.stderr.on('data', (chunk) => stderr.add(chunk));
|
|
143
|
+
child.on('error', (error) => {
|
|
144
|
+
settle(() => reject(new GitCommandError(`git could not be started: binary '${this.options.gitBinary}', directory '${this.options.cwd}' (${errorCode(error)})`, 'GIT_NOT_FOUND', argv, null, '')));
|
|
145
|
+
});
|
|
146
|
+
child.on('close', (code, signal) => {
|
|
147
|
+
if (ended !== null) {
|
|
148
|
+
settle(() => reject(endedError()));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (cap === undefined && (stdout.dropped || stderr.dropped)) {
|
|
152
|
+
killGroup(child.pid, 'SIGKILL');
|
|
153
|
+
settle(() => reject(new GitCommandError(`${command} wrote more than ${GIT_MAX_BUFFER_BYTES} bytes`, 'GIT_FAILED', argv, null, stderr.text())));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (code !== null) {
|
|
157
|
+
settle(() => resolve({
|
|
158
|
+
exitCode: code,
|
|
159
|
+
stdout: stdout.bytes(),
|
|
160
|
+
stderr: stderr.text(),
|
|
161
|
+
}));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
settle(() => reject(new GitCommandError(`${command} ended by signal ${signal ?? 'unknown'}`, 'GIT_FAILED', argv, null, stderr.text())));
|
|
165
|
+
});
|
|
166
|
+
child.stdin.on('error', () => undefined);
|
|
167
|
+
child.stdin.end(options.stdin ?? '');
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
failure(error, args, stderr) {
|
|
171
|
+
const command = `git ${args.join(' ')}`;
|
|
172
|
+
if (error.code === MAX_BUFFER_EXCEEDED) {
|
|
173
|
+
return new GitCommandError(`${command} wrote more than ${GIT_MAX_BUFFER_BYTES} bytes`, 'GIT_FAILED', args, null, stderr);
|
|
174
|
+
}
|
|
175
|
+
if (error.killed === true && error.signal === KILL_SIGNAL) {
|
|
176
|
+
return new GitCommandError(`${command} timed out after ${this.options.timeoutMs} ms`, 'GIT_TIMEOUT', args, null, stderr);
|
|
177
|
+
}
|
|
178
|
+
if (typeof error.code === 'string') {
|
|
179
|
+
return new GitCommandError(
|
|
180
|
+
// Node reports a missing binary and a missing working directory with
|
|
181
|
+
// the same ENOENT, so the message names both.
|
|
182
|
+
`git could not be started: binary '${this.options.gitBinary}', directory '${this.options.cwd}' (${error.code})`, 'GIT_NOT_FOUND', args, null, stderr);
|
|
183
|
+
}
|
|
184
|
+
return new GitCommandError(`${command} ended by signal ${error.signal ?? 'unknown'}`, 'GIT_FAILED', args, null, stderr);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Keeps the first `cap` bytes of a stream and counts that more arrived. */
|
|
188
|
+
class CappedOutput {
|
|
189
|
+
cap;
|
|
190
|
+
chunks = [];
|
|
191
|
+
length = 0;
|
|
192
|
+
dropped = false;
|
|
193
|
+
constructor(cap) {
|
|
194
|
+
this.cap = cap;
|
|
195
|
+
}
|
|
196
|
+
add(chunk) {
|
|
197
|
+
const room = this.cap - this.length;
|
|
198
|
+
if (room <= 0) {
|
|
199
|
+
this.dropped = this.dropped || chunk.length > 0;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const kept = chunk.length > room ? chunk.subarray(0, room) : chunk;
|
|
203
|
+
this.dropped = this.dropped || kept.length < chunk.length;
|
|
204
|
+
this.chunks.push(kept);
|
|
205
|
+
this.length += kept.length;
|
|
206
|
+
}
|
|
207
|
+
bytes() {
|
|
208
|
+
return Buffer.concat(this.chunks);
|
|
209
|
+
}
|
|
210
|
+
text() {
|
|
211
|
+
return this.bytes().toString('utf8');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** Signals git's whole process group; a group that is already gone is fine. */
|
|
215
|
+
function killGroup(pid, signal) {
|
|
216
|
+
if (pid === undefined) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
kill(-pid, signal);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (errorCode(error) !== 'ESRCH') {
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function errorCode(error) {
|
|
229
|
+
if (typeof error === 'object' && error !== null && 'code' in error) {
|
|
230
|
+
return typeof error.code === 'string' ? error.code : 'unknown';
|
|
231
|
+
}
|
|
232
|
+
return 'unknown';
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The adapter's environment, plus GIT_REFLOG_ACTION when the call names one.
|
|
236
|
+
* The shared environment is never changed, so no other call inherits it.
|
|
237
|
+
*/
|
|
238
|
+
function callEnvironment(env, reflogAction) {
|
|
239
|
+
if (reflogAction === undefined) {
|
|
240
|
+
return env;
|
|
241
|
+
}
|
|
242
|
+
return { ...env, GIT_REFLOG_ACTION: reflogAction };
|
|
243
|
+
}
|
|
244
|
+
function childEnvironment(baseEnv, indexFile) {
|
|
245
|
+
const env = {};
|
|
246
|
+
for (const [key, value] of Object.entries(baseEnv)) {
|
|
247
|
+
if (value !== undefined) {
|
|
248
|
+
env[key] = value;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (indexFile !== undefined) {
|
|
252
|
+
env.GIT_INDEX_FILE = indexFile;
|
|
253
|
+
}
|
|
254
|
+
// An https credential prompt fails instead of hanging the daemon.
|
|
255
|
+
env.GIT_TERMINAL_PROMPT = '0';
|
|
256
|
+
// git status takes no index.lock, so enmetu's reads never collide with a person's git.
|
|
257
|
+
env.GIT_OPTIONAL_LOCKS = '0';
|
|
258
|
+
// Stable English stderr for the two classifications the facade reads.
|
|
259
|
+
env.LC_ALL = 'C';
|
|
260
|
+
return env;
|
|
261
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ILogger, LogEntry } from '../../contracts/ILogger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Null Object for a step that must not log twice: the commit step's residue
|
|
4
|
+
* build repeats the guarded staging, whose EXCLUDED lines the first build
|
|
5
|
+
* already wrote (design 12.7). Built by the composition root only.
|
|
6
|
+
*/
|
|
7
|
+
export declare class NullLogger implements ILogger {
|
|
8
|
+
log(_entry: LogEntry): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Null Object for a step that must not log twice: the commit step's residue
|
|
3
|
+
* build repeats the guarded staging, whose EXCLUDED lines the first build
|
|
4
|
+
* already wrote (design 12.7). Built by the composition root only.
|
|
5
|
+
*/
|
|
6
|
+
export class NullLogger {
|
|
7
|
+
log(_entry) {
|
|
8
|
+
// Deliberately empty: the entries were already logged by the first build.
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IClock } from '../../contracts/IClock.js';
|
|
2
|
+
import type { ILogger, LogEntry } from '../../contracts/ILogger.js';
|
|
3
|
+
import type { ITextSink } from '../../contracts/ITextSink.js';
|
|
4
|
+
/** Adapter: one formatLogLine line plus a newline per entry, written to the injected sink. */
|
|
5
|
+
export declare class StderrLoggerAdapter implements ILogger {
|
|
6
|
+
private readonly sink;
|
|
7
|
+
private readonly clock;
|
|
8
|
+
constructor(sink: ITextSink, clock: IClock);
|
|
9
|
+
log(entry: LogEntry): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { formatLogLine } from '../../core/log/formatLogLine.js';
|
|
2
|
+
/** Adapter: one formatLogLine line plus a newline per entry, written to the injected sink. */
|
|
3
|
+
export class StderrLoggerAdapter {
|
|
4
|
+
sink;
|
|
5
|
+
clock;
|
|
6
|
+
constructor(sink, clock) {
|
|
7
|
+
this.sink = sink;
|
|
8
|
+
this.clock = clock;
|
|
9
|
+
}
|
|
10
|
+
log(entry) {
|
|
11
|
+
this.sink.write(`${formatLogLine(entry, this.clock.now())}\n`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IProcessHost, StopSignal } from '../../contracts/IProcessHost.js';
|
|
2
|
+
/** Adapter: process signals, kill(pid, 0) and os.hostname to IProcessHost. */
|
|
3
|
+
export declare class NodeProcessAdapter implements IProcessHost {
|
|
4
|
+
get pid(): number;
|
|
5
|
+
get hostname(): string;
|
|
6
|
+
isAlive(pid: number): boolean;
|
|
7
|
+
onStopSignal(handler: (signal: StopSignal) => void): () => void;
|
|
8
|
+
exitNow(code: number): never;
|
|
9
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
const STOP_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
3
|
+
/** Adapter: process signals, kill(pid, 0) and os.hostname to IProcessHost. */
|
|
4
|
+
export class NodeProcessAdapter {
|
|
5
|
+
get pid() {
|
|
6
|
+
return process.pid;
|
|
7
|
+
}
|
|
8
|
+
get hostname() {
|
|
9
|
+
return hostname();
|
|
10
|
+
}
|
|
11
|
+
isAlive(pid) {
|
|
12
|
+
// kill(0) and kill(-n) address process groups and would report a live
|
|
13
|
+
// group for a pid that names no process.
|
|
14
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
process.kill(pid, 0);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const code = errorCode(error);
|
|
23
|
+
if (code === 'ESRCH') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// The process exists but belongs to another user.
|
|
27
|
+
if (code === 'EPERM') {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
onStopSignal(handler) {
|
|
34
|
+
const subscriptions = STOP_SIGNALS.map((signal) => {
|
|
35
|
+
const listener = () => handler(signal);
|
|
36
|
+
process.on(signal, listener);
|
|
37
|
+
return { signal, listener };
|
|
38
|
+
});
|
|
39
|
+
return () => {
|
|
40
|
+
for (const { signal, listener } of subscriptions) {
|
|
41
|
+
process.off(signal, listener);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
exitNow(code) {
|
|
46
|
+
process.exit(code);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Not instanceof Error: an error from Node's core can come from another realm
|
|
50
|
+
// (Jest's test context), where instanceof is false.
|
|
51
|
+
function errorCode(error) {
|
|
52
|
+
if (typeof error === 'object' && error !== null && 'code' in error) {
|
|
53
|
+
return typeof error.code === 'string' ? error.code : undefined;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IWatcher, WatchListener } from '../../contracts/IWatcher.js';
|
|
2
|
+
/** The part of fs.FSWatcher the adapter uses. */
|
|
3
|
+
export interface WatchHandle {
|
|
4
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
5
|
+
close(): void;
|
|
6
|
+
}
|
|
7
|
+
/** A recursive watch of root that reports each event's root-relative filename (null when the platform gives none). */
|
|
8
|
+
export type WatchFunction = (root: string, onEvent: (filename: string | null) => void) => WatchHandle;
|
|
9
|
+
/** fs.watch(root, { recursive: true }), the production WatchFunction. */
|
|
10
|
+
export declare const nodeWatch: WatchFunction;
|
|
11
|
+
/**
|
|
12
|
+
* Adapter: fs.watch to IWatcher. Drops events isWatchNoise rejects, calls
|
|
13
|
+
* onChange() without a path for the rest, and reports the first watcher
|
|
14
|
+
* error through onFailure exactly once.
|
|
15
|
+
*/
|
|
16
|
+
export declare class FsWatchAdapter implements IWatcher {
|
|
17
|
+
private readonly root;
|
|
18
|
+
private readonly watchFunction;
|
|
19
|
+
private handle;
|
|
20
|
+
private failureReported;
|
|
21
|
+
constructor(root: string, watchFunction: WatchFunction);
|
|
22
|
+
start(listener: WatchListener): void;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
import { errorMessage } from '../../core/errors/errorMessage.js';
|
|
3
|
+
import { isWatchNoise } from '../../core/watch/watchFilter.js';
|
|
4
|
+
/** fs.watch(root, { recursive: true }), the production WatchFunction. */
|
|
5
|
+
export const nodeWatch = (root, onEvent) => watch(root, { recursive: true }, (_eventType, filename) => onEvent(filename));
|
|
6
|
+
/**
|
|
7
|
+
* Adapter: fs.watch to IWatcher. Drops events isWatchNoise rejects, calls
|
|
8
|
+
* onChange() without a path for the rest, and reports the first watcher
|
|
9
|
+
* error through onFailure exactly once.
|
|
10
|
+
*/
|
|
11
|
+
export class FsWatchAdapter {
|
|
12
|
+
root;
|
|
13
|
+
watchFunction;
|
|
14
|
+
handle;
|
|
15
|
+
failureReported = false;
|
|
16
|
+
constructor(root, watchFunction) {
|
|
17
|
+
this.root = root;
|
|
18
|
+
this.watchFunction = watchFunction;
|
|
19
|
+
}
|
|
20
|
+
start(listener) {
|
|
21
|
+
const reportFailure = (detail) => {
|
|
22
|
+
if (this.failureReported) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.failureReported = true;
|
|
26
|
+
listener.onFailure(detail);
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
this.handle = this.watchFunction(this.root, (filename) => {
|
|
30
|
+
if (!isWatchNoise(filename)) {
|
|
31
|
+
listener.onChange();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
// fs.watch throws instead of emitting when the watch limit is already
|
|
37
|
+
// reached at start (risk 6.2); the daemon degrades to the poll either way.
|
|
38
|
+
reportFailure(errorMessage(error));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.handle.on('error', (error) => reportFailure(errorMessage(error)));
|
|
42
|
+
}
|
|
43
|
+
close() {
|
|
44
|
+
this.handle?.close();
|
|
45
|
+
this.handle = undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ExitCode } from '../../contracts/exitCode.js';
|
|
2
|
+
import type { IFileSystem } from '../../contracts/IFileSystem.js';
|
|
3
|
+
import type { IGitReader } from '../../contracts/IGitReader.js';
|
|
4
|
+
import type { IRepositoryLocator, RepositoryPaths } from '../../contracts/IRepositoryLocator.js';
|
|
5
|
+
import type { ITextSink } from '../../contracts/ITextSink.js';
|
|
6
|
+
export interface HookCommandInput {
|
|
7
|
+
/** The hook's working directory (git runs hooks from the root). */
|
|
8
|
+
readonly startDir: string;
|
|
9
|
+
}
|
|
10
|
+
export interface HookCommandDeps {
|
|
11
|
+
readonly locator: IRepositoryLocator;
|
|
12
|
+
readonly fs: IFileSystem;
|
|
13
|
+
/** A reader whose git children inherit GIT_INDEX_FILE, so the check reads the index being committed. */
|
|
14
|
+
readonly openGit: (paths: RepositoryPaths) => IGitReader;
|
|
15
|
+
readonly stderr: ITextSink;
|
|
16
|
+
}
|
|
17
|
+
export interface HookCommandResult {
|
|
18
|
+
readonly exitCode: ExitCode;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Controller:
|
|
22
|
+
* 1. locator.locate(startDir); anything but found: notice, exit 0 (the hook fails open).
|
|
23
|
+
* 2. parseConfig of <root>/enmetu.config.json; a ConfigError prints a notice and
|
|
24
|
+
* applies the defaults, so a broken config never blocks the commit that fixes it.
|
|
25
|
+
* Unknown keys are ignored without a word: the daemon names them at start (design 14.17).
|
|
26
|
+
* 3. preCommitCheckController with the config's skipJsonCheck.
|
|
27
|
+
* 4. Violations: print the refusal of design 1.18 to stderr, exit 1. None: print nothing, exit 0.
|
|
28
|
+
*/
|
|
29
|
+
export declare function hookCommand(input: HookCommandInput, deps: HookCommandDeps): Promise<HookCommandResult>;
|