# 合并分支与冲突解决

> 文档：`vjcad/doc/version/versioning.md`
> 示例：
> - `vjcad/example/version/03mergebranch.js` — 完整 merge workflow
> - `vjcad/example/version/05conflictresolve.js` — 冲突场景 + 弹框处理

## 1. 合并入口

```js
const drawingManager = new DrawingManagerService();

const mergeResult = await drawingManager.mergeBranch({
    type:          'imports',       // 必填（imports | designs）
    mapid:         'test',
    version:       'v1',
    sourceBranch:  'feature-x',     // 要合并进来的分支
    targetBranch:  'main',          // 合并到哪条分支
    /* conflictResolution: ...      // 第二轮合并时传 */
});
```

返回 `IMergeBranchResponse`（节选）：

```ts
{
    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. 无冲突路径

```js
if (mergeResult.status && !mergeResult.conflict?.hasConflict) {
    // 响应字段是 status / patchId（不是 success / newPatchId）
    message.success('合并成功，新补丁：' + mergeResult.patchId);
    Engine.currentDoc.serverSource.lastPatchId = mergeResult.patchId;
}
```

## 3. 有冲突：使用 ConflictResolutionDialog

```js
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`）：

```ts
{
    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. 工作流回顾（无冲突示例）

```js
// 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` 命令；通常项目自己实现：

```js
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` — 冲突场景 + 弹框处理
