heroforge_core/fs/
transaction.rs

1//! Transaction management for filesystem operations
2//!
3//! This module handles grouping multiple filesystem operations into atomic,
4//! committable units. Transactions ensure that either all operations succeed
5//! or all are rolled back.
6
7use crate::fs::errors::{FsError, FsResult};
8use crate::fs::operations::{FsOperation, OperationSummary};
9use sha3::{Digest, Sha3_256};
10use std::sync::{Arc, Mutex};
11
12/// Transaction mode
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum TransactionMode {
15    /// Read-only transaction - no writes allowed
16    ReadOnly,
17
18    /// Read-write transaction - can read and write
19    ReadWrite,
20
21    /// Exclusive transaction - no concurrent access
22    Exclusive,
23}
24
25impl TransactionMode {
26    /// Check if mode allows writes
27    pub fn allows_writes(&self) -> bool {
28        matches!(
29            self,
30            TransactionMode::ReadWrite | TransactionMode::Exclusive
31        )
32    }
33
34    /// Check if mode is exclusive
35    pub fn is_exclusive(&self) -> bool {
36        *self == TransactionMode::Exclusive
37    }
38}
39
40/// Transaction state
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum TransactionState {
43    /// Transaction is active and accepting operations
44    Active,
45
46    /// Transaction has been committed
47    Committed,
48
49    /// Transaction has been rolled back
50    RolledBack,
51
52    /// Transaction encountered an error
53    Error,
54}
55
56/// A single transaction containing multiple operations
57///
58/// # Example
59///
60/// ```no_run
61/// use heroforge_core::Repository;
62/// use heroforge_core::fs::{Transaction, TransactionMode};
63///
64/// // Create a transaction
65/// let tx = Transaction::new(TransactionMode::ReadWrite);
66///
67/// // Transactions are typically used internally by FileSystem and Modify
68/// // For high-level operations, use those APIs instead
69/// # Ok::<(), heroforge_core::FossilError>(())
70/// ```
71#[derive(Clone)]
72pub struct Transaction {
73    /// Unique transaction ID
74    id: String,
75
76    /// Transaction mode
77    mode: TransactionMode,
78
79    /// Current state
80    state: Arc<Mutex<TransactionState>>,
81
82    /// Operations accumulated in this transaction
83    operations: Arc<Mutex<Vec<FsOperation>>>,
84
85    /// Operation summary
86    summary: Arc<Mutex<OperationSummary>>,
87
88    /// Parent commit hash (if any)
89    parent_commit: Option<String>,
90
91    /// Branch name (if applicable)
92    branch: Option<String>,
93
94    /// Timestamp of transaction creation
95    created_at: i64,
96
97    /// Maximum operations allowed (0 = unlimited)
98    max_operations: usize,
99}
100
101impl Transaction {
102    /// Create a new transaction
103    pub fn new(mode: TransactionMode) -> Self {
104        let id = uuid::Uuid::new_v4().to_string();
105        let now = std::time::SystemTime::now()
106            .duration_since(std::time::UNIX_EPOCH)
107            .unwrap_or_default()
108            .as_secs() as i64;
109
110        Self {
111            id,
112            mode,
113            state: Arc::new(Mutex::new(TransactionState::Active)),
114            operations: Arc::new(Mutex::new(Vec::new())),
115            summary: Arc::new(Mutex::new(OperationSummary::new())),
116            parent_commit: None,
117            branch: None,
118            created_at: now,
119            max_operations: 0,
120        }
121    }
122
123    /// Get transaction ID
124    pub fn id(&self) -> &str {
125        &self.id
126    }
127
128    /// Get current state
129    pub fn state(&self) -> TransactionState {
130        *self.state.lock().unwrap()
131    }
132
133    /// Check if transaction is active
134    pub fn is_active(&self) -> bool {
135        self.state() == TransactionState::Active
136    }
137
138    /// Check if transaction allows writes
139    pub fn allows_writes(&self) -> bool {
140        self.mode.allows_writes()
141    }
142
143    /// Get transaction mode
144    pub fn mode(&self) -> TransactionMode {
145        self.mode
146    }
147
148    /// Get number of operations in transaction
149    pub fn operation_count(&self) -> usize {
150        self.operations.lock().unwrap().len()
151    }
152
153    /// Get all operations
154    pub fn operations(&self) -> Vec<FsOperation> {
155        self.operations.lock().unwrap().clone()
156    }
157
158    /// Get operation summary
159    pub fn summary(&self) -> OperationSummary {
160        self.summary.lock().unwrap().clone()
161    }
162
163    /// Add an operation to the transaction
164    pub fn add_operation(&self, op: FsOperation) -> FsResult<()> {
165        // Check if transaction is active
166        if !self.is_active() {
167            return Err(FsError::TransactionError(
168                "Cannot add operation to inactive transaction".to_string(),
169            ));
170        }
171
172        // Check if mode allows this operation
173        if !self.allows_writes() {
174            return Err(FsError::TransactionError(
175                "Cannot add write operation to read-only transaction".to_string(),
176            ));
177        }
178
179        // Check operation limit
180        if self.max_operations > 0 && self.operation_count() >= self.max_operations {
181            return Err(FsError::TransactionError(format!(
182                "Transaction operation limit ({}) exceeded",
183                self.max_operations
184            )));
185        }
186
187        let mut ops = self.operations.lock().unwrap();
188        ops.push(op);
189
190        Ok(())
191    }
192
193    /// Set parent commit for this transaction
194    pub fn set_parent(&mut self, commit_hash: String) {
195        self.parent_commit = Some(commit_hash);
196    }
197
198    /// Set branch for this transaction
199    pub fn set_branch(&mut self, branch: String) {
200        self.branch = Some(branch);
201    }
202
203    /// Get parent commit hash
204    pub fn parent_commit(&self) -> Option<&str> {
205        self.parent_commit.as_deref()
206    }
207
208    /// Get branch name
209    pub fn branch(&self) -> Option<&str> {
210        self.branch.as_deref()
211    }
212
213    /// Commit the transaction
214    ///
215    /// This finalizes all operations and creates a new commit in the repository.
216    ///
217    /// # Arguments
218    ///
219    /// * `message` - Commit message describing the changes
220    /// * `author` - Author of the commit
221    ///
222    /// # Returns
223    ///
224    /// Returns the commit hash on success
225    pub fn commit(&self, message: &str, author: &str) -> FsResult<String> {
226        // Verify transaction is active
227        if !self.is_active() {
228            return Err(FsError::TransactionError(format!(
229                "Cannot commit {} transaction",
230                match self.state() {
231                    TransactionState::Committed => "already-committed",
232                    TransactionState::RolledBack => "rolled-back",
233                    TransactionState::Error => "error",
234                    TransactionState::Active => "unknown",
235                }
236            )));
237        }
238
239        let ops = self.operations.lock().unwrap();
240        if ops.is_empty() {
241            return Err(FsError::TransactionError(
242                "Cannot commit empty transaction".to_string(),
243            ));
244        }
245
246        // TODO: Perform actual commit in repository
247        let mut hasher = Sha3_256::new();
248        hasher.update(format!("{}{}{}", message, author, self.id).as_bytes());
249        let hash = hasher.finalize();
250        let commit_hash = format!("commit_{:x}", hash);
251
252        // Mark as committed
253        *self.state.lock().unwrap() = TransactionState::Committed;
254
255        Ok(commit_hash)
256    }
257
258    /// Rollback the transaction
259    ///
260    /// This discards all pending operations.
261    pub fn rollback(&self) -> FsResult<()> {
262        if !self.is_active() {
263            return Err(FsError::TransactionError(
264                "Cannot rollback inactive transaction".to_string(),
265            ));
266        }
267
268        self.operations.lock().unwrap().clear();
269        *self.state.lock().unwrap() = TransactionState::RolledBack;
270
271        Ok(())
272    }
273
274    /// Set error state
275    pub fn set_error(&self) {
276        *self.state.lock().unwrap() = TransactionState::Error;
277    }
278
279    /// Create a savepoint within the transaction
280    ///
281    /// Returns a handle that can be used to rollback to this point.
282    pub fn savepoint(&self) -> FsResult<SavePoint> {
283        if !self.is_active() {
284            return Err(FsError::TransactionError(
285                "Cannot create savepoint in inactive transaction".to_string(),
286            ));
287        }
288
289        let ops = self.operations.lock().unwrap();
290        Ok(SavePoint {
291            transaction_id: self.id.clone(),
292            operation_count: ops.len(),
293        })
294    }
295
296    /// Rollback to a specific savepoint
297    pub fn rollback_to_savepoint(&self, savepoint: &SavePoint) -> FsResult<()> {
298        if self.id != savepoint.transaction_id {
299            return Err(FsError::TransactionError(
300                "Savepoint is from a different transaction".to_string(),
301            ));
302        }
303
304        let mut ops = self.operations.lock().unwrap();
305        if savepoint.operation_count < ops.len() {
306            ops.truncate(savepoint.operation_count);
307            Ok(())
308        } else {
309            Err(FsError::TransactionError(
310                "Invalid savepoint state".to_string(),
311            ))
312        }
313    }
314}
315
316impl std::fmt::Debug for Transaction {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        f.debug_struct("Transaction")
319            .field("id", &self.id)
320            .field("mode", &self.mode)
321            .field("state", &self.state())
322            .field("operations", &self.operation_count())
323            .field("parent_commit", &self.parent_commit)
324            .field("branch", &self.branch)
325            .field("created_at", &self.created_at)
326            .finish()
327    }
328}
329
330/// A savepoint within a transaction
331#[derive(Debug, Clone)]
332pub struct SavePoint {
333    /// ID of the transaction this savepoint belongs to
334    transaction_id: String,
335
336    /// Number of operations at this savepoint
337    operation_count: usize,
338}
339
340impl SavePoint {
341    /// Get the transaction ID
342    pub fn transaction_id(&self) -> &str {
343        &self.transaction_id
344    }
345
346    /// Get the operation count at this savepoint
347    pub fn operation_count(&self) -> usize {
348        self.operation_count
349    }
350}
351
352/// Handle for managing a transaction's lifecycle
353pub struct TransactionHandle {
354    transaction: Arc<Transaction>,
355}
356
357impl TransactionHandle {
358    /// Create a new transaction handle
359    pub fn new(mode: TransactionMode) -> Self {
360        Self {
361            transaction: Arc::new(Transaction::new(mode)),
362        }
363    }
364
365    /// Get reference to the underlying transaction
366    pub fn transaction(&self) -> &Transaction {
367        &self.transaction
368    }
369
370    /// Commit and consume the handle
371    pub fn commit(self, message: &str, author: &str) -> FsResult<String> {
372        self.transaction.commit(message, author)
373    }
374
375    /// Rollback and consume the handle
376    pub fn rollback(self) -> FsResult<()> {
377        self.transaction.rollback()
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_transaction_creation() {
387        let tx = Transaction::new(TransactionMode::ReadWrite);
388        assert!(tx.is_active());
389        assert!(tx.allows_writes());
390        assert_eq!(tx.operation_count(), 0);
391    }
392
393    #[test]
394    fn test_transaction_state() {
395        let tx = Transaction::new(TransactionMode::ReadOnly);
396        assert_eq!(tx.state(), TransactionState::Active);
397        assert!(!tx.allows_writes());
398    }
399
400    #[test]
401    fn test_transaction_mode() {
402        let rw = TransactionMode::ReadWrite;
403        let ro = TransactionMode::ReadOnly;
404
405        assert!(rw.allows_writes());
406        assert!(!ro.allows_writes());
407        assert!(!rw.is_exclusive());
408        assert!(TransactionMode::Exclusive.is_exclusive());
409    }
410
411    #[test]
412    fn test_operation_count() {
413        let tx = Transaction::new(TransactionMode::ReadWrite);
414        assert_eq!(tx.operation_count(), 0);
415    }
416}