Add field-level 3-way merge for bidirectional conflicts
Bisher wurde bei einem Konflikt (beide Seiten geaendert) der ganze Datensatz ueberschrieben - eine gleichzeitige Aenderung an einem anderen Feld ging verloren (z.B. A aendert Telefon in Outlook, B aendert Mail in Starface -> eine Aenderung weg). Jetzt: - Mapping speichert je Seite einen Snapshot des letzten Sync-Stands (LastOutlook/LastStarface), zusaetzlich zu den Hashes. - Bei beidseitiger Aenderung im Both-Modus wird feldweise gemergt (ContactMerger): unterschiedliche Felder bleiben beide erhalten, nur bei echtem Konflikt am selben Feld gewinnt Outlook. - Echte Feld-Konflikte landen in SyncResult.Conflicts und werden im MainForm per Tray-Meldung angezeigt. - Snapshots werden in allen Baseline-Punkten gesetzt (Phase 1-3) und fuer aeltere Mappings beim naechsten unveraenderten Sync nachgetragen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,20 @@ namespace StarfaceOutlookSync.Services
|
||||
|
||||
private void Log(string message) => OnProgress?.Invoke(message);
|
||||
|
||||
/// <summary>
|
||||
/// Setzt die Baseline eines Mappings auf den uebergebenen Stand beider
|
||||
/// Seiten (Snapshot + Hash). Der Snapshot wird fuer das Feld-Merge bei
|
||||
/// kuenftigen Konflikten gebraucht.
|
||||
/// </summary>
|
||||
private static void SetBaseline(SyncMapping m, UnifiedContact outlook, UnifiedContact starface)
|
||||
{
|
||||
m.LastOutlook = outlook;
|
||||
m.LastStarface = starface;
|
||||
m.LastOutlookHash = outlook?.GetHash() ?? "";
|
||||
m.LastStarfaceHash = starface?.GetHash() ?? "";
|
||||
m.LastSyncHash = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Findet einen passenden Kontakt in der Kandidatenliste.
|
||||
/// Strenges Matching: Felder die auf einer Seite gefuellt sind muessen
|
||||
@@ -282,9 +296,7 @@ namespace StarfaceOutlookSync.Services
|
||||
if (string.IsNullOrEmpty(mapping.LastOutlookHash) &&
|
||||
string.IsNullOrEmpty(mapping.LastStarfaceHash))
|
||||
{
|
||||
mapping.LastOutlookHash = olHash;
|
||||
mapping.LastStarfaceHash = sfHash;
|
||||
mapping.LastSyncHash = "";
|
||||
SetBaseline(mapping, oc, sc);
|
||||
newMappings.Add(mapping);
|
||||
continue;
|
||||
}
|
||||
@@ -292,14 +304,23 @@ namespace StarfaceOutlookSync.Services
|
||||
bool olChanged = olHash != mapping.LastOutlookHash;
|
||||
bool sfChanged = sfHash != mapping.LastStarfaceHash;
|
||||
|
||||
if (!olChanged && !sfChanged)
|
||||
{
|
||||
// Unveraendert. Snapshots aelterer Mappings (nur Hash)
|
||||
// nachtragen, damit kuenftige Konflikte gemergt werden koennen.
|
||||
if (mapping.LastOutlook == null || mapping.LastStarface == null)
|
||||
SetBaseline(mapping, oc, sc);
|
||||
newMappings.Add(mapping);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface))
|
||||
{
|
||||
// Outlook hat sich geaendert -> Starface updaten
|
||||
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
|
||||
if (updated != null)
|
||||
{
|
||||
mapping.LastOutlookHash = olHash;
|
||||
mapping.LastStarfaceHash = updated.GetHash();
|
||||
SetBaseline(mapping, oc, updated);
|
||||
result.Updated++;
|
||||
Log($" Aktualisiert (OL->SF): {oc.DisplayName}");
|
||||
}
|
||||
@@ -310,22 +331,54 @@ namespace StarfaceOutlookSync.Services
|
||||
var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc);
|
||||
if (updated != null)
|
||||
{
|
||||
mapping.LastStarfaceHash = sfHash;
|
||||
mapping.LastOutlookHash = updated.GetHash();
|
||||
SetBaseline(mapping, updated, sc);
|
||||
result.Updated++;
|
||||
Log($" Aktualisiert (SF->OL): {sc.DisplayName}");
|
||||
}
|
||||
}
|
||||
else if (olChanged && sfChanged)
|
||||
{
|
||||
// Beide geaendert -> Konflikt, Outlook bevorzugt
|
||||
if (profile.SyncDirection != SyncDirection.StarfaceToOutlook)
|
||||
// Beide Seiten geaendert.
|
||||
if (profile.SyncDirection == SyncDirection.Both
|
||||
&& mapping.LastOutlook != null && mapping.LastStarface != null)
|
||||
{
|
||||
// Feldweises 3-Wege-Merge: unterschiedliche Felder
|
||||
// bleiben beide erhalten; nur bei gleichem Feld auf
|
||||
// beiden Seiten gewinnt Outlook.
|
||||
var (merged, conflicts) = ContactMerger.Merge(
|
||||
mapping.LastOutlook, mapping.LastStarface, oc, sc, outlookWins: true);
|
||||
|
||||
var updatedSf = await starface.UpdateContactAsync(mapping.StarfaceId, merged, profile.StarfaceAddressBook);
|
||||
var updatedOl = _outlookService.UpdateContact(mapping.OutlookEntryId, merged);
|
||||
|
||||
if (updatedSf != null || updatedOl != null)
|
||||
{
|
||||
if (updatedOl != null)
|
||||
{
|
||||
mapping.LastOutlook = updatedOl;
|
||||
mapping.LastOutlookHash = updatedOl.GetHash();
|
||||
}
|
||||
if (updatedSf != null)
|
||||
{
|
||||
mapping.LastStarface = updatedSf;
|
||||
mapping.LastStarfaceHash = updatedSf.GetHash();
|
||||
}
|
||||
mapping.LastSyncHash = "";
|
||||
result.Updated++;
|
||||
foreach (var cf in conflicts) result.Conflicts.Add(cf);
|
||||
Log(conflicts.Count > 0
|
||||
? $" Beidseitig geaendert, zusammengefuehrt ({conflicts.Count} Feld-Konflikt(e), Outlook gewinnt): {oc.DisplayName}"
|
||||
: $" Beidseitig geaendert, zusammengefuehrt: {oc.DisplayName}");
|
||||
}
|
||||
}
|
||||
else if (profile.SyncDirection != SyncDirection.StarfaceToOutlook)
|
||||
{
|
||||
// Fallback ohne Snapshot bzw. OutlookToStarface:
|
||||
// Outlook gewinnt komplett.
|
||||
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
|
||||
if (updated != null)
|
||||
{
|
||||
mapping.LastOutlookHash = olHash;
|
||||
mapping.LastStarfaceHash = updated.GetHash();
|
||||
SetBaseline(mapping, oc, updated);
|
||||
result.Updated++;
|
||||
Log($" Konflikt (OL gewinnt): {oc.DisplayName}");
|
||||
}
|
||||
@@ -335,14 +388,12 @@ namespace StarfaceOutlookSync.Services
|
||||
var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc);
|
||||
if (updated != null)
|
||||
{
|
||||
mapping.LastStarfaceHash = sfHash;
|
||||
mapping.LastOutlookHash = updated.GetHash();
|
||||
SetBaseline(mapping, updated, sc);
|
||||
result.Updated++;
|
||||
Log($" Konflikt (SF gewinnt): {sc.DisplayName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Beide unveraendert -> nichts tun
|
||||
}
|
||||
|
||||
newMappings.Add(mapping);
|
||||
@@ -382,6 +433,8 @@ namespace StarfaceOutlookSync.Services
|
||||
ProfileId = profile.Id,
|
||||
OutlookEntryId = oc.OutlookEntryId,
|
||||
StarfaceId = match.StarfaceId,
|
||||
LastOutlook = oc,
|
||||
LastStarface = updated,
|
||||
LastOutlookHash = oc.GetHash(),
|
||||
LastStarfaceHash = updated.GetHash()
|
||||
});
|
||||
@@ -403,6 +456,8 @@ namespace StarfaceOutlookSync.Services
|
||||
ProfileId = profile.Id,
|
||||
OutlookEntryId = oc.OutlookEntryId,
|
||||
StarfaceId = created.StarfaceId,
|
||||
LastOutlook = oc,
|
||||
LastStarface = created,
|
||||
LastOutlookHash = oc.GetHash(),
|
||||
LastStarfaceHash = created.GetHash()
|
||||
});
|
||||
@@ -458,6 +513,8 @@ namespace StarfaceOutlookSync.Services
|
||||
ProfileId = profile.Id,
|
||||
OutlookEntryId = match.OutlookEntryId,
|
||||
StarfaceId = sc.StarfaceId,
|
||||
LastStarface = sc,
|
||||
LastOutlook = updated,
|
||||
LastStarfaceHash = sc.GetHash(),
|
||||
LastOutlookHash = updated.GetHash()
|
||||
});
|
||||
@@ -478,6 +535,8 @@ namespace StarfaceOutlookSync.Services
|
||||
ProfileId = profile.Id,
|
||||
OutlookEntryId = created.OutlookEntryId,
|
||||
StarfaceId = sc.StarfaceId,
|
||||
LastStarface = sc,
|
||||
LastOutlook = created,
|
||||
LastStarfaceHash = sc.GetHash(),
|
||||
LastOutlookHash = created.GetHash()
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user