From d25129d829e0ebd70b4e60e399fe91c0d80aa1ad Mon Sep 17 00:00:00 2001 From: seth Date: Sun, 16 Jun 2024 07:15:13 -0400 Subject: use libgit2 to track PRs (#10) * nix: don't depend on registry for nixpkgs input * use libgit2 to track PRs * nix: don't use ci devShell as defaul * crates: bump serenity from `9ad74d4` to `0.12.2 * nix: fix cross compiled builds * crates: split more from client * bot-jobs: update remote refs more efficiently * git-tracker: account for HEAD commits * bot-config: use nixpkgs branches from environment * bot-commands: don't display branches prs haven't landed in * git-tracker: return false when commits aren't found this is annoying as a hard error since it turns out github will report garbage merge commit SHAs for PRs that *haven't* been merged yet. yay * bot: improve docs in some places * bot-client: display invite link on start * bot-http: add TeawieClientExt * bot-commands: add /about * docs: update readme todos * nix: enable StateDirectory in module * crates: bump to 0.2.0 --- crates/bot-commands/Cargo.toml | 34 +++++++++++ crates/bot-commands/src/about.rs | 44 ++++++++++++++ crates/bot-commands/src/lib.rs | 17 ++++++ crates/bot-commands/src/ping.rs | 21 +++++++ crates/bot-commands/src/track.rs | 121 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 237 insertions(+) create mode 100644 crates/bot-commands/Cargo.toml create mode 100644 crates/bot-commands/src/about.rs create mode 100644 crates/bot-commands/src/lib.rs create mode 100644 crates/bot-commands/src/ping.rs create mode 100644 crates/bot-commands/src/track.rs (limited to 'crates/bot-commands') diff --git a/crates/bot-commands/Cargo.toml b/crates/bot-commands/Cargo.toml new file mode 100644 index 0000000..3594c70 --- /dev/null +++ b/crates/bot-commands/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "bot-commands" +version = "0.2.0" +edition = "2021" + +authors = ["seth "] +description = "Discord application commands for nixpkgs-tracker-bot" +repository = "https://github.com/getchoo/nixpkgs-tracker-bot" + +publish = false + +[dependencies] +bot-config = { workspace = true } +bot-consts = { workspace = true } +bot-error = { workspace = true } +bot-http = { workspace = true } +git-tracker = { workspace = true } +log = { workspace = true } +serenity = { workspace = true } + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +complexity = "warn" +correctness = "deny" +pedantic = "warn" +perf = "warn" +style = "warn" +suspicious = "deny" +# NOTE: THIS ISN'T IN OTHER CRATES BUT IS HERE +# this is because we don't really care about error docs here +# and it could mess with poise's comment system in the future :p +missing-errors-doc = "allow" diff --git a/crates/bot-commands/src/about.rs b/crates/bot-commands/src/about.rs new file mode 100644 index 0000000..2e5efae --- /dev/null +++ b/crates/bot-commands/src/about.rs @@ -0,0 +1,44 @@ +use bot_error::Error; +use bot_http::TeawieClientExt; + +use serenity::builder::{ + CreateCommand, CreateEmbed, CreateEmbedFooter, CreateInteractionResponse, + CreateInteractionResponseMessage, +}; +use serenity::model::application::{CommandInteraction, InstallationContext}; +use serenity::prelude::Context; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY"); + +pub async fn respond( + ctx: &Context, + http: &bot_http::Client, + command: &CommandInteraction, +) -> Result<(), Error> { + let mut embed = CreateEmbed::new() + .title("About nixpkgs-tracker-bot") + .description("I help track what branches PRs to nixpkgs have reached. If you've used [Nixpkgs Pull Request Tracker](https://nixpk.gs/pr-tracker.html), you probably know what this is about.") + .fields([ + ("Version", VERSION, true), + ("Source code", &format!("[getchoo/nixpkgs-tracker-bot]({REPOSITORY})"), true), + ("Issues/Feature Requests", &format!("[getchoo/nixpkgs-tracker-bot/issues]({REPOSITORY}/issues)"), true) + ]); + + if let Some(teawie_url) = http.random_teawie().await? { + let footer = CreateEmbedFooter::new("Images courtesy of @sympathytea"); + embed = embed.image(teawie_url).footer(footer); + }; + + let message = CreateInteractionResponseMessage::new().embed(embed); + let response = CreateInteractionResponse::Message(message); + command.create_response(&ctx, response).await?; + + Ok(()) +} + +pub fn register() -> CreateCommand { + CreateCommand::new("about") + .description("Learn more about me") + .add_integration_type(InstallationContext::User) +} diff --git a/crates/bot-commands/src/lib.rs b/crates/bot-commands/src/lib.rs new file mode 100644 index 0000000..79fce17 --- /dev/null +++ b/crates/bot-commands/src/lib.rs @@ -0,0 +1,17 @@ +use serenity::builder::CreateCommand; + +pub mod about; +pub mod ping; +pub mod track; + +macro_rules! cmd { + ($module: ident) => { + $module::register() + }; +} + +/// Return a list of all our [`CreateCommand`]s +#[must_use] +pub fn to_vec() -> Vec { + vec![cmd!(about), cmd!(ping), cmd!(track)] +} diff --git a/crates/bot-commands/src/ping.rs b/crates/bot-commands/src/ping.rs new file mode 100644 index 0000000..b18a0b6 --- /dev/null +++ b/crates/bot-commands/src/ping.rs @@ -0,0 +1,21 @@ +use bot_error::Error; + +use serenity::builder::{ + CreateCommand, CreateInteractionResponse, CreateInteractionResponseMessage, +}; +use serenity::model::application::{CommandInteraction, InstallationContext}; +use serenity::prelude::Context; + +pub async fn respond(ctx: &Context, command: &CommandInteraction) -> Result<(), Error> { + let message = CreateInteractionResponseMessage::new().content("Pong!"); + let response = CreateInteractionResponse::Message(message); + command.create_response(&ctx, response).await?; + + Ok(()) +} + +pub fn register() -> CreateCommand { + CreateCommand::new("ping") + .description("Check if the bot is up") + .add_integration_type(InstallationContext::User) +} diff --git a/crates/bot-commands/src/track.rs b/crates/bot-commands/src/track.rs new file mode 100644 index 0000000..1f22d0e --- /dev/null +++ b/crates/bot-commands/src/track.rs @@ -0,0 +1,121 @@ +use bot_config::Config; +use bot_consts::{NIXPKGS_REMOTE, NIXPKGS_URL}; +use bot_error::Error; +use bot_http::{self as http, GithubClientExt}; +use git_tracker::Tracker; + +use log::trace; +use serenity::all::CreateEmbed; +use serenity::builder::{CreateCommand, CreateCommandOption, CreateInteractionResponseFollowup}; +use serenity::model::application::{ + CommandInteraction, CommandOptionType, InstallationContext, ResolvedOption, ResolvedValue, +}; +use serenity::prelude::Context; + +const REPO_OWNER: &str = "NixOS"; +const REPO_NAME: &str = "nixpkgs"; + +/// Collect the status of the commit SHA [`commit_sha`] in each of the nixpkgs +/// branches in [`branches`], using the repository at path [`repository_path`] +/// +/// # Errors +/// +/// Will return [`Err`] if we can't start tracking a repository at the given path, +/// or if we can't determine if the branch has given commit +fn collect_statuses_in<'a>( + repository_path: &str, + commit_sha: &str, + branches: impl IntoIterator, +) -> Result, Error> { + // start tracking nixpkgs + let tracker = Tracker::from_path(repository_path)?; + + // check to see what branches it's in + let mut status_results = vec![]; + for branch_name in branches { + trace!("Checking for commit in {branch_name}"); + let full_branch_name = format!("{NIXPKGS_REMOTE}/{branch_name}"); + let has_pr = tracker.branch_contains_sha(&full_branch_name, commit_sha)?; + + if has_pr { + status_results.push(format!("`{branch_name}` ✅")); + } + } + + Ok(status_results) +} + +pub async fn respond( + ctx: &Context, + http: &http::Client, + config: &Config, + command: &CommandInteraction, +) -> Result<(), Error> { + // this will probably take a while + command.defer(&ctx).await?; + + let options = command.data.options(); + let Some(ResolvedOption { + value: ResolvedValue::Integer(pr), + .. + }) = options.first() + else { + let resp = CreateInteractionResponseFollowup::new() + .content("Please provide a valid pull request!"); + command.create_followup(&ctx, resp).await?; + + return Ok(()); + }; + + let Ok(pr_id) = u64::try_from(*pr) else { + let resp = + CreateInteractionResponseFollowup::new().content("PR numbers aren't negative..."); + command.create_followup(&ctx, resp).await?; + + return Ok(()); + }; + + // find out what commit our PR was merged in + let Some(commit_sha) = http.merge_commit_for(REPO_OWNER, REPO_NAME, pr_id).await? else { + let response = CreateInteractionResponseFollowup::new() + .content("It seems this pull request is very old. I can't track it"); + command.create_followup(&ctx, response).await?; + + return Ok(()); + }; + + let status_results = collect_statuses_in( + &config.nixpkgs_path, + &commit_sha, + config.nixpkgs_branches.iter(), + )?; + + // if we don't find the commit in any branches from above, we can pretty safely assume + // it's an unmerged PR + let embed_description: String = if status_results.is_empty() { + "It doesn't look like this PR has been merged yet! (or maybe I just haven't updated)" + .to_string() + } else { + status_results.join("\n") + }; + + let embed = CreateEmbed::new() + .title(format!("Nixpkgs PR #{} Status", *pr)) + .url(format!("{NIXPKGS_URL}/pull/{pr}")) + .description(embed_description); + + let resp = CreateInteractionResponseFollowup::new().embed(embed); + command.create_followup(&ctx, resp).await?; + + Ok(()) +} + +pub fn register() -> CreateCommand { + CreateCommand::new("track") + .description("Track a nixpkgs PR") + .add_integration_type(InstallationContext::User) + .add_option( + CreateCommandOption::new(CommandOptionType::Integer, "pull_request", "PR to track") + .required(true), + ) +} -- cgit v1.2.3