Skip to content

Commit

Permalink
feat: Commit::short_id() (#301)
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Mar 1, 2022
1 parent f16f8e3 commit ea8ada4
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 99 deletions.
82 changes: 1 addition & 81 deletions git-hash/src/owned.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::cmp::Ordering;
use std::{borrow::Borrow, convert::TryInto, fmt, ops::Deref};

use crate::{borrowed::oid, Kind, SIZE_OF_SHA1_DIGEST};
Expand All @@ -13,86 +12,7 @@ pub struct Prefix {
}

///
pub mod prefix {
use quick_error::quick_error;

quick_error! {
/// The error returned by [Prefix::try_from_id()][super::Prefix::try_from_id()].
#[derive(Debug)]
#[allow(missing_docs)]
pub enum Error {
TooShort { hex_len: usize } {
display("The minimum hex length of a short object id is 4, got {}", hex_len)
}
TooLong { object_kind: crate::Kind, hex_len: usize } {
display("An object of kind {} cannot be larger than {} in hex, but {} was requested", object_kind, object_kind.len_in_hex(), hex_len)
}
}
}
}

impl Prefix {
/// Create a new instance by taking a full `id` as input and truncating it to `hex_len`.
///
/// For instance, with `hex_len` of 7 the resulting prefix is 3.5 bytes, or 3 bytes and 4 bits
/// wide, with all other bytes and bits set to zero.
pub fn new(id: impl AsRef<oid>, hex_len: usize) -> Result<Self, prefix::Error> {
let id = id.as_ref();
if hex_len > id.kind().len_in_hex() {
Err(prefix::Error::TooLong {
object_kind: id.kind(),
hex_len,
})
} else if hex_len < 4 {
Err(prefix::Error::TooShort { hex_len })
} else {
let mut prefix = ObjectId::null(id.kind());
let b = prefix.as_mut_slice();
let copy_len = (hex_len + 1) / 2;
b[..copy_len].copy_from_slice(&id.as_bytes()[..copy_len]);
if hex_len % 2 == 1 {
b[hex_len / 2] &= 0xf0;
}

Ok(Prefix { bytes: prefix, hex_len })
}
}

/// Returns the prefix as object id.
///
/// Note that it may be deceptive to use given that it looks like a full
/// object id, even though its post-prefix bytes/bits are set to zero.
pub fn as_oid(&self) -> &oid {
&self.bytes
}

/// Return the amount of hexadecimal characters that are set in the prefix.
///
/// This gives the prefix a granularity of 4 bits.
pub fn hex_len(&self) -> usize {
self.hex_len
}

/// Provided with candidate id which is a full hash, determine how this prefix compares to it,
/// only looking at the prefix bytes, ignoring everything behind that.
pub fn cmp_oid(&self, candidate: &oid) -> Ordering {
let common_len = self.hex_len / 2;

self.bytes.as_bytes()[..common_len]
.cmp(&candidate.as_bytes()[..common_len])
.then(if self.hex_len % 2 == 1 {
let half_byte_idx = self.hex_len / 2;
self.bytes.as_bytes()[half_byte_idx].cmp(&(candidate.as_bytes()[half_byte_idx] & 0xf0))
} else {
Ordering::Equal
})
}

/// Create an instance from the given hexadecimal prefix, e.g. `35e77c16` would yield a `Prefix` with `hex_len()` = 8.
pub fn from_hex(_hex: &str) -> Self {
todo!("Prefix::from_hex()")
}
}
pub mod prefix;

/// An owned hash identifying objects, most commonly Sha1
#[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy)]
Expand Down
86 changes: 86 additions & 0 deletions git-hash/src/owned/prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use crate::{oid, ObjectId, Prefix};
use quick_error::quick_error;
use std::cmp::Ordering;

quick_error! {
/// The error returned by [Prefix::try_from_id()][super::Prefix::try_from_id()].
#[derive(Debug)]
#[allow(missing_docs)]
pub enum Error {
TooShort { hex_len: usize } {
display("The minimum hex length of a short object id is 4, got {}", hex_len)
}
TooLong { object_kind: crate::Kind, hex_len: usize } {
display("An object of kind {} cannot be larger than {} in hex, but {} was requested", object_kind, object_kind.len_in_hex(), hex_len)
}
}
}

impl Prefix {
/// Create a new instance by taking a full `id` as input and truncating it to `hex_len`.
///
/// For instance, with `hex_len` of 7 the resulting prefix is 3.5 bytes, or 3 bytes and 4 bits
/// wide, with all other bytes and bits set to zero.
pub fn new(id: impl AsRef<oid>, hex_len: usize) -> Result<Self, Error> {
let id = id.as_ref();
if hex_len > id.kind().len_in_hex() {
Err(Error::TooLong {
object_kind: id.kind(),
hex_len,
})
} else if hex_len < 4 {
Err(Error::TooShort { hex_len })
} else {
let mut prefix = ObjectId::null(id.kind());
let b = prefix.as_mut_slice();
let copy_len = (hex_len + 1) / 2;
b[..copy_len].copy_from_slice(&id.as_bytes()[..copy_len]);
if hex_len % 2 == 1 {
b[hex_len / 2] &= 0xf0;
}

Ok(Prefix { bytes: prefix, hex_len })
}
}

/// Returns the prefix as object id.
///
/// Note that it may be deceptive to use given that it looks like a full
/// object id, even though its post-prefix bytes/bits are set to zero.
pub fn as_oid(&self) -> &oid {
&self.bytes
}

/// Return the amount of hexadecimal characters that are set in the prefix.
///
/// This gives the prefix a granularity of 4 bits.
pub fn hex_len(&self) -> usize {
self.hex_len
}

/// Provided with candidate id which is a full hash, determine how this prefix compares to it,
/// only looking at the prefix bytes, ignoring everything behind that.
pub fn cmp_oid(&self, candidate: &oid) -> Ordering {
let common_len = self.hex_len / 2;

self.bytes.as_bytes()[..common_len]
.cmp(&candidate.as_bytes()[..common_len])
.then(if self.hex_len % 2 == 1 {
let half_byte_idx = self.hex_len / 2;
self.bytes.as_bytes()[half_byte_idx].cmp(&(candidate.as_bytes()[half_byte_idx] & 0xf0))
} else {
Ordering::Equal
})
}

/// Create an instance from the given hexadecimal prefix, e.g. `35e77c16` would yield a `Prefix` with `hex_len()` = 8.
pub fn from_hex(_hex: &str) -> Self {
todo!("Prefix::from_hex()")
}
}

impl std::fmt::Display for Prefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.bytes.to_hex_with_len(self.hex_len).fmt(f)
}
}
8 changes: 5 additions & 3 deletions git-hash/tests/oid/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ mod prefix {
prefix.cmp_oid(&hex_to_id("b920bbf055e1efb9080592a409d3975738b6efb3")),
Ordering::Less
);
assert_eq!(prefix.to_string(), "b920bbb");
}

