Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 27 additions & 0 deletions Sources/Zero/Services/GitPanelService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ class GitPanelService: ObservableObject {
try gitService.pull(in: containerName)
await refresh()
} catch {
if isPullConflictError(error) {
let conflictedFiles = (try? gitService.conflictedFiles(in: containerName)) ?? []
errorMessage = pullConflictGuidance(conflictedFiles: conflictedFiles)
return
}

errorMessage = mapRemoteActionError(error, action: .pull)
}
}
Expand Down Expand Up @@ -182,6 +188,27 @@ class GitPanelService: ObservableObject {
return message
}

private func isPullConflictError(_ error: Error) -> Bool {
let normalized = error.localizedDescription.lowercased()
return containsAny(normalized, patterns: ["automatic merge failed", "merge conflict", "conflict"])
}

private func pullConflictGuidance(conflictedFiles: [String]) -> String {
let files = conflictedFiles.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }

guard !files.isEmpty else {
return "Pull hit merge conflicts. Resolve conflicted files, commit, then pull again."
}

let maxPreview = 3
let previewFiles = Array(files.prefix(maxPreview)).joined(separator: ", ")
let remaining = files.count - maxPreview
let suffix = remaining > 0 ? " (+\(remaining) more)" : ""

return "Pull hit merge conflicts in \(previewFiles)\(suffix). Resolve those files, commit, then pull again."
}

private func containsAny(_ text: String, patterns: [String]) -> Bool {
patterns.contains { text.contains($0) }
}
Expand Down
25 changes: 25 additions & 0 deletions Tests/ZeroTests/GitPanelServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ final class GitPanelServiceTests: XCTestCase {
func testPullMapsMergeConflictFailureToGuidance() async {
// Given
let runner = GitPanelMockContainerRunner()
runner.nextShellOutput = """
Sources/Zero/Views/EditorView.swift
Sources/Zero/Services/GitPanelService.swift
"""
runner.shellErrorsByCommandSubstring["git pull"] = NSError(
domain: "git",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "Automatic merge failed; fix conflicts and then commit the result."]
)
let service = makeService(runner: runner)

// When
await service.pull()

// Then
XCTAssertEqual(
service.errorMessage,
"Pull hit merge conflicts in Sources/Zero/Views/EditorView.swift, Sources/Zero/Services/GitPanelService.swift. Resolve those files, commit, then pull again."
)
}

func testPullConflictFallsBackToGenericGuidanceWhenNoFilesDetected() async {
// Given
let runner = GitPanelMockContainerRunner()
runner.nextShellOutput = ""
runner.shellErrorsByCommandSubstring["git pull"] = NSError(
domain: "git",
code: 1,
Expand Down