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> {
21 let p = repo_root.join(CONFLICT_FILE);
22 if let Some(parent) = p.parent() {
23 std::fs::create_dir_all(parent)?;
24 }
25 let json = serde_json::to_string_pretty(state)
26 .map_err(|e| SkillfileError::Manifest(format!("failed to serialize conflict: {e}")))?;
27 std::fs::write(&p, format!("{json}\n"))?;
28 Ok(())
29}
30
31pub fn clear_conflict(repo_root: &Path) -> Result<(), SkillfileError> {
33 let p = repo_root.join(CONFLICT_FILE);
34 if p.exists() {
35 std::fs::remove_file(&p)?;
36 }
37 Ok(())
38}
39
40#[must_use]
41pub fn has_conflict(repo_root: &Path) -> bool {
42 repo_root.join(CONFLICT_FILE).exists()
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use crate::models::EntityType;
49
50 fn make_state() -> ConflictState {
51 ConflictState {
52 entry: "foo".into(),
53 entity_type: EntityType::Agent,
54 old_sha: "a".repeat(40),
55 new_sha: "b".repeat(40),
56 }
57 }
58
59 #[test]
64 fn read_missing_returns_none() {
65 let dir = tempfile::tempdir().unwrap();
66 assert!(read_conflict(dir.path()).unwrap().is_none());
67 }
68
69 #[test]
70 fn write_then_read_roundtrip() {
71 let dir = tempfile::tempdir().unwrap();
72 let state = make_state();
73 write_conflict(dir.path(), &state).unwrap();
74 assert_eq!(read_conflict(dir.path()).unwrap(), Some(state));
75 }
76
77 #[test]
82 fn write_produces_valid_json_structure() {
83 let dir = tempfile::tempdir().unwrap();
84 let state = ConflictState {
85 entry: "bar".into(),
86 entity_type: EntityType::Skill,
87 ..make_state()
88 };
89 write_conflict(dir.path(), &state).unwrap();
90 let data: serde_json::Value =
91 serde_json::from_str(&std::fs::read_to_string(dir.path().join(CONFLICT_FILE)).unwrap())
92 .unwrap();
93 assert_eq!(data["entry"], "bar");
94 assert_eq!(data["entity_type"], "skill");
95 assert_eq!(data["old_sha"], "a".repeat(40));
96 assert_eq!(data["new_sha"], "b".repeat(40));
97 }
98
99 #[test]
100 fn write_creates_file() {
101 let dir = tempfile::tempdir().unwrap();
102 write_conflict(dir.path(), &make_state()).unwrap();
103 assert!(dir.path().join(CONFLICT_FILE).exists());
104 }
105
106 #[test]
111 fn has_conflict_false_when_missing() {
112 let dir = tempfile::tempdir().unwrap();
113 assert!(!has_conflict(dir.path()));
114 }
115
116 #[test]
117 fn has_conflict_true_after_write() {
118 let dir = tempfile::tempdir().unwrap();
119 write_conflict(dir.path(), &make_state()).unwrap();
120 assert!(has_conflict(dir.path()));
121 }
122
123 #[test]
128 fn clear_removes_file() {
129 let dir = tempfile::tempdir().unwrap();
130 write_conflict(dir.path(), &make_state()).unwrap();
131 clear_conflict(dir.path()).unwrap();
132 assert!(!has_conflict(dir.path()));
133 assert!(!dir.path().join(CONFLICT_FILE).exists());
134 }
135
136 #[test]
137 fn clear_noop_when_missing() {
138 let dir = tempfile::tempdir().unwrap();
139 clear_conflict(dir.path()).unwrap(); }
141}