diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d60b1f5..1dce1b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: build: name: Build and analyse - runs-on: macos-latest + runs-on: macos-26 strategy: matrix: diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 18bfcee..082ea3c 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1191,6 +1191,10 @@ } } }, + "Version: %@" : { + "comment" : "A label showing the current version of the app.", + "isCommentAutoGenerated" : true + }, "welcome.subtitle" : { "localizations" : { "de" : { diff --git a/PReek/ViewModel/PullRequestsViewModel.swift b/PReek/ViewModel/PullRequestsViewModel.swift index 1db7b6e..9e086cf 100644 --- a/PReek/ViewModel/PullRequestsViewModel.swift +++ b/PReek/ViewModel/PullRequestsViewModel.swift @@ -28,6 +28,12 @@ class PullRequestsViewModel: ObservableObject { updatePullRequestIndexMap() } + deinit { + timer?.invalidate() + timer = nil + cancellables.removeAll() + } + @Published private(set) var lastUpdated: Date? = nil @Published private(set) var isRefreshing = false @Published var error: Error? = nil @@ -64,6 +70,8 @@ class PullRequestsViewModel: ObservableObject { private let setFocusTrigger = PassthroughSubject() private var pullRequestIndexMap: [String: Int] = [:] + private let maxCacheSize = 500 + private func updatePullRequestIndexMap() { pullRequestIndexMap = Dictionary( pullRequests.enumerated().map { index, pr in (pr.id, index) }, @@ -166,8 +174,8 @@ class PullRequestsViewModel: ObservableObject { if timer != nil { return } - timer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { _ in - self.triggerUpdatePullRequests() + timer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in + self?.triggerUpdatePullRequests() } } @@ -332,10 +340,20 @@ class PullRequestsViewModel: ObservableObject { let daysToDeduct = (ConfigService.deleteAfterWeeks * 7) + 1 let deleteFrom = Calendar.current.date(byAdding: .day, value: daysToDeduct * -1, to: Date())! - let filteredPullRequestMap = pullRequestMap.filter { _, pullRequest in + var filteredPullRequestMap = pullRequestMap.filter { _, pullRequest in pullRequest.lastUpdated > deleteFrom || (ConfigService.deleteOnlyClosed && !pullRequest.isClosed) } + // Enforce maximum cache size to prevent unbounded memory growth + if filteredPullRequestMap.count > maxCacheSize { + let sortedPRs = filteredPullRequestMap.values.sorted { $0.lastUpdated > $1.lastUpdated } + let keysToKeep = Set(sortedPRs.prefix(maxCacheSize).map { $0.id }) + filteredPullRequestMap = filteredPullRequestMap.filter { keysToKeep.contains($0.key) } + // swiftformat:disable redundantSelf + logger.info("Limiting cache to \(self.maxCacheSize) most recent pull requests") + // swiftformat:enable redundantSelf + } + let filteredPullRequestReadMap = pullRequestReadMap.filter { pullRequestId, _ in filteredPullRequestMap.index(forKey: pullRequestId) != nil } @@ -345,7 +363,7 @@ class PullRequestsViewModel: ObservableObject { logger.info("Removing \(self.pullRequestMap.count - filteredPullRequestMap.count) pull requests") logger.info("Removing \(self.pullRequestReadMap.count - filteredPullRequestReadMap.count) pull requests read info") // swiftformat:enable redundantSelf - await MainActor.run { + await MainActor.run { [filteredPullRequestMap, filteredPullRequestReadMap] in self.pullRequestMap = filteredPullRequestMap self.pullRequestReadMap = filteredPullRequestReadMap } diff --git a/PReek/Views/PullRequestViews/DisclosureGroupList/PullRequestHeaderView.swift b/PReek/Views/PullRequestViews/DisclosureGroupList/PullRequestHeaderView.swift index 4b21258..da76842 100644 --- a/PReek/Views/PullRequestViews/DisclosureGroupList/PullRequestHeaderView.swift +++ b/PReek/Views/PullRequestViews/DisclosureGroupList/PullRequestHeaderView.swift @@ -106,6 +106,7 @@ struct PullRequestHeaderView: View, Equatable { ResourceIcon(image: .check) .frame(width: 13) .foregroundColor(.success) + .padding(.top, 1) } .help(usersToString(pullRequest.approvalFrom)) } @@ -115,7 +116,7 @@ struct PullRequestHeaderView: View, Equatable { ResourceIcon(image: .fileDiff) .frame(width: 12) .foregroundColor(.failure) - .padding(.top, 1) + .padding(.top, 2) } .help(usersToString(pullRequest.changesRequestedFrom)) } diff --git a/PReek/Views/Settings/SettingsScreen.swift b/PReek/Views/Settings/SettingsScreen.swift index a78f971..332e168 100644 --- a/PReek/Views/Settings/SettingsScreen.swift +++ b/PReek/Views/Settings/SettingsScreen.swift @@ -5,6 +5,13 @@ import SwiftUI import CodeScanner #endif +func getAppVersion() -> String { + if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { + return appVersion + } + return "Unknown" +} + struct SettingsScreen: View { @ObservedObject var configViewModel: ConfigViewModel @@ -32,20 +39,21 @@ struct SettingsScreen: View { } } .importSheet(configViewModel: configViewModel, isPresented: $showImportSheet) - #endif - #if os(macOS) - .safeAreaInset(edge: .bottom, spacing: 0) { - HStack { - Button("Quit App", action: { NSApplication.shared.terminate(nil) }) - NavigationLink(value: Screen.share) { - Text("Share") + #elseif os(macOS) + .safeAreaInset(edge: .bottom, spacing: 0) { + HStack { + Button("Quit App", action: { NSApplication.shared.terminate(nil) }) + NavigationLink(value: Screen.share) { + Text("Share") + } + Spacer() + Text("Version: \(getAppVersion())") + .foregroundStyle(.secondary) } - Spacer() + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(.windowBackground) } - .padding(.horizontal, 20) - .padding(.vertical, 10) - .background(.windowBackground) - } #endif } diff --git a/build-dmg.sh b/build-dmg.sh index a486561..39d60f4 100755 --- a/build-dmg.sh +++ b/build-dmg.sh @@ -1,8 +1,5 @@ #!/bin/bash -# Exit on error -set -e - # Configuration SCHEME_NAME="PReek" PROJECT_PATH="PReek.xcodeproj"