native macOS codings agent orchestrator
6
fork

Configure Feed

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

Merge pull request #209 from supabitapp/sbertix/delete-on-merge

Replace auto-archive toggle with merged worktree action picker

authored by

Stefano Bertagno and committed by
GitHub
5c1c97b4 f9ff4606

+405 -48
+2 -2
supacode/Features/App/Reducer/AppFeature.swift
··· 275 275 .send(.repositories(.setGithubIntegrationEnabled(settings.githubIntegrationEnabled))), 276 276 .send( 277 277 .repositories( 278 - .setAutomaticallyArchiveMergedWorktrees( 279 - settings.automaticallyArchiveMergedWorktrees 278 + .setMergedWorktreeAction( 279 + settings.mergedWorktreeAction 280 280 ) 281 281 ) 282 282 ),
+17 -12
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
··· 83 83 var removingRepositoryIDs: Set<Repository.ID> = [] 84 84 var pinnedWorktreeIDs: [Worktree.ID] = [] 85 85 var archivedWorktreeIDs: [Worktree.ID] = [] 86 - var automaticallyArchiveMergedWorktrees = false 86 + var mergedWorktreeAction: MergedWorktreeAction? 87 87 var moveNotifiedWorktreeToTop = true 88 88 var lastFocusedWorktreeID: Worktree.ID? 89 89 var shouldRestoreLastFocusedWorktree = false ··· 238 238 pullRequestsByWorktreeID: [Worktree.ID: GithubPullRequest?] 239 239 ) 240 240 case setGithubIntegrationEnabled(Bool) 241 - case setAutomaticallyArchiveMergedWorktrees(Bool) 241 + case setMergedWorktreeAction(MergedWorktreeAction?) 242 242 case setMoveNotifiedWorktreeToTop(Bool) 243 243 case pullRequestAction(Worktree.ID, PullRequestAction) 244 244 case showToast(StatusToast) ··· 2299 2299 return .none 2300 2300 } 2301 2301 var archiveWorktreeIDs: [Worktree.ID] = [] 2302 + var deleteWorktreeIDs: [Worktree.ID] = [] 2302 2303 for worktreeID in pullRequestsByWorktreeID.keys.sorted() { 2303 2304 guard let worktree = repository.worktrees[id: worktreeID] else { 2304 2305 continue ··· 2315 2316 pullRequest: pullRequest, 2316 2317 state: &state 2317 2318 ) 2318 - if state.automaticallyArchiveMergedWorktrees, 2319 + if let mergedAction = state.mergedWorktreeAction, 2319 2320 !previousMerged, 2320 2321 nextMerged, 2321 2322 !state.isMainWorktree(worktree), ··· 2323 2324 !state.deletingWorktreeIDs.contains(worktreeID), 2324 2325 !state.deleteScriptWorktreeIDs.contains(worktreeID) 2325 2326 { 2326 - archiveWorktreeIDs.append(worktreeID) 2327 + switch mergedAction { 2328 + case .archive: 2329 + archiveWorktreeIDs.append(worktreeID) 2330 + case .delete: 2331 + deleteWorktreeIDs.append(worktreeID) 2332 + } 2327 2333 } 2328 2334 } 2329 - guard !archiveWorktreeIDs.isEmpty else { 2335 + let effects: [Effect<Action>] = 2336 + archiveWorktreeIDs.map { .send(.archiveWorktreeConfirmed($0, repositoryID)) } 2337 + + deleteWorktreeIDs.map { .send(.deleteWorktreeConfirmed($0, repositoryID)) } 2338 + guard !effects.isEmpty else { 2330 2339 return .none 2331 2340 } 2332 - return .merge( 2333 - archiveWorktreeIDs.map { worktreeID in 2334 - .send(.archiveWorktreeConfirmed(worktreeID, repositoryID)) 2335 - } 2336 - ) 2341 + return .merge(effects) 2337 2342 2338 2343 case .pullRequestAction(let worktreeID, let action): 2339 2344 guard let worktree = state.worktree(for: worktreeID), ··· 2659 2664 .cancel(id: CancelID.githubIntegrationRecovery) 2660 2665 ) 2661 2666 2662 - case .setAutomaticallyArchiveMergedWorktrees(let isEnabled): 2663 - state.automaticallyArchiveMergedWorktrees = isEnabled 2667 + case .setMergedWorktreeAction(let action): 2668 + state.mergedWorktreeAction = action 2664 2669 return .none 2665 2670 2666 2671 case .setMoveNotifiedWorktreeToTop(let isEnabled):
+28 -7
supacode/Features/Settings/Models/GlobalSettings.swift
··· 13 13 var crashReportsEnabled: Bool 14 14 var githubIntegrationEnabled: Bool 15 15 var deleteBranchOnDeleteWorktree: Bool 16 - var automaticallyArchiveMergedWorktrees: Bool 16 + var mergedWorktreeAction: MergedWorktreeAction? 17 17 var promptForWorktreeCreation: Bool 18 18 var fetchOriginBeforeWorktreeCreation: Bool 19 19 var defaultWorktreeBaseDirectoryPath: String? ··· 39 39 crashReportsEnabled: true, 40 40 githubIntegrationEnabled: true, 41 41 deleteBranchOnDeleteWorktree: true, 42 - automaticallyArchiveMergedWorktrees: false, 42 + mergedWorktreeAction: nil, 43 43 promptForWorktreeCreation: true, 44 44 fetchOriginBeforeWorktreeCreation: true, 45 45 copyIgnoredOnWorktreeCreate: false, ··· 66 66 crashReportsEnabled: Bool, 67 67 githubIntegrationEnabled: Bool, 68 68 deleteBranchOnDeleteWorktree: Bool, 69 - automaticallyArchiveMergedWorktrees: Bool, 69 + mergedWorktreeAction: MergedWorktreeAction? = nil, 70 70 promptForWorktreeCreation: Bool, 71 71 fetchOriginBeforeWorktreeCreation: Bool = true, 72 72 copyIgnoredOnWorktreeCreate: Bool = false, ··· 91 91 self.crashReportsEnabled = crashReportsEnabled 92 92 self.githubIntegrationEnabled = githubIntegrationEnabled 93 93 self.deleteBranchOnDeleteWorktree = deleteBranchOnDeleteWorktree 94 - self.automaticallyArchiveMergedWorktrees = automaticallyArchiveMergedWorktrees 94 + self.mergedWorktreeAction = mergedWorktreeAction 95 95 self.promptForWorktreeCreation = promptForWorktreeCreation 96 96 self.fetchOriginBeforeWorktreeCreation = fetchOriginBeforeWorktreeCreation 97 97 self.copyIgnoredOnWorktreeCreate = copyIgnoredOnWorktreeCreate ··· 141 141 deleteBranchOnDeleteWorktree = 142 142 try container.decodeIfPresent(Bool.self, forKey: .deleteBranchOnDeleteWorktree) 143 143 ?? Self.default.deleteBranchOnDeleteWorktree 144 - automaticallyArchiveMergedWorktrees = 145 - try container.decodeIfPresent(Bool.self, forKey: .automaticallyArchiveMergedWorktrees) 146 - ?? Self.default.automaticallyArchiveMergedWorktrees 144 + // `try?` intentionally swallows decoding errors (e.g. unrecognized raw values 145 + // from a future app version) and falls through to the legacy migration path, 146 + // which defaults to `nil`. Silently resetting the preference is acceptable 147 + // because `nil` (do nothing) is the safest default. 148 + if let action = try? container.decodeIfPresent(MergedWorktreeAction.self, forKey: .mergedWorktreeAction) { 149 + mergedWorktreeAction = action 150 + } else { 151 + // Legacy migration. 152 + struct LegacyCodingKey: CodingKey { 153 + var stringValue: String 154 + init?(stringValue: String) { self.stringValue = stringValue } 155 + var intValue: Int? { nil } 156 + init?(intValue: Int) { nil } 157 + } 158 + let legacy = try decoder.container(keyedBy: LegacyCodingKey.self) 159 + if let legacyBool = try legacy.decodeIfPresent( 160 + Bool.self, 161 + forKey: LegacyCodingKey(stringValue: "automaticallyArchiveMergedWorktrees")! 162 + ) { 163 + mergedWorktreeAction = legacyBool ? .archive : Self.default.mergedWorktreeAction 164 + } else { 165 + mergedWorktreeAction = Self.default.mergedWorktreeAction 166 + } 167 + } 147 168 promptForWorktreeCreation = 148 169 try container.decodeIfPresent(Bool.self, forKey: .promptForWorktreeCreation) 149 170 ?? Self.default.promptForWorktreeCreation
+21
supacode/Features/Settings/Models/MergedWorktreeAction.swift
··· 1 + import Foundation 2 + 3 + /// Action to perform automatically when a worktree's pull request is merged. 4 + /// 5 + /// Use as `MergedWorktreeAction?` where `nil` means no automatic action. 6 + nonisolated enum MergedWorktreeAction: String, CaseIterable, Codable, Equatable, Sendable, Identifiable { 7 + case archive 8 + 9 + /// Deletes the worktree. Whether the local branch is also deleted 10 + /// depends on the `deleteBranchOnDeleteWorktree` setting. 11 + case delete 12 + 13 + var id: String { rawValue } 14 + 15 + var title: String { 16 + switch self { 17 + case .archive: return "Archive" 18 + case .delete: return "Delete" 19 + } 20 + } 21 + }
+4 -4
supacode/Features/Settings/Reducer/SettingsFeature.swift
··· 20 20 var crashReportsEnabled: Bool 21 21 var githubIntegrationEnabled: Bool 22 22 var deleteBranchOnDeleteWorktree: Bool 23 - var automaticallyArchiveMergedWorktrees: Bool 23 + var mergedWorktreeAction: MergedWorktreeAction? 24 24 var promptForWorktreeCreation: Bool 25 25 var fetchOriginBeforeWorktreeCreation: Bool 26 26 var copyIgnoredOnWorktreeCreate: Bool ··· 53 53 crashReportsEnabled = settings.crashReportsEnabled 54 54 githubIntegrationEnabled = settings.githubIntegrationEnabled 55 55 deleteBranchOnDeleteWorktree = settings.deleteBranchOnDeleteWorktree 56 - automaticallyArchiveMergedWorktrees = settings.automaticallyArchiveMergedWorktrees 56 + mergedWorktreeAction = settings.mergedWorktreeAction 57 57 promptForWorktreeCreation = settings.promptForWorktreeCreation 58 58 fetchOriginBeforeWorktreeCreation = settings.fetchOriginBeforeWorktreeCreation 59 59 copyIgnoredOnWorktreeCreate = settings.copyIgnoredOnWorktreeCreate ··· 82 82 crashReportsEnabled: crashReportsEnabled, 83 83 githubIntegrationEnabled: githubIntegrationEnabled, 84 84 deleteBranchOnDeleteWorktree: deleteBranchOnDeleteWorktree, 85 - automaticallyArchiveMergedWorktrees: automaticallyArchiveMergedWorktrees, 85 + mergedWorktreeAction: mergedWorktreeAction, 86 86 promptForWorktreeCreation: promptForWorktreeCreation, 87 87 fetchOriginBeforeWorktreeCreation: fetchOriginBeforeWorktreeCreation, 88 88 copyIgnoredOnWorktreeCreate: copyIgnoredOnWorktreeCreate, ··· 166 166 state.crashReportsEnabled = normalizedSettings.crashReportsEnabled 167 167 state.githubIntegrationEnabled = normalizedSettings.githubIntegrationEnabled 168 168 state.deleteBranchOnDeleteWorktree = normalizedSettings.deleteBranchOnDeleteWorktree 169 - state.automaticallyArchiveMergedWorktrees = normalizedSettings.automaticallyArchiveMergedWorktrees 169 + state.mergedWorktreeAction = normalizedSettings.mergedWorktreeAction 170 170 state.promptForWorktreeCreation = normalizedSettings.promptForWorktreeCreation 171 171 state.fetchOriginBeforeWorktreeCreation = normalizedSettings.fetchOriginBeforeWorktreeCreation 172 172 state.copyIgnoredOnWorktreeCreate = normalizedSettings.copyIgnoredOnWorktreeCreate
+15 -3
supacode/Features/Settings/Views/WorktreeSettingsView.swift
··· 42 42 } 43 43 } 44 44 Section("Clean-up") { 45 - Toggle(isOn: $store.automaticallyArchiveMergedWorktrees) { 46 - Text("Automatically archive merged worktrees") 47 - Text("Archives worktrees when their pull requests are merged.") 45 + Picker(selection: $store.mergedWorktreeAction) { 46 + Text("Do nothing").tag(MergedWorktreeAction?.none) 47 + ForEach(MergedWorktreeAction.allCases) { action in 48 + Text(action.title).tag(MergedWorktreeAction?.some(action)) 49 + } 50 + } label: { 51 + Text("When a pull request is merged") 52 + switch store.mergedWorktreeAction { 53 + case .archive: 54 + Text("Archives worktrees when their pull requests are merged.") 55 + case .delete: 56 + Text("Follows the \"Delete local branch with worktree\" option below.") 57 + case nil: 58 + EmptyView() 59 + } 48 60 } 49 61 Toggle(isOn: $store.deleteBranchOnDeleteWorktree) { 50 62 Text("Delete local branch with worktree")
+3 -3
supacodeTests/AppFeatureSettingsChangedTests.swift
··· 9 9 @Test(.dependencies) func settingsChangedPropagatesRepositorySettings() async { 10 10 var settings = GlobalSettings.default 11 11 settings.githubIntegrationEnabled = false 12 - settings.automaticallyArchiveMergedWorktrees = true 12 + settings.mergedWorktreeAction = .archive 13 13 settings.moveNotifiedWorktreeToTop = false 14 14 let store = TestStore(initialState: AppFeature.State()) { 15 15 AppFeature() ··· 19 19 await store.receive(\.repositories.setGithubIntegrationEnabled) { 20 20 $0.repositories.githubIntegrationAvailability = .disabled 21 21 } 22 - await store.receive(\.repositories.setAutomaticallyArchiveMergedWorktrees) { 23 - $0.repositories.automaticallyArchiveMergedWorktrees = true 22 + await store.receive(\.repositories.setMergedWorktreeAction) { 23 + $0.repositories.mergedWorktreeAction = .archive 24 24 } 25 25 await store.receive(\.repositories.setMoveNotifiedWorktreeToTop) { 26 26 $0.repositories.moveNotifiedWorktreeToTop = false
+226 -8
supacodeTests/RepositoriesFeatureTests.swift
··· 1436 1436 id: pendingID, 1437 1437 repositoryID: repository.id, 1438 1438 progress: WorktreeCreationProgress(stage: .loadingLocalBranches) 1439 - ), 1439 + ) 1440 1440 ] 1441 1441 let store = TestStore(initialState: state) { 1442 1442 RepositoriesFeature() ··· 1473 1473 stage: .checkingRepositoryMode, 1474 1474 worktreeName: "swift-otter" 1475 1475 ) 1476 - ), 1476 + ) 1477 1477 ] 1478 1478 let store = TestStore(initialState: state) { 1479 1479 RepositoriesFeature() ··· 1670 1670 addedLines: nil, 1671 1671 removedLines: nil, 1672 1672 pullRequest: makePullRequest(state: "MERGED") 1673 - ), 1673 + ) 1674 1674 ] 1675 1675 let store = TestStore(initialState: state) { 1676 1676 RepositoriesFeature() ··· 2868 2868 id: removedWorktree.id, 2869 2869 repositoryID: repository.id, 2870 2870 progress: WorktreeCreationProgress(stage: .choosingWorktreeName) 2871 - ), 2871 + ) 2872 2872 ] 2873 2873 initialState.pinnedWorktreeIDs = [removedWorktree.id] 2874 2874 initialState.worktreeInfoByID = [ ··· 2951 2951 id: pendingID, 2952 2952 repositoryID: repository.id, 2953 2953 progress: WorktreeCreationProgress(stage: .loadingLocalBranches) 2954 - ), 2954 + ) 2955 2955 ] 2956 2956 initialState.selection = .worktree(pendingID) 2957 2957 initialState.sidebarSelectedWorktreeIDs = [existingWorktree.id, pendingID] ··· 2995 2995 ) 2996 2996 let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree]) 2997 2997 var state = makeState(repositories: [repository]) 2998 - state.automaticallyArchiveMergedWorktrees = true 2998 + state.mergedWorktreeAction = .archive 2999 2999 let store = TestStore(initialState: state) { 3000 3000 RepositoriesFeature() 3001 3001 } ··· 3025 3025 let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) 3026 3026 let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree]) 3027 3027 var state = makeState(repositories: [repository]) 3028 - state.automaticallyArchiveMergedWorktrees = true 3028 + state.mergedWorktreeAction = .archive 3029 3029 let store = TestStore(initialState: state) { 3030 3030 RepositoriesFeature() 3031 3031 } ··· 3046 3046 await store.finish() 3047 3047 } 3048 3048 3049 + @Test func repositoryPullRequestsLoadedAutoDeletesWhenEnabled() async { 3050 + let repoRoot = "/tmp/auto-delete-repo" 3051 + let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) 3052 + let featureWorktree = makeWorktree( 3053 + id: "\(repoRoot)/feature", 3054 + name: "feature", 3055 + repoRoot: repoRoot 3056 + ) 3057 + let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree]) 3058 + var state = makeState(repositories: [repository]) 3059 + state.mergedWorktreeAction = .delete 3060 + let store = TestStore(initialState: state) { 3061 + RepositoriesFeature() 3062 + } 3063 + // Exhaustivity is off because `deleteWorktreeConfirmed` triggers 3064 + // async git operations that require extensive dependency mocking. 3065 + store.exhaustivity = .off 3066 + let mergedPullRequest = makePullRequest(state: "MERGED", headRefName: featureWorktree.name) 3067 + 3068 + await store.send( 3069 + .repositoryPullRequestsLoaded( 3070 + repositoryID: repository.id, 3071 + pullRequestsByWorktreeID: [featureWorktree.id: mergedPullRequest] 3072 + ) 3073 + ) { 3074 + $0.worktreeInfoByID[featureWorktree.id] = WorktreeInfoEntry( 3075 + addedLines: nil, 3076 + removedLines: nil, 3077 + pullRequest: mergedPullRequest 3078 + ) 3079 + } 3080 + await store.receive(\.deleteWorktreeConfirmed) 3081 + } 3082 + 3083 + @Test func repositoryPullRequestsLoadedDoesNothingWhenMergedWorktreeActionNil() async { 3084 + let repoRoot = "/tmp/repo" 3085 + let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) 3086 + let featureWorktree = makeWorktree( 3087 + id: "\(repoRoot)/feature", 3088 + name: "feature", 3089 + repoRoot: repoRoot 3090 + ) 3091 + let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree]) 3092 + var state = makeState(repositories: [repository]) 3093 + state.mergedWorktreeAction = nil 3094 + let store = TestStore(initialState: state) { 3095 + RepositoriesFeature() 3096 + } 3097 + let mergedPullRequest = makePullRequest(state: "MERGED", headRefName: featureWorktree.name) 3098 + 3099 + await store.send( 3100 + .repositoryPullRequestsLoaded( 3101 + repositoryID: repository.id, 3102 + pullRequestsByWorktreeID: [featureWorktree.id: mergedPullRequest] 3103 + ) 3104 + ) { 3105 + $0.worktreeInfoByID[featureWorktree.id] = WorktreeInfoEntry( 3106 + addedLines: nil, 3107 + removedLines: nil, 3108 + pullRequest: mergedPullRequest 3109 + ) 3110 + } 3111 + await store.finish() 3112 + } 3113 + 3114 + @Test func repositoryPullRequestsLoadedSkipsAutoActionForArchivedWorktree() async { 3115 + let repoRoot = "/tmp/repo" 3116 + let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) 3117 + let featureWorktree = makeWorktree( 3118 + id: "\(repoRoot)/feature", 3119 + name: "feature", 3120 + repoRoot: repoRoot 3121 + ) 3122 + let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree]) 3123 + var state = makeState(repositories: [repository]) 3124 + state.mergedWorktreeAction = .delete 3125 + state.archivedWorktreeIDs = [featureWorktree.id] 3126 + let store = TestStore(initialState: state) { 3127 + RepositoriesFeature() 3128 + } 3129 + let mergedPullRequest = makePullRequest(state: "MERGED", headRefName: featureWorktree.name) 3130 + 3131 + await store.send( 3132 + .repositoryPullRequestsLoaded( 3133 + repositoryID: repository.id, 3134 + pullRequestsByWorktreeID: [featureWorktree.id: mergedPullRequest] 3135 + ) 3136 + ) { 3137 + $0.worktreeInfoByID[featureWorktree.id] = WorktreeInfoEntry( 3138 + addedLines: nil, 3139 + removedLines: nil, 3140 + pullRequest: mergedPullRequest 3141 + ) 3142 + } 3143 + await store.finish() 3144 + } 3145 + 3146 + @Test func repositoryPullRequestsLoadedSkipsAutoActionForDeletingWorktree() async { 3147 + let repoRoot = "/tmp/repo" 3148 + let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) 3149 + let featureWorktree = makeWorktree( 3150 + id: "\(repoRoot)/feature", 3151 + name: "feature", 3152 + repoRoot: repoRoot 3153 + ) 3154 + let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree]) 3155 + var state = makeState(repositories: [repository]) 3156 + state.mergedWorktreeAction = .archive 3157 + state.deletingWorktreeIDs = [featureWorktree.id] 3158 + let store = TestStore(initialState: state) { 3159 + RepositoriesFeature() 3160 + } 3161 + let mergedPullRequest = makePullRequest(state: "MERGED", headRefName: featureWorktree.name) 3162 + 3163 + await store.send( 3164 + .repositoryPullRequestsLoaded( 3165 + repositoryID: repository.id, 3166 + pullRequestsByWorktreeID: [featureWorktree.id: mergedPullRequest] 3167 + ) 3168 + ) { 3169 + $0.worktreeInfoByID[featureWorktree.id] = WorktreeInfoEntry( 3170 + addedLines: nil, 3171 + removedLines: nil, 3172 + pullRequest: mergedPullRequest 3173 + ) 3174 + } 3175 + await store.finish() 3176 + } 3177 + 3178 + @Test func repositoryPullRequestsLoadedSkipsAutoActionForDeleteScriptWorktree() async { 3179 + let repoRoot = "/tmp/repo" 3180 + let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) 3181 + let featureWorktree = makeWorktree( 3182 + id: "\(repoRoot)/feature", 3183 + name: "feature", 3184 + repoRoot: repoRoot 3185 + ) 3186 + let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree]) 3187 + var state = makeState(repositories: [repository]) 3188 + state.mergedWorktreeAction = .delete 3189 + state.deleteScriptWorktreeIDs = [featureWorktree.id] 3190 + let store = TestStore(initialState: state) { 3191 + RepositoriesFeature() 3192 + } 3193 + let mergedPullRequest = makePullRequest(state: "MERGED", headRefName: featureWorktree.name) 3194 + 3195 + await store.send( 3196 + .repositoryPullRequestsLoaded( 3197 + repositoryID: repository.id, 3198 + pullRequestsByWorktreeID: [featureWorktree.id: mergedPullRequest] 3199 + ) 3200 + ) { 3201 + $0.worktreeInfoByID[featureWorktree.id] = WorktreeInfoEntry( 3202 + addedLines: nil, 3203 + removedLines: nil, 3204 + pullRequest: mergedPullRequest 3205 + ) 3206 + } 3207 + await store.finish() 3208 + } 3209 + 3210 + @Test func repositoryPullRequestsLoadedSkipsAutoActionWhenAlreadyMerged() async { 3211 + let repoRoot = "/tmp/repo" 3212 + let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) 3213 + let featureWorktree = makeWorktree( 3214 + id: "\(repoRoot)/feature", 3215 + name: "feature", 3216 + repoRoot: repoRoot 3217 + ) 3218 + let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree]) 3219 + let mergedPullRequest = makePullRequest(state: "MERGED", headRefName: featureWorktree.name) 3220 + var state = makeState(repositories: [repository]) 3221 + state.mergedWorktreeAction = .delete 3222 + state.worktreeInfoByID[featureWorktree.id] = WorktreeInfoEntry( 3223 + addedLines: nil, 3224 + removedLines: nil, 3225 + pullRequest: mergedPullRequest 3226 + ) 3227 + let store = TestStore(initialState: state) { 3228 + RepositoriesFeature() 3229 + } 3230 + // Re-receive a MERGED PR that differs in a field (updatedAt) so it passes 3231 + // the `previousPullRequest != pullRequest` check, but should still be 3232 + // skipped by the `!previousMerged` guard. 3233 + let refreshedPullRequest = GithubPullRequest( 3234 + number: mergedPullRequest.number, 3235 + title: "PR", 3236 + state: "MERGED", 3237 + additions: 0, 3238 + deletions: 0, 3239 + isDraft: false, 3240 + reviewDecision: nil, 3241 + mergeable: nil, 3242 + mergeStateStatus: nil, 3243 + updatedAt: Date(), 3244 + url: mergedPullRequest.url, 3245 + headRefName: featureWorktree.name, 3246 + baseRefName: "main", 3247 + commitsCount: 1, 3248 + authorLogin: "khoi", 3249 + statusCheckRollup: nil 3250 + ) 3251 + 3252 + await store.send( 3253 + .repositoryPullRequestsLoaded( 3254 + repositoryID: repository.id, 3255 + pullRequestsByWorktreeID: [featureWorktree.id: refreshedPullRequest] 3256 + ) 3257 + ) { 3258 + $0.worktreeInfoByID[featureWorktree.id] = WorktreeInfoEntry( 3259 + addedLines: nil, 3260 + removedLines: nil, 3261 + pullRequest: refreshedPullRequest 3262 + ) 3263 + } 3264 + await store.finish() 3265 + } 3266 + 3049 3267 @Test func pullRequestActionMergeRefreshesImmediatelyWithoutSyntheticMergedState() async { 3050 3268 let repoRoot = "/tmp/repo" 3051 3269 let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) ··· 3058 3276 let openPullRequest = makePullRequest(state: "OPEN", headRefName: featureWorktree.name, number: 12) 3059 3277 var state = makeState(repositories: [repository]) 3060 3278 state.githubIntegrationAvailability = .disabled 3061 - state.automaticallyArchiveMergedWorktrees = true 3279 + state.mergedWorktreeAction = .archive 3062 3280 state.worktreeInfoByID[featureWorktree.id] = WorktreeInfoEntry( 3063 3281 addedLines: nil, 3064 3282 removedLines: nil,
+6 -6
supacodeTests/SettingsFeatureTests.swift
··· 26 26 crashReportsEnabled: true, 27 27 githubIntegrationEnabled: true, 28 28 deleteBranchOnDeleteWorktree: false, 29 - automaticallyArchiveMergedWorktrees: true, 29 + mergedWorktreeAction: .archive, 30 30 promptForWorktreeCreation: true, 31 31 terminalThemeSyncEnabled: false, 32 32 ) ··· 53 53 $0.crashReportsEnabled = true 54 54 $0.githubIntegrationEnabled = true 55 55 $0.deleteBranchOnDeleteWorktree = false 56 - $0.automaticallyArchiveMergedWorktrees = true 56 + $0.mergedWorktreeAction = .archive 57 57 $0.promptForWorktreeCreation = true 58 58 $0.fetchOriginBeforeWorktreeCreation = true 59 59 $0.terminalThemeSyncEnabled = false ··· 77 77 crashReportsEnabled: false, 78 78 githubIntegrationEnabled: true, 79 79 deleteBranchOnDeleteWorktree: true, 80 - automaticallyArchiveMergedWorktrees: false, 80 + mergedWorktreeAction: nil, 81 81 promptForWorktreeCreation: false 82 82 ) 83 83 @Shared(.settingsFile) var settingsFile ··· 105 105 crashReportsEnabled: initialSettings.crashReportsEnabled, 106 106 githubIntegrationEnabled: initialSettings.githubIntegrationEnabled, 107 107 deleteBranchOnDeleteWorktree: initialSettings.deleteBranchOnDeleteWorktree, 108 - automaticallyArchiveMergedWorktrees: initialSettings.automaticallyArchiveMergedWorktrees, 108 + mergedWorktreeAction: initialSettings.mergedWorktreeAction, 109 109 promptForWorktreeCreation: initialSettings.promptForWorktreeCreation 110 110 ) 111 111 await store.receive(\.delegate.settingsChanged) ··· 185 185 crashReportsEnabled: false, 186 186 githubIntegrationEnabled: true, 187 187 deleteBranchOnDeleteWorktree: true, 188 - automaticallyArchiveMergedWorktrees: true, 188 + mergedWorktreeAction: .archive, 189 189 promptForWorktreeCreation: false 190 190 ) 191 191 ··· 204 204 $0.crashReportsEnabled = false 205 205 $0.githubIntegrationEnabled = true 206 206 $0.deleteBranchOnDeleteWorktree = true 207 - $0.automaticallyArchiveMergedWorktrees = true 207 + $0.mergedWorktreeAction = .archive 208 208 $0.promptForWorktreeCreation = false 209 209 $0.selection = selection 210 210 $0.repositorySettings = RepositorySettingsFeature.State(
+81 -1
supacodeTests/SettingsFilePersistenceTests.swift
··· 77 77 #expect(reloaded == .default) 78 78 } 79 79 80 + @Test(.dependencies) func decodesLegacyAutoArchiveTrueAsMergedWorktreeActionArchive() throws { 81 + let legacy = LegacySettingsFileWithArchiveFlag( 82 + global: LegacyGlobalSettingsWithArchiveFlag( 83 + appearanceMode: .dark, 84 + updatesAutomaticallyCheckForUpdates: true, 85 + updatesAutomaticallyDownloadUpdates: false, 86 + automaticallyArchiveMergedWorktrees: true 87 + ), 88 + repositories: [:] 89 + ) 90 + let data = try JSONEncoder().encode(legacy) 91 + let storage = MutableTestStorage(initialData: data) 92 + 93 + let settings: SettingsFile = withDependencies { 94 + $0.settingsFileStorage = storage.storage 95 + } operation: { 96 + @Shared(.settingsFile) var settings: SettingsFile 97 + return settings 98 + } 99 + 100 + #expect(settings.global.mergedWorktreeAction == .archive) 101 + } 102 + 103 + @Test(.dependencies) func decodesLegacyAutoArchiveFalseAsMergedWorktreeActionNil() throws { 104 + let legacy = LegacySettingsFileWithArchiveFlag( 105 + global: LegacyGlobalSettingsWithArchiveFlag( 106 + appearanceMode: .dark, 107 + updatesAutomaticallyCheckForUpdates: true, 108 + updatesAutomaticallyDownloadUpdates: false, 109 + automaticallyArchiveMergedWorktrees: false 110 + ), 111 + repositories: [:] 112 + ) 113 + let data = try JSONEncoder().encode(legacy) 114 + let storage = MutableTestStorage(initialData: data) 115 + 116 + let settings: SettingsFile = withDependencies { 117 + $0.settingsFileStorage = storage.storage 118 + } operation: { 119 + @Shared(.settingsFile) var settings: SettingsFile 120 + return settings 121 + } 122 + 123 + #expect(settings.global.mergedWorktreeAction == nil) 124 + } 125 + 126 + @Test(.dependencies) func roundTripsMergedWorktreeActionDelete() throws { 127 + let storage = SettingsTestStorage() 128 + 129 + withDependencies { 130 + $0.settingsFileStorage = storage.storage 131 + } operation: { 132 + @Shared(.settingsFile) var settings: SettingsFile 133 + $settings.withLock { 134 + $0.global.mergedWorktreeAction = .delete 135 + } 136 + } 137 + 138 + let reloaded: SettingsFile = withDependencies { 139 + $0.settingsFileStorage = storage.storage 140 + } operation: { 141 + @Shared(.settingsFile) var reloaded: SettingsFile 142 + return reloaded 143 + } 144 + 145 + #expect(reloaded.global.mergedWorktreeAction == .delete) 146 + } 147 + 80 148 @Test(.dependencies) func decodesMissingInAppNotificationsEnabled() throws { 81 149 let legacy = LegacySettingsFile( 82 150 global: LegacyGlobalSettings( ··· 108 176 #expect(settings.global.crashReportsEnabled == true) 109 177 #expect(settings.global.githubIntegrationEnabled == true) 110 178 #expect(settings.global.deleteBranchOnDeleteWorktree == true) 111 - #expect(settings.global.automaticallyArchiveMergedWorktrees == false) 179 + #expect(settings.global.mergedWorktreeAction == nil) 112 180 #expect(settings.global.promptForWorktreeCreation == true) 113 181 #expect(settings.global.defaultWorktreeBaseDirectoryPath == nil) 114 182 #expect(settings.global.defaultEditorID == OpenWorktreeAction.automaticSettingsID) ··· 159 227 var updatesAutomaticallyCheckForUpdates: Bool 160 228 var updatesAutomaticallyDownloadUpdates: Bool 161 229 } 230 + 231 + private struct LegacySettingsFileWithArchiveFlag: Codable { 232 + var global: LegacyGlobalSettingsWithArchiveFlag 233 + var repositories: [String: RepositorySettings] 234 + } 235 + 236 + private struct LegacyGlobalSettingsWithArchiveFlag: Codable { 237 + var appearanceMode: AppearanceMode 238 + var updatesAutomaticallyCheckForUpdates: Bool 239 + var updatesAutomaticallyDownloadUpdates: Bool 240 + var automaticallyArchiveMergedWorktrees: Bool 241 + }
+2 -2
supacodeTests/WorktreeTerminalManagerTests.swift
··· 98 98 title: "Unread", 99 99 body: "body", 100 100 isRead: false 101 - ), 101 + ) 102 102 ] 103 103 state.onNotificationIndicatorChanged?() 104 104 state.notifications = [ ··· 107 107 title: "Read", 108 108 body: "body", 109 109 isRead: true 110 - ), 110 + ) 111 111 ] 112 112 113 113 let stream = manager.eventStream()