diff --git a/Sources/Zero/Services/GitPanelService.swift b/Sources/Zero/Services/GitPanelService.swift index dee978f..ae29486 100644 --- a/Sources/Zero/Services/GitPanelService.swift +++ b/Sources/Zero/Services/GitPanelService.swift @@ -40,7 +40,7 @@ class GitPanelService: ObservableObject { branches = try gitService.branches(in: containerName) errorMessage = nil } catch { - errorMessage = error.localizedDescription + reportFailure(action: "refresh", userMessage: error.localizedDescription, error: error) } } @@ -51,7 +51,7 @@ class GitPanelService: ObservableObject { try gitService.add(files: files, in: containerName) await refresh() } catch { - errorMessage = error.localizedDescription + reportFailure(action: "stage", userMessage: error.localizedDescription, error: error) } } @@ -62,7 +62,7 @@ class GitPanelService: ObservableObject { try gitService.addAll(in: containerName) await refresh() } catch { - errorMessage = error.localizedDescription + reportFailure(action: "stageAll", userMessage: error.localizedDescription, error: error) } } @@ -73,7 +73,7 @@ class GitPanelService: ObservableObject { try gitService.commit(message: message, in: containerName) await refresh() } catch { - errorMessage = error.localizedDescription + reportFailure(action: "commit", userMessage: error.localizedDescription, error: error) } } @@ -84,7 +84,7 @@ class GitPanelService: ObservableObject { try gitService.commitAll(message: message, in: containerName) await refresh() } catch { - errorMessage = error.localizedDescription + reportFailure(action: "commitAll", userMessage: error.localizedDescription, error: error) } } @@ -95,7 +95,7 @@ class GitPanelService: ObservableObject { try gitService.createAndCheckoutBranch(name: name, in: containerName) await refresh() } catch { - errorMessage = error.localizedDescription + reportFailure(action: "createBranch", userMessage: error.localizedDescription, error: error) } } @@ -106,7 +106,7 @@ class GitPanelService: ObservableObject { try gitService.checkout(branch: branch, in: containerName) await refresh() } catch { - errorMessage = error.localizedDescription + reportFailure(action: "checkout", userMessage: error.localizedDescription, error: error) } } @@ -117,7 +117,8 @@ class GitPanelService: ObservableObject { try gitService.push(in: containerName) await refresh() } catch { - errorMessage = mapRemoteActionError(error, action: .push) + let message = mapRemoteActionError(error, action: .push) + reportFailure(action: "push", userMessage: message, error: error) } } @@ -130,11 +131,13 @@ class GitPanelService: ObservableObject { } catch { if isPullConflictError(error) { let conflictedFiles = (try? gitService.conflictedFiles(in: containerName)) ?? [] - errorMessage = pullConflictGuidance(conflictedFiles: conflictedFiles) + let message = pullConflictGuidance(conflictedFiles: conflictedFiles) + reportFailure(action: "pull", userMessage: message, error: error) return } - errorMessage = mapRemoteActionError(error, action: .pull) + let message = mapRemoteActionError(error, action: .pull) + reportFailure(action: "pull", userMessage: message, error: error) } } @@ -155,10 +158,15 @@ class GitPanelService: ObservableObject { } catch { selectedDiffTitle = "Diff · \(path)" selectedDiff = "Failed to load diff: \(error.localizedDescription)" - errorMessage = error.localizedDescription + reportFailure(action: "loadDiff", userMessage: error.localizedDescription, error: error) } } + private func reportFailure(action: String, userMessage: String, error: Error) { + errorMessage = userMessage + AppLogStore.shared.append("GitPanel \(action) failed: \(userMessage) [raw=\(error.localizedDescription)]") + } + func showUntrackedDiffPlaceholder(for path: String) { selectedDiffTitle = "Untracked · \(path)" selectedDiff = "Untracked files have no git diff until staged." diff --git a/Tests/ZeroTests/GitPanelServiceTests.swift b/Tests/ZeroTests/GitPanelServiceTests.swift index ba6a555..17ebecf 100644 --- a/Tests/ZeroTests/GitPanelServiceTests.swift +++ b/Tests/ZeroTests/GitPanelServiceTests.swift @@ -27,6 +27,16 @@ private final class GitPanelMockContainerRunner: ContainerRunning { @MainActor final class GitPanelServiceTests: XCTestCase { + override func setUp() { + super.setUp() + AppLogStore.shared.clear() + } + + override func tearDown() { + AppLogStore.shared.clear() + super.tearDown() + } + func testPushMapsNonFastForwardFailureToGuidance() async { // Given let runner = GitPanelMockContainerRunner() @@ -129,6 +139,47 @@ final class GitPanelServiceTests: XCTestCase { XCTAssertEqual(service.errorMessage, "fatal: unexpected socket close") } + func testPushFailureAppendsErrorToAppLogStore() async { + // Given + let runner = GitPanelMockContainerRunner() + runner.shellErrorsByCommandSubstring["git push"] = NSError( + domain: "git", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "fatal: unexpected socket close"] + ) + let service = makeService(runner: runner) + + // When + await service.push() + + // Then + let logEntries = AppLogStore.shared.recentEntries() + XCTAssertTrue(logEntries.contains { entry in + entry.contains("GitPanel push failed") && entry.contains("fatal: unexpected socket close") + }) + } + + func testPullConflictAppendsGuidanceToAppLogStore() async { + // Given + let runner = GitPanelMockContainerRunner() + runner.nextShellOutput = "Sources/Zero/Views/EditorView.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 + let logEntries = AppLogStore.shared.recentEntries() + XCTAssertTrue(logEntries.contains { entry in + entry.contains("GitPanel pull failed") && entry.contains("Pull hit merge conflicts") + }) + } + private func makeService(runner: GitPanelMockContainerRunner) -> GitPanelService { let service = GitPanelService() service.setup(gitService: GitService(runner: runner), containerName: "zero-dev-container")