use std::string::ToString;
use std::sync::Arc;
use bugzilla_query::Bug;
use color_eyre::eyre::{bail, eyre, Result, WrapErr};
use jira_query::Issue;
use crate::config::{tracker, KeyOrSearch, TicketQuery};
use crate::references::{ReferenceQueries, ReferenceSignatures};
use crate::ticket_abstraction::{AbstractTicket, IntoAbstract};
const JIRA_CHUNK_SIZE: u32 = 30;
const BZ_INCLUDED_FIELDS: &[&str; 3] = &["_default", "pool", "flags"];
const BZ_API_KEY_VAR: &str = "BZ_API_KEY";
const JIRA_API_KEY_VAR: &str = "JIRA_API_KEY";
const JIRA_USER_EMAIL_VAR: &str = "JIRA_USER_EMAIL";
const JIRA_ATLASSIAN_CLOUD_VAR: &str = "JIRA_ATLASSIAN_CLOUD";
#[derive(Clone)]
pub struct AnnotatedTicket {
pub ticket: AbstractTicket,
pub query: Arc<TicketQuery>,
}
impl AnnotatedTicket {
pub fn override_fields(&mut self) {
if let Some(overrides) = &self.query.overrides {
if let Some(doc_type) = &overrides.doc_type {
self.ticket.doc_type = doc_type.clone();
}
if let Some(components) = &overrides.components {
self.ticket.components = components.clone();
}
if let Some(subsystems) = &overrides.subsystems {
self.ticket.subsystems = Ok(subsystems.clone());
}
}
}
}
fn bz_instance(trackers: &tracker::Config) -> Result<bugzilla_query::BzInstance> {
let api_key = if let Some(key) = &trackers.bugzilla.api_key {
key.clone()
} else {
std::env::var(BZ_API_KEY_VAR)
.wrap_err_with(|| format!("Set the {BZ_API_KEY_VAR} environment variable."))?
};
Ok(
bugzilla_query::BzInstance::at(trackers.bugzilla.host.clone())?
.authenticate(bugzilla_query::Auth::ApiKey(api_key))
.paginate(bugzilla_query::Pagination::Unlimited)
.include_fields(BZ_INCLUDED_FIELDS.iter().map(ToString::to_string).collect()),
)
}
fn jira_instance(trackers: &tracker::Config) -> Result<jira_query::JiraInstance> {
let api_key = if let Some(key) = &trackers.jira.api_key {
key.clone()
} else {
std::env::var(JIRA_API_KEY_VAR)
.wrap_err_with(|| format!("Set the {JIRA_API_KEY_VAR} environment variable."))?
};
let is_cloud = std::env::var(JIRA_ATLASSIAN_CLOUD_VAR).unwrap_or_default() == "true";
let mut builder = jira_query::JiraInstance::at(trackers.jira.host.clone())?;
let auth = if is_cloud {
builder = builder.for_cloud();
log::info!("Configuring for Atlassian Cloud");
let user_email = std::env::var(JIRA_USER_EMAIL_VAR).wrap_err_with(|| {
format!(
"Set the {JIRA_USER_EMAIL_VAR} environment variable for Atlassian Cloud."
)
})?;
jira_query::Auth::Basic {
user: user_email,
password: api_key,
}
} else {
log::info!("Configuring for local Jira Server.");
jira_query::Auth::ApiKey(api_key)
};
Ok(builder
.authenticate(auth)
.paginate(jira_query::Pagination::ChunkSize(JIRA_CHUNK_SIZE)))
}
#[tokio::main]
pub async fn unsorted_tickets(
queries: &[Arc<TicketQuery>],
trackers: &tracker::Config,
) -> Result<Vec<AnnotatedTicket>> {
if queries.is_empty() {
bail!("No tickets are configured in this project.");
}
let queries: Vec<Arc<TicketQuery>> = queries.iter().map(Arc::clone).collect();
let ref_queries = ReferenceQueries::from(queries.as_slice());
let plain_bugs = bugs(QueriesKind::Plain(&queries), trackers);
let plain_issues = issues(QueriesKind::Plain(&queries), trackers);
let ref_bugs = bugs(QueriesKind::Ref(&ref_queries), trackers);
let ref_issues = issues(QueriesKind::Ref(&ref_queries), trackers);
let (plain_bugs, plain_issues, ref_bugs, ref_issues) =
tokio::try_join!(plain_bugs, plain_issues, ref_bugs, ref_issues)?;
let ref_signatures = ReferenceSignatures::new(ref_bugs, ref_issues, trackers)?;
let mut annotated_tickets = Vec::new();
annotated_tickets.append(&mut into_annotated_tickets(
plain_bugs,
trackers,
&ref_signatures,
)?);
annotated_tickets.append(&mut into_annotated_tickets(
plain_issues,
trackers,
&ref_signatures,
)?);
for annotated_ticket in &mut annotated_tickets {
annotated_ticket.override_fields();
}
Ok(annotated_tickets)
}
fn into_annotated_tickets(
issues: Vec<(Arc<TicketQuery>, impl IntoAbstract)>,
config: &tracker::Config,
ref_signatures: &ReferenceSignatures,
) -> Result<Vec<AnnotatedTicket>> {
let mut results = Vec::new();
for (query, issue) in issues {
let attached_references = ref_signatures.reattach_to(&query);
let ticket = issue.into_abstract(Some(attached_references), config)?;
let annotated = AnnotatedTicket { ticket, query };
results.push(annotated);
}
Ok(results)
}
fn take_id_queries(queries: &[Arc<TicketQuery>]) -> Vec<(&str, Arc<TicketQuery>)> {
queries
.iter()
.filter_map(|tq| {
if let KeyOrSearch::Key(key) = &tq.using {
Some((key.as_str(), Arc::clone(tq)))
} else {
None
}
})
.collect()
}
fn take_search_queries(queries: &[Arc<TicketQuery>]) -> Vec<(&str, Arc<TicketQuery>)> {
queries
.iter()
.filter_map(|tq| {
if let KeyOrSearch::Search(search) = &tq.using {
Some((search.as_str(), Arc::clone(tq)))
} else {
None
}
})
.collect()
}
enum QueriesKind<'a> {
Plain(&'a [Arc<TicketQuery>]),
Ref(&'a ReferenceQueries),
}
impl QueriesKind<'_> {
pub fn label(&self) -> &'static str {
match self {
Self::Plain(_) => "tickets",
Self::Ref(_) => "references",
}
}
pub fn list(&self) -> &[Arc<TicketQuery>] {
match self {
Self::Plain(qs) => qs,
Self::Ref(rqs) => &rqs.0,
}
}
}
async fn bugs(
queriesk: QueriesKind<'_>,
trackers: &tracker::Config,
) -> Result<Vec<(Arc<TicketQuery>, Bug)>> {
let queries = queriesk.list();
let bugzilla_queries: Vec<Arc<TicketQuery>> = queries
.iter()
.filter(|tq| tq.tracker == tracker::Service::Bugzilla)
.map(Arc::clone)
.collect();
if bugzilla_queries.is_empty() {
return Ok(Vec::new());
}
let queries_by_id = take_id_queries(&bugzilla_queries);
let queries_by_search = take_search_queries(&bugzilla_queries);
log::info!("Downloading {} from Bugzilla.", queriesk.label());
let bz_instance = bz_instance(trackers)?;
let mut all_bugs = Vec::new();
let bugs_from_ids = bugs_from_ids(&queries_by_id, &bz_instance);
let bugs_from_searches = bugs_from_searches(&queries_by_search, &bz_instance);
let (mut bugs_from_ids, mut bugs_from_searches) =
tokio::try_join!(bugs_from_ids, bugs_from_searches)?;
all_bugs.append(&mut bugs_from_ids);
all_bugs.append(&mut bugs_from_searches);
log::info!("Finished downloading {} from Bugzilla.", queriesk.label());
Ok(all_bugs)
}
async fn bugs_from_ids(
queries: &[(&str, Arc<TicketQuery>)],
bz_instance: &bugzilla_query::BzInstance,
) -> Result<Vec<(Arc<TicketQuery>, Bug)>> {
let bugs = bz_instance
.bugs(
&queries
.iter()
.map(|(key, _query)| *key)
.collect::<Vec<&str>>(),
)
.await
.wrap_err("Failed to download tickets from Bugzilla.")?;
let mut annotated_bugs: Vec<(Arc<TicketQuery>, Bug)> = Vec::new();
for bug in bugs {
let matching_query = queries
.iter()
.find(|(key, _query)| key == &bug.id.to_string().as_str())
.map(|(_key, query)| Arc::clone(query))
.ok_or_else(|| eyre!("Bug {} doesn't match any configured query.", bug.id))?;
annotated_bugs.push((matching_query, bug));
}
Ok(annotated_bugs)
}
async fn bugs_from_searches(
queries: &[(&str, Arc<TicketQuery>)],
bz_instance: &bugzilla_query::BzInstance,
) -> Result<Vec<(Arc<TicketQuery>, Bug)>> {
let mut annotated_bugs: Vec<(Arc<TicketQuery>, Bug)> = Vec::new();
for (search, query) in queries {
let mut bugs = bz_instance
.search(search)
.await
.wrap_err("Failed to download tickets from Bugzilla.")?
.into_iter()
.map(|bug| (Arc::clone(query), bug))
.collect();
annotated_bugs.append(&mut bugs);
}
Ok(annotated_bugs)
}
async fn issues(
queriesk: QueriesKind<'_>,
trackers: &tracker::Config,
) -> Result<Vec<(Arc<TicketQuery>, Issue)>> {
let queries = queriesk.list();
let jira_queries: Vec<Arc<TicketQuery>> = queries
.iter()
.filter(|&t| t.tracker == tracker::Service::Jira)
.map(Arc::clone)
.collect();
if jira_queries.is_empty() {
return Ok(Vec::new());
}
let queries_by_id = take_id_queries(&jira_queries);
let queries_by_search = take_search_queries(&jira_queries);
log::info!("Downloading {} from Jira.", queriesk.label());
let jira_instance = jira_instance(trackers)?;
let mut all_issues = Vec::new();
let jira_host = &trackers.jira.host;
let issues_from_ids = issues_from_ids(&queries_by_id, &jira_instance, jira_host);
let issues_from_searches = issues_from_searches(&queries_by_search, &jira_instance);
let (mut issues_from_ids, mut issues_from_searches) =
tokio::try_join!(issues_from_ids, issues_from_searches)?;
all_issues.append(&mut issues_from_ids);
all_issues.append(&mut issues_from_searches);
log::info!("Finished downloading {} from Jira.", queriesk.label());
Ok(all_issues)
}
async fn issues_from_ids(
queries: &[(&str, Arc<TicketQuery>)],
jira_instance: &jira_query::JiraInstance,
jira_host: &str,
) -> Result<Vec<(Arc<TicketQuery>, Issue)>> {
let issue_keys: Vec<&str> = queries.iter().map(|(key, _query)| *key).collect();
log::info!("Jira query by IDs: {:?}", issue_keys);
let issues = jira_instance
.issues(&issue_keys)
.await
.wrap_err("Failed to download tickets from Jira.")?;
let mut annotated_issues: Vec<(Arc<TicketQuery>, Issue)> = Vec::new();
for issue in issues {
let matching_query = queries
.iter()
.find(|(key, _query)| key == &issue.key.as_str())
.map(|(_key, query)| Arc::clone(query));
if let Some(query) = matching_query {
annotated_issues.push((query, issue));
} else {
let ticket_url = format!("{}/browse/{}", jira_host.trim_end_matches('/'), issue.key);
bail!(
"Ticket ID mismatch: Jira returned '{}' ({}) which doesn't match any configured query. \
This ticket was likely moved from another project. Check the logs above to see which \
ticket IDs were requested, then update your tickets.yaml with the new ID.",
issue.key,
ticket_url
);
}
}
Ok(annotated_issues)
}
async fn issues_from_searches(
queries: &[(&str, Arc<TicketQuery>)],
jira_instance: &jira_query::JiraInstance,
) -> Result<Vec<(Arc<TicketQuery>, Issue)>> {
let mut annotated_issues: Vec<(Arc<TicketQuery>, Issue)> = Vec::new();
for (search, query) in queries {
let mut issues = jira_instance
.search(search)
.await
.wrap_err("Failed to download tickets from Jira.")?
.into_iter()
.map(|issue| (Arc::clone(query), issue))
.collect();
annotated_issues.append(&mut issues);
}
Ok(annotated_issues)
}