Skip to content

Commit 81f8c02

Browse files
committed
Conflict messages
1 parent f7817a9 commit 81f8c02

File tree

1 file changed

+185
-5
lines changed

1 file changed

+185
-5
lines changed

App/Models/SyncSessionModel.cs

+185-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.Linq;
15
using Coder.Desktop.App.Converters;
26
using Coder.Desktop.MutagenSdk.Proto.Synchronization;
7+
using Coder.Desktop.MutagenSdk.Proto.Synchronization.Core;
38
using Coder.Desktop.MutagenSdk.Proto.Url;
49

510
namespace Coder.Desktop.App.Models;
@@ -44,6 +49,159 @@ public string Description(string linePrefix = "")
4449
}
4550
}
4651

52+
public enum SyncSessionModelEntryKind
53+
{
54+
Unknown,
55+
Directory,
56+
File,
57+
SymbolicLink,
58+
Untracked,
59+
Problematic,
60+
PhantomDirectory,
61+
}
62+
63+
public sealed class SyncSessionModelEntry
64+
{
65+
public readonly SyncSessionModelEntryKind Kind;
66+
67+
// For Kind == Directory only.
68+
public readonly ReadOnlyDictionary<string, SyncSessionModelEntry> Contents;
69+
70+
// For Kind == File only.
71+
public readonly string Digest = "";
72+
public readonly bool Executable;
73+
74+
// For Kind = SymbolicLink only.
75+
public readonly string Target = "";
76+
77+
// For Kind = Problematic only.
78+
public readonly string Problem = "";
79+
80+
public SyncSessionModelEntry(Entry protoEntry)
81+
{
82+
Kind = protoEntry.Kind switch
83+
{
84+
EntryKind.Directory => SyncSessionModelEntryKind.Directory,
85+
EntryKind.File => SyncSessionModelEntryKind.File,
86+
EntryKind.SymbolicLink => SyncSessionModelEntryKind.SymbolicLink,
87+
EntryKind.Untracked => SyncSessionModelEntryKind.Untracked,
88+
EntryKind.Problematic => SyncSessionModelEntryKind.Problematic,
89+
EntryKind.PhantomDirectory => SyncSessionModelEntryKind.PhantomDirectory,
90+
_ => SyncSessionModelEntryKind.Unknown,
91+
};
92+
93+
switch (Kind)
94+
{
95+
case SyncSessionModelEntryKind.Directory:
96+
{
97+
var contents = new Dictionary<string, SyncSessionModelEntry>();
98+
foreach (var (key, value) in protoEntry.Contents)
99+
contents[key] = new SyncSessionModelEntry(value);
100+
Contents = new ReadOnlyDictionary<string, SyncSessionModelEntry>(contents);
101+
break;
102+
}
103+
case SyncSessionModelEntryKind.File:
104+
Digest = BitConverter.ToString(protoEntry.Digest.ToByteArray()).Replace("-", "").ToLower();
105+
Executable = protoEntry.Executable;
106+
break;
107+
case SyncSessionModelEntryKind.SymbolicLink:
108+
Target = protoEntry.Target;
109+
break;
110+
case SyncSessionModelEntryKind.Problematic:
111+
Problem = protoEntry.Problem;
112+
break;
113+
}
114+
}
115+
116+
public new string ToString()
117+
{
118+
var str = Kind.ToString();
119+
switch (Kind)
120+
{
121+
case SyncSessionModelEntryKind.Directory:
122+
str += $" ({Contents.Count} entries)";
123+
break;
124+
case SyncSessionModelEntryKind.File:
125+
str += $" ({Digest}, executable: {Executable})";
126+
break;
127+
case SyncSessionModelEntryKind.SymbolicLink:
128+
str += $" (target: {Target})";
129+
break;
130+
case SyncSessionModelEntryKind.Problematic:
131+
str += $" ({Problem})";
132+
break;
133+
}
134+
135+
return str;
136+
}
137+
}
138+
139+
public sealed class SyncSessionModelConflictChange
140+
{
141+
public readonly string Path; // relative to sync root
142+
143+
// null means non-existent:
144+
public readonly SyncSessionModelEntry? Old;
145+
public readonly SyncSessionModelEntry? New;
146+
147+
public SyncSessionModelConflictChange(Change protoChange)
148+
{
149+
Path = protoChange.Path;
150+
Old = protoChange.Old != null ? new SyncSessionModelEntry(protoChange.Old) : null;
151+
New = protoChange.New != null ? new SyncSessionModelEntry(protoChange.New) : null;
152+
}
153+
154+
public new string ToString()
155+
{
156+
const string nonExistent = "<non-existent>";
157+
var oldStr = Old != null ? Old.ToString() : nonExistent;
158+
var newStr = New != null ? New.ToString() : nonExistent;
159+
return $"{Path} ({oldStr} -> {newStr})";
160+
}
161+
}
162+
163+
public sealed class SyncSessionModelConflict
164+
{
165+
public readonly string Root; // relative to sync root
166+
public readonly List<SyncSessionModelConflictChange> AlphaChanges;
167+
public readonly List<SyncSessionModelConflictChange> BetaChanges;
168+
169+
public SyncSessionModelConflict(Conflict protoConflict)
170+
{
171+
Root = protoConflict.Root;
172+
AlphaChanges = protoConflict.AlphaChanges.Select(change => new SyncSessionModelConflictChange(change)).ToList();
173+
BetaChanges = protoConflict.BetaChanges.Select(change => new SyncSessionModelConflictChange(change)).ToList();
174+
}
175+
176+
private string? FriendlyProblem()
177+
{
178+
// If the change is <non-existent> -> !<non-existent>.
179+
if (AlphaChanges.Count == 1 && BetaChanges.Count == 1 &&
180+
AlphaChanges[0].Old == null &&
181+
BetaChanges[0].Old == null &&
182+
AlphaChanges[0].New != null &&
183+
BetaChanges[0].New != null)
184+
return
185+
"An entry was created on both endpoints and they do not match. You can resolve this conflict by deleting one of the entries on either side.";
186+
187+
return null;
188+
}
189+
190+
public string Description()
191+
{
192+
// This formatting is very similar to Mutagen.
193+
var str = $"Conflict at path '{Root}':";
194+
foreach (var change in AlphaChanges)
195+
str += $"\n (alpha) {change.ToString()}";
196+
foreach (var change in AlphaChanges)
197+
str += $"\n (beta) {change.ToString()}";
198+
if (FriendlyProblem() is { } friendlyProblem)
199+
str += $"\n\n {friendlyProblem}";
200+
201+
return str;
202+
}
203+
}
204+
47205
public class SyncSessionModel
48206
{
49207
public readonly string Identifier;
@@ -61,7 +219,9 @@ public class SyncSessionModel
61219
public readonly SyncSessionModelEndpointSize AlphaSize;
62220
public readonly SyncSessionModelEndpointSize BetaSize;
63221

64-
public readonly string[] Errors = [];
222+
public readonly IReadOnlyList<SyncSessionModelConflict> Conflicts;
223+
public ulong OmittedConflicts;
224+
public readonly IReadOnlyList<string> Errors;
65225

66226
// If Paused is true, the session can be resumed. If false, the session can
67227
// be paused.
@@ -72,7 +232,9 @@ public string StatusDetails
72232
get
73233
{
74234
var str = $"{StatusString} ({StatusCategory})\n\n{StatusDescription}";
75-
foreach (var err in Errors) str += $"\n\n{err}";
235+
foreach (var err in Errors) str += $"\n\nError: {err}";
236+
foreach (var conflict in Conflicts) str += $"\n\n{conflict.Description()}";
237+
if (OmittedConflicts > 0) str += $"\n\n{OmittedConflicts:N0} conflicts omitted";
76238
return str;
77239
}
78240
}
@@ -192,6 +354,9 @@ public SyncSessionModel(State state)
192354
StatusDescription = "The session has conflicts that need to be resolved.";
193355
}
194356

