use serde::{Deserialize, Serialize};
use crate::code_audit::{AuditFinding, FindingConfidence};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IssueGroup {
pub command: String,
pub component_id: String,
pub category: String,
pub count: usize,
#[serde(default)]
pub label: String,
#[serde(default)]
pub body: String,
#[serde(default)]
pub confidence: Option<FindingConfidence>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackedIssue {
pub number: u64,
pub title: String,
#[serde(default)]
pub body: String,
pub url: String,
pub state: TrackedIssueState,
pub labels: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrackedIssueState {
Open,
ClosedCompleted,
ClosedNotPlanned,
}
impl TrackedIssueState {
pub fn is_open(self) -> bool {
matches!(self, TrackedIssueState::Open)
}
pub fn is_closed(self) -> bool {
!self.is_open()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReconcileConfig {
#[serde(default)]
pub suppressed_categories: Vec<String>,
#[serde(default)]
pub suppression_labels: Vec<String>,
#[serde(default = "default_review_only_categories")]
pub review_only_categories: Vec<String>,
#[serde(default = "default_refresh_closed")]
pub refresh_closed_not_planned: bool,
}
impl Default for ReconcileConfig {
fn default() -> Self {
Self {
suppressed_categories: Vec::new(),
suppression_labels: Vec::new(),
review_only_categories: default_review_only_categories(),
refresh_closed_not_planned: default_refresh_closed(),
}
}
}
fn default_refresh_closed() -> bool {
true
}
pub fn default_review_only_categories() -> Vec<String> {
AuditFinding::all_names()
.iter()
.copied()
.filter(|name| {
let Ok(finding) = name.parse::<AuditFinding>() else {
return false;
};
finding.confidence() == FindingConfidence::Heuristic
|| matches!(finding, AuditFinding::UnusedParameter)
})
.map(String::from)
.collect()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ReconcileAction {
FileNew {
command: String,
component_id: String,
category: String,
title: String,
body: String,
labels: Vec<String>,
count: usize,
},
Update {
number: u64,
title: String,
body: String,
category: String,
count: usize,
},
UpdateClosed {
number: u64,
body: String,
category: String,
count: usize,
},
Close {
number: u64,
category: String,
comment: String,
},
CloseDuplicate {
number: u64,
keep: u64,
category: String,
comment: String,
},
Skip {
category: String,
component_id: String,
reason: ReconcileSkipReason,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReconcileSkipReason {
SuppressedByConfig,
SuppressedByLabel,
ClosedNotPlannedNoRefresh,
NoFindingsNoIssue,
ReviewOnlyCategory,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReconcilePlan {
pub actions: Vec<ReconcileAction>,
}
impl ReconcilePlan {
pub fn counts(&self) -> ReconcilePlanCounts {
let mut c = ReconcilePlanCounts::default();
for action in &self.actions {
match action {
ReconcileAction::FileNew { .. } => c.file_new += 1,
ReconcileAction::Update { .. } => c.update += 1,
ReconcileAction::UpdateClosed { .. } => c.update_closed += 1,
ReconcileAction::Close { .. } => c.close += 1,
ReconcileAction::CloseDuplicate { .. } => c.close_duplicate += 1,
ReconcileAction::Skip { .. } => c.skip += 1,
}
}
c
}
pub fn is_noop(&self) -> bool {
self.actions
.iter()
.all(|a| matches!(a, ReconcileAction::Skip { .. }))
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ReconcilePlanCounts {
pub file_new: usize,
pub update: usize,
pub update_closed: usize,
pub close: usize,
pub close_duplicate: usize,
pub skip: usize,
}