1use crate::error::{Error, Result};
17use crate::repo::VaultRepo;
18use git2::{ObjectType, Oid};
19use tracing::instrument;
20
21#[derive(Debug, Clone)]
24pub struct Precondition {
25 pub path: String,
26 pub expected: Option<Oid>,
27}
28
29impl Precondition {
30 pub fn expect_blob(path: impl Into<String>, blob: Oid) -> Self {
32 Self {
33 path: path.into(),
34 expected: Some(blob),
35 }
36 }
37
38 pub fn expect_absent(path: impl Into<String>) -> Self {
40 Self {
41 path: path.into(),
42 expected: None,
43 }
44 }
45}
46
47impl VaultRepo {
48 pub fn blob_oid_of(content: &[u8]) -> Result<Oid> {
54 Ok(Oid::hash_object(ObjectType::Blob, content)?)
55 }
56
57 #[instrument(
63 skip(self, preconditions),
64 fields(base = ?base_tree, n = preconditions.len()),
65 name = "git_check_preconditions"
66 )]
67 pub fn check_preconditions(
68 &self,
69 base_tree: Option<Oid>,
70 preconditions: &[Precondition],
71 ) -> Result<()> {
72 for pc in preconditions {
73 let found = match base_tree {
74 Some(tree) => self.blob_oid_at(tree, &pc.path)?,
75 None => None, };
77 if found != pc.expected {
78 return Err(Error::PreconditionFailed {
79 path: pc.path.clone(),
80 expected: pc.expected,
81 found,
82 });
83 }
84 }
85 Ok(())
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::plumbing::TreeChange;
93 use git2::Repository;
94 use tempfile::TempDir;
95
96 fn open_unborn() -> (TempDir, VaultRepo) {
97 let tmp = TempDir::new().unwrap();
98 let mut opts = git2::RepositoryInitOptions::new();
99 opts.initial_head("main");
100 Repository::init_opts(tmp.path(), &opts).unwrap();
101 let vr = VaultRepo::open(tmp.path()).unwrap();
102 (tmp, vr)
103 }
104
105 fn upsert(path: &str, content: &str) -> TreeChange {
106 TreeChange::Upsert {
107 path: path.to_string(),
108 content: content.as_bytes().to_vec(),
109 }
110 }
111
112 #[test]
113 fn version_token_matches_stored_blob() {
114 let (_tmp, vr) = open_unborn();
117 let t = vr.build_tree(None, &[upsert("a.md", "alpha")]).unwrap();
118 let stored = vr.blob_oid_at(t, "a.md").unwrap().unwrap();
119 let token = VaultRepo::blob_oid_of(b"alpha").unwrap();
120 assert_eq!(
121 token, stored,
122 "version token must equal the stored blob oid"
123 );
124 }
125
126 #[test]
127 fn matching_preconditions_pass() {
128 let (_tmp, vr) = open_unborn();
129 let t = vr
130 .build_tree(None, &[upsert("a.md", "alpha"), upsert("b.md", "beta")])
131 .unwrap();
132 let a = VaultRepo::blob_oid_of(b"alpha").unwrap();
133 let b = VaultRepo::blob_oid_of(b"beta").unwrap();
134 vr.check_preconditions(
135 Some(t),
136 &[
137 Precondition::expect_blob("a.md", a),
138 Precondition::expect_blob("b.md", b),
139 Precondition::expect_absent("c.md"),
140 ],
141 )
142 .expect("all preconditions match");
143 }
144
145 #[test]
146 fn changed_blob_fails() {
147 let (_tmp, vr) = open_unborn();
148 let t = vr.build_tree(None, &[upsert("a.md", "alpha")]).unwrap();
149 let stale = VaultRepo::blob_oid_of(b"stale").unwrap();
151 match vr.check_preconditions(Some(t), &[Precondition::expect_blob("a.md", stale)]) {
152 Err(Error::PreconditionFailed { path, .. }) => assert_eq!(path, "a.md"),
153 other => panic!("expected PreconditionFailed, got {other:?}"),
154 }
155 }
156
157 #[test]
158 fn expect_absent_but_present_fails() {
159 let (_tmp, vr) = open_unborn();
160 let t = vr.build_tree(None, &[upsert("a.md", "alpha")]).unwrap();
161 assert!(matches!(
162 vr.check_preconditions(Some(t), &[Precondition::expect_absent("a.md")]),
163 Err(Error::PreconditionFailed { .. })
164 ));
165 }
166
167 #[test]
168 fn expect_blob_but_absent_fails() {
169 let (_tmp, vr) = open_unborn();
170 let t = vr.build_tree(None, &[upsert("a.md", "alpha")]).unwrap();
171 let phantom = VaultRepo::blob_oid_of(b"x").unwrap();
172 assert!(matches!(
173 vr.check_preconditions(Some(t), &[Precondition::expect_blob("missing.md", phantom)]),
174 Err(Error::PreconditionFailed { .. })
175 ));
176 }
177
178 #[test]
179 fn one_stale_among_many_aborts_all() {
180 let (_tmp, vr) = open_unborn();
182 let t = vr
183 .build_tree(None, &[upsert("a.md", "alpha"), upsert("b.md", "beta")])
184 .unwrap();
185 let a = VaultRepo::blob_oid_of(b"alpha").unwrap();
186 let b_stale = VaultRepo::blob_oid_of(b"OLD-beta").unwrap();
187 match vr.check_preconditions(
188 Some(t),
189 &[
190 Precondition::expect_blob("a.md", a),
191 Precondition::expect_blob("b.md", b_stale),
192 ],
193 ) {
194 Err(Error::PreconditionFailed { path, .. }) => assert_eq!(path, "b.md"),
195 other => panic!("expected PreconditionFailed on b.md, got {other:?}"),
196 }
197 }
198
199 #[test]
200 fn empty_base_treats_all_as_absent() {
201 let (_tmp, vr) = open_unborn();
202 vr.check_preconditions(None, &[Precondition::expect_absent("a.md")])
204 .expect("absent on empty base");
205 let phantom = VaultRepo::blob_oid_of(b"x").unwrap();
206 assert!(matches!(
207 vr.check_preconditions(None, &[Precondition::expect_blob("a.md", phantom)]),
208 Err(Error::PreconditionFailed { .. })
209 ));
210 }
211}