titor 0.2.0

A high-performance checkpointing library for time-travel through directory states
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
//! Core data types used throughout the Titor library
//!
//! This module contains fundamental data structures that are shared across
//! different components of the library.
//!
//! ## Overview
//!
//! The types in this module represent:
//! - **File System State**: `FileEntry`, `FileManifest` - representing files and their metadata
//! - **Operations**: `RestoreResult`, `CheckpointDiff`, `ChangeStats` - results of various operations
//! - **Configuration**: `TitorConfig`, `CheckpointOptions`, `RestoreOptions` - operation parameters
//! - **Storage**: `StorageObject`, `StorageMetadata` - internal storage representations
//! - **Hooks**: `CheckpointHook` - extensibility points for custom behavior
//!
//! ## Examples
//!
//! ```rust
//! use titor::types::{CheckpointOptions, FileEntry};
//! use std::path::PathBuf;
//! 
//! // Configure checkpoint creation
//! let options = CheckpointOptions {
//!     description: Some("Release v1.0".to_string()),
//!     tags: vec!["release".to_string(), "production".to_string()],
//!     ..Default::default()
//! };
//! ```

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::collections::HashMap;
use std::sync::Arc;

/// Represents a file entry in a checkpoint manifest
///
/// Contains all necessary information to restore a file to its exact state,
/// including content hash, permissions, and metadata.
///
/// # Examples
///
/// ```rust
/// # use titor::types::FileEntry;
/// # use std::path::PathBuf;
/// # use chrono::Utc;
/// let entry = FileEntry {
///     path: PathBuf::from("src/main.rs"),
///     content_hash: "abc123...".to_string(),
///     size: 1024,
///     permissions: 0o644,
///     modified: Utc::now(),
///     is_compressed: true,
///     metadata_hash: "def456...".to_string(),
///     combined_hash: "ghi789...".to_string(),
///     is_symlink: false,
///     symlink_target: None,
///     is_directory: false,
/// };
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileEntry {
    /// Relative path from the root directory
    pub path: PathBuf,
    /// SHA-256 hash of file content
    pub content_hash: String,
    /// File size in bytes
    pub size: u64,
    /// Unix file permissions
    pub permissions: u32,
    /// Last modified timestamp
    pub modified: DateTime<Utc>,
    /// Whether the file is compressed in storage
    pub is_compressed: bool,
    /// Hash of file metadata (permissions, timestamps, etc.)
    pub metadata_hash: String,
    /// Combined hash of content and metadata
    pub combined_hash: String,
    /// Whether this is a symbolic link
    pub is_symlink: bool,
    /// Target of symbolic link (if is_symlink is true)
    pub symlink_target: Option<PathBuf>,
    /// Whether this is a directory
    pub is_directory: bool,
}

/// Manifest containing all files in a checkpoint
///
/// A complete record of all files and directories at the time of checkpoint
/// creation, including their hashes and metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileManifest {
    /// Checkpoint ID this manifest belongs to
    pub checkpoint_id: String,
    /// List of all files in the checkpoint
    pub files: Vec<FileEntry>,
    /// Total size of all files (uncompressed)
    pub total_size: u64,
    /// Number of files
    pub file_count: usize,
    /// Merkle root of all file hashes
    pub merkle_root: String,
    /// Creation timestamp
    pub created_at: DateTime<Utc>,
}

/// Statistics about changes between checkpoints
///
/// Provides detailed information about what changed between two states,
/// useful for understanding the impact of changes and optimizing operations.
///
/// # Examples
///
/// ```rust
/// # use titor::types::ChangeStats;
/// let stats = ChangeStats {
///     files_added: 10,
///     files_modified: 5,
///     files_deleted: 2,
///     bytes_added: 50000,
///     bytes_modified: 10000,
///     bytes_deleted: 5000,
///     changed_files: vec![],
/// };
/// 
/// assert_eq!(stats.total_operations(), 17);
/// assert_eq!(stats.net_size_change(), 55000);
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ChangeStats {
    /// Number of files added
    pub files_added: usize,
    /// Number of files modified
    pub files_modified: usize,
    /// Number of files deleted
    pub files_deleted: usize,
    /// Total size of added files
    pub bytes_added: u64,
    /// Total size of modified files (new size)
    pub bytes_modified: u64,
    /// Total size of deleted files
    pub bytes_deleted: u64,
    /// Files that changed
    pub changed_files: Vec<PathBuf>,
}

