From 801230c0d9daa398318a090082e65283466e6223 Mon Sep 17 00:00:00 2001 From: Viktor Ashirov Date: Tue, 4 Nov 2025 12:20:19 +0100 Subject: [PATCH 1/4] Fix compilation warning --- src/status_report.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 68655c8fe82dadaabfe4eaffb982adc5c09189af Mon Sep 17 00:00:00 2001 From: Viktor Ashirov Date: Tue, 4 Nov 2025 12:35:30 +0100 Subject: [PATCH 2/4] Log references for tickets that have them --- src/references.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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) } } From 7a8089ad7390c4d3e737f7c5faa05467892ac4b5 Mon Sep 17 00:00:00 2001 From: Viktor Ashirov Date: Tue, 4 Nov 2025 13:01:42 +0100 Subject: [PATCH 3/4] Print a better message when ticket was moved --- src/tracker_access.rs | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) 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) From 58665dce96e46925d7463172e8a5e5b0116a6adf Mon Sep 17 00:00:00 2001 From: Viktor Ashirov Date: Tue, 4 Nov 2025 13:02:15 +0100 Subject: [PATCH 4/4] Add clickable URLs for tickets Error messages contain ticket IDs, but there is no reason why we can't put direct URLs there. Also slightly change the error message when some fields are empty or missing. --- src/extra_fields.rs | 51 +++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 18 deletions(-) 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);