1pub mod global_config;
7#[doc(hidden)]
8pub(crate) mod repo_config;
9
10use crate::engine::tree::FileTree;
11use crate::patch::types::{Patch, PatchId, TouchSet};
12use rusqlite::{Connection, params};
13use std::collections::BTreeMap;
14use std::path::Path;
15use suture_common::{BranchName, FileStatus, Hash, RepoPath};
16use thiserror::Error;
17
18#[derive(Error, Debug)]
20pub enum MetaError {
21 #[error("database error: {0}")]
22 Database(#[from] rusqlite::Error),
23
24 #[error("I/O error: {0}")]
25 Io(#[from] std::io::Error),
26
27 #[error("patch not found: {0}")]
28 PatchNotFound(String),
29
30 #[error("branch not found: {0}")]
31 BranchNotFound(String),
32
33 #[error("corrupt metadata: {0}")]
34 Corrupt(String),
35
36 #[error("migration failed: {0}")]
37 MigrationFailed(String),
38
39 #[error("{0}")]
40 Custom(String),
41}
42
43pub struct MetadataStore {
45 conn: Connection,
46}
47
48#[allow(dead_code)]
50const SCHEMA_VERSION: i32 = 2;
51
52impl MetadataStore {
53 pub fn open(path: &Path) -> Result<Self, MetaError> {
55 let conn = Connection::open(path)?;
56
57 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
59
60 let mut store = Self { conn };
61 store.migrate()?;
62 Ok(store)
63 }
64
65 pub fn open_in_memory() -> Result<Self, MetaError> {
67 let conn = Connection::open_in_memory()?;
68 conn.execute_batch("PRAGMA journal_mode=WAL;")?;
69 let mut store = Self { conn };
70 store.migrate()?;
71 Ok(store)
72 }
73
74 pub fn conn(&self) -> &Connection {
78 &self.conn
79 }
80
81 fn migrate(&mut self) -> Result<(), MetaError> {
83 self.conn.execute_batch(
85 "CREATE TABLE IF NOT EXISTS schema_version (
86 version INTEGER PRIMARY KEY,
87 applied_at TEXT NOT NULL DEFAULT (datetime('now'))
88 );",
89 )?;
90
91 let current_version: i32 = self
92 .conn
93 .query_row(
94 "SELECT COALESCE(MAX(version), 0) FROM schema_version",
95 [],
96 |row| row.get(0),
97 )
98 .unwrap_or(0);
99
100 if current_version < 1 {
101 self.conn.execute_batch(
102 "CREATE TABLE IF NOT EXISTS patches (
103 id TEXT PRIMARY KEY,
104 parent_ids TEXT NOT NULL,
105 operation_type TEXT NOT NULL,
106 touch_set TEXT NOT NULL,
107 target_path TEXT,
108 payload BLOB,
109 timestamp INTEGER NOT NULL,
110 author TEXT NOT NULL,
111 message TEXT NOT NULL
112 );
113
114 CREATE TABLE IF NOT EXISTS edges (
115 parent_id TEXT NOT NULL,
116 child_id TEXT NOT NULL,
117 PRIMARY KEY (parent_id, child_id)
118 );
119
120 CREATE TABLE IF NOT EXISTS branches (
121 name TEXT PRIMARY KEY,
122 target_patch_id TEXT NOT NULL,
123 created_at TEXT NOT NULL DEFAULT (datetime('now'))
124 );
125
126 CREATE TABLE IF NOT EXISTS working_set (
127 path TEXT PRIMARY KEY,
128 patch_id TEXT,
129 status TEXT NOT NULL
130 );
131
132 CREATE TABLE IF NOT EXISTS config (
133 key TEXT PRIMARY KEY,
134 value TEXT NOT NULL
135 );
136
137 CREATE INDEX IF NOT EXISTS idx_edges_parent ON edges(parent_id);
138 CREATE INDEX IF NOT EXISTS idx_edges_child ON edges(child_id);
139 CREATE INDEX IF NOT EXISTS idx_branches_target ON branches(target_patch_id);
140
141 CREATE TABLE IF NOT EXISTS public_keys (
142 author TEXT PRIMARY KEY,
143 public_key BLOB NOT NULL
144 );
145
146 CREATE TABLE IF NOT EXISTS signatures (
147 patch_id TEXT PRIMARY KEY,
148 signature BLOB NOT NULL
149 );
150 ",
151 )?;
152
153 self.conn.execute(
154 "INSERT INTO schema_version (version) VALUES (?)",
155 params![1],
156 )?;
157 }
158
159 if current_version < 2 {
160 self.conn.execute_batch(
161 "CREATE TABLE IF NOT EXISTS file_trees (
162 patch_id TEXT NOT NULL,
163 path TEXT NOT NULL,
164 blob_hash TEXT NOT NULL,
165 PRIMARY KEY (patch_id, path)
166 );
167
168 CREATE INDEX IF NOT EXISTS idx_file_trees_patch ON file_trees(patch_id);
169 CREATE INDEX IF NOT EXISTS idx_file_trees_path ON file_trees(path);
170
171 CREATE TABLE IF NOT EXISTS reflog (
172 id INTEGER PRIMARY KEY AUTOINCREMENT,
173 old_head TEXT NOT NULL,
174 new_head TEXT NOT NULL,
175 message TEXT NOT NULL,
176 timestamp INTEGER NOT NULL
177 );
178
179 CREATE INDEX IF NOT EXISTS idx_reflog_timestamp ON reflog(timestamp);
180 ",
181 )?;
182
183 self.conn.execute(
184 "INSERT INTO schema_version (version) VALUES (?)",
185 params![2],
186 )?;
187 }
188
189 Ok(())
190 }
191
192 pub fn store_patch(&self, patch: &Patch) -> Result<(), MetaError> {
194 let parent_ids_json = serde_json::to_string(&patch.parent_ids)
195 .map_err(|e| MetaError::Corrupt(e.to_string()))?;
196 let touch_set_json = serde_json::to_string(&patch.touch_set.iter().collect::<Vec<_>>())
197 .map_err(|e| MetaError::Corrupt(e.to_string()))?;
198
199 self.conn.execute(
200 "INSERT OR REPLACE INTO patches (id, parent_ids, operation_type, touch_set, target_path, payload, timestamp, author, message)
201 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
202 params![
203 patch.id.to_hex(),
204 parent_ids_json,
205 patch.operation_type.to_string(),
206 touch_set_json,
207 patch.target_path.as_deref(),
208 &patch.payload,
209 patch.timestamp as i64,
210 &patch.author,
211 &patch.message,
212 ],
213 )?;
214
215 for parent_id in &patch.parent_ids {
217 self.conn.execute(
218 "INSERT OR IGNORE INTO edges (parent_id, child_id) VALUES (?1, ?2)",
219 params![parent_id.to_hex(), patch.id.to_hex()],
220 )?;
221 }
222
223 Ok(())
224 }
225
226 pub fn get_patch(&self, id: &PatchId) -> Result<Patch, MetaError> {
228 let hex = id.to_hex();
229 self.conn
230 .query_row(
231 "SELECT id, parent_ids, operation_type, touch_set, target_path, payload, timestamp, author, message
232 FROM patches WHERE id = ?1",
233 params![hex],
234 |row| {
235 let parent_ids_json: String = row.get(1)?;
236 let op_type_str: String = row.get(2)?;
237 let touch_set_json: String = row.get(3)?;
238 let target_path: Option<String> = row.get(4)?;
239 let payload: Vec<u8> = row.get(5)?;
240 let timestamp: i64 = row.get(6)?;
241 let author: String = row.get(7)?;
242 let message: String = row.get(8)?;
243
244 let parent_ids: Vec<PatchId> = serde_json::from_str(&parent_ids_json)
245 .unwrap_or_default();
246 let touch_addrs: Vec<String> = serde_json::from_str(&touch_set_json)
247 .unwrap_or_default();
248 let touch_set = TouchSet::from_addrs(touch_addrs);
249
250 let op_type = match op_type_str.as_str() {
251 "create" => crate::patch::types::OperationType::Create,
252 "delete" => crate::patch::types::OperationType::Delete,
253 "modify" => crate::patch::types::OperationType::Modify,
254 "move" => crate::patch::types::OperationType::Move,
255 "metadata" => crate::patch::types::OperationType::Metadata,
256 "merge" => crate::patch::types::OperationType::Merge,
257 "identity" => crate::patch::types::OperationType::Identity,
258 "batch" => crate::patch::types::OperationType::Batch,
259 _ => crate::patch::types::OperationType::Modify,
260 };
261
262 Ok(Patch {
263 id: *id,
264 parent_ids,
265 operation_type: op_type,
266 touch_set,
267 target_path,
268 payload,
269 timestamp: timestamp as u64,
270 author,
271 message,
272 })
273 },
274 )
275 .map_err(|_| MetaError::PatchNotFound(hex))
276 }
277
278 pub fn set_branch(&self, name: &BranchName, target: &PatchId) -> Result<(), MetaError> {
280 self.conn.execute(
281 "INSERT OR REPLACE INTO branches (name, target_patch_id) VALUES (?1, ?2)",
282 params![name.as_str(), target.to_hex()],
283 )?;
284 Ok(())
285 }
286
287 pub fn get_branch(&self, name: &BranchName) -> Result<PatchId, MetaError> {
289 let hex: String = self
290 .conn
291 .query_row(
292 "SELECT target_patch_id FROM branches WHERE name = ?1",
293 params![name.as_str()],
294 |row| row.get(0),
295 )
296 .map_err(|_| MetaError::BranchNotFound(name.as_str().to_string()))?;
297
298 PatchId::from_hex(&hex).map_err(|e| MetaError::Corrupt(e.to_string()))
299 }
300
301 pub fn list_branches(&self) -> Result<Vec<(String, PatchId)>, MetaError> {
303 let mut stmt = self
304 .conn
305 .prepare("SELECT name, target_patch_id FROM branches ORDER BY name")?;
306
307 let branches = stmt
308 .query_map([], |row| {
309 let name: String = row.get(0)?;
310 let target_hex: String = row.get(1)?;
311 Ok((name, target_hex))
312 })?
313 .filter_map(|r| {
314 r.ok()
315 .and_then(|(name, hex)| Hash::from_hex(&hex).ok().map(|id| (name, id)))
316 })
317 .collect();
318
319 Ok(branches)
320 }
321
322 pub fn store_edge(&self, parent: &PatchId, child: &PatchId) -> Result<(), MetaError> {
324 self.conn.execute(
325 "INSERT OR IGNORE INTO edges (parent_id, child_id) VALUES (?1, ?2)",
326 params![parent.to_hex(), child.to_hex()],
327 )?;
328 Ok(())
329 }
330
331 pub fn get_edges(&self, patch_id: &PatchId) -> Result<(Vec<PatchId>, Vec<PatchId>), MetaError> {
333 let hex = patch_id.to_hex();
334
335 let parents: Vec<PatchId> = {
336 let mut stmt = self
337 .conn
338 .prepare("SELECT parent_id FROM edges WHERE child_id = ?1")?;
339 let rows = stmt.query_map(params![hex], |row| row.get::<_, String>(0))?;
340 rows.filter_map(|r| r.ok().and_then(|h| Hash::from_hex(&h).ok()))
341 .collect()
342 };
343
344 let children: Vec<PatchId> = {
345 let mut stmt = self
346 .conn
347 .prepare("SELECT child_id FROM edges WHERE parent_id = ?1")?;
348 let rows = stmt.query_map(params![hex], |row| row.get::<_, String>(0))?;
349 rows.filter_map(|r| r.ok().and_then(|h| Hash::from_hex(&h).ok()))
350 .collect()
351 };
352
353 Ok((parents, children))
354 }
355
356 pub fn working_set_add(&self, path: &RepoPath, status: FileStatus) -> Result<(), MetaError> {
358 self.conn.execute(
359 "INSERT OR REPLACE INTO working_set (path, status) VALUES (?1, ?2)",
360 params![path.as_str(), format!("{:?}", status).to_lowercase()],
361 )?;
362 Ok(())
363 }
364
365 pub fn working_set_remove(&self, path: &RepoPath) -> Result<(), MetaError> {
367 self.conn.execute(
368 "DELETE FROM working_set WHERE path = ?1",
369 params![path.as_str()],
370 )?;
371 Ok(())
372 }
373
374 pub fn working_set(&self) -> Result<Vec<(String, FileStatus)>, MetaError> {
376 let mut stmt = self
377 .conn
378 .prepare("SELECT path, status FROM working_set ORDER BY path")?;
379
380 let entries = stmt
381 .query_map([], |row| {
382 let path: String = row.get(0)?;
383 let status_str: String = row.get(1)?;
384 let status = match status_str.as_str() {
385 "added" => FileStatus::Added,
386 "modified" => FileStatus::Modified,
387 "deleted" => FileStatus::Deleted,
388 "clean" => FileStatus::Clean,
389 _ => FileStatus::Untracked,
390 };
391 Ok((path, status))
392 })?
393 .filter_map(|r| r.ok())
394 .collect();
395
396 Ok(entries)
397 }
398
399 pub fn set_config(&self, key: &str, value: &str) -> Result<(), MetaError> {
401 self.conn.execute(
402 "INSERT OR REPLACE INTO config (key, value) VALUES (?1, ?2)",
403 params![key, value],
404 )?;
405 Ok(())
406 }
407
408 pub fn list_config(&self) -> Result<Vec<(String, String)>, MetaError> {
410 let mut stmt = self
411 .conn
412 .prepare("SELECT key, value FROM config ORDER BY key")?;
413 let rows = stmt.query_map([], |row| {
414 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
415 })?;
416 let mut result = Vec::new();
417 for row in rows {
418 let Ok(pair) = row else { continue };
419 result.push(pair);
420 }
421 Ok(result)
422 }
423
424 pub fn delete_config(&self, key: &str) -> Result<(), MetaError> {
426 self.conn
427 .execute("DELETE FROM config WHERE key = ?", [key])?;
428 Ok(())
429 }
430
431 pub fn get_config(&self, key: &str) -> Result<Option<String>, MetaError> {
433 let result = self.conn.query_row(
434 "SELECT value FROM config WHERE key = ?1",
435 params![key],
436 |row| row.get::<_, String>(0),
437 );
438
439 match result {
440 Ok(value) => Ok(Some(value)),
441 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
442 Err(e) => Err(MetaError::Database(e)),
443 }
444 }
445
446 pub fn patch_count(&self) -> Result<i64, MetaError> {
448 let count: i64 = self
449 .conn
450 .query_row("SELECT COUNT(*) FROM patches", [], |row| row.get(0))?;
451 Ok(count)
452 }
453
454 pub fn store_public_key(&self, author: &str, public_key_bytes: &[u8]) -> Result<(), MetaError> {
455 self.conn
456 .execute(
457 "INSERT OR REPLACE INTO public_keys (author, public_key) VALUES (?1, ?2)",
458 params![author, public_key_bytes],
459 )
460 .map_err(|e| MetaError::Custom(e.to_string()))?;
461 Ok(())
462 }
463
464 pub fn get_public_key(&self, author: &str) -> Result<Option<Vec<u8>>, MetaError> {
465 let mut stmt = self
466 .conn
467 .prepare("SELECT public_key FROM public_keys WHERE author = ?1")
468 .map_err(|e| MetaError::Custom(e.to_string()))?;
469 let result = stmt.query_row(params![author], |row| row.get::<_, Vec<u8>>(0));
470 match result {
471 Ok(bytes) => Ok(Some(bytes)),
472 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
473 Err(e) => Err(MetaError::Custom(e.to_string())),
474 }
475 }
476
477 pub fn store_signature(&self, patch_id: &str, signature_bytes: &[u8]) -> Result<(), MetaError> {
478 self.conn
479 .execute(
480 "INSERT OR REPLACE INTO signatures (patch_id, signature) VALUES (?1, ?2)",
481 params![patch_id, signature_bytes],
482 )
483 .map_err(|e| MetaError::Custom(e.to_string()))?;
484 Ok(())
485 }
486
487 pub fn get_signature(&self, patch_id: &str) -> Result<Option<Vec<u8>>, MetaError> {
488 let mut stmt = self
489 .conn
490 .prepare("SELECT signature FROM signatures WHERE patch_id = ?1")
491 .map_err(|e| MetaError::Custom(e.to_string()))?;
492 let result = stmt.query_row(params![patch_id], |row| row.get::<_, Vec<u8>>(0));
493 match result {
494 Ok(bytes) => Ok(Some(bytes)),
495 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
496 Err(e) => Err(MetaError::Custom(e.to_string())),
497 }
498 }
499
500 pub fn store_file_tree(&self, patch_id: &PatchId, tree: &FileTree) -> Result<(), MetaError> {
508 let hex = patch_id.to_hex();
509 self.conn
510 .execute("DELETE FROM file_trees WHERE patch_id = ?1", params![hex])?;
511
512 let tx = self.conn.unchecked_transaction()?;
513 for (path, hash) in tree.iter() {
514 tx.execute(
515 "INSERT INTO file_trees (patch_id, path, blob_hash) VALUES (?1, ?2, ?3)",
516 params![hex, path.as_str(), hash.to_hex()],
517 )?;
518 }
519 tx.commit()?;
520 Ok(())
521 }
522
523 pub fn load_file_tree(&self, patch_id: &PatchId) -> Result<Option<FileTree>, MetaError> {
527 let hex = patch_id.to_hex();
528 let mut stmt = self
529 .conn
530 .prepare("SELECT path, blob_hash FROM file_trees WHERE patch_id = ?1 ORDER BY path")?;
531
532 let entries: BTreeMap<String, Hash> = stmt
533 .query_map(params![hex], |row| {
534 let path: String = row.get(0)?;
535 let hash_hex: String = row.get(1)?;
536 Ok((path, hash_hex))
537 })?
538 .filter_map(|r| {
539 r.ok().and_then(|(path, hash_hex)| {
540 Hash::from_hex(&hash_hex).ok().map(|hash| (path, hash))
541 })
542 })
543 .collect();
544
545 if entries.is_empty() {
546 Ok(None)
547 } else {
548 Ok(Some(FileTree::from_map(entries)))
549 }
550 }
551
552 pub fn file_tree_contains(&self, patch_id: &PatchId, path: &str) -> Result<bool, MetaError> {
554 let hex = patch_id.to_hex();
555 let result: i64 = self.conn.query_row(
556 "SELECT COUNT(*) FROM file_trees WHERE patch_id = ?1 AND path = ?2",
557 params![hex, path],
558 |row| row.get(0),
559 )?;
560 Ok(result > 0)
561 }
562
563 pub fn reflog_push(
569 &self,
570 old_head: &PatchId,
571 new_head: &PatchId,
572 message: &str,
573 ) -> Result<(), MetaError> {
574 self.conn.execute(
575 "INSERT INTO reflog (old_head, new_head, message, timestamp) VALUES (?1, ?2, ?3, ?4)",
576 params![
577 old_head.to_hex(),
578 new_head.to_hex(),
579 message,
580 std::time::SystemTime::now()
581 .duration_since(std::time::UNIX_EPOCH)
582 .map(|d| d.as_secs() as i64)
583 .unwrap_or(0),
584 ],
585 )?;
586 Ok(())
587 }
588
589 pub fn reflog_list(&self) -> Result<Vec<(String, String, String)>, MetaError> {
591 let mut stmt = self
592 .conn
593 .prepare("SELECT old_head, new_head, message FROM reflog ORDER BY id DESC")?;
594
595 let entries = stmt
596 .query_map([], |row| {
597 Ok((
598 row.get::<_, String>(0)?,
599 row.get::<_, String>(1)?,
600 row.get::<_, String>(2)?,
601 ))
602 })?
603 .filter_map(|r| r.ok())
604 .collect();
605
606 Ok(entries)
607 }
608
609 pub fn reflog_clear(&self) -> Result<usize, MetaError> {
611 let deleted = self.conn.execute("DELETE FROM reflog", [])?;
612 Ok(deleted)
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619 use crate::patch::types::{OperationType, Patch, TouchSet};
620
621 fn make_test_patch(addr: &str) -> Patch {
622 Patch::new(
623 OperationType::Modify,
624 TouchSet::single(addr),
625 Some(format!("file_{}", addr)),
626 vec![1, 2, 3],
627 vec![],
628 "test".to_string(),
629 format!("edit {}", addr),
630 )
631 }
632
633 #[test]
634 fn test_open_in_memory() {
635 let store = MetadataStore::open_in_memory().unwrap();
636 assert_eq!(store.patch_count().unwrap(), 0);
637 }
638
639 #[test]
640 fn test_store_and_get_patch() {
641 let store = MetadataStore::open_in_memory().unwrap();
642 let patch = make_test_patch("A1");
643 let id = patch.id;
644
645 store.store_patch(&patch).unwrap();
646 let retrieved = store.get_patch(&id).unwrap();
647
648 assert_eq!(retrieved.id, id);
649 assert_eq!(retrieved.author, "test");
650 assert_eq!(retrieved.payload, vec![1, 2, 3]);
651 }
652
653 #[test]
654 fn test_store_and_get_branch() {
655 let store = MetadataStore::open_in_memory().unwrap();
656 let patch = make_test_patch("root");
657 store.store_patch(&patch).unwrap();
658
659 let main = BranchName::new("main").unwrap();
660 store.set_branch(&main, &patch.id).unwrap();
661
662 let target = store.get_branch(&main).unwrap();
663 assert_eq!(target, patch.id);
664 }
665
666 #[test]
667 fn test_list_branches() {
668 let store = MetadataStore::open_in_memory().unwrap();
669 let patch = make_test_patch("root");
670 store.store_patch(&patch).unwrap();
671
672 store
673 .set_branch(&BranchName::new("main").unwrap(), &patch.id)
674 .unwrap();
675 store
676 .set_branch(&BranchName::new("dev").unwrap(), &patch.id)
677 .unwrap();
678
679 let branches = store.list_branches().unwrap();
680 assert_eq!(branches.len(), 2);
681 }
682
683 #[test]
684 fn test_working_set() {
685 let store = MetadataStore::open_in_memory().unwrap();
686
687 let path = RepoPath::new("src/main.rs").unwrap();
688 store.working_set_add(&path, FileStatus::Added).unwrap();
689
690 let ws = store.working_set().unwrap();
691 assert_eq!(ws.len(), 1);
692 assert_eq!(ws[0].0, "src/main.rs");
693 assert_eq!(ws[0].1, FileStatus::Added);
694
695 store.working_set_remove(&path).unwrap();
696 let ws = store.working_set().unwrap();
697 assert!(ws.is_empty());
698 }
699
700 #[test]
701 fn test_config() {
702 let store = MetadataStore::open_in_memory().unwrap();
703
704 assert!(store.get_config("key").unwrap().is_none());
705
706 store.set_config("key", "value").unwrap();
707 assert_eq!(store.get_config("key").unwrap(), Some("value".to_string()));
708
709 store.set_config("key", "updated").unwrap();
710 assert_eq!(
711 store.get_config("key").unwrap(),
712 Some("updated".to_string())
713 );
714 }
715
716 #[test]
717 fn test_edges() {
718 let store = MetadataStore::open_in_memory().unwrap();
719 let parent = make_test_patch("parent");
720 let child = make_test_patch("child");
721 store.store_patch(&parent).unwrap();
722 store.store_patch(&child).unwrap();
723
724 store.store_edge(&parent.id, &child.id).unwrap();
725
726 let (parents, _children) = store.get_edges(&child.id).unwrap();
727 assert_eq!(parents.len(), 1);
728 assert_eq!(parents[0], parent.id);
729
730 let (_, children) = store.get_edges(&parent.id).unwrap();
731 assert_eq!(children.len(), 1);
732 assert_eq!(children[0], child.id);
733 }
734
735 #[test]
736 fn test_store_and_load_file_tree() {
737 let store = MetadataStore::open_in_memory().unwrap();
738 let patch = make_test_patch("root");
739 let patch_id = patch.id;
740
741 let mut tree = FileTree::empty();
742 tree.insert("src/main.rs".to_string(), Hash::from_data(b"main"));
743 tree.insert("src/lib.rs".to_string(), Hash::from_data(b"lib"));
744
745 store.store_file_tree(&patch_id, &tree).unwrap();
746
747 let loaded = store.load_file_tree(&patch_id).unwrap().unwrap();
748 assert_eq!(loaded.len(), 2);
749 assert!(loaded.contains("src/main.rs"));
750 assert!(loaded.contains("src/lib.rs"));
751 assert_eq!(loaded.get("src/main.rs"), Some(&Hash::from_data(b"main")));
752 }
753
754 #[test]
755 fn test_file_tree_replace() {
756 let store = MetadataStore::open_in_memory().unwrap();
757 let patch = make_test_patch("root");
758 let patch_id = patch.id;
759
760 let mut tree1 = FileTree::empty();
761 tree1.insert("a.txt".to_string(), Hash::from_data(b"a"));
762
763 store.store_file_tree(&patch_id, &tree1).unwrap();
764
765 let mut tree2 = FileTree::empty();
766 tree2.insert("b.txt".to_string(), Hash::from_data(b"b"));
767
768 store.store_file_tree(&patch_id, &tree2).unwrap();
770
771 let loaded = store.load_file_tree(&patch_id).unwrap().unwrap();
772 assert_eq!(loaded.len(), 1);
773 assert!(!loaded.contains("a.txt"));
774 assert!(loaded.contains("b.txt"));
775 }
776
777 #[test]
778 fn test_file_tree_contains() {
779 let store = MetadataStore::open_in_memory().unwrap();
780 let patch = make_test_patch("root");
781 let patch_id = patch.id;
782
783 let mut tree = FileTree::empty();
784 tree.insert("tracked.txt".to_string(), Hash::from_data(b"data"));
785
786 store.store_file_tree(&patch_id, &tree).unwrap();
787
788 assert!(store.file_tree_contains(&patch_id, "tracked.txt").unwrap());
789 assert!(!store.file_tree_contains(&patch_id, "missing.txt").unwrap());
790 }
791
792 #[test]
793 fn test_load_file_tree_empty() {
794 let store = MetadataStore::open_in_memory().unwrap();
795 let patch = make_test_patch("root");
796 let patch_id = patch.id;
797
798 let result = store.load_file_tree(&patch_id).unwrap();
799 assert!(result.is_none());
800 }
801
802 #[test]
803 fn test_reflog_push_and_list() {
804 let store = MetadataStore::open_in_memory().unwrap();
805 let old = Hash::from_data(b"old");
806 let new = Hash::from_data(b"new");
807
808 store.reflog_push(&old, &new, "commit: test").unwrap();
809 store
810 .reflog_push(&new, &Hash::from_data(b"newer"), "checkout: feature")
811 .unwrap();
812
813 let log = store.reflog_list().unwrap();
814 assert_eq!(log.len(), 2);
815 assert!(log[0].2.contains("checkout"));
817 assert!(log[1].2.contains("commit"));
818 }
819
820 #[test]
821 fn test_reflog_clear() {
822 let store = MetadataStore::open_in_memory().unwrap();
823 let old = Hash::from_data(b"old");
824 let new = Hash::from_data(b"new");
825
826 store.reflog_push(&old, &new, "test").unwrap();
827 assert_eq!(store.reflog_list().unwrap().len(), 1);
828
829 let deleted = store.reflog_clear().unwrap();
830 assert_eq!(deleted, 1);
831 assert!(store.reflog_list().unwrap().is_empty());
832 }
833}