use std::path::Path;
use crate::error::SkillfileError;
use crate::models::ConflictState;
pub const CONFLICT_FILE: &str = ".skillfile/conflict";
pub fn read_conflict(repo_root: &Path) -> Result<Option<ConflictState>, SkillfileError> {
let p = repo_root.join(CONFLICT_FILE);
if !p.exists() {
return Ok(None);
}
let text = std::fs::read_to_string(&p)?;
let state: ConflictState = serde_json::from_str(&text)
.map_err(|e| SkillfileError::Manifest(format!("invalid conflict file: {e}")))?;
Ok(Some(state))
}
pub fn write_conflict(repo_root: &Path, state: &ConflictState) -> Result<(), SkillfileError> {
let p = repo_root.join(CONFLICT_FILE);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(state)
.map_err(|e| SkillfileError::Manifest(format!("failed to serialize conflict: {e}")))?;
std::fs::write(&p, format!("{json}\n"))?;
Ok(())
}
pub fn clear_conflict(repo_root: &Path) -> Result<(), SkillfileError> {
let p = repo_root.join(CONFLICT_FILE);
if p.exists() {
std::fs::remove_file(&p)?;
}
Ok(())
}
#[must_use]
pub fn has_conflict(repo_root: &Path) -> bool {
repo_root.join(CONFLICT_FILE).exists()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::EntityType;
fn make_state() -> ConflictState {
ConflictState {
entry: "foo".into(),
entity_type: EntityType::Agent,
old_sha: "a".repeat(40),
new_sha: "b".repeat(40),
}
}
#[test]
fn read_missing_returns_none() {
let dir = tempfile::tempdir().unwrap();
assert!(read_conflict(dir.path()).unwrap().is_none());
}
#[test]
fn write_then_read_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let state = make_state();
write_conflict(dir.path(), &state).unwrap();
assert_eq!(read_conflict(dir.path()).unwrap(), Some(state));
}
#[test]
fn write_produces_valid_json_structure() {
let dir = tempfile::tempdir().unwrap();
let state = ConflictState {
entry: "bar".into(),
entity_type: EntityType::Skill,
..make_state()
};
write_conflict(dir.path(), &state).unwrap();
let data: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(dir.path().join(CONFLICT_FILE)).unwrap())
.unwrap();
assert_eq!(data["entry"], "bar");
assert_eq!(data["entity_type"], "skill");
assert_eq!(data["old_sha"], "a".repeat(40));
assert_eq!(data["new_sha"], "b".repeat(40));
}
#[test]
fn write_creates_file() {
let dir = tempfile::tempdir().unwrap();
write_conflict(dir.path(), &make_state()).unwrap();
assert!(dir.path().join(CONFLICT_FILE).exists());
}
#[test]
fn has_conflict_false_when_missing() {
let dir = tempfile::tempdir().unwrap();
assert!(!has_conflict(dir.path()));
}
#[test]
fn has_conflict_true_after_write() {
let dir = tempfile::tempdir().unwrap();
write_conflict(dir.path(), &make_state()).unwrap();
assert!(has_conflict(dir.path()));
}
#[test]
fn clear_removes_file() {
let dir = tempfile::tempdir().unwrap();
write_conflict(dir.path(), &make_state()).unwrap();
clear_conflict(dir.path()).unwrap();
assert!(!has_conflict(dir.path()));
assert!(!dir.path().join(CONFLICT_FILE).exists());
}
#[test]
fn clear_noop_when_missing() {
let dir = tempfile::tempdir().unwrap();
clear_conflict(dir.path()).unwrap(); }
}