use radicle::{cob::patch::PatchId, git::BranchName, storage::git::Repository};
use radicle_ci_broker::{
ergo,
msg::RequestBuilder,
refs::{branch_ref, ref_string},
};
use super::*;
/// Trigger a CI run.
#[derive(Debug, Parser)]
pub struct TriggerCmd {
/// Set the node where the node originated from.
#[clap(long)]
node: Option<NodeId>,
/// Trigger CI to run for all repositories on the local node.
#[clap(long, required_unless_present_any = ["patch","commit"])]
all: bool,
/// Set the repository the event refers to. Can be a RID, or the
/// repository name.
#[clap(long, required_unless_present = "all")]
repo: Option<String>,
/// Set the name of the ref the event refers to.
#[clap(long = "ref", aliases = ["name"], default_value = "main")]
name: Option<String>,
/// Set the commit the event refers to. Can be the SHA1 commit id,
/// or a symbolic Git revision, as understood by `git rev-parse`.
/// For example, `HEAD`.
#[clap(long, required_unless_present_any = ["patch","all"])]
commit: Option<String>,
/// Trigger CI to run on this patch.
#[clap(long, required_unless_present_any = ["commit","all"])]
patch: Option<PatchId>,
/// Write the event ID to this file, after adding the event to the
/// queue.
#[clap(long)]
id_file: Option<PathBuf>,
/// Output a trigger message for a CI adapter, instead of an event
/// that `cib` will turn into a trigger message. Output is always to
/// stdout. There will be one message per line in the output.
#[clap(long)]
message: bool,
/// Output the event to trigger a CI run to the standard output,
/// instead of adding to the event queue in the database.
#[clap(long, conflicts_with = "all")]
stdout: bool,
/// Output the event to trigger a CI run to a named file, instead
/// of adding to the event queue in the database.
#[clap(long, conflicts_with = "all")]
output: Option<PathBuf>,
}
impl Leaf for TriggerCmd {
fn run(&self, args: &Args) -> Result<(), CibToolError> {
let r = ergo::Radicle::new().map_err(TriggerError::Ergonomic)?;
if self.all {
for ri in r.repositories().map_err(TriggerError::Ergonomic)? {
let repo = r.repository(&ri.rid).map_err(TriggerError::Ergonomic)?;
self.trigger(args, &r, &repo)?;
}
} else if let Some(repo) = &self.repo {
let repo = r
.repository_by_name(repo)
.map_err(TriggerError::Ergonomic)?;
self.trigger(args, &r, &repo)?;
} else {
Err(TriggerError::NoRepo)?
}
Ok(())
}
}
impl TriggerCmd {
fn oid(&self, r: &ergo::Radicle, repo: &Repository) -> Result<Oid, TriggerError> {
let oid = if let Some(commit) = &self.commit {
r.resolve_commit(&repo.id, commit)?
} else if let Some(wanted) = &self.patch {
*r.patch(&repo.id, wanted)?.head()
} else {
r.resolve_commit(&repo.id, "HEAD")?
};
Ok(oid)
}
fn branch(&self, r: &ergo::Radicle, repo: &Repository) -> Result<BranchName, TriggerError> {
if let Some(name) = &self.name {
Ok(branch_ref(&ref_string(name)?)?)
} else {
let project = r.project(&repo.id).map_err(TriggerError::Ergonomic)?;
Ok(project.default_branch().clone())
}
}
fn trigger(
&self,
args: &Args,
r: &ergo::Radicle,
repo: &Repository,
) -> Result<(), TriggerError> {
let profile = r.profile();
let nid = self.node.unwrap_or(*profile.id());
let oid = self.oid(r, repo)?;
let base = r
.resolve_commit(&repo.id, &format!("{oid}^"))
.unwrap_or(oid);
let branch_name = self.branch(r, repo)?;
let event = CiEvent::branch_updated(nid, repo.id, &branch_name, oid, base)
.map_err(TriggerError::CiEvent)?;
if self.message {
self.trigger_message(event, r)
} else {
self.trigger_event(event, args)
}
}
fn trigger_message(&self, event: CiEvent, r: &ergo::Radicle) -> Result<(), TriggerError> {
let msg = RequestBuilder::default()
.profile(r.profile())
.ci_event(&event)
.build_trigger_from_ci_event()
.map_err(TriggerError::TriggerMessage)?;
let json = serde_json::to_string(&msg).map_err(TriggerError::MessageToJson)?;
println!("{json}");
Ok(())
}
fn trigger_event(&self, event: CiEvent, args: &Args) -> Result<(), TriggerError> {
if self.stdout {
let json = event.to_pretty_json().map_err(TriggerError::EventToJson2)?;
println!("{json}");
} else if let Some(filename) = &self.output {
let json = event.to_pretty_json().map_err(TriggerError::EventToJson2)?;
std::fs::write(filename, &json)
.map_err(|err| TriggerError::Write(filename.into(), err))?;
} else {
let db = args
.open_db()
.map_err(|err| TriggerError::OpenDb(Box::new(err)))?;
let id = db.push_queued_ci_event(event)?;
println!("{id}");
if let Some(filename) = &self.id_file {
write(filename, id.to_string().as_bytes())
.map_err(|e| TriggerError::WriteEventId(filename.into(), e))?;
}
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum TriggerError {
#[error(transparent)]
Ergonomic(#[from] radicle_ci_broker::ergo::ErgoError),
#[error(transparent)]
RefError(#[from] radicle_ci_broker::refs::RefError),
#[error("repository must be given with --repo")]
NoRepo,
#[error("failed to construct a CI event")]
CiEvent(#[source] CiEventError),
#[error("failed to convert trigger message to JSON")]
MessageToJson(#[source] serde_json::Error),
#[error(transparent)]
EventToJson2(#[from] CiEventError),
#[error(transparent)]
Db(#[from] DbError),
#[error("failed to write file: {0}")]
Write(PathBuf, #[source] std::io::Error),
#[error("failed to write event ID to file {0}")]
WriteEventId(PathBuf, #[source] std::io::Error),
#[error("failed to get database")]
OpenDb(#[source] Box<CibToolError>),
#[error("failed to create a trigger message")]
TriggerMessage(#[source] radicle_ci_broker::msg::MessageError),
}