1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . Collections . ObjectModel ;
4+ using System . Linq ;
15using Coder . Desktop . App . Converters ;
26using Coder . Desktop . MutagenSdk . Proto . Synchronization ;
7+ using Coder . Desktop . MutagenSdk . Proto . Synchronization . Core ;
38using Coder . Desktop . MutagenSdk . Proto . Url ;
49
510namespace 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+
47205public 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 \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";
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