Skip to main content

opendev_models/
file_change.rs

1//! File change tracking models.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use strum::{Display, EnumString};
6use uuid::Uuid;
7
8/// Types of file changes.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
10#[serde(rename_all = "lowercase")]
11#[strum(serialize_all = "lowercase")]
12pub enum FileChangeType {
13    Created,
14    Modified,
15    Deleted,
16    Renamed,
17}
18
19/// Represents a file change within a session.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct FileChange {
22    #[serde(default = "generate_change_id")]
23    pub id: String,
24    #[serde(rename = "type")]
25    pub change_type: FileChangeType,
26    pub file_path: String,
27    /// For renames.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub old_path: Option<String>,
30    #[serde(default = "Utc::now", with = "crate::datetime_compat")]
31    pub timestamp: DateTime<Utc>,
32    #[serde(default)]
33    pub lines_added: u64,
34    #[serde(default)]
35    pub lines_removed: u64,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub tool_call_id: Option<String>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub session_id: Option<String>,
40    /// Human-readable description.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub description: Option<String>,
43}
44
45fn generate_change_id() -> String {
46    Uuid::new_v4().to_string()[..8].to_string()
47}
48
49impl FileChange {
50    /// Create a new FileChange with defaults.
51    pub fn new(change_type: FileChangeType, file_path: String) -> Self {
52        Self {
53            id: generate_change_id(),
54            change_type,
55            file_path,
56            old_path: None,
57            timestamp: Utc::now(),
58            lines_added: 0,
59            lines_removed: 0,
60            tool_call_id: None,
61            session_id: None,
62            description: None,
63        }
64    }
65
66    /// Get file icon based on type.
67    pub fn get_file_icon(&self) -> &'static str {
68        match self.change_type {
69            FileChangeType::Created => "+",
70            FileChangeType::Modified => "~",
71            FileChangeType::Deleted => "-",
72            FileChangeType::Renamed => ">",
73        }
74    }
75
76    /// Get status color for UI display.
77    pub fn get_status_color(&self) -> &'static str {
78        match self.change_type {
79            FileChangeType::Created => "green",
80            FileChangeType::Modified => "blue",
81            FileChangeType::Deleted => "red",
82            FileChangeType::Renamed => "orange",
83        }
84    }
85
86    /// Get human-readable change summary.
87    pub fn get_change_summary(&self) -> String {
88        match self.change_type {
89            FileChangeType::Created => "New file".to_string(),
90            FileChangeType::Modified => {
91                if self.lines_added > 0 && self.lines_removed > 0 {
92                    format!("+{} -{}", self.lines_added, self.lines_removed)
93                } else if self.lines_added > 0 {
94                    format!("+{}", self.lines_added)
95                } else if self.lines_removed > 0 {
96                    format!("-{}", self.lines_removed)
97                } else {
98                    "Modified".to_string()
99                }
100            }
101            FileChangeType::Deleted => "Deleted".to_string(),
102            FileChangeType::Renamed => format!("Renamed -> {}", self.file_path),
103        }
104    }
105}
106
107#[cfg(test)]
108#[path = "file_change_tests.rs"]
109mod tests;