@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,5 +1,5 @@
|
|
|
1
1
|
import { EnmetuError } from '../contracts/errors.js';
|
|
2
|
-
import { EMPTY_COMMIT_MEMORY, } from '../contracts/syncTypes.js';
|
|
2
|
+
import { EMPTY_COMMIT_MEMORY, HOLD_NOTHING, } from '../contracts/syncTypes.js';
|
|
3
3
|
import { withLastMove } from '../core/commit/commitMemory.js';
|
|
4
4
|
import { withCycleId } from '../core/errors/withCycleId.js';
|
|
5
5
|
import { incomingRefFor, verifiedRefFor } from '../core/git/enmetuRefs.js';
|
|
@@ -28,7 +28,9 @@ export const REBASE_REFUSED_AFTER = 3;
|
|
|
28
28
|
* memory (design 12.7); busy and paused return, committed and nothing go
|
|
29
29
|
* on, waiting goes on and is the outcome unless a later step returns
|
|
30
30
|
* something else. The check right before the commit is the commit step's,
|
|
31
|
-
* under index.lock.
|
|
31
|
+
* under index.lock. The hold policy for step 9 (design 14.21): the commit
|
|
32
|
+
* step's after committed or nothing, all after a hook rejection, nothing
|
|
33
|
+
* after a clean status.
|
|
32
34
|
* 6. Remote check: ls-remote, tracking ref, verified ref (seeded from tracking when missing).
|
|
33
35
|
* 7. Verify and fetch: fetch into the incoming ref when the remote moved; the remote sha
|
|
34
36
|
* must descend from the verified sha, else halt UPSTREAM_REWRITTEN; then move the
|
|
@@ -39,6 +41,12 @@ export const REBASE_REFUSED_AFTER = 3;
|
|
|
39
41
|
* git.hasStagedChanges() says the real index differs from HEAD (git would
|
|
40
42
|
* refuse the rebase), integrate is skipped, UNPUSHED reported and waiting
|
|
41
43
|
* returned (Decision 84).
|
|
44
|
+
* deferred (a save made during the cycle is dirty): UNPUSHED reported,
|
|
45
|
+
* waiting TREE_CHANGING returned, the refusal count left as it is
|
|
46
|
+
* (Decision 147), and pullDeferred carried to the next cycle: while it is
|
|
47
|
+
* carried, a cycle that committed reports UNPUSHED and returns waiting
|
|
48
|
+
* TREE_CHANGING before integrate; the first cycle that commits nothing
|
|
49
|
+
* pulls and clears it (Decision 162).
|
|
42
50
|
* not-started: the count of consecutive refusals (input.notStartedRebases)
|
|
43
51
|
* goes up by one; the first logs REBASE_NOT_STARTED with git's first stderr
|
|
44
52
|
* line; UNPUSHED is reported; the outcome is busy, or paused REBASE_REFUSED
|
|
@@ -58,6 +66,7 @@ export async function syncCycleController(input, deps) {
|
|
|
58
66
|
const carried = {
|
|
59
67
|
commitMemory: input.commitMemory,
|
|
60
68
|
notStartedRebases: input.notStartedRebases,
|
|
69
|
+
pullDeferred: input.pullDeferred,
|
|
61
70
|
};
|
|
62
71
|
const outcome = (cycleOutcome) => resultOf(cycleOutcome, null, carried);
|
|
63
72
|
try {
|
|
@@ -115,6 +124,12 @@ async function runCycle(input, deps, carried, outcome) {
|
|
|
115
124
|
// The commit's parent when this cycle committed: step 9 records the pull
|
|
116
125
|
// from there, so the repair covers the commit and the pull in one move.
|
|
117
126
|
let committedFrom = null;
|
|
127
|
+
// Which dirty paths the pull may set aside (design 14.21): after a clean
|
|
128
|
+
// status none, so a path dirty at step 9 is a save made during the cycle.
|
|
129
|
+
let hold = HOLD_NOTHING;
|
|
130
|
+
// Whether this cycle's commit step committed: with a deferred pull carried
|
|
131
|
+
// in, that defers the pull once more (Decision 162).
|
|
132
|
+
let committedThisCycle = false;
|
|
118
133
|
if ((await deps.git.status()).length > 0) {
|
|
119
134
|
// A person's merge, cherry-pick or revert that stopped after step 2 is in
|
|
120
135
|
// that status; the commit step would only find it under index.lock (Decision 52).
|
|
@@ -135,9 +150,18 @@ async function runCycle(input, deps, carried, outcome) {
|
|
|
135
150
|
}
|
|
136
151
|
if (committed.kind === 'waiting') {
|
|
137
152
|
waiting = committed.reason;
|
|
153
|
+
// A rejected commit leaves every dirty path uncommittable; a rejecting
|
|
154
|
+
// hook must not block incoming changes (D4), so the pull holds them all.
|
|
155
|
+
if (committed.reason === 'COMMIT_HOOK_REJECTED') {
|
|
156
|
+
hold = { kind: 'all' };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (committed.kind === 'committed' || committed.kind === 'nothing') {
|
|
160
|
+
hold = committed.hold;
|
|
138
161
|
}
|
|
139
162
|
if (committed.kind === 'committed') {
|
|
140
163
|
committedFrom = committed.memory.lastMove?.from ?? null;
|
|
164
|
+
committedThisCycle = true;
|
|
141
165
|
}
|
|
142
166
|
}
|
|
143
167
|
else if (carried.commitMemory !== undefined) {
|
|
@@ -156,7 +180,10 @@ async function runCycle(input, deps, carried, outcome) {
|
|
|
156
180
|
let verifiedSha = await deps.git.resolveCommit(verifiedRef);
|
|
157
181
|
if (remoteHead.kind === 'unreachable') {
|
|
158
182
|
await reportUnpushedAgainstLastKnown(trackingSha ?? verifiedSha, upstream, cycleId, deps);
|
|
159
|
-
return outcome({
|
|
183
|
+
return outcome({
|
|
184
|
+
kind: 'offline',
|
|
185
|
+
detail: offlineDetail(remoteHead.detail),
|
|
186
|
+
});
|
|
160
187
|
}
|
|
161
188
|
if (remoteHead.kind === 'absent') {
|
|
162
189
|
// A deleted remote branch is never recreated by enmetu, so commits pile
|
|
@@ -175,7 +202,7 @@ async function runCycle(input, deps, carried, outcome) {
|
|
|
175
202
|
const fetched = await deps.writer.fetch(upstream.remote, upstream.remoteRef, incomingRefFor(branchRef));
|
|
176
203
|
if (fetched.kind === 'failed') {
|
|
177
204
|
await reportUnpushedAgainstLastKnown(trackingSha ?? verifiedSha, upstream, cycleId, deps);
|
|
178
|
-
return outcome({ kind: 'offline' });
|
|
205
|
+
return outcome({ kind: 'offline', detail: offlineDetail(fetched.detail) });
|
|
179
206
|
}
|
|
180
207
|
}
|
|
181
208
|
// Checked even when the fetch was skipped: a person's own fetch may already
|
|
@@ -198,6 +225,11 @@ async function runCycle(input, deps, carried, outcome) {
|
|
|
198
225
|
// --- Step 9: integrate when behind ---
|
|
199
226
|
if (behind === 0) {
|
|
200
227
|
carried.notStartedRebases = resetCount(carried.notStartedRebases);
|
|
228
|
+
// A cycle that commits nothing ends a deferral; a committing one keeps
|
|
229
|
+
// it, the writer may still be at work, and pushes as before (Decision 162).
|
|
230
|
+
if (!committedThisCycle) {
|
|
231
|
+
carried.pullDeferred = resetFlag(carried.pullDeferred);
|
|
232
|
+
}
|
|
201
233
|
}
|
|
202
234
|
else {
|
|
203
235
|
// git rebase refuses an index that differs from HEAD (Decision 84). A
|
|
@@ -210,6 +242,16 @@ async function runCycle(input, deps, carried, outcome) {
|
|
|
210
242
|
reportUnpushed(ahead, upstream, cycleId, deps);
|
|
211
243
|
return outcome({ kind: 'waiting', reason: waiting });
|
|
212
244
|
}
|
|
245
|
+
// After a deferral for a save made during a cycle, a cycle that commits
|
|
246
|
+
// defers again; the pull runs in the first cycle whose commit step finds
|
|
247
|
+
// nothing new, as the WAITING line says (Decision 162). A check at one
|
|
248
|
+
// instant is not enough: a file saved every 40 ms is clean right after
|
|
249
|
+
// each commit, and a rebase started then walks it through every replayed
|
|
250
|
+
// version (CodeFloe run 1, 2026-11-08).
|
|
251
|
+
if (carried.pullDeferred === true && committedThisCycle) {
|
|
252
|
+
reportUnpushed(ahead, upstream, cycleId, deps);
|
|
253
|
+
return outcome({ kind: 'waiting', reason: 'TREE_CHANGING' });
|
|
254
|
+
}
|
|
213
255
|
const pulledFrom = committedFrom ?? (await deps.git.headCommit());
|
|
214
256
|
const integrated = await deps.integrate({
|
|
215
257
|
cycleId,
|
|
@@ -217,7 +259,16 @@ async function runCycle(input, deps, carried, outcome) {
|
|
|
217
259
|
ontoSha: remoteSha,
|
|
218
260
|
behind,
|
|
219
261
|
knownConflict: input.knownConflict,
|
|
262
|
+
hold,
|
|
220
263
|
});
|
|
264
|
+
if (integrated.kind === 'deferred') {
|
|
265
|
+
// The next cycle commits the save first and pulls only once a cycle
|
|
266
|
+
// commits nothing (Decision 162); the runner logs WAITING once.
|
|
267
|
+
carried.pullDeferred = true;
|
|
268
|
+
reportUnpushed(ahead, upstream, cycleId, deps);
|
|
269
|
+
return outcome({ kind: 'waiting', reason: 'TREE_CHANGING' });
|
|
270
|
+
}
|
|
271
|
+
carried.pullDeferred = resetFlag(carried.pullDeferred);
|
|
221
272
|
if (integrated.kind === 'not-started') {
|
|
222
273
|
const refusals = (carried.notStartedRebases ?? 0) + 1;
|
|
223
274
|
carried.notStartedRebases = refusals;
|
|
@@ -296,8 +347,13 @@ function resultOf(cycleOutcome, conflict, carried) {
|
|
|
296
347
|
conflict,
|
|
297
348
|
commitMemory: carried.commitMemory,
|
|
298
349
|
notStartedRebases: carried.notStartedRebases,
|
|
350
|
+
pullDeferred: carried.pullDeferred,
|
|
299
351
|
};
|
|
300
352
|
}
|
|
353
|
+
/** A flag the input carried goes back to false; an absent one stays absent, which means false. */
|
|
354
|
+
function resetFlag(flag) {
|
|
355
|
+
return flag === undefined ? undefined : false;
|
|
356
|
+
}
|
|
301
357
|
/** A count the input carried goes back to 0; an absent one stays absent, which means 0. */
|
|
302
358
|
function resetCount(count) {
|
|
303
359
|
return count === undefined ? undefined : 0;
|
|
@@ -330,6 +386,15 @@ function reportUnpushed(ahead, upstream, cycleId, deps) {
|
|
|
330
386
|
* observation of the remote; the verified ref covers a tracking ref that
|
|
331
387
|
* `git fetch --prune` removed after the remote branch was deleted.
|
|
332
388
|
*/
|
|
389
|
+
/**
|
|
390
|
+
* What git said, for the OFFLINE line (Decision 159): its first line is ssh's
|
|
391
|
+
* "Permission denied (publickey)." or "Connection refused", or the adapter's
|
|
392
|
+
* timeout message; the rest is git's generic advice.
|
|
393
|
+
*/
|
|
394
|
+
function offlineDetail(detail) {
|
|
395
|
+
const line = firstLine(detail);
|
|
396
|
+
return line === '' ? 'no detail from git' : line;
|
|
397
|
+
}
|
|
333
398
|
async function reportUnpushedAgainstLastKnown(lastKnownSha, upstream, cycleId, deps) {
|
|
334
399
|
if (lastKnownSha === null) {
|
|
335
400
|
deps.logger.log({
|
|
@@ -3,6 +3,7 @@ import { errorMessage } from '../core/errors/errorMessage.js';
|
|
|
3
3
|
const WAITING_MESSAGES = {
|
|
4
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
5
|
COMMIT_HOOK_REJECTED: 'waiting: a commit hook rejected the commit; enmetu tries again when the files change',
|
|
6
|
+
TREE_CHANGING: 'waiting: a file was saved while this cycle ran; the pull waits for a cycle that finds nothing new to commit, so no file you are editing is set aside',
|
|
6
7
|
};
|
|
7
8
|
/**
|
|
8
9
|
* Service: the memory that spans cycles. Numbers cycles c1, c2, ...; passes
|
|
@@ -33,10 +34,12 @@ export class SyncCycleRunner {
|
|
|
33
34
|
this.knownConflict = result.conflict;
|
|
34
35
|
if (this.carried !== null ||
|
|
35
36
|
result.commitMemory !== undefined ||
|
|
36
|
-
result.notStartedRebases !== undefined
|
|
37
|
+
result.notStartedRebases !== undefined ||
|
|
38
|
+
result.pullDeferred !== undefined) {
|
|
37
39
|
this.carried = {
|
|
38
40
|
commitMemory: result.commitMemory ?? EMPTY_COMMIT_MEMORY,
|
|
39
41
|
notStartedRebases: result.notStartedRebases ?? 0,
|
|
42
|
+
pullDeferred: result.pullDeferred ?? false,
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
this.reportQuietTransition(result.outcome, cycleId);
|
|
@@ -64,6 +67,7 @@ export class SyncCycleRunner {
|
|
|
64
67
|
knownConflict: this.knownConflict,
|
|
65
68
|
commitMemory: this.carried.commitMemory,
|
|
66
69
|
notStartedRebases: this.carried.notStartedRebases,
|
|
70
|
+
pullDeferred: this.carried.pullDeferred,
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
reportQuietTransition(outcome, cycleId) {
|
|
@@ -132,7 +136,7 @@ function quietEntry(outcome, cycleId) {
|
|
|
132
136
|
return {
|
|
133
137
|
level: 'WARN',
|
|
134
138
|
code: 'OFFLINE',
|
|
135
|
-
message:
|
|
139
|
+
message: `remote unreachable (${outcome.detail}); retrying on every poll`,
|
|
136
140
|
cycleId,
|
|
137
141
|
};
|
|
138
142
|
case 'waiting':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluado/enmetu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Keeps one git working tree committed and in sync with its upstream.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "fluado UG (haftungsbeschränkt)",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=24"
|
|
23
|
+
"node": ">=24",
|
|
24
|
+
"bun": ">=1.4.4"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@biomejs/biome": "2.3.8",
|