mod archive;
mod args;
mod assign;
mod cache;
mod checkout;
mod comment;
mod delete;
mod diff;
mod edit;
mod label;
mod list;
mod react;
mod ready;
mod redact;
mod resolve;
mod review;
mod show;
mod update;
use std::collections::BTreeSet;
use anyhow::anyhow;
use radicle::cob::patch::PatchId;
use radicle::cob::{patch, Label};
use radicle::patch::cache::Patches as _;
use radicle::storage::git::transport;
use radicle::{prelude::*, Node};
use crate::git::Rev;
use crate::node;
use crate::terminal as term;
use crate::terminal::patch::Message;
pub use args::Args;
use args::{AssignArgs, Command, CommentAction, LabelArgs};
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
let (workdir, rid) = if let Some(rid) = args.repo {
(None, rid)
} else {
radicle::rad::cwd()
.map(|(workdir, rid)| (Some(workdir), rid))
.map_err(|_| anyhow!("this command must be run in the context of a repository"))?
};
let profile = ctx.profile()?;
let repository = profile.storage.repository(rid)?;
let mut announce = args.should_announce();
let command = args
.command
.unwrap_or_else(|| Command::List(args.empty.into()));
announce &= command.should_announce();
transport::local::register(profile.storage.clone());
match command {
Command::List(args) => {
let mut authors: BTreeSet<Did> = args.authors.iter().cloned().collect();
if args.authored {
authors.insert(profile.did());
}
list::run((&args.state).into(), authors, &repository, &profile)?;
}
Command::Show { id, patch, verbose } => {
let patch_id = id.resolve(&repository.backend)?;
show::run(
&patch_id,
patch,
verbose,
&profile,
&repository,
workdir.as_ref(),
)?;
}
Command::Diff { id, revision } => {
let patch_id = id.resolve(&repository.backend)?;
let revision_id = revision
.map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
.transpose()?
.map(patch::RevisionId::from);
diff::run(&patch_id, revision_id, &repository, &profile)?;
}
Command::Update { id, base, message } => {
let message = Message::from(message);
let patch_id = id.resolve(&repository.backend)?;
let base_id = base
.as_ref()
.map(|base| base.resolve(&repository.backend))
.transpose()?;
let workdir = workdir.ok_or(anyhow!(
"this command must be run from a repository checkout"
))?;
update::run(patch_id, base_id, message, &profile, &repository, &workdir)?;
}
Command::Archive { id, undo } => {
let patch_id = id.resolve::<PatchId>(&repository.backend)?;
archive::run(&patch_id, undo, &profile, &repository)?;
}
Command::Ready { id, undo } => {
let patch_id = id.resolve::<PatchId>(&repository.backend)?;
if !ready::run(&patch_id, undo, &profile, &repository)? {
if undo {
anyhow::bail!("the patch must be open to be put in draft state");
} else {
anyhow::bail!("this patch must be in draft state to be put in open state");
}
}
}
Command::Delete { id } => {
let patch_id = id.resolve::<PatchId>(&repository.backend)?;
delete::run(&patch_id, &profile, &repository)?;
}
Command::Checkout { id, revision, opts } => {
let patch_id = id.resolve::<radicle::git::Oid>(&repository.backend)?;
let revision_id = revision
.map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
.transpose()?
.map(patch::RevisionId::from);
let workdir = workdir.ok_or(anyhow!(
"this command must be run from a repository checkout"
))?;
checkout::run(
&patch::PatchId::from(patch_id),
revision_id,
&repository,
&workdir,
&profile,
opts.into(),
)?;
}
Command::Comment(c) => match CommentAction::from(c) {
CommentAction::Comment {
revision,
message,
reply_to,
} => {
comment::run(
revision,
message,
reply_to,
args.quiet,
&repository,
&profile,
)?;
}
CommentAction::Edit {
revision,
comment,
message,
} => {
let comment = comment.resolve(&repository.backend)?;
comment::edit::run(
revision,
comment,
message,
args.quiet,
&repository,
&profile,
)?;
}
CommentAction::Redact { revision, comment } => {
let comment = comment.resolve(&repository.backend)?;
comment::redact::run(revision, comment, &repository, &profile)?;
}
CommentAction::React {
revision,
comment,
emoji,
undo,
} => {
let comment = comment.resolve(&repository.backend)?;
if undo {
comment::react::run(revision, comment, emoji, false, &repository, &profile)?;
} else {
comment::react::run(revision, comment, emoji, true, &repository, &profile)?;
}
}
},
Command::Review {
id,
revision,
options,
} => {
let patch_id = id.resolve(&repository.backend)?;
let revision_id = revision
.map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
.transpose()?
.map(patch::RevisionId::from);
review::run(patch_id, revision_id, options.into(), &profile, &repository)?;
}
Command::Resolve {
id,
review,
comment,
unresolve,
} => {
let patch = id.resolve(&repository.backend)?;
let review = patch::ReviewId::from(
review.resolve::<radicle::cob::EntryId>(&repository.backend)?,
);
let comment = comment.resolve(&repository.backend)?;
if unresolve {
resolve::unresolve(patch, review, comment, &repository, &profile)?;
term::success!("Unresolved comment {comment}");
} else {
resolve::resolve(patch, review, comment, &repository, &profile)?;
term::success!("Resolved comment {comment}");
}
}
Command::Edit {
id,
revision,
message,
} => {
let message = Message::from(message);
let patch_id = id.resolve(&repository.backend)?;
let revision_id = revision
.map(|id| id.resolve::<radicle::git::Oid>(&repository.backend))
.transpose()?
.map(patch::RevisionId::from);
edit::run(&patch_id, revision_id, message, &profile, &repository)?;
}
Command::Redact { id } => {
redact::run(&id, &profile, &repository)?;
}
Command::Assign {
id,
args: AssignArgs { add, delete },
} => {
let patch_id = id.resolve(&repository.backend)?;
assign::run(
&patch_id,
add.into_iter().collect(),
delete.into_iter().collect(),
&profile,
&repository,
)?;
}
Command::Label {
id,
args: LabelArgs { add, delete },
} => {
let patch_id = id.resolve(&repository.backend)?;
label::run(
&patch_id,
add.into_iter().collect(),
delete.into_iter().collect(),
&profile,
&repository,
)?;
}
Command::Set { id, remote } => {
let patches = term::cob::patches(&profile, &repository)?;
let patch_id = id.resolve(&repository.backend)?;
let patch = patches
.get(&patch_id)?
.ok_or_else(|| anyhow!("patch {patch_id} not found"))?;
let workdir = workdir.ok_or(anyhow!(
"this command must be run from a repository checkout"
))?;
radicle::rad::setup_patch_upstream(
&patch_id,
*patch.head(),
&workdir,
remote.as_ref().unwrap_or(&radicle::rad::REMOTE_NAME),
true,
)?;
}
Command::Cache { id, storage } => {
let mode = if storage {
cache::CacheMode::Storage
} else {
let patch_id = id.map(|id| id.resolve(&repository.backend)).transpose()?;
patch_id.map_or(
cache::CacheMode::Repository {
repository: &repository,
},
|id| cache::CacheMode::Patch {
id,
repository: &repository,
},
)
};
cache::run(mode, &profile)?;
}
Command::React {
id,
emoji: react,
undo,
} => {
if undo {
react::run(&id, react, false, &repository, &profile)?;
} else {
react::run(&id, react, true, &repository, &profile)?;
}
}
}
if announce {
let mut node = Node::new(profile.socket());
node::announce(
&repository,
node::SyncSettings::default(),
node::SyncReporting::default(),
&mut node,
&profile,
)?;
}
Ok(())
}