\Drupal\strata\Branch Merger

Plans and applies a merge of one config branch back into the trunk.

Configuration only, and the refusal names what it refused. A configuration object is captured WHOLE on every save, so its value at three commits is three complete documents and a three-way merge over them is well defined. Content entities and table rows are captured as FIELD DELTAS against a parent, so the same operation over them would mean replaying two divergent delta chains and inventing a value wherever they disagree - which is not a merge, it is data loss with extra steps. So a branch that received anything outside the configuration realm makes the plan refuse and names the realm and the subjects, rather than merging the config and quietly dropping the rest.

The target is always the trunk. A merge writes configuration to the live site, and the live site is what refs/heads/main describes. Merging into some other ref would write values the trunk does not have and leave the ref every restore resolves through describing a site that no longer exists.

The apply is a restricted logical restore, not a second write path. That is what gives the merge the forced pre-write snapshot, the per-object failure reporting and the audit row for free, and it is why there is no option here to skip the snapshot: the one moment somebody needs an undo is right after a merge they did not mean to run.

Three commits come out of an applied merge, in this order: the snapshot the restore forced, the ordinary commit that seals the configuration it wrote, and a two-parent merge commit whose second parent is the branch tip. That last one carries no segment; its whole job is to make the branch's commits reachable from the trunk, so a prune keeps them and a later merge of the same branch can see it has already been brought in.

Summary

Methods
Properties
Constants
__construct
branch
remove
list
plan
apply
merge
No public properties found
MAX_WALK
NAMED_SUBJECTS
No protected methods found
No protected properties found
No protected constants found
build
restorePlan
seal
touched
subjectsIn
configNames
refuseOtherRealms
readable
dataOf
actor
now
branches
refs
commits
index
mergeBase
threeWay
replayer
segments
restore
flusher
branchIndex
currentUser
logger
maxWalk
No private constants found

Constant

MAX_WALK

MAX_WALK = 5000

How many commits either side's walk reads before the plan gives up, when nothing says.

NAMED_SUBJECTS

NAMED_SUBJECTS = 20

How many subjects a refusal names before it stops listing them.

Properties

$branches

$branches : \Drupal\strata\Branch\BranchStore

Type

BranchStore

$refs

$refs : \Drupal\strata\Tree\RefStore

Type

RefStore

$commits

$commits : \Drupal\strata\Tree\CommitLog

Type

CommitLog

$index

$index : \Drupal\strata\Tree\CommitIndex

Type

CommitIndex

$mergeBase

$mergeBase : \Drupal\strata\Branch\MergeBase

Type

MergeBase

$threeWay

$threeWay : \Drupal\strata\Branch\ThreeWayMerge

Type

ThreeWayMerge

$replayer

$replayer : \Drupal\strata\Restore\Replayer

Type

Replayer

$segments

$segments : \Drupal\strata\Segment\SegmentReader

Type

SegmentReader

$restore

$restore : \Drupal\strata\Restore\LogicalRestore

Type

LogicalRestore

$flusher

$flusher : \Drupal\strata\Flush\Flusher

Type

Flusher

$branchIndex

$branchIndex : \Drupal\strata\Branch\BranchIndex

Type

BranchIndex

$currentUser

$currentUser : \Drupal\Core\Session\AccountProxyInterface

Type

AccountProxyInterface

$logger

$logger : \Psr\Log\LoggerInterface

Type

LoggerInterface

$maxWalk

$maxWalk : int

Type

int

Methods

__construct()

__construct(\Drupal\strata\Branch\BranchStore  $branches, \Drupal\strata\Tree\RefStore  $refs, \Drupal\strata\Tree\CommitLog  $commits, \Drupal\strata\Tree\CommitIndex  $index, \Drupal\strata\Branch\MergeBase  $mergeBase, \Drupal\strata\Branch\ThreeWayMerge  $threeWay, \Drupal\strata\Restore\Replayer  $replayer, \Drupal\strata\Segment\SegmentReader  $segments, \Drupal\strata\Restore\LogicalRestore  $restore, \Drupal\strata\Flush\Flusher  $flusher, \Drupal\strata\Branch\BranchIndex  $branchIndex, \Drupal\Core\Session\AccountProxyInterface  $currentUser, \Psr\Log\LoggerInterface  $logger, int  $maxWalk = \self::MAX_WALK) : mixed