#[test]
fn it_detects_equality() {
let id = hex_to_id("b920bbb055e1efb9080592a409d3975738b6efb3");
let prefix = git_hash::Prefix::new(id, 7).unwrap();
let id = hex_to_id("a920bbb055e1efb9080592a409d3975738b6efb3");
let prefix = git_hash::Prefix::new(id, 6).unwrap();
assert_eq!(prefix.cmp_oid(&id), Ordering::Equal);
assert_eq!(
prefix.cmp_oid(&hex_to_id("b920bbbfffffffffffffffffffffffffffffffff")),
prefix.cmp_oid(&hex_to_id("a920bbffffffffffffffffffffffffffffffffff")),
Ordering::Equal
);
assert_eq!(prefix.to_string(), "a920bb");
}
}
mod new {
Expand Down
4 changes: 2 additions & 2 deletions git-repository/src/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ impl<'repo> Id<'repo> {
}

///
mod prefix {
/// Returned by [`Oid::prefix()`][super::Oid::prefix()].
pub mod prefix {
/// Returned by [`Id::prefix()`][super::Id::prefix()].
#[derive(thiserror::Error, Debug)]
#[allow(missing_docs)]
pub enum Error {
Expand Down
6 changes: 6 additions & 0 deletions git-repository/src/object/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ mod error {
pub use error::Error;

impl<'repo> Commit<'repo> {
/// Turn this objects id into a shortened id with a length in hex as configured by `core.abbrev`.
pub fn short_id(&self) -> Result<git_hash::Prefix, crate::id::prefix::Error> {
use crate::ext::ObjectIdExt;
self.id.attach(self.repo).prefix()
}

/// Parse the commits message into a [`MessageRef`][git_object::commit::MessageRef], after decoding the entire commit object.
pub fn message(&self) -> Result<git_object::commit::MessageRef<'_>, git_object::decode::Error> {
Ok(self.decode()?.message())
Expand Down
11 changes: 11 additions & 0 deletions git-repository/src/object/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ impl<'repo> From<Object<'repo>> for DetachedObject {
}
}

impl<'repo> From<Commit<'repo>> for Object<'repo> {
fn from(mut r: Commit<'repo>) -> Self {
Object {
id: r.id,
kind: git_object::Kind::Commit,
data: steal_from_freelist(&mut r.data),
repo: r.repo,
}
}
}

impl<'repo> AsRef<[u8]> for Object<'repo> {
fn as_ref(&self) -> &[u8] {
&self.data
Expand Down
1 change: 1 addition & 0 deletions git-repository/tests/easy/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ fn prefix() -> crate::Result {
assert_eq!(prefix.cmp_oid(&id), Ordering::Equal);
assert_eq!(prefix.hex_len(), 8, "preconfigured via core.abbrev default value");

// TODO: do this in-memory (with or without writing to disk)
assert!(
std::process::Command::new("git")
.current_dir(worktree_dir.path())
Expand Down
28 changes: 15 additions & 13 deletions git-repository/tests/easy/object.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
mod commit {
use git_repository::{Commit, Repository};
use git_testtools::hex_to_id;
use std::cmp::Ordering;

use crate::basic_repo;

#[test]
fn tree() {
let handle = basic_repo().unwrap();
fn short_id() -> crate::Result {
let handle = basic_repo()?;
let commit = head_commit(&handle);
assert_eq!(commit.short_id()?.cmp_oid(&commit.id), Ordering::Equal);
Ok(())
}

#[test]
fn tree() -> crate::Result {
let handle = basic_repo()?;
let commit = head_commit(&handle);

assert_eq!(commit.tree().unwrap().id, commit.tree_id().expect("id present"));
assert_eq!(commit.tree()?.id, commit.tree_id().expect("id present"));
assert_eq!(
commit.tree_id(),
Some(hex_to_id("21d3ba9a26b790a4858d67754ae05d04dfce4d0c"))
)
);
Ok(())
}

#[test]
Expand All @@ -27,15 +37,7 @@ mod commit {
}

fn head_commit(repo: &Repository) -> Commit<'_> {
repo.head()
.unwrap()
// TODO: Add something like peel_to_commit() to cut the chain, deal with unborn as Error
.into_fully_peeled_id()
.expect("born")
.unwrap()
.object()
.unwrap()
.into_commit()
repo.head().unwrap().peel_to_commit_in_place().unwrap()
}
}

Expand Down

0 comments on commit ea8ada4

Please sign in to comment.