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