Constructs a merger.

Parameters

\Drupal\strata\Branch\BranchStore $branches

Resolves the branch being merged.

\Drupal\strata\Tree\RefStore $refs

Resolves the trunk's tip.

\Drupal\strata\Tree\CommitLog $commits

Reads the commits either side added.

\Drupal\strata\Tree\CommitIndex $index

Where the merge commit is mirrored, and what MergeBase walks.

\Drupal\strata\Branch\MergeBase $mergeBase

Finds the commit the two sides last agreed at.

\Drupal\strata\Branch\ThreeWayMerge $threeWay

Merges one object across the three.

\Drupal\strata\Restore\Replayer $replayer

Reconstructs each object at each of the three commits.

\Drupal\strata\Segment\SegmentReader $segments

Reads what each commit on either side actually touched.

\Drupal\strata\Restore\LogicalRestore $restore

Writes the merged configuration back, with its forced snapshot.

\Drupal\strata\Flush\Flusher $flusher

Seals the configuration the merge wrote before the merge commit names it.

\Drupal\strata\Branch\BranchIndex $branchIndex

Kept in step when a branch moves.

\Drupal\Core\Session\AccountProxyInterface $currentUser

Attributes the merge to whoever ran it.

\Psr\Log\LoggerInterface $logger

Records the outcome.

int $maxWalk

How many commits either side is read for before the plan gives up. Each one costs a segment read, so this bounds object reads rather than queries.

Returns

mixed —

branch()

branch(string  $name, string|null  $from = null) : \Drupal\strata\Branch\Branch

Cuts a branch off a commit and indexes it.

Parameters

string $name

The branch name.

string|null $from

Commit to fork from, or NULL for the trunk's current tip.

Throws

\RuntimeException

When the name is taken, the trunk has no tip to fork from, or the commit is not present.

Returns

\Drupal\strata\Branch\Branch —

The branch.

remove()

remove(string  $name) : bool

Removes a branch and forgets its row.

Parameters

string $name

The branch name.

Throws

\RuntimeException

When the name is the trunk.

Returns

bool —

TRUE when a branch was removed.

list()

list() : array<string,\Drupal\strata\Branch\Branch>

Every branch, with its row kept in step with the store.

Returns

array

Branch name keyed to the branch.

plan()

plan(string  $name, \Drupal\strata\Branch\MergeStrategy  $strategy = \Drupal\strata\Branch\MergeStrategy::REFUSE) : \Drupal\strata\Branch\MergePlan

Works out what merging a branch into the trunk would do.

Parameters

string $name

The branch name.

\Drupal\strata\Branch\MergeStrategy $strategy

What to do with a key both sides changed.

Returns

\Drupal\strata\Branch\MergePlan —

The plan, which carries its own refusal when it could not be built.

apply()

apply(\Drupal\strata\Branch\MergePlan  $plan, bool  $apply = true) : \Drupal\strata\Branch\MergeResult

Applies a plan that has already been made.

The plan is applied as it was printed rather than recomputed after the answer, which is what makes the manifest an operator approved the thing that happens.

Parameters

\Drupal\strata\Branch\MergePlan $plan

The plan.

bool $apply

FALSE to report what the plan would do and write nothing.

Returns

\Drupal\strata\Branch\MergeResult —

What was written, or what would be.

merge()

merge(string  $name, \Drupal\strata\Branch\MergeStrategy  $strategy = \Drupal\strata\Branch\MergeStrategy::REFUSE, bool  $apply = true) : \Drupal\strata\Branch\MergeResult

Merges a branch in one call.

Parameters

string $name

The branch name.

\Drupal\strata\Branch\MergeStrategy $strategy

What to do with a key both sides changed.

bool $apply

FALSE to plan and write nothing.

Returns

\Drupal\strata\Branch\MergeResult —

What was written, or what would be.

build()

build(\Drupal\strata\Branch\Branch  $branch, string  $target, string  $ours, \Drupal\strata\Branch\MergeStrategy  $strategy, float  $started) : \Drupal\strata\Branch\MergePlan

