diff --git a/src/extra_fields.rs b/src/extra_fields.rs index da33209..7525539 100644 --- a/src/extra_fields.rs +++ b/src/extra_fields.rs @@ -158,7 +158,7 @@ struct BzTeam { /// from a custom Bugzilla or Jira field. /// /// Returns an error is the field is missing or if it is not a string. -fn extract_field(field_name: Field, extra: &Value, fields: &[String], id: Id) -> Result { +fn extract_field(field_name: Field, extra: &Value, fields: &[String], id: Id, tracker: &impl tracker::FieldsConfig) -> Result { // Record all errors that occur with tried fields that exist. let mut errors = Vec::new(); // Record all empty but potentially okay fields. @@ -193,7 +193,7 @@ fn extract_field(field_name: Field, extra: &Value, fields: &[String], id: Id) -> // If all we've got are errors, return an error with the complete errors report. if empty_fields.is_empty() { - let report = error_chain(errors, field_name, fields, id); + let report = error_chain(errors, field_name, fields, id, tracker); Err(report) // If we at least got an existing but empty field, return an empty string. // I think it's safe to treat it as such. @@ -219,13 +219,26 @@ impl fmt::Display for Id<'_> { } } +impl Id<'_> { + /// Construct a URL to the ticket. + fn url(&self, tracker: &impl tracker::FieldsConfig) -> String { + match self { + Self::BZ(id) => format!("{}/show_bug.cgi?id={}", tracker.host(), id), + Self::Jira(key) => format!("{}/browse/{}", tracker.host(), key), + } + } +} + /// Prepare a user-readable list of errors, reported in the order that they occurred. -fn error_chain(mut errors: Vec, field_name: Field, fields: &[String], id: Id) -> Report { +fn error_chain(mut errors: Vec, field_name: Field, fields: &[String], id: Id, tracker: &impl tracker::FieldsConfig) -> Report { + let url = id.url(tracker); let top_error = eyre!( - "The {} field is missing or malformed in {}. \ - The configured fields are: {:?}", + "The {} field is missing or malformed in {} ({}).\n\ + The configured fields for '{}' are: {:?}", field_name, id, + url, + field_name, fields ); @@ -242,12 +255,12 @@ fn error_chain(mut errors: Vec, field_name: Field, fields: &[String], id impl ExtraFields for Bug { fn doc_type(&self, config: &impl tracker::FieldsConfig) -> Result { let fields = config.doc_type(); - extract_field(Field::DocType, &self.extra, fields, Id::BZ(self.id)) + extract_field(Field::DocType, &self.extra, fields, Id::BZ(self.id), config) } fn doc_text(&self, config: &impl tracker::FieldsConfig) -> Result { let fields = config.doc_text(); - extract_field(Field::DocText, &self.extra, fields, Id::BZ(self.id)) + extract_field(Field::DocText, &self.extra, fields, Id::BZ(self.id), config) } fn target_releases(&self, config: &impl tracker::FieldsConfig) -> Vec { @@ -255,7 +268,7 @@ impl ExtraFields for Bug { let mut errors = Vec::new(); // Try the custom overrides, if any. - match extract_field(Field::TargetRelease, &self.extra, fields, Id::BZ(self.id)) { + match extract_field(Field::TargetRelease, &self.extra, fields, Id::BZ(self.id), config) { Ok(release) => { // Bugzilla uses the "---" placeholder to represent an unset release. // TODO: Are there any more placeholder? @@ -280,7 +293,7 @@ impl ExtraFields for Bug { match &self.target_release { Some(versions) => versions.clone().into_vec(), None => { - let report = error_chain(errors, Field::TargetRelease, fields, Id::BZ(self.id)); + let report = error_chain(errors, Field::TargetRelease, fields, Id::BZ(self.id), config); log::warn!("{report}"); // Finally, return an empty list if everything else failed. @@ -323,7 +336,7 @@ impl ExtraFields for Bug { } } - let report = error_chain(errors, Field::Subsystems, fields, Id::BZ(self.id)); + let report = error_chain(errors, Field::Subsystems, fields, Id::BZ(self.id), config); Err(report) } @@ -360,7 +373,7 @@ impl ExtraFields for Bug { // If all we've got are errors, report an error with the complete errors report. if empty_fields.is_empty() { - let report = error_chain(errors, Field::DocTextStatus, fields, Id::BZ(self.id)); + let report = error_chain(errors, Field::DocTextStatus, fields, Id::BZ(self.id), config); log::warn!("{}", report); // If we at least got an existing but empty field, report the empty flags. } else { @@ -379,7 +392,7 @@ impl ExtraFields for Bug { let mut errors = Vec::new(); // Try the custom overrides, if any. - let docs_contact = extract_field(Field::DocsContact, &self.extra, fields, Id::BZ(self.id)); + let docs_contact = extract_field(Field::DocsContact, &self.extra, fields, Id::BZ(self.id), config); match docs_contact { Ok(docs_contact) => { @@ -392,7 +405,7 @@ impl ExtraFields for Bug { // No override succeeded. See if there's a value in the standard field. if self.docs_contact.is_none() { - let report = error_chain(errors, Field::DocsContact, fields, Id::BZ(self.id)); + let report = error_chain(errors, Field::DocsContact, fields, Id::BZ(self.id), config); log::warn!("{}", report); } @@ -461,7 +474,7 @@ impl ExtraFields for Issue { }; } - let report = error_chain(errors, Field::DocType, fields, Id::Jira(&self.key)); + let report = error_chain(errors, Field::DocType, fields, Id::Jira(&self.key), config); Err(report) } @@ -472,6 +485,7 @@ impl ExtraFields for Issue { &self.fields.extra, fields, Id::Jira(&self.key), + config, ) } @@ -513,6 +527,7 @@ impl ExtraFields for Issue { &self.extra, &[field.clone()], Id::Jira(&self.key), + config, ); match string { Ok(string) => { @@ -530,7 +545,7 @@ impl ExtraFields for Issue { // If any errors occurred, report them as warnings and continue. if !errors.is_empty() { let id = Id::Jira(&self.key); - let report = error_chain(errors, Field::TargetRelease, fields, id); + let report = error_chain(errors, Field::TargetRelease, fields, id, config); log::warn!("The custom target releases failed in {}. Falling back on the standard fix versions field.", id); // Provide this additional information on demand. @@ -599,7 +614,7 @@ impl ExtraFields for Issue { // No field produced a `Some` value. // Prepare a user-readable list of errors, if any occurred. - let report = error_chain(errors, Field::Subsystems, fields, Id::Jira(&self.key)); + let report = error_chain(errors, Field::Subsystems, fields, Id::Jira(&self.key), config); // Return the combined error. Err(report) @@ -648,7 +663,7 @@ impl ExtraFields for Issue { } // No field produced a `Some` value. - let report = error_chain(errors, Field::DocTextStatus, fields, Id::Jira(&self.key)); + let report = error_chain(errors, Field::DocTextStatus, fields, Id::Jira(&self.key), config); // Report all errors. log::warn!("{}", report); @@ -674,7 +689,7 @@ impl ExtraFields for Issue { } // No field produced a `Some` value. - let report = error_chain(Vec::new(), Field::DocsContact, fields, Id::Jira(&self.key)); + let report = error_chain(Vec::new(), Field::DocsContact, fields, Id::Jira(&self.key), config); // This field is non-critical. log::warn!("{}", report); diff --git a/src/references.rs b/src/references.rs index 842bf8a..45a4643 100644 --- a/src/references.rs +++ b/src/references.rs @@ -36,11 +36,20 @@ impl From<&[Arc]> for ReferenceQueries { // I don't know how to accomplish this in a functional style, unfortunately. for query in item { - for reference in &query.references { - reference_queries.push(Arc::clone(reference)); + if !query.references.is_empty() { + log::info!( + "Query {:?} has {} reference(s)", + query.using, + query.references.len() + ); + for reference in &query.references { + log::info!(" Reference: {:?}", reference.using); + reference_queries.push(Arc::clone(reference)); + } } } + log::info!("Total reference queries extracted: {}", reference_queries.len()); Self(reference_queries) } } diff --git a/src/status_report.rs b/src/status_report.rs index 5397a22..6bf0e8b 100644 --- a/src/status_report.rs +++ b/src/status_report.rs @@ -586,7 +586,7 @@ fn extract_number(caps: ®ex::Captures, index: usize) -> Option { } /// List the most common release set in the tickets. -fn most_common_release(tickets: &[AbstractTicket]) -> Option { +fn most_common_release(tickets: &[AbstractTicket]) -> Option> { let mut releases: Counter = Counter::new(); // Releases are a list, and each ticket can have several of them. diff --git a/src/tracker_access.rs b/src/tracker_access.rs index d05569c..e9811d5 100644 --- a/src/tracker_access.rs +++ b/src/tracker_access.rs @@ -385,7 +385,8 @@ async fn issues( let mut all_issues = Vec::new(); - let issues_from_ids = issues_from_ids(&queries_by_id, &jira_instance); + 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) = @@ -403,14 +404,13 @@ async fn issues( async fn issues_from_ids( queries: &[(&str, Arc)], jira_instance: &jira_query::JiraInstance, + jira_host: &str, ) -> Result, 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( - &queries - .iter() - .map(|(key, _query)| *key) - .collect::>(), - ) + .issues(&issue_keys) // This enables the download concurrency: .await .wrap_err("Failed to download tickets from Jira.")?; @@ -421,9 +421,23 @@ async fn issues_from_ids( let matching_query = queries .iter() .find(|(key, _query)| key == &issue.key.as_str()) - .map(|(_key, query)| Arc::clone(query)) - .ok_or_else(|| eyre!("Issue {} doesn't match any configured query.", issue.key))?; - annotated_issues.push((matching_query, issue)); + .map(|(_key, query)| Arc::clone(query)); + + if let Some(query) = matching_query { + annotated_issues.push((query, issue)); + } else { + // When we can't find a match, it's likely because the ticket was moved to another project + // and now has a different ID than what was configured. + 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)