remote-helper: Interpret verbosity option

The `verbosity` option is ignored, which makes it more difficult to
obtain debugging information if the "internal" push, i.e., the
`git push` invocation by `git-remote-rad` fails.

Provide two types for verbosity, and wire them up.
This commit is contained in:
Lorenz Leutgeb 2025-09-11 10:02:51 +02:00 committed by Fintan Halpenny
parent 11e8b89b20
commit 0a8317c35f
5 changed files with 111 additions and 25 deletions

View File

@ -132,7 +132,7 @@ fn find_patch_commit<'a>(
match working.find_commit(head) {
Ok(commit) => Ok(commit),
Err(e) if git::ext::is_not_found_err(&e) => {
git::process::fetch_local(workdir, stored, [head.into()])?;
git::process::fetch_local(workdir, stored, [head.into()], git::Verbosity::default())?;
working.find_commit(head).map_err(|e| e.into())
}
Err(e) => Err(e.into()),

View File

@ -7,7 +7,7 @@ use thiserror::Error;
use radicle::git;
use radicle::storage::ReadRepository;
use crate::read_line;
use crate::{read_line, Verbosity};
#[derive(Debug, Error)]
pub enum Error {
@ -31,6 +31,7 @@ pub fn run<R: ReadRepository>(
working: &Path,
stored: R,
stdin: &io::Stdin,
verbosity: Verbosity,
) -> Result<(), Error> {
// Read all the `fetch` lines.
let mut line = String::new();
@ -62,7 +63,7 @@ pub fn run<R: ReadRepository>(
// used in the working copy, this will always result in the object
// missing. This seems to only be an issue with `libgit2`/`git2`
// and not `git` itself.
git::process::fetch_local(working, &stored, oids)?;
git::process::fetch_local(working, &stored, oids, verbosity.into())?;
// Nb. An empty line means we're done.
println!();

View File

@ -70,6 +70,40 @@ pub enum Error {
List(#[from] list::Error),
}
/// Models values for the `verbosity` option, see
/// <https://git-scm.com/docs/gitremote-helpers#Documentation/gitremote-helpers.txt-optionverbosityn>.
#[derive(Copy, Clone, Debug)]
struct Verbosity(u8);
impl From<Verbosity> for radicle::git::Verbosity {
/// Converts the verbosity option passed to a Git remote helper to
/// one that can be passed to other Git commands via command line.
/// Note that these scales are one off: While the default verbosity
/// for remote helpers is 1, the default verbosity via command line
/// (omitting the flag) is 0.
/// This implementation also cuts off verbosities greater than [`i8::MAX`].
fn from(val: Verbosity) -> Self {
radicle::git::Verbosity::from(i8::try_from(val.0).unwrap_or(i8::MAX) - 1)
}
}
/// The documentation on Git remote helpers, see
/// <https://git-scm.com/docs/gitremote-helpers#Documentation/gitremote-helpers.txt-optionverbosityn>
/// says: "1 is the default level of verbosity".
impl Default for Verbosity {
fn default() -> Self {
Self(1)
}
}
impl FromStr for Verbosity {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
u8::from_str(s).map(Self)
}
}
#[derive(Debug, Default, Clone)]
pub struct Options {
/// Don't sync after push.
@ -84,6 +118,7 @@ pub struct Options {
base: Option<Rev>,
/// Patch message.
message: cli::patch::Message,
verbosity: Verbosity,
}
/// Run the radicle remote helper using the given profile.
@ -139,9 +174,15 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
println!("fetch");
println!();
}
["option", "verbosity"] => {
println!("ok");
}
["option", "verbosity", verbosity] => match verbosity.parse::<Verbosity>() {
Ok(verbosity) => {
opts.verbosity = verbosity;
println!("ok");
}
Err(err) => {
println!("error {err}");
}
},
["option", "push-option", args @ ..] => {
// Nb. Git documentation says that we can print `error <msg>` or `unsupported`
// for options that are not supported, but this results in Git saying that
@ -167,7 +208,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
path: working.clone(),
})?;
return fetch::run(vec![(oid, refstr)], working, stored, &stdin)
return fetch::run(vec![(oid, refstr)], working, stored, &stdin, opts.verbosity)
.map_err(Error::from);
}
["push", refspec] => {

View File

@ -31,7 +31,7 @@ use radicle::{git, rad};
use radicle_cli as cli;
use radicle_cli::terminal as term;
use crate::{hint, read_line, Options};
use crate::{hint, read_line, Options, Verbosity};
#[derive(Debug, Error)]
pub enum Error {
@ -346,8 +346,17 @@ pub fn run(
let rules = crefs.rules();
let me = Did::from(nid);
let explorer =
push(src, &dst, *force, &nid, &working, stored, patches, &signer)?;
let explorer = push(
src,
&dst,
*force,
&nid,
&working,
stored,
patches,
&signer,
opts.verbosity,
)?;
// If we're trying to update the canonical head, make sure
// we don't diverge from the current head. This only applies
// to repos with more than one delegate.
@ -487,7 +496,7 @@ where
//
// In case the reference is not properly deleted, the next attempt to open a patch should
// not fail, since the reference will already exist with the correct OID.
push_ref(src, &dst, false, working, stored.raw())?;
push_ref(src, &dst, false, working, stored.raw(), opts.verbosity)?;
let (_, target) = stored.canonical_head()?;
let base = if let Some(base) = opts.base {
@ -601,7 +610,7 @@ where
let commit = *src;
let dst = dst.with_namespace(nid.into());
push_ref(src, &dst, force, working, stored.raw())?;
push_ref(src, &dst, force, working, stored.raw(), opts.verbosity)?;
let Ok(Some(patch)) = patches.get(&patch_id) else {
return Err(Error::NotFound(patch_id));
@ -671,6 +680,7 @@ fn push<G>(
cob::cache::StoreWriter,
>,
signer: &Device<G>,
verbosity: Verbosity,
) -> Result<Option<ExplorerResource>, Error>
where
G: crypto::signature::Signer<crypto::Signature>,
@ -680,7 +690,7 @@ where
// It's ok for the destination reference to be unknown, eg. when pushing a new branch.
let old = stored.backend.find_reference(dst.as_str()).ok();
push_ref(src, &dst, force, working, stored.raw())?;
push_ref(src, &dst, force, working, stored.raw(), verbosity)?;
if let Some(old) = old {
let proj = stored.project()?;
@ -863,22 +873,22 @@ fn push_ref(
force: bool,
working: &git::raw::Repository,
stored: &git::raw::Repository,
verbosity: Verbosity,
) -> Result<(), Error> {
let url = git::url::File::new(stored.path()).to_string();
// Nb. The *force* indicator (`+`) is processed by Git tooling before we even reach this code.
// This happens during the `list for-push` phase.
let refspec = git::Refspec { src, dst, force };
let repo = working.workdir().unwrap_or_else(|| working.path());
let mut args = vec!["push".to_string()];
radicle::git::run::<_, _, &str, &str>(
repo,
[
"push",
url.to_string().as_str(),
refspec.to_string().as_str(),
],
[],
)
let verbosity: git::Verbosity = verbosity.into();
args.extend(verbosity.into_flag());
args.extend([url.to_string(), refspec.to_string()]);
radicle::git::run::<_, _, &str, &str>(repo, args, [])
.map_err(|err| {
Error::Io(std::io::Error::other(format!(
"failed to run `git push {url} {refspec}` in {:?}: {err}",

View File

@ -54,6 +54,38 @@ impl std::fmt::Display for Version {
}
}
/// Verbosity level for Git commands.
#[derive(Default, Clone, Copy)]
pub struct Verbosity(i8);
impl Verbosity {
/// Transform into a command line flag, helpful for passing to invocations
/// of `git`.
///
/// See <https://github.com/git/git/blob/c44beea485f0f2feaf460e2ac87fdd5608d63cf0/builtin/pull.c#L264-L276>
pub fn into_flag(&self) -> Option<String> {
const FLAG_PREFIX: &str = "-";
const FLAG_QUIET: &str = "q";
const FLAG_VERBOSE: &str = "v";
let repetitions = self.0.unsigned_abs() as usize;
if repetitions == 0 {
return None;
}
let flag = if self.0 > 0 { FLAG_VERBOSE } else { FLAG_QUIET };
Some(FLAG_PREFIX.to_string() + &flag.repeat(repetitions))
}
}
impl From<i8> for Verbosity {
fn from(v: i8) -> Self {
Self(v)
}
}
#[derive(thiserror::Error, Debug)]
pub enum VersionError {
#[error("malformed git version string")]
@ -761,7 +793,7 @@ pub mod process {
use crate::storage::ReadRepository;
use super::{run, url, Oid};
use super::{run, url, Oid, Verbosity};
/// Perform a local fetch, i.e. `file://<storage path>`.
///
@ -771,16 +803,18 @@ pub mod process {
working: &Path,
storage: &R,
oids: impl IntoIterator<Item = Oid>,
verbosity: Verbosity,
) -> Result<(), io::Error>
where
R: ReadRepository,
{
let mut fetch = vec![
"fetch".to_string(),
url::File::new(storage.path()).to_string(),
// N.b. avoid writing fetch head since we're only fetching objects
// Avoid writing fetch head since we're only fetching objects
"--no-write-fetch-head".to_string(),
];
fetch.extend(verbosity.into_flag());
fetch.push(url::File::new(storage.path()).to_string());
fetch.extend(oids.into_iter().map(|oid| oid.to_string()));
// N.b. `.` is used since we're fetching within the working copy
run::<_, _, &str, &str>(working, fetch, [])?;