| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use anyhow::Result; |
| use once_cell::sync::OnceCell; |
| use serde::{Deserialize, Serialize}; |
| |
| use std::collections::HashMap; |
| use std::fmt::Write; |
| |
| use crate::{ |
| api::{Api, ComponentId, CreateIssue, IssueId, Status, UpdateIssue}, |
| lint::{Lint, LintFile}, |
| owners::FileOwnership, |
| }; |
| |
| struct ComponentDefs { |
| defs: HashMap<String, ComponentId>, |
| } |
| |
| impl ComponentDefs { |
| fn load(api: &mut (impl Api + ?Sized)) -> Result<Self> { |
| let mut defs = HashMap::new(); |
| for component in api.list_components()? { |
| let path = component |
| .path |
| .iter() |
| .map(|x| x.replace(' ', "")) |
| .reduce(|acc, x| format!("{acc}>{x}")) |
| .unwrap_or_default(); |
| defs.insert(path, component.id); |
| } |
| Ok(Self { defs }) |
| } |
| |
| fn get(&self, component: &str) -> Option<ComponentId> { |
| self.defs.get(component).copied() |
| } |
| } |
| |
| pub struct IssueTemplate<'a> { |
| // Settings |
| filter: &'a [String], |
| codesearch_tag: Option<&'a str>, |
| template: Option<String>, |
| blocking_issue: Option<&'a str>, |
| max_cc_users: usize, |
| |
| // Cache |
| component_defs: OnceCell<ComponentDefs>, |
| } |
| |
| impl<'a> IssueTemplate<'a> { |
| pub fn new( |
| filter: &'a [String], |
| codesearch_tag: Option<&'a str>, |
| template: Option<String>, |
| blocking_issue: Option<&'a str>, |
| max_cc_users: usize, |
| ) -> Self { |
| Self { |
| filter, |
| codesearch_tag, |
| template, |
| blocking_issue, |
| max_cc_users, |
| |
| component_defs: OnceCell::new(), |
| } |
| } |
| |
| fn format_lints(&self, mut description: String, file: &LintFile) -> String { |
| let mut insertions = |
| file.lints.iter().map(|l| self.annotation_msg(l, &file.path)).collect::<Vec<_>>(); |
| insertions.sort(); |
| |
| write!( |
| &mut description, |
| "\n[{}]({})\n{}\n", |
| file.path, |
| self.codesearch_url(&file.path, None), |
| insertions.join("\n"), |
| ) |
| .unwrap(); |
| |
| description |
| } |
| |
| fn annotation_msg(&self, l: &Lint, path: &str) -> String { |
| let lints_url = "https://rust-lang.github.io/rust-clippy/master#".to_owned(); |
| let cs_url = self.codesearch_url(path, Some(l.span.start.line)); |
| |
| // format both clippy and normal rustc lints in markdown |
| if let Some(name) = l.name.strip_prefix("clippy::") { |
| format!( |
| "- [{name}]({}) on [line {}]({})", |
| &(lints_url + name), |
| l.span.start.line, |
| cs_url |
| ) |
| } else { |
| format!("- {} on [line {}]({})", l.name, l.span.start.line, cs_url) |
| } |
| } |
| |
| fn codesearch_url(&self, path: &str, line: Option<usize>) -> String { |
| let mut link = format!( |
| "https://cs.opensource.google/fuchsia/fuchsia/+/{}:{}", |
| self.codesearch_tag.unwrap_or("main"), |
| path |
| ); |
| if let Some(line) = line { |
| link.push_str(&format!(";l={}", line)) |
| } |
| link |
| } |
| |
| fn get_component_defs(&self, api: &mut (impl Api + ?Sized)) -> Result<&ComponentDefs> { |
| self.component_defs.get_or_try_init(|| ComponentDefs::load(api)) |
| } |
| |
| pub fn create( |
| &self, |
| api: &mut (impl Api + ?Sized), |
| ownership: &FileOwnership, |
| files: &[&LintFile], |
| holding_component_name: &str, |
| ) -> Result<Issue> { |
| let component_defs = self.get_component_defs(api)?; |
| |
| let mut title = "Please inspect ".to_string(); |
| match self.filter { |
| [a] => write!(title, "these {}", a)?, |
| [a, b] => write!(title, "these {} and {}", a, b)?, |
| _ => write!(title, "multiple new")?, |
| } |
| write!(title, " lints")?; |
| |
| let mut component = None; |
| let mut owner = None; |
| let mut cc_users = None; |
| let mut blocking_issues = Vec::new(); |
| |
| if let Some(component_name) = &ownership.component { |
| write!(title, " in {}", component_name)?; |
| if let Some(c) = component_defs.get(&component_name) { |
| component = Some(c); |
| } else { |
| eprintln!( |
| "could not find component '{component_name}' in components map, skipping" |
| ); |
| } |
| } |
| |
| if !ownership.owners.is_empty() { |
| // A small price to pay for salvation |
| owner = Some(ownership.owners[0].clone()); |
| |
| if ownership.component.is_none() && ownership.owners.len() > 1 { |
| cc_users = Some( |
| ownership |
| .owners |
| .iter() |
| // skip the owner |
| .skip(1) |
| .take(self.max_cc_users) |
| .cloned() |
| .collect(), |
| ); |
| } |
| } |
| |
| let details = files.iter().fold(String::new(), |d, f| self.format_lints(d, f)); |
| let description = if let Some(ref template) = self.template { |
| template.replace("INSERT_DETAILS_HERE", &details) |
| } else { |
| details |
| }; |
| |
| if let Some(i) = self.blocking_issue { |
| blocking_issues.push(IssueId::new(i.parse()?)); |
| } |
| |
| let holding_component = component_defs |
| .get(holding_component_name) |
| .expect("couldn't find holding component definition"); |
| let request = CreateIssue { |
| title, |
| description, |
| status: Status::New, |
| component: holding_component, |
| blocking_issues, |
| }; |
| |
| let id = api.create_issue(request)?; |
| |
| Ok(Issue { id, component, owner, cc_users }) |
| } |
| } |
| |
| #[derive(Deserialize, Serialize)] |
| pub struct Issue { |
| pub id: IssueId, |
| pub component: Option<ComponentId>, |
| pub owner: Option<String>, |
| pub cc_users: Option<Vec<String>>, |
| } |
| |
| impl Issue { |
| pub fn rollout( |
| mut issues: Vec<Self>, |
| api: &mut (impl Api + ?Sized), |
| verbose: bool, |
| ) -> Result<()> { |
| let issues_len = issues.len(); |
| for (i, issue) in issues.drain(..).enumerate() { |
| if verbose { |
| println!("[{}/{issues_len}] Rolling out https://fxbug.dev/{}", i + 1, issue.id); |
| } |
| |
| api.update_issue(UpdateIssue { |
| id: issue.id, |
| status: Some(Status::Assigned), |
| owner: issue.owner, |
| cc_users: issue.cc_users, |
| component: issue.component, |
| })?; |
| } |
| |
| Ok(()) |
| } |
| } |