Merging
Merging in Lix is the process of combining changes from different branches or development paths. This concept is fundamental to collaborative workflows and parallel development.
What is Merging?
Merging in Lix involves:
- Combining changes from two or more branches
- Creating a new change set that includes changes from all source branches
- Resolving any conflicts between divergent changes
- Preserving the history of all merged branches
Similar to Git, Lix uses a directed acyclic graph (DAG) to track changes, enabling sophisticated merging capabilities with fine-grained conflict detection.
The Merge Process
When you merge branches in Lix, the system:
- Identifies the common ancestor of the branches
- Determines the changes made in each branch since the common ancestor
- Combines non-conflicting changes automatically
- Identifies conflicting changes that need resolution
- Creates a new merge change set with multiple parents
Creating Merge Change Sets
Merge operations are performed using the createMergeChangeSet()
function:
import { createMergeChangeSet } from "@lix-js/sdk";
// Merge two branches
const mergeChangeSet = await createMergeChangeSet({
lix,
sources: [
{ id: mainBranchChangeSetId },
{ id: featureBranchChangeSetId }
]
});
console.log("Created merge change set:", mergeChangeSet.id);
The sources
parameter specifies the change sets to merge. The function returns a new change set that combines changes from all sources.
Merge Conflict Detection
Lix provides sophisticated conflict detection at the entity level, not just the file level. This means:
- Conflicts are detected at the property/field level
- Changes to different properties in the same file don't conflict
- Only changes to the same property from different branches create conflicts
For example, if branch A changes the title
of a document and branch B changes the content
of the same document, these changes don't conflict and can be merged automatically.
Conflict Resolution
When conflicts are detected during a merge, Lix provides several approaches to resolution:
Manual Resolution
You can manually resolve conflicts by:
- Identifying the conflicting changes
- Choosing which version to keep or creating a new combined version
- Creating a new change with the resolved content
- Adding this change to the merge change set
Here's an example of manual conflict resolution:
// Identify conflicts
const conflicts = await lix.db
.selectFrom("change")
.whereExists(qb =>
qb.selectFrom("change_set_element")
.whereRef("change_set_element.change_id", "=", "change.id")
.where("change_set_element.change_set_id", "=", mergeChangeSetId)
.where("change_set_element.conflict", "=", true)
)
.selectAll()
.execute();
// For each conflict, create a resolution
for (const conflict of conflicts) {
// Create a new change with resolved content
const resolvedChange = await createChange({
lix,
entity_id: conflict.entity_id,
schema_key: conflict.schema_key,
schema_version: conflict.schema_version,
file_id: conflict.file_id,
plugin_key: conflict.plugin_key,
// Set the resolved value here
snapshot: { /* resolved data */ }
});
// Add the resolved change to the merge change set
await lix.db
.updateTable("change_set_element")
.set({
conflict: false,
resolution_change_id: resolvedChange.id
})
.where("change_set_id", "=", mergeChangeSetId)
.where("change_id", "=", conflict.id)
.execute();
}
Automatic Resolution Strategies
For simpler conflicts, you might implement automatic resolution strategies:
- Keep Source: Always prefer changes from a specific branch
- Last Writer Wins: Choose the most recent change based on timestamp
- Combine Values: For certain data types, automatically merge the values
Merge Examples
Example: Merging Document Changes
Here's an example of merging changes to a document from different branches:
// Start with a common ancestor
await handleFileInsert({
lix,
file: {
path: "/document.json",
data: new TextEncoder().encode(JSON.stringify({
title: "Original Title",
content: "Original content",
metadata: { author: "User" }
}))
}
});
// Create the base change set
const baseChangeSet = await createChangeSet({ lix });
// Create Branch A: modify the title
await handleFileUpdate({
lix,
file: {
path: "/document.json",
data: new TextEncoder().encode(JSON.stringify({
title: "Updated Title",
content: "Original content",
metadata: { author: "User" }
}))
}
});
// Create change set for Branch A
const branchAChangeSet = await createChangeSet({ lix });
// Switch back to the base change set
await switchVersion({
lix,
to: baseChangeSet.id
});
// Create Branch B: modify the content
await handleFileUpdate({
lix,
file: {
path: "/document.json",
data: new TextEncoder().encode(JSON.stringify({
title: "Original Title",
content: "Updated content with new information",
metadata: { author: "User", lastModified: "2023-06-01" }
}))
}
});
// Create change set for Branch B
const branchBChangeSet = await createChangeSet({ lix });
// Merge the branches
const mergeChangeSet = await createMergeChangeSet({
lix,
sources: [
{ id: branchAChangeSet.id },
{ id: branchBChangeSet.id }
]
});
// Apply the merge change set
await applyChangeSet({
lix,
changeSet: mergeChangeSet
});
// The document now has changes from both branches:
// - title from Branch A
// - content and metadata from Branch B
Example: Resolving Conflicts
Here's an example that includes conflict resolution:
// Start with a common base
await handleFileInsert({
lix,
file: {
path: "/config.json",
data: new TextEncoder().encode(JSON.stringify({
theme: "light",
language: "en",
features: ["search", "notifications"]
}))
}
});
// Create base change set
const baseChangeSet = await createChangeSet({ lix });
// Branch A: Change theme and add a feature
await handleFileUpdate({
lix,
file: {
path: "/config.json",
data: new TextEncoder().encode(JSON.stringify({
theme: "dark",
language: "en",
features: ["search", "notifications", "offline-mode"]
}))
}
});
const branchAChangeSet = await createChangeSet({ lix });
// Switch back to base
await switchVersion({
lix,
to: baseChangeSet.id
});
// Branch B: Change theme and language
await handleFileUpdate({
lix,
file: {
path: "/config.json",
data: new TextEncoder().encode(JSON.stringify({
theme: "blue",
language: "fr",
features: ["search", "notifications"]
}))
}
});
const branchBChangeSet = await createChangeSet({ lix });
// Attempt to merge (will detect conflicts on the 'theme' property)
const mergeChangeSet = await createMergeChangeSet({
lix,
sources: [
{ id: branchAChangeSet.id },
{ id: branchBChangeSet.id }
]
});
// Resolve the conflict manually
const conflicts = await lix.db
.selectFrom("change")
.whereExists(qb =>
qb.selectFrom("change_set_element")
.whereRef("change_set_element.change_id", "=", "change.id")
.where("change_set_element.change_set_id", "=", mergeChangeSet.id)
.where("change_set_element.conflict", "=", true)
)
.selectAll()
.execute();
// Create a resolution (choosing the dark theme)
if (conflicts.length > 0) {
// Get the current file content
const file = await lix.db
.selectFrom("file")
.where("path", "=", "/config.json")
.selectAll()
.executeTakeFirstOrThrow();
// Create a resolved version
const resolvedData = {
theme: "dark", // Choose dark theme from Branch A
language: "fr", // Keep language from Branch B
features: ["search", "notifications", "offline-mode"] // Keep expanded features from Branch A
};
// Update the file with the resolved content
await handleFileUpdate({
lix,
file: {
path: "/config.json",
data: new TextEncoder().encode(JSON.stringify(resolvedData))
}
});
// Create a resolution change set
const resolutionChangeSet = await createChangeSet({ lix });
// Complete the merge by linking the resolution
await lix.db
.updateTable("change_set_edge")
.set({
resolution_change_set_id: resolutionChangeSet.id
})
.where("child_id", "=", mergeChangeSet.id)
.execute();
}
Visualizing Merges
Merges create a diamond pattern in the change graph:
D --- E Feature Branch
/ \
A --- B --- C --- F Main Branch
In this diagram:
- Branch point is at B
- Feature branch includes changes D and E
- Main branch continues with change C
- F is the merge change set that combines changes from both branches
Next Steps
Now that you understand merging in Lix, explore these related concepts: