cli: Allow git revision parameters as COB IDs

This commit allows any git revision to be used instead
of the full SHA-1, when specifying COBs.

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
This commit is contained in:
Vincenzo Palazzo 2023-03-22 14:35:17 +01:00 committed by Alexis Sellier
parent f2d31aa9c0
commit 654a21b2b3
No known key found for this signature in database
12 changed files with 142 additions and 109 deletions

View File

@ -145,7 +145,7 @@ increased to `1`.
Instead, let's accept the proposal: Instead, let's accept the proposal:
``` ```
$ rad id accept 0d396a83a5e1dda2b8929f7dc401d19dd1a79fb8 --no-confirm $ rad id accept 0d396a --no-confirm
✓ Accepted proposal ✓ ✓ Accepted proposal ✓
title: Add Bob title: Add Bob
description: Add Bob as a delegate description: Add Bob as a delegate

View File

@ -20,6 +20,19 @@ $ rad issue list
2e8c1bf3fe0532a314778357c886608a966a34bd "flux capacitor underpowered" unassigned 2e8c1bf3fe0532a314778357c886608a966a34bd "flux capacitor underpowered" unassigned
``` ```
Show the issue information issue.
```
$ rad issue show 2e8c1bf
title: flux capacitor underpowered
state: open
tags: []
assignees: []
Flux capacitor power requirements exceed current supply
```
Great! Now we've documented the issue for ourselves and others. Great! Now we've documented the issue for ourselves and others.
Just like with other project management systems, the issue can be assigned to Just like with other project management systems, the issue can be assigned to

View File

@ -111,7 +111,7 @@ $ rad comment 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 --message 'I cannot wait
Now, let's checkout the patch that we just created: Now, let's checkout the patch that we just created:
``` ```
$ rad patch checkout 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 $ rad patch checkout 191a14e52
✓ Switched to branch patch/191a14e520f ✓ Switched to branch patch/191a14e520f
``` ```

View File

@ -1,12 +1,12 @@
use std::ffi::OsString; use std::ffi::OsString;
use std::str::FromStr;
use anyhow::anyhow; use anyhow::anyhow;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use radicle::prelude::Did; use radicle::prelude::Did;
use crate::git::Rev;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{string, Args, Error, Help};
use radicle::cob; use radicle::cob;
use radicle::cob::issue; use radicle::cob::issue;
use radicle::storage::WriteStorage; use radicle::storage::WriteStorage;
@ -32,7 +32,7 @@ Options
#[derive(Debug)] #[derive(Debug)]
pub struct Options { pub struct Options {
pub id: issue::IssueId, pub id: Rev,
pub to: NonEmpty<Did>, pub to: NonEmpty<Did>,
} }
@ -41,7 +41,7 @@ impl Args for Options {
use lexopt::prelude::*; use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut id: Option<issue::IssueId> = None; let mut id: Option<Rev> = None;
let mut to: Vec<Did> = Vec::new(); let mut to: Vec<Did> = Vec::new();
while let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
@ -56,11 +56,8 @@ impl Args for Options {
to.push(did); to.push(did);
} }
Value(ref val) if id.is_none() => { Value(ref val) if id.is_none() => {
let val = val.to_string_lossy(); let val = string(val);
let Ok(val) = issue::IssueId::from_str(&val) else { id = Some(Rev::from(val));
return Err(anyhow!("invalid Issue ID '{}'", val));
};
id = Some(val);
} }
_ => { _ => {
return Err(anyhow!(arg.unexpected())); return Err(anyhow!(arg.unexpected()));
@ -83,14 +80,14 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let (_, id) = radicle::rad::cwd()?; let (_, id) = radicle::rad::cwd()?;
let repo = profile.storage.repository_mut(id)?; let repo = profile.storage.repository_mut(id)?;
let oid = options.id.resolve(&repo.backend)?;
let mut issues = issue::Issues::open(&repo)?; let mut issues = issue::Issues::open(&repo)?;
let mut issue = issues.get_mut(&options.id).map_err(|e| match e { let mut issue = issues.get_mut(&oid).map_err(|e| match e {
cob::store::Error::NotFound(_, _) => anyhow!("issue {} not found", options.id), cob::store::Error::NotFound(_, _) => anyhow!("issue {} not found", options.id),
_ => e.into(), _ => e.into(),
})?; })?;
let signer = term::signer(&profile)?; let signer = term::signer(&profile)?;
issue.assign(options.to.into_iter().map(Did::into), &signer)?; issue.assign(options.to.into_iter().map(Did::into), &signer)?;
Ok(()) Ok(())
} }

