1use crate::core::{find_repo_root, Blob, Commit, Object, ObjectHash, Tree};
2use crate::response::ImportGitResponse;
3use crate::storage::ObjectStore;
4use sha1::Digest as Sha1Digest;
5use std::collections::HashMap;
6use std::fs;
7use std::io::Read;
8use std::path::{Path, PathBuf};
9
10pub fn execute(source: String) -> Result<ImportGitResponse, crate::errors::LitError> {
14 let source_path = PathBuf::from(&source);
15 let git_dir = find_git_dir(&source_path)?;
16
17 let repo_root = match find_repo_root() {
19 Ok(r) => r,
20 Err(_) => {
21 crate::commands::init::execute(false, None)?;
22 find_repo_root()?
23 }
24 };
25
26 let store = ObjectStore::new(&repo_root);
27 let mut hash_map: HashMap<String, ObjectHash> = HashMap::new();
28 let mut objects_imported = 0u64;
29 let mut refs_imported = 0u64;
30
31 let objects_dir = git_dir.join("objects");
33 if objects_dir.exists() {
34 for entry in walkdir::WalkDir::new(&objects_dir)
35 .min_depth(2)
36 .max_depth(2)
37 {
38 let entry = entry.map_err(|e| format!("Failed to walk objects: {}", e))?;
39 if !entry.file_type().is_file() {
40 continue;
41 }
42 let path = entry.path();
43 if let (Some(dir_name), Some(file_name)) =
45 (path.parent().and_then(|p| p.file_name()), path.file_name())
46 {
47 let dir_str = dir_name.to_string_lossy();
48 let file_str = file_name.to_string_lossy();
49 if dir_str == "pack" || dir_str == "info" {
51 continue;
52 }
53 let git_hash = format!("{}{}", dir_str, file_str);
54
55 match import_loose_object(path, &git_hash, &store, &mut hash_map) {
56 Ok(_) => objects_imported += 1,
57 Err(e) => {
58 eprintln!("Warning: skipping object {}: {}", &git_hash[..8], e);
59 }
60 }
61 }
62 }
63 }
64
65 let pack_dir = objects_dir.join("pack");
67 if pack_dir.exists() {
68 for entry in
69 fs::read_dir(&pack_dir).map_err(|e| format!("Failed to read pack dir: {}", e))?
70 {
71 let entry = entry.map_err(|e| format!("Pack dir entry error: {}", e))?;
72 let path = entry.path();
73 if path.extension().and_then(|e| e.to_str()) == Some("pack") {
74 match import_pack_file(&path, &store, &mut hash_map) {
75 Ok(count) => objects_imported += count,
76 Err(e) => {
77 eprintln!("Warning: skipping pack {}: {}", path.display(), e);
78 }
79 }
80 }
81 }
82 }
83
84 let refs_heads = git_dir.join("refs").join("heads");
87 if refs_heads.exists() {
88 for entry in walkdir::WalkDir::new(&refs_heads).min_depth(1) {
89 let entry = entry.map_err(|e| format!("Failed to walk refs: {}", e))?;
90 if !entry.file_type().is_file() {
91 continue;
92 }
93 let branch_name = entry
94 .path()
95 .strip_prefix(&refs_heads)
96 .map_err(|e| format!("Path error: {}", e))?
97 .to_string_lossy()
98 .replace('\\', "/");
99 let git_hash = fs::read_to_string(entry.path())
100 .map_err(|e| format!("Failed to read ref: {}", e))?
101 .trim()
102 .to_string();
103 if let Some(lit_hash) = hash_map.get(&git_hash) {
104 crate::core::write_ref(
105 &repo_root,
106 &format!("heads/{}", branch_name),
107 lit_hash.as_str(),
108 )?;
109 refs_imported += 1;
110 }
111 }
112 }
113
114 let refs_tags = git_dir.join("refs").join("tags");
116 if refs_tags.exists() {
117 for entry in walkdir::WalkDir::new(&refs_tags).min_depth(1) {
118 let entry = entry.map_err(|e| format!("Failed to walk tags: {}", e))?;
119 if !entry.file_type().is_file() {
120 continue;
121 }
122 let tag_name = entry
123 .path()
124 .strip_prefix(&refs_tags)
125 .map_err(|e| format!("Path error: {}", e))?
126 .to_string_lossy()
127 .replace('\\', "/");
128 let git_hash = fs::read_to_string(entry.path())
129 .map_err(|e| format!("Failed to read ref: {}", e))?
130 .trim()
131 .to_string();
132 if let Some(lit_hash) = hash_map.get(&git_hash) {
133 crate::core::write_ref(
134 &repo_root,
135 &format!("tags/{}", tag_name),
136 lit_hash.as_str(),
137 )?;
138 refs_imported += 1;
139 }
140 }
141 }
142
143 let head_path = git_dir.join("HEAD");
145 if head_path.exists() {
146 let head_content =
147 fs::read_to_string(&head_path).map_err(|e| format!("Failed to read HEAD: {}", e))?;
148 let head_content = head_content.trim();
149 if let Some(ref_target) = head_content.strip_prefix("ref: refs/heads/") {
150 crate::core::update_head(&repo_root, ref_target)?;
151 }
152 }
153
154 let gitignore = source_path.join(".gitignore");
156 let litignore = repo_root.join(".litignore");
157 if gitignore.exists() && !litignore.exists() {
158 let _ = fs::copy(&gitignore, &litignore);
159 }
160
161 Ok(ImportGitResponse {
162 source: source.clone(),
163 objects_imported,
164 refs_imported,
165 hash_mapping_count: hash_map.len(),
166 message: format!(
167 "Imported {} objects and {} refs from Git repository",
168 objects_imported, refs_imported
169 ),
170 })
171}
172
173fn find_git_dir(path: &Path) -> Result<PathBuf, crate::errors::LitError> {
175 let dot_git = path.join(".git");
177 if dot_git.is_dir() {
178 return Ok(dot_git);
179 }
180 if path.join("objects").is_dir() && path.join("refs").is_dir() {
182 return Ok(path.to_path_buf());
183 }
184 Err(format!("Not a Git repository: {}", path.display()).into())
185}
186
187fn import_loose_object(
189 path: &Path,
190 _git_hash: &str,
191 store: &ObjectStore,
192 hash_map: &mut HashMap<String, ObjectHash>,
193) -> Result<(), crate::errors::LitError> {
194 let compressed = fs::read(path).map_err(|e| format!("Read error: {}", e))?;
195
196 let mut decoder = flate2::read::ZlibDecoder::new(&compressed[..]);
198 let mut raw = Vec::new();
199 decoder
200 .read_to_end(&mut raw)
201 .map_err(|e| format!("Decompress error: {}", e))?;
202
203 let null_pos = raw
205 .iter()
206 .position(|&b| b == 0)
207 .ok_or("Invalid Git object: no null byte")?;
208 let header = std::str::from_utf8(&raw[..null_pos]).map_err(|_| "Invalid Git object header")?;
209 let content = &raw[null_pos + 1..];
210
211 let (obj_type, _size_str) = header
212 .split_once(' ')
213 .ok_or("Invalid Git object header format")?;
214
215 let mut sha1 = sha1::Sha1::new();
217 sha1.update(&raw);
218 let git_hash_computed = hex::encode(sha1.finalize());
219
220 let lit_obj = match obj_type {
221 "blob" => Object::Blob(Blob::new(content.to_vec())),
222 "tree" => {
223 let tree = parse_git_tree(content, hash_map)?;
224 Object::Tree(tree)
225 }
226 "commit" => {
227 let commit = parse_git_commit(content, hash_map)?;
228 Object::Commit(commit)
229 }
230 "tag" => {
231 Object::Blob(Blob::new(content.to_vec()))
233 }
234 other => return Err(format!("Unknown object type: {}", other).into()),
235 };
236
237 let lit_hash = store.write(&lit_obj)?;
238 hash_map.insert(git_hash_computed, lit_hash);
239 Ok(())
240}
241
242fn parse_git_tree(
244 content: &[u8],
245 hash_map: &HashMap<String, ObjectHash>,
246) -> Result<Tree, crate::errors::LitError> {
247 let mut tree = Tree::new();
248 let mut pos = 0;
249
250 while pos < content.len() {
251 let space_pos = content[pos..]
253 .iter()
254 .position(|&b| b == b' ')
255 .ok_or("Invalid tree entry: no space")?
256 + pos;
257 let null_pos = content[space_pos..]
258 .iter()
259 .position(|&b| b == 0)
260 .ok_or("Invalid tree entry: no null")?
261 + space_pos;
262
263 let mode = std::str::from_utf8(&content[pos..space_pos])
264 .map_err(|_| "Invalid mode in tree")?
265 .to_string();
266 let name = std::str::from_utf8(&content[space_pos + 1..null_pos])
267 .map_err(|_| "Invalid name in tree")?
268 .to_string();
269
270 if null_pos + 21 > content.len() {
271 break;
272 }
273 let sha1_bytes = &content[null_pos + 1..null_pos + 21];
274 let git_hash = hex::encode(sha1_bytes);
275
276 let lit_hash = hash_map
278 .get(&git_hash)
279 .cloned()
280 .unwrap_or_else(|| ObjectHash::from_hex(format!("{:0>192}", git_hash)));
281
282 let obj_type = if mode.starts_with("40") {
283 "tree"
284 } else {
285 "blob"
286 }
287 .to_string();
288
289 tree.add_entry(mode, name, lit_hash, obj_type);
290 pos = null_pos + 21;
291 }
292
293 Ok(tree)
294}
295
296fn parse_git_commit(
298 content: &[u8],
299 hash_map: &HashMap<String, ObjectHash>,
300) -> Result<Commit, crate::errors::LitError> {
301 let text = std::str::from_utf8(content).map_err(|_| "Invalid commit: not UTF-8")?;
302
303 let mut tree_hash = String::new();
304 let mut parents = Vec::new();
305 let mut author = String::new();
306 let mut committer = String::new();
307 let mut timestamp: i64 = 0;
308 let mut in_body = false;
309 let mut message_lines = Vec::new();
310
311 for line in text.lines() {
312 if in_body {
313 message_lines.push(line);
314 continue;
315 }
316 if line.is_empty() {
317 in_body = true;
318 continue;
319 }
320 if let Some(rest) = line.strip_prefix("tree ") {
321 tree_hash = rest.trim().to_string();
322 } else if let Some(rest) = line.strip_prefix("parent ") {
323 parents.push(rest.trim().to_string());
324 } else if let Some(rest) = line.strip_prefix("author ") {
325 let (name, ts) = parse_git_ident(rest);
326 author = name;
327 timestamp = ts;
328 } else if let Some(rest) = line.strip_prefix("committer ") {
329 let (name, _) = parse_git_ident(rest);
330 committer = name;
331 }
332 }
333
334 let lit_tree = hash_map
336 .get(&tree_hash)
337 .cloned()
338 .unwrap_or_else(|| ObjectHash::from_hex(format!("{:0>192}", tree_hash)));
339
340 let lit_parents: Vec<ObjectHash> = parents
341 .iter()
342 .map(|p| {
343 hash_map
344 .get(p)
345 .cloned()
346 .unwrap_or_else(|| ObjectHash::from_hex(format!("{:0>192}", p)))
347 })
348 .collect();
349
350 Ok(Commit {
351 tree: lit_tree,
352 parents: lit_parents,
353 author,
354 committer,
355 timestamp,
356 message: message_lines.join("\n"),
357 pq_signature: None,
358 metadata: None,
359 })
360}
361
362fn parse_git_ident(ident: &str) -> (String, i64) {
364 if let Some(bracket_pos) = ident.rfind('>') {
366 let name_email = &ident[..=bracket_pos];
367 let rest = ident[bracket_pos + 1..].trim();
368 let timestamp = rest
369 .split_whitespace()
370 .next()
371 .and_then(|s| s.parse::<i64>().ok())
372 .unwrap_or(0);
373 (name_email.trim().to_string(), timestamp)
374 } else {
375 (ident.to_string(), 0)
376 }
377}
378
379fn import_pack_file(
381 pack_path: &Path,
382 store: &ObjectStore,
383 hash_map: &mut HashMap<String, ObjectHash>,
384) -> Result<u64, crate::errors::LitError> {
385 let data = fs::read(pack_path).map_err(|e| format!("Failed to read pack: {}", e))?;
386
387 if data.len() < 12 {
389 return Err("Pack file too small".into());
390 }
391 if &data[0..4] != b"PACK" {
392 return Err("Invalid pack file magic".into());
393 }
394 let version = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
395 if version != 2 && version != 3 {
396 return Err(format!("Unsupported pack version: {}", version).into());
397 }
398 let num_objects = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
399 let mut imported = 0u64;
400
401 let mut pos = 12;
403 for _ in 0..num_objects {
404 if pos >= data.len() - 20 {
405 break;
406 }
407 match parse_pack_entry(&data, &mut pos, store, hash_map) {
408 Ok(_) => imported += 1,
409 Err(e) => {
410 eprintln!("Warning: skipping pack entry: {}", e);
411 break;
412 }
413 }
414 }
415
416 Ok(imported)
417}
418
419fn parse_pack_entry(
421 data: &[u8],
422 pos: &mut usize,
423 store: &ObjectStore,
424 hash_map: &mut HashMap<String, ObjectHash>,
425) -> Result<(), crate::errors::LitError> {
426 if *pos >= data.len() {
427 return Err("Unexpected end of pack".into());
428 }
429
430 let mut byte = data[*pos];
432 let obj_type = (byte >> 4) & 0x07;
433 let mut _size: u64 = (byte & 0x0f) as u64;
434 let mut shift = 4;
435 *pos += 1;
436
437 while byte & 0x80 != 0 {
438 if *pos >= data.len() {
439 return Err("Truncated pack header".into());
440 }
441 byte = data[*pos];
442 _size |= ((byte & 0x7f) as u64) << shift;
443 shift += 7;
444 *pos += 1;
445 }
446
447 match obj_type {
448 1..=4 => {
449 let mut decoder = flate2::read::ZlibDecoder::new(&data[*pos..]);
451 let mut content = Vec::new();
452 decoder
453 .read_to_end(&mut content)
454 .map_err(|e| format!("Decompress error: {}", e))?;
455 *pos += decoder.total_in() as usize;
456
457 let type_name = match obj_type {
459 1 => "commit",
460 2 => "tree",
461 3 => "blob",
462 4 => "tag",
463 _ => unreachable!(),
464 };
465 let header = format!("{} {}\0", type_name, content.len());
466 let mut sha1 = sha1::Sha1::new();
467 sha1.update(header.as_bytes());
468 sha1.update(&content);
469 let git_hash = hex::encode(sha1.finalize());
470
471 let lit_obj = match obj_type {
472 3 => Object::Blob(Blob::new(content)),
473 2 => {
474 let tree = parse_git_tree(&content, hash_map)?;
475 Object::Tree(tree)
476 }
477 1 => {
478 let commit = parse_git_commit(&content, hash_map)?;
479 Object::Commit(commit)
480 }
481 _ => Object::Blob(Blob::new(content)),
482 };
483
484 let lit_hash = store.write(&lit_obj)?;
485 hash_map.insert(git_hash, lit_hash);
486 }
487 6 => {
488 let mut byte = data[*pos];
491 let mut _offset: u64 = (byte & 0x7f) as u64;
492 *pos += 1;
493 while byte & 0x80 != 0 {
494 byte = data[*pos];
495 _offset = ((_offset + 1) << 7) | (byte & 0x7f) as u64;
496 *pos += 1;
497 }
498 let mut decoder = flate2::read::ZlibDecoder::new(&data[*pos..]);
500 let mut delta = Vec::new();
501 let _ = decoder.read_to_end(&mut delta);
502 *pos += decoder.total_in() as usize;
503 }
504 7 => {
505 if *pos + 20 > data.len() {
507 return Err("Truncated ref delta".into());
508 }
509 *pos += 20; let mut decoder = flate2::read::ZlibDecoder::new(&data[*pos..]);
511 let mut delta = Vec::new();
512 let _ = decoder.read_to_end(&mut delta);
513 *pos += decoder.total_in() as usize;
514 }
515 _ => {
516 return Err(format!("Unknown pack object type: {}", obj_type).into());
517 }
518 }
519
520 Ok(())
521}