Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: use git config's `core.abbrev` for SHA-1 abbreviations
RadsammyT committed 1 year ago
commit b2c7d87a1c90e826e415bdced04d9136e343151d
parent ee1c867800eacea37d489d966622d0aff0af0b70
2 files changed +141 -5
modified radicle-cli/src/git.rs
@@ -9,10 +9,12 @@ use std::fmt::Display;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::Write;
+
use std::num::ParseIntError;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
+
use std::sync::OnceLock;

use anyhow::anyhow;
use anyhow::Context as _;
@@ -36,6 +38,14 @@ 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";

+
/// The default abbreviation length to fall back to should [`get_abbrev()`] fail
+
/// to get its value.
+
const CONFIG_ABBREV_DEFAULT: usize = 7;
+

+
/// The abbreviation length to format all SHA-1 hashes to be this long.
+
/// [`get_abbrev()`] should be called to get (or initialize) this value.
+
static CONFIG_ABBREV: OnceLock<usize> = OnceLock::new();
+

/// Git revision parameter. Supports extended SHA-1 syntax.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rev(String);
@@ -331,6 +341,123 @@ pub fn check_version() -> Result<Version, anyhow::Error> {
    Ok(git_version)
}

+
/// Values that match the possible values of [`core.abbrev`][coreabbrev].
+
///
+
/// `Abbreviation::default` gives a default value of [`CONFIG_ABBREV_DEFAULT`]
+
/// characters.
+
///
+
/// [coreabbrev]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreabbrev
+
#[derive(Clone, Copy, Debug)]
+
pub enum Abbreviation {
+
    /// Automatically decide the length by using `git` to find the shortest
+
    /// abbreviation to uniquely identify any SHA in this repository.
+
    Auto,
+
    /// Use the maximum abbreviation, i.e. 40 characters.
+
    No,
+
    /// Use the defined length found at `core.abbrev`.
+
    ///
+
    /// Note that the value must be between 4 and 40. If a smaller or larger
+
    /// value is used, it is clamped.
+
    Length(usize),
+
}
+

+
impl FromStr for Abbreviation {
+
    type Err = ParseIntError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        match s {
+
            "auto" => Ok(Self::Auto),
+
            "no" => Ok(Self::No),
+
            n => n
+
                .parse::<usize>()
+
                .map(|n| Self::Length(n.clamp(Self::MIN, Self::MAX))),
+
        }
+
    }
+
}
+

+
impl Default for Abbreviation {
+
    fn default() -> Self {
+
        Self::Length(CONFIG_ABBREV_DEFAULT)
+
    }
+
}
+

+
impl Abbreviation {
+
    /// The minimum number of characters to abbreviate a SHA to.
+
    const MIN: usize = 4;
+
    /// The maximum number of characters of SHA1.
+
    const MAX: usize = 40;
+
    /// The Git config key for the abbreviation.
+
    const KEY: &'static str = "core.abbrev";
+
    /// The Git value for automatically getting the abbreviation length.
+
    const AUTO: &'static str = "auto";
+

+
    /// Construct the `Abbreviation`.
+
    ///
+
    /// The `Abbreviation` is constructed by:
+
    ///   1. First check the local Git configuration.
+
    ///   2. If it could not determine the value in 1., then check the global
+
    ///        Git configuration.
+
    ///   3. Otherwise, default to [`CONFIG_ABBREV_DEFAULT`].
+
    pub fn new() -> Result<Self, git2::Error> {
+
        match repository().and_then(|repo| Ok(repo.config()?)) {
+
            Ok(local) => Self::from_config(&local).or_else(|_| Self::from_global()),
+
            Err(_) => Self::from_global(),
+
        }
+
    }
+

+
    fn from_global() -> Result<Self, git2::Error> {
+
        git2::Config::open_default()
+
            .and_then(|cfg| Self::from_config(&cfg))
+
            .or(Ok(Self::default()))
+
    }
+

+
    fn from_config(config: &git2::Config) -> Result<Self, git2::Error> {
+
        let entry = config.get_entry(Self::KEY)?;
+
        entry
+
            .value()
+
            .unwrap_or(Self::AUTO)
+
            .trim()
+
            .parse::<Self>()
+
            .map_err(|e| {
+
                git2::Error::new(
+
                    git2::ErrorCode::User,
+
                    git2::ErrorClass::Config,
+
                    e.to_string(),
+
                )
+
            })
+
    }
+

+
    /// Get the abbreviation length.
+
    ///
+
    /// In the case of `Abbreviation::Auto`, the `git` CLI is used to determine
+
    /// the shortest abbreviation length to use, if this fails a default of
+
    /// [`CONFIG_ABBREV_DEFAULT`] is returned.
+
    pub fn length(&self) -> usize {
+
        match self {
+
            Abbreviation::Auto => git(Path::new("."), ["rev-parse", "--short", "HEAD"])
+
                .unwrap_or("a".repeat(CONFIG_ABBREV_DEFAULT))
+
                .trim()
+
                .len(),
+
            Abbreviation::No => Self::MAX,
+
            Abbreviation::Length(n) => *n,
+
        }
+
    }
+
}
+

+
/// Get the abbreviation length to use for Git SHA formatting.
+
///
+
/// The Git config is used to lookup [`core.abbrev`][coreabbrev] to determine
+
/// the strategy for how many characters to abbreviate to (see [`Abbreviation`]
+
/// for more details).
+
///
+
/// [coreabbrev]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreabbrev
+
pub fn get_abbrev() -> usize {
+
    *OnceLock::get_or_init(&CONFIG_ABBREV, || {
+
        let abbrev = Abbreviation::new().unwrap_or_default();
+
        abbrev.length()
+
    })
+
}
+

/// Parse a remote refspec into a peer id and ref.
pub fn parse_remote(refspec: &str) -> Option<(NodeId, &str)> {
    refspec
@@ -348,9 +475,9 @@ pub fn view_diff(
    let workdir = repo
        .workdir()
        .ok_or_else(|| anyhow!("Could not get workdir current repository."))?;
-

-
    let left = format!("{:.7}", left.to_string());
-
    let right = format!("{:.7}", right.to_string());
+
    let abbrev = get_abbrev();
+
    let left = format!("{:.abbrev$}", left.to_string());
+
    let right = format!("{:.abbrev$}", right.to_string());

    let mut git = Command::new("git")
        .current_dir(workdir)
modified radicle-cli/src/terminal/format.rs
@@ -14,6 +14,7 @@ use radicle::profile::{env, Profile};
use radicle::storage::RefUpdate;
use radicle_term::element::Line;

+
use crate::git;
use crate::terminal as term;

/// Format a node id to be more compact.
@@ -27,7 +28,11 @@ pub fn node(node: &NodeId) -> Paint<String> {

/// Format a git Oid.
pub fn oid(oid: impl Into<radicle::git::Oid>) -> Paint<String> {
-
    Paint::new(format!("{:.7}", oid.into()))
+
    Paint::new(format!(
+
        "{:.abbrev$}",
+
        oid.into(),
+
        abbrev = git::get_abbrev()
+
    ))
}

/// Wrap parenthesis around styled input, eg. `"input"` -> `"(input)"`.
@@ -47,7 +52,11 @@ pub fn command<D: fmt::Display>(cmd: D) -> Paint<String> {

/// Format a COB id.
pub fn cob(id: &ObjectId) -> Paint<String> {
-
    Paint::new(format!("{:.7}", id.to_string()))
+
    Paint::new(format!(
+
        "{:.abbrev$}",
+
        id.to_string(),
+
        abbrev = git::get_abbrev()
+
    ))
}

/// Format a DID.