@@ -40,16 +40,28 @@ func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] {
4040 errors. append ( . generic( state. lastError) )
4141 }
4242 for 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 }
4545 for 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 }
4848 for 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 }
5151 for 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 }
5466 return 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 entry else {
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+ }
0 commit comments