use crate::error::{Error, Result};
use crate::repo::VaultRepo;
use git2::{ObjectType, Oid};
use tracing::instrument;
#[derive(Debug, Clone)]
pub struct Precondition {
pub path: String,
pub expected: Option<Oid>,
}
impl Precondition {
pub fn expect_blob(path: impl Into<String>, blob: Oid) -> Self {
Self {
path: path.into(),
expected: Some(blob),
}
}
pub fn expect_absent(path: impl Into<String>) -> Self {
Self {
path: path.into(),
expected: None,
}
}
}
impl VaultRepo {
pub fn blob_oid_of(content: &[u8]) -> Result<Oid> {
Ok(Oid::hash_object(ObjectType::Blob, content)?)
}
#[instrument(
skip(self, preconditions),
fields(base = ?base_tree, n = preconditions.len()),
name = "git_check_preconditions"
)]
pub fn check_preconditions(
&self,
base_tree: Option<Oid>,
preconditions: &[Precondition],
) -> Result<()> {
for pc in preconditions {
let found = match base_tree {
Some(tree) => self.blob_oid_at(tree, &pc.path)?,
None => None, };
if found != pc.expected {
return Err(Error::PreconditionFailed {
path: pc.path.clone(),
expected: pc.expected,
found,
});
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plumbing::TreeChange;
use git2::Repository;
use tempfile::TempDir;
fn open_unborn() -> (TempDir, VaultRepo) {
let tmp = TempDir::new().unwrap();
let mut opts = git2::RepositoryInitOptions::new();
opts.initial_head("main");
Repository::init_opts(tmp.path(), &opts).unwrap();
let vr = VaultRepo::open(tmp.path()).unwrap();
(tmp, vr)
}
fn upsert(path: &str, content: &str) -> TreeChange {
TreeChange::Upsert {
path: path.to_string(),
content: content.as_bytes().to_vec(),
}
}
#[test]
fn version_token_matches_stored_blob() {
let (_tmp, vr) = open_unborn();
let t = vr.build_tree(None, &[upsert("a.md", "alpha")]).unwrap();
let stored = vr.blob_oid_at(t, "a.md").unwrap().unwrap();
let token = VaultRepo::blob_oid_of(b"alpha").unwrap();
assert_eq!(
token, stored,
"version token must equal the stored blob oid"
);
}
#[test]
fn matching_preconditions_pass() {
let (_tmp, vr) = open_unborn();
let t = vr
.build_tree(None, &[upsert("a.md", "alpha"), upsert("b.md", "beta")])
.unwrap();
let a = VaultRepo::blob_oid_of(b"alpha").unwrap();
let b = VaultRepo::blob_oid_of(b"beta").unwrap();
vr.check_preconditions(
Some(t),
&[
Precondition::expect_blob("a.md", a),
Precondition::expect_blob("b.md", b),
Precondition::expect_absent("c.md"),
],
)
.expect("all preconditions match");
}
#[test]
fn changed_blob_fails() {
let (_tmp, vr) = open_unborn();
let t = vr.build_tree(None, &[upsert("a.md", "alpha")]).unwrap();
let stale = VaultRepo::blob_oid_of(b"stale").unwrap();
match vr.check_preconditions(Some(t), &[Precondition::expect_blob("a.md", stale)]) {
Err(Error::PreconditionFailed { path, .. }) => assert_eq!(path, "a.md"),
other => panic!("expected PreconditionFailed, got {other:?}"),
}
}
#[test]
fn expect_absent_but_present_fails() {
let (_tmp, vr) = open_unborn();
let t = vr.build_tree(None, &[upsert("a.md", "alpha")]).unwrap();
assert!(matches!(
vr.check_preconditions(Some(t), &[Precondition::expect_absent("a.md")]),
Err(Error::PreconditionFailed { .. })
));
}
#[test]
fn expect_blob_but_absent_fails() {
let (_tmp, vr) = open_unborn();
let t = vr.build_tree(None, &[upsert("a.md", "alpha")]).unwrap();
let phantom = VaultRepo::blob_oid_of(b"x").unwrap();
assert!(matches!(
vr.check_preconditions(Some(t), &[Precondition::expect_blob("missing.md", phantom)]),
Err(Error::PreconditionFailed { .. })
));
}
#[test]
fn one_stale_among_many_aborts_all() {
let (_tmp, vr) = open_unborn();
let t = vr
.build_tree(None, &[upsert("a.md", "alpha"), upsert("b.md", "beta")])
.unwrap();
let a = VaultRepo::blob_oid_of(b"alpha").unwrap();
let b_stale = VaultRepo::blob_oid_of(b"OLD-beta").unwrap();
match vr.check_preconditions(
Some(t),
&[
Precondition::expect_blob("a.md", a),
Precondition::expect_blob("b.md", b_stale),
],
) {
Err(Error::PreconditionFailed { path, .. }) => assert_eq!(path, "b.md"),
other => panic!("expected PreconditionFailed on b.md, got {other:?}"),
}
}
#[test]
fn empty_base_treats_all_as_absent() {
let (_tmp, vr) = open_unborn();
vr.check_preconditions(None, &[Precondition::expect_absent("a.md")])
.expect("absent on empty base");
let phantom = VaultRepo::blob_oid_of(b"x").unwrap();
assert!(matches!(
vr.check_preconditions(None, &[Precondition::expect_blob("a.md", phantom)]),
Err(Error::PreconditionFailed { .. })
));
}
}