View File

@ -1,9 +1,7 @@
use std::ffi::OsString; use std::ffi::OsString;
use std::str::FromStr;
use anyhow::anyhow; use anyhow::anyhow;
use radicle::cob;
use radicle::cob::issue::Issues; use radicle::cob::issue::Issues;
use radicle::cob::patch::Patches; use radicle::cob::patch::Patches;
use radicle::cob::store; use radicle::cob::store;
@ -11,7 +9,9 @@ use radicle::cob::thread;
use radicle::prelude::*; use radicle::prelude::*;
use radicle::storage; use radicle::storage;
use crate::git::Rev;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::string;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{Args, Error, Help};
use crate::terminal::patch::Message; use crate::terminal::patch::Message;
@ -34,25 +34,17 @@ Options
#[derive(Debug)] #[derive(Debug)]
pub struct Options { pub struct Options {
pub id: cob::ObjectId, pub id: Rev,
pub message: Message, pub message: Message,
pub reply_to: Option<thread::CommentId>, pub reply_to: Option<thread::CommentId>,
} }
#[inline]
fn parse_cob_id(val: OsString) -> anyhow::Result<cob::ObjectId> {
let val = val
.to_str()
.ok_or_else(|| anyhow!("object id specified is not UTF-8"))?;
cob::ObjectId::from_str(val).map_err(|_| anyhow!("invalid object id '{}'", val))
}
impl Args for Options { impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> { fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*; use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut id: Option<cob::ObjectId> = None; let mut id: Option<Rev> = None;
let mut message = Message::default(); let mut message = Message::default();
let mut reply_to = None; let mut reply_to = None;
@ -76,7 +68,10 @@ impl Args for Options {
// Common. // Common.
Long("help") => return Err(Error::Help.into()), Long("help") => return Err(Error::Help.into()),
Value(val) if id.is_none() => id = Some(parse_cob_id(val)?), Value(val) if id.is_none() => {
let val = string(&val);
id = Some(Rev::from(val));
}
_ => return Err(anyhow::anyhow!(arg.unexpected())), _ => return Err(anyhow::anyhow!(arg.unexpected())),
} }
} }
@ -103,7 +98,8 @@ fn comment(
} }
let mut issues = Issues::open(repo)?; let mut issues = Issues::open(repo)?;
match issues.get_mut(&options.id) { let id = options.id.resolve(&repo.backend)?;
match issues.get_mut(&id) {
Ok(mut issue) => { Ok(mut issue) => {
let comment_id = options.reply_to.unwrap_or_else(|| { let comment_id = options.reply_to.unwrap_or_else(|| {
let (comment_id, _) = issue.comments().next().expect("root comment always exists"); let (comment_id, _) = issue.comments().next().expect("root comment always exists");
@ -119,7 +115,7 @@ fn comment(
} }
let mut patches = Patches::open(repo)?; let mut patches = Patches::open(repo)?;
match patches.get_mut(&options.id) { match patches.get_mut(&id) {
Ok(mut patch) => { Ok(mut patch) => {
let (revision_id, _) = patch.revisions().last().expect("patch has a revision"); let (revision_id, _) = patch.revisions().last().expect("patch has a revision");
let comment_id = patch.comment(*revision_id, message, options.reply_to, &signer)?; let comment_id = patch.comment(*revision_id, message, options.reply_to, &signer)?;

View File

@ -1,15 +1,16 @@
use std::{ffi::OsString, str::FromStr as _}; use std::{ffi::OsString, str::FromStr as _};
use anyhow::{anyhow, Context as _}; use anyhow::{anyhow, Context as _};
use radicle::cob::identity::{self, Proposal, ProposalId, Proposals, Revision, RevisionId}; use radicle::cob::identity::{self, Proposal, Proposals, Revision, RevisionId};
use radicle::git::Oid; use radicle::git::Oid;
use radicle::identity::Identity; use radicle::identity::Identity;
use radicle::prelude::{Did, Doc}; use radicle::prelude::{Did, Doc};
use radicle::storage::ReadStorage as _; use radicle::storage::ReadStorage as _;
use radicle_crypto::Verified; use radicle_crypto::Verified;
use crate::git::Rev;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{string, Args, Error, Help};
use crate::terminal::Element; use crate::terminal::Element;
use crate::terminal::Interactive; use crate::terminal::Interactive;
@ -53,11 +54,11 @@ impl Metadata {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub enum Operation { pub enum Operation {
Accept { Accept {
id: ProposalId, id: Rev,
rev: Option<RevisionId>, rev: Option<RevisionId>,
}, },
Reject { Reject {
id: ProposalId, id: Rev,
rev: Option<RevisionId>, rev: Option<RevisionId>,
}, },
Edit { Edit {
@ -67,7 +68,7 @@ pub enum Operation {
threshold: Option<usize>, threshold: Option<usize>,
}, },
Update { Update {
id: ProposalId, id: Rev,
rev: Option<RevisionId>, rev: Option<RevisionId>,
title: Option<String>, title: Option<String>,
description: Option<String>, description: Option<String>,
@ -75,22 +76,22 @@ pub enum Operation {
threshold: Option<usize>, threshold: Option<usize>,
}, },
Rebase { Rebase {
id: ProposalId, id: Rev,
rev: Option<RevisionId>, rev: Option<RevisionId>,
}, },
Show { Show {
id: ProposalId, id: Rev,
rev: Option<RevisionId>, rev: Option<RevisionId>,
show_revisions: bool, show_revisions: bool,
}, },
#[default] #[default]
List, List,
Commit { Commit {
id: ProposalId, id: Rev,
rev: Option<RevisionId>, rev: Option<RevisionId>,
}, },
Close { Close {
id: ProposalId, id: Rev,
}, },
} }
@ -119,7 +120,7 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut op: Option<OperationName> = None; let mut op: Option<OperationName> = None;
let mut id: Option<ProposalId> = None; let mut id: Option<Rev> = None;
let mut rev: Option<RevisionId> = None; let mut rev: Option<RevisionId> = None;
let mut title: Option<String> = None; let mut title: Option<String> = None;
let mut description: Option<String> = None; let mut description: Option<String> = None;
@ -173,14 +174,8 @@ impl Args for Options {
show_revisions = true; show_revisions = true;
} }
Value(val) if op.is_some() => { Value(val) if op.is_some() => {
let val = val let val = string(&val);
.to_str() id = Some(Rev::from(val));
.ok_or_else(|| anyhow!("proposal id specified is not UTF-8"))?;
id = Some(
ProposalId::from_str(val)
.map_err(|_| anyhow!("invalid proposal id '{}'", val))?,
);
} }
_ => { _ => {
return Err(anyhow!(arg.unexpected())); return Err(anyhow!(arg.unexpected()));
@ -245,6 +240,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let interactive = &options.interactive; let interactive = &options.interactive;
match options.op { match options.op {
Operation::Accept { id, rev } => { Operation::Accept { id, rev } => {
let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?; let mut proposal = proposals.get_mut(&id)?;
let (rid, revision) = select(&proposal, rev, &previous, interactive)?; let (rid, revision) = select(&proposal, rev, &previous, interactive)?;
warn_out_of_date(revision, &previous); warn_out_of_date(revision, &previous);
@ -257,6 +253,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
} }
} }
Operation::Reject { id, rev } => { Operation::Reject { id, rev } => {
let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?; let mut proposal = proposals.get_mut(&id)?;
let (rid, revision) = select(&proposal, rev, &previous, interactive)?; let (rid, revision) = select(&proposal, rev, &previous, interactive)?;
warn_out_of_date(revision, &previous); warn_out_of_date(revision, &previous);
@ -311,6 +308,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
delegates, delegates,
threshold, threshold,
} => { } => {
let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?; let mut proposal = proposals.get_mut(&id)?;
let (_, revision) = select(&proposal, rev, &previous, interactive)?; let (_, revision) = select(&proposal, rev, &previous, interactive)?;
@ -354,6 +352,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
} }
} }
Operation::Rebase { id, rev } => { Operation::Rebase { id, rev } => {
let id = id.resolve(&repo.backend)?;
// TODO: it would be nice if rebasing also handled fast-forwards nicely. // TODO: it would be nice if rebasing also handled fast-forwards nicely.
let mut proposal = proposals.get_mut(&id)?; let mut proposal = proposals.get_mut(&id)?;
let (_, revision) = select(&proposal, rev, &previous, interactive)?; let (_, revision) = select(&proposal, rev, &previous, interactive)?;
@ -407,6 +406,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
t.print(); t.print();
} }
Operation::Commit { id, rev } => { Operation::Commit { id, rev } => {
let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?; let mut proposal = proposals.get_mut(&id)?;
let (rid, revision) = commit_select(&proposal, rev, &previous, interactive)?; let (rid, revision) = commit_select(&proposal, rev, &previous, interactive)?;
warn_out_of_date(revision, &previous); warn_out_of_date(revision, &previous);
@ -419,6 +419,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
} }
} }
Operation::Close { id } => { Operation::Close { id } => {
let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?; let mut proposal = proposals.get_mut(&id)?;
let yes = confirm(interactive, "Are you sure you want to close?"); let yes = confirm(interactive, "Are you sure you want to close?");
if yes { if yes {
@ -432,6 +433,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
rev, rev,
show_revisions, show_revisions,
} => { } => {
let id = id.resolve(&repo.backend)?;
let proposal = proposals let proposal = proposals
.get(&id)? .get(&id)?
.context("No proposal with the given ID exists")?; .context("No proposal with the given ID exists")?;

View File

@ -6,13 +6,14 @@ use anyhow::{anyhow, Context as _};
use radicle::node::Handle; use radicle::node::Handle;
use radicle::prelude::Did; use radicle::prelude::Did;
use crate::git::Rev;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{string, Args, Error, Help};
use crate::terminal::Element; use crate::terminal::Element;
use radicle::cob::common::{Reaction, Tag}; use radicle::cob::common::{Reaction, Tag};
use radicle::cob::issue; use radicle::cob::issue;
use radicle::cob::issue::{CloseReason, IssueId, Issues, State}; use radicle::cob::issue::{CloseReason, Issues, State};
use radicle::storage::WriteStorage; use radicle::storage::WriteStorage;
use radicle::{cob, Node}; use radicle::{cob, Node};
@ -71,17 +72,17 @@ pub enum Operation {
description: Option<String>, description: Option<String>,
}, },
Show { Show {
id: IssueId, id: Rev,
}, },
State { State {
id: IssueId, id: Rev,
state: State, state: State,
}, },
Delete { Delete {
id: IssueId, id: Rev,
}, },
React { React {
id: IssueId, id: Rev,
reaction: Reaction, reaction: Reaction,
}, },
List { List {
@ -101,7 +102,7 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut op: Option<OperationName> = None; let mut op: Option<OperationName> = None;
let mut id: Option<IssueId> = None; let mut id: Option<Rev> = None;
let mut assigned: Option<Assigned> = None; let mut assigned: Option<Assigned> = None;
let mut title: Option<String> = None; let mut title: Option<String> = None;
let mut reaction: Option<Reaction> = None; let mut reaction: Option<Reaction> = None;
@ -161,14 +162,8 @@ impl Args for Options {
unknown => anyhow::bail!("unknown operation '{}'", unknown), unknown => anyhow::bail!("unknown operation '{}'", unknown),
}, },
Value(val) if op.is_some() => { Value(val) if op.is_some() => {
let val = val let val = string(&val);
.to_str() id = Some(Rev::from(val));
.ok_or_else(|| anyhow!("issue id specified is not UTF-8"))?;
id = Some(
IssueId::from_str(val)
.map_err(|_| anyhow!("invalid issue id '{}'", val))?,
);
} }
_ => { _ => {
return Err(anyhow!(arg.unexpected())); return Err(anyhow!(arg.unexpected()));
@ -225,16 +220,19 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
show_issue(&issue)?; show_issue(&issue)?;
} }
Operation::Show { id } => { Operation::Show { id } => {
let id = id.resolve(&repo.backend)?;
let issue = issues let issue = issues
.get(&id)? .get(&id)?
.context("No issue with the given ID exists")?; .context("No issue with the given ID exists")?;
show_issue(&issue)?; show_issue(&issue)?;
} }
Operation::State { id, state } => { Operation::State { id, state } => {
let id = id.resolve(&repo.backend)?;
let mut issue = issues.get_mut(&id)?; let mut issue = issues.get_mut(&id)?;
issue.lifecycle(state, &signer)?; issue.lifecycle(state, &signer)?;
} }
Operation::React { id, reaction } => { Operation::React { id, reaction } => {
let id = id.resolve(&repo.backend)?;
if let Ok(mut issue) = issues.get_mut(&id) { if let Ok(mut issue) = issues.get_mut(&id) {
let (comment_id, _) = term::io::comment_select(&issue).unwrap(); let (comment_id, _) = term::io::comment_select(&issue).unwrap();
issue.react(*comment_id, reaction, &signer)?; issue.react(*comment_id, reaction, &signer)?;
@ -325,6 +323,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
t.print(); t.print();
} }
Operation::Delete { id } => { Operation::Delete { id } => {
let id = id.resolve(&repo.backend)?;
issues.remove(&id, &signer)?; issues.remove(&id, &signer)?;
} }
} }

View File

@ -5,8 +5,9 @@ use std::str::FromStr;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use crate::git::Rev;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{string, Args, Error, Help};
use radicle::cob::patch::RevisionIx; use radicle::cob::patch::RevisionIx;
use radicle::cob::patch::{Patch, PatchId, Patches}; use radicle::cob::patch::{Patch, PatchId, Patches};
use radicle::git; use radicle::git;
@ -72,7 +73,7 @@ pub enum State {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Options { pub struct Options {
pub id: PatchId, pub id: Rev,
pub interactive: bool, pub interactive: bool,
pub revision: Option<RevisionIx>, pub revision: Option<RevisionIx>,
} }
@ -82,7 +83,7 @@ impl Args for Options {
use lexopt::prelude::*; use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut id: Option<PatchId> = None; let mut id: Option<Rev> = None;
let mut revision: Option<RevisionIx> = None; let mut revision: Option<RevisionIx> = None;
let mut interactive = false; let mut interactive = false;
@ -103,14 +104,8 @@ impl Args for Options {
revision = Some(id); revision = Some(id);
} }
Value(val) => { Value(val) => {
let val = val let val = string(&val);
.to_str() id = Some(Rev::from(val));
.ok_or_else(|| anyhow!("patch id specified is not UTF-8"))?;
id = Some(
PatchId::from_str(val)
.map_err(|_| anyhow!("invalid patch id '{}'", val))?,
);
} }
_ => return Err(anyhow::anyhow!(arg.unexpected())), _ => return Err(anyhow::anyhow!(arg.unexpected())),
} }
@ -149,10 +144,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
// //
// Get patch information // Get patch information
// //
let patch_id = options.id; let patch_id = options.id.resolve(&repository.backend)?;
let mut patch = patches let mut patch = patches
.get_mut(&patch_id) .get_mut(&patch_id)
.map_err(|e| anyhow!("couldn't find patch {} locally: {e}", &options.id))?; .map_err(|e| anyhow!("couldn't find patch {} locally: {e}", &options.id.clone()))?;
let head = repo.head()?; let head = repo.head()?;
let branch = head let branch = head

View File

@ -21,8 +21,9 @@ use radicle::cob::patch::PatchId;
use radicle::{prelude::*, Node}; use radicle::{prelude::*, Node};
use crate::commands::rad_fetch as fetch; use crate::commands::rad_fetch as fetch;
use crate::git::Rev;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{string, Args, Error, Help};
use crate::terminal::patch::Message; use crate::terminal::patch::Message;
pub const HELP: Help = Help { pub const HELP: Help = Help {
@ -70,17 +71,17 @@ pub enum Operation {
message: Message, message: Message,
}, },
Show { Show {
patch_id: PatchId, patch_id: Rev,
}, },
Update { Update {
patch_id: Option<PatchId>, patch_id: Option<Rev>,
message: Message, message: Message,
}, },
Delete { Delete {
patch_id: PatchId, patch_id: Rev,
}, },
Checkout { Checkout {
patch_id: PatchId, patch_id: Rev,
}, },
List, List,
} }
@ -113,7 +114,7 @@ impl Args for Options {
Long("message") | Short('m') => { Long("message") | Short('m') => {
if message != Message::Blank { if message != Message::Blank {
// We skip this code when `no-message` is specified. // We skip this code when `no-message` is specified.
let txt: String = parser.value()?.to_string_lossy().into(); let txt: String = term::args::string(&parser.value()?);
message.append(&txt); message.append(&txt);
} }
} }
@ -156,17 +157,11 @@ impl Args for Options {
"c" | "checkout" => op = Some(OperationName::Checkout), "c" | "checkout" => op = Some(OperationName::Checkout),
unknown => anyhow::bail!("unknown operation '{}'", unknown), unknown => anyhow::bail!("unknown operation '{}'", unknown),
}, },
Value(val) if op == Some(OperationName::Show) && patch_id.is_none() => { Value(val)
patch_id = Some(term::cob::parse_patch_id(val)?); if op.is_some() && op != Some(OperationName::List) && patch_id.is_none() =>
} {
Value(val) if op == Some(OperationName::Update) && patch_id.is_none() => { let val = string(&val);
patch_id = Some(term::cob::parse_patch_id(val)?); patch_id = Some(Rev::from(val));
}
Value(val) if op == Some(OperationName::Checkout) && patch_id.is_none() => {
patch_id = Some(term::cob::parse_patch_id(val)?);
}
Value(val) if op == Some(OperationName::Delete) && patch_id.is_none() => {
patch_id = Some(term::cob::parse_patch_id(val)?);
} }
_ => return Err(anyhow::anyhow!(arg.unexpected())), _ => return Err(anyhow::anyhow!(arg.unexpected())),
} }
@ -218,13 +213,18 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Operation::List => { Operation::List => {
list::run(&repository, &profile, Some(workdir))?; list::run(&repository, &profile, Some(workdir))?;
} }
Operation::Show { ref patch_id } => { Operation::Show { patch_id } => {
show::run(&repository, &workdir, patch_id)?; let patch_id = patch_id.resolve(&repository.backend)?;
show::run(&repository, &workdir, &patch_id)?;
} }
Operation::Update { Operation::Update {
patch_id, ref patch_id,
ref message, ref message,
} => { } => {
let patch_id = patch_id
.as_ref()
.map(|id| id.resolve(&repository.backend))
.transpose()?;
update::run( update::run(
&repository, &repository,
&profile, &profile,
@ -235,10 +235,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
)?; )?;
} }
Operation::Delete { patch_id } => { Operation::Delete { patch_id } => {
let patch_id = patch_id.resolve(&repository.backend)?;
delete::run(&repository, &profile, &patch_id)?; delete::run(&repository, &profile, &patch_id)?;
} }
Operation::Checkout { ref patch_id } => { Operation::Checkout { patch_id } => {
checkout::run(&repository, &workdir, patch_id)?; let patch_id = patch_id.resolve(&repository.backend)?;
checkout::run(&repository, &workdir, &patch_id)?;
} }
} }
Ok(()) Ok(())

View File

@ -3,12 +3,13 @@ use std::str::FromStr;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use radicle::cob::patch::{PatchId, Patches, RevisionIx, Verdict}; use radicle::cob::patch::{Patches, RevisionIx, Verdict};
use radicle::prelude::*; use radicle::prelude::*;
use radicle::rad; use radicle::rad;
use crate::git::Rev;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{string, Args, Error, Help};
use crate::terminal::patch::Message; use crate::terminal::patch::Message;
pub const HELP: Help = Help { pub const HELP: Help = Help {
@ -46,7 +47,7 @@ Markdown supported.
#[derive(Debug)] #[derive(Debug)]
pub struct Options { pub struct Options {
pub id: PatchId, pub id: Rev,
pub revision: Option<RevisionIx>, pub revision: Option<RevisionIx>,
pub message: Message, pub message: Message,
pub sync: bool, pub sync: bool,
@ -60,7 +61,7 @@ impl Args for Options {
use lexopt::prelude::*; use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut id: Option<PatchId> = None; let mut id: Option<Rev> = None;
let mut revision: Option<RevisionIx> = None; let mut revision: Option<RevisionIx> = None;
let mut message = Message::default(); let mut message = Message::default();
let mut sync = true; let mut sync = true;
@ -109,14 +110,7 @@ impl Args for Options {
verdict = Some(Verdict::Reject); verdict = Some(Verdict::Reject);
} }
Value(val) => { Value(val) => {
let val = val id = Some(Rev::from(string(&val)));
.to_str()
.ok_or_else(|| anyhow!("patch id specified is not UTF-8"))?;
id = Some(
PatchId::from_str(val)
.map_err(|_| anyhow!("invalid patch id '{}'", val))?,
);
} }
_ => return Err(anyhow::anyhow!(arg.unexpected())), _ => return Err(anyhow::anyhow!(arg.unexpected())),
} }
@ -148,7 +142,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
.context(format!("couldn't load project {id} from local state"))?; .context(format!("couldn't load project {id} from local state"))?;
let mut patches = Patches::open(&repository)?; let mut patches = Patches::open(&repository)?;
let patch_id = options.id; let patch_id = options.id.resolve(&repository.backend)?;
let mut patch = patches let mut patch = patches
.get_mut(&patch_id) .get_mut(&patch_id)
.context(format!("couldn't find patch {patch_id} locally"))?; .context(format!("couldn't find patch {patch_id} locally"))?;

View File

@ -1,5 +1,6 @@
//! Git-related functions and types. //! Git-related functions and types.
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt::Display;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io; use std::io;
use std::io::Write; use std::io::Write;
@ -10,6 +11,7 @@ use std::str::FromStr;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::Context as _; use anyhow::Context as _;
use radicle::cob::ObjectId;
use radicle::crypto::ssh; use radicle::crypto::ssh;
use radicle::git; use radicle::git;
use radicle::git::raw as git2; use radicle::git::raw as git2;
@ -27,6 +29,35 @@ pub const CONFIG_GPG_FORMAT: &str = "gpg.format";
pub const CONFIG_GPG_SSH_PROGRAM: &str = "gpg.ssh.program"; pub const CONFIG_GPG_SSH_PROGRAM: &str = "gpg.ssh.program";
pub const CONFIG_GPG_SSH_ALLOWED_SIGNERS: &str = "gpg.ssh.allowedSignersFile"; pub const CONFIG_GPG_SSH_ALLOWED_SIGNERS: &str = "gpg.ssh.allowedSignersFile";
/// Git revision parameter. Supports extended SHA-1 syntax.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rev(String);
impl Rev {
/// Return the revision as a string.
pub fn as_str(&self) -> &str {
&self.0
}
/// Resolve the revision to an [`ObjectId`].
pub fn resolve(&self, repo: &git2::Repository) -> Result<ObjectId, git2::Error> {
let object = repo.revparse_single(self.as_str())?;
Ok(ObjectId::from(object.id()))
}
}
impl Display for Rev {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for Rev {
fn from(value: String) -> Self {
Rev(value)
}
}
/// Get the git repository in the current directory. /// Get the git repository in the current directory.
pub fn repository() -> Result<Repository, anyhow::Error> { pub fn repository() -> Result<Repository, anyhow::Error> {
match Repository::open(".") { match Repository::open(".") {

View File

@ -101,3 +101,7 @@ pub fn addr(val: &OsString) -> anyhow::Result<Address> {
let val = val.to_string_lossy(); let val = val.to_string_lossy();
Address::from_str(&val).map_err(|_| anyhow!("invalid address '{}'", val)) Address::from_str(&val).map_err(|_| anyhow!("invalid address '{}'", val))
} }
pub fn string(val: &OsString) -> String {
val.to_string_lossy().to_string()
}