Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Allow git revision parameters as COB IDs
Vincenzo Palazzo committed 3 years ago
commit 654a21b2b38efb85742423538dee110fbc356f01
parent f2d31aa9c072f2f6967fefaf990723b57be202a0
12 files changed +142 -109
modified radicle-cli/examples/rad-id.md
@@ -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
modified radicle-cli/examples/rad-issue.md
@@ -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
modified radicle-cli/examples/rad-patch.md
@@ -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
```

modified radicle-cli/src/commands/assign.rs
@@ -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(())
}
modified radicle-cli/src/commands/comment.rs
@@ -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)?;
modified radicle-cli/src/commands/id.rs
@@ -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")?;
modified radicle-cli/src/commands/issue.rs
@@ -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)?;
        }
    }
modified radicle-cli/src/commands/merge.rs
@@ -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
modified radicle-cli/src/commands/patch.rs
@@ -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(())
modified radicle-cli/src/commands/review.rs
@@ -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"))?;
modified radicle-cli/src/git.rs
@@ -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(".") {
modified radicle-cli/src/terminal/args.rs
@@ -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()
+
}