|
@@ -19,7 +19,7 @@
|
|
|
|
|
|
use crate::{
|
|
|
config::AssignConfig,
|
|
|
- github::{self, Event, Issue, IssuesAction, Selection},
|
|
|
+ github::{self, Event, FileDiff, Issue, IssuesAction, Selection},
|
|
|
handlers::{Context, GithubClient, IssuesEvent},
|
|
|
interactions::EditIssueBody,
|
|
|
};
|
|
@@ -80,13 +80,11 @@ struct AssignData {
|
|
|
}
|
|
|
|
|
|
/// Input for auto-assignment when a PR is created.
|
|
|
-pub(super) struct AssignInput {
|
|
|
- git_diff: String,
|
|
|
-}
|
|
|
+pub(super) struct AssignInput {}
|
|
|
|
|
|
/// Prepares the input when a new PR is opened.
|
|
|
pub(super) async fn parse_input(
|
|
|
- ctx: &Context,
|
|
|
+ _ctx: &Context,
|
|
|
event: &IssuesEvent,
|
|
|
config: Option<&AssignConfig>,
|
|
|
) -> Result<Option<AssignInput>, String> {
|
|
@@ -100,15 +98,7 @@ pub(super) async fn parse_input(
|
|
|
{
|
|
|
return Ok(None);
|
|
|
}
|
|
|
- let git_diff = match event.issue.diff(&ctx.github).await {
|
|
|
- Ok(None) => return Ok(None),
|
|
|
- Err(e) => {
|
|
|
- log::error!("failed to fetch diff: {:?}", e);
|
|
|
- return Ok(None);
|
|
|
- }
|
|
|
- Ok(Some(diff)) => diff,
|
|
|
- };
|
|
|
- Ok(Some(AssignInput { git_diff }))
|
|
|
+ Ok(Some(AssignInput {}))
|
|
|
}
|
|
|
|
|
|
/// Handles the work of setting an assignment for a new PR and posting a
|
|
@@ -117,11 +107,18 @@ pub(super) async fn handle_input(
|
|
|
ctx: &Context,
|
|
|
config: &AssignConfig,
|
|
|
event: &IssuesEvent,
|
|
|
- input: AssignInput,
|
|
|
+ _input: AssignInput,
|
|
|
) -> anyhow::Result<()> {
|
|
|
+ let Some(diff) = event.issue.diff(&ctx.github).await? else {
|
|
|
+ bail!(
|
|
|
+ "expected issue {} to be a PR, but the diff could not be determined",
|
|
|
+ event.issue.number
|
|
|
+ )
|
|
|
+ };
|
|
|
+
|
|
|
// Don't auto-assign or welcome if the user manually set the assignee when opening.
|
|
|
if event.issue.assignees.is_empty() {
|
|
|
- let (assignee, from_comment) = determine_assignee(ctx, event, config, &input).await?;
|
|
|
+ let (assignee, from_comment) = determine_assignee(ctx, event, config, &diff).await?;
|
|
|
if assignee.as_deref() == Some("ghost") {
|
|
|
// "ghost" is GitHub's placeholder account for deleted accounts.
|
|
|
// It is used here as a convenient way to prevent assignment. This
|
|
@@ -180,7 +177,7 @@ pub(super) async fn handle_input(
|
|
|
if config.warn_non_default_branch {
|
|
|
warnings.extend(non_default_branch(event));
|
|
|
}
|
|
|
- warnings.extend(modifies_submodule(&input.git_diff));
|
|
|
+ warnings.extend(modifies_submodule(diff));
|
|
|
if !warnings.is_empty() {
|
|
|
let warnings: Vec<_> = warnings
|
|
|
.iter()
|
|
@@ -222,9 +219,9 @@ fn non_default_branch(event: &IssuesEvent) -> Option<String> {
|
|
|
}
|
|
|
|
|
|
/// Returns a message if the PR modifies a git submodule.
|
|
|
-fn modifies_submodule(diff: &str) -> Option<String> {
|
|
|
+fn modifies_submodule(diff: &[FileDiff]) -> Option<String> {
|
|
|
let re = regex::Regex::new(r"\+Subproject\scommit\s").unwrap();
|
|
|
- if re.is_match(diff) {
|
|
|
+ if diff.iter().any(|fd| re.is_match(&fd.diff)) {
|
|
|
Some(SUBMODULE_WARNING_MSG.to_string())
|
|
|
} else {
|
|
|
None
|
|
@@ -278,7 +275,7 @@ async fn determine_assignee(
|
|
|
ctx: &Context,
|
|
|
event: &IssuesEvent,
|
|
|
config: &AssignConfig,
|
|
|
- input: &AssignInput,
|
|
|
+ diff: &[FileDiff],
|
|
|
) -> anyhow::Result<(Option<String>, bool)> {
|
|
|
let teams = crate::team_data::teams(&ctx.github).await?;
|
|
|
if let Some(name) = find_assign_command(ctx, event) {
|
|
@@ -298,7 +295,7 @@ async fn determine_assignee(
|
|
|
}
|
|
|
}
|
|
|
// Errors fall-through to try fallback group.
|
|
|
- match find_reviewers_from_diff(config, &input.git_diff) {
|
|
|
+ match find_reviewers_from_diff(config, diff) {
|
|
|
Ok(candidates) if !candidates.is_empty() => {
|
|
|
match find_reviewer_from_names(&teams, config, &event.issue, &candidates) {
|
|
|
Ok(assignee) => return Ok((Some(assignee), false)),
|
|
@@ -346,60 +343,61 @@ async fn determine_assignee(
|
|
|
/// May return an error if the owners map is misconfigured.
|
|
|
///
|
|
|
/// Beware this may return an empty list if nothing matches.
|
|
|
-fn find_reviewers_from_diff(config: &AssignConfig, diff: &str) -> anyhow::Result<Vec<String>> {
|
|
|
+fn find_reviewers_from_diff(
|
|
|
+ config: &AssignConfig,
|
|
|
+ diff: &[FileDiff],
|
|
|
+) -> anyhow::Result<Vec<String>> {
|
|
|
// Map of `owners` path to the number of changes found in that path.
|
|
|
// This weights the reviewer choice towards places where the most edits are done.
|
|
|
let mut counts: HashMap<&str, u32> = HashMap::new();
|
|
|
- // List of the longest `owners` patterns that match the current path. This
|
|
|
- // prefers choosing reviewers from deeply nested paths over those defined
|
|
|
- // for top-level paths, under the assumption that they are more
|
|
|
- // specialized.
|
|
|
- //
|
|
|
- // This is a list to handle the situation if multiple paths of the same
|
|
|
- // length match.
|
|
|
- let mut longest_owner_patterns = Vec::new();
|
|
|
- // Iterate over the diff, finding the start of each file. After each file
|
|
|
- // is found, it counts the number of modified lines in that file, and
|
|
|
- // tracks those in the `counts` map.
|
|
|
- for line in diff.split('\n') {
|
|
|
- if line.starts_with("diff --git ") {
|
|
|
- // Start of a new file.
|
|
|
- longest_owner_patterns.clear();
|
|
|
- let path = line[line.find(" b/").unwrap()..]
|
|
|
- .strip_prefix(" b/")
|
|
|
- .unwrap();
|
|
|
- // Find the longest `owners` entries that match this path.
|
|
|
- let mut longest = HashMap::new();
|
|
|
- for owner_pattern in config.owners.keys() {
|
|
|
- let ignore = ignore::gitignore::GitignoreBuilder::new("/")
|
|
|
- .add_line(None, owner_pattern)
|
|
|
- .with_context(|| format!("owner file pattern `{owner_pattern}` is not valid"))?
|
|
|
- .build()?;
|
|
|
- if ignore.matched_path_or_any_parents(path, false).is_ignore() {
|
|
|
- let owner_len = owner_pattern.split('/').count();
|
|
|
- longest.insert(owner_pattern, owner_len);
|
|
|
- }
|
|
|
- }
|
|
|
- let max_count = longest.values().copied().max().unwrap_or(0);
|
|
|
- longest_owner_patterns.extend(
|
|
|
- longest
|
|
|
- .iter()
|
|
|
- .filter(|(_, count)| **count == max_count)
|
|
|
- .map(|x| *x.0),
|
|
|
- );
|
|
|
- // Give some weight to these patterns to start. This helps with
|
|
|
- // files modified without any lines changed.
|
|
|
- for owner_pattern in &longest_owner_patterns {
|
|
|
- *counts.entry(owner_pattern).or_default() += 1;
|
|
|
+ // Iterate over the diff, counting the number of modified lines in each
|
|
|
+ // file, and tracks those in the `counts` map.
|
|
|
+ for file_diff in diff {
|
|
|
+ // List of the longest `owners` patterns that match the current path. This
|
|
|
+ // prefers choosing reviewers from deeply nested paths over those defined
|
|
|
+ // for top-level paths, under the assumption that they are more
|
|
|
+ // specialized.
|
|
|
+ //
|
|
|
+ // This is a list to handle the situation if multiple paths of the same
|
|
|
+ // length match.
|
|
|
+ let mut longest_owner_patterns = Vec::new();
|
|
|
+
|
|
|
+ // Find the longest `owners` entries that match this path.
|
|
|
+ let mut longest = HashMap::new();
|
|
|
+ for owner_pattern in config.owners.keys() {
|
|
|
+ let ignore = ignore::gitignore::GitignoreBuilder::new("/")
|
|
|
+ .add_line(None, owner_pattern)
|
|
|
+ .with_context(|| format!("owner file pattern `{owner_pattern}` is not valid"))?
|
|
|
+ .build()?;
|
|
|
+ if ignore
|
|
|
+ .matched_path_or_any_parents(&file_diff.path, false)
|
|
|
+ .is_ignore()
|
|
|
+ {
|
|
|
+ let owner_len = owner_pattern.split('/').count();
|
|
|
+ longest.insert(owner_pattern, owner_len);
|
|
|
}
|
|
|
- continue;
|
|
|
}
|
|
|
- // Check for a modified line.
|
|
|
- if (!line.starts_with("+++") && line.starts_with('+'))
|
|
|
- || (!line.starts_with("---") && line.starts_with('-'))
|
|
|
- {
|
|
|
- for owner_path in &longest_owner_patterns {
|
|
|
- *counts.entry(owner_path).or_default() += 1;
|
|
|
+ let max_count = longest.values().copied().max().unwrap_or(0);
|
|
|
+ longest_owner_patterns.extend(
|
|
|
+ longest
|
|
|
+ .iter()
|
|
|
+ .filter(|(_, count)| **count == max_count)
|
|
|
+ .map(|x| *x.0),
|
|
|
+ );
|
|
|
+ // Give some weight to these patterns to start. This helps with
|
|
|
+ // files modified without any lines changed.
|
|
|
+ for owner_pattern in &longest_owner_patterns {
|
|
|
+ *counts.entry(owner_pattern).or_default() += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Count the modified lines.
|
|
|
+ for line in file_diff.diff.lines() {
|
|
|
+ if (!line.starts_with("+++") && line.starts_with('+'))
|
|
|
+ || (!line.starts_with("---") && line.starts_with('-'))
|
|
|
+ {
|
|
|
+ for owner_path in &longest_owner_patterns {
|
|
|
+ *counts.entry(owner_path).or_default() += 1;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|