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#[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#[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#[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#[derive(Serialize, Deserialize, Default, Clone, Copy, Debug, PartialEq, Eq)]
81pub struct FetchOptions {
82 pub prune: bool,
83}
84
85#[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#[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}