Merges API
Diff branches, open merge requests, resolve conflicts, and merge.
Merge methods diff two refs of an entry, open and manage merge requests, resolve conflicts, and integrate one branch into another. They live at cms.api.<collection>.<method> and mirror on the client with identical types (the examples below use pages as the sample collection). Read methods take a query object, write methods take a body object.
Every commit-producing mutation returns commit: { id, message, createdAt, createdBy }, the new head after the change.
Diff Two Refs
Diff two refs of one entry to see exactly what changed between them, as a flat change list, an annotated render tree, or both. Each side is a branch (resolved to its head commit) or a commit, and the target can also be the entry's current publication, so you can preview precisely which draft edits are not yet live. The base is the common ancestor of the two resolved commits.
mergeRequest:read/{collection}/getDiffconst data = await cms.api.pages.getDiff({
query: {
sourceBranchId: 'br_draft', // one source ref: sourceBranchId XOR sourceCommitId
targetBranchId: 'br_main', // one target ref: targetBranchId, targetCommitId, or targetPublished
},
});const { data, error } = await client.pages.getDiff({
query: {
sourceBranchId: 'br_draft', // one source ref: sourceBranchId XOR sourceCommitId
targetBranchId: 'br_main', // one target ref: targetBranchId, targetCommitId, or targetPublished
},
});sourceBranchIdstringSource branch ref. Provide exactly one of sourceBranchId or sourceCommitId.
sourceCommitIdstringSource commit ref. Provide exactly one of sourceBranchId or sourceCommitId.
targetBranchIdstringTarget branch ref. Provide exactly one of targetBranchId, targetCommitId, or targetPublished.
targetCommitIdstringTarget commit ref. Provide exactly one of targetBranchId, targetCommitId, or targetPublished.
targetPublishedbooleanDiff against the source root's current publication (the published branch's live head).
view'list' | 'tree' | 'both'= 'both'Which representations to return. `diff` is null when 'tree', `tree` is null when 'list'.
withAttributionboolean= falseAttach per-entry attribution (commit id, timestamp, author) to each change.
diffBlockChange[] | nullThe flat change list (see The diff shape below). `null` when `view` is `tree`.
treeAnnotatedBlockTreeNode | nullThe annotated draft tree. `null` when `view` is `list`, and when the source ref deleted the entry root block.
summaryDiffSummaryPer-changeType entry counts: `{ added, deleted, modified, moved, reordered }` (all numbers).
sourceCommitIdstringThe commit the source ref resolved to.
targetCommitIdstringThe commit the target ref resolved to.
commonAncestorCommitIdstringThe common ancestor of the two resolved commits (the diff base).
Both refs must belong to the same entry (BRANCHES_NOT_SAME_ROOT otherwise). The base is the common ancestor of the two resolved commits; when one commit is an ancestor of the other, the common ancestor is that commit and the diff degenerates to an exact two-way comparison (a branch against targetPublished yields exactly the draft edits that are not live yet, the publish preview).
The diff shape
diff(withview: 'list'or'both', otherwisenull): the flat change list. Each entry carriesblockId,changeTypes(added,deleted,modified,moved,childrenReordered),propertyChangeswhen modified (per-propertypath,kind,from,to, plus word-leveltextDiffsegments forrichTextproperties),typeChangewhen the block type changed,slugChangeon the root entry when the draft slug changed,moved(kind: 'reparented' | 'reordered'with old and new parent id and index), and the fullsourceVersion/targetVersion/baseVersionpayloads.movedmarks only blocks that actually moved (siblings whose index merely shifted are not marked), andchildrenReorderedonly parents whose surviving children truly changed relative order. A slug-only change is notmodified: it surfaces as a root entry with an emptychangeTypesarray and onlyslugChangeset.tree(withview: 'tree'or'both'): the annotated draft tree, the source branch's block tree where changed nodes carry adiffannotation and deleted blocks are re-inserted as ghost nodes at their old position. It is structurally a regular block tree, so it renders through the same component maps.nullwhen the view is'list', and also when the draft deleted the entry's root block (guard before rendering).summary: per-changeType entry counts{ added, deleted, modified, moved, reordered }.
With withAttribution: true, each diff entry and each tree annotation additionally carries attribution: { commitId, changedAt, changedBy, changedByUser? } — which commit made the change, and who. changedByUser is present only when called with query.withUser, limited to your user config's exposeColumns allowlist. See Review changes visually for rendering the annotated tree.
Check for Conflicts
Check whether two branches would collide before you open a merge request, using a three-way merge (base, source, target). You get back the conflicting blocks and a hasConflicts flag, so you can surface trouble to the author up front.
mergeRequest:read/{collection}/checkConflictsconst data = await cms.api.pages.checkConflicts({
query: {
sourceBranchId: 'br_draft', // required
targetBranchId: 'br_main', // required
},
});const { data, error } = await client.pages.checkConflicts({
query: {
sourceBranchId: 'br_draft', // required
targetBranchId: 'br_main', // required
},
});sourceBranchIdstringrequiredSource branch id.
targetBranchIdstringrequiredTarget branch id.
hasConflictsbooleanWhether any block conflicts between the two branches.
conflicts{ blockId, sourceVersionId, targetVersionId, baseVersionId }[]One entry per conflicting block. Each version id is `null` when that side has no version for the block.
commonAncestorCommitIdstringThe common ancestor of the two branch heads (the three-way base).
sourceCommitIdstringThe source branch's head commit.
targetCommitIdstringThe target branch's head commit.
Open a Merge Request
Open a merge request to merge the source branch into the target branch. You get the new request back together with any conflicts detected at creation time, so reviewers see them straight away. Fails if an open merge request already exists for this source-target pair.
mergeRequest:create/{collection}/createMergeRequestconst data = await cms.api.pages.createMergeRequest({
body: {
sourceBranchId: 'br_draft', // required
targetBranchId: 'br_main', // required
title: 'Publish homepage refresh', // required
},
});const { data, error } = await client.pages.createMergeRequest({
body: {
sourceBranchId: 'br_draft', // required
targetBranchId: 'br_main', // required
title: 'Publish homepage refresh', // required
},
});sourceBranchIdstringrequiredSource branch to merge from.
targetBranchIdstringrequiredTarget branch to merge into.
titlestringrequiredMerge request title (at least 1 character).
descriptionstringLonger description.
createdBystringExplicit actor id, used only when the request has no session user (context takes precedence).
mergeRequestMergeRequestThe newly created merge request row: id, root id, source/target branch ids, status, title, commit ids, and timestamps.
hasConflictsbooleanWhether conflicts were detected at creation time.
conflicts{ blockId, sourceVersionId, targetVersionId, baseVersionId }[]One entry per conflicting block, persisted against the merge request.
List Merge Requests
List the merge requests in a collection, filtering by status, branch, or creator and sorting however you need. All query fields are optional, and requests whose entry was archived are excluded.
mergeRequest:read/{collection}/listMergeRequestsconst data = await cms.api.pages.listMergeRequests({
query: {
status: 'open',
limit: 20,
},
});const { data, error } = await client.pages.listMergeRequests({
query: {
status: 'open',
limit: 20,
},
});limitnumber= 20Page size, 1 to 100.
offsetnumber= 0Rows to skip.
rootIdstringFilter by root id.
sourceBranchIdstringFilter by source branch.
targetBranchIdstringFilter by target branch.
status'open' | 'merged' | 'closed'Filter by status.
createdBystringFilter by creator.
searchstringSubstring match over title and description.
sortBy'createdAt' | 'updatedAt' | 'status' | 'title'= 'createdAt'Sort field.
sortDirection'asc' | 'desc'= 'desc'Sort order.
mergeRequestsMergeRequestListItem[]The merge requests on this page. Each carries branch names, status, conflict and comment counts, and `hasConflicts`.
totalnumberTotal matching merge requests across all pages, ignoring limit/offset.
hasMorebooleanWhether more merge requests exist after this page.
Update a Merge Request
Update the title and/or description of an open merge request. Passing neither is a no-op that returns the merge request unchanged.
mergeRequest:update/{collection}/updateMergeRequestconst data = await cms.api.pages.updateMergeRequest({
body: {
mergeRequestId: 'mr_8fd21c', // required
title: 'Updated title',
},
});const { data, error } = await client.pages.updateMergeRequest({
body: {
mergeRequestId: 'mr_8fd21c', // required
title: 'Updated title',
},
});mergeRequestIdstringrequiredThe merge request id.
titlestringNew title.
descriptionstringNew description.
mergeRequestMergeRequestThe updated merge request row.
Execute a Merge
Integrate a merge request into its target branch. It fast-forwards when the target has not diverged, otherwise it writes a merge commit; force a merge commit with noFastForward: true or config mergeStrategy: 'merge-commit'. Every conflict must be resolved first, and a pending approval request blocks the merge.
mergeRequest:update/{collection}/executeMergeconst data = await cms.api.pages.executeMerge({
body: {
mergeRequestId: 'mr_8fd21c', // required
},
});const { data, error } = await client.pages.executeMerge({
body: {
mergeRequestId: 'mr_8fd21c', // required
},
});mergeRequestIdstringrequiredThe merge request id.
mergedBystringExplicit actor id, used only when the request has no session user (context takes precedence).
messagestringCustom merge commit message (auto-generated if omitted).
noFastForwardbooleanForce a merge commit even when a fast-forward is possible (git's --no-ff). `false` forces a fast-forward.
commitCommitThe commit envelope `{ id, message, createdAt, createdBy }`: the merge commit on a non-fast-forward, or the source head that became the new target head on a fast-forward.
fastForwardbooleanWhether the merge fast-forwarded (no merge commit written).
rootIdstringThe merged entry's id.
targetBranchIdstringThe branch that received the merge.
commit is the merge commit on a non-fast-forward, or the source head that became the new target head on a fast-forward.
Create a Merge Block Version
Create a new block version to resolve a conflict by hand (a third-way resolution). The version is persisted for the conflict and goes live once the merge is executed. Input is a discriminated union over type, one variant per block definition plus 'root'.
mergeRequest:create/{collection}/createMergeBlockVersionconst data = await cms.api.pages.createMergeBlockVersion({
body: {
mergeRequestId: 'mr_8fd21c', // required
blockId: 'blk_hero', // required
type: 'hero', // required: a block name from the collection, or 'root'
properties: { title: 'Resolved heading' }, // required
},
});const { data, error } = await client.pages.createMergeBlockVersion({
body: {
mergeRequestId: 'mr_8fd21c', // required
blockId: 'blk_hero', // required
type: 'hero', // required: a block name from the collection, or 'root'
properties: { title: 'Resolved heading' }, // required
},
});mergeRequestIdstringrequiredThe merge request id.
blockIdstringrequiredThe conflicting block id.
typeblock name | 'root'requiredThe block's type literal (a block name from the collection, or 'root').
propertiestyped block propertiesrequiredFull properties for the resolved version, typed by `type`.
childrenstring[]Child block ids for the resolved version.
blockVersionIdstringThe id of the new block version, referenced later as a `manual` resolution.
Apply Conflict Resolutions
Resolve one or more conflicts at once, choosing which version to keep for each. A 'manual' resolution must reference an existing block version (from createMergeBlockVersion or a preexisting one). You get back the resolved rows and a count of the conflicts still outstanding.
mergeRequest:update/{collection}/applyConflictResolutionsconst data = await cms.api.pages.applyConflictResolutions({
body: {
mergeRequestId: 'mr_8fd21c', // required
resolutions: [
{ conflictId: 'cf_1a2b', resolution: 'source', resolvedBy: 'usr_1' }, // required, at least one
],
},
});const { data, error } = await client.pages.applyConflictResolutions({
body: {
mergeRequestId: 'mr_8fd21c', // required
resolutions: [
{ conflictId: 'cf_1a2b', resolution: 'source', resolvedBy: 'usr_1' }, // required, at least one
],
},
});mergeRequestIdstringrequiredThe merge request id.
resolutions{ conflictId, resolution, resolvedVersionId?, resolvedBy }[]requiredAt least one resolution. `resolution` is 'source', 'target', or 'manual'; a 'manual' resolution needs `resolvedVersionId`.
resolvedMergeConflict[]The updated conflict rows, each with its chosen `resolution`, `resolvedVersionId`, `resolvedBy`, and `resolvedAt`.
remainingUnresolvednumberHow many conflicts in the merge request still have no resolution.
Close a Merge Request
Close an open merge request without merging it. If someone other than the creator closes it, the creator gets notified.
mergeRequest:update/{collection}/closeMergeRequestconst data = await cms.api.pages.closeMergeRequest({
body: {
mergeRequestId: 'mr_8fd21c', // required
reason: 'No longer needed',
},
});const { data, error } = await client.pages.closeMergeRequest({
body: {
mergeRequestId: 'mr_8fd21c', // required
reason: 'No longer needed',
},
});mergeRequestIdstringrequiredThe merge request id.
reasonstringOptional reason for closing.
mergeRequestMergeRequestThe updated merge request row, now in `closed` status.
Reopen a Merge Request
Reopen a closed merge request back to open status. Fails if it was already merged, or if an open merge request already exists for the same source-target pair.
mergeRequest:update/{collection}/reopenMergeRequestconst data = await cms.api.pages.reopenMergeRequest({
body: {
mergeRequestId: 'mr_8fd21c', // required
},
});const { data, error } = await client.pages.reopenMergeRequest({
body: {
mergeRequestId: 'mr_8fd21c', // required
},
});mergeRequestIdstringrequiredThe merge request id.
mergeRequestMergeRequestThe updated merge request row, now back in `open` status.