native macOS codings agent orchestrator
6
fork

Configure Feed

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

Merge pull request #190 from onevcat/feat/issue-175-merged-worktree-action

feat: merged worktree action picker

authored by

Wei Wang and committed by
GitHub
41240300 fd637b14

+288 -64
+2 -2
supacode/Features/App/Reducer/AppFeature.swift
··· 410 410 .send( 411 411 .repositories( 412 412 .githubIntegration( 413 - .setAutomaticallyArchiveMergedWorktrees( 414 - settings.automaticallyArchiveMergedWorktrees 413 + .setMergedWorktreeAction( 414 + settings.mergedWorktreeAction 415 415 ) 416 416 ) 417 417 )
+22 -11
supacode/Features/Repositories/Reducer/RepositoriesFeature+GithubIntegration.swift
··· 176 176 guard let repository = state.repositories[id: repositoryID] else { 177 177 return .none 178 178 } 179 - var archiveWorktreeIDs: [Worktree.ID] = [] 179 + var mergedWorktreeIDs: [Worktree.ID] = [] 180 180 for worktreeID in pullRequestsByWorktreeID.keys.sorted() { 181 181 guard let worktree = repository.worktrees[id: worktreeID] else { 182 182 continue ··· 193 193 pullRequest: pullRequest, 194 194 state: &state 195 195 ) 196 - if state.automaticallyArchiveMergedWorktrees, 196 + if state.mergedWorktreeAction != nil, 197 197 !previousMerged, 198 198 nextMerged, 199 199 !state.isMainWorktree(worktree), 200 200 !state.isWorktreeArchived(worktreeID), 201 201 !state.deletingWorktreeIDs.contains(worktreeID) 202 202 { 203 - archiveWorktreeIDs.append(worktreeID) 203 + mergedWorktreeIDs.append(worktreeID) 204 204 } 205 205 } 206 - guard !archiveWorktreeIDs.isEmpty else { 206 + guard !mergedWorktreeIDs.isEmpty else { 207 + return .none 208 + } 209 + switch state.mergedWorktreeAction { 210 + case .archive: 211 + return .merge( 212 + mergedWorktreeIDs.map { worktreeID in 213 + .send(.worktreeLifecycle(.archiveWorktreeConfirmed(worktreeID, repositoryID))) 214 + } 215 + ) 216 + case .delete: 217 + return .merge( 218 + mergedWorktreeIDs.map { worktreeID in 219 + .send(.worktreeLifecycle(.deleteWorktreeConfirmed(worktreeID, repositoryID))) 220 + } 221 + ) 222 + case nil: 207 223 return .none 208 224 } 209 - return .merge( 210 - archiveWorktreeIDs.map { worktreeID in 211 - .send(.worktreeLifecycle(.archiveWorktreeConfirmed(worktreeID, repositoryID))) 212 - } 213 - ) 214 225 215 226 case .pullRequestAction(let worktreeID, let action): 216 227 guard let worktree = state.worktree(for: worktreeID), ··· 534 545 .cancel(id: CancelID.githubIntegrationRecovery) 535 546 ) 536 547 537 - case .setAutomaticallyArchiveMergedWorktrees(let isEnabled): 538 - state.automaticallyArchiveMergedWorktrees = isEnabled 548 + case .setMergedWorktreeAction(let action): 549 + state.mergedWorktreeAction = action 539 550 return .none 540 551 } 541 552 }
+2 -2
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
··· 160 160 pullRequestsByWorktreeID: [Worktree.ID: GithubPullRequest?] 161 161 ) 162 162 case setGithubIntegrationEnabled(Bool) 163 - case setAutomaticallyArchiveMergedWorktrees(Bool) 163 + case setMergedWorktreeAction(MergedWorktreeAction?) 164 164 case pullRequestAction(Worktree.ID, PullRequestAction) 165 165 } 166 166 ··· 201 201 var pinnedWorktreeIDs: [Worktree.ID] = [] 202 202 var archivedWorktrees: [ArchivedWorktree] = [] 203 203 var archivedAutoDeletePeriod: AutoDeletePeriod? 204 - var automaticallyArchiveMergedWorktrees = false 204 + var mergedWorktreeAction: MergedWorktreeAction? 205 205 var moveNotifiedWorktreeToTop = true 206 206 var lastFocusedWorktreeID: Worktree.ID? 207 207 var preCanvasWorktreeID: Worktree.ID?
+70 -7
supacode/Features/Settings/Models/GlobalSettings.swift
··· 15 15 var crashReportsEnabled: Bool 16 16 var githubIntegrationEnabled: Bool 17 17 var deleteBranchOnDeleteWorktree: Bool 18 - var automaticallyArchiveMergedWorktrees: Bool 18 + var mergedWorktreeAction: MergedWorktreeAction? 19 19 var promptForWorktreeCreation: Bool 20 20 var fetchOriginBeforeWorktreeCreation: Bool 21 21 var defaultWorktreeBaseDirectoryPath: String? ··· 41 41 crashReportsEnabled: true, 42 42 githubIntegrationEnabled: true, 43 43 deleteBranchOnDeleteWorktree: true, 44 - automaticallyArchiveMergedWorktrees: false, 44 + mergedWorktreeAction: nil, 45 45 promptForWorktreeCreation: true, 46 46 fetchOriginBeforeWorktreeCreation: true, 47 47 defaultWorktreeBaseDirectoryPath: nil, ··· 68 68 crashReportsEnabled: Bool, 69 69 githubIntegrationEnabled: Bool, 70 70 deleteBranchOnDeleteWorktree: Bool, 71 - automaticallyArchiveMergedWorktrees: Bool, 71 + mergedWorktreeAction: MergedWorktreeAction? = nil, 72 72 promptForWorktreeCreation: Bool, 73 73 fetchOriginBeforeWorktreeCreation: Bool = true, 74 74 defaultWorktreeBaseDirectoryPath: String? = nil, ··· 93 93 self.crashReportsEnabled = crashReportsEnabled 94 94 self.githubIntegrationEnabled = githubIntegrationEnabled 95 95 self.deleteBranchOnDeleteWorktree = deleteBranchOnDeleteWorktree 96 - self.automaticallyArchiveMergedWorktrees = automaticallyArchiveMergedWorktrees 96 + self.mergedWorktreeAction = mergedWorktreeAction 97 97 self.promptForWorktreeCreation = promptForWorktreeCreation 98 98 self.fetchOriginBeforeWorktreeCreation = fetchOriginBeforeWorktreeCreation 99 99 self.defaultWorktreeBaseDirectoryPath = defaultWorktreeBaseDirectoryPath ··· 103 103 self.keybindingUserOverrides = keybindingUserOverrides 104 104 } 105 105 106 + func encode(to encoder: any Encoder) throws { 107 + var container = encoder.container(keyedBy: CodingKeys.self) 108 + try container.encode(appearanceMode, forKey: .appearanceMode) 109 + try container.encode(defaultEditorID, forKey: .defaultEditorID) 110 + try container.encode(confirmBeforeQuit, forKey: .confirmBeforeQuit) 111 + try container.encode(updateChannel, forKey: .updateChannel) 112 + try container.encode(updatesAutomaticallyCheckForUpdates, forKey: .updatesAutomaticallyCheckForUpdates) 113 + try container.encode(updatesAutomaticallyDownloadUpdates, forKey: .updatesAutomaticallyDownloadUpdates) 114 + try container.encode(inAppNotificationsEnabled, forKey: .inAppNotificationsEnabled) 115 + try container.encode(notificationSoundEnabled, forKey: .notificationSoundEnabled) 116 + try container.encode(systemNotificationsEnabled, forKey: .systemNotificationsEnabled) 117 + try container.encode(moveNotifiedWorktreeToTop, forKey: .moveNotifiedWorktreeToTop) 118 + try container.encode(commandFinishedNotificationEnabled, forKey: .commandFinishedNotificationEnabled) 119 + try container.encode(commandFinishedNotificationThreshold, forKey: .commandFinishedNotificationThreshold) 120 + try container.encode(analyticsEnabled, forKey: .analyticsEnabled) 121 + try container.encode(crashReportsEnabled, forKey: .crashReportsEnabled) 122 + try container.encode(githubIntegrationEnabled, forKey: .githubIntegrationEnabled) 123 + try container.encode(deleteBranchOnDeleteWorktree, forKey: .deleteBranchOnDeleteWorktree) 124 + try container.encodeIfPresent(mergedWorktreeAction, forKey: .mergedWorktreeAction) 125 + try container.encode(promptForWorktreeCreation, forKey: .promptForWorktreeCreation) 126 + try container.encode(fetchOriginBeforeWorktreeCreation, forKey: .fetchOriginBeforeWorktreeCreation) 127 + try container.encodeIfPresent(defaultWorktreeBaseDirectoryPath, forKey: .defaultWorktreeBaseDirectoryPath) 128 + try container.encode(restoreTerminalLayoutOnLaunch, forKey: .restoreTerminalLayoutOnLaunch) 129 + try container.encodeIfPresent(archivedAutoDeletePeriod?.rawValue, forKey: .archivedAutoDeletePeriod) 130 + try container.encodeIfPresent(terminalFontSize, forKey: .terminalFontSize) 131 + try container.encode(keybindingUserOverrides, forKey: .keybindingUserOverrides) 132 + } 133 + 134 + private enum CodingKeys: String, CodingKey { 135 + case appearanceMode 136 + case defaultEditorID 137 + case confirmBeforeQuit 138 + case updateChannel 139 + case updatesAutomaticallyCheckForUpdates 140 + case updatesAutomaticallyDownloadUpdates 141 + case inAppNotificationsEnabled 142 + case notificationSoundEnabled 143 + case systemNotificationsEnabled 144 + case moveNotifiedWorktreeToTop 145 + case commandFinishedNotificationEnabled 146 + case commandFinishedNotificationThreshold 147 + case analyticsEnabled 148 + case crashReportsEnabled 149 + case githubIntegrationEnabled 150 + case deleteBranchOnDeleteWorktree 151 + case mergedWorktreeAction 152 + case promptForWorktreeCreation 153 + case fetchOriginBeforeWorktreeCreation 154 + case defaultWorktreeBaseDirectoryPath 155 + case restoreTerminalLayoutOnLaunch 156 + case archivedAutoDeletePeriod 157 + case terminalFontSize 158 + case keybindingUserOverrides 159 + // Legacy key for migration 160 + case automaticallyArchiveMergedWorktrees 161 + } 162 + 106 163 init(from decoder: any Decoder) throws { 107 164 let container = try decoder.container(keyedBy: CodingKeys.self) 108 165 appearanceMode = try container.decode(AppearanceMode.self, forKey: .appearanceMode) ··· 147 204 deleteBranchOnDeleteWorktree = 148 205 try container.decodeIfPresent(Bool.self, forKey: .deleteBranchOnDeleteWorktree) 149 206 ?? Self.default.deleteBranchOnDeleteWorktree 150 - automaticallyArchiveMergedWorktrees = 151 - try container.decodeIfPresent(Bool.self, forKey: .automaticallyArchiveMergedWorktrees) 152 - ?? Self.default.automaticallyArchiveMergedWorktrees 207 + if let decoded = try container.decodeIfPresent(MergedWorktreeAction.self, forKey: .mergedWorktreeAction) { 208 + mergedWorktreeAction = decoded 209 + } else if let legacyBool = try container.decodeIfPresent( 210 + Bool.self, forKey: .automaticallyArchiveMergedWorktrees 211 + ) { 212 + mergedWorktreeAction = legacyBool ? .archive : nil 213 + } else { 214 + mergedWorktreeAction = Self.default.mergedWorktreeAction 215 + } 153 216 promptForWorktreeCreation = 154 217 try container.decodeIfPresent(Bool.self, forKey: .promptForWorktreeCreation) 155 218 ?? Self.default.promptForWorktreeCreation
+15
supacode/Features/Settings/Models/MergedWorktreeAction.swift
··· 1 + import Foundation 2 + 3 + nonisolated enum MergedWorktreeAction: String, CaseIterable, Codable, Equatable, Sendable, Identifiable { 4 + case archive 5 + case delete 6 + 7 + var id: String { rawValue } 8 + 9 + var title: String { 10 + switch self { 11 + case .archive: "Archive" 12 + case .delete: "Delete" 13 + } 14 + } 15 + }
+4 -4
supacode/Features/Settings/Reducer/SettingsFeature.swift
··· 21 21 var crashReportsEnabled: Bool 22 22 var githubIntegrationEnabled: Bool 23 23 var deleteBranchOnDeleteWorktree: Bool 24 - var automaticallyArchiveMergedWorktrees: Bool 24 + var mergedWorktreeAction: MergedWorktreeAction? 25 25 var archivedAutoDeletePeriod: AutoDeletePeriod? 26 26 var promptForWorktreeCreation: Bool 27 27 var fetchRemoteBeforeWorktreeCreation: 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 archivedAutoDeletePeriod = settings.archivedAutoDeletePeriod 58 58 promptForWorktreeCreation = settings.promptForWorktreeCreation 59 59 fetchRemoteBeforeWorktreeCreation = settings.fetchOriginBeforeWorktreeCreation ··· 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: fetchRemoteBeforeWorktreeCreation, 88 88 defaultWorktreeBaseDirectoryPath: SupacodePaths.normalizedWorktreeBaseDirectoryPath( ··· 180 180 state.crashReportsEnabled = normalizedSettings.crashReportsEnabled 181 181 state.githubIntegrationEnabled = normalizedSettings.githubIntegrationEnabled 182 182 state.deleteBranchOnDeleteWorktree = normalizedSettings.deleteBranchOnDeleteWorktree 183 - state.automaticallyArchiveMergedWorktrees = normalizedSettings.automaticallyArchiveMergedWorktrees 183 + state.mergedWorktreeAction = normalizedSettings.mergedWorktreeAction 184 184 state.archivedAutoDeletePeriod = normalizedSettings.archivedAutoDeletePeriod 185 185 state.promptForWorktreeCreation = normalizedSettings.promptForWorktreeCreation 186 186 state.fetchRemoteBeforeWorktreeCreation = normalizedSettings.fetchOriginBeforeWorktreeCreation
+37 -24
supacode/Features/Settings/Views/WorktreeSettingsView.swift
··· 14 14 ) 15 15 VStack(alignment: .leading) { 16 16 Form { 17 - Section("Worktree") { 17 + Section("Creation") { 18 18 VStack(alignment: .leading) { 19 19 TextField( 20 20 "Default: current behavior", ··· 30 30 .frame(maxWidth: .infinity, alignment: .leading) 31 31 VStack(alignment: .leading) { 32 32 Toggle( 33 + "Prompt for branch name during creation", 34 + isOn: $store.promptForWorktreeCreation 35 + ) 36 + .help("Ask for branch name and base ref before creating a worktree.") 37 + Text("When enabled, you choose the branch name and where it branches from before creating the worktree.") 38 + .foregroundStyle(.secondary) 39 + } 40 + VStack(alignment: .leading) { 41 + Toggle( 42 + "Fetch remote before creating worktree", 43 + isOn: $store.fetchRemoteBeforeWorktreeCreation 44 + ) 45 + .help("Runs git fetch <remote> before creating a worktree.") 46 + Text("Keeps remote-tracking base branches current. Fetch failures are logged and creation continues.") 47 + .foregroundStyle(.secondary) 48 + } 49 + } 50 + Section("Cleanup") { 51 + VStack(alignment: .leading) { 52 + Toggle( 33 53 "Also delete local branch when deleting a worktree", 34 54 isOn: $store.deleteBranchOnDeleteWorktree 35 55 ) ··· 40 60 .foregroundStyle(.red) 41 61 } 42 62 .frame(maxWidth: .infinity, alignment: .leading) 43 - Toggle( 44 - "Automatically archive merged worktrees", 45 - isOn: $store.automaticallyArchiveMergedWorktrees 46 - ) 47 - .help("Archive worktrees automatically when their pull requests are merged.") 63 + Picker(selection: $store.mergedWorktreeAction) { 64 + Text("Do nothing").tag(MergedWorktreeAction?.none) 65 + ForEach(MergedWorktreeAction.allCases) { action in 66 + Text(action.title).tag(MergedWorktreeAction?.some(action)) 67 + } 68 + } label: { 69 + Text("When a pull request is merged") 70 + switch store.mergedWorktreeAction { 71 + case .archive: 72 + Text("Archives worktrees when their pull requests are merged.") 73 + case .delete: 74 + Text("Follows the \"Also delete local branch when deleting a worktree\" option above.") 75 + case nil: 76 + EmptyView() 77 + } 78 + } 48 79 VStack(alignment: .leading) { 49 80 Picker(selection: $store.archivedAutoDeletePeriod) { 50 81 Text("Never").tag(AutoDeletePeriod?.none) ··· 55 86 Text("Auto-delete archived worktrees") 56 87 Text("Permanently removes archived worktrees after the selected period.") 57 88 } 58 - } 59 - VStack(alignment: .leading) { 60 - Toggle( 61 - "Prompt for branch name during creation", 62 - isOn: $store.promptForWorktreeCreation 63 - ) 64 - .help("Ask for branch name and base ref before creating a worktree.") 65 - Text("When enabled, you choose the branch name and where it branches from before creating the worktree.") 66 - .foregroundStyle(.secondary) 67 - } 68 - VStack(alignment: .leading) { 69 - Toggle( 70 - "Fetch remote before creating worktree", 71 - isOn: $store.fetchRemoteBeforeWorktreeCreation 72 - ) 73 - .help("Runs git fetch <remote> before creating a worktree.") 74 - Text("Keeps remote-tracking base branches current. Fetch failures are logged and creation continues.") 75 - .foregroundStyle(.secondary) 76 89 } 77 90 } 78 91 }
+4 -4
supacodeTests/AppFeatureSettingsChangedTests.swift
··· 10 10 @Test(.dependencies) func settingsChangedPropagatesRepositorySettings() async { 11 11 var settings = GlobalSettings.default 12 12 settings.githubIntegrationEnabled = false 13 - settings.automaticallyArchiveMergedWorktrees = true 13 + settings.mergedWorktreeAction = .archive 14 14 settings.moveNotifiedWorktreeToTop = false 15 15 let store = TestStore(initialState: AppFeature.State()) { 16 16 AppFeature() ··· 20 20 await store.receive(\.repositories.githubIntegration.setGithubIntegrationEnabled) { 21 21 $0.repositories.githubIntegrationAvailability = .disabled 22 22 } 23 - await store.receive(\.repositories.githubIntegration.setAutomaticallyArchiveMergedWorktrees) { 24 - $0.repositories.automaticallyArchiveMergedWorktrees = true 23 + await store.receive(\.repositories.githubIntegration.setMergedWorktreeAction) { 24 + $0.repositories.mergedWorktreeAction = .archive 25 25 } 26 26 await store.receive(\.repositories.setArchivedAutoDeletePeriod) 27 27 await store.receive(\.repositories.worktreeOrdering.setMoveNotifiedWorktreeToTop) { ··· 82 82 $0.resolvedKeybindings = expectedResolved 83 83 } 84 84 await store.receive(\.repositories.githubIntegration.setGithubIntegrationEnabled) 85 - await store.receive(\.repositories.githubIntegration.setAutomaticallyArchiveMergedWorktrees) 85 + await store.receive(\.repositories.githubIntegration.setMergedWorktreeAction) 86 86 await store.receive(\.repositories.setArchivedAutoDeletePeriod) 87 87 await store.receive(\.repositories.worktreeOrdering.setMoveNotifiedWorktreeToTop) 88 88 await store.receive(\.updates.applySettings) {
+67 -3
supacodeTests/RepositoriesFeatureTests.swift
··· 2755 2755 ) 2756 2756 let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree]) 2757 2757 var state = makeState(repositories: [repository]) 2758 - state.automaticallyArchiveMergedWorktrees = true 2758 + state.mergedWorktreeAction = .archive 2759 2759 let fixedDate = Date(timeIntervalSince1970: 1_000_000) 2760 2760 let store = TestStore(initialState: state) { 2761 2761 RepositoriesFeature() ··· 2789 2789 let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) 2790 2790 let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree]) 2791 2791 var state = makeState(repositories: [repository]) 2792 - state.automaticallyArchiveMergedWorktrees = true 2792 + state.mergedWorktreeAction = .archive 2793 + let store = TestStore(initialState: state) { 2794 + RepositoriesFeature() 2795 + } 2796 + let mergedPullRequest = makePullRequest(state: "MERGED", headRefName: mainWorktree.name) 2797 + 2798 + await store.send( 2799 + .githubIntegration( 2800 + .repositoryPullRequestsLoaded( 2801 + repositoryID: repository.id, 2802 + pullRequestsByWorktreeID: [mainWorktree.id: mergedPullRequest] 2803 + )) 2804 + ) { 2805 + $0.worktreeInfoByID[mainWorktree.id] = WorktreeInfoEntry( 2806 + addedLines: nil, 2807 + removedLines: nil, 2808 + pullRequest: mergedPullRequest 2809 + ) 2810 + } 2811 + await store.finish() 2812 + } 2813 + 2814 + @Test func repositoryPullRequestsLoadedAutoDeletesWhenEnabled() async { 2815 + let repoRoot = "/tmp/repo" 2816 + let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) 2817 + let featureWorktree = makeWorktree( 2818 + id: "\(repoRoot)/feature", 2819 + name: "feature", 2820 + repoRoot: repoRoot 2821 + ) 2822 + let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree, featureWorktree]) 2823 + var state = makeState(repositories: [repository]) 2824 + state.mergedWorktreeAction = .delete 2825 + let store = TestStore(initialState: state) { 2826 + RepositoriesFeature() 2827 + } withDependencies: { 2828 + $0.gitClient.removeWorktree = { worktree, _ in worktree.workingDirectory } 2829 + } 2830 + store.exhaustivity = .off 2831 + let mergedPullRequest = makePullRequest(state: "MERGED", headRefName: featureWorktree.name) 2832 + 2833 + await store.send( 2834 + .githubIntegration( 2835 + .repositoryPullRequestsLoaded( 2836 + repositoryID: repository.id, 2837 + pullRequestsByWorktreeID: [featureWorktree.id: mergedPullRequest] 2838 + )) 2839 + ) { 2840 + $0.worktreeInfoByID[featureWorktree.id] = WorktreeInfoEntry( 2841 + addedLines: nil, 2842 + removedLines: nil, 2843 + pullRequest: mergedPullRequest 2844 + ) 2845 + } 2846 + await store.receive(\.worktreeLifecycle.deleteWorktreeConfirmed) { 2847 + $0.deletingWorktreeIDs = [featureWorktree.id] 2848 + } 2849 + } 2850 + 2851 + @Test func repositoryPullRequestsLoadedSkipsAutoDeleteForMainWorktree() async { 2852 + let repoRoot = "/tmp/repo" 2853 + let mainWorktree = makeWorktree(id: repoRoot, name: "main", repoRoot: repoRoot) 2854 + let repository = makeRepository(id: repoRoot, worktrees: [mainWorktree]) 2855 + var state = makeState(repositories: [repository]) 2856 + state.mergedWorktreeAction = .delete 2793 2857 let store = TestStore(initialState: state) { 2794 2858 RepositoriesFeature() 2795 2859 } ··· 2823 2887 let openPullRequest = makePullRequest(state: "OPEN", headRefName: featureWorktree.name, number: 12) 2824 2888 var state = makeState(repositories: [repository]) 2825 2889 state.githubIntegrationAvailability = .disabled 2826 - state.automaticallyArchiveMergedWorktrees = true 2890 + state.mergedWorktreeAction = .archive 2827 2891 state.worktreeInfoByID[featureWorktree.id] = WorktreeInfoEntry( 2828 2892 addedLines: nil, 2829 2893 removedLines: nil,
+6 -6
supacodeTests/SettingsFeatureTests.swift
··· 25 25 crashReportsEnabled: true, 26 26 githubIntegrationEnabled: true, 27 27 deleteBranchOnDeleteWorktree: false, 28 - automaticallyArchiveMergedWorktrees: true, 28 + mergedWorktreeAction: .archive, 29 29 promptForWorktreeCreation: true 30 30 ) 31 31 @Shared(.settingsFile) var settingsFile ··· 51 51 $0.crashReportsEnabled = true 52 52 $0.githubIntegrationEnabled = true 53 53 $0.deleteBranchOnDeleteWorktree = false 54 - $0.automaticallyArchiveMergedWorktrees = true 54 + $0.mergedWorktreeAction = .archive 55 55 $0.promptForWorktreeCreation = true 56 56 } 57 57 await store.receive(\.delegate.settingsChanged) ··· 73 73 crashReportsEnabled: false, 74 74 githubIntegrationEnabled: true, 75 75 deleteBranchOnDeleteWorktree: true, 76 - automaticallyArchiveMergedWorktrees: false, 76 + mergedWorktreeAction: nil, 77 77 promptForWorktreeCreation: false 78 78 ) 79 79 @Shared(.settingsFile) var settingsFile ··· 101 101 crashReportsEnabled: initialSettings.crashReportsEnabled, 102 102 githubIntegrationEnabled: initialSettings.githubIntegrationEnabled, 103 103 deleteBranchOnDeleteWorktree: initialSettings.deleteBranchOnDeleteWorktree, 104 - automaticallyArchiveMergedWorktrees: initialSettings.automaticallyArchiveMergedWorktrees, 104 + mergedWorktreeAction: initialSettings.mergedWorktreeAction, 105 105 promptForWorktreeCreation: initialSettings.promptForWorktreeCreation 106 106 ) 107 107 await store.receive(\.delegate.settingsChanged) ··· 171 171 crashReportsEnabled: false, 172 172 githubIntegrationEnabled: true, 173 173 deleteBranchOnDeleteWorktree: true, 174 - automaticallyArchiveMergedWorktrees: true, 174 + mergedWorktreeAction: .archive, 175 175 promptForWorktreeCreation: false 176 176 ) 177 177 ··· 190 190 $0.crashReportsEnabled = false 191 191 $0.githubIntegrationEnabled = true 192 192 $0.deleteBranchOnDeleteWorktree = true 193 - $0.automaticallyArchiveMergedWorktrees = true 193 + $0.mergedWorktreeAction = .archive 194 194 $0.promptForWorktreeCreation = false 195 195 $0.selection = selection 196 196 $0.repositorySettings = RepositorySettingsFeature.State(
+59 -1
supacodeTests/SettingsFilePersistenceTests.swift
··· 108 108 #expect(settings.global.crashReportsEnabled == true) 109 109 #expect(settings.global.githubIntegrationEnabled == true) 110 110 #expect(settings.global.deleteBranchOnDeleteWorktree == true) 111 - #expect(settings.global.automaticallyArchiveMergedWorktrees == false) 111 + #expect(settings.global.mergedWorktreeAction == nil) 112 112 #expect(settings.global.promptForWorktreeCreation == true) 113 113 #expect(settings.global.defaultWorktreeBaseDirectoryPath == nil) 114 114 #expect(settings.global.restoreTerminalLayoutOnLaunch == false) ··· 116 116 #expect(settings.repositoryRoots.isEmpty) 117 117 #expect(settings.pinnedWorktreeIDs.isEmpty) 118 118 } 119 + 120 + @Test(.dependencies) func legacyAutomaticallyArchiveMergedWorktreesMigratesToMergedWorktreeAction() throws { 121 + let legacy = LegacyAutoArchiveSettingsFile( 122 + global: LegacyAutoArchiveGlobalSettings( 123 + appearanceMode: .dark, 124 + updatesAutomaticallyCheckForUpdates: false, 125 + updatesAutomaticallyDownloadUpdates: true, 126 + automaticallyArchiveMergedWorktrees: true 127 + ), 128 + repositories: [:] 129 + ) 130 + let data = try JSONEncoder().encode(legacy) 131 + let storage = MutableTestStorage(initialData: data) 132 + 133 + let settings: SettingsFile = withDependencies { 134 + $0.settingsFileStorage = storage.storage 135 + } operation: { 136 + @Shared(.settingsFile) var settings: SettingsFile 137 + return settings 138 + } 139 + 140 + #expect(settings.global.mergedWorktreeAction == .archive) 141 + } 142 + 143 + @Test(.dependencies) func legacyAutomaticallyArchiveFalseMigratesToNil() throws { 144 + let legacy = LegacyAutoArchiveSettingsFile( 145 + global: LegacyAutoArchiveGlobalSettings( 146 + appearanceMode: .dark, 147 + updatesAutomaticallyCheckForUpdates: false, 148 + updatesAutomaticallyDownloadUpdates: true, 149 + automaticallyArchiveMergedWorktrees: false 150 + ), 151 + repositories: [:] 152 + ) 153 + let data = try JSONEncoder().encode(legacy) 154 + let storage = MutableTestStorage(initialData: data) 155 + 156 + let settings: SettingsFile = withDependencies { 157 + $0.settingsFileStorage = storage.storage 158 + } operation: { 159 + @Shared(.settingsFile) var settings: SettingsFile 160 + return settings 161 + } 162 + 163 + #expect(settings.global.mergedWorktreeAction == nil) 164 + } 119 165 } 120 166 121 167 nonisolated private final class MutableTestStorage: @unchecked Sendable { ··· 148 194 defer { lock.unlock() } 149 195 self.data = data 150 196 } 197 + } 198 + 199 + private struct LegacyAutoArchiveSettingsFile: Codable { 200 + var global: LegacyAutoArchiveGlobalSettings 201 + var repositories: [String: RepositorySettings] 202 + } 203 + 204 + private struct LegacyAutoArchiveGlobalSettings: Codable { 205 + var appearanceMode: AppearanceMode 206 + var updatesAutomaticallyCheckForUpdates: Bool 207 + var updatesAutomaticallyDownloadUpdates: Bool 208 + var automaticallyArchiveMergedWorktrees: Bool 151 209 } 152 210 153 211 private struct LegacySettingsFile: Codable {