Builds the plan once both tips are known.

Parameters

\Drupal\strata\Branch\Branch $branch

The branch being merged.

string $target

Ref name being merged into.

string $ours

The trunk's tip.

\Drupal\strata\Branch\MergeStrategy $strategy

What to do with a key both sides changed.

float $started

When planning began.

Throws

\Throwable

When a walk cannot be completed, which the caller turns into a refusal.

Returns

\Drupal\strata\Branch\MergePlan —

The plan.

restorePlan()

restorePlan(\Drupal\strata\Branch\MergePlan  $plan) : \Drupal\strata\Restore\RestorePlan

The restricted restore plan a merge applies.

Concurrent-change detection is switched off, by passing no plan time. It compares each subject's last-written timestamp against the moment the plan was built, and the restore's own forced snapshot writes every pending subject between those two moments, so every object in the plan would look like somebody else's edit. The three-way comparison against the merge base is the stronger check and it has already run.

Parameters

\Drupal\strata\Branch\MergePlan $plan

The plan.

Returns

\Drupal\strata\Restore\RestorePlan —

A plan holding one config subject per object the merge writes.

seal()

seal(\Drupal\strata\Branch\MergePlan  $plan) : string|null

Writes the two-parent commit that records the merge.

Parameters

\Drupal\strata\Branch\MergePlan $plan

The plan that was applied.

Returns

string|null —

The merge commit id, or NULL when the trunk lost its tip mid-merge or another writer moved it.

touched()

touched(string  $from, array  $behind) : list<string>

Every subject one side touched since the merge base.

Read from the segments the commits on that side sealed, rather than by comparing the two whole subject sets. A site has thousands of subjects and a branch touches a handful, so the difference is between reading a handful of segments and materializing the whole site three times.

Parameters

string $from

Tip to walk back from.

array $behind

Commits at or behind the merge base, which end the walk.

Throws

\RuntimeException

When the walk passes $maxWalk, which means the branch is further from the trunk than a merge will look.

Returns

list

Subject paths, such as config/system.site, with duplicates removed.

subjectsIn()

subjectsIn(\Drupal\strata\Tree\Commit  $commit) : list<string>

The subjects one commit's segment names.

Parameters

\Drupal\strata\Tree\Commit $commit

The commit.

Throws

\RuntimeException

When a segment will not read. A merge over a side whose history is partly unreadable would compare against a smaller set of objects than the branch actually touched, and would silently leave the rest behind.

Returns

list

Subject paths, empty when the commit sealed no segment.

configNames()

configNames(list  $subjects) : list<string>

The configuration object names in a set of subject paths.

Parameters

list $subjects

Subject paths from either side.

Returns

list

Object names, sorted and deduplicated.

refuseOtherRealms()

refuseOtherRealms(\Drupal\strata\Branch\Branch  $branch, list  $subjects) : list<string>

One refusal line per realm the branch touched that a branch cannot carry.

Parameters

\Drupal\strata\Branch\Branch $branch

The branch being merged.

list $subjects

Subject paths the branch touched.

Returns

list

The refusals, empty when the branch touched configuration only.

readable()

readable(string  $subject, array  $results) : string|null

Why one object cannot be merged, when a replay could only partly reconstruct it.

A merge over a partial document would read every field the replay could not read as a deletion, so it would produce a config object that is neither side's and write it over a live one.

Parameters

string $subject

The subject path.

array $results

Commit id keyed to what the replay produced there.

Returns

string|null —

The refusal, or NULL when every side read completely.

dataOf()

dataOf(\Drupal\strata\Restore\ReplayResult|null  $result) : array<string,mixed>|null

The data a replay produced, or NULL when the object was not there.

Parameters

\Drupal\strata\Restore\ReplayResult|null $result

What the replay produced.

Returns

array|null —

The configuration data, or NULL for an object that did not exist at that commit.

actor()

actor() : int|null

The user a branch or a merge is attributed to.

Returns

int|null —

A Drupal user id, or NULL for an unattended run.

now()

now() : int

The current time in unix microseconds.

Returns

int —

Microseconds since the epoch.