1use std::path::Path;
28use suture_core::repository::Repository;
29use thiserror::Error;
30
31#[derive(Error, Debug)]
32pub enum BridgeError {
33 #[error("git command failed: {0}")]
34 GitCommand(String),
35 #[error("I/O error: {0}")]
36 Io(#[from] std::io::Error),
37 #[error("suture error: {0}")]
38 Suture(String),
39 #[error("invalid git repository: {0}")]
40 InvalidGitRepo(String),
41}
42
43#[deprecated(
67 since = "0.1.0",
68 note = "Git bridge is experimental and may lose data. See module docs."
69)]
70pub fn import_from_git(
71 git_path: &Path,
72 suture_path: &Path,
73 author: &str,
74) -> Result<ImportResult, BridgeError> {
75 use std::process::Command;
76
77 let output = Command::new("git")
78 .args(["-C", &git_path.to_string_lossy(), "rev-parse", "--git-dir"])
79 .output()
80 .map_err(|e| BridgeError::GitCommand(format!("git not found: {}", e)))?;
81
82 if !output.status.success() {
83 return Err(BridgeError::InvalidGitRepo(
84 git_path.to_string_lossy().to_string(),
85 ));
86 }
87
88 let mut repo =
89 Repository::init(suture_path, author).map_err(|e| BridgeError::Suture(e.to_string()))?;
90
91 let _ = repo.set_config("user.name", author);
92
93 let output = Command::new("git")
94 .args([
95 "-C",
96 &git_path.to_string_lossy(),
97 "log",
98 "--reverse",
99 "--format=%H %s",
100 "--all",
101 ])
102 .output()
103 .map_err(|e| BridgeError::GitCommand(e.to_string()))?;
104
105 let commit_list = String::from_utf8_lossy(&output.stdout);
106 let mut patches_imported = 0usize;
107 let mut branches_imported = 0usize;
108
109 for line in commit_list.lines() {
110 let parts: Vec<&str> = line.splitn(2, ' ').collect();
111 if parts.len() != 2 {
112 continue;
113 }
114 let sha = parts[0];
115 let message = parts[1];
116
117 let diff_output = Command::new("git")
118 .args([
119 "-C",
120 &git_path.to_string_lossy(),
121 "diff-tree",
122 "--no-commit-id",
123 "-r",
124 "--name-status",
125 sha,
126 ])
127 .output()
128 .map_err(|e| BridgeError::GitCommand(e.to_string()))?;
129
130 let diff = String::from_utf8_lossy(&diff_output.stdout);
131
132 for diff_line in diff.lines() {
133 let parts: Vec<&str> = diff_line.splitn(2, '\t').collect();
134 if parts.len() != 2 {
135 continue;
136 }
137 let status = parts[0].trim();
138 let filepath = parts[1].trim();
139
140 let git_file = git_path.join(filepath);
141 let suture_file = suture_path.join(filepath);
142
143 match status {
144 "M" | "A" => {
145 if let Some(parent) = suture_file.parent() {
146 std::fs::create_dir_all(parent)?;
147 }
148 if git_file.exists() {
149 std::fs::copy(&git_file, &suture_file)?;
150 repo.add(filepath)
151 .map_err(|e| BridgeError::Suture(e.to_string()))?;
152 }
153 }
154 "D" => {
155 if suture_file.exists() {
156 std::fs::remove_file(&suture_file)?;
157 repo.add(filepath)
158 .map_err(|e| BridgeError::Suture(e.to_string()))?;
159 }
160 }
161 "R" => {
162 let rename_parts: Vec<&str> = filepath.split('\t').collect();
163 if rename_parts.len() == 2 {
164 let new_path = suture_path.join(rename_parts[1]);
165 if let Some(new_parent) = new_path.parent() {
166 std::fs::create_dir_all(new_parent)?;
167 }
168 let old_path = suture_path.join(rename_parts[0]);
169 if old_path.exists() {
170 std::fs::rename(&old_path, &new_path)?;
171 repo.rename_file(rename_parts[0], rename_parts[1])
172 .map_err(|e| BridgeError::Suture(e.to_string()))?;
173 }
174 }
175 }
176 _ => {}
177 }
178 }
179
180 if diff.lines().count() > 0 {
181 repo.commit(message)
182 .map_err(|e| BridgeError::Suture(e.to_string()))?;
183 patches_imported += 1;
184 }
185 }
186
187 let branch_output = Command::new("git")
188 .args([
189 "-C",
190 &git_path.to_string_lossy(),
191 "branch",
192 "--format=%(refname:short)",
193 ])
194 .output()
195 .map_err(|e| BridgeError::GitCommand(e.to_string()))?;
196
197 let branches = String::from_utf8_lossy(&branch_output.stdout);
198 for branch in branches.lines() {
199 let branch = branch.trim();
200 if branch.is_empty() || branch == "HEAD" {
201 continue;
202 }
203 let _sha_output = Command::new("git")
204 .args(["-C", &git_path.to_string_lossy(), "rev-parse", branch])
205 .output()
206 .map_err(|e| BridgeError::GitCommand(e.to_string()))?;
207
208 branches_imported += 1;
209 }
210
211 Ok(ImportResult {
212 patches_imported,
213 branches_imported,
214 })
215}
216
217#[derive(Debug, Clone)]
219pub struct ImportResult {
220 pub patches_imported: usize,
222 pub branches_imported: usize,
224}
225
226#[deprecated(
243 since = "0.1.0",
244 note = "Git bridge is experimental and may produce incorrect results. See module docs."
245)]
246pub fn export_to_git(suture_path: &Path, git_path: &Path) -> Result<ExportResult, BridgeError> {
247 use std::process::Command;
248
249 let output = Command::new("git")
250 .args(["init", &git_path.to_string_lossy()])
251 .output()
252 .map_err(|e| BridgeError::GitCommand(e.to_string()))?;
253
254 if !output.status.success() {
255 return Err(BridgeError::GitCommand("git init failed".to_string()));
256 }
257
258 Command::new("git")
259 .args([
260 "-C",
261 &git_path.to_string_lossy(),
262 "config",
263 "user.name",
264 "suture-bridge",
265 ])
266 .output()
267 .map_err(|e| BridgeError::GitCommand(e.to_string()))?;
268
269 Command::new("git")
270 .args([
271 "-C",
272 &git_path.to_string_lossy(),
273 "config",
274 "user.email",
275 "bridge@suture.dev",
276 ])
277 .output()
278 .map_err(|e| BridgeError::GitCommand(e.to_string()))?;
279
280 let repo = Repository::open(suture_path).map_err(|e| BridgeError::Suture(e.to_string()))?;
281
282 let branches = repo.list_branches();
283
284 let main_id = branches
285 .iter()
286 .find(|(name, _)| name == "main")
287 .map(|(_, id)| *id);
288
289 let mut patches_exported = 0usize;
290 let mut branches_exported = 0usize;
291
292 if let Some(_tip_id) = main_id {
293 let log = repo
294 .log(None)
295 .map_err(|e| BridgeError::Suture(e.to_string()))?;
296
297 let head_tree = repo
298 .snapshot_head()
299 .map_err(|e| BridgeError::Suture(e.to_string()))?;
300
301 for patch in &log {
302 if let Some(ref target_path) = patch.target_path {
303 let git_file = git_path.join(target_path);
304
305 match patch.operation_type {
306 suture_core::patch::types::OperationType::Delete => {
307 if git_file.exists() {
308 std::fs::remove_file(&git_file)?;
309 }
310 }
311 _ => {
312 if let Some(hash) = head_tree.get(target_path) {
313 if let Ok(blob) = repo.cas().get_blob(hash) {
314 if let Some(parent) = git_file.parent() {
315 std::fs::create_dir_all(parent)?;
316 }
317 std::fs::write(&git_file, blob)?;
318 }
319 }
320 }
321 }
322 }
323
324 Command::new("git")
325 .args(["-C", &git_path.to_string_lossy(), "add", "-A"])
326 .output()
327 .map_err(|e| BridgeError::GitCommand(e.to_string()))?;
328
329 let output = Command::new("git")
330 .args([
331 "-C",
332 &git_path.to_string_lossy(),
333 "commit",
334 "-m",
335 &patch.message,
336 "--allow-empty",
337 ])
338 .output()
339 .map_err(|e| BridgeError::GitCommand(e.to_string()))?;
340
341 if output.status.success() {
342 patches_exported += 1;
343 }
344 }
345
346 branches_exported += 1;
347 }
348
349 Ok(ExportResult {
350 patches_exported,
351 branches_exported,
352 })
353}
354
355#[derive(Debug, Clone)]
357pub struct ExportResult {
358 pub patches_exported: usize,
360 pub branches_exported: usize,
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_bridge_error_display() {
370 let err = BridgeError::GitCommand("git not found".to_string());
371 assert!(err.to_string().contains("git not found"));
372 }
373
374 #[test]
375 fn test_import_result_fields() {
376 let result = ImportResult {
377 patches_imported: 10,
378 branches_imported: 3,
379 };
380 assert_eq!(result.patches_imported, 10);
381 assert_eq!(result.branches_imported, 3);
382 }
383
384 #[test]
385 fn test_export_result_fields() {
386 let result = ExportResult {
387 patches_exported: 5,
388 branches_exported: 1,
389 };
390 assert_eq!(result.patches_exported, 5);
391 assert_eq!(result.branches_exported, 1);
392 }
393
394 #[test]
395 #[allow(deprecated)]
396 fn test_invalid_git_repo() {
397 let result = import_from_git(
398 Path::new("/nonexistent/path/to/git/repo"),
399 Path::new("/tmp/suture-test"),
400 "test",
401 );
402 assert!(result.is_err());
403 }
404}