1use crate::error::{CollabError, Result};
4use chrono::Utc;
5use serde::{Deserialize, Serialize};
6use sqlx::{Pool, Sqlite};
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
11pub struct Commit {
12 pub id: Uuid,
14 pub workspace_id: Uuid,
16 pub author_id: Uuid,
18 pub message: String,
20 pub parent_id: Option<Uuid>,
22 pub version: i64,
24 pub snapshot: serde_json::Value,
26 pub changes: serde_json::Value,
28 pub created_at: chrono::DateTime<Utc>,
30}
31
32impl Commit {
33 #[must_use]
35 pub fn new(
36 workspace_id: Uuid,
37 author_id: Uuid,
38 message: String,
39 parent_id: Option<Uuid>,
40 version: i64,
41 snapshot: serde_json::Value,
42 changes: serde_json::Value,
43 ) -> Self {
44 Self {
45 id: Uuid::new_v4(),
46 workspace_id,
47 author_id,
48 message,
49 parent_id,
50 version,
51 snapshot,
52 changes,
53 created_at: Utc::now(),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
60pub struct Snapshot {
61 pub id: Uuid,
63 pub workspace_id: Uuid,
65 pub name: String,
67 pub description: Option<String>,
69 pub commit_id: Uuid,
71 pub created_by: Uuid,
73 pub created_at: chrono::DateTime<Utc>,
75}
76
77impl Snapshot {
78 #[must_use]
80 pub fn new(
81 workspace_id: Uuid,
82 name: String,
83 description: Option<String>,
84 commit_id: Uuid,
85 created_by: Uuid,
86 ) -> Self {
87 Self {
88 id: Uuid::new_v4(),
89 workspace_id,
90 name,
91 description,
92 commit_id,
93 created_by,
94 created_at: Utc::now(),
95 }
96 }
97}
98
99pub struct VersionControl {
101 db: Pool<Sqlite>,
102}
103
104impl VersionControl {
105 #[must_use]
107 pub const fn new(db: Pool<Sqlite>) -> Self {
108 Self { db }
109 }
110
111 pub async fn create_commit(
113 &self,
114 workspace_id: Uuid,
115 author_id: Uuid,
116 message: String,
117 parent_id: Option<Uuid>,
118 version: i64,
119 snapshot: serde_json::Value,
120 changes: serde_json::Value,
121 ) -> Result<Commit> {
122 let commit =
123 Commit::new(workspace_id, author_id, message, parent_id, version, snapshot, changes);
124
125 sqlx::query!(
126 r#"
127 INSERT INTO commits (id, workspace_id, author_id, message, parent_id, version, snapshot, changes, created_at)
128 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
129 "#,
130 commit.id,
131 commit.workspace_id,
132 commit.author_id,
133 commit.message,
134 commit.parent_id,
135 commit.version,
136 commit.snapshot,
137 commit.changes,
138 commit.created_at
139 )
140 .execute(&self.db)
141 .await?;
142
143 Ok(commit)
144 }
145
146 pub async fn get_commit(&self, commit_id: Uuid) -> Result<Commit> {
148 sqlx::query_as!(
149 Commit,
150 r#"
151 SELECT
152 id as "id: Uuid",
153 workspace_id as "workspace_id: Uuid",
154 author_id as "author_id: Uuid",
155 message,
156 parent_id as "parent_id: Uuid",
157 version,
158 snapshot,
159 changes,
160 created_at as "created_at: chrono::DateTime<chrono::Utc>"
161 FROM commits
162 WHERE id = ?
163 "#,
164 commit_id
165 )
166 .fetch_optional(&self.db)
167 .await?
168 .ok_or_else(|| CollabError::Internal(format!("Commit not found: {commit_id}")))
169 }
170
171 pub async fn get_history(&self, workspace_id: Uuid, limit: Option<i32>) -> Result<Vec<Commit>> {
173 let limit = limit.unwrap_or(100);
174
175 let commits = sqlx::query_as!(
176 Commit,
177 r#"
178 SELECT
179 id as "id: Uuid",
180 workspace_id as "workspace_id: Uuid",
181 author_id as "author_id: Uuid",
182 message,
183 parent_id as "parent_id: Uuid",
184 version,
185 snapshot,
186 changes,
187 created_at as "created_at: chrono::DateTime<chrono::Utc>"
188 FROM commits
189 WHERE workspace_id = ?
190 ORDER BY created_at DESC
191 LIMIT ?
192 "#,
193 workspace_id,
194 limit
195 )
196 .fetch_all(&self.db)
197 .await?;
198
199 Ok(commits)
200 }
201
202 pub async fn get_latest_commit(&self, workspace_id: Uuid) -> Result<Option<Commit>> {
204 let commit = sqlx::query_as!(
205 Commit,
206 r#"
207 SELECT
208 id as "id: Uuid",
209 workspace_id as "workspace_id: Uuid",
210 author_id as "author_id: Uuid",
211 message,
212 parent_id as "parent_id: Uuid",
213 version,
214 snapshot,
215 changes,
216 created_at as "created_at: chrono::DateTime<chrono::Utc>"
217 FROM commits
218 WHERE workspace_id = ?
219 ORDER BY created_at DESC
220 LIMIT 1
221 "#,
222 workspace_id
223 )
224 .fetch_optional(&self.db)
225 .await?;
226
227 Ok(commit)
228 }
229
230 pub async fn create_snapshot(
232 &self,
233 workspace_id: Uuid,
234 name: String,
235 description: Option<String>,
236 commit_id: Uuid,
237 created_by: Uuid,
238 ) -> Result<Snapshot> {
239 self.get_commit(commit_id).await?;
241
242 let snapshot = Snapshot::new(workspace_id, name, description, commit_id, created_by);
243
244 sqlx::query!(
245 r#"
246 INSERT INTO snapshots (id, workspace_id, name, description, commit_id, created_by, created_at)
247 VALUES (?, ?, ?, ?, ?, ?, ?)
248 "#,
249 snapshot.id,
250 snapshot.workspace_id,
251 snapshot.name,
252 snapshot.description,
253 snapshot.commit_id,
254 snapshot.created_by,
255 snapshot.created_at
256 )
257 .execute(&self.db)
258 .await?;
259
260 Ok(snapshot)
261 }
262
263 pub async fn get_snapshot(&self, workspace_id: Uuid, name: &str) -> Result<Snapshot> {
265 sqlx::query_as!(
266 Snapshot,
267 r#"
268 SELECT
269 id as "id: Uuid",
270 workspace_id as "workspace_id: Uuid",
271 name,
272 description,
273 commit_id as "commit_id: Uuid",
274 created_by as "created_by: Uuid",
275 created_at as "created_at: chrono::DateTime<chrono::Utc>"
276 FROM snapshots
277 WHERE workspace_id = ? AND name = ?
278 "#,
279 workspace_id,
280 name
281 )
282 .fetch_optional(&self.db)
283 .await?
284 .ok_or_else(|| CollabError::Internal(format!("Snapshot not found: {name}")))
285 }
286
287 pub async fn list_snapshots(&self, workspace_id: Uuid) -> Result<Vec<Snapshot>> {
289 let snapshots = sqlx::query_as!(
290 Snapshot,
291 r#"
292 SELECT
293 id as "id: Uuid",
294 workspace_id as "workspace_id: Uuid",
295 name,
296 description,
297 commit_id as "commit_id: Uuid",
298 created_by as "created_by: Uuid",
299 created_at as "created_at: chrono::DateTime<chrono::Utc>"
300 FROM snapshots
301 WHERE workspace_id = ?
302 ORDER BY created_at DESC
303 "#,
304 workspace_id
305 )
306 .fetch_all(&self.db)
307 .await?;
308
309 Ok(snapshots)
310 }
311
312 pub async fn restore_to_commit(
314 &self,
315 workspace_id: Uuid,
316 commit_id: Uuid,
317 ) -> Result<serde_json::Value> {
318 let commit = self.get_commit(commit_id).await?;
319
320 if commit.workspace_id != workspace_id {
321 return Err(CollabError::InvalidInput(
322 "Commit does not belong to this workspace".to_string(),
323 ));
324 }
325
326 Ok(commit.snapshot)
327 }
328
329 pub async fn diff(&self, from_commit: Uuid, to_commit: Uuid) -> Result<serde_json::Value> {
331 let from = self.get_commit(from_commit).await?;
332 let to = self.get_commit(to_commit).await?;
333
334 let diff = serde_json::json!({
336 "from": from.snapshot,
337 "to": to.snapshot,
338 "changes": to.changes
339 });
340
341 Ok(diff)
342 }
343}
344
345pub struct History {
347 version_control: VersionControl,
348 auto_commit: bool,
349}
350
351impl History {
352 #[must_use]
354 pub const fn new(db: Pool<Sqlite>) -> Self {
355 Self {
356 version_control: VersionControl::new(db),
357 auto_commit: true,
358 }
359 }
360
361 pub const fn set_auto_commit(&mut self, enabled: bool) {
363 self.auto_commit = enabled;
364 }
365
366 pub async fn track_change(
368 &self,
369 workspace_id: Uuid,
370 user_id: Uuid,
371 message: String,
372 new_state: serde_json::Value,
373 changes: serde_json::Value,
374 ) -> Result<Option<Commit>> {
375 if !self.auto_commit {
376 return Ok(None);
377 }
378
379 let latest = self.version_control.get_latest_commit(workspace_id).await?;
380 let parent_id = latest.as_ref().map(|c| c.id);
381 let version = latest.map_or(1, |c| c.version + 1);
382
383 let commit = self
384 .version_control
385 .create_commit(workspace_id, user_id, message, parent_id, version, new_state, changes)
386 .await?;
387
388 Ok(Some(commit))
389 }
390
391 pub async fn get_history(&self, workspace_id: Uuid, limit: Option<i32>) -> Result<Vec<Commit>> {
393 self.version_control.get_history(workspace_id, limit).await
394 }
395
396 pub async fn create_snapshot(
398 &self,
399 workspace_id: Uuid,
400 name: String,
401 description: Option<String>,
402 user_id: Uuid,
403 ) -> Result<Snapshot> {
404 let latest = self
406 .version_control
407 .get_latest_commit(workspace_id)
408 .await?
409 .ok_or_else(|| CollabError::Internal("No commits found".to_string()))?;
410
411 self.version_control
412 .create_snapshot(workspace_id, name, description, latest.id, user_id)
413 .await
414 }
415
416 pub async fn restore_snapshot(
418 &self,
419 workspace_id: Uuid,
420 snapshot_name: &str,
421 ) -> Result<serde_json::Value> {
422 let snapshot = self.version_control.get_snapshot(workspace_id, snapshot_name).await?;
423 self.version_control.restore_to_commit(workspace_id, snapshot.commit_id).await
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn test_commit_creation() {
433 let workspace_id = Uuid::new_v4();
434 let author_id = Uuid::new_v4();
435 let commit = Commit::new(
436 workspace_id,
437 author_id,
438 "Initial commit".to_string(),
439 None,
440 1,
441 serde_json::json!({}),
442 serde_json::json!({}),
443 );
444
445 assert_eq!(commit.workspace_id, workspace_id);
446 assert_eq!(commit.author_id, author_id);
447 assert_eq!(commit.version, 1);
448 assert!(commit.parent_id.is_none());
449 }
450
451 #[test]
452 fn test_snapshot_creation() {
453 let workspace_id = Uuid::new_v4();
454 let commit_id = Uuid::new_v4();
455 let created_by = Uuid::new_v4();
456 let snapshot = Snapshot::new(
457 workspace_id,
458 "v1.0.0".to_string(),
459 Some("First release".to_string()),
460 commit_id,
461 created_by,
462 );
463
464 assert_eq!(snapshot.name, "v1.0.0");
465 assert_eq!(snapshot.workspace_id, workspace_id);
466 assert_eq!(snapshot.commit_id, commit_id);
467 }
468}