Skip to content
Open
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
10 changes: 10 additions & 0 deletions supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4220,6 +4220,13 @@ struct RepositoriesFeature {
await send(.repositoryPullRequestRefreshCompleted(repositoryID))
return
}
// A PR whose head is the repository's default branch is always a sync PR
// (e.g. `main -> dev`), never the default-branch worktree's own work, so it
// must not badge that worktree — the head=default branch persists after
// merge, leaving a stale merged PR permanently matched to it (#695). Fetch
// defensively: an unknown default branch falls back to today's behavior
// rather than dropping every worktree's badge on a transient `gh` failure.
let defaultBranch = try? await githubCLI.defaultBranch(repositoryRootURL)
do {
let prsByBranch = try await githubCLI.batchPullRequests(
remoteInfo.host,
Expand All @@ -4229,6 +4236,9 @@ struct RepositoriesFeature {
)
var pullRequestsByWorktreeID: [Worktree.ID: GithubPullRequest?] = [:]
for worktree in worktrees {
if worktree.name == defaultBranch {
continue
}
pullRequestsByWorktreeID[worktree.id] = prsByBranch[worktree.name]
}
await send(
Expand Down
87 changes: 87 additions & 0 deletions supacodeTests/RepositoriesFeatureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5608,6 +5608,93 @@ struct RepositoriesFeatureTests {
await store.finish()
}

// A worktree on the repository's default branch must never show a PR badge: any
// PR whose head is the default branch is a sync PR (e.g. `main -> dev`), not that
// worktree's own work, and it persists after merge as a stale match (#695).
@Test func repositoryPullRequestRefreshSkipsDefaultBranchWorktree() async {
let repoRoot = "/tmp/repo"
let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot)
let featureWorktree = makeWorktree(
id: "\(repoRoot)/feature",
name: "feature",
repoRoot: repoRoot
)
let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree])
var state = makeState(repositories: [repository])
state.githubIntegrationAvailability = .available
state.reconcileSidebarForTesting()
// The newest merged `head=main` sync PR is what the matcher would otherwise pin to `main`.
let mergedMainPullRequest = makePullRequest(state: "MERGED", headRefName: "main", number: 320)
let featurePullRequest = makePullRequest(state: "OPEN", headRefName: "feature", number: 5)
let store = TestStore(initialState: state) {
RepositoriesFeature()
} withDependencies: {
$0.gitClient.remoteInfo = { _ in GithubRemoteInfo(host: "github.com", owner: "o", repo: "r") }
$0.githubCLI.defaultBranch = { _ in "main" }
$0.githubCLI.batchPullRequests = { _, _, _, _ in
["main": mergedMainPullRequest, "feature": featurePullRequest]
}
}
store.exhaustivity = .off

await store.send(
.worktreeInfoEvent(
.repositoryPullRequestRefresh(
repositoryRootURL: URL(fileURLWithPath: repoRoot),
worktreeIDs: [mainWorktree.id, featureWorktree.id]
)
)
)
await store.receive(\.repositoryPullRequestRefreshCompleted)
await store.finish()

#expect(store.state.sidebarItems[id: mainWorktree.id]?.pullRequest == nil)
#expect(store.state.sidebarItems[id: featureWorktree.id]?.pullRequest == featurePullRequest)
}

// Guard the defensive fetch: if the default-branch lookup fails, the refresh must
// fall back to today's behavior (assign by branch name) rather than dropping every
// worktree's badge, so a transient `gh` failure never regresses the whole repo (#695).
@Test func repositoryPullRequestRefreshAssignsWhenDefaultBranchLookupFails() async {
let repoRoot = "/tmp/repo"
let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot)
let featureWorktree = makeWorktree(
id: "\(repoRoot)/feature",
name: "feature",
repoRoot: repoRoot
)
let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree])
var state = makeState(repositories: [repository])
state.githubIntegrationAvailability = .available
state.reconcileSidebarForTesting()
let mainPullRequest = makePullRequest(state: "OPEN", headRefName: "main", number: 320)
let featurePullRequest = makePullRequest(state: "OPEN", headRefName: "feature", number: 5)
let store = TestStore(initialState: state) {
RepositoriesFeature()
} withDependencies: {
$0.gitClient.remoteInfo = { _ in GithubRemoteInfo(host: "github.com", owner: "o", repo: "r") }
$0.githubCLI.defaultBranch = { _ in throw GithubCLIError.commandFailed("boom") }
$0.githubCLI.batchPullRequests = { _, _, _, _ in
["main": mainPullRequest, "feature": featurePullRequest]
}
}
store.exhaustivity = .off

await store.send(
.worktreeInfoEvent(
.repositoryPullRequestRefresh(
repositoryRootURL: URL(fileURLWithPath: repoRoot),
worktreeIDs: [mainWorktree.id, featureWorktree.id]
)
)
)
await store.receive(\.repositoryPullRequestRefreshCompleted)
await store.finish()

#expect(store.state.sidebarItems[id: mainWorktree.id]?.pullRequest == mainPullRequest)
#expect(store.state.sidebarItems[id: featureWorktree.id]?.pullRequest == featurePullRequest)
}

@Test func unarchiveWorktreeNoopsWhenNotArchived() async {
let worktree = makeWorktree(id: "/tmp/wt", name: "owl")
let repository = makeRepository(id: "/tmp/repo", worktrees: [worktree])
Expand Down
Loading