diff --git a/supacode/Domain/Worktree.swift b/supacode/Domain/Worktree.swift index 26089e3c..a65d827d 100644 --- a/supacode/Domain/Worktree.swift +++ b/supacode/Domain/Worktree.swift @@ -7,6 +7,7 @@ nonisolated struct Worktree: Identifiable, Hashable, Sendable { let workingDirectory: URL let repositoryRootURL: URL let createdAt: Date? + let isMain: Bool nonisolated init( id: String, @@ -22,6 +23,14 @@ nonisolated struct Worktree: Identifiable, Hashable, Sendable { self.workingDirectory = workingDirectory self.repositoryRootURL = repositoryRootURL self.createdAt = createdAt + // Pre-compute the main-worktree flag at construction time so that hot SwiftUI + // paths never call the expensive `URL.standardizedFileURL` getter during view + // updates. The fast equality check covers the common case where callers + // already pass normalized URLs; the standardized fallback protects against + // any future call site that forgets to normalize first. + self.isMain = + workingDirectory == repositoryRootURL + || workingDirectory.standardizedFileURL == repositoryRootURL.standardizedFileURL } } diff --git a/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift b/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift index 1de469de..f6fd46a7 100644 --- a/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift +++ b/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift @@ -1573,7 +1573,7 @@ extension RepositoriesFeature.State { } func isMainWorktree(_ worktree: Worktree) -> Bool { - worktree.workingDirectory.standardizedFileURL == worktree.repositoryRootURL.standardizedFileURL + worktree.isMain } func isWorktreeMerged(_ worktree: Worktree) -> Bool { diff --git a/supacodeTests/WorktreeIsMainTests.swift b/supacodeTests/WorktreeIsMainTests.swift new file mode 100644 index 00000000..e2bf31f0 --- /dev/null +++ b/supacodeTests/WorktreeIsMainTests.swift @@ -0,0 +1,66 @@ +import Foundation +import Testing + +@testable import supacode + +@MainActor +struct WorktreeIsMainTests { + @Test func identicalURLsAreMain() { + let root = URL(fileURLWithPath: "/tmp/repo") + let worktree = Worktree( + id: "/tmp/repo", + name: "main", + detail: ".", + workingDirectory: root, + repositoryRootURL: root, + ) + #expect(worktree.isMain == true) + } + + @Test func subdirectoryWorktreeIsNotMain() { + let worktree = Worktree( + id: "/tmp/repo/wt-1", + name: "feature", + detail: "wt-1", + workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt-1"), + repositoryRootURL: URL(fileURLWithPath: "/tmp/repo"), + ) + #expect(worktree.isMain == false) + } + + @Test func siblingWorktreeIsNotMain() { + let worktree = Worktree( + id: "/tmp/repo.wt/feature", + name: "feature", + detail: "../repo.wt/feature", + workingDirectory: URL(fileURLWithPath: "/tmp/repo.wt/feature"), + repositoryRootURL: URL(fileURLWithPath: "/tmp/repo"), + ) + #expect(worktree.isMain == false) + } + + @Test func dotComponentEquivalentURLsAreMain() { + // `/tmp/./repo` and `/tmp/repo` are semantically the same directory. + // The standardization fallback in Worktree.init covers this case even + // if the caller forgot to normalize the URL beforehand. + let worktree = Worktree( + id: "/tmp/repo", + name: "main", + detail: ".", + workingDirectory: URL(fileURLWithPath: "/tmp/./repo"), + repositoryRootURL: URL(fileURLWithPath: "/tmp/repo"), + ) + #expect(worktree.isMain == true) + } + + @Test func dotDotComponentEquivalentURLsAreMain() { + let worktree = Worktree( + id: "/tmp/repo", + name: "main", + detail: ".", + workingDirectory: URL(fileURLWithPath: "/tmp/nested/../repo"), + repositoryRootURL: URL(fileURLWithPath: "/tmp/repo"), + ) + #expect(worktree.isMain == true) + } +}