use knope_versioning::changes::Change;
use miette::Diagnostic;
use tracing::{debug, info, warn};
use crate::{app_config, config, integrations::http, state, state::RunType};
#[derive(serde::Deserialize)]
struct PullRequestInfo {
number: u64,
user: Option<PullRequestUser>,
}
#[derive(serde::Deserialize)]
struct PullRequestUser {
login: String,
}
pub(crate) async fn enrich_git_info(
changes: &mut [Change],
github_config: &config::GitHub,
github_state: state::GitHub,
run_type: RunType<()>,
) -> Result<state::GitHub, Error> {
if run_type.is_dry_run() {
info!(
"Would fetch Pull Request info from GitHub repo {owner}/{repo} for {num_changes} changes",
owner = github_config.owner,
repo = github_config.repo,
num_changes = changes.len(),
);
for git_info in changes.iter_mut().filter_map(|change| change.git.as_mut()) {
git_info.pr_number = Some(1234);
git_info.pr_author_login = Some("some-user".to_string());
}
return Ok(github_state);
}
let (token, client) = github_state.maybe_authenticated()?;
let authorization = token.as_ref().map(|token| format!("Bearer {token}"));
for git_info in changes.iter_mut().filter_map(|change| change.git.as_mut()) {
let short_hash = &git_info.hash;
match fetch_pr_for_commit(&client, authorization.as_deref(), github_config, short_hash)
.await
{
Ok(Some((pr_number, author_login))) => {
git_info.pr_number = Some(pr_number);
git_info.pr_author_login = author_login;
}
Ok(None) => {
debug!("No PR found for commit {short_hash}");
}
Err(e) => {
warn!("Failed to fetch PR info for commit {short_hash}: {e}");
}
}
}
if let Some(token) = token {
Ok(state::GitHub::Authenticated { token, client })
} else {
Ok(state::GitHub::Unauthenticated { client })
}
}
async fn fetch_pr_for_commit(
client: &http::Client,
authorization: Option<&str>,
config: &config::GitHub,
commit_sha: &str,
) -> Result<Option<(u64, Option<String>)>, Error> {
let url = format!(
"https://api.github.com/repos/{owner}/{repo}/commits/{commit_sha}/pulls",
owner = config.owner,
repo = config.repo,
);
let mut request = client
.get(&url)
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2026-03-10");
if let Some(authorization) = authorization {
request = request.header("Authorization", authorization);
}
let response = request.send().await;
let response = http::handle_response(
response,
"GitHub".to_string(),
format!("fetching PRs for commit {commit_sha}"),
)
.await?;
let pulls: Vec<PullRequestInfo> =
response.json().await.map_err(|source| Error::ApiResponse {
message: source.to_string(),
activity: format!("reading PR info for commit {commit_sha}"),
})?;
Ok(pulls.into_iter().next().map(|pr| {
let login = pr.user.map(|u| u.login);
(pr.number, login)
}))
}
#[derive(Debug, Diagnostic, thiserror::Error)]
pub(crate) enum Error {
#[error(transparent)]
#[diagnostic(transparent)]
AppConfig(#[from] app_config::Error),
#[error(transparent)]
#[diagnostic(transparent)]
ApiRequest(#[from] http::ApiRequestError),
#[error("Trouble decoding the response from GitHub while {activity}: {message}")]
#[diagnostic(
code(github::api_response_error),
help(
"Failure to decode a response from GitHub is probably a bug. Please report it at https://github.com/knope-dev/knope"
)
)]
ApiResponse { message: String, activity: String },
}