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:
```
$ rad id accept 0d396a83a5e1dda2b8929f7dc401d19dd1a79fb8 --no-confirm
$ rad id accept 0d396a --no-confirm
✓ Accepted proposal ✓
title: Add Bob
description: Add Bob as a delegate

View File

@ -20,6 +20,19 @@ $ rad issue list
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.
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:
```
$ rad patch checkout 191a14e520f2eeff7c0e3ee0a5523c5217eecb89
$ rad patch checkout 191a14e52
✓ Switched to branch patch/191a14e520f
```

View File

@ -1,12 +1,12 @@
use std::ffi::OsString;
use std::str::FromStr;
use anyhow::anyhow;
use nonempty::NonEmpty;
use radicle::prelude::Did;
use crate::git::Rev;
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::issue;
use radicle::storage::WriteStorage;
@ -32,7 +32,7 @@ Options
#[derive(Debug)]
pub struct Options {
pub id: issue::IssueId,
pub id: Rev,
pub to: NonEmpty<Did>,
}
@ -41,7 +41,7 @@ impl Args for Options {
use lexopt::prelude::*;
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();
while let Some(arg) = parser.next()? {
@ -56,11 +56,8 @@ impl Args for Options {
to.push(did);
}
Value(ref val) if id.is_none() => {
let val = val.to_string_lossy();
let Ok(val) = issue::IssueId::from_str(&val) else {
return Err(anyhow!("invalid Issue ID '{}'", val));
};
id = Some(val);
let val = string(val);
id = Some(Rev::from(val));
}
_ => {
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 (_, id) = radicle::rad::cwd()?;
let repo = profile.storage.repository_mut(id)?;
let oid = options.id.resolve(&repo.backend)?;
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),
_ => e.into(),
})?;
let signer = term::signer(&profile)?;
issue.assign(options.to.into_iter().map(Did::into), &signer)?;
Ok(())
}

View File

@ -1,9 +1,7 @@
use std::ffi::OsString;
use std::str::FromStr;
use anyhow::anyhow;
use radicle::cob;
use radicle::cob::issue::Issues;
use radicle::cob::patch::Patches;
use radicle::cob::store;
@ -11,7 +9,9 @@ use radicle::cob::thread;
use radicle::prelude::*;
use radicle::storage;
use crate::git::Rev;
use crate::terminal as term;
use crate::terminal::args::string;
use crate::terminal::args::{Args, Error, Help};
use crate::terminal::patch::Message;
@ -34,25 +34,17 @@ Options
#[derive(Debug)]
pub struct Options {
pub id: cob::ObjectId,
pub id: Rev,
pub message: Message,
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 {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
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 reply_to = None;
@ -76,7 +68,10 @@ impl Args for Options {
// Common.
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())),
}
}
@ -103,7 +98,8 @@ fn comment(
}
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) => {
let comment_id = options.reply_to.unwrap_or_else(|| {
let (comment_id, _) = issue.comments().next().expect("root comment always exists");
@ -119,7 +115,7 @@ fn comment(
}
let mut patches = Patches::open(repo)?;
match patches.get_mut(&options.id) {
match patches.get_mut(&id) {
Ok(mut patch) => {
let (revision_id, _) = patch.revisions().last().expect("patch has a revision");
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 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::identity::Identity;
use radicle::prelude::{Did, Doc};
use radicle::storage::ReadStorage as _;
use radicle_crypto::Verified;
use crate::git::Rev;
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::Interactive;
@ -53,11 +54,11 @@ impl Metadata {
#[derive(Clone, Debug, Default)]
pub enum Operation {
Accept {
id: ProposalId,
id: Rev,
rev: Option<RevisionId>,
},
Reject {
id: ProposalId,
id: Rev,
rev: Option<RevisionId>,
},
Edit {
@ -67,7 +68,7 @@ pub enum Operation {
threshold: Option<usize>,
},
Update {
id: ProposalId,
id: Rev,
rev: Option<RevisionId>,
title: Option<String>,
description: Option<String>,
@ -75,22 +76,22 @@ pub enum Operation {
threshold: Option<usize>,
},
Rebase {
id: ProposalId,
id: Rev,
rev: Option<RevisionId>,
},
Show {
id: ProposalId,
id: Rev,
rev: Option<RevisionId>,
show_revisions: bool,
},
#[default]
List,
Commit {
id: ProposalId,
id: Rev,
rev: Option<RevisionId>,
},
Close {
id: ProposalId,
id: Rev,
},
}
@ -119,7 +120,7 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args);
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 title: Option<String> = None;
let mut description: Option<String> = None;
@ -173,14 +174,8 @@ impl Args for Options {
show_revisions = true;
}
Value(val) if op.is_some() => {
let val = val
.to_str()
.ok_or_else(|| anyhow!("proposal id specified is not UTF-8"))?;
id = Some(
ProposalId::from_str(val)
.map_err(|_| anyhow!("invalid proposal id '{}'", val))?,
);
let val = string(&val);
id = Some(Rev::from(val));
}
_ => {
return Err(anyhow!(arg.unexpected()));
@ -245,6 +240,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let interactive = &options.interactive;
match options.op {
Operation::Accept { id, rev } => {
let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?;
let (rid, revision) = select(&proposal, rev, &previous, interactive)?;
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 } => {
let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?;
let (rid, revision) = select(&proposal, rev, &previous, interactive)?;
warn_out_of_date(revision, &previous);
@ -311,6 +308,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
delegates,
threshold,
} => {
let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?;
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 } => {
let id = id.resolve(&repo.backend)?;
// TODO: it would be nice if rebasing also handled fast-forwards nicely.
let mut proposal = proposals.get_mut(&id)?;
let (_, revision) = select(&proposal, rev, &previous, interactive)?;
@ -407,6 +406,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
t.print();
}
Operation::Commit { id, rev } => {
let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?;
let (rid, revision) = commit_select(&proposal, rev, &previous, interactive)?;
warn_out_of_date(revision, &previous);
@ -419,6 +419,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
}
}
Operation::Close { id } => {
let id = id.resolve(&repo.backend)?;
let mut proposal = proposals.get_mut(&id)?;
let yes = confirm(interactive, "Are you sure you want to close?");
if yes {
@ -432,6 +433,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
rev,
show_revisions,
} => {
let id = id.resolve(&repo.backend)?;
let proposal = proposals
.get(&id)?
.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::prelude::Did;
use crate::git::Rev;
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 radicle::cob::common::{Reaction, Tag};
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::{cob, Node};
@ -71,17 +72,17 @@ pub enum Operation {
description: Option<String>,
},
Show {
id: IssueId,
id: Rev,
},
State {
id: IssueId,
id: Rev,
state: State,
},
Delete {
id: IssueId,
id: Rev,
},
React {
id: IssueId,
id: Rev,
reaction: Reaction,
},
List {
@ -101,7 +102,7 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args);
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 title: Option<String> = None;
let mut reaction: Option<Reaction> = None;
@ -161,14 +162,8 @@ impl Args for Options {
unknown => anyhow::bail!("unknown operation '{}'", unknown),
},
Value(val) if op.is_some() => {
let val = val
.to_str()
.ok_or_else(|| anyhow!("issue id specified is not UTF-8"))?;
id = Some(
IssueId::from_str(val)
.map_err(|_| anyhow!("invalid issue id '{}'", val))?,
);
let val = string(&val);
id = Some(Rev::from(val));
}
_ => {
return Err(anyhow!(arg.unexpected()));
@ -225,16 +220,19 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
show_issue(&issue)?;
}
Operation::Show { id } => {
let id = id.resolve(&repo.backend)?;
let issue = issues
.get(&id)?
.context("No issue with the given ID exists")?;
show_issue(&issue)?;
}
Operation::State { id, state } => {
let id = id.resolve(&repo.backend)?;
let mut issue = issues.get_mut(&id)?;
issue.lifecycle(state, &signer)?;
}
Operation::React { id, reaction } => {
let id = id.resolve(&repo.backend)?;
if let Ok(mut issue) = issues.get_mut(&id) {
let (comment_id, _) = term::io::comment_select(&issue).unwrap();
issue.react(*comment_id, reaction, &signer)?;
@ -325,6 +323,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
t.print();
}
Operation::Delete { id } => {
let id = id.resolve(&repo.backend)?;
issues.remove(&id, &signer)?;
}
}

View File

@ -5,8 +5,9 @@ use std::str::FromStr;
use anyhow::{anyhow, Context};
use crate::git::Rev;
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::{Patch, PatchId, Patches};
use radicle::git;
@ -72,7 +73,7 @@ pub enum State {
}
#[derive(Debug)]
pub struct Options {
pub id: PatchId,
pub id: Rev,
pub interactive: bool,
pub revision: Option<RevisionIx>,
}
@ -82,7 +83,7 @@ impl Args for Options {
use lexopt::prelude::*;
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 interactive = false;
@ -103,14 +104,8 @@ impl Args for Options {
revision = Some(id);
}
Value(val) => {
let val = 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))?,
);
let val = string(&val);
id = Some(Rev::from(val));
}
_ => return Err(anyhow::anyhow!(arg.unexpected())),
}
@ -149,10 +144,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
//
// Get patch information
//
let patch_id = options.id;
let patch_id = options.id.resolve(&repository.backend)?;
let mut patch = patches
.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 branch = head

View File

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

View File

@ -3,12 +3,13 @@ use std::str::FromStr;
use anyhow::{anyhow, Context};
use radicle::cob::patch::{PatchId, Patches, RevisionIx, Verdict};
use radicle::cob::patch::{Patches, RevisionIx, Verdict};
use radicle::prelude::*;
use radicle::rad;
use crate::git::Rev;
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;
pub const HELP: Help = Help {
@ -46,7 +47,7 @@ Markdown supported.
#[derive(Debug)]
pub struct Options {
pub id: PatchId,
pub id: Rev,
pub revision: Option<RevisionIx>,
pub message: Message,
pub sync: bool,
@ -60,7 +61,7 @@ impl Args for Options {
use lexopt::prelude::*;
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 message = Message::default();
let mut sync = true;
@ -109,14 +110,7 @@ impl Args for Options {
verdict = Some(Verdict::Reject);
}
Value(val) => {
let val = 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))?,
);
id = Some(Rev::from(string(&val)));
}
_ => 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"))?;
let mut patches = Patches::open(&repository)?;
let patch_id = options.id;
let patch_id = options.id.resolve(&repository.backend)?;
let mut patch = patches
.get_mut(&patch_id)
.context(format!("couldn't find patch {patch_id} locally"))?;

View File

@ -1,5 +1,6 @@
//! Git-related functions and types.
use std::collections::HashSet;
use std::fmt::Display;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::Write;
@ -10,6 +11,7 @@ use std::str::FromStr;
use anyhow::anyhow;
use anyhow::Context as _;
use radicle::cob::ObjectId;
use radicle::crypto::ssh;
use radicle::git;
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_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.
pub fn repository() -> Result<Repository, anyhow::Error> {
match Repository::open(".") {

View File

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