use std::str::FromStr;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictInfo {
pub path: String,
pub local_modified_at: Option<i64>,
pub remote_modified_at: Option<DateTime<Utc>>,
pub local_hash: Option<String>,
pub remote_hash: Option<String>,
}
impl ConflictInfo {
pub fn is_content_different(&self) -> bool {
match (&self.local_hash, &self.remote_hash) {
(Some(local), Some(remote)) => local != remote,
_ => true,
}
}
pub fn conflict_file_name(&self) -> String {
if let Some(dot_pos) = self.path.rfind('.') {
format!(
"{}.conflict{}",
&self.path[..dot_pos],
&self.path[dot_pos..]
)
} else {
format!("{}.conflict", self.path)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConflictResolution {
KeepLocal,
KeepRemote,
Merge {
content: String,
},
KeepBoth,
Skip,
}
impl FromStr for ConflictResolution {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"local" | "keep_local" | "keep-local" => Ok(ConflictResolution::KeepLocal),
"remote" | "keep_remote" | "keep-remote" => Ok(ConflictResolution::KeepRemote),
"both" | "keep_both" | "keep-both" => Ok(ConflictResolution::KeepBoth),
"skip" => Ok(ConflictResolution::Skip),
_ => Err(()),
}
}
}
impl ConflictResolution {
pub fn keeps_local(&self) -> bool {
matches!(
self,
ConflictResolution::KeepLocal
| ConflictResolution::KeepBoth
| ConflictResolution::Merge { .. }
)
}
pub fn keeps_remote(&self) -> bool {
matches!(
self,
ConflictResolution::KeepRemote | ConflictResolution::KeepBoth
)
}
}
#[derive(Debug)]
pub struct ConflictResolutionResult {
pub success: bool,
pub path: String,
pub conflict_file_path: Option<String>,
pub error: Option<String>,
}
impl ConflictResolutionResult {
pub fn success(path: impl Into<String>) -> Self {
Self {
success: true,
path: path.into(),
conflict_file_path: None,
error: None,
}
}
pub fn success_with_conflict_file(
path: impl Into<String>,
conflict_path: impl Into<String>,
) -> Self {
Self {
success: true,
path: path.into(),
conflict_file_path: Some(conflict_path.into()),
error: None,
}
}
pub fn failure(path: impl Into<String>, error: impl Into<String>) -> Self {
Self {
success: false,
path: path.into(),
conflict_file_path: None,
error: Some(error.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conflict_info_content_different() {
let conflict = ConflictInfo {
path: "test.md".to_string(),
local_modified_at: Some(1000),
remote_modified_at: None,
local_hash: Some("abc123".to_string()),
remote_hash: Some("abc123".to_string()),
};
assert!(!conflict.is_content_different());
let conflict = ConflictInfo {
path: "test.md".to_string(),
local_modified_at: Some(1000),
remote_modified_at: None,
local_hash: Some("abc123".to_string()),
remote_hash: Some("xyz789".to_string()),
};
assert!(conflict.is_content_different());
let conflict = ConflictInfo {
path: "test.md".to_string(),
local_modified_at: Some(1000),
remote_modified_at: None,
local_hash: Some("abc123".to_string()),
remote_hash: None,
};
assert!(conflict.is_content_different());
}
#[test]
fn test_conflict_file_name() {
let conflict = ConflictInfo {
path: "notes/test.md".to_string(),
local_modified_at: None,
remote_modified_at: None,
local_hash: None,
remote_hash: None,
};
assert_eq!(conflict.conflict_file_name(), "notes/test.conflict.md");
let conflict = ConflictInfo {
path: "README".to_string(),
local_modified_at: None,
remote_modified_at: None,
local_hash: None,
remote_hash: None,
};
assert_eq!(conflict.conflict_file_name(), "README.conflict");
}
#[test]
fn test_conflict_resolution_from_str() {
assert!(matches!(
ConflictResolution::from_str("local"),
Ok(ConflictResolution::KeepLocal)
));
assert!(matches!(
ConflictResolution::from_str("keep-remote"),
Ok(ConflictResolution::KeepRemote)
));
assert!(matches!(
ConflictResolution::from_str("BOTH"),
Ok(ConflictResolution::KeepBoth)
));
assert!(matches!(
ConflictResolution::from_str("skip"),
Ok(ConflictResolution::Skip)
));
assert!(ConflictResolution::from_str("invalid").is_err());
}
#[test]
fn test_resolution_keeps_versions() {
assert!(ConflictResolution::KeepLocal.keeps_local());
assert!(!ConflictResolution::KeepLocal.keeps_remote());
assert!(!ConflictResolution::KeepRemote.keeps_local());
assert!(ConflictResolution::KeepRemote.keeps_remote());
assert!(ConflictResolution::KeepBoth.keeps_local());
assert!(ConflictResolution::KeepBoth.keeps_remote());
let merge = ConflictResolution::Merge {
content: "merged".to_string(),
};
assert!(merge.keeps_local());
assert!(!merge.keeps_remote());
}
}