dx_forge/storage/
mod.rs

1pub mod blob;
2pub mod db;
3pub mod git_interop;
4pub mod oplog;
5pub mod r2;
6
7use anyhow::Result;
8use colored::*;
9use ropey::Rope;
10use std::path::Path;
11
12pub use blob::{Blob, BlobMetadata, BlobRepository};
13pub use db::Database;
14pub use oplog::OperationLog;
15pub use r2::{batch_upload_blobs, R2Config, R2Storage, SyncResult};
16
17const FORGE_DIR: &str = ".dx/forge";
18
19pub async fn init(path: &Path) -> Result<()> {
20    let forge_path = path.join(FORGE_DIR);
21
22    tokio::fs::create_dir_all(&forge_path).await?;
23    tokio::fs::create_dir_all(forge_path.join("objects")).await?;
24    tokio::fs::create_dir_all(forge_path.join("refs")).await?;
25    tokio::fs::create_dir_all(forge_path.join("logs")).await?;
26    tokio::fs::create_dir_all(forge_path.join("context")).await?;
27
28    // Initialize database
29    let db = Database::new(&forge_path)?;
30    db.initialize()?;
31
32    // Create config
33    let config = serde_json::json!({
34        "version": "0.1.0",
35        "actor_id": uuid::Uuid::new_v4().to_string(),
36        "repo_id": uuid::Uuid::new_v4().to_string(),
37        "git_interop": true,
38        "real_time_sync": false,
39    });
40
41    tokio::fs::write(
42        forge_path.join("config.json"),
43        serde_json::to_string_pretty(&config)?,
44    )
45    .await?;
46
47    Ok(())
48}
49
50pub async fn show_log(file: Option<std::path::PathBuf>, limit: usize) -> Result<()> {
51    let db = Database::open(".dx/forge")?;
52    let operations = db.get_operations(file.as_deref(), limit)?;
53
54    println!("{}", "Operation Log".cyan().bold());
55    println!("{}", "═".repeat(80).bright_black());
56
57    for op in operations {
58        let time = op.timestamp.format("%Y-%m-%d %H:%M:%S%.3f");
59        let op_type = match &op.op_type {
60            crate::crdt::OperationType::Insert { length, .. } => {
61                format!("+{} chars", length).green()
62            }
63            crate::crdt::OperationType::Delete { length, .. } => format!("-{} chars", length).red(),
64            crate::crdt::OperationType::Replace {
65                old_content,
66                new_content,
67                ..
68            } => format!("~{}->{} chars", old_content.len(), new_content.len()).yellow(),
69            crate::crdt::OperationType::FileCreate { .. } => "FILE_CREATE".bright_green(),
70            crate::crdt::OperationType::FileDelete => "FILE_DELETE".bright_red(),
71            crate::crdt::OperationType::FileRename { old_path, new_path } => {
72                format!("RENAME {} -> {}", old_path, new_path).bright_yellow()
73            }
74        };
75
76        println!(
77            "{} {} {} {}",
78            format!("[{}]", time).bright_black(),
79            op_type.bold(),
80            op.file_path.bright_white(),
81            format!("({})", op.id).bright_black()
82        );
83    }
84
85    Ok(())
86}
87
88pub async fn git_sync(path: &Path) -> Result<()> {
89    git_interop::sync_with_git(path).await
90}
91
92pub async fn time_travel(file: &Path, timestamp: Option<String>) -> Result<()> {
93    println!(
94        "{}",
95        format!("🕐 Time traveling: {}", file.display())
96            .cyan()
97            .bold()
98    );
99
100    let repo_root = std::env::current_dir()?;
101    let forge_path = repo_root.join(FORGE_DIR);
102    let db = Database::new(&forge_path)?;
103    db.initialize()?;
104
105    let target_path = if file.is_absolute() {
106        file.to_path_buf()
107    } else {
108        repo_root.join(file)
109    };
110    let target_canon = normalize_path(&target_path);
111
112    let mut operations = db.get_operations(None, 2000)?;
113
114    // Reconstruct file state at timestamp
115    let target_time = if let Some(ts) = timestamp {
116        chrono::DateTime::parse_from_rfc3339(&ts)?.with_timezone(&chrono::Utc)
117    } else {
118        chrono::Utc::now()
119    };
120
121    operations.retain(|op| {
122        op.timestamp <= target_time
123            && normalize_path(std::path::Path::new(&op.file_path)) == target_canon
124    });
125    operations.sort_by_key(|op| op.timestamp);
126
127    let mut rope = Rope::new();
128
129    for op in operations.iter() {
130        match &op.op_type {
131            crate::crdt::OperationType::FileCreate { content: c } => {
132                rope = Rope::from_str(c);
133            }
134            crate::crdt::OperationType::Insert {
135                position, content, ..
136            } => {
137                let char_idx = clamp_offset(&rope, position.offset);
138                rope.insert(char_idx, content);
139            }
140            crate::crdt::OperationType::Delete { position, length } => {
141                let start = clamp_offset(&rope, position.offset);
142                let end = clamp_offset(&rope, start + *length);
143                if start < end {
144                    rope.remove(start..end);
145                }
146            }
147            crate::crdt::OperationType::Replace {
148                position,
149                old_content,
150                new_content,
151            } => {
152                let start = clamp_offset(&rope, position.offset);
153                let end = clamp_offset(&rope, start + old_content.chars().count());
154                if start < end {
155                    rope.remove(start..end);
156                }
157                rope.insert(start, new_content);
158            }
159            crate::crdt::OperationType::FileDelete => {
160                rope = Rope::new();
161            }
162            crate::crdt::OperationType::FileRename { .. } => {
163                // Rename events are handled by resolving the target path above.
164            }
165        }
166    }
167
168    let content = rope.to_string();
169
170    println!("\n{}", "─".repeat(80).bright_black());
171    println!("{}", content);
172    println!("{}", "─".repeat(80).bright_black());
173
174    Ok(())
175}
176
177fn normalize_path(path: &Path) -> std::path::PathBuf {
178    path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
179}
180
181fn clamp_offset(rope: &Rope, offset: usize) -> usize {
182    offset.min(rope.len_chars())
183}