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; } }