@fgbg/mdocs 0.7.8 → 0.7.9
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/dist/server/documents/commit-graph.js +44 -13
- package/dist/server/documents/commit-graph.js.map +1 -1
- package/dist/server/documents/commit-graph.test.js +7 -2
- package/dist/server/documents/commit-graph.test.js.map +1 -1
- package/dist/server/documents/document-commit.integration.test.js +70 -17
- package/dist/server/documents/document-commit.integration.test.js.map +1 -1
- package/dist/server/documents/document.service.js +85 -37
- package/dist/server/documents/document.service.js.map +1 -1
- package/dist/server/routes/documents.routes.js +44 -10
- package/dist/server/routes/documents.routes.js.map +1 -1
- package/dist/server/storage/file-store.js +20 -1
- package/dist/server/storage/file-store.js.map +1 -1
- package/dist/web/assets/{_baseUniq-Bt5mr5Sg.js → _baseUniq-CJKNOD1q.js} +1 -1
- package/dist/web/assets/{arc-DngOgKkd.js → arc-BSTUZ8vq.js} +1 -1
- package/dist/web/assets/{architectureDiagram-Q4EWVU46-IT7muYzK.js → architectureDiagram-Q4EWVU46-Di8Xlyl5.js} +1 -1
- package/dist/web/assets/{blockDiagram-DXYQGD6D-Beo5sULj.js → blockDiagram-DXYQGD6D-ZF5fC9Pa.js} +1 -1
- package/dist/web/assets/{c4Diagram-AHTNJAMY-BJ-7wJwT.js → c4Diagram-AHTNJAMY-f4BtRGqQ.js} +1 -1
- package/dist/web/assets/channel-CC9YDRol.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-CKozhjJC.js → chunk-4BX2VUAB-C9vZaXrt.js} +1 -1
- package/dist/web/assets/{chunk-4TB4RGXK-PQDNT7_Q.js → chunk-4TB4RGXK-AuXGx94-.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-YSSe9cKE.js → chunk-55IACEB6-C6HqGEbC.js} +1 -1
- package/dist/web/assets/{chunk-EDXVE4YY-BhqUHLMZ.js → chunk-EDXVE4YY-CBuQGMbF.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-7IM24lYb.js → chunk-FMBD7UC4-DImfvV9Y.js} +1 -1
- package/dist/web/assets/{chunk-OYMX7WX6-DwRSKz5j.js → chunk-OYMX7WX6-BXB7Lalj.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-DHYKFJMD.js → chunk-QZHKN3VN-D3-iHoV_.js} +1 -1
- package/dist/web/assets/{chunk-YZCP3GAM-DLg5pjoy.js → chunk-YZCP3GAM-DRfuXRwg.js} +1 -1
- package/dist/web/assets/classDiagram-6PBFFD2Q-DSK9m5pj.js +1 -0
- package/dist/web/assets/classDiagram-v2-HSJHXN6E-DSK9m5pj.js +1 -0
- package/dist/web/assets/clone-CTsZZpQf.js +1 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-DueL5HgY.js → cose-bilkent-S5V4N54A-CKXOhawd.js} +1 -1
- package/dist/web/assets/{dagre-KV5264BT-CvTMrLUl.js → dagre-KV5264BT-BYCBVC56.js} +1 -1
- package/dist/web/assets/{diagram-5BDNPKRD-q4NHEVUW.js → diagram-5BDNPKRD-XhBrm65s.js} +1 -1
- package/dist/web/assets/{diagram-G4DWMVQ6-a8nJl1e5.js → diagram-G4DWMVQ6-CGbxizxQ.js} +1 -1
- package/dist/web/assets/{diagram-MMDJMWI5-TBImmTdz.js → diagram-MMDJMWI5-CLyKsrJB.js} +1 -1
- package/dist/web/assets/{diagram-TYMM5635-BF1sUiOW.js → diagram-TYMM5635-D9ncu2lv.js} +1 -1
- package/dist/web/assets/{erDiagram-SMLLAGMA-Bv2B8Exy.js → erDiagram-SMLLAGMA-4Jlnrszd.js} +1 -1
- package/dist/web/assets/{flowDiagram-DWJPFMVM-C_vyRbo9.js → flowDiagram-DWJPFMVM-DcZvLb4F.js} +1 -1
- package/dist/web/assets/{ganttDiagram-T4ZO3ILL-CWdSaU12.js → ganttDiagram-T4ZO3ILL-Dw7Zc7xo.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-UUTBAWPF-BevLTyQU.js → gitGraphDiagram-UUTBAWPF-Ftc1gioV.js} +1 -1
- package/dist/web/assets/{graph-CblvkuNX.js → graph-D9a8-HhF.js} +1 -1
- package/dist/web/assets/index-CaRsgF0A.css +1 -0
- package/dist/web/assets/{index-Cv3eJNuk.js → index-DDVYimXd.js} +1184 -1191
- package/dist/web/assets/{infoDiagram-42DDH7IO-BKac3Yhz.js → infoDiagram-42DDH7IO-BOWu6CZc.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-UXIWVN3A-DersdL4Q.js → ishikawaDiagram-UXIWVN3A-f_P4Hv5V.js} +1 -1
- package/dist/web/assets/{journeyDiagram-VCZTEJTY-CQpBsOyx.js → journeyDiagram-VCZTEJTY-D3Gjgb3t.js} +1 -1
- package/dist/web/assets/{kanban-definition-6JOO6SKY-BejAWn9C.js → kanban-definition-6JOO6SKY-JaFivQmV.js} +1 -1
- package/dist/web/assets/{layout-D2aBaGbB.js → layout-DnKRMZlc.js} +1 -1
- package/dist/web/assets/{linear-BS6JsWa0.js → linear-CzU9ZCYG.js} +1 -1
- package/dist/web/assets/{min-CL06Vx8J.js → min-CbYw71ss.js} +1 -1
- package/dist/web/assets/{mindmap-definition-QFDTVHPH-O6ljclCO.js → mindmap-definition-QFDTVHPH-BA1rL4W9.js} +1 -1
- package/dist/web/assets/{pieDiagram-DEJITSTG-DjZMjzvi.js → pieDiagram-DEJITSTG-C164MrXt.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-34T5L4WZ-z3A5Dnao.js → quadrantDiagram-34T5L4WZ-URQJg9YA.js} +1 -1
- package/dist/web/assets/{requirementDiagram-MS252O5E-DoM2kgbQ.js → requirementDiagram-MS252O5E-LgbP4sh2.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-XADWPNL6-fviGw2kn.js → sankeyDiagram-XADWPNL6-DUIw0zkt.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-FGHM5R23-CgtOcbDY.js → sequenceDiagram-FGHM5R23-BTzdpMq_.js} +1 -1
- package/dist/web/assets/{stateDiagram-FHFEXIEX-UpnR040Y.js → stateDiagram-FHFEXIEX-CmrCJk8c.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-QKLJ7IA2-B2lUq_Mb.js +1 -0
- package/dist/web/assets/{timeline-definition-GMOUNBTQ-CjtkZKIX.js → timeline-definition-GMOUNBTQ-D0SQHe-v.js} +1 -1
- package/dist/web/assets/{vennDiagram-DHZGUBPP-BSxx1uwH.js → vennDiagram-DHZGUBPP-8C1P6BpB.js} +1 -1
- package/dist/web/assets/{wardley-RL74JXVD-BABpJ4WY.js → wardley-RL74JXVD-CcK1nJva.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-NUSXRM2D-DyT1ySrj.js → wardleyDiagram-NUSXRM2D-n_8Mywtv.js} +1 -1
- package/dist/web/assets/{xychartDiagram-5P7HB3ND-TAmB0u0X.js → xychartDiagram-5P7HB3ND-Csstb1uX.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +8 -1
- package/dist/web/assets/channel-epQ5ykWT.js +0 -1
- package/dist/web/assets/classDiagram-6PBFFD2Q-CiHf1uhs.js +0 -1
- package/dist/web/assets/classDiagram-v2-HSJHXN6E-CiHf1uhs.js +0 -1
- package/dist/web/assets/clone-BgUoGhin.js +0 -1
- package/dist/web/assets/index-CLOOI7eJ.css +0 -1
- package/dist/web/assets/stateDiagram-v2-QKLJ7IA2-gFGG7JgL.js +0 -1
|
@@ -7,7 +7,6 @@ import { findCommitById, listParentCommitIds } from "../db/repositories/commit.r
|
|
|
7
7
|
export function isAncestorOf(db, ancestorId, nodeId) {
|
|
8
8
|
if (ancestorId === nodeId)
|
|
9
9
|
return true;
|
|
10
|
-
// hash set优化
|
|
11
10
|
const visited = new Set();
|
|
12
11
|
const queue = [nodeId];
|
|
13
12
|
while (queue.length > 0) {
|
|
@@ -16,7 +15,6 @@ export function isAncestorOf(db, ancestorId, nodeId) {
|
|
|
16
15
|
for (const p of parents) {
|
|
17
16
|
if (p === ancestorId)
|
|
18
17
|
return true;
|
|
19
|
-
// bfs的时候会存在重复遍历的情况,set 去重
|
|
20
18
|
if (!visited.has(p)) {
|
|
21
19
|
visited.add(p);
|
|
22
20
|
queue.push(p);
|
|
@@ -32,21 +30,54 @@ export function assertCommitBelongsToDocument(db, commitId, documentId) {
|
|
|
32
30
|
throw new DocumentError("INVALID_COMMIT", "提交节点不存在或不属于该文档", 400);
|
|
33
31
|
}
|
|
34
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* 求两提交在 DAG 上的最近公共祖先(LCA),用于 merge 三路 diff 的 mergeBaseCommitId。
|
|
35
|
+
* 若一方是另一方的祖先,返回祖先侧节点。
|
|
36
|
+
*/
|
|
37
|
+
export function findMergeBaseCommitId(db, commitA, commitB) {
|
|
38
|
+
if (commitA === commitB)
|
|
39
|
+
return commitA;
|
|
40
|
+
if (isAncestorOf(db, commitA, commitB))
|
|
41
|
+
return commitA;
|
|
42
|
+
if (isAncestorOf(db, commitB, commitA))
|
|
43
|
+
return commitB;
|
|
44
|
+
const ancestorsOfA = new Set();
|
|
45
|
+
const queueA = [commitA];
|
|
46
|
+
while (queueA.length > 0) {
|
|
47
|
+
const cur = queueA.shift();
|
|
48
|
+
if (ancestorsOfA.has(cur))
|
|
49
|
+
continue;
|
|
50
|
+
ancestorsOfA.add(cur);
|
|
51
|
+
for (const p of listParentCommitIds(db, cur)) {
|
|
52
|
+
queueA.push(p);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const visitedB = new Set();
|
|
56
|
+
const queueB = [commitB];
|
|
57
|
+
while (queueB.length > 0) {
|
|
58
|
+
const cur = queueB.shift();
|
|
59
|
+
if (ancestorsOfA.has(cur))
|
|
60
|
+
return cur;
|
|
61
|
+
if (visitedB.has(cur))
|
|
62
|
+
continue;
|
|
63
|
+
visitedB.add(cur);
|
|
64
|
+
for (const p of listParentCommitIds(db, cur)) {
|
|
65
|
+
queueB.push(p);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw new DocumentError("BAD_REQUEST", "无法计算 mergeBaseCommitId:两提交无公共祖先", 400);
|
|
69
|
+
}
|
|
35
70
|
/**
|
|
36
71
|
* merge 发布前校验分叉关系。
|
|
37
|
-
* -
|
|
38
|
-
* -
|
|
72
|
+
* - localBaseCommitId 与 remoteCommitId 不能相同(否则应走普通线性发布)
|
|
73
|
+
* - localBaseCommitId 必须是 remoteCommitId 的祖先(允许远端在分叉后线性前进)
|
|
39
74
|
*/
|
|
40
|
-
export function assertMergeFork(db,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
* 则不走 conflict merge逻辑, 应该走普通线性发布逻辑
|
|
44
|
-
*/
|
|
45
|
-
if (baseCommitId === expectedHeadCommitId) {
|
|
46
|
-
throw new DocumentError("BAD_REQUEST", "合并发布要求 base 与 expectedHead 不同", 400);
|
|
75
|
+
export function assertMergeFork(db, localBaseCommitId, remoteCommitId) {
|
|
76
|
+
if (localBaseCommitId === remoteCommitId) {
|
|
77
|
+
throw new DocumentError("BAD_REQUEST", "合并发布要求 localBaseCommitId 与 remoteCommitId 不同", 400);
|
|
47
78
|
}
|
|
48
|
-
if (!isAncestorOf(db,
|
|
49
|
-
throw new DocumentError("BAD_REQUEST", "
|
|
79
|
+
if (!isAncestorOf(db, localBaseCommitId, remoteCommitId)) {
|
|
80
|
+
throw new DocumentError("BAD_REQUEST", "localBaseCommitId 不是 remoteCommitId 的祖先,无法合并", 400);
|
|
50
81
|
}
|
|
51
82
|
}
|
|
52
83
|
//# sourceMappingURL=commit-graph.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commit-graph.js","sourceRoot":"","sources":["../../../src/server/documents/commit-graph.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAExF;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,EAAqB,EACrB,UAAkB,EAClB,MAAc;IAEd,IAAI,UAAU,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACvC,
|
|
1
|
+
{"version":3,"file":"commit-graph.js","sourceRoot":"","sources":["../../../src/server/documents/commit-graph.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeAll } from "vitest";
|
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import { applySchema } from "../db/schema.js";
|
|
4
4
|
import { insertCommit, insertCommitParent } from "../db/repositories/commit.repo.js";
|
|
5
|
-
import { isAncestorOf, assertMergeFork } from "./commit-graph.js";
|
|
5
|
+
import { isAncestorOf, assertMergeFork, findMergeBaseCommitId } from "./commit-graph.js";
|
|
6
6
|
import { DocumentError } from "../access/access-control.js";
|
|
7
7
|
describe("commit-graph", () => {
|
|
8
8
|
let db;
|
|
@@ -43,10 +43,15 @@ describe("commit-graph", () => {
|
|
|
43
43
|
expect(isAncestorOf(db, "commit-c", "commit-d")).toBe(true);
|
|
44
44
|
expect(isAncestorOf(db, "commit-d", "commit-a")).toBe(false);
|
|
45
45
|
});
|
|
46
|
-
it("assertMergeFork:
|
|
46
|
+
it("assertMergeFork:localBase 必须是 remote 的真祖先", () => {
|
|
47
47
|
expect(() => assertMergeFork(db, "commit-a", "commit-d")).not.toThrow();
|
|
48
48
|
expect(() => assertMergeFork(db, "commit-d", "commit-d")).toThrow(DocumentError);
|
|
49
49
|
expect(() => assertMergeFork(db, "commit-d", "commit-a")).toThrow(DocumentError);
|
|
50
50
|
});
|
|
51
|
+
it("findMergeBaseCommitId:线性链与 reset 方向", () => {
|
|
52
|
+
expect(findMergeBaseCommitId(db, "commit-a", "commit-d")).toBe("commit-a");
|
|
53
|
+
expect(findMergeBaseCommitId(db, "commit-d", "commit-a")).toBe("commit-a");
|
|
54
|
+
expect(findMergeBaseCommitId(db, "commit-c", "commit-d")).toBe("commit-c");
|
|
55
|
+
});
|
|
51
56
|
});
|
|
52
57
|
//# sourceMappingURL=commit-graph.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commit-graph.test.js","sourceRoot":"","sources":["../../../src/server/documents/commit-graph.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"commit-graph.test.js","sourceRoot":"","sources":["../../../src/server/documents/commit-graph.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACzF,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,IAAI,EAAqB,CAAC;IAC1B,MAAM,GAAG,GAAG,OAAO,CAAC;IACpB,MAAM,GAAG,GAAG,0BAA0B,CAAC;IAEvC,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,GAAG,IAAI,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC9B,WAAW,CAAC,EAAE,CAAC,CAAC;QAChB,EAAE,CAAC,OAAO,CACR;;;;6FAIuF,CACxF,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QAErB,cAAc;QACd,MAAM,CAAC,GAAG,UAAU,CAAC;QACrB,MAAM,CAAC,GAAG,UAAU,CAAC;QACrB,MAAM,CAAC,GAAG,UAAU,CAAC;QACrB,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI;YACvB,CAAC,CAAC,EAAE,IAAI,CAAC;YACT,CAAC,CAAC,EAAE,IAAI,CAAC;YACT,CAAC,CAAC,EAAE,IAAI,CAAC;SACD,EAAE,CAAC;YACX,YAAY,CAAC,EAAE,EAAE;gBACf,QAAQ,EAAE,EAAE;gBACZ,UAAU,EAAE,GAAG;gBACf,WAAW,EAAE,IAAI;gBACjB,OAAO,EAAE,GAAG,IAAI,OAAO;gBACvB,eAAe,EAAE,IAAI;gBACrB,SAAS,EAAE,GAAG;aACf,CAAC,CAAC;QACL,CAAC;QACD,kBAAkB,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC;QAChE,kBAAkB,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC;QAChE,EAAE,CAAC,OAAO,CAAC,+DAA+D,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;QAC3B,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5D,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5D,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACxE,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QACjF,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IACnF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,qBAAqB,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC3E,MAAM,CAAC,qBAAqB,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC3E,MAAM,CAAC,qBAAqB,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -8,8 +8,10 @@ import { createHash } from "node:crypto";
|
|
|
8
8
|
const testDbRef = vi.hoisted(() => ({ db: null }));
|
|
9
9
|
const fileStoreMocks = vi.hoisted(() => ({
|
|
10
10
|
lastWriteContent: "",
|
|
11
|
+
commitBlobs: new Map(),
|
|
11
12
|
reset() {
|
|
12
13
|
fileStoreMocks.lastWriteContent = "";
|
|
14
|
+
fileStoreMocks.commitBlobs.clear();
|
|
13
15
|
},
|
|
14
16
|
}));
|
|
15
17
|
vi.mock("../db/connection.js", () => ({
|
|
@@ -27,18 +29,28 @@ vi.mock("../storage/file-store.js", () => ({
|
|
|
27
29
|
.update(fileStoreMocks.lastWriteContent || "{}")
|
|
28
30
|
.digest("hex"),
|
|
29
31
|
}),
|
|
30
|
-
writeCommitBlob: (contentHash, content) =>
|
|
31
|
-
blobRef
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
writeCommitBlob: (contentHash, content) => {
|
|
33
|
+
const blobRef = `${contentHash.slice(0, 2)}/${contentHash.slice(2)}`;
|
|
34
|
+
fileStoreMocks.commitBlobs.set(blobRef, content);
|
|
35
|
+
return { blobRef, bytes: content.length };
|
|
36
|
+
},
|
|
37
|
+
readCommitBlob: (blobRef) => {
|
|
38
|
+
const content = fileStoreMocks.commitBlobs.get(blobRef);
|
|
39
|
+
if (!content)
|
|
40
|
+
throw new Error("commit blob not found");
|
|
41
|
+
return content;
|
|
42
|
+
},
|
|
34
43
|
deleteDocumentFile: () => { },
|
|
35
44
|
sha256: (buf) => createHash("sha256").update(buf).digest("hex"),
|
|
36
45
|
}));
|
|
37
46
|
import { DocumentError } from "../access/access-control.js";
|
|
38
|
-
import { createDocument, updateDocument } from "./document.service.js";
|
|
47
|
+
import { createDocument, getDocumentMergeContext, updateDocument } from "./document.service.js";
|
|
48
|
+
import { findCommitById } from "../db/repositories/commit.repo.js";
|
|
39
49
|
import { findDocumentById } from "../db/repositories/document.repo.js";
|
|
50
|
+
import { insertDocumentInvite } from "../db/repositories/document.repo.js";
|
|
40
51
|
import { countParents, listParentCommitIds, } from "../db/repositories/commit.repo.js";
|
|
41
52
|
const OWNER = "owner-commit-test";
|
|
53
|
+
const INVITED_EDITOR = "invited-editor";
|
|
42
54
|
beforeAll(() => {
|
|
43
55
|
const db = new Database(":memory:");
|
|
44
56
|
applySchema(db);
|
|
@@ -46,6 +58,8 @@ beforeAll(() => {
|
|
|
46
58
|
const now = new Date().toISOString();
|
|
47
59
|
db.prepare(`INSERT INTO visitors (visitor_id, visitor_name, visitor_token_hash, created_at)
|
|
48
60
|
VALUES (?, ?, ?, ?)`).run(OWNER, "Owner", "hash", now);
|
|
61
|
+
db.prepare(`INSERT INTO visitors (visitor_id, visitor_name, visitor_token_hash, created_at)
|
|
62
|
+
VALUES (?, ?, ?, ?)`).run(INVITED_EDITOR, "Invited", "hash2", now);
|
|
49
63
|
});
|
|
50
64
|
afterEach(() => {
|
|
51
65
|
const db = testDbRef.db;
|
|
@@ -56,6 +70,22 @@ afterEach(() => {
|
|
|
56
70
|
fileStoreMocks.reset();
|
|
57
71
|
});
|
|
58
72
|
describe("updateDocument 版本冲突", () => {
|
|
73
|
+
it("invite 编辑者发布后返回 invitedEdit=true", () => {
|
|
74
|
+
const created = createDocument({
|
|
75
|
+
actorVisitorId: OWNER,
|
|
76
|
+
fileName: "invite-edit.md",
|
|
77
|
+
content: '{"v":1}',
|
|
78
|
+
domainId: "default",
|
|
79
|
+
});
|
|
80
|
+
insertDocumentInvite(testDbRef.db, created.documentId, INVITED_EDITOR, "edit");
|
|
81
|
+
const updated = updateDocument({
|
|
82
|
+
actorVisitorId: INVITED_EDITOR,
|
|
83
|
+
documentId: created.documentId,
|
|
84
|
+
content: '{"v":2}',
|
|
85
|
+
version: { localBaseCommitId: created.headCommitId },
|
|
86
|
+
});
|
|
87
|
+
expect(updated.invitedEdit).toBe(true);
|
|
88
|
+
});
|
|
59
89
|
it("base 与 head 一致时成功并推进 head", () => {
|
|
60
90
|
const created = createDocument({
|
|
61
91
|
actorVisitorId: OWNER,
|
|
@@ -68,7 +98,7 @@ describe("updateDocument 版本冲突", () => {
|
|
|
68
98
|
actorVisitorId: OWNER,
|
|
69
99
|
documentId: created.documentId,
|
|
70
100
|
content: '{"v":2}',
|
|
71
|
-
version: {
|
|
101
|
+
version: { localBaseCommitId: base },
|
|
72
102
|
});
|
|
73
103
|
expect(updated.headCommitId).not.toBe(base);
|
|
74
104
|
const row = findDocumentById(testDbRef.db, created.documentId);
|
|
@@ -86,21 +116,21 @@ describe("updateDocument 版本冲突", () => {
|
|
|
86
116
|
actorVisitorId: OWNER,
|
|
87
117
|
documentId: created.documentId,
|
|
88
118
|
content: '{"v":2}',
|
|
89
|
-
version: {
|
|
119
|
+
version: { localBaseCommitId: base },
|
|
90
120
|
});
|
|
91
121
|
fileStoreMocks.lastWriteContent = '{"v":2}';
|
|
92
122
|
expect(() => updateDocument({
|
|
93
123
|
actorVisitorId: OWNER,
|
|
94
124
|
documentId: created.documentId,
|
|
95
125
|
content: '{"v":3}',
|
|
96
|
-
version: {
|
|
126
|
+
version: { localBaseCommitId: base },
|
|
97
127
|
})).toThrow(DocumentError);
|
|
98
128
|
try {
|
|
99
129
|
updateDocument({
|
|
100
130
|
actorVisitorId: OWNER,
|
|
101
131
|
documentId: created.documentId,
|
|
102
132
|
content: '{"v":3}',
|
|
103
|
-
version: {
|
|
133
|
+
version: { localBaseCommitId: base },
|
|
104
134
|
});
|
|
105
135
|
}
|
|
106
136
|
catch (e) {
|
|
@@ -140,7 +170,7 @@ describe("updateDocument merge 发布", () => {
|
|
|
140
170
|
actorVisitorId: OWNER,
|
|
141
171
|
documentId: created.documentId,
|
|
142
172
|
content: '{"remote":true}',
|
|
143
|
-
version: {
|
|
173
|
+
version: { localBaseCommitId: base },
|
|
144
174
|
});
|
|
145
175
|
const head = remote.headCommitId;
|
|
146
176
|
const merged = updateDocument({
|
|
@@ -148,9 +178,9 @@ describe("updateDocument merge 发布", () => {
|
|
|
148
178
|
documentId: created.documentId,
|
|
149
179
|
content: '{"merged":true}',
|
|
150
180
|
version: {
|
|
151
|
-
|
|
181
|
+
localBaseCommitId: base,
|
|
152
182
|
merge: {
|
|
153
|
-
|
|
183
|
+
remoteCommitId: head,
|
|
154
184
|
localSnapshotContent: '{"local":true}',
|
|
155
185
|
},
|
|
156
186
|
},
|
|
@@ -163,7 +193,7 @@ describe("updateDocument merge 发布", () => {
|
|
|
163
193
|
expect(localCommitId).toBeTruthy();
|
|
164
194
|
expect(listParentCommitIds(testDbRef.db, localCommitId)).toEqual([base]);
|
|
165
195
|
});
|
|
166
|
-
it("
|
|
196
|
+
it("remoteCommitId 与当前 head 不一致时 409", () => {
|
|
167
197
|
const created = createDocument({
|
|
168
198
|
actorVisitorId: OWNER,
|
|
169
199
|
fileName: "merge-stale.md",
|
|
@@ -175,23 +205,46 @@ describe("updateDocument merge 发布", () => {
|
|
|
175
205
|
actorVisitorId: OWNER,
|
|
176
206
|
documentId: created.documentId,
|
|
177
207
|
content: '{"r":1}',
|
|
178
|
-
version: {
|
|
208
|
+
version: { localBaseCommitId: base },
|
|
179
209
|
});
|
|
180
210
|
updateDocument({
|
|
181
211
|
actorVisitorId: OWNER,
|
|
182
212
|
documentId: created.documentId,
|
|
183
213
|
content: '{"r":2}',
|
|
184
|
-
version: {
|
|
214
|
+
version: { localBaseCommitId: remote.headCommitId },
|
|
185
215
|
});
|
|
186
216
|
expect(() => updateDocument({
|
|
187
217
|
actorVisitorId: OWNER,
|
|
188
218
|
documentId: created.documentId,
|
|
189
219
|
content: '{"m":1}',
|
|
190
220
|
version: {
|
|
191
|
-
|
|
192
|
-
merge: {
|
|
221
|
+
localBaseCommitId: base,
|
|
222
|
+
merge: { remoteCommitId: remote.headCommitId },
|
|
193
223
|
},
|
|
194
224
|
})).toThrow(DocumentError);
|
|
195
225
|
});
|
|
196
226
|
});
|
|
227
|
+
describe("getDocumentMergeContext", () => {
|
|
228
|
+
it("线性落后:LCA 为 localBase 并返回其 blob 正文", () => {
|
|
229
|
+
const created = createDocument({
|
|
230
|
+
actorVisitorId: OWNER,
|
|
231
|
+
fileName: "merge-ctx.md",
|
|
232
|
+
content: '{"v":1}',
|
|
233
|
+
domainId: "default",
|
|
234
|
+
});
|
|
235
|
+
const localBase = created.headCommitId;
|
|
236
|
+
const remote = updateDocument({
|
|
237
|
+
actorVisitorId: OWNER,
|
|
238
|
+
documentId: created.documentId,
|
|
239
|
+
content: '{"v":2}',
|
|
240
|
+
version: { localBaseCommitId: localBase },
|
|
241
|
+
});
|
|
242
|
+
const ctx = getDocumentMergeContext(created.documentId, localBase, remote.headCommitId);
|
|
243
|
+
expect(ctx.mode).toBe("three_way");
|
|
244
|
+
expect(ctx.mergeBaseCommitId).toBe(localBase);
|
|
245
|
+
const baseCommit = findCommitById(testDbRef.db, localBase);
|
|
246
|
+
expect(ctx.mergeBaseContent).toBe(fileStoreMocks.commitBlobs.get(baseCommit.blob_ref));
|
|
247
|
+
expect(ctx.mergeBaseContent).toBe('{"v":1}');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
197
250
|
//# sourceMappingURL=document-commit.integration.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-commit.integration.test.js","sourceRoot":"","sources":["../../../src/server/documents/document-commit.integration.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACxE,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,SAAS,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAgC,EAAE,CAAC,CAAC,CAAC;AAE/E,MAAM,cAAc,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACvC,gBAAgB,EAAE,EAAE;IACpB,KAAK;QACH,cAAc,CAAC,gBAAgB,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"document-commit.integration.test.js","sourceRoot":"","sources":["../../../src/server/documents/document-commit.integration.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,12 +2,13 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { getDb } from "../db/connection.js";
|
|
3
3
|
import { deleteDocument, findDocumentById, findDocumentByPath, findDocumentInvite, findChildrenByParent, insertDocument, insertDocumentInvite, deleteDocumentInvite, listDocumentInvites, listDocumentsByDomain, listDocumentsByPathPrefix, updateDocumentContent, updateDocumentHeadCommit, } from "../db/repositories/document.repo.js";
|
|
4
4
|
import { insertCommit, insertCommitParent } from "../db/repositories/commit.repo.js";
|
|
5
|
-
import { assertCommitBelongsToDocument, assertMergeFork, isAncestorOf, } from "./commit-graph.js";
|
|
5
|
+
import { assertCommitBelongsToDocument, assertMergeFork, findMergeBaseCommitId, isAncestorOf, } from "./commit-graph.js";
|
|
6
|
+
import { findCommitById } from "../db/repositories/commit.repo.js";
|
|
6
7
|
import { findDomainById, isDomainMember, } from "../db/repositories/domain.repo.js";
|
|
7
8
|
import { resolveDomainAccess, canEnterDomainTree, } from "../access/domain-access.js";
|
|
8
9
|
import { DocumentError, Permission, canReadDocument, canEditDocument, validateDomainPermission, } from "../access/access-control.js";
|
|
9
10
|
import { insertAuditLog } from "../db/repositories/audit.repo.js";
|
|
10
|
-
import { deleteDocumentFile, readDocument, sha256, writeCommitBlob, writeDocument, } from "../storage/file-store.js";
|
|
11
|
+
import { deleteDocumentFile, readCommitBlob, readDocument, sha256, writeCommitBlob, writeDocument, } from "../storage/file-store.js";
|
|
11
12
|
import { markDirty, removeIndex, rebuildDocument, } from "../search/document-index-manager.js";
|
|
12
13
|
import { getConfig } from "../config/index.js";
|
|
13
14
|
import { markdownToLexicalJson } from "./markdown-to-lexical.js";
|
|
@@ -263,12 +264,12 @@ export function updateDocument(params) {
|
|
|
263
264
|
const displayName = params.displayName?.trim() || row.display_name;
|
|
264
265
|
const now = new Date().toISOString();
|
|
265
266
|
const version = params.version;
|
|
266
|
-
const
|
|
267
|
+
const localBaseCommitId = version?.localBaseCommitId;
|
|
267
268
|
const mergeCtx = version?.merge;
|
|
268
269
|
// 冲突解决后的合并发布:插 r_local + 双父 merge 节点
|
|
269
270
|
if (mergeCtx) {
|
|
270
|
-
if (!
|
|
271
|
-
throw new DocumentError("BAD_REQUEST", "merge 发布需要在 version 中提供
|
|
271
|
+
if (!localBaseCommitId) {
|
|
272
|
+
throw new DocumentError("BAD_REQUEST", "merge 发布需要在 version 中提供 localBaseCommitId", 400);
|
|
272
273
|
}
|
|
273
274
|
const localSnapshotContent = mergeCtx.localSnapshotContent !== undefined
|
|
274
275
|
? normalizeDocumentContent(mergeCtx.localSnapshotContent, params.contentFormat)
|
|
@@ -279,14 +280,14 @@ export function updateDocument(params) {
|
|
|
279
280
|
content,
|
|
280
281
|
displayName,
|
|
281
282
|
permission: params.permission,
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
localBaseCommitId,
|
|
284
|
+
remoteCommitId: mergeCtx.remoteCommitId,
|
|
284
285
|
localSnapshotContent,
|
|
285
286
|
now,
|
|
286
287
|
});
|
|
287
288
|
}
|
|
288
289
|
// 普通线性发布:客户端 base 必须等于当前 head(未传 version 则跳过校验)
|
|
289
|
-
assertNoVersionConflict(db, row,
|
|
290
|
+
assertNoVersionConflict(db, row, localBaseCommitId);
|
|
290
291
|
const write = writeDocument(row.domain_id, row.relative_path, content);
|
|
291
292
|
const commitId = randomUUID();
|
|
292
293
|
const blob = writeCommitBlob(write.contentHash, content);
|
|
@@ -339,6 +340,10 @@ export function updateDocument(params) {
|
|
|
339
340
|
// ignore
|
|
340
341
|
}
|
|
341
342
|
});
|
|
343
|
+
// 写后直接返回最新 DocumentDetail(而非要求前端再 GET):
|
|
344
|
+
// 1) 降低竞争态:避免 PUT 与后续 GET 间隙被他人再次推进 head;
|
|
345
|
+
// 2) 单一真相:以后端实际落库结果为准(权限、head、invite 派生字段等)。
|
|
346
|
+
const invitedEdit = resolveInvitedEditFlag(db, row, params.actorVisitorId);
|
|
342
347
|
return buildDocumentDetail(row, {
|
|
343
348
|
displayName,
|
|
344
349
|
content,
|
|
@@ -347,6 +352,7 @@ export function updateDocument(params) {
|
|
|
347
352
|
updatedAt: now,
|
|
348
353
|
permission: params.permission !== undefined ? params.permission : row.permission,
|
|
349
354
|
headCommitId: commitId,
|
|
355
|
+
invitedEdit,
|
|
350
356
|
});
|
|
351
357
|
}
|
|
352
358
|
// ============================================================
|
|
@@ -505,6 +511,7 @@ function buildDocumentDetail(row, overrides) {
|
|
|
505
511
|
updatedBy: overrides.updatedBy,
|
|
506
512
|
updatedAt: overrides.updatedAt,
|
|
507
513
|
permission: overrides.permission,
|
|
514
|
+
...(overrides.invitedEdit ? { invitedEdit: true } : {}),
|
|
508
515
|
};
|
|
509
516
|
}
|
|
510
517
|
/**
|
|
@@ -519,19 +526,19 @@ function throwVersionConflict(row, serverContent, serverContentHash) {
|
|
|
519
526
|
});
|
|
520
527
|
}
|
|
521
528
|
/**
|
|
522
|
-
* 普通发布前的乐观锁:
|
|
523
|
-
* 未传
|
|
529
|
+
* 普通发布前的乐观锁:localBaseCommitId 与 head 不一致则冲突。
|
|
530
|
+
* 未传 localBaseCommitId 时跳过(兼容尚未接入的前端)。
|
|
524
531
|
*/
|
|
525
|
-
function assertNoVersionConflict(db, row,
|
|
526
|
-
// 客户端没传
|
|
532
|
+
function assertNoVersionConflict(db, row, localBaseCommitId) {
|
|
533
|
+
// 客户端没传 localBase:旧版前端或未启用冲突检测,直接放行(不进行冲突校验)
|
|
527
534
|
// 文档尚无 head:历史数据/异常态,无法比对,也放行
|
|
528
|
-
if (!
|
|
535
|
+
if (!localBaseCommitId || !row.head_commit_id)
|
|
529
536
|
return;
|
|
530
537
|
// 客户端认为的「我基于的那一版」仍等于服务端当前 head → 无人插队,可 fast-forward 发布
|
|
531
|
-
if (
|
|
538
|
+
if (localBaseCommitId === row.head_commit_id)
|
|
532
539
|
return;
|
|
533
|
-
// 走到这里:
|
|
534
|
-
assertCommitBelongsToDocument(db,
|
|
540
|
+
// 走到这里:localBase 有值且与 head 不同 → 说明在你编辑期间 head 已被别人/另一标签推进 → 409
|
|
541
|
+
assertCommitBelongsToDocument(db, localBaseCommitId, row.document_id);
|
|
535
542
|
const { content, contentHash } = readDocument(row.domain_id, row.relative_path);
|
|
536
543
|
throwVersionConflict(row, content, contentHash);
|
|
537
544
|
}
|
|
@@ -540,14 +547,14 @@ function assertNoVersionConflict(db, row, baseCommitId) {
|
|
|
540
547
|
*
|
|
541
548
|
* 提交图(merge 完成后)示意:
|
|
542
549
|
*
|
|
543
|
-
*
|
|
544
|
-
* ├──
|
|
545
|
-
* └── r_local
|
|
550
|
+
* localBaseCommitId(开编分叉点,version.localBaseCommitId)
|
|
551
|
+
* ├── remoteCommitId(冲突时远端 tip,version.merge.remoteCommitId)
|
|
552
|
+
* └── localCommitId(r_local,本次首次落库;父 = localBaseCommitId)
|
|
546
553
|
* \ /
|
|
547
|
-
*
|
|
554
|
+
* mergeCommitId(合成稿,新 head;父 = remoteCommitId + localCommitId)
|
|
548
555
|
*
|
|
549
556
|
* 步骤:
|
|
550
|
-
* 1. 校验:
|
|
557
|
+
* 1. 校验:remoteCommitId 仍为当前 head;localBase 是 remote 的祖先(assertMergeFork)
|
|
551
558
|
* 2. 写盘:工作区 = 合成稿;commits 目录落 r_local / merge 快照(writeCommitBlob)
|
|
552
559
|
* 3. 事务:插 r_local + merge 节点与边,更新 documents.head_commit_id
|
|
553
560
|
*/
|
|
@@ -559,13 +566,13 @@ function publishMergeDocument(params) {
|
|
|
559
566
|
throw new DocumentError("BAD_REQUEST", "文档尚无提交历史,无法合并发布", 400);
|
|
560
567
|
}
|
|
561
568
|
// 合并过程中若 head 又被别人推进,同样 409
|
|
562
|
-
if (params.
|
|
569
|
+
if (params.remoteCommitId !== row.head_commit_id) {
|
|
563
570
|
const { content: serverContent, contentHash } = readDocument(row.domain_id, row.relative_path);
|
|
564
571
|
throwVersionConflict(row, serverContent, contentHash);
|
|
565
572
|
}
|
|
566
|
-
assertCommitBelongsToDocument(db, params.
|
|
567
|
-
assertCommitBelongsToDocument(db, params.
|
|
568
|
-
assertMergeFork(db, params.
|
|
573
|
+
assertCommitBelongsToDocument(db, params.localBaseCommitId, row.document_id);
|
|
574
|
+
assertCommitBelongsToDocument(db, params.remoteCommitId, row.document_id);
|
|
575
|
+
assertMergeFork(db, params.localBaseCommitId, params.remoteCommitId);
|
|
569
576
|
// 写盘:工作区 + 两份历史快照(r_local 支 / merge 结果)
|
|
570
577
|
const localContent = params.localSnapshotContent ?? content;
|
|
571
578
|
const localHash = sha256(Buffer.from(localContent, "utf8"));
|
|
@@ -597,7 +604,7 @@ function publishMergeDocument(params) {
|
|
|
597
604
|
});
|
|
598
605
|
insertCommitParent(db, {
|
|
599
606
|
childCommitId: localCommitId,
|
|
600
|
-
parentCommitId: params.
|
|
607
|
+
parentCommitId: params.localBaseCommitId,
|
|
601
608
|
});
|
|
602
609
|
updateDocumentContent(db, {
|
|
603
610
|
documentId: row.document_id,
|
|
@@ -620,7 +627,7 @@ function publishMergeDocument(params) {
|
|
|
620
627
|
// 这里存储的是远端head
|
|
621
628
|
insertCommitParent(db, {
|
|
622
629
|
childCommitId: mergeCommitId,
|
|
623
|
-
parentCommitId: params.
|
|
630
|
+
parentCommitId: params.remoteCommitId,
|
|
624
631
|
});
|
|
625
632
|
// 这里存储的是r_local
|
|
626
633
|
insertCommitParent(db, {
|
|
@@ -639,8 +646,8 @@ function publishMergeDocument(params) {
|
|
|
639
646
|
targetType: "document",
|
|
640
647
|
targetId: row.document_id,
|
|
641
648
|
metadata: {
|
|
642
|
-
|
|
643
|
-
|
|
649
|
+
localBaseCommitId: params.localBaseCommitId,
|
|
650
|
+
remoteCommitId: params.remoteCommitId,
|
|
644
651
|
localCommitId,
|
|
645
652
|
mergeCommitId,
|
|
646
653
|
},
|
|
@@ -657,6 +664,10 @@ function publishMergeDocument(params) {
|
|
|
657
664
|
// ignore
|
|
658
665
|
}
|
|
659
666
|
});
|
|
667
|
+
// merge 发布同样采用“写后即返回最新资源”:
|
|
668
|
+
// - 避免前端二次查询引入时序偏差;
|
|
669
|
+
// - 确保 UI 使用服务端合并后的最终状态作为唯一真相。
|
|
670
|
+
const invitedEdit = resolveInvitedEditFlag(db, row, actorVisitorId);
|
|
660
671
|
return buildDocumentDetail(row, {
|
|
661
672
|
displayName,
|
|
662
673
|
content,
|
|
@@ -665,6 +676,7 @@ function publishMergeDocument(params) {
|
|
|
665
676
|
updatedAt: now,
|
|
666
677
|
permission: params.permission !== undefined ? params.permission : row.permission,
|
|
667
678
|
headCommitId: mergeCommitId,
|
|
679
|
+
invitedEdit,
|
|
668
680
|
});
|
|
669
681
|
}
|
|
670
682
|
function deriveDisplayName(raw, relativePath) {
|
|
@@ -678,32 +690,68 @@ function deriveDisplayName(raw, relativePath) {
|
|
|
678
690
|
function normalizeDocumentContent(content, contentFormat) {
|
|
679
691
|
return contentFormat === "markdown" ? markdownToLexicalJson(content) : content;
|
|
680
692
|
}
|
|
693
|
+
function resolveInvitedEditFlag(db, row, actorVisitorId) {
|
|
694
|
+
if (row.owner_visitor_id === actorVisitorId)
|
|
695
|
+
return false;
|
|
696
|
+
const invite = findDocumentInvite(db, row.document_id, actorVisitorId);
|
|
697
|
+
return invite?.permission === "edit";
|
|
698
|
+
}
|
|
681
699
|
// ============================================================
|
|
682
700
|
// 同步状态(轻量轮询,不返回正文)
|
|
683
701
|
// ============================================================
|
|
684
702
|
/**
|
|
685
|
-
* 比较客户端
|
|
686
|
-
* - up_to_date:
|
|
687
|
-
* - behind:
|
|
688
|
-
* - ahead
|
|
703
|
+
* 比较客户端 localBaseCommitId 与服务端 head(remoteCommitId),用于 Sync 指示。
|
|
704
|
+
* - up_to_date:localBase 未传、无 head、或 localBase === head
|
|
705
|
+
* - behind:localBase 是 head 的祖先(远端有更新)
|
|
706
|
+
* - ahead:其余(含分叉、历史回退等)
|
|
689
707
|
*/
|
|
690
|
-
export function getDocumentSyncStatus(documentId,
|
|
708
|
+
export function getDocumentSyncStatus(documentId, localBaseCommitId) {
|
|
691
709
|
const row = findDocumentById(getDb(), documentId);
|
|
692
710
|
if (!row)
|
|
693
711
|
throw new DocumentError("DOC_NOT_FOUND", "文档不存在", 404);
|
|
694
712
|
const head = row.head_commit_id;
|
|
695
|
-
if (!
|
|
713
|
+
if (!localBaseCommitId || !head) {
|
|
696
714
|
return { status: "up_to_date", headCommitId: head };
|
|
697
715
|
}
|
|
698
|
-
if (
|
|
716
|
+
if (localBaseCommitId === head) {
|
|
699
717
|
return { status: "up_to_date", headCommitId: head };
|
|
700
718
|
}
|
|
701
719
|
const db = getDb();
|
|
702
|
-
if (isAncestorOf(db,
|
|
720
|
+
if (isAncestorOf(db, localBaseCommitId, head)) {
|
|
703
721
|
return { status: "behind", headCommitId: head };
|
|
704
722
|
}
|
|
705
723
|
return { status: "ahead", headCommitId: head };
|
|
706
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* 为 merge UI 提供 LCA 正文:根据 localBase / remote 在 DAG 上求 mergeBase,读取 blob。
|
|
727
|
+
* 无法求 LCA 或读 blob 失败时返回 two_way(前端可退化为开编快照或两方 diff)。
|
|
728
|
+
*/
|
|
729
|
+
export function getDocumentMergeContext(documentId, localBaseCommitId, remoteCommitId) {
|
|
730
|
+
const row = findDocumentById(getDb(), documentId);
|
|
731
|
+
if (!row)
|
|
732
|
+
throw new DocumentError("DOC_NOT_FOUND", "文档不存在", 404);
|
|
733
|
+
const db = getDb();
|
|
734
|
+
assertCommitBelongsToDocument(db, localBaseCommitId, documentId);
|
|
735
|
+
assertCommitBelongsToDocument(db, remoteCommitId, documentId);
|
|
736
|
+
let mergeBaseCommitId;
|
|
737
|
+
try {
|
|
738
|
+
mergeBaseCommitId = findMergeBaseCommitId(db, localBaseCommitId, remoteCommitId);
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
return { mode: "two_way", mergeBaseCommitId: null, mergeBaseContent: null };
|
|
742
|
+
}
|
|
743
|
+
const baseCommit = findCommitById(db, mergeBaseCommitId);
|
|
744
|
+
if (!baseCommit) {
|
|
745
|
+
return { mode: "two_way", mergeBaseCommitId: null, mergeBaseContent: null };
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
const mergeBaseContent = readCommitBlob(baseCommit.blob_ref);
|
|
749
|
+
return { mode: "three_way", mergeBaseCommitId, mergeBaseContent };
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
return { mode: "two_way", mergeBaseCommitId: null, mergeBaseContent: null };
|
|
753
|
+
}
|
|
754
|
+
}
|
|
707
755
|
/** 内容格式转换(merge 发布:markdown → Lexical)。 */
|
|
708
756
|
export function convertDocumentContent(params) {
|
|
709
757
|
if (params.from === params.to)
|