合并分支与冲突解决
文档:
vjcad/doc/version/versioning.md示例:
vjcad/example/version/03mergebranch.js— 完整 merge workflowvjcad/example/version/05conflictresolve.js— 冲突场景 + 弹框处理
1. 合并入口
const drawingManager = new DrawingManagerService();
const mergeResult = await drawingManager.mergeBranch({
type: 'imports', // 必填(imports | designs)
mapid: 'test',
version: 'v1',
sourceBranch: 'feature-x', // 要合并进来的分支
targetBranch: 'main', // 合并到哪条分支
/* conflictResolution: ... // 第二轮合并时传 */
});
返回 IMergeBranchResponse(节选):
{
status: boolean; // 不是 success
patchId?: string; // 成功时:合并后产生的新 patch(不是 newPatchId)
targetBranch?: string;
idMapping?: Record<string, number>;
conflict?: {
hasConflict: boolean;
conflictingEntities: IConflictEntityInfo[]; // {entityId, entityType?, serverVersion?, clientVersion?, conflictingProperties?}
// ⚠️ IMergeBranchResponse.conflict 没有 conflictingPatches 字段
// (conflictingPatches 在 ISaveWebcadPatchResponse.conflict 上)
conflictingLayers?: IConflictLayerInfo[]; // 类型上有,但合并路径服务端不检测图层冲突,恒为空
latestPatchId?: string; // ⚠️ 在 conflict 内部,不是顶层;合并冲突时服务端不返回
sourceBranch: string; // ⚠️ 同上
targetBranch: string;
};
error?: string;
errorCode?: string;
}
2. 无冲突路径
if (mergeResult.status && !mergeResult.conflict?.hasConflict) {
// 响应字段是 status / patchId(不是 success / newPatchId)
message.success('合并成功,新补丁:' + mergeResult.patchId);
Engine.currentDoc.serverSource.lastPatchId = mergeResult.patchId;
}
3. 有冲突:使用 ConflictResolutionDialog
if (mergeResult.conflict?.hasConflict) {
const dialog = new ConflictResolutionDialog();
const resolution = await dialog.showDialog({
conflictingEntities: mergeResult.conflict.conflictingEntities,
latestPatchId: '' // 合并冲突不返回 latestPatchId,传 ''(DrawingBrowserDialog 同款)
});
if (resolution && resolution.action === 'resolve') {
// ⚠️ 对话框返回 choice: 'server'|'client',mergeBranch 要 'target'|'source':
// 'server'(目标分支) → 'target';'client'(源分支) → 'source'(DrawingBrowserDialog 同款映射)
const mergeConflictResolution = {};
for (const [id, r] of Object.entries(resolution.resolution)) {
mergeConflictResolution[id] = {
choice: r.choice === 'server' ? 'target' : 'source',
entityData: r.entityData
};
}
const second = await drawingManager.mergeBranch({
type: 'imports', // type 必填
mapid: 'test', version: 'v1',
sourceBranch: 'feature-x',
targetBranch: 'main',
conflictResolution: mergeConflictResolution
});
if (second.status) message.success('冲突已解决');
}
}
resolution.resolution 真实结构(IConflictResolutionResult):
{
action: 'resolve' | 'cancel';
resolution?: Record<string, IEntityResolution>; // { entityId: { choice, entityData? } }
latestPatchId?: string;
}
interface IEntityResolution {
choice: 'server' | 'client'; // ⚠️ 是 server/client,不是 source/target
entityData?: any; // choice='client' 时对话框附上该实体在 Engine.currentDoc 里的完整数据
}
entityData 不是"人工编辑结果":服务端只在目标侧 已删除 该实体、而你选了 client/source 时,用它生成 add 操作把实体加回来;实体仍存在时忽略它、照常应用源侧修改。选 server/target 则丢弃源侧对该实体的全部操作。
完整冲突示例:
vjcad/example/version/05conflictresolve.js
4. 工作流回顾(无冲突示例)
// 1. 在 main 上创建 dev1
await drawingManager.createBranch({
type: 'imports', mapid: 'test', version: 'v1', branchName: 'dev1',
sourceBranch: 'main', sourcePatchId: 'base' // ICreateBranchParams 用 sourceXxx
});
// 2. 切到 dev1,加一些实体后保存
// ...openDbDoc → 修改 → saveDrawing...
// 3. 合并回 main
const r = await drawingManager.mergeBranch({
type: 'imports', // type 必填
mapid: 'test', version: 'v1',
sourceBranch: 'dev1',
targetBranch: 'main'
});
vjcad/example/version/03mergebranch.js 提供了完整的 createBranch → modify → save → merge 流程。
5. 冲突识别规则(简化)
| 场景 | 冲突? |
|---|---|
| 双方各自加了不同 id 的实体 | 否 |
| 双方都修改了同一 id 实体的不同字段 | 否(自动合并) |
| 双方都修改了同一 id 实体的同一字段(值不同) | 是 |
| 一方删除、另一方修改同一实体 | 是 |
| 双方都修改图层(颜色 / on / freeze) | 否:合并只对实体做冲突检测,源分支的图层改动直接应用(图层冲突只在保存补丁时检测) |
合并实现:服务端计算"共同祖先 → 源分支最新"的整体 diff 与"祖先 → 目标分支最新"的 diff,对双方都改过的实体做属性级比对,再把源侧 diff(按 conflictResolution 过滤后)作为 一个新 patch 追加到目标分支。源分支相对祖先没有变化时返回错误 Source branch has no changes to merge。
6. 命令式合并
vjcad 没有内建 MERGE 命令;通常项目自己实现:
class MergeBranchCommand {
async main() {
const branches = await drawingManager.listBranches({
type: 'imports', mapid, version // 对象参数
});
const opt = new KeywordInputOptions('请选择来源分支');
opt.keywords = branches.map(b => b.name); // IBranchInfo.name 不是 branchName
const source = await Engine.getKeyword(opt);
if (source.status !== InputStatusEnum.OK) return;
// 然后弹 ConflictResolutionDialog 或自定义 UI
}
}
红旗
- 冲突时 第二次 调用
mergeBranch必须带conflictResolution,否则服务端再次返回冲突 - 部分后端要求合并时
target分支没有未推送的远端 patch;先 listBranches 校验 conflictResolution的choice只有'source'/'target'(对话框是'client'/'server',需映射),没有manual选项- 合并成功后
serverSource.lastPatchId必须更新,否则后续 saveDrawing 出错 IConflictEntityInfo只有entityId/entityType?/serverVersion?(目标分支版)/clientVersion?(源分支版)/conflictingProperties?,没有.base(公共祖先)字段;版本对象里可能带_changes(属性操作列表)与_isPartialData
参考(官方文档 MCP)
- 文档:
vjcad/doc/version/versioning.md - 示例:
vjcad/example/version/03mergebranch.js— 完整 merge workflowvjcad/example/version/05conflictresolve.js— 冲突场景 + 弹框处理