@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
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center"><sub>/enˈme.tu/, Esperanto for "commit!"</sub></p>
|
|
8
8
|
|
|
9
|
+
<p align="center"><a href="https://www.npmjs.com/package/@fluado/enmetu"><img src="https://img.shields.io/npm/v/@fluado/enmetu?label=release&color=blue" alt="latest release"></a></p>
|
|
10
|
+
|
|
9
11
|
Commits and syncs a git working tree automatically. Run it inside a repository: it commits after you stop editing, rebases onto the upstream, pushes, and pulls new commits.
|
|
10
12
|
|
|
11
13
|
## Why enmetu
|
|
@@ -14,45 +16,45 @@ Most auto-commit tools commit and push. enmetu also handles what goes wrong:
|
|
|
14
16
|
|
|
15
17
|
- **Conflicts.** A pull that conflicts is undone and held. Your commits stay local, and enmetu retries when either side changes. It never commits conflict markers and never force-pushes.
|
|
16
18
|
- **No stash.** A file it won't commit (conflict markers, invalid JSON) stays on disk and doesn't block the rest of the sync. Its pre-commit hook applies the same check to your own commits.
|
|
17
|
-
- **Plain git.** You and your agents keep using git. enmetu commits through its own index, waits up to 5 minutes on a partial staging,
|
|
19
|
+
- **Plain git.** You and your agents keep using git. enmetu commits through its own index, waits up to 5 minutes on a partial staging, pauses during your merge or rebase, and never pulls over a file you saved since its last commit: it commits the save first, then pulls.
|
|
18
20
|
- **Halts.** A rewritten upstream, or a state it cannot verify, halts it. The halt survives restarts until you run `enmetu resume`.
|
|
19
21
|
- **Status.** `enmetu status` says whether your work is committed and pushed, and during a conflict prints the commands to end it.
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
+
- **Commit messages.** Can be generated by a script that gets the diff, e.g. with an LLM. Falls back to a default message.
|
|
23
|
+
- **Dependencies.** None besides git. Runs as an executable, under Node or Bun, or as a library.
|
|
24
|
+
|
|
25
|
+
All of it is tested with real git against [35 conflict shapes](https://codefloe.com/fluado/enmetu/src/branch/main/src/__tests__/conflictMatrix.scenario.integration.test.ts) (renames against deletions, binary files, case-only renames, line endings, a merge in progress), [several clones converging on one history](https://codefloe.com/fluado/enmetu/src/branch/main/src/__tests__/convergence.scenario.integration.test.ts), and [a stress test](https://codefloe.com/fluado/enmetu/src/branch/main/src/__tests__/concurrencyStress.scenario.integration.test.ts) where a scripted person runs git next to enmetu as fast as it can, with every write checked against the final history.
|
|
22
26
|
|
|
23
27
|
## Prerequisites
|
|
24
28
|
|
|
25
|
-
- Node 24+ or Bun 1.4.4+
|
|
26
|
-
- git 2.
|
|
29
|
+
- The standalone executable, or Node 24+ or Bun 1.4.4+
|
|
30
|
+
- git 2.39+
|
|
27
31
|
- A checked-out branch with an upstream, at least one commit, and a committer identity
|
|
28
32
|
|
|
29
33
|
## Install
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Inside a repository:
|
|
35
|
+
Executable, macOS or Linux:
|
|
34
36
|
|
|
35
37
|
```sh
|
|
36
|
-
|
|
38
|
+
curl -fsSL https://codefloe.com/fluado/enmetu/raw/branch/main/install.sh | sh
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
Node:
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
```sh
|
|
44
|
+
npx @fluado/enmetu run
|
|
45
|
+
```
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
Bun:
|
|
44
48
|
|
|
45
49
|
```sh
|
|
46
50
|
bunx --bun @fluado/enmetu run
|
|
47
51
|
```
|
|
48
52
|
|
|
49
|
-
As a project dependency: `bun add -d @fluado/enmetu`, then `bunx --bun enmetu run`.
|
|
50
|
-
|
|
51
53
|
Keep `--bun`: without it, `bunx` runs enmetu with Node when Node is installed.
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
As a dependency: `npm install -D @fluado/enmetu` or `bun add -d @fluado/enmetu`, then `npx enmetu run` or `bunx --bun enmetu run`.
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
Pin a version with `@fluado/enmetu@0.5.0`, or `ENMETU_VERSION=v0.5.0` for the installer. The binaries are also on the [releases page](https://codefloe.com/fluado/enmetu/releases).
|
|
56
58
|
|
|
57
59
|
## Commands
|
|
58
60
|
|
|
@@ -94,7 +96,7 @@ startEnmetu({
|
|
|
94
96
|
})
|
|
95
97
|
```
|
|
96
98
|
|
|
97
|
-
- `git diff-tree -r <parent> <tree>` in `root` lists the change.
|
|
99
|
+
- `git diff-tree -r -M <parent> <tree>` in `root` lists the change.
|
|
98
100
|
- Return the message, its first line the subject, or `null` for `commitSubject`.
|
|
99
101
|
- `runCommand()` runs the person's `commitMessageCommand` on this change, at most once, and gives its message or `null`. With `commitMessage` set, the command runs only through this call.
|
|
100
102
|
- 10 seconds, then `signal` aborts and enmetu commits with `commitSubject`. So does a throw, or text without a subject; the log says `MESSAGE_WRITER_FAILED`.
|
|
@@ -190,6 +192,7 @@ stderr, one line per event:
|
|
|
190
192
|
| `EXCLUDED` | file not committed: conflict markers or invalid JSON |
|
|
191
193
|
| `CONFLICT`, `UNPUSHED` | local commits not pushed |
|
|
192
194
|
| `WAITING`, `PAUSED`, `OFFLINE` | sync on hold |
|
|
195
|
+
| `HELD`, `RESTORED` | a file enmetu could not commit was set aside for a pull and put back; see Known limitations |
|
|
193
196
|
| `HALTED` | see `enmetu status` |
|
|
194
197
|
| `MESSAGE_COMMAND_FAILED`, `MESSAGE_WRITER_FAILED` | message from the fallback named in the line |
|
|
195
198
|
| `COMMIT_ORPHANED`, `STAGED_RESET`, `STAGED_REVERT`, `QUARANTINE_KEPT` | see Known limitations |
|
|
@@ -231,23 +234,26 @@ if command -v enmetu >/dev/null 2>&1; then enmetu hook pre-commit || exit 1; els
|
|
|
231
234
|
- `commit-msg` and `prepare-commit-msg` hooks don't run on enmetu's commits; `commitMessageCommand` writes the message instead.
|
|
232
235
|
- Under Bun without file-watching permission, enmetu commits on its poll interval.
|
|
233
236
|
- Under Node on Linux, enmetu does not watch files: from Node 24.22, Node's recursive watch there can end the process ([nodejs/node#66791](https://github.com/nodejs/node/issues/66791)). enmetu commits on its poll interval (`pollMs`, 5 seconds by default) and says so once at start (`WATCHER_FAILED`). Bun watches.
|
|
234
|
-
- A
|
|
237
|
+
- A file written faster than a cycle runs keeps the pull waiting (`WAITING` with `TREE_CHANGING`) until it stops.
|
|
238
|
+
- A file enmetu cannot commit (invalid JSON, conflict markers, a rejected `pre-commit`) is set aside during a pull and shows the committed version from `HELD` until `RESTORED`, about 0.1 to 0.5 seconds. A save made meanwhile is merged back, or kept as a blob named in `QUARANTINE_KEPT` when it does not merge.
|
|
239
|
+
- A save landing in the milliseconds before git rewrites a file, inside a pull or its abort, is lost without a record.
|
|
235
240
|
|
|
236
241
|
Overlapping git commands:
|
|
237
242
|
|
|
238
243
|
| Symptom | Cause | Fix |
|
|
239
244
|
|---|---|---|
|
|
245
|
+
| a file briefly showed old content | `HELD`: enmetu set it aside for a pull | wait for `RESTORED`; a `QUARANTINE_KEPT` line names a blob: `git cat-file -p <id>` |
|
|
240
246
|
| `cannot lock ref 'HEAD'` | raced enmetu's commit | after `INDEX_REPAIRED` in the log: `git reset --merge`, retry. If `not uptodate`: `git merge --quit`, `git reset` |
|
|
241
247
|
| `STAGED_RESET` | staged during enmetu's rebase | command in the log line |
|
|
242
248
|
| `QUARANTINE_KEPT` | your edit collided with a version enmetu held aside | command in the log line |
|
|
243
249
|
| `COMMIT_ORPHANED` | committed during enmetu's pull | `git cherry-pick -x` command in `enmetu status` |
|
|
244
250
|
| `STAGED_REVERT` | a refused `git merge` restored old content | `git restore` command in the log line |
|
|
245
251
|
| `could not write index` on a refused merge, or with older git (2.39) your edits gone after `would be overwritten by merge` | git stashed your edits | `git fsck --no-reflogs --unreachable`, then `git stash apply <id>` |
|
|
246
|
-
| `git rebase` fails, no rebase in progress | started together with enmetu's | `git rebase --quit` if needed, `git reset --merge
|
|
252
|
+
| `git rebase` fails, no rebase in progress | started together with enmetu's | `git rebase --quit` if needed, `git reset --merge` if files are unmerged, `git checkout -m <branch>` (keeps your edits), retry |
|
|
247
253
|
|
|
248
254
|
## Contributing
|
|
249
255
|
|
|
250
|
-
We built enmetu for ourselves. We use it every day at [fluado](https://fluado.com) and we're sharing it because it might be useful to others.
|
|
256
|
+
We built enmetu for ourselves. We use it every day at [fluado](https://fluado.com) and we're sharing it because it might be useful to others. It is the sync engine of [faru](https://github.com/fluado/faru), our git-native kanban board.
|
|
251
257
|
|
|
252
258
|
This is not a community project. We don't have the bandwidth to review PRs, triage issues, or maintain a roadmap for external contributors. If you open an issue with a good idea, we might pick it up when it aligns with what we need. No promises.
|
|
253
259
|
|
|
@@ -9,11 +9,6 @@ import type { IGitWriter } from '../../contracts/IGitWriter.js';
|
|
|
9
9
|
export declare const GIT_BATCH_CHUNK_BYTES = 67108864;
|
|
10
10
|
/** A hook's stdout and stderr beyond this are dropped, never a failure (design 12.7, Decision 83). */
|
|
11
11
|
export declare const HOOK_OUTPUT_CAP_BYTES = 65536;
|
|
12
|
-
/**
|
|
13
|
-
* Facade: typed questions and commands over the git CLI, with exactly the
|
|
14
|
-
* argv and interpretations of design section 1.3. gitDir locates the state
|
|
15
|
-
* files repositoryState and the rebase readers check.
|
|
16
|
-
*/
|
|
17
12
|
export declare class GitFacade implements IGitReader, IGitWriter, IGitCommitter {
|
|
18
13
|
private readonly runner;
|
|
19
14
|
private readonly fs;
|
|
@@ -135,7 +130,9 @@ export declare class GitFacade implements IGitReader, IGitWriter, IGitCommitter
|
|
|
135
130
|
* Runs a command that talks to the remote. A timeout there is the network or
|
|
136
131
|
* a credential prompt (ssh connect, passphrase, pinentry), so it comes back as
|
|
137
132
|
* a value the cycle reports (OFFLINE, or PUSH_FAILED with UNPUSHED, D2),
|
|
138
|
-
* never an error that ends the cycle in CYCLE_FAILED.
|
|
133
|
+
* never an error that ends the cycle in CYCLE_FAILED. The command leads its
|
|
134
|
+
* own process group, so the timeout ends the ssh it started too (Decision
|
|
135
|
+
* 158); killed alone, git would leave ssh waiting on the dead connection.
|
|
139
136
|
*/
|
|
140
137
|
private remoteGit;
|
|
141
138
|
/** Runs git and expects exit code 0. */
|
|
@@ -54,6 +54,8 @@ const OPERATION_MARKERS = [
|
|
|
54
54
|
* argv and interpretations of design section 1.3. gitDir locates the state
|
|
55
55
|
* files repositoryState and the rebase readers check.
|
|
56
56
|
*/
|
|
57
|
+
/** How the three remote commands run (Decision 158). */
|
|
58
|
+
const REMOTE_RUN = { killGroup: true };
|
|
57
59
|
export class GitFacade {
|
|
58
60
|
runner;
|
|
59
61
|
fs;
|
|
@@ -653,11 +655,13 @@ export class GitFacade {
|
|
|
653
655
|
* Runs a command that talks to the remote. A timeout there is the network or
|
|
654
656
|
* a credential prompt (ssh connect, passphrase, pinentry), so it comes back as
|
|
655
657
|
* a value the cycle reports (OFFLINE, or PUSH_FAILED with UNPUSHED, D2),
|
|
656
|
-
* never an error that ends the cycle in CYCLE_FAILED.
|
|
658
|
+
* never an error that ends the cycle in CYCLE_FAILED. The command leads its
|
|
659
|
+
* own process group, so the timeout ends the ssh it started too (Decision
|
|
660
|
+
* 158); killed alone, git would leave ssh waiting on the dead connection.
|
|
657
661
|
*/
|
|
658
662
|
async remoteGit(args) {
|
|
659
663
|
try {
|
|
660
|
-
return { kind: 'ran', result: await this.git(args) };
|
|
664
|
+
return { kind: 'ran', result: await this.git(args, REMOTE_RUN) };
|
|
661
665
|
}
|
|
662
666
|
catch (error) {
|
|
663
667
|
if (error instanceof GitCommandError && error.code === 'GIT_TIMEOUT') {
|
|
@@ -40,7 +40,8 @@ export class GitProcessAdapter {
|
|
|
40
40
|
const env = callEnvironment(this.env, options?.reflogAction);
|
|
41
41
|
if (options?.timeoutMs !== undefined ||
|
|
42
42
|
options?.outputCapBytes !== undefined ||
|
|
43
|
-
options?.abort !== undefined
|
|
43
|
+
options?.abort !== undefined ||
|
|
44
|
+
options?.killGroup === true) {
|
|
44
45
|
return this.runInGroup(argv, env, options);
|
|
45
46
|
}
|
|
46
47
|
return new Promise((resolve, reject) => {
|
|
@@ -3,13 +3,20 @@ import { type ExitCode } from '../../contracts/exitCode.js';
|
|
|
3
3
|
import type { IDaemon } from '../../contracts/IDaemon.js';
|
|
4
4
|
import type { IFileSystem } from '../../contracts/IFileSystem.js';
|
|
5
5
|
import type { ILogger } from '../../contracts/ILogger.js';
|
|
6
|
-
import type { IRepositoryLocator, RepositoryPaths } from '../../contracts/IRepositoryLocator.js';
|
|
6
|
+
import type { GitVersion, IRepositoryLocator, RepositoryPaths } from '../../contracts/IRepositoryLocator.js';
|
|
7
7
|
export interface RunCommandInput {
|
|
8
8
|
/** --repo, else the working directory. */
|
|
9
9
|
readonly startDir: string;
|
|
10
10
|
}
|
|
11
|
+
/** The JavaScript runtime enmetu runs on and its version (design 14.25). */
|
|
12
|
+
export interface Engine {
|
|
13
|
+
readonly kind: 'node' | 'bun';
|
|
14
|
+
readonly version: string;
|
|
15
|
+
}
|
|
11
16
|
export interface RunCommandDeps {
|
|
12
17
|
readonly locator: IRepositoryLocator;
|
|
18
|
+
/** Checked first: an engine below the floor exits 5 before any git call (Decision 160). */
|
|
19
|
+
readonly engine: Engine;
|
|
13
20
|
readonly fs: IFileSystem;
|
|
14
21
|
/** Every enmetu.* entry of git config for the resolved repository (design 14.18). */
|
|
15
22
|
readonly readGitConfig: (paths: RepositoryPaths) => Promise<readonly GitConfigEntry[]>;
|
|
@@ -20,15 +27,30 @@ export interface RunCommandDeps {
|
|
|
20
27
|
export interface RunCommandResult {
|
|
21
28
|
readonly exitCode: ExitCode;
|
|
22
29
|
}
|
|
23
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Minimum git version: 2.39, the oldest git the suites run on (Debian bookworm,
|
|
32
|
+
* Decision 156). The feature floor is 2.38, where --no-update-refs appeared
|
|
33
|
+
* (Decision 16); no older git has run the suites, so none is promised.
|
|
34
|
+
*/
|
|
24
35
|
export declare const MIN_GIT_VERSION: {
|
|
25
36
|
major: number;
|
|
26
37
|
minor: number;
|
|
27
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Minimum engine versions (design 14.25, Decision 160): Node 24 is the
|
|
41
|
+
* package's engines field; Bun 1.4.4 is measured, 1.3.6 drops the watch event
|
|
42
|
+
* for a file git merge rewrites (Decision 128). Shown as the README states them.
|
|
43
|
+
*/
|
|
44
|
+
export declare const MIN_ENGINE_VERSION: Readonly<Record<Engine['kind'], {
|
|
45
|
+
readonly floor: GitVersion;
|
|
46
|
+
readonly shown: string;
|
|
47
|
+
}>>;
|
|
28
48
|
/**
|
|
29
49
|
* Controller:
|
|
50
|
+
* 0. The engine below its floor: NOT_ELIGIBLE (ENGINE_TOO_OLD), exit 5, before
|
|
51
|
+
* any git call (design 14.25); a version that does not parse passes.
|
|
30
52
|
* 1. locator.locate(startDir): not a repository, bare, or PATH_UNSUPPORTED log NOT_ELIGIBLE, exit 5.
|
|
31
|
-
* 2. Git older than 2.
|
|
53
|
+
* 2. Git older than 2.39: NOT_ELIGIBLE (GIT_TOO_OLD), exit 5.
|
|
32
54
|
* 3. parseConfig of <root>/enmetu.config.json and readGitConfig(paths), git
|
|
33
55
|
* config overriding the file (design 14.18): ConfigError logs
|
|
34
56
|
* CONFIG_INVALID naming the file or the git config key and scope, exit 2;
|
|
@@ -2,12 +2,27 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { ConfigError, PreflightError, } from '../../contracts/errors.js';
|
|
3
3
|
import { EXIT } from '../../contracts/exitCode.js';
|
|
4
4
|
import { CONFIG_FILE_NAME, parseConfig } from '../../core/config/parseConfig.js';
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Minimum git version: 2.39, the oldest git the suites run on (Debian bookworm,
|
|
7
|
+
* Decision 156). The feature floor is 2.38, where --no-update-refs appeared
|
|
8
|
+
* (Decision 16); no older git has run the suites, so none is promised.
|
|
9
|
+
*/
|
|
10
|
+
export const MIN_GIT_VERSION = { major: 2, minor: 39 };
|
|
11
|
+
/**
|
|
12
|
+
* Minimum engine versions (design 14.25, Decision 160): Node 24 is the
|
|
13
|
+
* package's engines field; Bun 1.4.4 is measured, 1.3.6 drops the watch event
|
|
14
|
+
* for a file git merge rewrites (Decision 128). Shown as the README states them.
|
|
15
|
+
*/
|
|
16
|
+
export const MIN_ENGINE_VERSION = {
|
|
17
|
+
node: { floor: { major: 24, minor: 0, patch: 0 }, shown: '24' },
|
|
18
|
+
bun: { floor: { major: 1, minor: 4, patch: 4 }, shown: '1.4.4' },
|
|
19
|
+
};
|
|
7
20
|
/**
|
|
8
21
|
* Controller:
|
|
22
|
+
* 0. The engine below its floor: NOT_ELIGIBLE (ENGINE_TOO_OLD), exit 5, before
|
|
23
|
+
* any git call (design 14.25); a version that does not parse passes.
|
|
9
24
|
* 1. locator.locate(startDir): not a repository, bare, or PATH_UNSUPPORTED log NOT_ELIGIBLE, exit 5.
|
|
10
|
-
* 2. Git older than 2.
|
|
25
|
+
* 2. Git older than 2.39: NOT_ELIGIBLE (GIT_TOO_OLD), exit 5.
|
|
11
26
|
* 3. parseConfig of <root>/enmetu.config.json and readGitConfig(paths), git
|
|
12
27
|
* config overriding the file (design 14.18): ConfigError logs
|
|
13
28
|
* CONFIG_INVALID naming the file or the git config key and scope, exit 2;
|
|
@@ -18,6 +33,12 @@ export const MIN_GIT_VERSION = { major: 2, minor: 38 };
|
|
|
18
33
|
* 4. createDaemon(paths, config).run(), its exit code returned.
|
|
19
34
|
*/
|
|
20
35
|
export async function runCommand(input, deps) {
|
|
36
|
+
// --- Step 0: the engine floor (design 14.25) ---
|
|
37
|
+
const engineVersion = parseVersion(deps.engine.version);
|
|
38
|
+
const minimum = MIN_ENGINE_VERSION[deps.engine.kind];
|
|
39
|
+
if (engineVersion !== null && olderThan(engineVersion, minimum.floor)) {
|
|
40
|
+
return notEligible(deps, 'ENGINE_TOO_OLD', `${deps.engine.kind} ${deps.engine.version} is older than ${minimum.shown}`);
|
|
41
|
+
}
|
|
21
42
|
// --- Step 1: the repository root through git, not the start directory (D9) ---
|
|
22
43
|
let location;
|
|
23
44
|
try {
|
|
@@ -101,9 +122,25 @@ function logConfigSources(parsed, logger) {
|
|
|
101
122
|
}
|
|
102
123
|
}
|
|
103
124
|
function olderThanMinimum(version) {
|
|
104
|
-
return (version
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
return olderThan(version, { ...MIN_GIT_VERSION, patch: 0 });
|
|
126
|
+
}
|
|
127
|
+
function olderThan(version, floor) {
|
|
128
|
+
if (version.major !== floor.major)
|
|
129
|
+
return version.major < floor.major;
|
|
130
|
+
if (version.minor !== floor.minor)
|
|
131
|
+
return version.minor < floor.minor;
|
|
132
|
+
return version.patch < floor.patch;
|
|
133
|
+
}
|
|
134
|
+
/** `major.minor.patch` at the start of the text (a label may follow); null when absent. */
|
|
135
|
+
function parseVersion(text) {
|
|
136
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
137
|
+
if (match === null)
|
|
138
|
+
return null;
|
|
139
|
+
return {
|
|
140
|
+
major: Number(match[1]),
|
|
141
|
+
minor: Number(match[2]),
|
|
142
|
+
patch: Number(match[3]),
|
|
143
|
+
};
|
|
107
144
|
}
|
|
108
145
|
function notEligible(deps, code, message) {
|
|
109
146
|
deps.logger.log({
|
package/dist/cli/exitCodes.js
CHANGED
|
@@ -10,6 +10,7 @@ const EXIT_CODE_BY_ERROR = {
|
|
|
10
10
|
LOCK_UNREADABLE: EXIT.LOCKED,
|
|
11
11
|
GIT_NOT_FOUND: EXIT.NOT_ELIGIBLE,
|
|
12
12
|
GIT_TOO_OLD: EXIT.NOT_ELIGIBLE,
|
|
13
|
+
ENGINE_TOO_OLD: EXIT.NOT_ELIGIBLE,
|
|
13
14
|
NOT_A_REPOSITORY: EXIT.NOT_ELIGIBLE,
|
|
14
15
|
BARE_REPOSITORY: EXIT.NOT_ELIGIBLE,
|
|
15
16
|
PATH_UNSUPPORTED: EXIT.NOT_ELIGIBLE,
|
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { main } from './cli/main.js';
|
|
4
4
|
/**
|
|
5
|
-
* Entry
|
|
6
|
-
* process.
|
|
5
|
+
* Entry of the package's CLI: with src/index.ts and src/standalone.ts one of
|
|
6
|
+
* the three files that read process.argv, process.env, process.cwd(),
|
|
7
|
+
* process.execPath, process.stdout and process.stderr and set
|
|
7
8
|
* process.exitCode. It gathers them into a Runtime and calls main.
|
|
8
9
|
*/
|
|
9
10
|
/** package.json sits next to dist/ and src/, one level up from this file in both. */
|
|
@@ -21,13 +22,18 @@ const runtime = {
|
|
|
21
22
|
argv: process.argv.slice(2),
|
|
22
23
|
env: process.env,
|
|
23
24
|
cwd: process.cwd(),
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
launch: {
|
|
26
|
+
kind: 'script',
|
|
27
|
+
interpreter: process.execPath,
|
|
28
|
+
// Node makes argv[1] absolute. Should it ever be missing, the hook
|
|
29
|
+
// records an empty path, finds no CLI there and lets commits through
|
|
30
|
+
// (fail open).
|
|
31
|
+
script: process.argv[1] ?? '',
|
|
32
|
+
},
|
|
28
33
|
version: packageVersion(),
|
|
29
34
|
platform: process.platform,
|
|
30
35
|
engine: process.versions.bun === undefined ? 'node' : 'bun',
|
|
36
|
+
engineVersion: process.versions.bun ?? process.versions.node,
|
|
31
37
|
stdout: process.stdout,
|
|
32
38
|
stderr: process.stderr,
|
|
33
39
|
};
|
|
@@ -7,7 +7,7 @@ import type { IGitReader } from '../contracts/IGitReader.js';
|
|
|
7
7
|
import type { IGitWriter } from '../contracts/IGitWriter.js';
|
|
8
8
|
import type { IHostMessageWriter } from '../contracts/IHostMessageWriter.js';
|
|
9
9
|
import type { ILogger } from '../contracts/ILogger.js';
|
|
10
|
-
import type { CommitMemory, WaitReason } from '../contracts/syncTypes.js';
|
|
10
|
+
import type { CommitMemory, HoldPolicy, WaitReason } from '../contracts/syncTypes.js';
|
|
11
11
|
/** An unchanged tree a hook rejected is offered to the hooks again after this long (design 12.7). */
|
|
12
12
|
export declare const HOOK_RETRY_MS = 600000;
|
|
13
13
|
/** commitMessageCommand reads at most this much of the patch on stdin (design 14.18). */
|
|
@@ -66,11 +66,19 @@ export interface CommitLocalChangesDeps {
|
|
|
66
66
|
readonly clock: IClock;
|
|
67
67
|
readonly settings: CommitSettings;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* hold on nothing and committed (design 14.21, Decision 147): the dirty paths
|
|
71
|
+
* a pull may set aside, which are the ones this step could not commit. `only`
|
|
72
|
+
* names the paths the content guard excluded from the build whose tree was
|
|
73
|
+
* committed or found empty; `all` says every dirty path is the hooks' residue.
|
|
74
|
+
*/
|
|
69
75
|
export type CommitLocalChangesResult = {
|
|
70
76
|
readonly kind: 'nothing';
|
|
77
|
+
readonly hold: HoldPolicy;
|
|
71
78
|
readonly memory: CommitMemory;
|
|
72
79
|
} | {
|
|
73
80
|
readonly kind: 'committed';
|
|
81
|
+
readonly hold: HoldPolicy;
|
|
74
82
|
readonly memory: CommitMemory;
|
|
75
83
|
} | {
|
|
76
84
|
readonly kind: 'waiting';
|
|
@@ -108,15 +116,17 @@ export type CommitLocalChanges = (input: CommitLocalChangesInput) => Promise<Com
|
|
|
108
116
|
* SELECTION_WAIT_ENDED and goes on; 0 goes on at once (design 14.17).
|
|
109
117
|
* 2. Build: parent = git.headCommit(); fs.copy(realIndex, commit index);
|
|
110
118
|
* committer.seedFrom(parent); stageGuardedChanges on the commit index.
|
|
111
|
-
* Nothing staged returns nothing.
|
|
112
|
-
* 3. tree = committer.writeTree(); equal to memory.residueTree returns nothing
|
|
113
|
-
*
|
|
119
|
+
* Nothing staged returns nothing, hold only the excluded paths (14.21).
|
|
120
|
+
* 3. tree = committer.writeTree(); equal to memory.residueTree returns nothing
|
|
121
|
+
* with hold all (the residue is every dirty path); equal to
|
|
122
|
+
* memory.rejection.tree within HOOK_RETRY_MS returns waiting
|
|
114
123
|
* COMMIT_HOOK_REJECTED without running a hook.
|
|
115
124
|
* 4. commitHooks: pre-commit on the commit index (rejected, timed out or
|
|
116
125
|
* killed: HOOK_REJECTED, waiting, rejection remembered); then re-stage and
|
|
117
126
|
* write the tree again, so a formatter's rewrite is in this commit. The
|
|
118
127
|
* message hooks do not run (Decision 80). The re-stage logs through
|
|
119
|
-
* residueLogger: its EXCLUDED lines repeat the first build's
|
|
128
|
+
* residueLogger: its EXCLUDED lines repeat the first build's; its excluded
|
|
129
|
+
* paths are the ones committed's hold names (14.21).
|
|
120
130
|
* 4b. The message (design 14.18, 14.19), before index.lock, so no writer
|
|
121
131
|
* ever holds up a person's git. The command's message: when
|
|
122
132
|
* commitMessageCommand is set, messageCommand runs with
|
|
@@ -2,7 +2,7 @@ import { EnmetuError, GitCommandError } from '../contracts/errors.js';
|
|
|
2
2
|
import { withLastMove } from '../core/commit/commitMemory.js';
|
|
3
3
|
import { gitBoolean, messageFromText } from '../core/commit/commitMessage.js';
|
|
4
4
|
import { repairStaleIndexController } from './repairStaleIndexController.js';
|
|
5
|
-
import { stageGuardedChanges } from './stageGuardedChanges.js';
|
|
5
|
+
import { stageGuardedChanges, } from './stageGuardedChanges.js';
|
|
6
6
|
/** An unchanged tree a hook rejected is offered to the hooks again after this long (design 12.7). */
|
|
7
7
|
export const HOOK_RETRY_MS = 600000;
|
|
8
8
|
/** commitMessageCommand reads at most this much of the patch on stdin (design 14.18). */
|
|
@@ -23,15 +23,17 @@ export const MESSAGE_PATCH_CAP_BYTES = 65536;
|
|
|
23
23
|
* SELECTION_WAIT_ENDED and goes on; 0 goes on at once (design 14.17).
|
|
24
24
|
* 2. Build: parent = git.headCommit(); fs.copy(realIndex, commit index);
|
|
25
25
|
* committer.seedFrom(parent); stageGuardedChanges on the commit index.
|
|
26
|
-
* Nothing staged returns nothing.
|
|
27
|
-
* 3. tree = committer.writeTree(); equal to memory.residueTree returns nothing
|
|
28
|
-
*
|
|
26
|
+
* Nothing staged returns nothing, hold only the excluded paths (14.21).
|
|
27
|
+
* 3. tree = committer.writeTree(); equal to memory.residueTree returns nothing
|
|
28
|
+
* with hold all (the residue is every dirty path); equal to
|
|
29
|
+
* memory.rejection.tree within HOOK_RETRY_MS returns waiting
|
|
29
30
|
* COMMIT_HOOK_REJECTED without running a hook.
|
|
30
31
|
* 4. commitHooks: pre-commit on the commit index (rejected, timed out or
|
|
31
32
|
* killed: HOOK_REJECTED, waiting, rejection remembered); then re-stage and
|
|
32
33
|
* write the tree again, so a formatter's rewrite is in this commit. The
|
|
33
34
|
* message hooks do not run (Decision 80). The re-stage logs through
|
|
34
|
-
* residueLogger: its EXCLUDED lines repeat the first build's
|
|
35
|
+
* residueLogger: its EXCLUDED lines repeat the first build's; its excluded
|
|
36
|
+
* paths are the ones committed's hold names (14.21).
|
|
35
37
|
* 4b. The message (design 14.18, 14.19), before index.lock, so no writer
|
|
36
38
|
* ever holds up a person's git. The command's message: when
|
|
37
39
|
* commitMessageCommand is set, messageCommand runs with
|
|
@@ -111,13 +113,16 @@ export async function commitLocalChangesController(input, deps) {
|
|
|
111
113
|
// The preflight refuses an unborn HEAD; this guards the type.
|
|
112
114
|
throw new EnmetuError('HEAD is unborn; there is nothing to commit onto', 'HEAD_UNBORN', cycleId);
|
|
113
115
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
const built = await build(parent, cycleId, deps.logger, deps);
|
|
117
|
+
let hold = { kind: 'only', paths: built.excluded };
|
|
118
|
+
if (!built.staged) {
|
|
119
|
+
return { kind: 'nothing', hold, memory: unchanged };
|
|
116
120
|
}
|
|
117
121
|
// --- Step 3: trees enmetu already knows ---
|
|
118
122
|
let tree = await deps.commitIndex.committer.writeTree();
|
|
119
123
|
if (tree === memory.residueTree) {
|
|
120
|
-
|
|
124
|
+
// Every dirty path is what the hooks wrote after the last commit (Decision 90).
|
|
125
|
+
return { kind: 'nothing', hold: { kind: 'all' }, memory: unchanged };
|
|
121
126
|
}
|
|
122
127
|
const rejection = memory.rejection;
|
|
123
128
|
if (rejection !== null &&
|
|
@@ -156,8 +161,9 @@ export async function commitLocalChangesController(input, deps) {
|
|
|
156
161
|
logger: deps.residueLogger,
|
|
157
162
|
skipJsonCheck: settings.skipJsonCheck,
|
|
158
163
|
});
|
|
164
|
+
hold = { kind: 'only', paths: restaged.excluded };
|
|
159
165
|
if (!restaged.staged) {
|
|
160
|
-
return { kind: 'nothing', memory: unchanged };
|
|
166
|
+
return { kind: 'nothing', hold, memory: unchanged };
|
|
161
167
|
}
|
|
162
168
|
tree = await deps.commitIndex.committer.writeTree();
|
|
163
169
|
}
|
|
@@ -192,6 +198,7 @@ export async function commitLocalChangesController(input, deps) {
|
|
|
192
198
|
}
|
|
193
199
|
return {
|
|
194
200
|
kind: 'committed',
|
|
201
|
+
hold,
|
|
195
202
|
memory: {
|
|
196
203
|
partialStagingSince: null,
|
|
197
204
|
rejection: null,
|
|
@@ -213,7 +220,7 @@ export async function commitLocalChangesController(input, deps) {
|
|
|
213
220
|
async function runPostCommitHooks(commit, tree, cycleId, deps) {
|
|
214
221
|
const mayWrite = (await deps.realHooks.hookExists('post-index-change')) ||
|
|
215
222
|
(await deps.realHooks.hookExists('post-commit'));
|
|
216
|
-
const pendingBefore = mayWrite && (await build(commit, cycleId, deps.residueLogger, deps));
|
|
223
|
+
const pendingBefore = mayWrite && (await build(commit, cycleId, deps.residueLogger, deps)).staged;
|
|
217
224
|
// Both run through `git hook run --ignore-missing` whatever hookExists
|
|
218
225
|
// said, so what runs is git's decision alone; git ignores both exit codes,
|
|
219
226
|
// and so does enmetu.
|
|
@@ -222,7 +229,7 @@ async function runPostCommitHooks(commit, tree, cycleId, deps) {
|
|
|
222
229
|
if (!mayWrite || pendingBefore) {
|
|
223
230
|
return null;
|
|
224
231
|
}
|
|
225
|
-
if (!(await build(commit, cycleId, deps.residueLogger, deps))) {
|
|
232
|
+
if (!(await build(commit, cycleId, deps.residueLogger, deps)).staged) {
|
|
226
233
|
return null;
|
|
227
234
|
}
|
|
228
235
|
const residue = await deps.commitIndex.committer.writeTree();
|
|
@@ -324,7 +331,8 @@ async function commandMessage(parent, tree, cycleId, deps) {
|
|
|
324
331
|
/**
|
|
325
332
|
* Step 2 of the chain: copy the real index (git's stat data and skip-worktree
|
|
326
333
|
* bits, so add -A does not re-hash every file), seed it with the commit's
|
|
327
|
-
* tree, stage the guarded working tree
|
|
334
|
+
* tree, stage the guarded working tree; staged when anything is, with the
|
|
335
|
+
* paths the content rules kept out (design 14.21).
|
|
328
336
|
*/
|
|
329
337
|
async function build(seed, cycleId, logger, deps) {
|
|
330
338
|
const { commitIndex } = deps;
|
|
@@ -334,13 +342,12 @@ async function build(seed, cycleId, logger, deps) {
|
|
|
334
342
|
await deps.fs.remove(commitIndex.path);
|
|
335
343
|
}
|
|
336
344
|
await commitIndex.committer.seedFrom(seed);
|
|
337
|
-
|
|
345
|
+
return stageGuardedChanges({ cycleId }, {
|
|
338
346
|
git: commitIndex.reader,
|
|
339
347
|
writer: commitIndex.writer,
|
|
340
348
|
logger,
|
|
341
349
|
skipJsonCheck: deps.settings.skipJsonCheck,
|
|
342
350
|
});
|
|
343
|
-
return staged;
|
|
344
351
|
}
|
|
345
352
|
/**
|
|
346
353
|
* Step 6: under index.lock, in this order (Decisions 72, 81, 121):
|
|
@@ -12,6 +12,8 @@ export interface StageGuardedDeps {
|
|
|
12
12
|
}
|
|
13
13
|
export interface StageGuardedResult {
|
|
14
14
|
readonly staged: boolean;
|
|
15
|
+
/** The paths unstaged by the content rules, in git's order: the EXCLUDED lines (design 14.21). */
|
|
16
|
+
readonly excluded: readonly string[];
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Controller: stage everything, inspect the staged blobs with the shared
|
|
@@ -20,7 +22,7 @@ export interface StageGuardedResult {
|
|
|
20
22
|
* 1. writer.stageAll(); partial logs STAGE_PARTIAL.
|
|
21
23
|
* 2. For each git.stagedFiles(MAX_SCAN_BYTES): findContentViolation.
|
|
22
24
|
* 3. Violations: one writer.unstage(paths) call, EXCLUDED logged per path.
|
|
23
|
-
* 4. staged = git.hasStagedChanges().
|
|
25
|
+
* 4. staged = git.hasStagedChanges(); excluded = the violations' paths.
|
|
24
26
|
*
|
|
25
27
|
* Inspecting the staged blobs rather than the working tree means what is
|
|
26
28
|
* inspected is exactly what the next commit records (Decision 5). The
|
|
@@ -6,7 +6,7 @@ import { findContentViolation, MAX_SCAN_BYTES, } from '../core/content/contentRu
|
|
|
6
6
|
* 1. writer.stageAll(); partial logs STAGE_PARTIAL.
|
|
7
7
|
* 2. For each git.stagedFiles(MAX_SCAN_BYTES): findContentViolation.
|
|
8
8
|
* 3. Violations: one writer.unstage(paths) call, EXCLUDED logged per path.
|
|
9
|
-
* 4. staged = git.hasStagedChanges().
|
|
9
|
+
* 4. staged = git.hasStagedChanges(); excluded = the violations' paths.
|
|
10
10
|
*
|
|
11
11
|
* Inspecting the staged blobs rather than the working tree means what is
|
|
12
12
|
* inspected is exactly what the next commit records (Decision 5). The
|
|
@@ -34,10 +34,11 @@ export async function stageGuardedChanges(input, deps) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
// --- Step 3: unstage the violations in one call, in git's order ---
|
|
37
|
+
const excluded = violations.map((violation) => violation.path);
|
|
37
38
|
if (violations.length > 0) {
|
|
38
39
|
// reset also clears unmerged entries, which git add has already turned
|
|
39
40
|
// into stage-0 marker text; the bytes stay on disk for the person.
|
|
40
|
-
await deps.writer.unstage(
|
|
41
|
+
await deps.writer.unstage(excluded);
|
|
41
42
|
for (const violation of violations) {
|
|
42
43
|
deps.logger.log({
|
|
43
44
|
level: 'WARN',
|
|
@@ -47,6 +48,6 @@ export async function stageGuardedChanges(input, deps) {
|
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
|
-
// --- Step 4: report whether anything is left to commit ---
|
|
51
|
-
return { staged: await deps.git.hasStagedChanges() };
|
|
51
|
+
// --- Step 4: report whether anything is left to commit, and what was kept out ---
|
|
52
|
+
return { staged: await deps.git.hasStagedChanges(), excluded };
|
|
52
53
|
}
|
|
@@ -20,6 +20,13 @@ export interface GitRunOptions {
|
|
|
20
20
|
* repository hook this way (design 12.7, Decision 92).
|
|
21
21
|
*/
|
|
22
22
|
readonly abort?: AbortSignal | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Runs git leading its own process group with the adapter's per-command
|
|
25
|
+
* timeout, so the timeout ends git and everything git started: the ssh of a
|
|
26
|
+
* remote command, which a signal to git alone would orphan on a dead
|
|
27
|
+
* connection (design 14.24, Decision 158). The three remote commands set it.
|
|
28
|
+
*/
|
|
29
|
+
readonly killGroup?: boolean | undefined;
|
|
23
30
|
/**
|
|
24
31
|
* Sets GIT_REFLOG_ACTION for this call only, so the entries git writes to
|
|
25
32
|
* HEAD's reflog carry this action instead of the command's name: enmetu's
|
|
@@ -7,6 +7,12 @@ export interface SyncCycleInput {
|
|
|
7
7
|
readonly commitMemory?: CommitMemory | undefined;
|
|
8
8
|
/** Consecutive cycles whose rebase git refused to start (Decision 82); absent is 0. */
|
|
9
9
|
readonly notStartedRebases?: number | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The previous cycle deferred the pull for a save made during a cycle
|
|
12
|
+
* (TREE_CHANGING); the pull waits until a cycle commits nothing (Decision
|
|
13
|
+
* 162). Absent is false.
|
|
14
|
+
*/
|
|
15
|
+
readonly pullDeferred?: boolean | undefined;
|
|
10
16
|
}
|
|
11
17
|
export interface SyncCycleResult {
|
|
12
18
|
readonly outcome: CycleOutcome;
|
|
@@ -16,6 +22,8 @@ export interface SyncCycleResult {
|
|
|
16
22
|
readonly commitMemory?: CommitMemory | undefined;
|
|
17
23
|
/** The count for the next cycle (Decision 82); absent is 0. */
|
|
18
24
|
readonly notStartedRebases?: number | undefined;
|
|
25
|
+
/** Whether the pull is still deferred for the next cycle (Decision 162); absent is false. */
|
|
26
|
+
readonly pullDeferred?: boolean | undefined;
|
|
19
27
|
}
|
|
20
28
|
/** One cycle with its deps bound: a closure over syncCycleController, built by the composition root. */
|
|
21
29
|
export interface ISyncCycle {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HaltCode } from './IHaltStore.js';
|
|
2
|
-
export type EnmetuErrorCode = 'GIT_NOT_FOUND' | 'GIT_TIMEOUT' | 'GIT_ABORTED' | 'GIT_FAILED' | 'GIT_INDEX_LOCKED' | 'GIT_OUTPUT_UNPARSEABLE' | 'CONFIG_INVALID_JSON' | 'CONFIG_INVALID_VALUE' | 'NOT_A_REPOSITORY' | 'BARE_REPOSITORY' | 'GIT_TOO_OLD' | 'PATH_UNSUPPORTED' | 'OPERATION_IN_PROGRESS' | 'DETACHED_HEAD' | 'HEAD_UNBORN' | 'NO_UPSTREAM' | 'UPSTREAM_NOT_REMOTE' | 'IDENTITY_MISSING' | 'LOCK_HELD' | 'LOCK_UNREADABLE' | 'HALTED' | 'USAGE';
|
|
2
|
+
export type EnmetuErrorCode = 'GIT_NOT_FOUND' | 'GIT_TIMEOUT' | 'GIT_ABORTED' | 'GIT_FAILED' | 'GIT_INDEX_LOCKED' | 'GIT_OUTPUT_UNPARSEABLE' | 'CONFIG_INVALID_JSON' | 'CONFIG_INVALID_VALUE' | 'NOT_A_REPOSITORY' | 'BARE_REPOSITORY' | 'GIT_TOO_OLD' | 'ENGINE_TOO_OLD' | 'PATH_UNSUPPORTED' | 'OPERATION_IN_PROGRESS' | 'DETACHED_HEAD' | 'HEAD_UNBORN' | 'NO_UPSTREAM' | 'UPSTREAM_NOT_REMOTE' | 'IDENTITY_MISSING' | 'LOCK_HELD' | 'LOCK_UNREADABLE' | 'HALTED' | 'USAGE';
|
|
3
3
|
export type GitCommandErrorCode = 'GIT_NOT_FOUND' | 'GIT_TIMEOUT' | 'GIT_ABORTED' | 'GIT_FAILED' | 'GIT_INDEX_LOCKED';
|
|
4
4
|
export type ConfigErrorCode = 'CONFIG_INVALID_JSON' | 'CONFIG_INVALID_VALUE';
|
|
5
5
|
export type PreflightErrorCode = 'NOT_A_REPOSITORY' | 'BARE_REPOSITORY' | 'GIT_TOO_OLD' | 'PATH_UNSUPPORTED' | 'OPERATION_IN_PROGRESS' | 'DETACHED_HEAD' | 'HEAD_UNBORN' | 'NO_UPSTREAM' | 'UPSTREAM_NOT_REMOTE' | 'IDENTITY_MISSING';
|
|
@@ -29,7 +29,7 @@ export interface CommitChange {
|
|
|
29
29
|
readonly root: string;
|
|
30
30
|
/** The commit's only parent. */
|
|
31
31
|
readonly parent: string;
|
|
32
|
-
/** The commit's tree; `git diff-tree -r <parent> <tree>` lists the change. */
|
|
32
|
+
/** The commit's tree; `git diff-tree -r -M <parent> <tree>` lists the change. */
|
|
33
33
|
readonly tree: string;
|
|
34
34
|
/**
|
|
35
35
|
* The person's commitMessageCommand on this change: its message, or null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type LogCode = 'STARTED' | 'STOPPING' | 'STOPPED' | 'LOCK_STALE_TAKEN' | 'RECOVERED' | 'HOOK_INSTALLED' | 'HOOK_UPDATED' | 'HOOK_SKIPPED' | 'HOOK_FAILED' | 'WATCHER_FAILED' | 'COMMITTED' | 'STAGE_PARTIAL' | 'EXCLUDED' | 'PULLED' | 'PUSHED' | 'PUSH_REJECTED' | 'PUSH_FAILED' | 'OFFLINE' | 'CONFLICT' | 'UNPUSHED' | 'PAUSED' | 'BUSY' | 'RESUMED' | 'QUARANTINE_KEPT' | 'HALTED' | 'CYCLE_FAILED' | 'NOT_ELIGIBLE' | 'LOCKED' | 'CONFIG_INVALID' | 'CONFIG_KEY_IGNORED' | 'CONFIG_FROM_GIT' | 'MESSAGE_COMMAND_FAILED' | 'MESSAGE_WRITER_FAILED' | 'STATE_WRITE_FAILED' | 'WAITING' | 'SELECTION_WAIT_ENDED' | 'HOOK_REJECTED' | 'COMMIT_DISCARDED' | 'INDEX_REPAIRED' | 'QUARANTINE_UPSTREAM_CHANGED' | 'REBASE_NOT_STARTED' | 'STAGED_RESET' | 'STAGED_REVERT' | 'REBASE_BRANCH_MOVED' | 'COMMIT_ORPHANED';
|
|
1
|
+
export type LogCode = 'STARTED' | 'STOPPING' | 'STOPPED' | 'LOCK_STALE_TAKEN' | 'RECOVERED' | 'HOOK_INSTALLED' | 'HOOK_UPDATED' | 'HOOK_SKIPPED' | 'HOOK_FAILED' | 'WATCHER_FAILED' | 'COMMITTED' | 'STAGE_PARTIAL' | 'EXCLUDED' | 'PULLED' | 'PUSHED' | 'PUSH_REJECTED' | 'PUSH_FAILED' | 'OFFLINE' | 'CONFLICT' | 'UNPUSHED' | 'PAUSED' | 'BUSY' | 'RESUMED' | 'QUARANTINE_KEPT' | 'HALTED' | 'CYCLE_FAILED' | 'NOT_ELIGIBLE' | 'LOCKED' | 'CONFIG_INVALID' | 'CONFIG_KEY_IGNORED' | 'CONFIG_FROM_GIT' | 'MESSAGE_COMMAND_FAILED' | 'MESSAGE_WRITER_FAILED' | 'STATE_WRITE_FAILED' | 'WAITING' | 'SELECTION_WAIT_ENDED' | 'HOOK_REJECTED' | 'COMMIT_DISCARDED' | 'INDEX_REPAIRED' | 'QUARANTINE_UPSTREAM_CHANGED' | 'REBASE_NOT_STARTED' | 'STAGED_RESET' | 'STAGED_REVERT' | 'REBASE_BRANCH_MOVED' | 'COMMIT_ORPHANED' | 'HELD' | 'RESTORED';
|