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