skillfile_core/
conflict.rs1use std::path::Path;
2
3use crate::error::SkillfileError;
4use crate::models::ConflictState;
5
6pub const CONFLICT_FILE: &str = ".skillfile/conflict";
7
8pub fn read_conflict(repo_root: &Path) -> Result<Option<ConflictState>, SkillfileError> {
10 let p = repo_root.join(CONFLICT_FILE);
11 if !p.exists() {
12 return Ok(None);
13 }
14 let text = std::fs::read_to_string(&p)?;
15 let state: ConflictState = serde_json::from_str(&text)
16 .map_err(|e| SkillfileError::Manifest(format!("invalid conflict file: {e}")))?;
17 Ok(Some(state))
18}
19
20pub fn write_conflict(repo_root: &Path, state: &ConflictState) -> Result<(), SkillfileError> {
22 let p = repo_root.join(CONFLICT_FILE);
23 if let Some(parent) = p.parent() {
24 std::fs::create_dir_all(parent)?;
25 }
26 let json = serde_json::to_string_pretty(state)
27 .map_err(|e| SkillfileError::Manifest(format!("failed to serialize conflict: {e}")))?;
28 std::fs::write(&p, format!("{json}\n"))?;
29 Ok(())
30}
31
32pub fn clear_conflict(repo_root: &Path) -> Result<(), SkillfileError> {
34 let p = repo_root.join(CONFLICT_FILE);
35 if p.exists() {
36 std::fs::remove_file(&p)?;
37 }
38 Ok(())
39}
40
41#[must_use]
43pub fn has_conflict(repo_root: &Path) -> bool {
44 repo_root.join(CONFLICT_FILE).exists()
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50
51 fn make_state() -> ConflictState {
52 ConflictState {
53 entry: "foo".into(),
54 entity_type: "agent".into(),
55 old_sha: "a".repeat(40),
56 new_sha: "b".repeat(40),
57 }
58 }
59
60 #[test]
65 fn read_missing_returns_none() {
66 let dir = tempfile::tempdir().unwrap();
67 assert!(read_conflict(dir.path()).unwrap().is_none());
68 }
69
70 #[test]
71 fn write_then_read_roundtrip() {
72 let dir = tempfile::tempdir().unwrap();
73 let state = make_state();
74 write_conflict(dir.path(), &state).unwrap();
75 assert_eq!(read_conflict(dir.path()).unwrap(), Some(state));
76 }
77
78 #[test]
83 fn write_produces_valid_json_structure() {
84 let dir = tempfile::tempdir().unwrap();
85 let state = ConflictState {
86 entry: "bar".into(),
87 entity_type: "skill".into(),
88 ..make_state()
89 };
90 write_conflict(dir.path(), &state).unwrap();
91 let data: serde_json::Value =
92 serde_json::from_str(&std::fs::read_to_string(dir.path().join(CONFLICT_FILE)).unwrap())
93 .unwrap();
94 assert_eq!(data["entry"], "bar");
95 assert_eq!(data["entity_type"], "skill");
96 assert_eq!(data["old_sha"], "a".repeat(40));
97 assert_eq!(data["new_sha"], "b".repeat(40));
98 }
99
100 #[test]
101 fn write_creates_file() {
102 let dir = tempfile::tempdir().unwrap();
103 write_conflict(dir.path(), &make_state()).unwrap();
104 assert!(dir.path().join(CONFLICT_FILE).exists());
105 }
106
107 #[test]
112 fn has_conflict_false_when_missing() {
113 let dir = tempfile::tempdir().unwrap();
114 assert!(!has_conflict(dir.path()));
115 }
116
117 #[test]
118 fn has_conflict_true_after_write() {
119 let dir = tempfile::tempdir().unwrap();
120 write_conflict(dir.path(), &make_state()).unwrap();
121 assert!(has_conflict(dir.path()));
122 }
123
124 #[test]
129 fn clear_removes_file() {
130 let dir = tempfile::tempdir().unwrap();
131 write_conflict(dir.path(), &make_state()).unwrap();
132 clear_conflict(dir.path()).unwrap();
133 assert!(!has_conflict(dir.path()));
134 assert!(!dir.path().join(CONFLICT_FILE).exists());
135 }
136
137 #[test]
138 fn clear_noop_when_missing() {
139 let dir = tempfile::tempdir().unwrap();
140 clear_conflict(dir.path()).unwrap(); }
142}