@fluado/enmetu 0.3.0 → 0.5.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/README.md +26 -20
- package/dist/adapters/git/gitFacade.d.ts +3 -6
- package/dist/adapters/git/gitFacade.js +6 -2
- package/dist/adapters/git/gitProcessAdapter.js +2 -1
- package/dist/cli/commands/runCommand.d.ts +25 -3
- package/dist/cli/commands/runCommand.js +43 -6
- package/dist/cli/exitCodes.js +1 -0
- package/dist/cli.js +12 -6
- package/dist/commit/commitLocalChangesController.d.ts +15 -5
- package/dist/commit/commitLocalChangesController.js +21 -14
- package/dist/commit/stageGuardedChanges.d.ts +3 -1
- package/dist/commit/stageGuardedChanges.js +5 -4
- package/dist/contracts/IGitRunner.d.ts +7 -0
- package/dist/contracts/ISyncCycle.d.ts +8 -0
- package/dist/contracts/errors.d.ts +1 -1
- package/dist/contracts/library.d.ts +1 -1
- package/dist/contracts/logCodes.d.ts +1 -1
- package/dist/contracts/runtime.d.ts +26 -5
- package/dist/contracts/syncTypes.d.ts +27 -5
- package/dist/contracts/syncTypes.js +2 -0
- package/dist/core/quarantine/restoreWarnings.d.ts +6 -3
- package/dist/core/quarantine/restoreWarnings.js +12 -3
- package/dist/core/state/stateGuards.js +1 -0
- package/dist/core/status/statusHeadline.js +3 -0
- package/dist/enmetuInstances.d.ts +3 -3
- package/dist/enmetuInstances.js +6 -4
- package/dist/hooks/hookScript.d.ts +6 -11
- package/dist/hooks/hookScript.js +35 -11
- package/dist/hooks/preCommitHookInstaller.d.ts +12 -10
- package/dist/hooks/preCommitHookInstaller.js +23 -16
- package/dist/index.js +8 -4
- package/dist/library/enmetuLibrary.js +2 -2
- package/dist/sync/integrateUpstreamController.d.ts +16 -2
- package/dist/sync/integrateUpstreamController.js +21 -1
- package/dist/sync/syncCycleController.d.ts +9 -1
- package/dist/sync/syncCycleController.js +69 -4
- package/dist/sync/syncCycleRunner.js +6 -2
- package/package.json +3 -2
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
import type { ITextSink } from './ITextSink.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* How this enmetu is started again, recorded in the pre-commit hook (designs
|
|
4
|
+
* 1.7, 13.1, 14.22).
|
|
5
|
+
*/
|
|
6
|
+
export type Launch =
|
|
7
|
+
/**
|
|
8
|
+
* An interpreter and the CLI script: node or bun with dist/cli.js. The
|
|
9
|
+
* interpreter is process.execPath, absolute under Node, a bare name under a
|
|
10
|
+
* Bun started by name (13.1); the script is process.argv[1], which Node
|
|
11
|
+
* makes absolute.
|
|
12
|
+
*/
|
|
13
|
+
{
|
|
14
|
+
readonly kind: 'script';
|
|
15
|
+
readonly interpreter: string;
|
|
16
|
+
readonly script: string;
|
|
17
|
+
}
|
|
18
|
+
/** The standalone executable of design 14.22 (process.execPath): it runs its own hook check. */
|
|
19
|
+
| {
|
|
20
|
+
readonly kind: 'executable';
|
|
21
|
+
readonly path: string;
|
|
22
|
+
};
|
|
23
|
+
/** Everything the process gives enmetu, gathered once by an entry (src/cli.ts, src/index.ts, src/standalone.ts). */
|
|
3
24
|
export interface Runtime {
|
|
4
25
|
/** Arguments after the node binary and the script. */
|
|
5
26
|
readonly argv: readonly string[];
|
|
6
27
|
/** Base environment for every git child. */
|
|
7
28
|
readonly env: Readonly<Record<string, string | undefined>>;
|
|
8
29
|
readonly cwd: string;
|
|
9
|
-
/**
|
|
10
|
-
readonly
|
|
11
|
-
/** Absolute path of the CLI script (Node resolves argv[1]), recorded in the hook. */
|
|
12
|
-
readonly cliPath: string;
|
|
30
|
+
/** Recorded in the hook. */
|
|
31
|
+
readonly launch: Launch;
|
|
13
32
|
readonly version: string;
|
|
14
33
|
/** process.platform: 'linux', 'darwin', 'win32', ... */
|
|
15
34
|
readonly platform: string;
|
|
16
35
|
/** The JavaScript runtime enmetu runs on: 'bun' when process.versions.bun is set. */
|
|
17
36
|
readonly engine: 'node' | 'bun';
|
|
37
|
+
/** Its version: process.versions.bun under Bun, process.versions.node under Node (design 14.25). */
|
|
38
|
+
readonly engineVersion: string;
|
|
18
39
|
readonly stdout: ITextSink;
|
|
19
40
|
readonly stderr: ITextSink;
|
|
20
41
|
}
|
|
@@ -11,11 +11,30 @@ export type PauseReason = 'FOREIGN_OPERATION' | 'DETACHED_HEAD' | 'NO_UPSTREAM'
|
|
|
11
11
|
*/
|
|
12
12
|
| 'REBASE_REFUSED';
|
|
13
13
|
/**
|
|
14
|
-
* Why a cycle
|
|
15
|
-
* a person has a selection staged and partialStagingWaitMs has not
|
|
16
|
-
* (design 14.17),
|
|
14
|
+
* Why a cycle did less than everything although the tree has changes (design
|
|
15
|
+
* 12.7): a person has a selection staged and partialStagingWaitMs has not
|
|
16
|
+
* passed (design 14.17), a repository commit hook rejected enmetu's commit, or
|
|
17
|
+
* a file was saved while the cycle ran and the pull waits for a cycle that
|
|
18
|
+
* finds nothing new to commit (design 14.21, Decision 147).
|
|
17
19
|
*/
|
|
18
|
-
export type WaitReason = 'PARTIAL_SELECTION_STAGED' | 'COMMIT_HOOK_REJECTED';
|
|
20
|
+
export type WaitReason = 'PARTIAL_SELECTION_STAGED' | 'COMMIT_HOOK_REJECTED' | 'TREE_CHANGING';
|
|
21
|
+
/**
|
|
22
|
+
* Which dirty tracked paths integrate may set aside for a pull (design 14.21,
|
|
23
|
+
* Decision 147): the ones the commit step could not commit this cycle. Any
|
|
24
|
+
* other dirty path is a save that landed during the cycle, and the pull waits.
|
|
25
|
+
*/
|
|
26
|
+
export type HoldPolicy =
|
|
27
|
+
/** Exactly these paths (the content guard's EXCLUDED paths); empty when the cycle committed everything or found nothing. */
|
|
28
|
+
{
|
|
29
|
+
readonly kind: 'only';
|
|
30
|
+
readonly paths: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
/** Every dirty tracked path: a commit hook rejected the commit, or the hooks' residue is all that changed. */
|
|
33
|
+
| {
|
|
34
|
+
readonly kind: 'all';
|
|
35
|
+
};
|
|
36
|
+
/** The policy after a clean status: any dirty path at the pull is a save made during the cycle. */
|
|
37
|
+
export declare const HOLD_NOTHING: HoldPolicy;
|
|
19
38
|
export type CycleOutcome = {
|
|
20
39
|
readonly kind: 'synced';
|
|
21
40
|
} | {
|
|
@@ -23,8 +42,11 @@ export type CycleOutcome = {
|
|
|
23
42
|
readonly reason: PauseReason;
|
|
24
43
|
} | {
|
|
25
44
|
readonly kind: 'busy';
|
|
26
|
-
}
|
|
45
|
+
}
|
|
46
|
+
/** The remote could not be reached; detail is git's first line of reason (design 14.24, Decision 159). */
|
|
47
|
+
| {
|
|
27
48
|
readonly kind: 'offline';
|
|
49
|
+
readonly detail: string;
|
|
28
50
|
} | {
|
|
29
51
|
readonly kind: 'conflict';
|
|
30
52
|
} | {
|
|
@@ -2,6 +2,8 @@ import type { ILogger } from '../../contracts/ILogger.js';
|
|
|
2
2
|
import type { KeptBlob } from '../../contracts/IPathQuarantine.js';
|
|
3
3
|
/** What a restore reports that a person should see; the fields of RestoreResult's restored. */
|
|
4
4
|
export interface RestoreWarnings {
|
|
5
|
+
/** Paths put back; 0 logs no RESTORED line (design 14.21). */
|
|
6
|
+
readonly count: number;
|
|
5
7
|
readonly kept: readonly KeptBlob[];
|
|
6
8
|
readonly changedUnderHold: readonly string[];
|
|
7
9
|
readonly overwritten: readonly KeptBlob[];
|
|
@@ -9,8 +11,9 @@ export interface RestoreWarnings {
|
|
|
9
11
|
/** The QUARANTINE_UPSTREAM_CHANGED message (Decision 99). */
|
|
10
12
|
export declare function changedUnderHoldMessage(path: string): string;
|
|
11
13
|
/**
|
|
12
|
-
* One
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* One INFO RESTORED with the count of paths put back (Decision 148), then one
|
|
15
|
+
* WARN per kept blob and per path restored over a change, the same at every
|
|
16
|
+
* restore: recovery, a pending memento, and integrate (written three times
|
|
17
|
+
* before it was extracted).
|
|
15
18
|
*/
|
|
16
19
|
export declare function logRestoreWarnings(warnings: RestoreWarnings, cycleId: string, logger: ILogger): void;
|
|
@@ -4,11 +4,20 @@ export function changedUnderHoldMessage(path) {
|
|
|
4
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
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
* One
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* One INFO RESTORED with the count of paths put back (Decision 148), then one
|
|
8
|
+
* WARN per kept blob and per path restored over a change, the same at every
|
|
9
|
+
* restore: recovery, a pending memento, and integrate (written three times
|
|
10
|
+
* before it was extracted).
|
|
10
11
|
*/
|
|
11
12
|
export function logRestoreWarnings(warnings, cycleId, logger) {
|
|
13
|
+
if (warnings.count > 0) {
|
|
14
|
+
logger.log({
|
|
15
|
+
level: 'INFO',
|
|
16
|
+
code: 'RESTORED',
|
|
17
|
+
message: `${warnings.count === 1 ? '1 held path' : `${warnings.count} held paths`} put back`,
|
|
18
|
+
cycleId,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
12
21
|
for (const kept of warnings.kept) {
|
|
13
22
|
logger.log({
|
|
14
23
|
level: 'WARN',
|
|
@@ -69,6 +69,9 @@ function runningHeadline(record, uncommitted, unpushed) {
|
|
|
69
69
|
if (record.reason === 'COMMIT_HOOK_REJECTED') {
|
|
70
70
|
return `waiting: a commit hook rejected the last commit since ${since}, ${work}`;
|
|
71
71
|
}
|
|
72
|
+
if (record.reason === 'TREE_CHANGING') {
|
|
73
|
+
return `waiting: a save landed during the last cycle since ${since}; the pull waits for a quiet cycle, ${work}`;
|
|
74
|
+
}
|
|
72
75
|
return `waiting:${reasonText(record)} since ${since}, ${work}`;
|
|
73
76
|
case 'paused':
|
|
74
77
|
return `paused:${reasonText(record)} since ${since}, ${work}`;
|
|
@@ -31,9 +31,9 @@ export declare function createRecoveryDeps(paths: RepositoryPaths, config: Enmet
|
|
|
31
31
|
*/
|
|
32
32
|
export declare function createDaemon(paths: RepositoryPaths, config: EnmetuConfig, runtime: Runtime, processHost?: IProcessHost | undefined, commitMessage?: CommitMessageWriter | undefined): IDaemon;
|
|
33
33
|
/**
|
|
34
|
-
* The daemon's hook installer for one repository (
|
|
35
|
-
* Runtime.
|
|
36
|
-
* is a bare name
|
|
34
|
+
* The daemon's hook installer for one repository (designs 13.1, 14.22): it
|
|
35
|
+
* records Runtime.launch, its interpreter or executable resolved through the
|
|
36
|
+
* PATH enmetu was started with when it is a bare name.
|
|
37
37
|
*/
|
|
38
38
|
export declare function createHookInstaller(paths: RepositoryPaths, config: EnmetuConfig, runtime: Runtime): IHookInstaller;
|
|
39
39
|
export declare function createRunDeps(runtime: Runtime): RunCommandDeps;
|
package/dist/enmetuInstances.js
CHANGED
|
@@ -107,9 +107,9 @@ export function createDaemon(paths, config, runtime, processHost, commitMessage)
|
|
|
107
107
|
return new Daemon({ root: paths.root, quietMs: config.quietMs, pollMs: config.pollMs }, lockFor(paths, parts.fs, host, parts.clock), parts.halt, recovery, preflight, hookInstaller, watcherFor(paths.root, runtime), scheduler, host, parts.logger, state, parts.clock);
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
|
-
* The daemon's hook installer for one repository (
|
|
111
|
-
* Runtime.
|
|
112
|
-
* is a bare name
|
|
110
|
+
* The daemon's hook installer for one repository (designs 13.1, 14.22): it
|
|
111
|
+
* records Runtime.launch, its interpreter or executable resolved through the
|
|
112
|
+
* PATH enmetu was started with when it is a bare name.
|
|
113
113
|
*/
|
|
114
114
|
export function createHookInstaller(paths, config, runtime) {
|
|
115
115
|
return hookInstallerOf(guardedRepositoryParts(paths, config.gitTimeoutMs, runtime), paths, config, runtime);
|
|
@@ -117,7 +117,7 @@ export function createHookInstaller(paths, config, runtime) {
|
|
|
117
117
|
// Null Object: the daemon has no branch for installHook (section 2).
|
|
118
118
|
function hookInstallerOf(parts, paths, config, runtime) {
|
|
119
119
|
return config.installHook
|
|
120
|
-
? new PreCommitHookInstaller(parts.git, parts.fs, paths.hooksDir,
|
|
120
|
+
? new PreCommitHookInstaller(parts.git, parts.fs, paths.hooksDir, runtime.launch, runtime.env.PATH)
|
|
121
121
|
: new NullHookInstaller();
|
|
122
122
|
}
|
|
123
123
|
export function createRunDeps(runtime) {
|
|
@@ -127,6 +127,7 @@ export function createRunDeps(runtime) {
|
|
|
127
127
|
fs,
|
|
128
128
|
readGitConfig: gitConfigReaderFor(runtime, fs),
|
|
129
129
|
logger: new StderrLoggerAdapter(runtime.stderr, new SystemClockAdapter()),
|
|
130
|
+
engine: { kind: runtime.engine, version: runtime.engineVersion },
|
|
130
131
|
createDaemon: (paths, config) => createDaemon(paths, config, runtime),
|
|
131
132
|
};
|
|
132
133
|
}
|
|
@@ -351,6 +352,7 @@ export function createEmbeddedRunDeps(runtime, commitMessage) {
|
|
|
351
352
|
fs,
|
|
352
353
|
readGitConfig: gitConfigReaderFor(runtime, fs),
|
|
353
354
|
logger: new StderrLoggerAdapter(runtime.stderr, new SystemClockAdapter()),
|
|
355
|
+
engine: { kind: runtime.engine, version: runtime.engineVersion },
|
|
354
356
|
createDaemon: (paths, config) => createDaemon(paths, config, runtime, processHost, commitMessage),
|
|
355
357
|
},
|
|
356
358
|
requestStop: () => processHost.requestStop(),
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
+
import type { Launch } from '../contracts/runtime.js';
|
|
1
2
|
export declare const HOOK_MARKER = "# enmetu-managed-hook v1";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
readonly nodePath: string;
|
|
8
|
-
/** Absolute enmetu CLI script recorded at install time. */
|
|
9
|
-
readonly cliPath: string;
|
|
10
|
-
}
|
|
11
|
-
/** The POSIX sh pre-commit hook of design 1.7; a single quote becomes '\''. */
|
|
12
|
-
export declare function renderHookScript(paths: HookScriptPaths): string;
|
|
3
|
+
/**
|
|
4
|
+
* The POSIX sh pre-commit hook of design 1.7 in the shape the launch needs
|
|
5
|
+
* (design 14.22); a single quote becomes '\''.
|
|
6
|
+
*/
|
|
7
|
+
export declare function renderHookScript(launch: Launch): string;
|
|
13
8
|
/** true when the content carries the enmetu marker line. */
|
|
14
9
|
export declare function isEnmetuHook(content: string): boolean;
|
|
15
10
|
/**
|
package/dist/hooks/hookScript.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
export const HOOK_MARKER = '# enmetu-managed-hook v1';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
const HEADER = [
|
|
3
|
+
'#!/bin/sh',
|
|
4
|
+
HOOK_MARKER,
|
|
5
|
+
'# Written by enmetu at startup; set "installHook": false in enmetu.config.json to stop that.',
|
|
6
|
+
'# Refuses a commit that records conflict markers or a .json file that does not parse.',
|
|
7
|
+
'# To commit anyway: git commit --no-verify',
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* The POSIX sh pre-commit hook of design 1.7 in the shape the launch needs
|
|
11
|
+
* (design 14.22); a single quote becomes '\''.
|
|
12
|
+
*/
|
|
13
|
+
export function renderHookScript(launch) {
|
|
14
|
+
const body = launch.kind === 'script' ? scriptForm(launch) : executableForm(launch);
|
|
15
|
+
return [...HEADER, ...body, ''].join('\n');
|
|
16
|
+
}
|
|
17
|
+
/** The interpreter and the CLI script, as src/cli.ts and src/index.ts record them. */
|
|
18
|
+
function scriptForm(launch) {
|
|
4
19
|
return [
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'# Written by enmetu at startup; set "installHook": false in enmetu.config.json to stop that.',
|
|
8
|
-
'# Refuses a commit that records conflict markers or a .json file that does not parse.',
|
|
9
|
-
'# To commit anyway: git commit --no-verify',
|
|
10
|
-
`ENMETU_NODE=${shellQuote(paths.nodePath)}`,
|
|
11
|
-
`ENMETU_CLI=${shellQuote(paths.cliPath)}`,
|
|
20
|
+
`ENMETU_NODE=${shellQuote(launch.interpreter)}`,
|
|
21
|
+
`ENMETU_CLI=${shellQuote(launch.script)}`,
|
|
12
22
|
// The recorded interpreter may vanish with an upgrade (nvm, a moved Bun)
|
|
13
23
|
// or be a bare name no PATH entry held at install time: the same name on
|
|
14
24
|
// the commit's PATH keeps a Bun-only machine checking, node on PATH is
|
|
@@ -26,8 +36,22 @@ export function renderHookScript(paths) {
|
|
|
26
36
|
'fi',
|
|
27
37
|
'echo "enmetu: pre-commit check skipped (node or enmetu not found)" >&2',
|
|
28
38
|
'exit 0',
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
/** The standalone executable, which runs its own check (design 14.22, Decision 151). */
|
|
42
|
+
function executableForm(launch) {
|
|
43
|
+
return [
|
|
44
|
+
`ENMETU_BIN=${shellQuote(launch.path)}`,
|
|
45
|
+
// The recorded file may be gone (reinstalled elsewhere, another clone of
|
|
46
|
+
// the hook's machine): the same name on the commit's PATH, then enmetu on
|
|
47
|
+
// PATH, then fail open, the shape of Decision 102's line.
|
|
48
|
+
'[ -x "$ENMETU_BIN" ] || ENMETU_BIN="$(command -v "${ENMETU_BIN##*/}" 2>/dev/null || command -v enmetu 2>/dev/null)"',
|
|
49
|
+
'if [ -n "$ENMETU_BIN" ]; then',
|
|
50
|
+
' exec "$ENMETU_BIN" hook pre-commit',
|
|
51
|
+
'fi',
|
|
52
|
+
'echo "enmetu: pre-commit check skipped (enmetu not found)" >&2',
|
|
53
|
+
'exit 0',
|
|
54
|
+
];
|
|
31
55
|
}
|
|
32
56
|
/** true when the content carries the enmetu marker line. */
|
|
33
57
|
export function isEnmetuHook(content) {
|
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
import type { IFileSystem } from '../contracts/IFileSystem.js';
|
|
2
2
|
import type { IGitConfigReader } from '../contracts/IGitConfigReader.js';
|
|
3
3
|
import type { HookInstallResult, IHookInstaller } from '../contracts/IHookInstaller.js';
|
|
4
|
-
import {
|
|
4
|
+
import type { Launch } from '../contracts/runtime.js';
|
|
5
5
|
/**
|
|
6
6
|
* Service: installs <hooksDir>/pre-commit following rules 1 to 5 of design
|
|
7
7
|
* 1.7 (D10). Reads git configuration only through IGitConfigReader, so it
|
|
8
8
|
* cannot write any; never overwrites a hook without the enmetu marker.
|
|
9
9
|
* A bare-name interpreter (Bun started by name reports the bare name "bun"
|
|
10
|
-
* for its own executable path) is resolved through searchPath
|
|
11
|
-
* hook is rendered (
|
|
10
|
+
* for its own executable path) or executable is resolved through searchPath
|
|
11
|
+
* before the hook is rendered (designs 13.1 and 14.22, Decision 102).
|
|
12
12
|
*/
|
|
13
13
|
export declare class PreCommitHookInstaller implements IHookInstaller {
|
|
14
14
|
private readonly config;
|
|
15
15
|
private readonly fs;
|
|
16
16
|
private readonly hooksDir;
|
|
17
|
-
private readonly
|
|
17
|
+
private readonly launch;
|
|
18
18
|
/** The PATH the daemon was started with (Runtime.env.PATH); absent: no resolution. */
|
|
19
19
|
private readonly searchPath?;
|
|
20
|
-
constructor(config: IGitConfigReader, fs: IFileSystem, hooksDir: string,
|
|
20
|
+
constructor(config: IGitConfigReader, fs: IFileSystem, hooksDir: string, launch: Launch,
|
|
21
21
|
/** The PATH the daemon was started with (Runtime.env.PATH); absent: no resolution. */
|
|
22
22
|
searchPath?: string | undefined);
|
|
23
23
|
ensure(): Promise<HookInstallResult>;
|
|
24
24
|
private install;
|
|
25
|
+
/** The launch with its interpreter or executable resolved for the record. */
|
|
26
|
+
private resolvedLaunch;
|
|
25
27
|
/**
|
|
26
|
-
* The
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* The file to record: an absolute name unchanged; for a bare name,
|
|
29
|
+
* <dir>/<name> for the first absolute search path entry where that is an
|
|
30
|
+
* executable file or a link; else the bare name, which the hook looks up
|
|
31
|
+
* on the commit's PATH.
|
|
30
32
|
*/
|
|
31
|
-
private
|
|
33
|
+
private resolve;
|
|
32
34
|
}
|
|
@@ -9,22 +9,22 @@ const OTHER_LANGUAGE_NOTE = 'A hook in another language runs "enmetu hook pre-co
|
|
|
9
9
|
* 1.7 (D10). Reads git configuration only through IGitConfigReader, so it
|
|
10
10
|
* cannot write any; never overwrites a hook without the enmetu marker.
|
|
11
11
|
* A bare-name interpreter (Bun started by name reports the bare name "bun"
|
|
12
|
-
* for its own executable path) is resolved through searchPath
|
|
13
|
-
* hook is rendered (
|
|
12
|
+
* for its own executable path) or executable is resolved through searchPath
|
|
13
|
+
* before the hook is rendered (designs 13.1 and 14.22, Decision 102).
|
|
14
14
|
*/
|
|
15
15
|
export class PreCommitHookInstaller {
|
|
16
16
|
config;
|
|
17
17
|
fs;
|
|
18
18
|
hooksDir;
|
|
19
|
-
|
|
19
|
+
launch;
|
|
20
20
|
searchPath;
|
|
21
|
-
constructor(config, fs, hooksDir,
|
|
21
|
+
constructor(config, fs, hooksDir, launch,
|
|
22
22
|
/** The PATH the daemon was started with (Runtime.env.PATH); absent: no resolution. */
|
|
23
23
|
searchPath) {
|
|
24
24
|
this.config = config;
|
|
25
25
|
this.fs = fs;
|
|
26
26
|
this.hooksDir = hooksDir;
|
|
27
|
-
this.
|
|
27
|
+
this.launch = launch;
|
|
28
28
|
this.searchPath = searchPath;
|
|
29
29
|
}
|
|
30
30
|
async ensure() {
|
|
@@ -73,11 +73,8 @@ export class PreCommitHookInstaller {
|
|
|
73
73
|
instruction: `${target} exists and is not enmetu's. To add enmetu's check, add this line to it: ${pathInvocationLine()}. ${OTHER_LANGUAGE_NOTE}`,
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
-
// 4. An enmetu hook is rewritten only when the recorded
|
|
77
|
-
const rendered = renderHookScript(
|
|
78
|
-
nodePath: await this.interpreterPath(),
|
|
79
|
-
cliPath: this.script.cliPath,
|
|
80
|
-
});
|
|
76
|
+
// 4. An enmetu hook is rewritten only when the recorded launch changed.
|
|
77
|
+
const rendered = renderHookScript(await this.resolvedLaunch());
|
|
81
78
|
if (existing === rendered) {
|
|
82
79
|
return { kind: 'unchanged' };
|
|
83
80
|
}
|
|
@@ -89,14 +86,24 @@ export class PreCommitHookInstaller {
|
|
|
89
86
|
});
|
|
90
87
|
return { kind: existing === null ? 'installed' : 'updated' };
|
|
91
88
|
}
|
|
89
|
+
/** The launch with its interpreter or executable resolved for the record. */
|
|
90
|
+
async resolvedLaunch() {
|
|
91
|
+
if (this.launch.kind === 'script') {
|
|
92
|
+
return {
|
|
93
|
+
kind: 'script',
|
|
94
|
+
interpreter: await this.resolve(this.launch.interpreter),
|
|
95
|
+
script: this.launch.script,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { kind: 'executable', path: await this.resolve(this.launch.path) };
|
|
99
|
+
}
|
|
92
100
|
/**
|
|
93
|
-
* The
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
101
|
+
* The file to record: an absolute name unchanged; for a bare name,
|
|
102
|
+
* <dir>/<name> for the first absolute search path entry where that is an
|
|
103
|
+
* executable file or a link; else the bare name, which the hook looks up
|
|
104
|
+
* on the commit's PATH.
|
|
97
105
|
*/
|
|
98
|
-
async
|
|
99
|
-
const name = this.script.nodePath;
|
|
106
|
+
async resolve(name) {
|
|
100
107
|
if (name.includes('/') || this.searchPath === undefined) {
|
|
101
108
|
return name;
|
|
102
109
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,13 +16,17 @@ function processRuntime() {
|
|
|
16
16
|
argv: [],
|
|
17
17
|
env: process.env,
|
|
18
18
|
cwd: process.cwd(),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
launch: {
|
|
20
|
+
kind: 'script',
|
|
21
|
+
interpreter: process.execPath,
|
|
22
|
+
// The CLI ships next to this file; the hook records it like
|
|
23
|
+
// `enmetu run` records its own argv[1].
|
|
24
|
+
script: fileURLToPath(new URL('./cli.js', import.meta.url)),
|
|
25
|
+
},
|
|
23
26
|
version: packageVersion(),
|
|
24
27
|
platform: process.platform,
|
|
25
28
|
engine: process.versions.bun === undefined ? 'node' : 'bun',
|
|
29
|
+
engineVersion: process.versions.bun ?? process.versions.node,
|
|
26
30
|
stdout: process.stdout,
|
|
27
31
|
stderr: process.stderr,
|
|
28
32
|
};
|
|
@@ -39,11 +39,11 @@ function embeddedRuntime(options, runtime) {
|
|
|
39
39
|
argv: [],
|
|
40
40
|
env: options.env ?? runtime.env,
|
|
41
41
|
cwd: runtime.cwd,
|
|
42
|
-
|
|
43
|
-
cliPath: runtime.cliPath,
|
|
42
|
+
launch: runtime.launch,
|
|
44
43
|
version: runtime.version,
|
|
45
44
|
platform: runtime.platform,
|
|
46
45
|
engine: runtime.engine,
|
|
46
|
+
engineVersion: runtime.engineVersion,
|
|
47
47
|
stdout: runtime.stdout,
|
|
48
48
|
stderr: options.log ?? runtime.stderr,
|
|
49
49
|
};
|
|
@@ -6,7 +6,7 @@ import type { ILogger } from '../contracts/ILogger.js';
|
|
|
6
6
|
import type { IOperationJournal } from '../contracts/IOperationJournal.js';
|
|
7
7
|
import type { IPathQuarantine } from '../contracts/IPathQuarantine.js';
|
|
8
8
|
import type { IOrphanedCommitRecorder } from '../contracts/IStateRecorder.js';
|
|
9
|
-
import type { ConflictKey, PauseReason } from '../contracts/syncTypes.js';
|
|
9
|
+
import type { ConflictKey, HoldPolicy, PauseReason } from '../contracts/syncTypes.js';
|
|
10
10
|
export interface IntegrateUpstreamInput {
|
|
11
11
|
readonly cycleId: string;
|
|
12
12
|
/** The branch the cycle resolved its upstream for. */
|
|
@@ -16,6 +16,8 @@ export interface IntegrateUpstreamInput {
|
|
|
16
16
|
/** Commits the upstream has that HEAD lacks, for the PULLED line. */
|
|
17
17
|
readonly behind: number;
|
|
18
18
|
readonly knownConflict: ConflictKey | null;
|
|
19
|
+
/** Which dirty tracked paths step 2 may hold (design 14.21, Decision 147). */
|
|
20
|
+
readonly hold: HoldPolicy;
|
|
19
21
|
}
|
|
20
22
|
export interface IntegrateUpstreamDeps {
|
|
21
23
|
readonly git: IGitReader;
|
|
@@ -51,6 +53,14 @@ export type IntegrateUpstreamResult = {
|
|
|
51
53
|
} | {
|
|
52
54
|
readonly kind: 'halted';
|
|
53
55
|
readonly code: HaltCode;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Dirty tracked paths the hold policy does not name were found: saves made
|
|
59
|
+
* during the cycle. Nothing was held or rebased (design 14.21, Decision 147).
|
|
60
|
+
*/
|
|
61
|
+
| {
|
|
62
|
+
readonly kind: 'deferred';
|
|
63
|
+
readonly paths: readonly string[];
|
|
54
64
|
};
|
|
55
65
|
/**
|
|
56
66
|
* The integrate step with its deps bound, the shape the sync cycle consumes. The
|
|
@@ -63,7 +73,11 @@ export type IntegrateUpstream = (input: IntegrateUpstreamInput) => Promise<Integ
|
|
|
63
73
|
* paths held aside (design 1.11).
|
|
64
74
|
*
|
|
65
75
|
* 1. head = git.headCommit(); an identical knownConflict logs CONFLICT and returns it (Decision 11).
|
|
66
|
-
* 2. Dirty tracked paths:
|
|
76
|
+
* 2. Dirty tracked paths: any the hold policy does not name is a save made
|
|
77
|
+
* during the cycle, and the step returns deferred with those paths, nothing
|
|
78
|
+
* held or rebased (design 14.21, Decision 147). Otherwise HELD names the
|
|
79
|
+
* paths (Decision 148) and quarantine.hold runs; pending returns busy,
|
|
80
|
+
* unsupported pauses QUARANTINE_UNSUPPORTED.
|
|
67
81
|
* 3. From here on in try/finally:
|
|
68
82
|
* a. git.repositoryState() again: operation pauses FOREIGN_OPERATION, indexLocked returns busy,
|
|
69
83
|
* another branch pauses BRANCH_CHANGED.
|
|
@@ -9,7 +9,11 @@ const SHORT_SHA_LENGTH = 7;
|
|
|
9
9
|
* paths held aside (design 1.11).
|
|
10
10
|
*
|
|
11
11
|
* 1. head = git.headCommit(); an identical knownConflict logs CONFLICT and returns it (Decision 11).
|
|
12
|
-
* 2. Dirty tracked paths:
|
|
12
|
+
* 2. Dirty tracked paths: any the hold policy does not name is a save made
|
|
13
|
+
* during the cycle, and the step returns deferred with those paths, nothing
|
|
14
|
+
* held or rebased (design 14.21, Decision 147). Otherwise HELD names the
|
|
15
|
+
* paths (Decision 148) and quarantine.hold runs; pending returns busy,
|
|
16
|
+
* unsupported pauses QUARANTINE_UNSUPPORTED.
|
|
13
17
|
* 3. From here on in try/finally:
|
|
14
18
|
* a. git.repositoryState() again: operation pauses FOREIGN_OPERATION, indexLocked returns busy,
|
|
15
19
|
* another branch pauses BRANCH_CHANGED.
|
|
@@ -65,8 +69,24 @@ export async function integrateUpstreamController(input, deps) {
|
|
|
65
69
|
entry.worktree === 'D' ||
|
|
66
70
|
entry.worktree === 'T'))
|
|
67
71
|
.map((entry) => entry.path);
|
|
72
|
+
// A dirty path the commit step could have committed is a save that landed
|
|
73
|
+
// since its add -A: holding it would rewrite a file the person is editing
|
|
74
|
+
// (design 14.21). The pull waits for a cycle that commits it first.
|
|
75
|
+
const policy = input.hold;
|
|
76
|
+
const fresh = policy.kind === 'all'
|
|
77
|
+
? []
|
|
78
|
+
: dirty.filter((path) => !policy.paths.includes(path));
|
|
79
|
+
if (fresh.length > 0) {
|
|
80
|
+
return { kind: 'deferred', paths: fresh };
|
|
81
|
+
}
|
|
68
82
|
let held = false;
|
|
69
83
|
if (dirty.length > 0) {
|
|
84
|
+
deps.logger.log({
|
|
85
|
+
level: 'WARN',
|
|
86
|
+
code: 'HELD',
|
|
87
|
+
message: `set aside for the pull, showing the committed version until RESTORED: ${dirty.join(', ')}`,
|
|
88
|
+
cycleId,
|
|
89
|
+
});
|
|
70
90
|
const hold = await deps.quarantine.hold(dirty);
|
|
71
91
|
if (hold.kind === 'pending') {
|
|
72
92
|
return { kind: 'busy' };
|
|
@@ -42,7 +42,9 @@ export interface SyncCycleDeps {
|
|
|
42
42
|
* memory (design 12.7); busy and paused return, committed and nothing go
|
|
43
43
|
* on, waiting goes on and is the outcome unless a later step returns
|
|
44
44
|
* something else. The check right before the commit is the commit step's,
|
|
45
|
-
* under index.lock.
|
|
45
|
+
* under index.lock. The hold policy for step 9 (design 14.21): the commit
|
|
46
|
+
* step's after committed or nothing, all after a hook rejection, nothing
|
|
47
|
+
* after a clean status.
|
|
46
48
|
* 6. Remote check: ls-remote, tracking ref, verified ref (seeded from tracking when missing).
|
|
47
49
|
* 7. Verify and fetch: fetch into the incoming ref when the remote moved; the remote sha
|
|
48
50
|
* must descend from the verified sha, else halt UPSTREAM_REWRITTEN; then move the
|
|
@@ -53,6 +55,12 @@ export interface SyncCycleDeps {
|
|
|
53
55
|
* git.hasStagedChanges() says the real index differs from HEAD (git would
|
|
54
56
|
* refuse the rebase), integrate is skipped, UNPUSHED reported and waiting
|
|
55
57
|
* returned (Decision 84).
|
|
58
|
+
* deferred (a save made during the cycle is dirty): UNPUSHED reported,
|
|
59
|
+
* waiting TREE_CHANGING returned, the refusal count left as it is
|
|
60
|
+
* (Decision 147), and pullDeferred carried to the next cycle: while it is
|
|
61
|
+
* carried, a cycle that committed reports UNPUSHED and returns waiting
|
|
62
|
+
* TREE_CHANGING before integrate; the first cycle that commits nothing
|
|
63
|
+
* pulls and clears it (Decision 162).
|
|
56
64
|
* not-started: the count of consecutive refusals (input.notStartedRebases)
|
|
57
65
|
* goes up by one; the first logs REBASE_NOT_STARTED with git's first stderr
|
|
58
66
|
* line; UNPUSHED is reported; the outcome is busy, or paused REBASE_REFUSED
|