1
+ using System ;
2
+ using System . Collections . Generic ;
3
+ using System . Collections . ObjectModel ;
4
+ using System . Linq ;
1
5
using Coder . Desktop . App . Converters ;
2
6
using Coder . Desktop . MutagenSdk . Proto . Synchronization ;
7
+ using Coder . Desktop . MutagenSdk . Proto . Synchronization . Core ;
3
8
using Coder . Desktop . MutagenSdk . Proto . Url ;
4
9
5
10
namespace Coder . Desktop . App . Models ;
@@ -44,6 +49,159 @@ public string Description(string linePrefix = "")
44
49
}
45
50
}
46
51
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
+
47
205
public class SyncSessionModel
48
206
{
49
207
public readonly string Identifier ;
@@ -61,7 +219,9 @@ public class SyncSessionModel
61
219
public readonly SyncSessionModelEndpointSize AlphaSize ;
62
220
public readonly SyncSessionModelEndpointSize BetaSize ;
63
221
64
- public readonly string [ ] Errors = [ ] ;
222
+ public readonly IReadOnlyList < SyncSessionModelConflict > Conflicts ;
223
+ public ulong OmittedConflicts ;
224
+ public readonly IReadOnlyList < string > Errors ;
65
225
66
226
// If Paused is true, the session can be resumed. If false, the session can
67
227
// be paused.
@@ -72,7 +232,9 @@ public string StatusDetails
72
232
get
73
233
{
74
234
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 \n Error: { err } ";
236
+ foreach ( var conflict in Conflicts ) str += $ "\n \n { conflict . Description ( ) } ";
237
+ if ( OmittedConflicts > 0 ) str += $ "\n \n { OmittedConflicts : N0} conflicts omitted";
76
238
return str ;
77
239
}
78
240
}
@@ -192,6 +354,9 @@ public SyncSessionModel(State state)
192
354
StatusDescription = "The session has conflicts that need to be resolved." ;
193
355
}
194
356
357
+ Conflicts = state . Conflicts . Select ( c => new SyncSessionModelConflict ( c ) ) . ToList ( ) ;
358
+ OmittedConflicts = state . ExcludedConflicts ;
359
+
195
360
AlphaSize = new SyncSessionModelEndpointSize
196
361
{
197
362
SizeBytes = state . AlphaState . TotalFileSize ,
@@ -207,9 +372,24 @@ public SyncSessionModel(State state)
207
372
SymlinkCount = state . BetaState . SymbolicLinks ,
208
373
} ;
209
374
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 ;
213
393
}
214
394
215
395
private static ( string , string ) NameAndPathFromUrl ( URL url )
0 commit comments