Watch
1
0
Fork
You've already forked souveraine
0

memory: persist every git index mutation

This commit is contained in:
Fimeg 2026-08-07 16:02:16 -04:00
commit 0c9db09888

View file

@ -224,6 +224,7 @@ impl MemoryRepo {
index
.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)
.context("staging initial memory files")?;
index.write().context("persisting initial git index")?;
let tree_id = index.write_tree().context("writing git tree")?;
let tree = repo.find_tree(tree_id)?;
let signature = git2::Signature::now(
@ -576,15 +577,7 @@ impl MemoryRepo {
let repo_path = self.root.clone();
let is_git_repo = repo_path.join(".git").exists();
let mut file_count = 0;
if let Ok(entries) = std::fs::read_dir(&repo_path) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name != ".git" && name.ends_with(".md") {
file_count += 1;
}
}
}
let file_count = Self::count_md_files(&repo_path);
let (last_commit, has_uncommitted, remote_url) = if is_git_repo {
match git2::Repository::open(&repo_path) {
@ -639,9 +632,14 @@ impl MemoryRepo {
index.add_path(Path::new(&md_path))?;
} else if self.root.join(&rel_path).exists() {
index.add_path(Path::new(&rel_path))?;
} else if index.get_path(Path::new(&md_path), 0).is_some() {
index.remove_path(Path::new(&md_path))?;
} else if index.get_path(Path::new(&rel_path), 0).is_some() {
index.remove_path(Path::new(&rel_path))?;
}
}
index.write().context("persisting git index")?;
let tree_id = index.write_tree().context("writing tree")?;
let tree = repo.find_tree(tree_id)?;
let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
@ -781,6 +779,34 @@ impl MemoryRepo {
git2::Repository::open(&self.root).context("opening memory git repository")
}
fn count_md_files(root: &Path) -> usize {
let mut count = 0;
let mut directories = vec![root.to_path_buf()];
while let Some(directory) = directories.pop() {
let Ok(entries) = std::fs::read_dir(directory) else {
continue;
};
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy().starts_with(".git") {
continue;
}
let Ok(kind) = entry.file_type() else {
continue;
};
if kind.is_dir() {
directories.push(entry.path());
} else if kind.is_file() && entry.path().extension().is_some_and(|ext| ext == "md")
{
count += 1;
}
}
}
count
}
/// Whether the `description` frontmatter contract applies to a label.
///
/// `description` is required on `MemoryFrontmatter` files — the prompt
@ -1413,6 +1439,67 @@ mod tests {
assert_eq!(file.body, "Hello memory world");
}
#[tokio::test]
async fn consecutive_writes_survive_in_head_and_index() {
let dir = TempDir::new().unwrap();
let repo = MemoryRepo::new("test-agent", dir.path());
repo.init().await.unwrap();
repo.write("journal/first", "first thought").await.unwrap();
repo.write("journal/second", "second thought")
.await
.unwrap();
let git = git2::Repository::open(repo.root()).unwrap();
let head = git.head().unwrap().peel_to_commit().unwrap();
let tree = head.tree().unwrap();
let index = git.index().unwrap();
for path in [
"system/persona.md",
"system/state.md",
"journal/first.md",
"journal/second.md",
] {
assert!(
tree.get_path(Path::new(path)).is_ok(),
"HEAD lost {path}"
);
assert!(
index.get_path(Path::new(path), 0).is_some(),
"index lost {path}"
);
}
let status = repo.status().unwrap();
assert_eq!(status.file_count, 4);
assert!(!status.has_uncommitted);
}
#[tokio::test]
async fn delete_survives_in_head_and_index() {
let dir = TempDir::new().unwrap();
let repo = MemoryRepo::new("test-agent", dir.path());
repo.init().await.unwrap();
repo.write("journal/keep", "keep me").await.unwrap();
repo.write("journal/remove", "remove me").await.unwrap();
repo.delete("journal/remove").await.unwrap();
let git = git2::Repository::open(repo.root()).unwrap();
let head = git.head().unwrap().peel_to_commit().unwrap();
let tree = head.tree().unwrap();
let index = git.index().unwrap();
assert!(tree.get_path(Path::new("journal/keep.md")).is_ok());
assert!(tree.get_path(Path::new("journal/remove.md")).is_err());
assert!(index
.get_path(Path::new("journal/remove.md"), 0)
.is_none());
let status = repo.status().unwrap();
assert_eq!(status.file_count, 3);
assert!(!status.has_uncommitted);
}
#[tokio::test]
async fn sync_without_remote_errors_with_provisioning_hint() {
let dir = TempDir::new().unwrap();
@ -1443,6 +1530,9 @@ mod tests {
let repo = MemoryRepo::new("test-agent", dir.path());
repo.init().await.unwrap();
repo.write("journal/entry", "first thought").await.unwrap();
repo.write("journal/second", "second thought")
.await
.unwrap();
let remote_dir = TempDir::new().unwrap();
let bare = remote_dir.path().join("memfs.git");
@ -1464,6 +1554,13 @@ mod tests {
branch.target().unwrap().to_string(),
repo.head_commit_hex().unwrap()
);
let tree = remote_repo
.find_commit(branch.target().unwrap())
.unwrap()
.tree()
.unwrap();
assert!(tree.get_path(Path::new("journal/entry.md")).is_ok());
assert!(tree.get_path(Path::new("journal/second.md")).is_ok());
}
#[tokio::test]