namespace DodoSSH.Client.Domain;
/// The merged snippet, and everything that had to be overridden to produce it.
/// The snippet to store and push.
/// Empty when the two sides were reconcilable field by field.
public sealed record SnippetMergeResult(
SnippetSecret Merged,
IReadOnlyList Conflicts)
{
/// Whether anything had to be overridden.
public bool HasConflicts => Conflicts.Count > 0;
}
///
/// Merges two divergent versions of a snippet against the version they both started from.
///
///
///
/// Three strings and a flag, so the shape is 's and it reuses
/// for the same reason. Nothing is redacted: a snippet is a command somebody
/// wrote down on purpose, and a notice that hid the discarded version would leave the user unable to tell
/// whether the one that survived is the one they wanted to keep.
///
///
/// cannot conflict, and it is worth knowing why rather than
/// assuming it. A three-way clash needs local and remote each to differ from the ancestor and
/// from one another; with only two possible values, the first two conditions force the third to fail. So this
/// field always resolves to whichever side actually changed it, and a merge can never turn a snippet into one
/// that runs on its own — the outcome the ordinary rule would have made possible if the field had a third
/// state. An earlier draft special-cased it to resolve to on a clash; the branch was
/// unreachable, and unreachable safety code is worse than none, because it reads as protection.
///
///
public static class SnippetSecretMerge
{
/// Produces the merged snippet.
/// The version both sides branched from.
/// The pending local version.
/// The server's current version.
public static SnippetMergeResult Merge(
SnippetSecret ancestor,
SnippetSecret local,
SnippetSecret remote)
{
ArgumentNullException.ThrowIfNull(ancestor);
ArgumentNullException.ThrowIfNull(local);
ArgumentNullException.ThrowIfNull(remote);
var conflicts = new List();
var merged = new SnippetSecret
{
// Null-forgiving on the two required fields, as the neighbouring merges do for the same reason:
// the merge returns one of its three inputs, and all three are non-null by construction.
Label = Text(
nameof(SnippetSecret.Label), ancestor.Label, local.Label, remote.Label, conflicts)!,
Command = Text(
nameof(SnippetSecret.Command),
ancestor.Command,
local.Command,
remote.Command,
conflicts)!,
Notes = Text(
nameof(SnippetSecret.Notes), ancestor.Notes, local.Notes, remote.Notes, conflicts),
RunsOnInsert = ThreeWayMerge
.Scalar(ancestor.RunsOnInsert, local.RunsOnInsert, remote.RunsOnInsert)
.Value,
};
return new SnippetMergeResult(merged, conflicts);
}
private static string? Text(
string name,
string? ancestor,
string? local,
string? remote,
List conflicts)
{
var merge = ThreeWayMerge.Scalar(ancestor, local, remote, StringComparer.Ordinal);
if (merge.IsConflicted)
{
conflicts.Add(new HostFieldConflict(
name,
MergeSide.Local,
merge.Value ?? "(none)",
merge.Discarded ?? "(none)",
DiscardedWasRemoval: false));
}
return merge.Value;
}
}