diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e460d1ea..807c5bbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,4 +78,5 @@ jobs: -scheme macgit \ -destination 'platform=macOS' \ CODE_SIGNING_ALLOWED=NO \ + -parallel-testing-enabled NO \ test diff --git a/macgit.xcodeproj/xcshareddata/xcschemes/macgit.xcscheme b/macgit.xcodeproj/xcshareddata/xcschemes/macgit.xcscheme index 8b95d06a..4036b2a0 100644 --- a/macgit.xcodeproj/xcshareddata/xcschemes/macgit.xcscheme +++ b/macgit.xcodeproj/xcshareddata/xcschemes/macgit.xcscheme @@ -32,7 +32,7 @@ + parallelizable = "NO"> FirebaseBootstrapStatus { if FirebaseApp.app() != nil { return .configured diff --git a/macgit/App/macgitApp.swift b/macgit/App/macgitApp.swift index 4ebdbfb7..ae788f04 100644 --- a/macgit/App/macgitApp.swift +++ b/macgit/App/macgitApp.swift @@ -38,34 +38,37 @@ struct macgitApp: App { init() { NSWindow.allowsAutomaticWindowTabbing = true let firebaseStatus = FirebaseBootstrap.configure() + // Unit-test hosts must not open Firestore: the shared LevelDB cache + // aborts when several test hosts (or a running app) use it at once. + let cloudFeaturesEnabled = firebaseStatus == .configured && !FirebaseBootstrap.isRunningUnitTests let appState = AppState.shared _appState = StateObject(wrappedValue: appState) let accountController = AccountSessionController( auth: FirebaseAuthService(), bootstrapStatus: firebaseStatus, - entitlementProvider: firebaseStatus == .configured + entitlementProvider: cloudFeaturesEnabled ? FirestoreEntitlementStore() : nil, entitlementCache: UserDefaultsEntitlementCache(), - webAccountSessionProvider: firebaseStatus == .configured + webAccountSessionProvider: cloudFeaturesEnabled ? FirebaseWebAccountSessionService() : nil, openWebURL: NSWorkspace.shared.open, appState: appState, - settingsStore: firebaseStatus == .configured + settingsStore: cloudFeaturesEnabled ? FirestoreSettingsStore() : nil, - deviceIdentity: firebaseStatus == .configured + deviceIdentity: cloudFeaturesEnabled ? CommitPlusDeviceIdentityProvider() : nil, - deviceAccessProvider: firebaseStatus == .configured + deviceAccessProvider: cloudFeaturesEnabled ? FirestoreDeviceAccessService() : nil, deviceSessionCache: UserDefaultsAccountDeviceSessionCache() ) _accountController = StateObject(wrappedValue: accountController) let featureAccessController = FeatureAccessController( - provider: firebaseStatus == .configured + provider: cloudFeaturesEnabled ? FirestoreFeaturePolicyStore() : nil, cache: UserDefaultsFeaturePolicyCache() @@ -73,7 +76,7 @@ struct macgitApp: App { _featureAccessController = StateObject(wrappedValue: featureAccessController) let providerConfiguration = GitHubProviderAuthConfiguration.appConfiguration() let gitLabProviderConfiguration = GitLabProviderAuthConfiguration.appConfiguration() - let providerCloudStore: GitProviderAccountCloudStore? = firebaseStatus == .configured + let providerCloudStore: GitProviderAccountCloudStore? = cloudFeaturesEnabled ? FirestoreGitProviderAccountStore() : nil let providerStore = LocalFirstGitProviderAccountStore(cloudStore: providerCloudStore) @@ -131,14 +134,14 @@ struct macgitApp: App { ) _repositoryBookmarkController = StateObject( wrappedValue: RepositoryBookmarkController( - cloudStore: firebaseStatus == .configured + cloudStore: cloudFeaturesEnabled ? FirestoreRepositoryBookmarkStore() : nil ) ) _gitFlowConfigurationSyncController = StateObject( wrappedValue: GitFlowConfigurationSyncController( - cloudStore: firebaseStatus == .configured + cloudStore: cloudFeaturesEnabled ? FirestoreGitFlowConfigurationStore() : nil ) diff --git a/macgit/Models/RepositoryAIFileContext.swift b/macgit/Models/RepositoryAIFileContext.swift index 5f600c41..28a3d741 100644 --- a/macgit/Models/RepositoryAIFileContext.swift +++ b/macgit/Models/RepositoryAIFileContext.swift @@ -216,6 +216,10 @@ nonisolated enum RepositoryAIAnswerDecoder { let content: String if let suffixRange = body.range(of: #"\"\s*\}\s*$"#, options: [.regularExpression, .backwards]) { content = String(body[.. String? { + let output = try await runRemoteGit( + arguments: ["ls-remote", "--heads", "--", url, "refs/heads/\(branch)"], + in: repositoryURL, injection: injection + ) + return output.split(whereSeparator: \.isNewline).first? + .split(whereSeparator: \.isWhitespace).first.map(String.init) + } } diff --git a/macgit/Services/RepositoryAIGitCommandPolicy.swift b/macgit/Services/RepositoryAIGitCommandPolicy.swift index c6a87364..80bba8d2 100644 --- a/macgit/Services/RepositoryAIGitCommandPolicy.swift +++ b/macgit/Services/RepositoryAIGitCommandPolicy.swift @@ -41,6 +41,12 @@ nonisolated enum RepositoryAIGitCommandPolicy { "verify-pack", "whatchanged", ] + /// Commands that can render a diff, and therefore must not invoke an + /// external diff driver or textconv filter. + private static let diffProducingBuiltins: Set = [ + "diff", "diff-files", "diff-index", "diff-tree", "log", "show", "whatchanged", + ] + static func validatedArguments(_ arguments: [String]) throws -> [String] { guard let command = arguments.first?.trimmingCharacters(in: .whitespacesAndNewlines), !command.isEmpty else { @@ -57,7 +63,7 @@ nonisolated enum RepositoryAIGitCommandPolicy { ) } - let builtinSafetyArguments = command.hasPrefix("diff") + let builtinSafetyArguments = diffProducingBuiltins.contains(command) ? ["--no-ext-diff", "--no-textconv"] : [] return safeGlobalArguments + [command] + builtinSafetyArguments + commandArguments diff --git a/macgitTests/AICommitMessageTests.swift b/macgitTests/AICommitMessageTests.swift index bac728ce..1ea3e817 100644 --- a/macgitTests/AICommitMessageTests.swift +++ b/macgitTests/AICommitMessageTests.swift @@ -113,7 +113,7 @@ final class AICommitMessageTests: XCTestCase { ids, [.appleIntelligence, .openAI, .googleGemini, .anthropic, .deepSeek, .openRouter] ) - XCTAssertEqual(registry.provider(for: .appleIntelligence)?.descriptor.billing, .none) + XCTAssertEqual(registry.provider(for: .appleIntelligence)?.descriptor.billing, AIProviderBilling.none) for id in [AIProviderID.openAI, .anthropic, .googleGemini, .deepSeek, .openRouter] { let provider = registry.provider(for: id) let availability = await provider?.availability() diff --git a/macgitTests/ConflictAIResolutionTests.swift b/macgitTests/ConflictAIResolutionTests.swift index dbf814cc..1bbc8989 100644 --- a/macgitTests/ConflictAIResolutionTests.swift +++ b/macgitTests/ConflictAIResolutionTests.swift @@ -28,7 +28,7 @@ final class ConflictAIResolutionTests: XCTestCase { { "sectionIndex": 1, "action": "replace", - "replacementText": "merged()\n", + "replacementText": "merged()\\n", "reason": "Combines both behaviors", "question": "", "options": [] diff --git a/macgitTests/RepositoryAIPullRequestContextServiceTests.swift b/macgitTests/RepositoryAIPullRequestContextServiceTests.swift index a3f4b92e..c3b480ba 100644 --- a/macgitTests/RepositoryAIPullRequestContextServiceTests.swift +++ b/macgitTests/RepositoryAIPullRequestContextServiceTests.swift @@ -40,7 +40,7 @@ final class RepositoryAIPullRequestContextServiceTests: XCTestCase { XCTAssertEqual(provider.detailNumber, 12) XCTAssertEqual(provider.changesNumber, 12) XCTAssertEqual(result.toolName, "pull_request_context") - XCTAssertTrue(result.content.contains("PR #12")) + XCTAssertTrue(result.content.contains("Number: #12")) XCTAssertFalse(result.content.contains("secret-token")) XCTAssertFalse(result.content.contains("refresh-secret")) XCTAssertFalse(result.content.contains("Authorization")) diff --git a/macgitTests/RepositoryBookmarkTests.swift b/macgitTests/RepositoryBookmarkTests.swift index b0f31f0a..57ca0f50 100644 --- a/macgitTests/RepositoryBookmarkTests.swift +++ b/macgitTests/RepositoryBookmarkTests.swift @@ -102,7 +102,7 @@ final class RepositoryBookmarkTests: XCTestCase { ) ) let bookmark = RepositoryBookmark(identity: identity) - let localURL = URL(fileURLWithPath: "/Users/test/Project/codex") + let localURL = URL(fileURLWithPath: "/Users/test/Project/codex", isDirectory: true) controller.link(bookmark, to: localURL)