Skip to content

Commit a02e382

Browse files
fix(git): error on missing historical mappings (#211)
1 parent f5e3ed8 commit a02e382

2 files changed

Lines changed: 94 additions & 6 deletions

File tree

src/git/versioned_store/backends.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -727,18 +727,29 @@ impl<const N: usize> VersionedKvStore<N, GitNodeStorage<N>, GitMetadataBackend>
727727
let config_result = self.metadata.read_file_at_commit(commit_id, &config_path);
728728
let mapping_result = self.metadata.read_file_at_commit(commit_id, &mapping_path);
729729

730-
// If files are not found, this might be an initial empty commit, return empty
731-
if config_result.is_err() || mapping_result.is_err() {
732-
return Ok(HashMap::new());
733-
}
730+
let (config_data, mapping_data) = match (config_result, mapping_result) {
731+
(Ok(config_data), Ok(mapping_data)) => (config_data, mapping_data),
732+
(Err(_), Err(_)) => {
733+
// Both files missing can be an initial empty commit.
734+
return Ok(HashMap::new());
735+
}
736+
(Ok(_), Err(e)) => {
737+
return Err(GitKvError::GitObjectError(format!(
738+
"Historical commit {commit_id} has {config_path} but is missing {mapping_path}: {e}"
739+
)));
740+
}
741+
(Err(e), Ok(_)) => {
742+
return Err(GitKvError::GitObjectError(format!(
743+
"Historical commit {commit_id} has {mapping_path} but is missing {config_path}: {e}"
744+
)));
745+
}
746+
};
734747

735-
let config_data = config_result?;
736748
let config: TreeConfig<N> = serde_json::from_slice(&config_data).map_err(|e| {
737749
GitKvError::GitObjectError(format!("Failed to deserialize config: {e}"))
738750
})?;
739751

740752
// Load the hash mappings from the tree as string format and parse
741-
let mapping_data = mapping_result?;
742753
let mapping_str = String::from_utf8(mapping_data)
743754
.map_err(|e| GitKvError::GitObjectError(format!("Invalid UTF-8 in mappings: {e}")))?;
744755

@@ -792,6 +803,11 @@ impl<const N: usize> VersionedKvStore<N, GitNodeStorage<N>, GitMetadataBackend>
792803

793804
// For Git storage, reconstruct the tree from hash mappings
794805
if hash_mappings.is_empty() {
806+
if config.root_hash.is_some() {
807+
return Err(GitKvError::GitObjectError(format!(
808+
"Historical commit {commit_id} has a tree root but {mapping_path} contains no hash mappings"
809+
)));
810+
}
795811
return Ok(HashMap::new());
796812
}
797813

src/git/versioned_store/tests.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,78 @@ mod proof_tests {
215215
Some(&b"value3".to_vec())
216216
);
217217
}
218+
219+
#[test]
220+
fn get_keys_at_ref_errors_when_committed_hash_mappings_are_missing() {
221+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
222+
let repo_path = temp_dir.path().to_str().unwrap();
223+
224+
std::process::Command::new("git")
225+
.args(["init"])
226+
.current_dir(repo_path)
227+
.output()
228+
.expect("Failed to initialize git repo");
229+
std::process::Command::new("git")
230+
.args(["config", "user.name", "Test User"])
231+
.current_dir(repo_path)
232+
.output()
233+
.expect("Failed to set git user name");
234+
std::process::Command::new("git")
235+
.args(["config", "user.email", "test@example.com"])
236+
.current_dir(repo_path)
237+
.output()
238+
.expect("Failed to set git user email");
239+
240+
let dataset_path = temp_dir.path().join("dataset");
241+
std::fs::create_dir(&dataset_path).expect("Failed to create dataset directory");
242+
243+
let _cwd_guard = CwdGuard::set(&dataset_path);
244+
let mut store =
245+
GitVersionedKvStore::<32>::init(&dataset_path).expect("Failed to initialize store");
246+
247+
store
248+
.insert(b"key1".to_vec(), b"value1".to_vec())
249+
.expect("Failed to insert key1");
250+
let good_commit = store.commit("Add key1").expect("Failed to commit");
251+
let good_keys = store
252+
.get_keys_at_ref(&good_commit.to_hex().to_string())
253+
.expect("good commit should be readable");
254+
assert_eq!(good_keys.get(&b"key1".to_vec()), Some(&b"value1".to_vec()));
255+
256+
drop(store);
257+
258+
let rm_output = std::process::Command::new("git")
259+
.args(["rm", "dataset/prolly_hash_mappings"])
260+
.current_dir(repo_path)
261+
.output()
262+
.expect("git rm failed to run");
263+
assert!(
264+
rm_output.status.success(),
265+
"git rm failed: {}",
266+
String::from_utf8_lossy(&rm_output.stderr)
267+
);
268+
let commit_output = std::process::Command::new("git")
269+
.args(["commit", "-m", "Remove hash mappings"])
270+
.current_dir(repo_path)
271+
.output()
272+
.expect("git commit failed to run");
273+
assert!(
274+
commit_output.status.success(),
275+
"git commit failed: {}",
276+
String::from_utf8_lossy(&commit_output.stderr)
277+
);
278+
279+
let store =
280+
GitVersionedKvStore::<32>::open(&dataset_path).expect("corrupt store still opens");
281+
let err = store
282+
.get_keys_at_ref("HEAD")
283+
.expect_err("missing committed mappings must not look like an empty store");
284+
285+
assert!(
286+
err.to_string().contains("prolly_hash_mappings"),
287+
"unexpected error: {err}"
288+
);
289+
}
218290
}
219291

220292
#[cfg(test)]

0 commit comments

Comments
 (0)