use std::path::PathBuf;
use anyhow::Result;
pub type Oid<'a> = &'a [u8];
pub type Extent = (u64, u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum ObjType {
Commit = 1,
Tree = 2,
Blob = 3,
Tag = 4,
OfsDelta = 6,
RefDelta = 7,
}
impl ObjType {
pub fn code(self) -> u8 {
self as u8
}
pub fn from_code(c: u8) -> Option<Self> {
Some(match c {
1 => ObjType::Commit,
2 => ObjType::Tree,
3 => ObjType::Blob,
4 => ObjType::Tag,
6 => ObjType::OfsDelta,
7 => ObjType::RefDelta,
_ => return None,
})
}
pub fn as_str(self) -> &'static str {
match self {
ObjType::Commit => "commit",
ObjType::Tree => "tree",
ObjType::Blob => "blob",
ObjType::Tag => "tag",
ObjType::OfsDelta => "ofs-delta",
ObjType::RefDelta => "ref-delta",
}
}
pub const ALL: [ObjType; 6] = [
ObjType::Commit,
ObjType::Tree,
ObjType::Blob,
ObjType::Tag,
ObjType::OfsDelta,
ObjType::RefDelta,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TxId {
pub pack_id: Option<u64>,
pub extent: Option<Extent>,
pub push_seq: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefRow {
pub name: String,
pub oid: Option<Vec<u8>>,
pub peeled: Option<Vec<u8>>,
pub symref_target: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefUpdate {
pub name: String,
pub target: Option<String>,
pub peeled: Option<String>,
pub symref_target: Option<String>,
}
impl RefUpdate {
pub fn set(name: impl Into<String>, target: impl Into<String>) -> Self {
Self {
name: name.into(),
target: Some(target.into()),
peeled: None,
symref_target: None,
}
}
pub fn delete(name: impl Into<String>) -> Self {
Self {
name: name.into(),
target: None,
peeled: None,
symref_target: None,
}
}
pub fn symbolic(name: impl Into<String>, points_to: impl Into<String>) -> Self {
Self {
name: name.into(),
target: None,
peeled: None,
symref_target: Some(points_to.into()),
}
}
pub fn with_peeled(mut self, peeled: impl Into<String>) -> Self {
self.peeled = Some(peeled.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefCas<'a> {
pub name: String,
pub old: Option<Oid<'a>>,
pub new: Option<Oid<'a>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefTarget {
Object(Vec<u8>),
Symbolic(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Observed {
Nothing,
Value(RefTarget),
Unreadable(String),
}
impl Observed {
pub fn absent() -> Self {
Observed::Nothing
}
pub fn oid(raw: &[u8]) -> Self {
Observed::Value(RefTarget::Object(raw.to_vec()))
}
pub fn is_unreadable(&self) -> bool {
matches!(self, Observed::Unreadable(_))
}
}
fn hex_into(f: &mut std::fmt::Formatter<'_>, raw: &[u8]) -> std::fmt::Result {
for b in raw {
write!(f, "{b:02x}")?;
}
Ok(())
}
impl std::fmt::Display for Observed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Observed::Nothing => f.write_str("nothing"),
Observed::Value(RefTarget::Object(raw)) => hex_into(f, raw),
Observed::Value(RefTarget::Symbolic(name)) => write!(f, "ref: {name}"),
Observed::Unreadable(why) => {
write!(f, "a value that could not be decoded: {why}")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefRejection {
Cas {
name: String,
expected: Observed,
actual: Observed,
},
Locked { name: String },
}
impl RefRejection {
pub fn name(&self) -> &str {
match self {
RefRejection::Cas { name, .. } | RefRejection::Locked { name } => name,
}
}
pub fn is_cas_failure(&self) -> bool {
matches!(self, RefRejection::Cas { .. })
}
pub fn is_lock_contention(&self) -> bool {
matches!(self, RefRejection::Locked { .. })
}
pub fn of(err: &anyhow::Error) -> Option<&RefRejection> {
err.chain().find_map(|e| e.downcast_ref::<RefRejection>())
}
}
impl std::fmt::Display for RefRejection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RefRejection::Cas {
name,
expected,
actual,
} => write!(
f,
"compare-and-swap on {name} failed: it is {actual}, the caller expected \
{expected} — nothing was written"
),
RefRejection::Locked { name } => write!(
f,
"{name} is locked by another writer — transient, retry; this is not a \
backend fault"
),
}
}
}
impl std::error::Error for RefRejection {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Stored {
pub obj_type: ObjType,
pub uncompressed_size: u64,
pub extent: Extent,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GcReport {
pub strategy: &'static str,
pub archive: PathBuf,
pub retired: Option<PathBuf>,
pub bytes_before: u64,
pub bytes_after: u64,
pub rows: u64,
pub delta_rows: u64,
pub verified: bool,
pub retired_packs: u64,
}
pub trait GitOps: Send + Sync {
fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> Result<TxId>;
fn put_pack(&self, bytes: &[u8]) -> Result<TxId>;
fn put_refs(&self, updates: &[RefUpdate]) -> Result<TxId>;
fn get(&self, oid: Oid<'_>) -> Result<Option<Stored>>;
fn has(&self, oid: Oid<'_>) -> Result<bool>;
fn size(&self, oid: Oid<'_>) -> Result<Option<u64>>;
fn extents(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<Extent>>>;
fn refs(&self) -> Result<Vec<RefRow>>;
fn update_ref(&self, name: &str, old: Option<Oid<'_>>, new: Option<Oid<'_>>) -> Result<TxId>;
fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> Result<TxId>;
fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Vec<Vec<u8>>>;
fn gc(&self) -> Result<GcReport>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Caps {
pub thin: bool,
pub ofs_delta: bool,
}
impl Caps {
pub fn modern() -> Self {
Caps {
thin: false,
ofs_delta: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PackStats {
pub bytes: u64,
pub objects: u64,
pub copied: u64,
pub recompressed: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct OidList {
bytes: Vec<u8>,
oid_len: usize,
}
impl OidList {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(oids: usize, oid_len: usize) -> Self {
OidList {
bytes: Vec::with_capacity(oids * oid_len),
oid_len: 0,
}
}
pub fn push(&mut self, oid: &[u8]) -> Result<()> {
if self.bytes.is_empty() {
anyhow::ensure!(
!oid.is_empty(),
"an empty oid has no width, and a list of them would report a length of zero \
objects while holding some"
);
self.oid_len = oid.len();
} else if oid.len() != self.oid_len {
anyhow::bail!(
"this oid list is {}-byte oids and was handed a {}-byte one; a mixed-width list \
cannot be read back",
self.oid_len,
oid.len()
);
}
self.bytes.extend_from_slice(oid);
Ok(())
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = &[u8]> + '_ {
self.bytes.chunks_exact(self.oid_len.max(1))
}
pub fn len(&self) -> usize {
if self.oid_len == 0 {
0
} else {
self.bytes.len() / self.oid_len
}
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
pub fn oid_len(&self) -> usize {
self.oid_len
}
pub fn contains(&self, oid: &[u8]) -> bool {
oid.len() == self.oid_len && self.iter().any(|o| o == oid)
}
}
impl<T: AsRef<[u8]>> FromIterator<T> for OidList {
fn from_iter<I: IntoIterator<Item = T>>(items: I) -> Self {
let mut out = OidList::new();
for item in items {
out.push(item.as_ref())
.expect("one repository holds one hash kind");
}
out
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ReachSet {
pub objects: Vec<Vec<u8>>,
pub commits: Vec<Vec<u8>>,
pub client_has: OidList,
}
pub trait GitServe: GitOps {
fn read(&self, oid: Oid<'_>) -> Result<Option<(ObjType, Vec<u8>)>>;
fn header(&self, oid: Oid<'_>) -> Result<Option<(ObjType, u64)>>;
fn sizes(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<u64>>>;
fn head(&self) -> Result<Option<RefRow>>;
fn set_head(&self, target: &str) -> Result<TxId>;
fn emit_pack(
&self,
objects: &[Oid<'_>],
have: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
) -> Result<PackStats>;
fn select(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Option<ReachSet>>;
}
#[cfg(test)]
mod tests {
use super::OidList;
#[test]
fn a_flat_list_reads_back_exactly_what_was_pushed() {
let ids: Vec<Vec<u8>> = (0u8..5).map(|i| vec![i; 20]).collect();
let list: OidList = ids.iter().collect();
assert_eq!(list.len(), 5, "five 20-byte oids");
assert_eq!(list.oid_len(), 20);
let back: Vec<Vec<u8>> = list.iter().map(<[u8]>::to_vec).collect();
assert_eq!(back, ids, "push order and bytes, both");
assert!(list.contains(&[3u8; 20]));
assert!(!list.contains(&[9u8; 20]), "an oid nobody pushed");
assert!(!list.contains(&[0u8; 32]));
}
#[test]
fn mixing_hash_widths_in_one_list_is_refused() {
let mut list = OidList::new();
list.push(&[1u8; 20])
.expect("the first push sets the width");
let err = list
.push(&[2u8; 32])
.expect_err("a 32-byte oid in a 20-byte list must not be accepted");
assert!(
err.to_string().contains("mixed-width"),
"the refusal must say why: {err}"
);
assert_eq!(list.len(), 1, "the refused push must not have landed");
}
#[test]
fn an_empty_voucher_equals_the_default_one() {
let mut built = OidList::with_capacity(64, 20);
assert_eq!(built, OidList::default(), "nothing pushed, no width yet");
built.push(&[7u8; 20]).unwrap();
assert_ne!(built, OidList::default());
assert!(!built.is_empty());
}
}