impl ChangeStats {
    /// Check if there are any changes
    ///
    /// Returns `true` if any files were added, modified, or deleted.
    pub fn has_changes(&self) -> bool {
        self.files_added > 0 || self.files_modified > 0 || self.files_deleted > 0
    }
    
    /// Get total number of file operations
    ///
    /// Returns the sum of added, modified, and deleted files.
    pub fn total_operations(&self) -> usize {
        self.files_added + self.files_modified + self.files_deleted
    }
    
    /// Get net size change in bytes
    ///
    /// Returns the difference between bytes added/modified and bytes deleted.
    /// Positive values indicate growth, negative values indicate shrinkage.
    pub fn net_size_change(&self) -> i64 {
        (self.bytes_added + self.bytes_modified) as i64 - self.bytes_deleted as i64
    }
}

/// Result of a restore operation
///
/// Contains statistics and information about a completed restore operation,
/// including any warnings that occurred during the process.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreResult {
    /// ID of the checkpoint that was restored
    pub checkpoint_id: String,
    /// Number of files restored
    pub files_restored: usize,
    /// Number of files deleted (that existed before but not in checkpoint)
    pub files_deleted: usize,
    /// Total bytes written
    pub bytes_written: u64,
    /// Total bytes deleted
    pub bytes_deleted: u64,
    /// Time taken for restoration in milliseconds
    pub duration_ms: u64,
    /// Any warnings during restoration
    pub warnings: Vec<String>,
}

/// Difference between two checkpoints
///
/// Represents the changes between two checkpoints, showing which files
/// were added, modified, or deleted.
///
/// # Examples
///
/// ```rust
/// # use titor::types::CheckpointDiff;
/// # use titor::Titor;
/// # use std::path::PathBuf;
/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
/// # let titor = Titor::init(PathBuf::from("."), PathBuf::from(".titor"))?;
/// let diff = titor.diff("checkpoint1", "checkpoint2")?;
/// 
/// for file in &diff.added_files {
///     println!("Added: {:?}", file.path);
/// }
/// 
/// for (old, new) in &diff.modified_files {
///     println!("Modified: {:?} ({} -> {} bytes)", 
///              old.path, old.size, new.size);
/// }
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointDiff {
    /// Source checkpoint ID
    pub from_id: String,
    /// Target checkpoint ID
    pub to_id: String,
    /// Files added in target
    pub added_files: Vec<FileEntry>,
    /// Files modified between checkpoints (old, new)
    pub modified_files: Vec<(FileEntry, FileEntry)>,
    /// Files deleted in target
    pub deleted_files: Vec<FileEntry>,
    /// Change statistics
    pub stats: ChangeStats,
}

/// Represents a single line change in a diff
///
/// This enum represents different types of line changes in a unified diff format.
/// Each variant contains the line number and content.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum LineChange {
    /// Line added in the new version (line_number in new file, content)
    Added(usize, String),
    /// Line deleted from the old version (line_number in old file, content)
    Deleted(usize, String),
    /// Context line that exists in both versions (line_number in old file, content)
    Context(usize, String),
}

/// A contiguous block of changes in a file diff
///
/// A hunk represents a section of the file where changes occur, along with
/// surrounding context lines. This is similar to git's unified diff format.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffHunk {
    /// Starting line number in the source file
    pub from_line: usize,
    /// Number of lines from the source file in this hunk
    pub from_count: usize,
    /// Starting line number in the target file
    pub to_line: usize,
    /// Number of lines in the target file in this hunk
    pub to_count: usize,
    /// The actual line changes in this hunk
    pub changes: Vec<LineChange>,
}

/// Line-level diff information for a single file
///
/// Contains detailed information about changes to a file, including
/// line-by-line differences organized into hunks.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileDiff {
    /// Path of the file being diffed
    pub path: PathBuf,
    /// Content hash in the source checkpoint
    pub from_hash: String,
    /// Content hash in the target checkpoint
    pub to_hash: String,
    /// Whether this is a binary file (no line diff available)
    pub is_binary: bool,
    /// Diff hunks containing line changes
    pub hunks: Vec<DiffHunk>,
    /// Total number of lines added
    pub lines_added: usize,
    /// Total number of lines deleted
    pub lines_deleted: usize,
}

