blob: 4cafa8c7e4ebe951fc4196223e9610fde9a61888 [file] [log] [blame]
// 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::{
lint::{Lint, LintFile},
monorail::{schema, Monorail},
owners::FileOwnership,
};
struct ComponentDefs {
defs: HashMap<String, u64>,
}
impl ComponentDefs {
fn load(monorail: &mut (impl Monorail + ?Sized)) -> Result<Self> {
let mut defs = HashMap::new();
let mut page_token = None;
loop {
let response = monorail.list_component_defs(schema::ListComponentDefsRequest {
parent: "projects/fuchsia".to_string(),
page_size: 100,
page_token,
})?;
for component_def in response.component_defs {
let id = component_def
.name
.strip_prefix("projects/fuchsia/componentDefs/")
.unwrap()
.parse()
.unwrap();
defs.insert(component_def.value, id);
}
page_token = response.next_page_token;
if page_token.is_none() {
break;
}
}
Ok(Self { defs })
}
fn get(&self, component: &str) -> Option<String> {
self.defs.get(component).map(|id| format!("projects/fuchsia/componentDefs/{id}"))
}
}
pub struct IssueTemplate<'a> {
// Settings
filter: &'a [String],
codesearch_tag: Option<&'a str>,
template: Option<String>,
blocking_issue: Option<&'a str>,
labels: &'a [String],
max_cc_users: usize,
// Cache
component_defs: OnceCell<ComponentDefs>,
}
const HOLDING_COMPONENT: &'static str = "Rust>tools>Shush>Rollout";
impl<'a> IssueTemplate<'a> {
pub fn new(
filter: &'a [String],
codesearch_tag: Option<&'a str>,
template: Option<String>,
blocking_issue: Option<&'a str>,
labels: &'a [String],
max_cc_users: usize,
) -> Self {
Self {
filter,
codesearch_tag,
template,
blocking_issue,
labels,
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,
monorail: &mut (impl Monorail + ?Sized),
) -> Result<&ComponentDefs> {
self.component_defs.get_or_try_init(|| ComponentDefs::load(monorail))
}
pub fn create(
&self,
monorail: &mut (impl Monorail + ?Sized),
ownership: &FileOwnership,
files: &[LintFile],
) -> Result<Issue> {
let component_defs = self.get_component_defs(monorail)?;
let mut summary = "Please inspect ".to_string();
match self.filter {
[a] => write!(summary, "these {}", a)?,
[a, b] => write!(summary, "these {} and {}", a, b)?,
_ => write!(summary, "multiple new")?,
}
write!(summary, " lints")?;
let mut components = None;
let mut owner = None;
let mut cc_users = None;
if let Some(component) = &ownership.component {
write!(summary, " in {}", component)?;
if let Some(component) = component_defs.get(component) {
components = Some(vec![schema::ComponentValue::from(component)]);
} else {
eprintln!("could not find component '{component}' in components map, skipping");
}
}
if !ownership.owners.is_empty() {
// A small price to pay for salvation
owner =
Some(schema::UserValue::from(format!("users/{}@google.com", ownership.owners[0])));
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)
.map(|owner| {
schema::UserValue::from(format!("users/{}@google.com", owner.clone()))
})
.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
};
let blocking_issue_refs = self.blocking_issue.map(|issue| {
vec![schema::IssueRef::from(format!("projects/fuchsia/issues/{}", issue))]
});
// Ensure RVG label present.
let mut label_vec = self.labels.to_vec();
let rvg_label = "Restrict-View-Google".to_string();
if !label_vec.contains(&rvg_label) {
label_vec.push(rvg_label);
}
let labels =
Some(label_vec.iter().cloned().map(schema::LabelValue::from).collect::<Vec<_>>());
let holding_component = component_defs.get(HOLDING_COMPONENT).expect("couldn't find holding component definition, you may need to rerun with --prpc to fetch component definitions");
let request = schema::MakeIssueRequest {
parent: "projects/fuchsia",
issue: schema::Issue {
name: None,
summary: Some(summary),
status: Some(schema::StatusValue::from("NeedsInfo".to_string())),
owner: None,
cc_users: None,
labels,
components: Some(vec![schema::ComponentValue::from(holding_component)]),
field_values: None,
merged_into_issue_ref: None,
blocked_on_issue_refs: None,
blocking_issue_refs,
},
description,
notify_type: schema::NotifyType::NotifyTypeUnspecified,
};
let issue = monorail.create_issue(request)?;
// The returned issue name is of the form "fuchsia/issues/{number}".
let id = issue.name.unwrap().split('/').last().unwrap().parse()?;
Ok(Issue { id, components, owner, cc_users })
}
}
#[derive(Deserialize, Serialize)]
pub struct Issue {
id: usize,
components: Option<Vec<schema::ComponentValue>>,
owner: Option<schema::UserValue>,
cc_users: Option<Vec<schema::UserValue>>,
}
impl Issue {
fn update_from_deltas(
monorail: &mut (impl Monorail + ?Sized),
deltas: Vec<schema::IssueDelta>,
) -> Result<()> {
monorail.update_issues(schema::ModifyIssuesRequest {
deltas: Some(deltas),
notify_type: schema::NotifyType::Email,
comment_content: None,
})
}
pub fn id(&self) -> usize {
self.id
}
pub fn rollout(
mut issues: Vec<Self>,
monorail: &mut (impl Monorail + ?Sized),
verbose: bool,
) -> Result<()> {
let component_defs = ComponentDefs::load(monorail)?;
// Monorail modification requests are batched up in groups of up to 100 modifications.
let mut deltas = Vec::new();
let issues_len = issues.len();
for (i, issue) in issues.drain(..).enumerate() {
if verbose {
println!("[{i}/{issues_len}] Rolling out fxbug.dev/{}", issue.id());
}
let mut update_mask = "status,components".to_string();
if issue.owner.is_some() {
update_mask += ",owner";
}
if issue.cc_users.is_some() {
update_mask += ",cc_users";
}
deltas.push(schema::IssueDelta {
issue: schema::Issue {
name: Some(format!("projects/fuchsia/issues/{}", issue.id())),
summary: None,
status: Some(schema::StatusValue::from("Available".to_string())),
owner: issue.owner,
cc_users: issue.cc_users,
labels: None,
components: issue.components,
field_values: None,
merged_into_issue_ref: None,
blocked_on_issue_refs: None,
blocking_issue_refs: None,
},
update_mask,
components_remove: Some(vec![component_defs.get(HOLDING_COMPONENT).unwrap()]),
});
if deltas.len() >= 100 {
let deltas = ::core::mem::replace(&mut deltas, Vec::new());
Self::update_from_deltas(monorail, deltas)?;
}
}
if !deltas.is_empty() {
Self::update_from_deltas(monorail, deltas)?;
}
Ok(())
}
}