@bevel-software/platform-core-backend 0.9.1 → 0.11.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/THIRD-PARTY-NOTICES.md +2 -2
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +16 -1
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +25 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +45 -1
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core-config.d.ts +19 -1
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +44 -4
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +54 -28
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +129 -14
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +435 -66
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts.map +1 -1
- package/dist/modules/access/access-declarations.js +5 -3
- package/dist/modules/access/access-declarations.js.map +1 -1
- package/dist/modules/access/access-mutation.service.d.ts +39 -6
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +78 -18
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.d.ts +31 -4
- package/dist/modules/access/access-splice.d.ts.map +1 -1
- package/dist/modules/access/access-splice.js +40 -16
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +204 -82
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/admin-locked-commit.d.ts +134 -0
- package/dist/modules/access/admin-locked-commit.d.ts.map +1 -0
- package/dist/modules/access/admin-locked-commit.js +277 -0
- package/dist/modules/access/admin-locked-commit.js.map +1 -0
- package/dist/modules/access/admin-route-helpers.d.ts +32 -0
- package/dist/modules/access/admin-route-helpers.d.ts.map +1 -0
- package/dist/modules/access/admin-route-helpers.js +44 -0
- package/dist/modules/access/admin-route-helpers.js.map +1 -0
- package/dist/modules/access/capability-registry.d.ts +41 -0
- package/dist/modules/access/capability-registry.d.ts.map +1 -0
- package/dist/modules/access/capability-registry.js +46 -0
- package/dist/modules/access/capability-registry.js.map +1 -0
- package/dist/modules/access/directory-sync-bot.d.ts +13 -0
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -0
- package/dist/modules/access/directory-sync-bot.js +64 -0
- package/dist/modules/access/directory-sync-bot.js.map +1 -0
- package/dist/modules/access/group-files.d.ts +83 -0
- package/dist/modules/access/group-files.d.ts.map +1 -0
- package/dist/modules/access/group-files.js +167 -0
- package/dist/modules/access/group-files.js.map +1 -0
- package/dist/modules/access/groups-admin.routes.d.ts +19 -0
- package/dist/modules/access/groups-admin.routes.d.ts.map +1 -0
- package/dist/modules/access/groups-admin.routes.js +98 -0
- package/dist/modules/access/groups-admin.routes.js.map +1 -0
- package/dist/modules/access/groups-admin.service.d.ts +166 -0
- package/dist/modules/access/groups-admin.service.d.ts.map +1 -0
- package/dist/modules/access/groups-admin.service.js +442 -0
- package/dist/modules/access/groups-admin.service.js.map +1 -0
- package/dist/modules/access/groups-edit.d.ts +58 -0
- package/dist/modules/access/groups-edit.d.ts.map +1 -0
- package/dist/modules/access/groups-edit.js +162 -0
- package/dist/modules/access/groups-edit.js.map +1 -0
- package/dist/modules/access/reference-scan.d.ts +141 -0
- package/dist/modules/access/reference-scan.d.ts.map +1 -0
- package/dist/modules/access/reference-scan.js +440 -0
- package/dist/modules/access/reference-scan.js.map +1 -0
- package/dist/modules/access/roles-admin.service.d.ts +88 -119
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +230 -384
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/access/roles-edit.d.ts +51 -25
- package/dist/modules/access/roles-edit.d.ts.map +1 -1
- package/dist/modules/access/roles-edit.js +133 -59
- package/dist/modules/access/roles-edit.js.map +1 -1
- package/dist/modules/access/synced-groups-committer.d.ts +28 -0
- package/dist/modules/access/synced-groups-committer.d.ts.map +1 -0
- package/dist/modules/access/synced-groups-committer.js +139 -0
- package/dist/modules/access/synced-groups-committer.js.map +1 -0
- package/dist/modules/access/synced-groups-writer.d.ts +78 -0
- package/dist/modules/access/synced-groups-writer.d.ts.map +1 -0
- package/dist/modules/access/synced-groups-writer.js +219 -0
- package/dist/modules/access/synced-groups-writer.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +17 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +9 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +13 -2
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +61 -2
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +9 -3
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +126 -2
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +14 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +6 -1
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/update-check/update-check.routes.d.ts +13 -0
- package/dist/modules/update-check/update-check.routes.d.ts.map +1 -0
- package/dist/modules/update-check/update-check.routes.js +21 -0
- package/dist/modules/update-check/update-check.routes.js.map +1 -0
- package/dist/modules/update-check/update-check.service.d.ts +57 -0
- package/dist/modules/update-check/update-check.service.d.ts.map +1 -0
- package/dist/modules/update-check/update-check.service.js +109 -0
- package/dist/modules/update-check/update-check.service.js.map +1 -0
- package/dist/modules/workflow/file-lock.service.d.ts +11 -1
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +15 -1
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +34 -11
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +173 -37
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/locking-filesystem.d.ts +4 -0
- package/dist/modules/workflow/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/workflow/locking-filesystem.js +181 -28
- package/dist/modules/workflow/locking-filesystem.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +10 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +19 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +17 -2
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +113 -19
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/version.d.ts +14 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +25 -0
- package/dist/version.js.map +1 -1
- package/kb-template/AGENTS.md +4 -1
- package/migrations/0004_file_lock_mode.sql +2 -0
- package/migrations/meta/0004_snapshot.json +1564 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/src/__tests__/core-config.domain.test.ts +92 -0
- package/src/core/create-core-server.ts +21 -0
- package/src/core/create-core-services.ts +82 -0
- package/src/core-config.ts +48 -4
- package/src/index.ts +10 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +9 -3
- package/src/modules/access/__tests__/access-groups.test.ts +427 -0
- package/src/modules/access/__tests__/access-mutation.service.test.ts +171 -5
- package/src/modules/access/__tests__/access-splice.test.ts +65 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +337 -0
- package/src/modules/access/__tests__/access.routes.revoke.test.ts +48 -0
- package/src/modules/access/__tests__/admin-locked-commit.test.ts +221 -0
- package/src/modules/access/__tests__/admin-route-helpers.test.ts +61 -0
- package/src/modules/access/__tests__/directory-sync-bot.test.ts +106 -0
- package/src/modules/access/__tests__/grant-sources.test.ts +67 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +440 -0
- package/src/modules/access/__tests__/reference-scan.test.ts +288 -0
- package/src/modules/access/__tests__/roles-admin.service.test.ts +161 -83
- package/src/modules/access/__tests__/roles-capabilities.test.ts +325 -0
- package/src/modules/access/__tests__/roles-edit.test.ts +104 -28
- package/src/modules/access/__tests__/roles.routes.test.ts +63 -40
- package/src/modules/access/__tests__/synced-groups-committer.test.ts +238 -0
- package/src/modules/access/__tests__/synced-groups-writer.test.ts +249 -0
- package/src/modules/access/access-control.interface.ts +66 -32
- package/src/modules/access/access-control.service.ts +536 -73
- package/src/modules/access/access-declarations.ts +5 -2
- package/src/modules/access/access-mutation.service.ts +88 -14
- package/src/modules/access/access-splice.ts +55 -17
- package/src/modules/access/access.routes.ts +227 -93
- package/src/modules/access/admin-locked-commit.ts +331 -0
- package/src/modules/access/admin-route-helpers.ts +55 -0
- package/src/modules/access/capability-registry.ts +74 -0
- package/src/modules/access/directory-sync-bot.ts +76 -0
- package/src/modules/access/group-files.ts +212 -0
- package/src/modules/access/groups-admin.routes.ts +113 -0
- package/src/modules/access/groups-admin.service.ts +551 -0
- package/src/modules/access/groups-edit.ts +187 -0
- package/src/modules/access/reference-scan.ts +513 -0
- package/src/modules/access/roles-admin.service.ts +290 -418
- package/src/modules/access/roles-edit.ts +134 -61
- package/src/modules/access/synced-groups-committer.ts +177 -0
- package/src/modules/access/synced-groups-writer.ts +303 -0
- package/src/modules/database/core-schema.ts +9 -0
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +116 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
- package/src/modules/mcp/__tests__/mcp.routes.delete.test.ts +6 -1
- package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +301 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +61 -1
- package/src/modules/mcp/mcp.routes.ts +137 -2
- package/src/modules/mcp/mcp.service.ts +6 -1
- package/src/modules/update-check/__tests__/update-check.routes.test.ts +69 -0
- package/src/modules/update-check/__tests__/update-check.service.test.ts +170 -0
- package/src/modules/update-check/update-check.routes.ts +28 -0
- package/src/modules/update-check/update-check.service.ts +130 -0
- package/src/modules/workflow/__tests__/locking-filesystem.test.ts +336 -0
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +32 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +13 -2
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +139 -1
- package/src/modules/workflow/file-lock.service.ts +15 -0
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +0 -1
- package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +132 -0
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +0 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +0 -1
- package/src/modules/workflow/git/git.service.ts +182 -35
- package/src/modules/workflow/locking-filesystem.ts +188 -26
- package/src/modules/workflow/pending-commits.service.ts +27 -1
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +0 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +0 -1
- package/src/modules/workflow/workflow.service.ts +140 -20
- package/src/version.ts +28 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-app update check — the server learns the newest published Hexis release
|
|
3
|
+
* and tells admins when the running deployment is behind (the pattern Immich
|
|
4
|
+
* and Gitea use: a quiet banner, no auto-anything).
|
|
5
|
+
*
|
|
6
|
+
* Deliberate shape:
|
|
7
|
+
*
|
|
8
|
+
* - LAZY, never on a timer. The first `check()` triggers the fetch; a
|
|
9
|
+
* deployment nobody looks at makes zero calls. Results are cached (~6h),
|
|
10
|
+
* concurrent callers share one in-flight request.
|
|
11
|
+
* - The request goes to api.github.com and carries NOTHING but itself — no
|
|
12
|
+
* auth token (public repo; this must never hold credentials), no
|
|
13
|
+
* identifier, no telemetry. `UPDATE_CHECK=false` removes even that.
|
|
14
|
+
* - FAILURE IS SILENT. Offline and air-gapped deployments must stay quiet,
|
|
15
|
+
* not broken: a failed fetch is cached briefly (~15min, so a dead network
|
|
16
|
+
* isn't re-probed per request) and reported as "no update available".
|
|
17
|
+
* - Only a clean `x.y.z` newer than the running version counts. Prerelease
|
|
18
|
+
* or malformed tags — and a running version we can't parse — resolve to
|
|
19
|
+
* "no update" rather than a guess or a throw.
|
|
20
|
+
*/
|
|
21
|
+
const RELEASES_LATEST_URL = 'https://api.github.com/repos/Bevel-Software/Hexis/releases/latest';
|
|
22
|
+
const SUCCESS_TTL_MS = 6 * 60 * 60 * 1000;
|
|
23
|
+
const FAILURE_TTL_MS = 15 * 60 * 1000;
|
|
24
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
25
|
+
/** `v1.2.3` / `1.2.3` → `[1,2,3]`; anything else (prerelease included) → null. */
|
|
26
|
+
export function parseReleaseVersion(raw) {
|
|
27
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(raw.trim());
|
|
28
|
+
if (!m)
|
|
29
|
+
return null;
|
|
30
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether `latest` is STRICTLY newer than `current`. Numeric per-part compare
|
|
34
|
+
* (no string compare — `0.10.0` > `0.9.1`). Either side failing to parse is
|
|
35
|
+
* `false`: an answer we can't trust is not an update announcement.
|
|
36
|
+
*/
|
|
37
|
+
export function isNewerVersion(latest, current) {
|
|
38
|
+
const a = parseReleaseVersion(latest);
|
|
39
|
+
const b = parseReleaseVersion(current);
|
|
40
|
+
if (!a || !b)
|
|
41
|
+
return false;
|
|
42
|
+
for (let i = 0; i < 3; i++) {
|
|
43
|
+
if (a[i] !== b[i])
|
|
44
|
+
return a[i] > b[i];
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
export class UpdateCheckService {
|
|
49
|
+
opts;
|
|
50
|
+
cached = null;
|
|
51
|
+
inFlight = null;
|
|
52
|
+
constructor(opts) {
|
|
53
|
+
this.opts = opts;
|
|
54
|
+
}
|
|
55
|
+
async check() {
|
|
56
|
+
if (!this.opts.enabled) {
|
|
57
|
+
return { updateAvailable: false, current: this.opts.currentVersion };
|
|
58
|
+
}
|
|
59
|
+
const now = (this.opts.now ?? Date.now)();
|
|
60
|
+
if (this.cached && now < this.cached.expiresAt)
|
|
61
|
+
return this.cached.result;
|
|
62
|
+
// One request serves every concurrent caller; `finally` clears the slot so
|
|
63
|
+
// the NEXT check after settlement reads the cache (or starts a fresh
|
|
64
|
+
// fetch once the TTL lapses).
|
|
65
|
+
this.inFlight ??= this.fetchAndCache().finally(() => {
|
|
66
|
+
this.inFlight = null;
|
|
67
|
+
});
|
|
68
|
+
return this.inFlight;
|
|
69
|
+
}
|
|
70
|
+
async fetchAndCache() {
|
|
71
|
+
const current = this.opts.currentVersion;
|
|
72
|
+
const nowFn = this.opts.now ?? Date.now;
|
|
73
|
+
try {
|
|
74
|
+
const fetchFn = this.opts.fetchFn ?? fetch;
|
|
75
|
+
const res = await fetchFn(this.opts.releasesUrl ?? RELEASES_LATEST_URL, {
|
|
76
|
+
headers: { Accept: 'application/vnd.github+json' },
|
|
77
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
throw new Error(`GitHub answered ${res.status}`);
|
|
81
|
+
const body = (await res.json());
|
|
82
|
+
const tag = typeof body.tag_name === 'string' ? body.tag_name.trim() : '';
|
|
83
|
+
// Validate the RAW tag before normalizing: a malformed `vv1.2.3` would
|
|
84
|
+
// otherwise shed one prefix and land on `v1.2.3`, which the parser's
|
|
85
|
+
// lenient `v?` then accepts — announcing a tag we should distrust.
|
|
86
|
+
const latest = parseReleaseVersion(tag) ? tag.replace(/^v/, '') : '';
|
|
87
|
+
const notesUrl = typeof body.html_url === 'string' ? body.html_url : undefined;
|
|
88
|
+
const result = isNewerVersion(latest, current)
|
|
89
|
+
? { updateAvailable: true, current, latest, notesUrl }
|
|
90
|
+
: // Equal, older, prerelease or malformed: all "no update". `latest`
|
|
91
|
+
// rides along only when it parsed — a garbage tag is not an answer.
|
|
92
|
+
{
|
|
93
|
+
updateAvailable: false,
|
|
94
|
+
current,
|
|
95
|
+
...(parseReleaseVersion(latest) ? { latest } : {}),
|
|
96
|
+
};
|
|
97
|
+
this.cached = { result, expiresAt: nowFn() + SUCCESS_TTL_MS };
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Silent by design — no log spam, no error to the UI. The short TTL is
|
|
102
|
+
// what keeps a dead network from being probed on every request.
|
|
103
|
+
const result = { updateAvailable: false, current };
|
|
104
|
+
this.cached = { result, expiresAt: nowFn() + FAILURE_TTL_MS };
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=update-check.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-check.service.js","sourceRoot":"","sources":["../../../src/modules/update-check/update-check.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -32,8 +32,18 @@ export declare class FileLockService {
|
|
|
32
32
|
* "Locked by X" without a follow-up call.
|
|
33
33
|
* - Existing row, live, same user: refresh TTL + `{ acquired: true }`
|
|
34
34
|
* so re-acquiring your own lock is idempotent.
|
|
35
|
+
*
|
|
36
|
+
* `opts.coordination` stamps the row's `mode` as `'coordination'` — a
|
|
37
|
+
* pure-mutex hold that grants no write authority (see
|
|
38
|
+
* `IWorkflowService.acquireLock`). The mode is persisted so the workflow
|
|
39
|
+
* service's write paths can refuse to treat the hold as write possession
|
|
40
|
+
* for the row's whole lifetime; a takeover of an expired row re-stamps the
|
|
41
|
+
* mode, so a stale coordination row can't launder a later edit acquire
|
|
42
|
+
* (or vice versa).
|
|
35
43
|
*/
|
|
36
|
-
acquire(workspaceId: string, branch: string, targetPath: string, user: AuthUser
|
|
44
|
+
acquire(workspaceId: string, branch: string, targetPath: string, user: AuthUser, opts?: {
|
|
45
|
+
coordination?: boolean;
|
|
46
|
+
}): Promise<AcquireLockResult>;
|
|
37
47
|
/**
|
|
38
48
|
* Extend the lock's TTL. Refuses if the lock is held by someone else or
|
|
39
49
|
* doesn't exist — the client is then expected to re-call `acquire`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-lock.service.d.ts","sourceRoot":"","sources":["../../../src/modules/workflow/file-lock.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAE1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"file-lock.service.d.ts","sourceRoot":"","sources":["../../../src/modules/workflow/file-lock.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAE1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AA2B7F,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ;IAEzC;;;;;;;;;;;;;;;;;;;OAmBG;IACG,OAAO,CACX,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,QAAQ,EACd,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,iBAAiB,CAAC;IA0F7B;;;;;OAKG;IACG,SAAS,CACb,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,QAAQ,CAAC;IAoCpB;;;;OAIG;IACG,OAAO,CACX,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,IAAI,CAAC;IAahB;;;;OAIG;IACG,GAAG,CACP,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAqC3B;;;;;;;OAOG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAQ1D"}
|
|
@@ -33,6 +33,9 @@ function rowToFileLock(row) {
|
|
|
33
33
|
path: row.path,
|
|
34
34
|
holderUserId: row.holderUserId,
|
|
35
35
|
holderName: row.holderName,
|
|
36
|
+
// The column carries a NOT NULL 'edit' default, so rows predating the
|
|
37
|
+
// mode column read back as plain edit locks — the strictest reading.
|
|
38
|
+
mode: row.mode === 'coordination' ? 'coordination' : 'edit',
|
|
36
39
|
acquiredAt: row.acquiredAt.toISOString(),
|
|
37
40
|
lastHeartbeatAt: row.lastHeartbeatAt.toISOString(),
|
|
38
41
|
expiresAt: row.expiresAt.toISOString(),
|
|
@@ -54,8 +57,16 @@ export class FileLockService {
|
|
|
54
57
|
* "Locked by X" without a follow-up call.
|
|
55
58
|
* - Existing row, live, same user: refresh TTL + `{ acquired: true }`
|
|
56
59
|
* so re-acquiring your own lock is idempotent.
|
|
60
|
+
*
|
|
61
|
+
* `opts.coordination` stamps the row's `mode` as `'coordination'` — a
|
|
62
|
+
* pure-mutex hold that grants no write authority (see
|
|
63
|
+
* `IWorkflowService.acquireLock`). The mode is persisted so the workflow
|
|
64
|
+
* service's write paths can refuse to treat the hold as write possession
|
|
65
|
+
* for the row's whole lifetime; a takeover of an expired row re-stamps the
|
|
66
|
+
* mode, so a stale coordination row can't launder a later edit acquire
|
|
67
|
+
* (or vice versa).
|
|
57
68
|
*/
|
|
58
|
-
async acquire(workspaceId, branch, targetPath, user) {
|
|
69
|
+
async acquire(workspaceId, branch, targetPath, user, opts) {
|
|
59
70
|
const now = new Date();
|
|
60
71
|
const expiresAt = new Date(now.getTime() + LOCK_TTL_MS);
|
|
61
72
|
// Atomic acquire-or-takeover, in a single statement. Two callers
|
|
@@ -86,6 +97,7 @@ export class FileLockService {
|
|
|
86
97
|
// `heartbeat()` (which has its own same-user UPDATE), and the
|
|
87
98
|
// editor's save flow detects "I already hold it" via `getLock` in
|
|
88
99
|
// `workspace.routes.withLock` rather than re-acquiring.
|
|
100
|
+
const mode = opts?.coordination ? 'coordination' : 'edit';
|
|
89
101
|
const upsertResult = await this.db
|
|
90
102
|
.insert(fileLocks)
|
|
91
103
|
.values({
|
|
@@ -94,6 +106,7 @@ export class FileLockService {
|
|
|
94
106
|
path: targetPath,
|
|
95
107
|
holderUserId: user.id,
|
|
96
108
|
holderName: user.name,
|
|
109
|
+
mode,
|
|
97
110
|
acquiredAt: now,
|
|
98
111
|
lastHeartbeatAt: now,
|
|
99
112
|
expiresAt,
|
|
@@ -103,6 +116,7 @@ export class FileLockService {
|
|
|
103
116
|
set: {
|
|
104
117
|
holderUserId: user.id,
|
|
105
118
|
holderName: user.name,
|
|
119
|
+
mode,
|
|
106
120
|
acquiredAt: now,
|
|
107
121
|
lastHeartbeatAt: now,
|
|
108
122
|
expiresAt,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-lock.service.js","sourceRoot":"","sources":["../../../src/modules/workflow/file-lock.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,MAAM,CAAC;AAE3B,SAAS,aAAa,CAAC,GAAkC;IACvD,OAAO;QACL,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,UAAU,EAAE,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE;QACxC,eAAe,EAAE,GAAG,CAAC,eAAe,CAAC,WAAW,EAAE;QAClD,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE;KACvC,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,eAAe;IACG;IAA7B,YAA6B,EAAY;QAAZ,OAAE,GAAF,EAAE,CAAU;IAAG,CAAC;IAE7C
|
|
1
|
+
{"version":3,"file":"file-lock.service.js","sourceRoot":"","sources":["../../../src/modules/workflow/file-lock.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,17 @@ import { PROTECTED_BRANCHES } from './branch-name.js';
|
|
|
17
17
|
* we keep the new path and discard the old one.
|
|
18
18
|
*/
|
|
19
19
|
export declare function parsePorcelainZ(stdout: string): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Like {@link parsePorcelainZ}, but also reports each rename/copy entry's
|
|
22
|
+
* OLD path (keyed by the new path). A pathspec-scoped commit needs the old
|
|
23
|
+
* path too: committing only the new path of a staged rename records an ADD
|
|
24
|
+
* and leaves the deletion of the old path behind — the rename becomes two
|
|
25
|
+
* files.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parsePorcelainZDetailed(stdout: string): {
|
|
28
|
+
paths: string[];
|
|
29
|
+
renameOldByNew: Map<string, string>;
|
|
30
|
+
};
|
|
20
31
|
export declare class GitService implements IGitService {
|
|
21
32
|
private readonly workspaceService;
|
|
22
33
|
/**
|
|
@@ -159,20 +170,26 @@ export declare class GitService implements IGitService {
|
|
|
159
170
|
discardPath(workspaceId: string, relativePath: string): Promise<void>;
|
|
160
171
|
commitFile(workspaceId: string, user: AuthUser, relativePath: string, summary?: string, skipValidator?: boolean): Promise<CommitAttribution | null>;
|
|
161
172
|
/**
|
|
162
|
-
* Atomic multi-file commit — commits
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
173
|
+
* Atomic multi-file commit — commits the caller's batch as ONE commit
|
|
174
|
+
* attributed to `user`, then returns its attribution (or null when nothing
|
|
175
|
+
* in scope is dirty — idempotent re-apply). The multi-file sibling of
|
|
176
|
+
* `commit()`, WITHOUT the one-file-per-change guard — the caller has
|
|
177
|
+
* assembled + written + validated a batch (bulk node upload; the lock-aware
|
|
178
|
+
* saves and admin roster writes via `LockingFilesystem` /
|
|
179
|
+
* `AdminLockedCommits`).
|
|
180
|
+
*
|
|
181
|
+
* `onlyPaths` (workspace-relative) scopes staging + commit to the caller's
|
|
182
|
+
* own files via git pathspecs: on the shared per-branch workspace other
|
|
183
|
+
* paths may be dirty from a concurrent save whose commit is still queued,
|
|
184
|
+
* and sweeping them in would land them under this caller's author/summary.
|
|
185
|
+
* Omitted → the whole dirty set (`git add -A`), for flows that own the
|
|
186
|
+
* workspace.
|
|
168
187
|
*
|
|
169
188
|
* Deliberately does NOT write file content itself — disk writes are the
|
|
170
|
-
* caller's job. This is the git layer: it only stages + commits what is
|
|
171
|
-
* on disk
|
|
172
|
-
* `git add -A` stages exactly the caller's batch). Caller pushes separately
|
|
173
|
-
* (mirrors `commitFile` + `push`).
|
|
189
|
+
* caller's job. This is the git layer: it only stages + commits what is
|
|
190
|
+
* already on disk. Caller pushes separately (mirrors `commitFile` + `push`).
|
|
174
191
|
*/
|
|
175
|
-
commitChanges(workspaceId: string, user: AuthUser, summary: string): Promise<CommitAttribution | null>;
|
|
192
|
+
commitChanges(workspaceId: string, user: AuthUser, summary: string, onlyPaths?: string[]): Promise<CommitAttribution | null>;
|
|
176
193
|
push(workspaceId: string, user: AuthUser, opts?: {
|
|
177
194
|
systemAuthorized?: boolean;
|
|
178
195
|
}): Promise<void>;
|
|
@@ -289,6 +306,12 @@ export declare class GitService implements IGitService {
|
|
|
289
306
|
* Used by the pending-commits worker's no-op arm: a clean tree does NOT
|
|
290
307
|
* mean "nothing to share" when a prior best-effort push (the autosave
|
|
291
308
|
* path) failed and left the committed change stranded locally.
|
|
309
|
+
*
|
|
310
|
+
* Serialized under the workspace mutex: a pull-rebase in flight moves HEAD
|
|
311
|
+
* through states where the local commits are momentarily unreachable, and
|
|
312
|
+
* an unserialized read there answers "nothing unpushed" about work that is
|
|
313
|
+
* about to be replayed — callers use this as a publication PROOF, so it may
|
|
314
|
+
* only ever see settled states.
|
|
292
315
|
*/
|
|
293
316
|
hasUnpushedCommits(workspaceId: string): Promise<boolean>;
|
|
294
317
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/workflow/git/git.service.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,QAAQ,EACR,UAAU,EACV,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAA2B,MAAM,sBAAsB,CAAC;AACnF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0CAA0C,CAAC;AAE/E,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,EAML,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AA8B1B;;;;;GAKG;AACH;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"git.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/workflow/git/git.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -34,8 +34,19 @@ const BOT_EMAIL = 'bevel-workflow@bevel.software';
|
|
|
34
34
|
* we keep the new path and discard the old one.
|
|
35
35
|
*/
|
|
36
36
|
export function parsePorcelainZ(stdout) {
|
|
37
|
+
return parsePorcelainZDetailed(stdout).paths;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Like {@link parsePorcelainZ}, but also reports each rename/copy entry's
|
|
41
|
+
* OLD path (keyed by the new path). A pathspec-scoped commit needs the old
|
|
42
|
+
* path too: committing only the new path of a staged rename records an ADD
|
|
43
|
+
* and leaves the deletion of the old path behind — the rename becomes two
|
|
44
|
+
* files.
|
|
45
|
+
*/
|
|
46
|
+
export function parsePorcelainZDetailed(stdout) {
|
|
37
47
|
const tokens = stdout.split('\0').filter((t) => t.length > 0);
|
|
38
48
|
const paths = new Set();
|
|
49
|
+
const renameOldByNew = new Map();
|
|
39
50
|
let i = 0;
|
|
40
51
|
while (i < tokens.length) {
|
|
41
52
|
const token = tokens[i];
|
|
@@ -46,12 +57,21 @@ export function parsePorcelainZ(stdout) {
|
|
|
46
57
|
}
|
|
47
58
|
const x = token[0];
|
|
48
59
|
const y = token[1];
|
|
49
|
-
|
|
60
|
+
const newPath = token.slice(3);
|
|
61
|
+
paths.add(newPath);
|
|
50
62
|
// Rename or copy in either the index or worktree position carries an
|
|
51
|
-
// extra NUL-separated old-path field
|
|
52
|
-
|
|
63
|
+
// extra NUL-separated old-path field.
|
|
64
|
+
if (x === 'R' || x === 'C' || y === 'R' || y === 'C') {
|
|
65
|
+
const oldPath = tokens[i + 1];
|
|
66
|
+
if (oldPath)
|
|
67
|
+
renameOldByNew.set(newPath, oldPath);
|
|
68
|
+
i += 2;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
i += 1;
|
|
72
|
+
}
|
|
53
73
|
}
|
|
54
|
-
return Array.from(paths);
|
|
74
|
+
return { paths: Array.from(paths), renameOldByNew };
|
|
55
75
|
}
|
|
56
76
|
/**
|
|
57
77
|
* Default commit subject used when a caller (typically `LockingFilesystem`'s
|
|
@@ -839,20 +859,26 @@ export class GitService {
|
|
|
839
859
|
});
|
|
840
860
|
}
|
|
841
861
|
/**
|
|
842
|
-
* Atomic multi-file commit — commits
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
862
|
+
* Atomic multi-file commit — commits the caller's batch as ONE commit
|
|
863
|
+
* attributed to `user`, then returns its attribution (or null when nothing
|
|
864
|
+
* in scope is dirty — idempotent re-apply). The multi-file sibling of
|
|
865
|
+
* `commit()`, WITHOUT the one-file-per-change guard — the caller has
|
|
866
|
+
* assembled + written + validated a batch (bulk node upload; the lock-aware
|
|
867
|
+
* saves and admin roster writes via `LockingFilesystem` /
|
|
868
|
+
* `AdminLockedCommits`).
|
|
869
|
+
*
|
|
870
|
+
* `onlyPaths` (workspace-relative) scopes staging + commit to the caller's
|
|
871
|
+
* own files via git pathspecs: on the shared per-branch workspace other
|
|
872
|
+
* paths may be dirty from a concurrent save whose commit is still queued,
|
|
873
|
+
* and sweeping them in would land them under this caller's author/summary.
|
|
874
|
+
* Omitted → the whole dirty set (`git add -A`), for flows that own the
|
|
875
|
+
* workspace.
|
|
848
876
|
*
|
|
849
877
|
* Deliberately does NOT write file content itself — disk writes are the
|
|
850
|
-
* caller's job. This is the git layer: it only stages + commits what is
|
|
851
|
-
* on disk
|
|
852
|
-
* `git add -A` stages exactly the caller's batch). Caller pushes separately
|
|
853
|
-
* (mirrors `commitFile` + `push`).
|
|
878
|
+
* caller's job. This is the git layer: it only stages + commits what is
|
|
879
|
+
* already on disk. Caller pushes separately (mirrors `commitFile` + `push`).
|
|
854
880
|
*/
|
|
855
|
-
async commitChanges(workspaceId, user, summary) {
|
|
881
|
+
async commitChanges(workspaceId, user, summary, onlyPaths) {
|
|
856
882
|
assertValidAuthor(user);
|
|
857
883
|
const subject = summary?.trim();
|
|
858
884
|
if (!subject)
|
|
@@ -866,8 +892,28 @@ export class GitService {
|
|
|
866
892
|
const branch = await this.currentBranch(cwd);
|
|
867
893
|
// Nothing dirty → no-op (idempotent). Compute touched BEFORE `git add` so
|
|
868
894
|
// the protected-branch gate sees the same path set `commit()` would.
|
|
869
|
-
|
|
870
|
-
|
|
895
|
+
// `--untracked-files=all` lists every untracked FILE individually —
|
|
896
|
+
// without it a brand-new directory shows as one `?? dir/` entry, so a
|
|
897
|
+
// scoped batch creating files under a new folder would match nothing
|
|
898
|
+
// against `onlyPaths` and silently no-op.
|
|
899
|
+
const { stdout: porcelain } = await this.git(cwd, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
|
|
900
|
+
const detailed = parsePorcelainZDetailed(porcelain);
|
|
901
|
+
let touched = detailed.paths;
|
|
902
|
+
// Scope to the caller's own paths (see the interface doc): other files
|
|
903
|
+
// may be dirty from a concurrent same-branch save whose commit is still
|
|
904
|
+
// queued — sweeping them in would land them under this caller's
|
|
905
|
+
// author/summary, and gate the commit on paths the caller never touched.
|
|
906
|
+
// An in-scope RENAME keeps its old path too: a pathspec naming only the
|
|
907
|
+
// new path records an add and strands the deletion of the old one.
|
|
908
|
+
if (onlyPaths) {
|
|
909
|
+
const scope = new Set(onlyPaths.map((p) => this.stripRepoPrefix(p)));
|
|
910
|
+
touched = touched.filter((p) => scope.has(p));
|
|
911
|
+
for (const p of touched.slice()) {
|
|
912
|
+
const oldPath = detailed.renameOldByNew.get(p);
|
|
913
|
+
if (oldPath && !touched.includes(oldPath))
|
|
914
|
+
touched.push(oldPath);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
871
917
|
if (touched.length === 0)
|
|
872
918
|
return null;
|
|
873
919
|
// Protected-branch access gate (defence in depth — callers already gate at
|
|
@@ -876,19 +922,85 @@ export class GitService {
|
|
|
876
922
|
if (isProtectedBranch(branch)) {
|
|
877
923
|
await this.assertCanWriteAtRef(workspaceId, 'HEAD', user.email, touched);
|
|
878
924
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
925
|
+
// Scoped → stage the batch (a brand-new file must be `add`ed before a
|
|
926
|
+
// pathspec commit will know it), then commit WITH the pathspec: it
|
|
927
|
+
// records exactly those paths, so neither other dirty files nor an
|
|
928
|
+
// unrelated entry someone left staged can ride along.
|
|
929
|
+
//
|
|
930
|
+
// Both stages feed the pathspecs over STDIN (`--pathspec-from-file=-`,
|
|
931
|
+
// NUL-separated) instead of argv: a several-hundred-file batch of long
|
|
932
|
+
// KB paths would otherwise blow Windows' ~32K command-line limit. The
|
|
933
|
+
// add is ONE spawn for the whole batch; only when git reports the ONE
|
|
934
|
+
// expected miss — a fully-staged deletion (or the old half of a rename)
|
|
935
|
+
// has nothing left in the worktree to match, which fails the entire
|
|
936
|
+
// batched add — do we fall back to per-path adds with the same
|
|
937
|
+
// tolerance, in argv-safe chunks of one path per spawn.
|
|
938
|
+
if (onlyPaths) {
|
|
939
|
+
const pathspecInput = touched.join('\0');
|
|
940
|
+
const pathspecArgs = ['--pathspec-from-file=-', '--pathspec-file-nul'];
|
|
941
|
+
const isExpectedMiss = (err) => {
|
|
942
|
+
// (LC_ALL=C pins the message to English.)
|
|
943
|
+
const e = err;
|
|
944
|
+
return /did not match any files/.test(`${e.stderr ?? ''}\n${e.message}`);
|
|
945
|
+
};
|
|
946
|
+
try {
|
|
947
|
+
await this.git(cwd, ['add', '-A', ...pathspecArgs], { input: pathspecInput });
|
|
948
|
+
}
|
|
949
|
+
catch (err) {
|
|
950
|
+
// Any other add failure — index.lock contention, I/O — must abort:
|
|
951
|
+
// committing without staging would record stale index content as
|
|
952
|
+
// this caller's change.
|
|
953
|
+
if (!isExpectedMiss(err))
|
|
954
|
+
throw err;
|
|
955
|
+
for (const p of touched) {
|
|
956
|
+
try {
|
|
957
|
+
await this.git(cwd, ['add', '-A', '--', p]);
|
|
958
|
+
}
|
|
959
|
+
catch (perPathErr) {
|
|
960
|
+
if (!isExpectedMiss(perPathErr))
|
|
961
|
+
throw perPathErr;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
await this.git(cwd, ['commit', `--author=${user.name} <${user.email}>`, '-m', subject, ...pathspecArgs], { input: pathspecInput });
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
await this.git(cwd, ['add', '-A']);
|
|
969
|
+
await this.git(cwd, ['commit', `--author=${user.name} <${user.email}>`, '-m', subject]);
|
|
970
|
+
}
|
|
971
|
+
// CONTRACT: past this point the commit EXISTS, so this method must not
|
|
972
|
+
// throw — callers (writeAndCommitLocked) treat a throw as "nothing was
|
|
973
|
+
// committed" and restore their pre-edit bytes, which here would publish
|
|
974
|
+
// a compensating revert of a commit that DID land. The attribution read
|
|
975
|
+
// is best-effort; on failure fall back to what we already know.
|
|
976
|
+
let sha = '';
|
|
977
|
+
let authorName = user.name;
|
|
978
|
+
let authorEmail = user.email;
|
|
979
|
+
let subj = subject;
|
|
980
|
+
let committedAt = '';
|
|
981
|
+
try {
|
|
982
|
+
const { stdout } = await this.git(cwd, [
|
|
983
|
+
'log', '-1', '--pretty=format:%H%x00%an%x00%ae%x00%s%x00%aI',
|
|
984
|
+
]);
|
|
985
|
+
[sha, authorName, authorEmail, subj, committedAt] = stdout.split('\x00');
|
|
986
|
+
}
|
|
987
|
+
catch {
|
|
988
|
+
// The commit landed; only its attribution read failed. Give the sha
|
|
989
|
+
// one simpler second chance — callers use it as the commit id, and an
|
|
990
|
+
// empty sha would make a landed change unidentifiable downstream.
|
|
991
|
+
try {
|
|
992
|
+
const { stdout } = await this.git(cwd, ['rev-parse', 'HEAD']);
|
|
993
|
+
sha = stdout;
|
|
994
|
+
}
|
|
995
|
+
catch { /* keep the fallback attribution */ }
|
|
996
|
+
}
|
|
885
997
|
this.accessControl?.invalidate(workspaceId);
|
|
886
998
|
return {
|
|
887
999
|
sha: sha?.trim() ?? '',
|
|
888
1000
|
authorName: authorName ?? '',
|
|
889
1001
|
authorEmail: authorEmail ?? '',
|
|
890
1002
|
subject: subj ?? subject,
|
|
891
|
-
committedAt: committedAt?.trim()
|
|
1003
|
+
committedAt: committedAt?.trim() || new Date().toISOString(),
|
|
892
1004
|
};
|
|
893
1005
|
});
|
|
894
1006
|
}
|
|
@@ -1289,19 +1401,27 @@ export class GitService {
|
|
|
1289
1401
|
* Used by the pending-commits worker's no-op arm: a clean tree does NOT
|
|
1290
1402
|
* mean "nothing to share" when a prior best-effort push (the autosave
|
|
1291
1403
|
* path) failed and left the committed change stranded locally.
|
|
1404
|
+
*
|
|
1405
|
+
* Serialized under the workspace mutex: a pull-rebase in flight moves HEAD
|
|
1406
|
+
* through states where the local commits are momentarily unreachable, and
|
|
1407
|
+
* an unserialized read there answers "nothing unpushed" about work that is
|
|
1408
|
+
* about to be replayed — callers use this as a publication PROOF, so it may
|
|
1409
|
+
* only ever see settled states.
|
|
1292
1410
|
*/
|
|
1293
1411
|
async hasUnpushedCommits(workspaceId) {
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1412
|
+
return this.mutex.run(workspaceId, async () => {
|
|
1413
|
+
const cwd = await this.repoDir(workspaceId);
|
|
1414
|
+
const branch = await this.currentBranch(cwd);
|
|
1415
|
+
try {
|
|
1416
|
+
const { stdout } = await this.git(cwd, [
|
|
1417
|
+
'rev-list', '--count', `refs/remotes/origin/${branch}..HEAD`,
|
|
1418
|
+
]);
|
|
1419
|
+
return Number(stdout.trim()) > 0;
|
|
1420
|
+
}
|
|
1421
|
+
catch {
|
|
1422
|
+
return true;
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1305
1425
|
}
|
|
1306
1426
|
/**
|
|
1307
1427
|
* Hard-reset the workspace's checked-out branch to `origin/<branch>`, fetching
|
|
@@ -1484,6 +1604,14 @@ export class GitService {
|
|
|
1484
1604
|
try {
|
|
1485
1605
|
const { stdout } = await this.git(cwd, [
|
|
1486
1606
|
'show',
|
|
1607
|
+
// Patch only, no commit header — and not for looks: whether `show`
|
|
1608
|
+
// prints the header for a commit the pathspec does not touch is
|
|
1609
|
+
// git-version-dependent (older gits simplified the commit away,
|
|
1610
|
+
// newer ones print the header over an empty diff). Suppressing the
|
|
1611
|
+
// format makes "commit didn't touch the file" mean an EMPTY string
|
|
1612
|
+
// on every git. The metadata the header carried already reaches the
|
|
1613
|
+
// history view via `logForFile`.
|
|
1614
|
+
'--format=',
|
|
1487
1615
|
sha,
|
|
1488
1616
|
'--',
|
|
1489
1617
|
repoRelativePath,
|
|
@@ -2002,9 +2130,9 @@ export class GitService {
|
|
|
2002
2130
|
]);
|
|
2003
2131
|
return stdout.trim().length > 0;
|
|
2004
2132
|
}
|
|
2005
|
-
async git(cwd, args) {
|
|
2133
|
+
async git(cwd, args, opts) {
|
|
2006
2134
|
try {
|
|
2007
|
-
const
|
|
2135
|
+
const pending = execFileAsync('git', args, {
|
|
2008
2136
|
cwd,
|
|
2009
2137
|
// `GIT_LITERAL_PATHSPECS=1` makes git treat every pathspec literally
|
|
2010
2138
|
// instead of interpreting `[`, `]`, `*`, `?`, `!`, or `:(magic)` as
|
|
@@ -2025,6 +2153,14 @@ export class GitService {
|
|
|
2025
2153
|
env: { ...process.env, GIT_LITERAL_PATHSPECS: '1', LC_ALL: 'C', LANG: 'C' },
|
|
2026
2154
|
maxBuffer: 32 * 1024 * 1024,
|
|
2027
2155
|
});
|
|
2156
|
+
if (opts?.input !== undefined && pending.child.stdin) {
|
|
2157
|
+
// A dying git can close stdin mid-write; the promise below still
|
|
2158
|
+
// rejects with the exit code, which is the error we want to surface.
|
|
2159
|
+
pending.child.stdin.on('error', () => undefined);
|
|
2160
|
+
pending.child.stdin.write(opts.input);
|
|
2161
|
+
pending.child.stdin.end();
|
|
2162
|
+
}
|
|
2163
|
+
const { stdout, stderr } = await pending;
|
|
2028
2164
|
return { stdout: stdout.toString(), stderr: stderr.toString() };
|
|
2029
2165
|
}
|
|
2030
2166
|
catch (err) {
|