@@ -40,16 +40,28 @@ func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] {
4040 errors. append ( . generic( state. lastError) )
4141}
4242for problem in state. alphaState. scanProblems{
43- errors. append ( . problem( . local , . scan, path: problem. path, error: problem. error) )
43+ errors. append ( . problem( . alpha , . scan, path: problem. path, error: problem. error) )
4444}
4545for problem in state. alphaState. transitionProblems{
46- errors. append ( . problem( . local , . transition, path: problem. path, error: problem. error) )
46+ errors. append ( . problem( . alpha , . transition, path: problem. path, error: problem. error) )
4747}
4848for problem in state. betaState. scanProblems{
49- errors. append ( . problem( . remote , . scan, path: problem. path, error: problem. error) )
49+ errors. append ( . problem( . beta , . scan, path: problem. path, error: problem. error) )
5050}
5151for problem in state. betaState. transitionProblems{
52- errors. append ( . problem( . remote, . transition, path: problem. path, error: problem. error) )
52+ errors. append ( . problem( . beta, . transition, path: problem. path, error: problem. error) )
53+ }
54+ if state. alphaState. excludedScanProblems> 0 {
55+ errors. append ( . excludedProblems( . alpha, . scan, state. alphaState. excludedScanProblems) )
56+ }
57+ if state. alphaState. excludedTransitionProblems> 0 {
58+ errors. append ( . excludedProblems( . alpha, . transition, state. alphaState. excludedTransitionProblems) )
59+ }
60+ if state. betaState. excludedScanProblems> 0 {
61+ errors. append ( . excludedProblems( . beta, . scan, state. betaState. excludedScanProblems) )
62+ }
63+ if state. betaState. excludedTransitionProblems> 0 {
64+ errors. append ( . excludedProblems( . beta, . transition, state. betaState. excludedTransitionProblems) )
5365}
5466return errors
5567}
@@ -80,3 +92,123 @@ extension Prompting_HostResponse {
8092}
8193}
8294}
95+
96+ // Translated from `cmd/mutagen/sync/list_monitor_common.go`
97+ func formatConflicts( conflicts: [ Core_Conflict ] , excludedConflicts: UInt64 ) -> String {
98+ var result = " "
99+ for (i, conflict) in conflicts. enumerated ( ) {
100+ var changesByPath : [ String : ( alpha: [ Core_Change ] , beta: [ Core_Change ] ) ] = [ : ]
101+
102+ // Group alpha changes by path
103+ for alphaChange in conflict. alphaChanges{
104+ let path = alphaChange. path
105+ if changesByPath [ path] == nil {
106+ changesByPath [ path] = ( alpha: [ ] , beta: [ ] )
107+ }
108+ changesByPath [ path] !. alpha. append ( alphaChange)
109+ }
110+
111+ // Group beta changes by path
112+ for betaChange in conflict. betaChanges{
113+ let path = betaChange. path
114+ if changesByPath [ path] == nil {
115+ changesByPath [ path] = ( alpha: [ ] , beta: [ ] )
116+ }
117+ changesByPath [ path] !. beta. append ( betaChange)
118+ }
119+
120+ result+= formatChanges ( changesByPath)
121+
122+ if i< conflicts. count- 1 || excludedConflicts> 0 {
123+ result+= " \n "
124+ }
125+ }
126+
127+ if excludedConflicts> 0 {
128+ result+= " ...+ \( excludedConflicts) more conflicts... \n "
129+ }
130+
131+ return result
132+ }
133+
134+ func formatChanges( _ changesByPath: [ String : ( alpha: [ Core_Change ] , beta: [ Core_Change ] ) ] ) -> String {
135+ var result = " "
136+
137+ for (path, changes) in changesByPath{
138+ if changes. alpha. count== 1 , changes. beta. count== 1 {
139+ // Simple message for basic file conflicts
140+ if changes. alpha [ 0 ] . hasNew,
141+ changes. beta [ 0 ] . hasNew,
142+ changes. alpha [ 0 ] . new. kind== . file,
143+ changes. beta [ 0 ] . new. kind== . file
144+ {
145+ result+= " File: ` \( formatPath ( path) ) ` \n "
146+ continue
147+ }
148+ // Friendly message for `<non-existent -> !<non-existent>` conflicts
149+ if !changes. alpha [ 0 ] . hasOld,
150+ !changes. beta [ 0 ] . hasOld,
151+ changes. alpha [ 0 ] . hasNew,
152+ changes. beta [ 0 ] . hasNew
153+ {
154+ result+= """
155+ An entry, ` \( formatPath ( path) ) `, was created on both endpoints that does not match.
156+ You can resolve this conflict by deleting one of the entries. \n
157+ """
158+ continue
159+ }
160+ }
161+
162+ let formattedPath = formatPath ( path)
163+ result+= " Path: \( formattedPath) \n "
164+
165+ // TODO: Local & Remote should be replaced with Alpha & Beta, once it's possible to configure which is which
166+
167+ if !changes. alpha. isEmpty{
168+ result+= " Local changes: \n "
169+ for change in changes. alpha{
170+ let old = formatEntry ( change. hasOld? change. old: nil )
171+ let new = formatEntry ( change. hasNew? change. new: nil )
172+ result+= " \( old) → \( new) \n "
173+ }
174+ }
175+
176+ if !changes. beta. isEmpty{
177+ result+= " Remote changes: \n "
178+ for change in changes. beta{
179+ let old = formatEntry ( change. hasOld? change. old: nil )
180+ let new = formatEntry ( change. hasNew? change. new: nil )
181+ result+= " \( old) → \( new) \n "
182+ }
183+ }
184+ }
185+
186+ return result
187+ }
188+
189+ func formatPath( _ path: String ) -> String {
190+ path. isEmpty? " <root> " : path
191+ }
192+
193+ func formatEntry( _ entry: Core_Entry ? ) -> String {
194+ guard let entryelse {
195+ return " <non-existent> "
196+ }
197+
198+ switch entry. kind{
199+ case . directory:
200+ return " Directory "
201+ case . file:
202+ return entry. executable? " Executable File " : " File "
203+ case . symbolicLink:
204+ return " Symbolic Link ( \( entry. target) ) "
205+ case . untracked:
206+ return " Untracked content "
207+ case . problematic:
208+ return " Problematic content ( \( entry. problem) ) "
209+ case . UNRECOGNIZED:
210+ return " <unknown> "
211+ case . phantomDirectory:
212+ return " Phantom Directory "
213+ }
214+ }