#[allow(dead_code)]
mod support;
use self::support::{with_mounted_timefs, RepositoryFixture};
use std::ffi::OsString;
use std::fs;
use std::os::unix::ffi::OsStringExt;
#[test]
fn history_resolver_lists_versions_oldest_to_newest() {
let fixture = history_fixture();
let expected = git_history_entries(&fixture, "docs/story.rs");
assert_eq!(
expected.len(),
4,
"fixture should create four history entries"
);
let (_value, mount) = with_mounted_timefs(fixture.path(), &[], &["-f"], |mounted| {
let root_entries = sorted_dir_entries(mounted.path());
assert_eq!(
root_entries,
vec![
b"at".to_vec(),
b"commits".to_vec(),
b"history".to_vec(),
b"now".to_vec(),
b"refs".to_vec(),
]
);
let history_root = mounted.path().join("history");
assert!(
fs::read_dir(&history_root)
.expect("history/ should be readable")
.next()
.is_none(),
"history/ should not enumerate arbitrary paths"
);
assert!(
fs::read_dir(history_root.join("docs"))
.expect("history/docs should be readable")
.next()
.is_none(),
"history prefix directories should stay synthetic"
);
let mounted_history = history_root.join("docs").join("story.rs");
let mounted_versions = sorted_dir_entries(&mounted_history);
assert_eq!(
mounted_versions,
expected
.iter()
.map(|entry| entry.name.clone())
.collect::<Vec<_>>()
);
let first = fs::read(mounted_history.join(OsString::from_vec(expected[0].name.clone())))
.expect("first history entry should be readable");
let second = fs::read(mounted_history.join(OsString::from_vec(expected[1].name.clone())))
.expect("second history entry should be readable");
assert_ne!(first, second, "successive history versions should differ");
for entry in &expected {
let mounted_path = mounted_history.join(OsString::from_vec(entry.name.clone()));
assert_eq!(
fs::read(&mounted_path).expect("history file should be readable"),
fixture.git_show_path(entry.commit.as_str(), &entry.path)
);
}
});
assert!(
mount.stderr.is_empty(),
"history mount emitted unexpected stderr: {}",
mount.stderr
);
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ExpectedHistoryEntry {
name: Vec<u8>,
commit: String,
path: Vec<u8>,
}
fn history_fixture() -> RepositoryFixture {
let fixture = RepositoryFixture::new("timefs-task9-history");
fs::create_dir_all(fixture.path().join("docs"))
.expect("fixture docs directory should be created");
fs::write(fixture.path().join("docs/story.txt"), b"alpha\n")
.expect("initial story write should succeed");
fixture.git(["add", "."]);
fixture.git(["commit", "-m", "Add story"]);
fs::write(fixture.path().join("docs/story.txt"), b"alpha\nbeta\n")
.expect("second story write should succeed");
fixture.git(["add", "."]);
fixture.git(["commit", "-m", "Expand story"]);
fixture.git(["mv", "docs/story.txt", "docs/story.rs"]);
fixture.git(["commit", "-m", "Rename story"]);
fs::write(
fixture.path().join("docs/story.rs"),
b"fn main() {\n println!(\"alpha\");\n println!(\"beta\");\n}\n",
)
.expect("final story write should succeed");
fixture.git(["add", "."]);
fixture.git(["commit", "-m", "Update story"]);
fixture
}
fn git_history_entries(fixture: &RepositoryFixture, path: &str) -> Vec<ExpectedHistoryEntry> {
let output = fixture.git([
"log",
"--follow",
"-z",
"--format=commit:%H",
"--name-status",
"--diff-filter=AMCRT",
"--",
path,
]);
let mut tokens = output
.stdout
.split(|byte| *byte == 0)
.filter(|token| !token.is_empty());
let mut entries = Vec::new();
while let Some(commit_token) = tokens.next() {
let Some(hex) = commit_token.strip_prefix(b"commit:") else {
continue;
};
let commit = String::from_utf8(hex.to_vec()).expect("history commit should be UTF-8");
let Some(status_token) = tokens.next() else {
break;
};
let status = status_token.strip_prefix(b"\n").unwrap_or(status_token);
let Some(kind) = status.first().copied() else {
continue;
};
let Some(first_path) = tokens.next() else {
break;
};
let path_at_commit = match kind {
b'R' | b'C' => tokens
.next()
.expect("rename history should include the destination path")
.to_vec(),
_ => first_path.to_vec(),
};
entries.push(ExpectedHistoryEntry {
name: Vec::new(),
commit,
path: path_at_commit,
});
}
entries.reverse();
let width = entries.len().to_string().len().max(4);
for (index, entry) in entries.iter_mut().enumerate() {
entry.name = history_version_name(index + 1, width, &entry.commit, &entry.path);
}
entries
}
fn history_version_name(ordinal: usize, width: usize, commit: &str, path: &[u8]) -> Vec<u8> {
let mut name = format!("{ordinal:0width$}-").into_bytes();
let short_len = commit.len().min(7);
name.extend_from_slice(commit.as_bytes().get(..short_len).unwrap_or_default());
name.extend_from_slice(history_version_suffix(path));
name
}
fn history_version_suffix(path: &[u8]) -> &[u8] {
let basename = path.rsplit(|byte| *byte == b'/').next().unwrap_or(path);
if basename.first().is_some_and(|byte| *byte == b'.') {
return basename;
}
match basename.iter().position(|byte| *byte == b'.') {
Some(index) => basename.get(index..).unwrap_or_default(),
None => &[],
}
}
fn sorted_dir_entries(path: &std::path::Path) -> Vec<Vec<u8>> {
let mut entries = fs::read_dir(path)
.expect("directory should be readable")
.filter_map(Result::ok)
.map(|entry| entry.file_name().into_vec())
.collect::<Vec<_>>();
entries.sort();
entries
}