namespace DodoSSH.Client.Domain; /// The merged bucket, and everything that had to be overridden to produce it. /// The bucket to store and push. /// Empty when the two sides were reconcilable field by field. public sealed record ObjectStoreMergeResult( ObjectStoreSecret Merged, IReadOnlyList Conflicts) { /// Whether anything had to be overridden. public bool HasConflicts => Conflicts.Count > 0; } /// /// Merges two divergent versions of a bucket against the version they both started from. /// /// /// /// Every field is a scalar, so this is 's shape and it reuses /// for the same reason. /// /// /// The secret access key never reaches the conflict log, exactly as a password does not: a discarded /// one is very often still live on the service it belongs to. The access key id is shown, because it /// is an identifier rather than a secret and knowing which of two key pairs the merge dropped is the whole /// content of the notice. /// /// public static class ObjectStoreSecretMerge { /// Produces the merged bucket. /// The version both sides branched from. /// The pending local version. /// The server's current version. public static ObjectStoreMergeResult Merge( ObjectStoreSecret ancestor, ObjectStoreSecret local, ObjectStoreSecret remote) { ArgumentNullException.ThrowIfNull(ancestor); ArgumentNullException.ThrowIfNull(local); ArgumentNullException.ThrowIfNull(remote); var conflicts = new List(); var merged = new ObjectStoreSecret { // Null-forgiving on the required fields, as the neighbouring merges do: the merge returns one of // its three inputs, and all three are non-null by construction. Label = Resolve( nameof(ObjectStoreSecret.Label), ancestor.Label, local.Label, remote.Label, conflicts)!, Bucket = Resolve( nameof(ObjectStoreSecret.Bucket), ancestor.Bucket, local.Bucket, remote.Bucket, conflicts)!, AccessKeyId = Resolve( nameof(ObjectStoreSecret.AccessKeyId), ancestor.AccessKeyId, local.AccessKeyId, remote.AccessKeyId, conflicts)!, SecretAccessKey = Resolve( nameof(ObjectStoreSecret.SecretAccessKey), ancestor.SecretAccessKey, local.SecretAccessKey, remote.SecretAccessKey, conflicts, redact: true)!, Region = Resolve( nameof(ObjectStoreSecret.Region), ancestor.Region, local.Region, remote.Region, conflicts), Endpoint = Resolve( nameof(ObjectStoreSecret.Endpoint), ancestor.Endpoint, local.Endpoint, remote.Endpoint, conflicts), UsePathStyle = ThreeWayMerge .Scalar(ancestor.UsePathStyle, local.UsePathStyle, remote.UsePathStyle) .Value, Notes = Resolve( nameof(ObjectStoreSecret.Notes), ancestor.Notes, local.Notes, remote.Notes, conflicts), }; return new ObjectStoreMergeResult(merged, conflicts); } private static string? Resolve( string name, string? ancestor, string? local, string? remote, List conflicts, bool redact = false) { var merge = ThreeWayMerge.Scalar(ancestor, local, remote, StringComparer.Ordinal); if (merge.IsConflicted) { conflicts.Add(new HostFieldConflict( name, MergeSide.Local, redact ? "(kept the server's value)" : merge.Value ?? "(none)", redact ? "(a different value was discarded)" : merge.Discarded ?? "(none)", DiscardedWasRemoval: false)); } return merge.Value; } }