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 #[allow(clippy::too_many_arguments)]
117 pub async fn create_commit(
118 &self,
119 workspace_id: Uuid,
120 author_id: Uuid,
121 message: String,
122 parent_id: Option<Uuid>,
123 version: i64,
124 snapshot: serde_json::Value,
125 changes: serde_json::Value,
126 ) -> Result<Commit> {
127 let commit =
128 Commit::new(workspace_id, author_id, message, parent_id, version, snapshot, changes);
129
130 sqlx::query!(
131 r#"
132 INSERT INTO commits (id, workspace_id, author_id, message, parent_id, version, snapshot, changes, created_at)
133 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
134 "#,
135 commit.id,
136 commit.workspace_id,
137 commit.author_id,
138 commit.message,
139 commit.parent_id,
140 commit.version,
141 commit.snapshot,
142 commit.changes,
143 commit.created_at
144 )
145 .execute(&self.db)
146 .await?;
147
148 Ok(commit)
149 }
150
151 pub async fn get_commit(&self, commit_id: Uuid) -> Result<Commit> {
157 sqlx::query_as!(
158 Commit,
159 r#"
160 SELECT
161 id as "id: Uuid",
162 workspace_id as "workspace_id: Uuid",
163 author_id as "author_id: Uuid",
164 message,
165 parent_id as "parent_id: Uuid",
166 version,
167 snapshot as "snapshot: serde_json::Value",
168 changes as "changes: serde_json::Value",
169 created_at as "created_at: chrono::DateTime<chrono::Utc>"
170 FROM commits
171 WHERE id = ?
172 "#,
173 commit_id
174 )
175 .fetch_optional(&self.db)
176 .await?
177 .ok_or_else(|| CollabError::Internal(format!("Commit not found: {commit_id}")))
178 }
179
180 pub async fn get_history(&self, workspace_id: Uuid, limit: Option<i32>) -> Result<Vec<Commit>> {
186 let limit = limit.unwrap_or(100);
187
188 let commits = sqlx::query_as!(
189 Commit,
190 r#"
191 SELECT
192 id as "id: Uuid",
193 workspace_id as "workspace_id: Uuid",
194 author_id as "author_id: Uuid",
195 message,
196 parent_id as "parent_id: Uuid",
197 version,
198 snapshot as "snapshot: serde_json::Value",
199 changes as "changes: serde_json::Value",
200 created_at as "created_at: chrono::DateTime<chrono::Utc>"
201 FROM commits
202 WHERE workspace_id = ?
203 ORDER BY created_at DESC
204 LIMIT ?
205 "#,
206 workspace_id,
207 limit
208 )
209 .fetch_all(&self.db)
210 .await?;
211
212 Ok(commits)
213 }
214
215 pub async fn get_latest_commit(&self, workspace_id: Uuid) -> Result<Option<Commit>> {
221 let commit = sqlx::query_as!(
222 Commit,
223 r#"
224 SELECT
225 id as "id: Uuid",
226 workspace_id as "workspace_id: Uuid",
227 author_id as "author_id: Uuid",
228 message,
229 parent_id as "parent_id: Uuid",
230 version,
231 snapshot as "snapshot: serde_json::Value",
232 changes as "changes: serde_json::Value",
233 created_at as "created_at: chrono::DateTime<chrono::Utc>"
234 FROM commits
235 WHERE workspace_id = ?
236 ORDER BY created_at DESC
237 LIMIT 1
238 "#,
239 workspace_id
240 )
241 .fetch_optional(&self.db)
242 .await?;
243
244 Ok(commit)
245 }
246
247 pub async fn create_snapshot(
253 &self,
254 workspace_id: Uuid,
255 name: String,
256 description: Option<String>,
257 commit_id: Uuid,
258 created_by: Uuid,
259 ) -> Result<Snapshot> {
260 self.get_commit(commit_id).await?;
262
263 let snapshot = Snapshot::new(workspace_id, name, description, commit_id, created_by);
264
265 sqlx::query!(
266 r#"
267 INSERT INTO snapshots (id, workspace_id, name, description, commit_id, created_by, created_at)
268 VALUES (?, ?, ?, ?, ?, ?, ?)
269 "#,
270 snapshot.id,
271 snapshot.workspace_id,
272 snapshot.name,
273 snapshot.description,
274 snapshot.commit_id,
275 snapshot.created_by,
276 snapshot.created_at
277 )
278 .execute(&self.db)
279 .await?;
280
281 Ok(snapshot)
282 }
283
284 pub async fn get_snapshot(&self, workspace_id: Uuid, name: &str) -> Result<Snapshot> {
290 sqlx::query_as!(
291 Snapshot,
292 r#"
293 SELECT
294 id as "id: Uuid",
295 workspace_id as "workspace_id: Uuid",
296 name,
297 description,
298 commit_id as "commit_id: Uuid",
299 created_by as "created_by: Uuid",
300 created_at as "created_at: chrono::DateTime<chrono::Utc>"
301 FROM snapshots
302 WHERE workspace_id = ? AND name = ?
303 "#,
304 workspace_id,
305 name
306 )
307 .fetch_optional(&self.db)
308 .await?
309 .ok_or_else(|| CollabError::Internal(format!("Snapshot not found: {name}")))
310 }
311
312 pub async fn list_snapshots(&self, workspace_id: Uuid) -> Result<Vec<Snapshot>> {
318 let snapshots = sqlx::query_as!(
319 Snapshot,
320 r#"
321 SELECT
322 id as "id: Uuid",
323 workspace_id as "workspace_id: Uuid",
324 name,
325 description,
326 commit_id as "commit_id: Uuid",
327 created_by as "created_by: Uuid",
328 created_at as "created_at: chrono::DateTime<chrono::Utc>"
329 FROM snapshots
330 WHERE workspace_id = ?
331 ORDER BY created_at DESC
332 "#,
333 workspace_id
334 )
335 .fetch_all(&self.db)
336 .await?;
337
338 Ok(snapshots)
339 }
340
341 pub async fn restore_to_commit(
347 &self,
348 workspace_id: Uuid,
349 commit_id: Uuid,
350 ) -> Result<serde_json::Value> {
351 let commit = self.get_commit(commit_id).await?;
352
353 if commit.workspace_id != workspace_id {
354 return Err(CollabError::InvalidInput(
355 "Commit does not belong to this workspace".to_string(),
356 ));
357 }
358
359 Ok(commit.snapshot)
360 }
361
362 pub async fn diff(&self, from_commit: Uuid, to_commit: Uuid) -> Result<serde_json::Value> {
368 let from = self.get_commit(from_commit).await?;
369 let to = self.get_commit(to_commit).await?;
370
371 let diff = serde_json::json!({
373 "from": from.snapshot,
374 "to": to.snapshot,
375 "changes": to.changes
376 });
377
378 Ok(diff)
379 }
380}
381
382pub struct History {
384 version_control: VersionControl,
385 auto_commit: bool,
386}
387
388impl History {
389 #[must_use]
391 pub const fn new(db: Pool<Sqlite>) -> Self {
392 Self {
393 version_control: VersionControl::new(db),
394 auto_commit: true,
395 }
396 }
397
398 pub const fn set_auto_commit(&mut self, enabled: bool) {
400 self.auto_commit = enabled;
401 }
402
403 pub async fn track_change(
409 &self,
410 workspace_id: Uuid,
411 user_id: Uuid,
412 message: String,
413 new_state: serde_json::Value,
414 changes: serde_json::Value,
415 ) -> Result<Option<Commit>> {
416 if !self.auto_commit {
417 return Ok(None);
418 }
419
420 let latest = self.version_control.get_latest_commit(workspace_id).await?;
421 let parent_id = latest.as_ref().map(|c| c.id);
422 let version = latest.map_or(1, |c| c.version + 1);
423
424 let commit = self
425 .version_control
426 .create_commit(workspace_id, user_id, message, parent_id, version, new_state, changes)
427 .await?;
428
429 Ok(Some(commit))
430 }
431
432 pub async fn get_history(&self, workspace_id: Uuid, limit: Option<i32>) -> Result<Vec<Commit>> {
438 self.version_control.get_history(workspace_id, limit).await
439 }
440
441 pub async fn create_snapshot(
447 &self,
448 workspace_id: Uuid,
449 name: String,
450 description: Option<String>,
451 user_id: Uuid,
452 ) -> Result<Snapshot> {
453 let latest = self
455 .version_control
456 .get_latest_commit(workspace_id)
457 .await?
458 .ok_or_else(|| CollabError::Internal("No commits found".to_string()))?;
459
460 self.version_control
461 .create_snapshot(workspace_id, name, description, latest.id, user_id)
462 .await
463 }
464
465 pub async fn restore_snapshot(
471 &self,
472 workspace_id: Uuid,
473 snapshot_name: &str,
474 ) -> Result<serde_json::Value> {
475 let snapshot = self.version_control.get_snapshot(workspace_id, snapshot_name).await?;
476 self.version_control.restore_to_commit(workspace_id, snapshot.commit_id).await
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_commit_creation() {
486 let workspace_id = Uuid::new_v4();
487 let author_id = Uuid::new_v4();
488 let commit = Commit::new(
489 workspace_id,
490 author_id,
491 "Initial commit".to_string(),
492 None,
493 1,
494 serde_json::json!({}),
495 serde_json::json!({}),
496 );
497
498 assert_eq!(commit.workspace_id, workspace_id);
499 assert_eq!(commit.author_id, author_id);
500 assert_eq!(commit.version, 1);
501 assert!(commit.parent_id.is_none());
502 }
503
504 #[test]
505 fn test_snapshot_creation() {
506 let workspace_id = Uuid::new_v4();
507 let commit_id = Uuid::new_v4();
508 let created_by = Uuid::new_v4();
509 let snapshot = Snapshot::new(
510 workspace_id,
511 "v1.0.0".to_string(),
512 Some("First release".to_string()),
513 commit_id,
514 created_by,
515 );
516
517 assert_eq!(snapshot.name, "v1.0.0");
518 assert_eq!(snapshot.workspace_id, workspace_id);
519 assert_eq!(snapshot.commit_id, commit_id);
520 }
521}