native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #121 from supabitapp/fix-worktree-run-script-removal

Fix worktree removal state cleanup

authored by

khoi and committed by
GitHub
7eed2ede 11e6d6e5

+181 -7
+56 -7
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
··· 148 148 149 149 case .repositoriesLoaded(let repositories, let failures, let roots, let animated): 150 150 let previousSelection = state.selectedWorktreeID 151 + let previousSelectedWorktree = state.worktree(for: previousSelection) 151 152 let mergedRepositories = mergeRepositories( 152 153 roots: roots, 153 154 loaded: repositories, ··· 166 167 message: errors.joined(separator: "\n") 167 168 ) 168 169 } 169 - let selectionChanged = previousSelection != state.selectedWorktreeID 170 170 let selectedWorktree = state.worktree(for: state.selectedWorktreeID) 171 + let selectionChanged = previousSelectedWorktree != selectedWorktree 171 172 var allEffects: [Effect<Action>] = [ 172 173 .send(.delegate(.repositoriesChanged(mergedRepositories))) 173 174 ] ··· 218 219 219 220 case .openRepositoriesFinished(let repositories, let failures, let invalidRoots, let roots): 220 221 let previousSelection = state.selectedWorktreeID 222 + let previousSelectedWorktree = state.worktree(for: previousSelection) 221 223 let mergedRepositories = mergeRepositories( 222 224 roots: roots, 223 225 loaded: repositories, ··· 244 246 ) 245 247 } 246 248 } 247 - let selectionChanged = previousSelection != state.selectedWorktreeID 248 249 let selectedWorktree = state.worktree(for: state.selectedWorktreeID) 250 + let selectionChanged = previousSelectedWorktree != selectedWorktree 249 251 var allEffects: [Effect<Action>] = [ 250 252 .send(.delegate(.repositoriesChanged(mergedRepositories))) 251 253 ] ··· 520 522 case .worktreeRemoved( 521 523 let worktreeID, 522 524 let repositoryID, 523 - let selectionWasRemoved, 525 + _, 524 526 let nextSelection 525 527 ): 528 + let previousSelection = state.selectedWorktreeID 529 + let previousSelectedWorktree = state.worktree(for: previousSelection) 530 + let wasPinned = state.pinnedWorktreeIDs.contains(worktreeID) 526 531 state.deletingWorktreeIDs.remove(worktreeID) 532 + state.pendingWorktrees.removeAll { $0.id == worktreeID } 527 533 state.pendingSetupScriptWorktreeIDs.remove(worktreeID) 528 534 state.pendingTerminalFocusWorktreeIDs.remove(worktreeID) 529 - let roots = state.repositories.map(\.rootURL) 530 - if selectionWasRemoved { 535 + state.worktreeInfoByID.removeValue(forKey: worktreeID) 536 + state.pinnedWorktreeIDs.removeAll { $0 == worktreeID } 537 + _ = removeWorktree(worktreeID, repositoryID: repositoryID, state: &state) 538 + let selectionNeedsUpdate = state.selectedWorktreeID == worktreeID 539 + if selectionNeedsUpdate { 531 540 state.selectedWorktreeID = 532 541 nextSelection ?? firstAvailableWorktreeID(in: repositoryID, state: state) 533 542 } 534 - return .merge( 543 + let roots = state.repositories.map(\.rootURL) 544 + let repositories = state.repositories 545 + let selectedWorktree = state.worktree(for: state.selectedWorktreeID) 546 + let selectionChanged = previousSelectedWorktree != selectedWorktree 547 + var immediateEffects: [Effect<Action>] = [ 548 + .send(.delegate(.repositoriesChanged(repositories))), 549 + ] 550 + if selectionChanged { 551 + immediateEffects.append(.send(.delegate(.selectedWorktreeChanged(selectedWorktree)))) 552 + } 553 + var followupEffects: [Effect<Action>] = [ 535 554 roots.isEmpty ? .none : .send(.reloadRepositories(animated: true)), 536 - .send(.delegate(.selectedWorktreeChanged(state.worktree(for: state.selectedWorktreeID)))) 555 + ] 556 + if wasPinned { 557 + let pinnedWorktreeIDs = state.pinnedWorktreeIDs 558 + followupEffects.append( 559 + .run { _ in 560 + await repositoryPersistence.savePinnedWorktreeIDs(pinnedWorktreeIDs) 561 + } 562 + ) 563 + } 564 + return .concatenate( 565 + .merge(immediateEffects), 566 + .merge(followupEffects) 537 567 ) 538 568 539 569 case .worktreeRemovalFailed(let message, let worktreeID): ··· 1106 1136 name: repository.name, 1107 1137 worktrees: worktrees 1108 1138 ) 1139 + } 1140 + 1141 + @discardableResult 1142 + private func removeWorktree( 1143 + _ worktreeID: Worktree.ID, 1144 + repositoryID: Repository.ID, 1145 + state: inout RepositoriesFeature.State 1146 + ) -> Bool { 1147 + guard let index = state.repositories.firstIndex(where: { $0.id == repositoryID }) else { return false } 1148 + let repository = state.repositories[index] 1149 + let filteredWorktrees = repository.worktrees.filter { $0.id != worktreeID } 1150 + guard filteredWorktrees.count != repository.worktrees.count else { return false } 1151 + state.repositories[index] = Repository( 1152 + id: repository.id, 1153 + rootURL: repository.rootURL, 1154 + name: repository.name, 1155 + worktrees: filteredWorktrees 1156 + ) 1157 + return true 1109 1158 } 1110 1159 1111 1160 private func updateWorktreeName(
+125
supacodeTests/RepositoriesFeatureTests.swift
··· 145 145 await store.receive(.delegate(.repositoriesChanged([repository]))) 146 146 } 147 147 148 + @Test func repositoriesLoadedUpdatesSelectedWorktreeDelegateOnChange() async { 149 + let repoRoot = "/tmp/repo" 150 + let worktree = makeWorktree(id: "/tmp/repo/main", name: "main", repoRoot: repoRoot) 151 + let repository = makeRepository(id: repoRoot, worktrees: [worktree]) 152 + let updatedWorktree = makeWorktree(id: "/tmp/repo/main", name: "main-updated", repoRoot: repoRoot) 153 + let updatedRepository = makeRepository(id: repoRoot, worktrees: [updatedWorktree]) 154 + var initialState = RepositoriesFeature.State(repositories: [repository]) 155 + initialState.selectedWorktreeID = worktree.id 156 + let store = TestStore(initialState: initialState) { 157 + RepositoriesFeature() 158 + } 159 + 160 + await store.send( 161 + .repositoriesLoaded( 162 + [updatedRepository], 163 + failures: [], 164 + roots: [repository.rootURL], 165 + animated: false 166 + ) 167 + ) { 168 + $0.repositories = [updatedRepository] 169 + } 170 + await store.receive(.delegate(.repositoriesChanged([updatedRepository]))) 171 + await store.receive(.delegate(.selectedWorktreeChanged(updatedWorktree))) 172 + } 173 + 174 + @Test func worktreeRemovedPrunesStateAndSendsDelegates() async { 175 + let repoRoot = "/tmp/repo" 176 + let mainWorktree = makeWorktree(id: "/tmp/repo/main", name: "main", repoRoot: repoRoot) 177 + let removedWorktree = makeWorktree(id: "/tmp/repo/feature", name: "feature", repoRoot: repoRoot) 178 + let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, removedWorktree]) 179 + let updatedRepository = makeRepository(id: repoRoot, worktrees: [mainWorktree]) 180 + var initialState = RepositoriesFeature.State(repositories: [repository]) 181 + initialState.selectedWorktreeID = mainWorktree.id 182 + initialState.deletingWorktreeIDs = [removedWorktree.id] 183 + initialState.pendingSetupScriptWorktreeIDs = [removedWorktree.id] 184 + initialState.pendingTerminalFocusWorktreeIDs = [removedWorktree.id] 185 + initialState.pendingWorktrees = [ 186 + PendingWorktree( 187 + id: removedWorktree.id, 188 + repositoryID: repository.id, 189 + name: "pending", 190 + detail: "" 191 + ), 192 + ] 193 + initialState.pinnedWorktreeIDs = [removedWorktree.id] 194 + initialState.worktreeInfoByID = [ 195 + removedWorktree.id: WorktreeInfoEntry(addedLines: 1, removedLines: 2, pullRequest: nil) 196 + ] 197 + let store = TestStore(initialState: initialState) { 198 + RepositoriesFeature() 199 + } withDependencies: { 200 + $0.gitClient.worktrees = { _ in [mainWorktree] } 201 + } 202 + 203 + await store.send( 204 + .worktreeRemoved( 205 + removedWorktree.id, 206 + repositoryID: repository.id, 207 + selectionWasRemoved: false, 208 + nextSelection: nil 209 + ) 210 + ) { 211 + $0.deletingWorktreeIDs = [] 212 + $0.pendingSetupScriptWorktreeIDs = [] 213 + $0.pendingTerminalFocusWorktreeIDs = [] 214 + $0.pendingWorktrees = [] 215 + $0.pinnedWorktreeIDs = [] 216 + $0.worktreeInfoByID = [:] 217 + $0.repositories = [updatedRepository] 218 + } 219 + await store.receive(.delegate(.repositoriesChanged([updatedRepository]))) 220 + await store.receive(.reloadRepositories(animated: true)) 221 + await store.receive( 222 + .repositoriesLoaded( 223 + [updatedRepository], 224 + failures: [], 225 + roots: [repository.rootURL], 226 + animated: true 227 + ) 228 + ) 229 + await store.receive(.delegate(.repositoriesChanged([updatedRepository]))) 230 + } 231 + 232 + @Test func worktreeRemovedResetsSelectionWhenDriftedToDeletingWorktree() async { 233 + let repoRoot = "/tmp/repo" 234 + let mainWorktree = makeWorktree(id: "/tmp/repo/main", name: "main", repoRoot: repoRoot) 235 + let removedWorktree = makeWorktree(id: "/tmp/repo/feature", name: "feature", repoRoot: repoRoot) 236 + let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, removedWorktree]) 237 + let updatedRepository = makeRepository(id: repoRoot, worktrees: [mainWorktree]) 238 + var initialState = RepositoriesFeature.State(repositories: [repository]) 239 + initialState.selectedWorktreeID = removedWorktree.id 240 + initialState.deletingWorktreeIDs = [removedWorktree.id] 241 + let store = TestStore(initialState: initialState) { 242 + RepositoriesFeature() 243 + } withDependencies: { 244 + $0.gitClient.worktrees = { _ in [mainWorktree] } 245 + } 246 + 247 + await store.send( 248 + .worktreeRemoved( 249 + removedWorktree.id, 250 + repositoryID: repository.id, 251 + selectionWasRemoved: false, 252 + nextSelection: nil 253 + ) 254 + ) { 255 + $0.deletingWorktreeIDs = [] 256 + $0.repositories = [updatedRepository] 257 + $0.selectedWorktreeID = mainWorktree.id 258 + } 259 + await store.receive(.delegate(.repositoriesChanged([updatedRepository]))) 260 + await store.receive(.delegate(.selectedWorktreeChanged(mainWorktree))) 261 + await store.receive(.reloadRepositories(animated: true)) 262 + await store.receive( 263 + .repositoriesLoaded( 264 + [updatedRepository], 265 + failures: [], 266 + roots: [repository.rootURL], 267 + animated: true 268 + ) 269 + ) 270 + await store.receive(.delegate(.repositoriesChanged([updatedRepository]))) 271 + } 272 + 148 273 @Test func createRandomWorktreeSucceededSendsRepositoriesChanged() async { 149 274 let repoRoot = "/tmp/repo" 150 275 let existingWorktree = makeWorktree(id: "/tmp/repo/wt-main", name: "main", repoRoot: repoRoot)