Skip to content

Commit

Permalink
Merge pull request #1856 from ehuss/merge-conflict
Browse files Browse the repository at this point in the history
Add merge-conflict notifications
  • Loading branch information
ehuss authored Nov 23, 2024
2 parents 742b66b + 5301321 commit d7d4bab
Show file tree
Hide file tree
Showing 4 changed files with 456 additions and 1 deletion.
14 changes: 14 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub(crate) struct Config {
pub(crate) validate_config: Option<ValidateConfig>,
pub(crate) pr_tracking: Option<ReviewPrefsConfig>,
pub(crate) transfer: Option<TransferConfig>,
pub(crate) merge_conflicts: Option<MergeConflictConfig>,
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
Expand Down Expand Up @@ -350,6 +351,18 @@ pub(crate) struct ReviewPrefsConfig {
#[serde(deny_unknown_fields)]
pub(crate) struct TransferConfig {}

#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub(crate) struct MergeConflictConfig {
#[serde(default)]
pub remove: HashSet<String>,
#[serde(default)]
pub add: HashSet<String>,
#[serde(default)]
pub unless: HashSet<String>,
}

fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
let cache = CONFIG_CACHE.read().unwrap();
cache.get(repo).and_then(|(config, fetch_time)| {
Expand Down Expand Up @@ -527,6 +540,7 @@ mod tests {
validate_config: Some(ValidateConfig {}),
pr_tracking: None,
transfer: None,
merge_conflicts: None,
}
);
}
Expand Down
104 changes: 103 additions & 1 deletion src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,8 @@ pub struct Issue {
/// Whether it is open or closed.
pub state: IssueState,
pub milestone: Option<Milestone>,
/// Whether a PR has merge conflicts.
pub mergeable: Option<bool>,
}

#[derive(Debug, serde::Deserialize, Eq, PartialEq)]
Expand Down Expand Up @@ -1826,6 +1828,101 @@ impl Repository {
.await
.with_context(|| format!("{} failed to get issue {issue_num}", self.full_name))
}

/// Fetches information about merge conflicts on open PRs.
pub async fn get_merge_conflict_prs(
&self,
client: &GithubClient,
) -> anyhow::Result<Vec<MergeConflictInfo>> {
let mut prs = Vec::new();
let mut after = None;
loop {
let mut data = client
.graphql_query(
"query($owner:String!, $repo:String!, $after:String) {
repository(owner: $owner, name: $repo) {
pullRequests(states: OPEN, first: 100, after: $after) {
edges {
node {
number
mergeable
baseRefName
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}",
serde_json::json!({
"owner": self.owner(),
"repo": self.name(),
"after": after,
}),
)
.await?;
let edges = data["data"]["repository"]["pullRequests"]["edges"].take();
let serde_json::Value::Array(edges) = edges else {
anyhow::bail!("expected array edges, got {edges:?}");
};
let this_page = edges
.into_iter()
.map(|mut edge| {
serde_json::from_value(edge["node"].take())
.with_context(|| "failed to deserialize merge conflicts")
})
.collect::<Result<Vec<_>, _>>()?;
prs.extend(this_page);
if !data["data"]["repository"]["pullRequests"]["pageInfo"]["hasNextPage"]
.as_bool()
.unwrap_or(false)
{
break;
}
after = Some(
data["data"]["repository"]["pullRequests"]["pageInfo"]["endCursor"]
.as_str()
.expect("endCursor is string")
.to_string(),
);
}
Ok(prs)
}

/// Returns a list of PRs "associated" with a commit.
pub async fn pulls_for_commit(
&self,
client: &GithubClient,
sha: &str,
) -> anyhow::Result<Vec<Issue>> {
let url = format!("{}/commits/{sha}/pulls", self.url(client));
client
.json(client.get(&url))
.await
.with_context(|| format!("{} failed to get pulls for commit {sha}", self.full_name))
}
}

/// Information about a merge conflict on a PR.
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MergeConflictInfo {
/// Pull request number.
pub number: u64,
/// Whether this pull can be merged.
pub mergeable: MergeableState,
/// The branch name where this PR is requesting to be merged to.
pub base_ref_name: String,
}

#[derive(Debug, serde::Deserialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MergeableState {
Conflicting,
Mergeable,
Unknown,
}

pub struct Query<'a> {
Expand Down Expand Up @@ -2081,9 +2178,14 @@ pub struct CreateEvent {

#[derive(Debug, serde::Deserialize)]
pub struct PushEvent {
/// The SHA of the most recent commit on `ref` after the push.
pub after: String,
/// The full git ref that was pushed.
///
/// Example: `refs/heads/main` or `refs/tags/v3.14.1`.
#[serde(rename = "ref")]
pub git_ref: String,
repository: Repository,
pub repository: Repository,
sender: User,
}

Expand Down
15 changes: 15 additions & 0 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ mod github_releases;
mod glacier;
mod major_change;
mod mentions;
mod merge_conflicts;
mod milestone_prs;
mod no_merges;
mod nominate;
Expand Down Expand Up @@ -144,6 +145,20 @@ pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
}
}

if let Some(conflict_config) = config
.as_ref()
.ok()
.and_then(|c| c.merge_conflicts.as_ref())
{
if let Err(e) = merge_conflicts::handle(ctx, event, conflict_config).await {
log::error!(
"failed to process event {:?} with merge_conflicts handler: {:?}",
event,
e
);
}
}

errors
}

Expand Down
Loading

0 comments on commit d7d4bab

Please sign in to comment.