/// Options for controlling diff generation
///
/// These options allow fine-tuning how diffs are generated and displayed.
#[derive(Debug, Clone)]
pub struct DiffOptions {
    /// Number of context lines to show around changes (default: 3)
    pub context_lines: usize,
    /// Whether to ignore whitespace changes
    pub ignore_whitespace: bool,
    /// Whether to show line numbers in the output
    pub show_line_numbers: bool,
    /// Maximum file size to compute line diffs for (default: 10MB)
    pub max_file_size: u64,
}

impl Default for DiffOptions {
    fn default() -> Self {
        Self {
            context_lines: 3,
            ignore_whitespace: false,
            show_line_numbers: true,
            max_file_size: 10 * 1024 * 1024, // 10MB
        }
    }
}

/// Enhanced checkpoint diff with line-level changes
///
/// This extends the basic CheckpointDiff with detailed line-level
/// diff information for modified text files.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetailedCheckpointDiff {
    /// Basic diff information
    pub basic_diff: CheckpointDiff,
    /// Line-level diffs for modified files
    pub file_diffs: Vec<FileDiff>,
    /// Total lines added across all files
    pub total_lines_added: usize,
    /// Total lines deleted across all files
    pub total_lines_deleted: usize,
}

/// Statistics from garbage collection
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GcStats {
    /// Number of objects examined
    pub objects_examined: usize,
    /// Number of objects deleted
    pub objects_deleted: usize,
    /// Bytes reclaimed
    pub bytes_reclaimed: u64,
    /// Duration in milliseconds
    pub duration_ms: u64,
    /// Unreferenced objects found
    pub unreferenced_objects: Vec<String>,
}

/// Configuration for Titor instance
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TitorConfig {
    /// Root directory being tracked
    pub root_path: PathBuf,
    /// Storage location
    pub storage_path: PathBuf,
    /// Maximum file size to track (0 = unlimited)
    pub max_file_size: u64,
    /// Number of parallel workers for operations
    pub parallel_workers: usize,
    /// Ignore patterns (gitignore style)
    pub ignore_patterns: Vec<String>,
    /// Compression strategy name
    pub compression_strategy: String,
    /// Whether to follow symbolic links
    pub follow_symlinks: bool,
    /// Titor version that created this config
    pub version: String,
}

/// Metadata stored with the storage
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageMetadata {
    /// Version of storage format
    pub format_version: u32,
    /// Titor version that created the storage
    pub titor_version: String,
    /// Creation timestamp
    pub created_at: DateTime<Utc>,
    /// Last accessed timestamp
    pub last_accessed: DateTime<Utc>,
    /// Configuration
    pub config: TitorConfig,
}

/// Progress callback for long-running operations
pub type ProgressCallback = Arc<dyn Fn(ProgressInfo) + Send + Sync>;

/// Information passed to progress callbacks
#[derive(Debug, Clone)]
pub struct ProgressInfo {
    /// Operation being performed
    pub operation: String,
    /// Current item being processed
    pub current_item: Option<String>,
    /// Items processed so far
    pub processed: usize,
    /// Total items to process (if known)
    pub total: Option<usize>,
    /// Bytes processed so far
    pub bytes_processed: u64,
    /// Total bytes to process (if known)
    pub total_bytes: Option<u64>,
}

impl ProgressInfo {
    /// Get progress as a percentage (0-100)
    pub fn percentage(&self) -> Option<f32> {
        match self.total {
            Some(total) if total > 0 => Some((self.processed as f32 / total as f32) * 100.0),
            _ => None,
        }
    }
}

