1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::path::Path;
5
6use crate::graph::Graph;
7
8#[derive(Debug, Serialize, Deserialize, PartialEq)]
9pub struct Lockfile {
10 pub lockfile_version: u32,
11 #[serde(default)]
12 pub nodes: BTreeMap<String, LockfileNode>,
13}
14
15#[derive(Debug, Serialize, Deserialize, PartialEq)]
16pub struct LockfileNode {
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub hash: Option<String>,
19}
20
21impl Lockfile {
22 pub fn from_graph(graph: &Graph) -> Self {
26 let mut nodes = BTreeMap::new();
27 for (path, node) in &graph.nodes {
28 if !node.included {
29 continue;
30 }
31 nodes.insert(
32 path.clone(),
33 LockfileNode {
34 hash: node.hash.clone(),
35 },
36 );
37 }
38
39 Lockfile {
40 lockfile_version: 2,
41 nodes,
42 }
43 }
44
45 pub fn to_toml(&self) -> Result<String> {
47 toml::to_string_pretty(self).context("failed to serialize lockfile")
48 }
49
50 pub fn from_toml(content: &str) -> Result<Self> {
52 let lockfile: Self = toml::from_str(content).context("failed to parse lockfile")?;
53 Ok(lockfile)
54 }
55}
56
57pub fn read_lockfile(root: &Path) -> Result<Option<Lockfile>> {
60 let path = root.join("drft.lock");
61 if !path.exists() {
62 return Ok(None);
63 }
64 let content = std::fs::read_to_string(&path)
65 .with_context(|| format!("failed to read {}", path.display()))?;
66 let lockfile = Lockfile::from_toml(&content)?;
67 Ok(Some(lockfile))
68}
69
70pub fn write_lockfile(root: &Path, lockfile: &Lockfile) -> Result<()> {
72 let content = lockfile.to_toml()?;
73 let lock_path = root.join("drft.lock");
74 let tmp_path = root.join("drft.lock.tmp");
75
76 std::fs::write(&tmp_path, &content)
77 .with_context(|| format!("failed to write {}", tmp_path.display()))?;
78
79 std::fs::rename(&tmp_path, &lock_path).with_context(|| {
80 let _ = std::fs::remove_file(&tmp_path);
81 format!(
82 "failed to rename {} to {}",
83 tmp_path.display(),
84 lock_path.display()
85 )
86 })?;
87
88 Ok(())
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::graph::Node;
95 use std::collections::HashMap;
96 use tempfile::TempDir;
97
98 fn make_graph() -> Graph {
99 let mut g = Graph::new();
100 g.add_node(Node {
101 path: "index.md".into(),
102 node_type: Some(crate::graph::NodeType::File),
103 included: true,
104 hash: Some("b3:aaa".into()),
105 metadata: HashMap::new(),
106 });
107 g.add_node(Node {
108 path: "setup.md".into(),
109 node_type: Some(crate::graph::NodeType::File),
110 included: true,
111 hash: Some("b3:bbb".into()),
112 metadata: HashMap::new(),
113 });
114 g
115 }
116
117 #[test]
118 fn from_graph_produces_sorted_nodes() {
119 let lf = Lockfile::from_graph(&make_graph());
120 let keys: Vec<&String> = lf.nodes.keys().collect();
121 assert_eq!(keys, vec!["index.md", "setup.md"]);
122 }
123
124 #[test]
125 fn roundtrip_toml() {
126 let lf = Lockfile::from_graph(&make_graph());
127 let toml_str = lf.to_toml().unwrap();
128 let parsed = Lockfile::from_toml(&toml_str).unwrap();
129 assert_eq!(lf, parsed);
130 }
131
132 #[test]
133 fn deterministic_output() {
134 let lf = Lockfile::from_graph(&make_graph());
135 let a = lf.to_toml().unwrap();
136 let b = lf.to_toml().unwrap();
137 assert_eq!(a, b);
138 }
139
140 #[test]
141 fn write_and_read() {
142 let dir = TempDir::new().unwrap();
143 let lf = Lockfile::from_graph(&make_graph());
144 write_lockfile(dir.path(), &lf).unwrap();
145 let read_back = read_lockfile(dir.path()).unwrap().unwrap();
146 assert_eq!(lf, read_back);
147 }
148
149 #[test]
150 fn read_missing_returns_none() {
151 let dir = TempDir::new().unwrap();
152 assert!(read_lockfile(dir.path()).unwrap().is_none());
153 }
154
155 #[test]
156 fn no_edges_in_lockfile() {
157 let lf = Lockfile::from_graph(&make_graph());
158 let toml_str = lf.to_toml().unwrap();
159 assert!(
160 !toml_str.contains("[[edges]]"),
161 "lockfile v2 should not contain edges"
162 );
163 }
164
165 #[test]
166 fn parses_current_lockfile_version() {
167 let toml = "lockfile_version = 2\n";
168 let result = Lockfile::from_toml(toml);
169 assert!(result.is_ok());
170 }
171}