Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: add conflict descriptions and file sync context menu#126

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 21 additions & 16 deletionsCoder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,12 +29,23 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
TableColumn("Size") { Text($0.localSize.humanSizeBytes).help($0.sizeDescription) }
.width(min: 60, ideal: 80)
}
.contextMenu(forSelectionType: FileSyncSession.ID.self, menu: { _ in },
primaryAction: { selectedSessions in
if let session = selectedSessions.first {
editingSession = fileSync.sessionState.first(where: { $0.id == session })
}
})
.contextMenu(forSelectionType: FileSyncSession.ID.self, menu: { selections in
// TODO: We only support single selections for now
if let selected = selections.first,
let session = fileSync.sessionState.first(where: { $0.id == selected })
{
Button("Edit") { editingSession = session }
Button(session.status.isResumable ? "Resume" : "Pause")
{ Task { await pauseResume(session: session) } }
Button("Reset") { Task { await reset(session: session) } }
Button("Terminate") { Task { await delete(session: session) } }
}
},
primaryAction: { selectedSessions in
if let session = selectedSessions.first {
editingSession = fileSync.sessionState.first(where: { $0.id == session })
}
})
.frame(minWidth: 400, minHeight: 200)
.padding(.bottom, 25)
.overlay(alignment: .bottom) {
Expand DownExpand Up@@ -142,12 +153,9 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
Divider()
Button { Task { await pauseResume(session: selectedSession) } }
label: {
switch selectedSession.status {
case .paused, .error(.haltedOnRootEmptied),
.error(.haltedOnRootDeletion),
.error(.haltedOnRootTypeChange):
if selectedSession.status.isResumable {
Image(systemName: "play").frame(width: 24, height: 24).help("Pause")
default:
} else {
Image(systemName: "pause").frame(width: 24, height: 24).help("Resume")
}
}
Expand DownExpand Up@@ -182,12 +190,9 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
loading = true
defer { loading = false }
do throws(DaemonError) {
switch session.status {
case .paused, .error(.haltedOnRootEmptied),
.error(.haltedOnRootDeletion),
.error(.haltedOnRootTypeChange):
if session.status.isResumable {
try await fileSync.resumeSessions(ids: [session.id])
default:
} else {
try await fileSync.pauseSessions(ids: [session.id])
}
} catch {
Expand Down
32 changes: 26 additions & 6 deletionsCoder-Desktop/VPNLib/FileSync/FileSyncSession.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,7 +46,12 @@ public struct FileSyncSession: Identifiable {
}
if case .error = status {} else {
if state.conflicts.count > 0 {
status = .conflicts
status = .conflicts(
formatConflicts(
conflicts: state.conflicts,
excludedConflicts: state.excludedConflicts
)
)
}
}
self.status = status
Expand DownExpand Up@@ -121,7 +126,7 @@ public enum FileSyncStatus {
case error(FileSyncErrorStatus)
case ok
case paused
case conflicts
case conflicts(String)
case working(FileSyncWorkingStatus)

public var color: Color {
Expand DownExpand Up@@ -168,8 +173,8 @@ public enum FileSyncStatus {
"The session is watching for filesystem changes."
case .paused:
"The session is paused."
case .conflicts:
"The session has conflicts that need to be resolved."
caselet.conflicts(details):
"The session has conflicts that need to be resolved:\n\n\(details)"
case let .working(status):
status.description
}
Expand All@@ -178,6 +183,18 @@ public enum FileSyncStatus {
public var column: some View {
Text(type).foregroundColor(color)
}

public var isResumable: Bool {
switch self {
case .paused,
.error(.haltedOnRootEmptied),
.error(.haltedOnRootDeletion),
.error(.haltedOnRootTypeChange):
true
default:
false
}
}
}

public enum FileSyncWorkingStatus {
Expand DownExpand Up@@ -272,8 +289,8 @@ public enum FileSyncErrorStatus {
}

public enum FileSyncEndpoint {
caselocal
caseremote
casealpha
casebeta
}

public enum FileSyncProblemType {
Expand All@@ -284,13 +301,16 @@ public enum FileSyncProblemType {
public enum FileSyncError {
case generic(String)
case problem(FileSyncEndpoint, FileSyncProblemType, path: String, error: String)
case excludedProblems(FileSyncEndpoint, FileSyncProblemType, UInt64)

var description: String {
switch self {
case let .generic(error):
error
case let .problem(endpoint, type, path, error):
"\(endpoint) \(type) error at \(path): \(error)"
case let .excludedProblems(endpoint, type, count):
"+ \(count) \(endpoint) \(type) problems"
}
}
}
Expand Down
140 changes: 136 additions & 4 deletionsCoder-Desktop/VPNLib/FileSync/MutagenConvert.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,16 +40,28 @@ func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] {
errors.append(.generic(state.lastError))
}
for problem in state.alphaState.scanProblems {
errors.append(.problem(.local, .scan, path: problem.path, error: problem.error))
errors.append(.problem(.alpha, .scan, path: problem.path, error: problem.error))
}
for problem in state.alphaState.transitionProblems {
errors.append(.problem(.local, .transition, path: problem.path, error: problem.error))
errors.append(.problem(.alpha, .transition, path: problem.path, error: problem.error))
}
for problem in state.betaState.scanProblems {
errors.append(.problem(.remote, .scan, path: problem.path, error: problem.error))
errors.append(.problem(.beta, .scan, path: problem.path, error: problem.error))
}
for problem in state.betaState.transitionProblems {
errors.append(.problem(.remote, .transition, path: problem.path, error: problem.error))
errors.append(.problem(.beta, .transition, path: problem.path, error: problem.error))
}
if state.alphaState.excludedScanProblems > 0 {
errors.append(.excludedProblems(.alpha, .scan, state.alphaState.excludedScanProblems))
}
if state.alphaState.excludedTransitionProblems > 0 {
errors.append(.excludedProblems(.alpha, .transition, state.alphaState.excludedTransitionProblems))
}
if state.betaState.excludedScanProblems > 0 {
errors.append(.excludedProblems(.beta, .scan, state.betaState.excludedScanProblems))
}
if state.betaState.excludedTransitionProblems > 0 {
errors.append(.excludedProblems(.beta, .transition, state.betaState.excludedTransitionProblems))
}
return errors
}
Expand DownExpand Up@@ -80,3 +92,123 @@ extension Prompting_HostResponse {
}
}
}

// Translated from `cmd/mutagen/sync/list_monitor_common.go`
func formatConflicts(conflicts: [Core_Conflict], excludedConflicts: UInt64) -> String {
var result = ""
for (i, conflict) in conflicts.enumerated() {
var changesByPath: [String: (alpha: [Core_Change], beta: [Core_Change])] = [:]

// Group alpha changes by path
for alphaChange in conflict.alphaChanges {
let path = alphaChange.path
if changesByPath[path] == nil {
changesByPath[path] = (alpha: [], beta: [])
}
changesByPath[path]!.alpha.append(alphaChange)
}

// Group beta changes by path
for betaChange in conflict.betaChanges {
let path = betaChange.path
if changesByPath[path] == nil {
changesByPath[path] = (alpha: [], beta: [])
}
changesByPath[path]!.beta.append(betaChange)
}

result += formatChanges(changesByPath)

if i < conflicts.count - 1 || excludedConflicts > 0 {
result += "\n"
}
}

if excludedConflicts > 0 {
result += "...+\(excludedConflicts) more conflicts...\n"
}

return result
}

func formatChanges(_ changesByPath: [String: (alpha: [Core_Change], beta: [Core_Change])]) -> String {
var result = ""

for (path, changes) in changesByPath {
if changes.alpha.count == 1, changes.beta.count == 1 {
// Simple message for basic file conflicts
if changes.alpha[0].hasNew,
changes.beta[0].hasNew,
changes.alpha[0].new.kind == .file,
changes.beta[0].new.kind == .file
{
result += "File: '\(formatPath(path))'\n"
continue
}
// Friendly message for `<non-existent -> !<non-existent>` conflicts
if !changes.alpha[0].hasOld,
!changes.beta[0].hasOld,
changes.alpha[0].hasNew,
changes.beta[0].hasNew
{
result += """
An entry, '\(formatPath(path))', was created on both endpoints that does not match.
You can resolve this conflict by deleting one of the entries.\n
"""
continue
}
}

let formattedPath = formatPath(path)
result += "Path: '\(formattedPath)'\n"

// TODO: Local & Remote should be replaced with Alpha & Beta, once it's possible to configure which is which

if !changes.alpha.isEmpty {
result += " Local changes:\n"
for change in changes.alpha {
let old = formatEntry(change.hasOld ? change.old : nil)
let new = formatEntry(change.hasNew ? change.new : nil)
result += " \(old) → \(new)\n"
}
}

if !changes.beta.isEmpty {
result += " Remote changes:\n"
for change in changes.beta {
let old = formatEntry(change.hasOld ? change.old : nil)
let new = formatEntry(change.hasNew ? change.new : nil)
result += " \(old) → \(new)\n"
}
}
}

return result
}

func formatPath(_ path: String) -> String {
path.isEmpty ? "<root>" : path
}

func formatEntry(_ entry: Core_Entry?) -> String {
guard let entry else {
return "<non-existent>"
}

switch entry.kind {
case .directory:
return "Directory"
case .file:
return entry.executable ? "Executable File" : "File"
case .symbolicLink:
return "Symbolic Link (\(entry.target))"
case .untracked:
return "Untracked content"
case .problematic:
return "Problematic content (\(entry.problem))"
case .UNRECOGNIZED:
return "<unknown>"
case .phantomDirectory:
return "Phantom Directory"
}
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp