use std::{collections::HashMap, env, ffi::OsString, fmt::Debug};
pub(crate) mod graphql;
mod pull_request;
use config::Config;
use git2::Repository;
use keep_a_changelog::{ChangeKind, ChangelogParseOptions};
use octocrate::{APIConfig, AppAuthorization, GitHubAPI, PersonalAccessToken};
use owo_colors::{OwoColorize, Style};
use self::pull_request::PullRequest;
use crate::{Error, PrTitle};
const END_POINT: &str = "https://api.github.com/graphql";
pub struct Client {
#[allow(dead_code)]
pub(crate) git_repo: Repository,
pub(crate) github_rest: GitHubAPI,
pub(crate) github_graphql: gql_client::Client,
pub(crate) github_token: String,
pub(crate) owner: String,
pub(crate) repo: String,
pub(crate) default_branch: String,
pub(crate) branch: Option<String>,
pull_request: Option<PullRequest>,
pub(crate) prlog: OsString,
pub(crate) line_limit: usize,
pub(crate) prlog_parse_options: ChangelogParseOptions,
pub(crate) prlog_update: Option<PrTitle>,
pub(crate) commit_message: String,
}
impl Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("github_graphql", &self.github_graphql)
.field("owner", &self.owner)
.field("repo", &self.repo)
.field("default_branch", &self.default_branch)
.field("branch", &self.branch)
.field("pull_request", &self.pull_request)
.field("prlog", &self.prlog)
.field("line_limit", &self.line_limit)
.field("prlog_parse_options", &self.prlog_parse_options)
.field("prlog_update", &self.prlog_update)
.field("commit_message", &self.commit_message)
.finish()
}
}
impl Client {
pub async fn new_with(settings: &Config) -> Result<Self, Error> {
let cmd = settings
.get::<String>("command")
.map_err(|_| Error::CommandNotSet)?;
log::trace!("cmd: {cmd:?}");
log::trace!("owner: {:?}", settings.get::<String>("username"));
let pcu_owner: String = settings
.get("username")
.map_err(|_| Error::EnvVarBranchNotSet)?;
let owner = env::var(pcu_owner).map_err(|_| Error::EnvVarBranchNotFound)?;
log::trace!("repo: {:?}", settings.get::<String>("reponame"));
let pcu_repo: String = settings
.get("reponame")
.map_err(|_| Error::EnvVarBranchNotSet)?;
let repo = env::var(pcu_repo).map_err(|_| Error::EnvVarBranchNotFound)?;
let default_branch = settings
.get::<String>("default_branch")
.unwrap_or("main".to_string());
let commit_message = settings
.get::<String>("commit_message")
.unwrap_or("".to_string());
let line_limit = settings.get::<usize>("line_limit").unwrap_or(10);
log::trace!("Getting the github api with {settings:#?}, {owner}, {repo}");
let (github_rest, github_graphql, github_token) =
Client::get_github_apis(settings, &owner, &repo).await?;
let git_repo = git2::Repository::open(".")?;
log::trace!("Executing for command: {}", &cmd);
let (branch, pull_request) = if &cmd == "pr" || &cmd == "push" {
log::trace!("branch: {:?}", settings.get::<String>("branch"));
let pcu_branch: String = settings
.get("branch")
.map_err(|_| Error::EnvVarBranchNotSet)?;
let branch = env::var(pcu_branch).map_err(|_| Error::EnvVarBranchNotFound)?;
let branch = if branch.is_empty() {
None
} else {
Some(branch)
};
let from_merge = settings.get::<bool>("from_merge").unwrap_or(false);
let pull_request = if from_merge {
log::info!("Using from_merge mode - looking up PR from HEAD commit");
Some(
PullRequest::from_head_commit(&git_repo, &github_graphql, &owner, &repo)
.await?,
)
} else {
PullRequest::new_pull_request_opt(settings, &github_graphql).await?
};
(branch, pull_request)
} else {
let branch = None;
let pull_request = None;
(branch, pull_request)
};
log::trace!("branch: {branch:?} and pull_request: {pull_request:?}");
log::trace!("log: {:?}", settings.get::<String>("prlog"));
let prlog: String = settings
.get("prlog")
.map_err(|_| Error::DefaultChangeLogNotSet)?;
let prlog = OsString::from(prlog);
let svs_root = settings
.get("dev_platform")
.unwrap_or_else(|_| "https://github.com/".to_string());
let prefix = settings
.get("version_prefix")
.unwrap_or_else(|_| "v".to_string());
let repo_url = Some(format!("{svs_root}{owner}/{repo}"));
let prlog_parse_options = ChangelogParseOptions {
url: repo_url,
head: Some("HEAD".to_string()),
tag_prefix: Some(prefix),
};
Ok(Self {
git_repo,
github_rest,
github_graphql,
github_token,
default_branch,
branch,
owner,
repo,
pull_request,
prlog,
line_limit,
prlog_parse_options,
prlog_update: None,
commit_message,
})
}
async fn get_github_apis(
settings: &Config,
owner: &str,
repo: &str,
) -> Result<(GitHubAPI, gql_client::Client, String), Error> {
let bld_style = Style::new().bold();
log::info!("\n***Get GitHub API instance***\n");
log::trace!("Settings: {settings:#?}");
let (config, token) = match settings.get::<String>("app_id") {
Ok(app_id) => {
log::info!("Using {} for authentication", "GitHub App".style(bld_style));
let private_key = settings
.get::<String>("private_key")
.map_err(|_| Error::NoGitHubAPIPrivateKey)?;
log::trace!("Using private key {private_key:#?} for authentication");
let app_authorization = AppAuthorization::new(app_id, private_key);
let config = APIConfig::with_token(app_authorization).shared();
let api = GitHubAPI::new(&config);
let installation = api
.apps
.get_repo_installation(owner, repo)
.send()
.await
.unwrap();
let installation_token = api
.apps
.create_installation_access_token(installation.id)
.send()
.await
.unwrap();
(
APIConfig::with_token(installation_token.clone()).shared(),
installation_token.token,
)
}
Err(_) => {
let pat = settings
.get::<String>("pat")
.map_err(|_| Error::NoGitHubAPIAuth)?;
log::warn!(
"Falling back to {} for authentication — PAT lacks branch protection bypass authority",
"Personal Access Token".style(bld_style)
);
let personal_access_token = PersonalAccessToken::new(&pat);
(APIConfig::with_token(personal_access_token).shared(), pat)
}
};
let auth = format!("Bearer {token}");
let headers = HashMap::from([
("X-Github-Next-Global-ID", "1"),
("User-Agent", owner),
("Authorization", &auth),
]);
let github_graphql = gql_client::Client::new_with_headers(END_POINT, headers);
let github_rest = GitHubAPI::new(&config);
Ok((github_rest, github_graphql, token))
}
pub fn branch_or_main(&self) -> &str {
self.branch.as_ref().map_or("main", |v| v)
}
pub fn pull_request(&self) -> &str {
if let Some(pr) = self.pull_request.as_ref() {
&pr.pull_request
} else {
""
}
}
pub fn title(&self) -> &str {
if let Some(pr) = self.pull_request.as_ref() {
&pr.title
} else {
""
}
}
pub fn body(&self) -> &str {
if let Some(pr) = self.pull_request.as_ref() {
&pr.body
} else {
""
}
}
pub fn pr_number(&self) -> i64 {
if let Some(pr) = self.pull_request.as_ref() {
pr.pr_number
} else {
0
}
}
pub fn owner(&self) -> &str {
&self.owner
}
pub fn repo(&self) -> &str {
&self.repo
}
pub fn line_limit(&self) -> usize {
self.line_limit
}
pub fn set_title(&mut self, title: &str) {
if let Some(pr) = self.pull_request.as_mut() {
pr.title = title.to_string();
}
}
pub fn is_default_branch(&self) -> bool {
if let Some(branch) = &self.branch {
*branch == self.default_branch
} else {
false
}
}
pub fn section(&self) -> Option<&str> {
if let Some(update) = &self.prlog_update {
if let Some(section) = &update.section {
match section {
ChangeKind::Added => Some("Added"),
ChangeKind::Changed => Some("Changed"),
ChangeKind::Deprecated => Some("Deprecated"),
ChangeKind::Fixed => Some("Fixed"),
ChangeKind::Removed => Some("Removed"),
ChangeKind::Security => Some("Security"),
}
} else {
None
}
} else {
None
}
}
pub fn entry(&self) -> Option<&str> {
if let Some(update) = &self.prlog_update {
Some(&update.entry)
} else {
None
}
}
pub fn prlog_as_str(&self) -> &str {
if let Some(cl) = &self.prlog.to_str() {
cl
} else {
""
}
}
pub fn set_prlog(&mut self, value: &str) {
self.prlog = value.into();
}
}