mod bsky;
mod checkout;
mod commit;
mod label;
mod linkedin;
mod pull_request;
mod push;
mod release;
mod verify_signatures;
use std::{env, fmt::Display, fs};
use bsky::Bsky;
use checkout::Checkout;
use clap::{Parser, Subcommand};
use color_eyre::Result;
use commit::Commit;
use config::Config;
use label::Label;
use linkedin::Linkedin;
use pull_request::Pr;
use push::Push;
use release::Release;
use verify_signatures::VerifySignatures;
use crate::{Client, Error, GitOps, Sign};
const GITHUB_PAT: &str = "GITHUB_TOKEN";
pub enum CIExit {
Updated,
UnChanged,
Committed,
Pushed(String),
Released,
Label(String),
NoLabel,
DraftedForBluesky,
PostedToBluesky,
NoFilesToProcess,
NothingToPush,
SharedToLinkedIn,
NoContentForLinkedIn,
NoBlogPostsForBluesky,
VerificationPassed,
SwitchedBranch(String),
}
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
pub struct Cli {
#[clap(flatten)]
pub logging: clap_verbosity_flag::Verbosity,
#[clap(short, long)]
pub sign: Option<Sign>,
#[clap(long)]
pub no_signoff: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Subcommand, Clone)]
pub enum Commands {
Pr(Pr),
Release(Release),
Commit(Commit),
Push(Push),
#[clap(long_about = "
Apply a label to a pull request.
In default use applies the `rebase` label to the pull request with
the lowest number submitted by the `renovate` or `app/renovate` user")]
Label(Label),
Bsky(Bsky),
Linkedin(Linkedin),
VerifySignatures(VerifySignatures),
Checkout(Checkout),
}
impl Display for Commands {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Commands::Pr(_) => write!(f, "prequest"),
Commands::Release(_) => write!(f, "release"),
Commands::Commit(_) => write!(f, "commit"),
Commands::Push(_) => write!(f, "push"),
Commands::Label(_) => write!(f, "label"),
Commands::Bsky(_) => write!(f, "bluesky"),
Commands::Linkedin(_) => write!(f, "linkedin"),
Commands::VerifySignatures(_) => write!(f, "verify-signatures"),
Commands::Checkout(_) => write!(f, "checkout"),
}
}
}
impl Commands {
async fn get_client(&self) -> Result<Client, Error> {
let settings = self.get_settings()?;
let client = Client::new_with(&settings).await?;
Ok(client)
}
fn get_settings(&self) -> Result<Config, Error> {
let mut settings = Config::builder()
.set_default("prlog", "PRLOG.md")?
.set_default("branch", "CIRCLE_BRANCH")?
.set_default("default_branch", "main")?
.set_default("pull_request", "CIRCLE_PULL_REQUEST")?
.set_default("username", "CIRCLE_PROJECT_USERNAME")?
.set_default("reponame", "CIRCLE_PROJECT_REPONAME")?
.set_default("commit_message", "chore: update prlog")?
.set_default("dev_platform", "https://github.com/")?
.set_default("version_prefix", "v")?
.add_source(config::File::with_name("pcu.toml").required(false))
.add_source(config::Environment::with_prefix("PCU"));
log::trace!("Initial settings (default, pcu.toml and environment: {settings:#?}");
settings = match self {
Commands::Pr(pr) => settings
.set_override("commit_message", "chore: update prlog for pr")?
.set_override("command", "pr")?
.set_override("from_merge", pr.from_merge)?,
Commands::Release(_) => settings
.set_override("commit_message", "chore: update prlog for release")?
.set_override("command", "release")?,
Commands::Commit(_) => settings
.set_override("commit_message", "chore: adding changed files")?
.set_override("command", "commit")?,
Commands::Push(_) => settings
.set_override("commit_message", "chore: update prlog for release")?
.set_override("command", "push")?,
Commands::Label(_) => settings
.set_override("commit_message", "chore: update prlog for release")?
.set_override("command", "label")?,
Commands::Bsky(bsky) => settings
.set_override("commit_message", "chore: add Bluesky posts to repository")?
.set_override("store", bsky.store.clone())?
.set_override("command", "bsky")?,
Commands::Linkedin(_) => settings
.set_override("commit_message", "chore: announce release on LinkedIn")?
.set_override("command", "linkedin")?,
Commands::VerifySignatures(_) => {
settings.set_override("command", "verify-signatures")?
}
Commands::Checkout(_) => settings.set_override("command", "checkout")?,
};
settings = if let Commands::Bsky(bsky) = self {
if let Some(_owner) = &bsky.owner {
settings.set_override("username", "OWNER")?
} else {
settings
}
} else {
settings
};
settings = if let Commands::Bsky(bsky) = self {
if let Some(_repo) = &bsky.repo {
settings.set_override("reponame", "REPO")?
} else {
settings
}
} else {
settings
};
settings = if let Commands::Bsky(bsky) = self {
if let Some(_branch) = &bsky.branch {
settings.set_override("branch", "BRANCH")?
} else {
settings
}
} else {
settings
};
settings = if let Ok(pat) = env::var(GITHUB_PAT) {
settings.set_override("pat", pat.to_string())?
} else {
settings
};
match settings.build() {
Ok(settings) => Ok(settings),
Err(e) => {
log::error!("Error: {e}");
Err(e.into())
}
}
}
}
fn print_prlog(prlog_path: &str, mut line_limit: usize) -> String {
let mut output = String::new();
if let Ok(change_log) = fs::read_to_string(prlog_path) {
let mut line_count = 0;
if line_limit == 0 {
line_limit = change_log.lines().count();
};
output.push_str("\n*****Changelog*****:\n----------------------------");
for line in change_log.lines() {
output.push_str(format!("{line}\n").as_str());
line_count += 1;
if line_count >= line_limit {
break;
}
}
output.push_str("----------------------------\n");
};
output
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SignConfig;
use clap::Parser;
#[test]
fn test_cli_default_signoff_enabled() {
let args =
Cli::try_parse_from(["pcu", "commit", "--commit-message", "test message"]).unwrap();
assert!(
!args.no_signoff,
"Default should have signoff enabled (no_signoff=false)"
);
let sign = args.sign.unwrap_or_default();
let sign_config = SignConfig::with_signoff(sign, !args.no_signoff);
assert!(
sign_config.is_signoff_enabled(),
"SignConfig should have signoff enabled by default"
);
}
#[test]
fn test_cli_no_signoff_flag() {
let args = Cli::try_parse_from([
"pcu",
"--no-signoff",
"commit",
"--commit-message",
"test message",
])
.unwrap();
assert!(
args.no_signoff,
"--no-signoff flag should set no_signoff to true"
);
let sign = args.sign.unwrap_or_default();
let sign_config = SignConfig::with_signoff(sign, !args.no_signoff);
assert!(
!sign_config.is_signoff_enabled(),
"SignConfig should have signoff disabled with --no-signoff"
);
}
#[test]
fn test_cli_no_signoff_with_explicit_sign() {
let args = Cli::try_parse_from([
"pcu",
"--sign",
"none",
"--no-signoff",
"commit",
"--commit-message",
"test",
])
.unwrap();
assert!(args.no_signoff);
let sign = args.sign.unwrap_or_default();
let sign_config = SignConfig::with_signoff(sign, !args.no_signoff);
assert_eq!(
sign_config.sign,
Sign::None,
"Should use Sign::None variant"
);
assert!(
!sign_config.is_signoff_enabled(),
"signoff should be disabled"
);
}
}