唯杰地图 logo唯杰地图
VJMAP
EN

合并分支与冲突解决

vjcad/references/version/merge-resolve.md 7.4 KB174 行
原文

文档:vjcad/doc/version/versioning.md 示例:

  • vjcad/example/version/03mergebranch.js — 完整 merge workflow
  • vjcad/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 workflow
  • vjcad/example/version/05conflictresolve.js — 冲突场景 + 弹框处理