dx_forge/storage/
mod.rs

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