Skip to content
Merged
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
14 changes: 12 additions & 2 deletions Packages/CrowCore/Sources/CrowCore/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,20 @@ public final class AppState {

public var excludeReviewRepos: [String] = []
public var excludeTicketRepos: [String] = []
public var ignoreReviewLabels: [String] = []

public var filteredReviewRequests: [ReviewRequest] {
guard !excludeReviewRepos.isEmpty else { return reviewRequests }
return reviewRequests.filter { !repoMatchesExcludePatterns($0.repo, patterns: excludeReviewRepos) }
var result = reviewRequests
if !excludeReviewRepos.isEmpty {
result = result.filter { !repoMatchesExcludePatterns($0.repo, patterns: excludeReviewRepos) }
}
if !ignoreReviewLabels.isEmpty {
let lowerLabels = Set(ignoreReviewLabels.map { $0.lowercased() })
result = result.filter { request in
!request.labels.contains(where: { lowerLabels.contains($0.lowercased()) })
}
}
return result
}

/// IDs of review requests the user has already seen (for badge count).
Expand Down
8 changes: 6 additions & 2 deletions Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ public struct ConfigDefaults: Codable, Sendable, Equatable {
public var excludeDirs: [String]
public var excludeReviewRepos: [String]
public var excludeTicketRepos: [String]
public var ignoreReviewLabels: [String]

/// Characters that are invalid in git ref names (see `git check-ref-format`).
private static let invalidBranchChars = CharacterSet(charactersIn: " ~^:?*[\\")
Expand All @@ -201,14 +202,16 @@ public struct ConfigDefaults: Codable, Sendable, Equatable {
branchPrefix: String = "feature/",
excludeDirs: [String] = ["node_modules", ".git", "vendor", "dist", "build", "target"],
excludeReviewRepos: [String] = [],
excludeTicketRepos: [String] = []
excludeTicketRepos: [String] = [],
ignoreReviewLabels: [String] = []
) {
self.provider = provider
self.cli = cli
self.branchPrefix = branchPrefix
self.excludeDirs = excludeDirs
self.excludeReviewRepos = excludeReviewRepos
self.excludeTicketRepos = excludeTicketRepos
self.ignoreReviewLabels = ignoreReviewLabels
}

public init(from decoder: Decoder) throws {
Expand All @@ -219,10 +222,11 @@ public struct ConfigDefaults: Codable, Sendable, Equatable {
excludeDirs = try container.decodeIfPresent([String].self, forKey: .excludeDirs) ?? ["node_modules", ".git", "vendor", "dist", "build", "target"]
excludeReviewRepos = try container.decodeIfPresent([String].self, forKey: .excludeReviewRepos) ?? []
excludeTicketRepos = try container.decodeIfPresent([String].self, forKey: .excludeTicketRepos) ?? []
ignoreReviewLabels = try container.decodeIfPresent([String].self, forKey: .ignoreReviewLabels) ?? []
}

private enum CodingKeys: String, CodingKey {
case provider, cli, branchPrefix, excludeDirs, excludeReviewRepos, excludeTicketRepos
case provider, cli, branchPrefix, excludeDirs, excludeReviewRepos, excludeTicketRepos, ignoreReviewLabels
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public struct ReviewRequest: Identifiable, Codable, Sendable {
public var baseBranch: String
public var isDraft: Bool
public var requestedAt: Date?
public var labels: [String]
public var provider: Provider
public var reviewSessionID: UUID? // set if a review session already exists

Expand All @@ -26,6 +27,7 @@ public struct ReviewRequest: Identifiable, Codable, Sendable {
baseBranch: String,
isDraft: Bool = false,
requestedAt: Date? = nil,
labels: [String] = [],
provider: Provider = .github,
reviewSessionID: UUID? = nil
) {
Expand All @@ -39,6 +41,7 @@ public struct ReviewRequest: Identifiable, Codable, Sendable {
self.baseBranch = baseBranch
self.isDraft = isDraft
self.requestedAt = requestedAt
self.labels = labels
self.provider = provider
self.reviewSessionID = reviewSessionID
}
Expand Down
17 changes: 17 additions & 0 deletions Packages/CrowCore/Tests/CrowCoreTests/AppConfigTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,20 @@ import Testing
let decoded2 = try JSONDecoder().decode(AppConfig.self, from: data2)
#expect(decoded2.experimentalTmuxBackend == false)
}

@Test func ignoreReviewLabelsRoundTrip() throws {
let config = AppConfig(
defaults: ConfigDefaults(ignoreReviewLabels: ["dependencies", "renovate", "automated"])
)
let data = try JSONEncoder().encode(config)
let decoded = try JSONDecoder().decode(AppConfig.self, from: data)
#expect(decoded.defaults.ignoreReviewLabels == ["dependencies", "renovate", "automated"])
}

@Test func ignoreReviewLabelsDefaultsEmptyWhenKeyMissing() throws {
let json = """
{"defaults": {"provider": "github", "cli": "gh", "branchPrefix": "feature/", "excludeDirs": []}}
""".data(using: .utf8)!
let config = try JSONDecoder().decode(AppConfig.self, from: json)
#expect(config.defaults.ignoreReviewLabels.isEmpty)
}
15 changes: 15 additions & 0 deletions Packages/CrowUI/Sources/CrowUI/AutomationSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public struct AutomationSettingsView: View {
var onSave: (() -> Void)?

@State private var excludeReviewReposText: String
@State private var ignoreReviewLabelsText: String
@State private var excludeTicketReposText: String

public init(
Expand All @@ -27,6 +28,7 @@ public struct AutomationSettingsView: View {
self._autoRespond = autoRespond
self.onSave = onSave
self._excludeReviewReposText = State(initialValue: defaults.wrappedValue.excludeReviewRepos.joined(separator: ", "))
self._ignoreReviewLabelsText = State(initialValue: defaults.wrappedValue.ignoreReviewLabels.joined(separator: ", "))
self._excludeTicketReposText = State(initialValue: defaults.wrappedValue.excludeTicketRepos.joined(separator: ", "))
}

Expand All @@ -48,6 +50,19 @@ public struct AutomationSettingsView: View {
Text("Per-workspace auto-review opt-ins are configured in Workspaces → edit a workspace.")
.font(.caption)
.foregroundStyle(.secondary)

TextField("Ignored Labels", text: $ignoreReviewLabelsText)
.textFieldStyle(.roundedBorder)
.onChange(of: ignoreReviewLabelsText) { _, _ in
defaults.ignoreReviewLabels = ignoreReviewLabelsText
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
onSave?()
}
Text("Comma-separated labels to ignore from the review board (e.g., dependencies, renovate, automated).")
.font(.caption)
.foregroundStyle(.secondary)
}

Section("Tickets") {
Expand Down
2 changes: 2 additions & 0 deletions Sources/Crow/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
appState.managerAutoPermissionMode = config.managerAutoPermissionMode
appState.excludeReviewRepos = config.defaults.excludeReviewRepos
appState.excludeTicketRepos = config.defaults.excludeTicketRepos
appState.ignoreReviewLabels = config.defaults.ignoreReviewLabels

// Create session service and hydrate state
let service = SessionService(store: store, appState: appState, telemetryPort: config.telemetry.enabled ? config.telemetry.port : nil)
Expand Down Expand Up @@ -680,6 +681,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
appState.managerAutoPermissionMode = config.managerAutoPermissionMode
appState.excludeReviewRepos = config.defaults.excludeReviewRepos
appState.excludeTicketRepos = config.defaults.excludeTicketRepos
appState.ignoreReviewLabels = config.defaults.ignoreReviewLabels
}

// MARK: - Socket Server
Expand Down
11 changes: 11 additions & 0 deletions Sources/Crow/App/IssueTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,13 @@ final class IssueTracker {
if !reviewExcludePatterns.isEmpty {
reviews = reviews.filter { !repoMatchesExcludePatterns($0.repo, patterns: reviewExcludePatterns) }
}
let ignoreLabels = config.defaults.ignoreReviewLabels
if !ignoreLabels.isEmpty {
let lowerLabels = Set(ignoreLabels.map { $0.lowercased() })
reviews = reviews.filter { request in
!request.labels.contains(where: { lowerLabels.contains($0.lowercased()) })
}
}
let currentIDs = Set(reviews.map(\.id))
let newIDs = currentIDs.subtracting(previousReviewRequestIDs)
previousReviewRequestIDs = allCurrentIDs
Expand Down Expand Up @@ -562,6 +569,7 @@ final class IssueTracker {
number title url isDraft updatedAt headRefName baseRefName state
author { login }
repository { nameWithOwner }
labels(first: 20) { nodes { name } }
}
}
}
Expand Down Expand Up @@ -1095,6 +1103,8 @@ final class IssueTracker {
let headBranch = node["headRefName"] as? String ?? ""
let baseBranch = node["baseRefName"] as? String ?? ""
let updatedAt = (node["updatedAt"] as? String).flatMap { dateFormatter.date(from: $0) }
let labels = ((node["labels"] as? [String: Any])?["nodes"] as? [[String: Any]])?
.compactMap { $0["name"] as? String } ?? []

requests.append(ReviewRequest(
id: "github:\(repoName)#\(number)",
Expand All @@ -1107,6 +1117,7 @@ final class IssueTracker {
baseBranch: baseBranch,
isDraft: isDraft,
requestedAt: updatedAt,
labels: labels,
provider: .github
))
}
Expand Down
Loading