/// Auto-checkpoint strategies
///
/// Defines when checkpoints should be created automatically.
/// This allows for hands-free operation while ensuring important
/// changes are captured.
///
/// # Examples
///
/// ```rust
/// use titor::types::AutoCheckpointStrategy;
/// use std::time::Duration;
///
/// // Checkpoint every 100 file operations
/// let strategy = AutoCheckpointStrategy::AfterOperations(100);
///
/// // Checkpoint every hour
/// let strategy = AutoCheckpointStrategy::TimeBased(Duration::from_secs(3600));
///
/// // Smart detection with custom thresholds
/// let strategy = AutoCheckpointStrategy::Smart {
///     min_files_changed: 10,
///     min_size_changed: 1_000_000, // 1MB
///     max_time_between: Duration::from_secs(86400), // 24 hours
/// };
/// ```
#[derive(Clone)]
pub enum AutoCheckpointStrategy {
    /// No automatic checkpoints
    Disabled,
    /// Checkpoint after N file operations
    AfterOperations(usize),
    /// Checkpoint after duration
    TimeBased(std::time::Duration),
    /// Smart detection of significant changes
    Smart {
        /// Minimum files changed to trigger
        min_files_changed: usize,
        /// Minimum size changed to trigger
        min_size_changed: u64,
        /// Maximum time between checkpoints
        max_time_between: std::time::Duration,
    },
    /// Custom strategy with callback
    Custom(Arc<dyn Fn(&ChangeStats) -> bool + Send + Sync>),
}

impl std::fmt::Debug for AutoCheckpointStrategy {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Disabled => write!(f, "Disabled"),
            Self::AfterOperations(n) => f.debug_tuple("AfterOperations").field(n).finish(),
            Self::TimeBased(duration) => f.debug_tuple("TimeBased").field(duration).finish(),
            Self::Smart { min_files_changed, min_size_changed, max_time_between } => f
                .debug_struct("Smart")
                .field("min_files_changed", min_files_changed)
                .field("min_size_changed", min_size_changed)
                .field("max_time_between", max_time_between)
                .finish(),
            Self::Custom(_) => write!(f, "Custom(Fn)"),
        }
    }
}

/// File operation for tracking changes
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileOperation {
    /// File was added
    Added(PathBuf),
    /// File was modified
    Modified(PathBuf),
    /// File was deleted
    Deleted(PathBuf),
}

/// Options for checkpoint creation
#[derive(Clone, Default)]
pub struct CheckpointOptions {
    /// Description for the checkpoint
    pub description: Option<String>,
    /// Tags to associate with checkpoint
    pub tags: Vec<String>,
    /// Custom metadata
    pub metadata: HashMap<String, String>,
    /// Progress callback
    pub progress_callback: Option<ProgressCallback>,
}

impl std::fmt::Debug for CheckpointOptions {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CheckpointOptions")
            .field("description", &self.description)
            .field("tags", &self.tags)
            .field("metadata", &self.metadata)
            .field("progress_callback", &self.progress_callback.is_some())
            .finish()
    }
}

/// Options for restore operations
#[derive(Clone, Default)]
pub struct RestoreOptions {
    /// Whether to verify hashes during restore
    pub verify_hashes: bool,
    /// Whether to preserve current file timestamps
    pub preserve_timestamps: bool,
    /// Files to exclude from restoration
    pub exclude_patterns: Vec<String>,
    /// Progress callback
    pub progress_callback: Option<ProgressCallback>,
    /// Whether to do a dry run (no actual changes)
    pub dry_run: bool,
}

impl std::fmt::Debug for RestoreOptions {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RestoreOptions")
            .field("verify_hashes", &self.verify_hashes)
            .field("preserve_timestamps", &self.preserve_timestamps)
            .field("exclude_patterns", &self.exclude_patterns)
            .field("progress_callback", &self.progress_callback.is_some())
            .field("dry_run", &self.dry_run)
            .finish()
    }
}

/// Information about a storage object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageObject {
    /// Hash of the object
    pub hash: String,
    /// Size in bytes (compressed)
    pub compressed_size: u64,
    /// Size in bytes (uncompressed)
    pub uncompressed_size: u64,
    /// Reference count
    pub ref_count: usize,
    /// Whether the object is compressed
    pub is_compressed: bool,
    /// Creation timestamp
    pub created_at: DateTime<Utc>,
    /// Last accessed timestamp
    pub last_accessed: DateTime<Utc>,
}

/// File filter for operations
pub type FileFilter = Arc<dyn Fn(&FileEntry) -> bool + Send + Sync>;

