openvcs_core/
models.rs

1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3
4#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
5#[serde(tag = "type")]
6pub enum BranchKind {
7    Local,
8    Remote { remote: String },
9    Unknown,
10}
11
12#[derive(Serialize, Deserialize, Default, Clone, Copy, Debug, PartialEq, Eq)]
13pub struct StatusSummary {
14    pub untracked: usize,
15    pub modified: usize,
16    pub staged: usize,
17    pub conflicted: usize,
18}
19
20#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
21pub struct BranchItem {
22    pub name: String,
23    pub full_ref: String,
24    pub kind: BranchKind,
25    pub current: bool,
26}
27
28/// A single file’s status in the working tree / index.
29/// `status` is backend-agnostic (e.g., "A" | "M" | "D" | "R?" etc).
30#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
31pub struct FileEntry {
32    pub path: String,
33    #[serde(default)]
34    pub old_path: Option<String>,
35    pub status: String,
36    #[serde(default)]
37    pub staged: bool,
38    #[serde(default)]
39    pub resolved_conflict: bool,
40    pub hunks: Vec<String>,
41}
42
43#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
44pub struct ConflictDetails {
45    pub path: String,
46    pub ours: Option<String>,
47    pub theirs: Option<String>,
48    pub base: Option<String>,
49    #[serde(default)]
50    pub binary: bool,
51    #[serde(default)]
52    pub lfs_pointer: bool,
53}
54
55#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
56#[serde(rename_all = "kebab-case")]
57pub enum ConflictSide {
58    Ours,
59    Theirs,
60}
61
62/// Flat status summary plus file list.
63#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
64pub struct StatusPayload {
65    pub files: Vec<FileEntry>,
66    pub ahead: u32,
67    pub behind: u32,
68}
69
70/// Lightweight commit representation for lists.
71#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
72pub struct CommitItem {
73    pub id: String,
74    pub msg: String,
75    pub meta: String,
76    pub author: String,
77}
78
79/// Options controlling fetch behavior.
80#[derive(Serialize, Deserialize, Default, Clone, Copy, Debug, PartialEq, Eq)]
81pub struct FetchOptions {
82    pub prune: bool,
83}
84
85/// A single stash entry (backend-agnostic)
86#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
87pub struct StashItem {
88    pub selector: String,
89    pub msg: String,
90    pub meta: String,
91}
92
93/// Query for commit history.
94#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
95pub struct LogQuery {
96    pub rev: Option<String>,
97    pub path: Option<String>,
98    pub since_utc: Option<String>,
99    pub until_utc: Option<String>,
100    pub author_contains: Option<String>,
101    pub skip: u32,
102    pub limit: u32,
103    pub topo_order: bool,
104    pub include_merges: bool,
105}
106
107impl LogQuery {
108    pub fn head(limit: u32) -> Self {
109        Self {
110            limit,
111            ..Default::default()
112        }
113    }
114}
115
116#[derive(Serialize, Deserialize, Clone, Debug, Default)]
117pub struct Capabilities {
118    pub commits: bool,
119    pub branches: bool,
120    pub tags: bool,
121    pub staging: bool,
122    pub push_pull: bool,
123    pub fast_forward: bool,
124}
125
126#[derive(Serialize, Deserialize, Clone, Debug)]
127#[serde(tag = "type", rename_all = "kebab-case")]
128pub enum VcsEvent {
129    Info {
130        msg: String,
131    },
132    RemoteMessage {
133        msg: String,
134    },
135    Progress {
136        phase: String,
137        detail: String,
138    },
139    Auth {
140        method: String,
141        detail: String,
142    },
143    PushStatus {
144        refname: String,
145        status: Option<String>,
146    },
147    Warning {
148        msg: String,
149    },
150    Error {
151        msg: String,
152    },
153}
154
155pub type OnEvent = Arc<dyn Fn(VcsEvent) + Send + Sync + 'static>;
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn log_query_head_sets_limit_and_defaults_rest() {
163        let query = LogQuery::head(25);
164        assert_eq!(query.limit, 25);
165        assert!(query.rev.is_none());
166        assert!(query.path.is_none());
167        assert_eq!(query.skip, 0);
168        assert!(!query.topo_order);
169    }
170
171    #[test]
172    fn status_summary_default_is_zeroed() {
173        let summary = StatusSummary::default();
174        assert_eq!(summary.untracked, 0);
175        assert_eq!(summary.modified, 0);
176        assert_eq!(summary.staged, 0);
177        assert_eq!(summary.conflicted, 0);
178    }
179
180    #[test]
181    fn branch_kind_roundtrips_via_json() {
182        let local = BranchKind::Local;
183        let local_json = serde_json::to_value(&local).expect("serialize");
184        let local_back: BranchKind = serde_json::from_value(local_json).expect("deserialize");
185        assert_eq!(local_back, BranchKind::Local);
186
187        let remote = BranchKind::Remote {
188            remote: "origin".into(),
189        };
190        let remote_json = serde_json::to_value(&remote).expect("serialize");
191        let remote_back: BranchKind = serde_json::from_value(remote_json).expect("deserialize");
192        assert_eq!(remote_back, remote);
193    }
194
195    #[test]
196    fn conflict_side_serializes_as_kebab_case_strings() {
197        let ours = serde_json::to_value(ConflictSide::Ours).expect("serialize");
198        let theirs = serde_json::to_value(ConflictSide::Theirs).expect("serialize");
199        assert_eq!(ours, serde_json::Value::String("ours".into()));
200        assert_eq!(theirs, serde_json::Value::String("theirs".into()));
201
202        let ours_back: ConflictSide = serde_json::from_value(serde_json::json!("ours")).unwrap();
203        let theirs_back: ConflictSide =
204            serde_json::from_value(serde_json::json!("theirs")).unwrap();
205        assert_eq!(ours_back, ConflictSide::Ours);
206        assert_eq!(theirs_back, ConflictSide::Theirs);
207    }
208
209    #[test]
210    fn file_entry_deserializes_optional_fields_with_defaults() {
211        let v = serde_json::json!({
212            "path": "a.txt",
213            "status": "M",
214            "hunks": []
215        });
216
217        let entry: FileEntry = serde_json::from_value(v).expect("deserialize");
218        assert_eq!(entry.path, "a.txt");
219        assert_eq!(entry.status, "M");
220        assert!(entry.old_path.is_none());
221        assert!(!entry.staged);
222        assert!(!entry.resolved_conflict);
223        assert!(entry.hunks.is_empty());
224    }
225
226    #[test]
227    fn vcs_event_roundtrips_via_json() {
228        let events = vec![
229            VcsEvent::Info {
230                msg: "hello".into(),
231            },
232            VcsEvent::Progress {
233                phase: "fetch".into(),
234                detail: "10/20".into(),
235            },
236            VcsEvent::Auth {
237                method: "ssh".into(),
238                detail: "key".into(),
239            },
240            VcsEvent::RemoteMessage {
241                msg: "remote".into(),
242            },
243            VcsEvent::Warning { msg: "warn".into() },
244            VcsEvent::Error { msg: "err".into() },
245        ];
246
247        for e in events {
248            let v = serde_json::to_value(&e).expect("serialize");
249            let back: VcsEvent = serde_json::from_value(v).expect("deserialize");
250            assert_eq!(format!("{e:?}"), format!("{back:?}"));
251        }
252    }
253}