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