/// Hook trait for checkpoint operations
///
/// Allows extending Titor with custom behavior at key points in the
/// checkpoint lifecycle. Implement this trait to add logging, notifications,
/// validation, or other custom logic.
///
/// # Examples
///
/// ```rust
/// use titor::types::{CheckpointHook, ChangeStats, RestoreResult};
/// use titor::checkpoint::Checkpoint;
/// use titor::Result;
///
/// struct NotificationHook {
///     webhook_url: String,
/// }
///
/// impl CheckpointHook for NotificationHook {
///     fn pre_checkpoint(&self, stats: &ChangeStats) -> Result<()> {
///         if stats.total_operations() > 100 {
///             println!("Large checkpoint coming: {} operations", stats.total_operations());
///         }
///         Ok(())
///     }
///     
///     fn post_checkpoint(&self, checkpoint: &Checkpoint) -> Result<()> {
///         println!("Checkpoint {} created", checkpoint.id);
///         // Send webhook notification here
///         Ok(())
///     }
///     
///     fn pre_restore(&self, from: &Checkpoint, to: &Checkpoint) -> Result<()> {
///         println!("Restoring from {} to {}", from.short_id(), to.short_id());
///         Ok(())
///     }
///     
///     fn post_restore(&self, result: &RestoreResult) -> Result<()> {
///         println!("Restore complete: {} files restored", result.files_restored);
///         Ok(())
///     }
/// }
/// ```
pub trait CheckpointHook: Send + Sync {
    /// Called before creating a checkpoint
    ///
    /// Use this to validate or prepare for checkpoint creation.
    /// Returning an error will cancel the checkpoint.
    ///
    /// # Arguments
    ///
    /// * `stats` - Statistics about changes since last checkpoint
    fn pre_checkpoint(&self, stats: &ChangeStats) -> crate::error::Result<()>;
    
    /// Called after checkpoint creation
    ///
    /// Use this for notifications, logging, or post-processing.
    ///
    /// # Arguments
    ///
    /// * `checkpoint` - The newly created checkpoint
    fn post_checkpoint(&self, checkpoint: &crate::checkpoint::Checkpoint) -> crate::error::Result<()>;
    
    /// Called before restoration
    ///
    /// Use this to prepare for restoration or validate the operation.
    /// Returning an error will cancel the restore.
    ///
    /// # Arguments
    ///
    /// * `from` - Current checkpoint
    /// * `to` - Target checkpoint to restore
    fn pre_restore(&self, from: &crate::checkpoint::Checkpoint, to: &crate::checkpoint::Checkpoint) -> crate::error::Result<()>;
    
    /// Called after restoration
    ///
    /// Use this for cleanup, notifications, or verification.
    ///
    /// # Arguments
    ///
    /// * `result` - Statistics about the restore operation
    fn post_restore(&self, result: &RestoreResult) -> crate::error::Result<()>;
}

/// Default implementation of CheckpointHook that does nothing
#[derive(Debug)]
pub struct NoOpHook;

impl CheckpointHook for NoOpHook {
    fn pre_checkpoint(&self, _stats: &ChangeStats) -> crate::error::Result<()> {
        Ok(())
    }
    
    fn post_checkpoint(&self, _checkpoint: &crate::checkpoint::Checkpoint) -> crate::error::Result<()> {
        Ok(())
    }
    
    fn pre_restore(&self, _from: &crate::checkpoint::Checkpoint, _to: &crate::checkpoint::Checkpoint) -> crate::error::Result<()> {
        Ok(())
    }
    
    fn post_restore(&self, _result: &RestoreResult) -> crate::error::Result<()> {
        Ok(())
    }
}

/// State of the timeline persisted to disk
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineState {
    /// Current checkpoint ID
    pub current_checkpoint_id: Option<String>,
    /// Format version for future compatibility
    pub version: u32,
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_change_stats() {
        let mut stats = ChangeStats::default();
        assert!(!stats.has_changes());
        
        stats.files_added = 5;
        stats.bytes_added = 1000;
        assert!(stats.has_changes());
        assert_eq!(stats.total_operations(), 5);
        assert_eq!(stats.net_size_change(), 1000);
    }
    
    #[test]
    fn test_progress_info() {
        let info = ProgressInfo {
            operation: "Scanning".to_string(),
            current_item: None,
            processed: 50,
            total: Some(100),
            bytes_processed: 0,
            total_bytes: None,
        };
        
        assert_eq!(info.percentage(), Some(50.0));
    }
}