357+
Conflicts = state.Conflicts.Select(c => new SyncSessionModelConflict(c)).ToList();
358+
OmittedConflicts = state.ExcludedConflicts;
359+
195360
AlphaSize = new SyncSessionModelEndpointSize
196361
{
197362
SizeBytes = state.AlphaState.TotalFileSize,
@@ -207,9 +372,24 @@ public SyncSessionModel(State state)
207372
SymlinkCount = state.BetaState.SymbolicLinks,
208373
};
209374

210-
// TODO: accumulate errors, there seems to be multiple fields they can
211-
// come from
212-
if (!string.IsNullOrWhiteSpace(state.LastError)) Errors = [state.LastError];
375+
List<string> errors = [];
376+
if (!string.IsNullOrWhiteSpace(state.LastError)) errors.Add($"Last error:\n {state.LastError}");
377+
// TODO: scan problems + transition problems + omissions should probably be fields
378+
foreach (var scanProblem in state.AlphaState.ScanProblems) errors.Add($"Alpha scan problem: {scanProblem}");
379+
if (state.AlphaState.ExcludedScanProblems > 0)
380+
errors.Add($"Alpha scan problems omitted: {state.AlphaState.ExcludedScanProblems}");
381+
foreach (var scanProblem in state.AlphaState.ScanProblems) errors.Add($"Beta scan problem: {scanProblem}");
382+
if (state.BetaState.ExcludedScanProblems > 0)
383+
errors.Add($"Beta scan problems omitted: {state.BetaState.ExcludedScanProblems}");
384+
foreach (var transitionProblem in state.AlphaState.TransitionProblems)
385+
errors.Add($"Alpha transition problem: {transitionProblem}");
386+
if (state.AlphaState.ExcludedTransitionProblems > 0)
387+
errors.Add($"Alpha transition problems omitted: {state.AlphaState.ExcludedTransitionProblems}");
388+
foreach (var transitionProblem in state.AlphaState.TransitionProblems)
389+
errors.Add($"Beta transition problem: {transitionProblem}");
390+
if (state.BetaState.ExcludedTransitionProblems > 0)
391+
errors.Add($"Beta transition problems omitted: {state.BetaState.ExcludedTransitionProblems}");
392+
Errors = errors;
213393
}
214394

215395
private static (string, string) NameAndPathFromUrl(URL url)

0 commit comments

Comments
 (0)