void_core/ops/
merge_state.rs1use std::fs;
12use std::path::Path;
13
14use serde::{Deserialize, Serialize};
15use void_crypto::CommitCid;
16
17use crate::support::util::atomic_write_str;
18use crate::support::Result;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct MergeState {
23 pub merge_head: CommitCid,
25 pub merge_base: Option<CommitCid>,
27 pub orig_head: CommitCid,
29 pub conflicts: Vec<String>,
31 pub message: String,
33}
34
35const MERGE_HEAD: &str = "MERGE_HEAD";
36const MERGE_BASE: &str = "MERGE_BASE";
37const MERGE_MSG: &str = "MERGE_MSG";
38const MERGE_CONFLICTS: &str = "MERGE_CONFLICTS";
39const ORIG_HEAD: &str = "ORIG_HEAD";
40
41pub fn read_merge_state(void_dir: &Path) -> Result<Option<MergeState>> {
45 let merge_head_path = void_dir.join(MERGE_HEAD);
46
47 if !merge_head_path.exists() {
49 return Ok(None);
50 }
51
52 let merge_head_hex = fs::read_to_string(&merge_head_path)?;
54 let merge_head = CommitCid::from_bytes(hex::decode(merge_head_hex.trim())
55 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?);
56
57 let orig_head_path = void_dir.join(ORIG_HEAD);
59 let orig_head_hex = fs::read_to_string(orig_head_path)?;
60 let orig_head = CommitCid::from_bytes(hex::decode(orig_head_hex.trim())
61 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?);
62
63 let merge_base_path = void_dir.join(MERGE_BASE);
65 let merge_base = if merge_base_path.exists() {
66 let hex_str = fs::read_to_string(&merge_base_path)?;
67 Some(CommitCid::from_bytes(
68 hex::decode(hex_str.trim())
69 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?,
70 ))
71 } else {
72 None
73 };
74
75 let message_path = void_dir.join(MERGE_MSG);
77 let message = if message_path.exists() {
78 fs::read_to_string(&message_path)?
79 } else {
80 String::new()
81 };
82
83 let conflicts_path = void_dir.join(MERGE_CONFLICTS);
85 let conflicts: Vec<String> = if conflicts_path.exists() {
86 let content = fs::read_to_string(&conflicts_path)?;
87 serde_json::from_str(&content)
88 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?
89 } else {
90 Vec::new()
91 };
92
93 Ok(Some(MergeState {
94 merge_head,
95 merge_base,
96 orig_head,
97 conflicts,
98 message,
99 }))
100}
101
102pub fn write_merge_state(void_dir: &Path, state: &MergeState) -> Result<()> {
106 let merge_head_hex = hex::encode(state.merge_head.as_bytes());
108 atomic_write_str(void_dir.join(MERGE_HEAD), &merge_head_hex)?;
109
110 let orig_head_hex = hex::encode(state.orig_head.as_bytes());
112 atomic_write_str(void_dir.join(ORIG_HEAD), &orig_head_hex)?;
113
114 if let Some(ref base) = state.merge_base {
116 let base_hex = hex::encode(base.as_bytes());
117 atomic_write_str(void_dir.join(MERGE_BASE), &base_hex)?;
118 }
119
120 atomic_write_str(void_dir.join(MERGE_MSG), &state.message)?;
122
123 let conflicts_json = serde_json::to_string(&state.conflicts)
125 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
126 atomic_write_str(void_dir.join(MERGE_CONFLICTS), &conflicts_json)?;
127
128 Ok(())
129}
130
131pub fn clear_merge_state(void_dir: &Path) -> Result<()> {
136 let files = [
137 MERGE_HEAD,
138 MERGE_BASE,
139 MERGE_MSG,
140 MERGE_CONFLICTS,
141 ORIG_HEAD,
142 ];
143
144 for file in files {
145 let path = void_dir.join(file);
146 if path.exists() {
147 fs::remove_file(&path)?;
148 }
149 }
150
151 Ok(())
152}
153
154pub fn is_merge_in_progress(void_dir: &Path) -> bool {
158 void_dir.join(MERGE_HEAD).exists()
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use tempfile::TempDir;
165
166 fn sample_state() -> MergeState {
167 MergeState {
168 merge_head: CommitCid::from_bytes(vec![0x01, 0x02, 0x03, 0x04]),
169 merge_base: Some(CommitCid::from_bytes(vec![0x05, 0x06, 0x07, 0x08])),
170 orig_head: CommitCid::from_bytes(vec![0x09, 0x0a, 0x0b, 0x0c]),
171 conflicts: vec!["src/main.rs".to_string(), "README.md".to_string()],
172 message: "Merge branch 'feature' into main".to_string(),
173 }
174 }
175
176 #[test]
177 fn write_read_roundtrip() {
178 let temp = TempDir::new().unwrap();
179 let void_dir = temp.path();
180
181 let state = sample_state();
182 write_merge_state(void_dir, &state).unwrap();
183
184 let read_state = read_merge_state(void_dir).unwrap().unwrap();
185
186 assert_eq!(read_state.merge_head, state.merge_head);
187 assert_eq!(read_state.merge_base, state.merge_base);
188 assert_eq!(read_state.orig_head, state.orig_head);
189 assert_eq!(read_state.conflicts, state.conflicts);
190 assert_eq!(read_state.message, state.message);
191 }
192
193 #[test]
194 fn write_read_roundtrip_no_base() {
195 let temp = TempDir::new().unwrap();
196 let void_dir = temp.path();
197
198 let mut state = sample_state();
199 state.merge_base = None;
200 write_merge_state(void_dir, &state).unwrap();
201
202 let read_state = read_merge_state(void_dir).unwrap().unwrap();
203
204 assert_eq!(read_state.merge_base, None);
205 }
206
207 #[test]
208 fn write_read_roundtrip_empty_conflicts() {
209 let temp = TempDir::new().unwrap();
210 let void_dir = temp.path();
211
212 let mut state = sample_state();
213 state.conflicts = Vec::new();
214 write_merge_state(void_dir, &state).unwrap();
215
216 let read_state = read_merge_state(void_dir).unwrap().unwrap();
217
218 assert!(read_state.conflicts.is_empty());
219 }
220
221 #[test]
222 fn clear_removes_all_files() {
223 let temp = TempDir::new().unwrap();
224 let void_dir = temp.path();
225
226 let state = sample_state();
227 write_merge_state(void_dir, &state).unwrap();
228
229 assert!(void_dir.join(MERGE_HEAD).exists());
231 assert!(void_dir.join(MERGE_BASE).exists());
232 assert!(void_dir.join(MERGE_MSG).exists());
233 assert!(void_dir.join(MERGE_CONFLICTS).exists());
234 assert!(void_dir.join(ORIG_HEAD).exists());
235
236 clear_merge_state(void_dir).unwrap();
237
238 assert!(!void_dir.join(MERGE_HEAD).exists());
240 assert!(!void_dir.join(MERGE_BASE).exists());
241 assert!(!void_dir.join(MERGE_MSG).exists());
242 assert!(!void_dir.join(MERGE_CONFLICTS).exists());
243 assert!(!void_dir.join(ORIG_HEAD).exists());
244 }
245
246 #[test]
247 fn clear_handles_missing_files() {
248 let temp = TempDir::new().unwrap();
249 let void_dir = temp.path();
250
251 clear_merge_state(void_dir).unwrap();
253 }
254
255 #[test]
256 fn is_merge_in_progress_true() {
257 let temp = TempDir::new().unwrap();
258 let void_dir = temp.path();
259
260 let state = sample_state();
261 write_merge_state(void_dir, &state).unwrap();
262
263 assert!(is_merge_in_progress(void_dir));
264 }
265
266 #[test]
267 fn is_merge_in_progress_false() {
268 let temp = TempDir::new().unwrap();
269 let void_dir = temp.path();
270
271 assert!(!is_merge_in_progress(void_dir));
272 }
273
274 #[test]
275 fn is_merge_in_progress_after_clear() {
276 let temp = TempDir::new().unwrap();
277 let void_dir = temp.path();
278
279 let state = sample_state();
280 write_merge_state(void_dir, &state).unwrap();
281 assert!(is_merge_in_progress(void_dir));
282
283 clear_merge_state(void_dir).unwrap();
284 assert!(!is_merge_in_progress(void_dir));
285 }
286
287 #[test]
288 fn read_when_no_merge_returns_none() {
289 let temp = TempDir::new().unwrap();
290 let void_dir = temp.path();
291
292 let result = read_merge_state(void_dir).unwrap();
293 assert!(result.is_none());
294 }
295}