use crate::core::{find_repo_root, Blob, Commit, Object, ObjectHash, Tree};
use crate::response::ImportGitResponse;
use crate::storage::ObjectStore;
use sha1::Digest as Sha1Digest;
use std::collections::HashMap;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
pub fn execute(source: String) -> Result<ImportGitResponse, crate::errors::LitError> {
let source_path = PathBuf::from(&source);
let git_dir = find_git_dir(&source_path)?;
let repo_root = match find_repo_root() {
Ok(r) => r,
Err(_) => {
crate::commands::init::execute(false, None)?;
find_repo_root()?
}
};
let store = ObjectStore::new(&repo_root);
let mut hash_map: HashMap<String, ObjectHash> = HashMap::new();
let mut objects_imported = 0u64;
let mut refs_imported = 0u64;
let objects_dir = git_dir.join("objects");
if objects_dir.exists() {
for entry in walkdir::WalkDir::new(&objects_dir)
.min_depth(2)
.max_depth(2)
{
let entry = entry.map_err(|e| format!("Failed to walk objects: {}", e))?;
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
if let (Some(dir_name), Some(file_name)) =
(path.parent().and_then(|p| p.file_name()), path.file_name())
{
let dir_str = dir_name.to_string_lossy();
let file_str = file_name.to_string_lossy();
if dir_str == "pack" || dir_str == "info" {
continue;
}
let git_hash = format!("{}{}", dir_str, file_str);
match import_loose_object(path, &git_hash, &store, &mut hash_map) {
Ok(_) => objects_imported += 1,
Err(e) => {
eprintln!("Warning: skipping object {}: {}", &git_hash[..8], e);
}
}
}
}
}
let pack_dir = objects_dir.join("pack");
if pack_dir.exists() {
for entry in
fs::read_dir(&pack_dir).map_err(|e| format!("Failed to read pack dir: {}", e))?
{
let entry = entry.map_err(|e| format!("Pack dir entry error: {}", e))?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("pack") {
match import_pack_file(&path, &store, &mut hash_map) {
Ok(count) => objects_imported += count,
Err(e) => {
eprintln!("Warning: skipping pack {}: {}", path.display(), e);
}
}
}
}
}
let refs_heads = git_dir.join("refs").join("heads");
if refs_heads.exists() {
for entry in walkdir::WalkDir::new(&refs_heads).min_depth(1) {
let entry = entry.map_err(|e| format!("Failed to walk refs: {}", e))?;
if !entry.file_type().is_file() {
continue;
}
let branch_name = entry
.path()
.strip_prefix(&refs_heads)
.map_err(|e| format!("Path error: {}", e))?
.to_string_lossy()
.replace('\\', "/");
let git_hash = fs::read_to_string(entry.path())
.map_err(|e| format!("Failed to read ref: {}", e))?
.trim()
.to_string();
if let Some(lit_hash) = hash_map.get(&git_hash) {
crate::core::write_ref(
&repo_root,
&format!("heads/{}", branch_name),
lit_hash.as_str(),
)?;
refs_imported += 1;
}
}
}
let refs_tags = git_dir.join("refs").join("tags");
if refs_tags.exists() {
for entry in walkdir::WalkDir::new(&refs_tags).min_depth(1) {
let entry = entry.map_err(|e| format!("Failed to walk tags: {}", e))?;
if !entry.file_type().is_file() {
continue;
}
let tag_name = entry
.path()
.strip_prefix(&refs_tags)
.map_err(|e| format!("Path error: {}", e))?
.to_string_lossy()
.replace('\\', "/");
let git_hash = fs::read_to_string(entry.path())
.map_err(|e| format!("Failed to read ref: {}", e))?
.trim()
.to_string();
if let Some(lit_hash) = hash_map.get(&git_hash) {
crate::core::write_ref(
&repo_root,
&format!("tags/{}", tag_name),
lit_hash.as_str(),
)?;
refs_imported += 1;
}
}
}
let head_path = git_dir.join("HEAD");
if head_path.exists() {
let head_content =
fs::read_to_string(&head_path).map_err(|e| format!("Failed to read HEAD: {}", e))?;
let head_content = head_content.trim();
if let Some(ref_target) = head_content.strip_prefix("ref: refs/heads/") {
crate::core::update_head(&repo_root, ref_target)?;
}
}
let gitignore = source_path.join(".gitignore");
let litignore = repo_root.join(".litignore");
if gitignore.exists() && !litignore.exists() {
let _ = fs::copy(&gitignore, &litignore);
}
Ok(ImportGitResponse {
source: source.clone(),
objects_imported,
refs_imported,
hash_mapping_count: hash_map.len(),
message: format!(
"Imported {} objects and {} refs from Git repository",
objects_imported, refs_imported
),
})
}
fn find_git_dir(path: &Path) -> Result<PathBuf, crate::errors::LitError> {
let dot_git = path.join(".git");
if dot_git.is_dir() {
return Ok(dot_git);
}
if path.join("objects").is_dir() && path.join("refs").is_dir() {
return Ok(path.to_path_buf());
}
Err(format!("Not a Git repository: {}", path.display()).into())
}
fn import_loose_object(
path: &Path,
_git_hash: &str,
store: &ObjectStore,
hash_map: &mut HashMap<String, ObjectHash>,
) -> Result<(), crate::errors::LitError> {
let compressed = fs::read(path).map_err(|e| format!("Read error: {}", e))?;
let mut decoder = flate2::read::ZlibDecoder::new(&compressed[..]);
let mut raw = Vec::new();
decoder
.read_to_end(&mut raw)
.map_err(|e| format!("Decompress error: {}", e))?;
let null_pos = raw
.iter()
.position(|&b| b == 0)
.ok_or("Invalid Git object: no null byte")?;
let header = std::str::from_utf8(&raw[..null_pos]).map_err(|_| "Invalid Git object header")?;
let content = &raw[null_pos + 1..];
let (obj_type, _size_str) = header
.split_once(' ')
.ok_or("Invalid Git object header format")?;
let mut sha1 = sha1::Sha1::new();
sha1.update(&raw);
let git_hash_computed = hex::encode(sha1.finalize());
let lit_obj = match obj_type {
"blob" => Object::Blob(Blob::new(content.to_vec())),
"tree" => {
let tree = parse_git_tree(content, hash_map)?;
Object::Tree(tree)
}
"commit" => {
let commit = parse_git_commit(content, hash_map)?;
Object::Commit(commit)
}
"tag" => {
Object::Blob(Blob::new(content.to_vec()))
}
other => return Err(format!("Unknown object type: {}", other).into()),
};
let lit_hash = store.write(&lit_obj)?;
hash_map.insert(git_hash_computed, lit_hash);
Ok(())
}
fn parse_git_tree(
content: &[u8],
hash_map: &HashMap<String, ObjectHash>,
) -> Result<Tree, crate::errors::LitError> {
let mut tree = Tree::new();
let mut pos = 0;
while pos < content.len() {
let space_pos = content[pos..]
.iter()
.position(|&b| b == b' ')
.ok_or("Invalid tree entry: no space")?
+ pos;
let null_pos = content[space_pos..]
.iter()
.position(|&b| b == 0)
.ok_or("Invalid tree entry: no null")?
+ space_pos;
let mode = std::str::from_utf8(&content[pos..space_pos])
.map_err(|_| "Invalid mode in tree")?
.to_string();
let name = std::str::from_utf8(&content[space_pos + 1..null_pos])
.map_err(|_| "Invalid name in tree")?
.to_string();
if null_pos + 21 > content.len() {
break;
}
let sha1_bytes = &content[null_pos + 1..null_pos + 21];
let git_hash = hex::encode(sha1_bytes);
let lit_hash = hash_map
.get(&git_hash)
.cloned()
.unwrap_or_else(|| ObjectHash::from_hex(format!("{:0>192}", git_hash)));
let obj_type = if mode.starts_with("40") {
"tree"
} else {
"blob"
}
.to_string();
tree.add_entry(mode, name, lit_hash, obj_type);
pos = null_pos + 21;
}
Ok(tree)
}
fn parse_git_commit(
content: &[u8],
hash_map: &HashMap<String, ObjectHash>,
) -> Result<Commit, crate::errors::LitError> {
let text = std::str::from_utf8(content).map_err(|_| "Invalid commit: not UTF-8")?;
let mut tree_hash = String::new();
let mut parents = Vec::new();
let mut author = String::new();
let mut committer = String::new();
let mut timestamp: i64 = 0;
let mut in_body = false;
let mut message_lines = Vec::new();
for line in text.lines() {
if in_body {
message_lines.push(line);
continue;
}
if line.is_empty() {
in_body = true;
continue;
}
if let Some(rest) = line.strip_prefix("tree ") {
tree_hash = rest.trim().to_string();
} else if let Some(rest) = line.strip_prefix("parent ") {
parents.push(rest.trim().to_string());
} else if let Some(rest) = line.strip_prefix("author ") {
let (name, ts) = parse_git_ident(rest);
author = name;
timestamp = ts;
} else if let Some(rest) = line.strip_prefix("committer ") {
let (name, _) = parse_git_ident(rest);
committer = name;
}
}
let lit_tree = hash_map
.get(&tree_hash)
.cloned()
.unwrap_or_else(|| ObjectHash::from_hex(format!("{:0>192}", tree_hash)));
let lit_parents: Vec<ObjectHash> = parents
.iter()
.map(|p| {
hash_map
.get(p)
.cloned()
.unwrap_or_else(|| ObjectHash::from_hex(format!("{:0>192}", p)))
})
.collect();
Ok(Commit {
tree: lit_tree,
parents: lit_parents,
author,
committer,
timestamp,
message: message_lines.join("\n"),
pq_signature: None,
metadata: None,
})
}
fn parse_git_ident(ident: &str) -> (String, i64) {
if let Some(bracket_pos) = ident.rfind('>') {
let name_email = &ident[..=bracket_pos];
let rest = ident[bracket_pos + 1..].trim();
let timestamp = rest
.split_whitespace()
.next()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
(name_email.trim().to_string(), timestamp)
} else {
(ident.to_string(), 0)
}
}
fn import_pack_file(
pack_path: &Path,
store: &ObjectStore,
hash_map: &mut HashMap<String, ObjectHash>,
) -> Result<u64, crate::errors::LitError> {
let data = fs::read(pack_path).map_err(|e| format!("Failed to read pack: {}", e))?;
if data.len() < 12 {
return Err("Pack file too small".into());
}
if &data[0..4] != b"PACK" {
return Err("Invalid pack file magic".into());
}
let version = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
if version != 2 && version != 3 {
return Err(format!("Unsupported pack version: {}", version).into());
}
let num_objects = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
let mut imported = 0u64;
let mut pos = 12;
for _ in 0..num_objects {
if pos >= data.len() - 20 {
break;
}
match parse_pack_entry(&data, &mut pos, store, hash_map) {
Ok(_) => imported += 1,
Err(e) => {
eprintln!("Warning: skipping pack entry: {}", e);
break;
}
}
}
Ok(imported)
}
fn parse_pack_entry(
data: &[u8],
pos: &mut usize,
store: &ObjectStore,
hash_map: &mut HashMap<String, ObjectHash>,
) -> Result<(), crate::errors::LitError> {
if *pos >= data.len() {
return Err("Unexpected end of pack".into());
}
let mut byte = data[*pos];
let obj_type = (byte >> 4) & 0x07;
let mut _size: u64 = (byte & 0x0f) as u64;
let mut shift = 4;
*pos += 1;
while byte & 0x80 != 0 {
if *pos >= data.len() {
return Err("Truncated pack header".into());
}
byte = data[*pos];
_size |= ((byte & 0x7f) as u64) << shift;
shift += 7;
*pos += 1;
}
match obj_type {
1..=4 => {
let mut decoder = flate2::read::ZlibDecoder::new(&data[*pos..]);
let mut content = Vec::new();
decoder
.read_to_end(&mut content)
.map_err(|e| format!("Decompress error: {}", e))?;
*pos += decoder.total_in() as usize;
let type_name = match obj_type {
1 => "commit",
2 => "tree",
3 => "blob",
4 => "tag",
_ => unreachable!(),
};
let header = format!("{} {}\0", type_name, content.len());
let mut sha1 = sha1::Sha1::new();
sha1.update(header.as_bytes());
sha1.update(&content);
let git_hash = hex::encode(sha1.finalize());
let lit_obj = match obj_type {
3 => Object::Blob(Blob::new(content)),
2 => {
let tree = parse_git_tree(&content, hash_map)?;
Object::Tree(tree)
}
1 => {
let commit = parse_git_commit(&content, hash_map)?;
Object::Commit(commit)
}
_ => Object::Blob(Blob::new(content)),
};
let lit_hash = store.write(&lit_obj)?;
hash_map.insert(git_hash, lit_hash);
}
6 => {
let mut byte = data[*pos];
let mut _offset: u64 = (byte & 0x7f) as u64;
*pos += 1;
while byte & 0x80 != 0 {
byte = data[*pos];
_offset = ((_offset + 1) << 7) | (byte & 0x7f) as u64;
*pos += 1;
}
let mut decoder = flate2::read::ZlibDecoder::new(&data[*pos..]);
let mut delta = Vec::new();
let _ = decoder.read_to_end(&mut delta);
*pos += decoder.total_in() as usize;
}
7 => {
if *pos + 20 > data.len() {
return Err("Truncated ref delta".into());
}
*pos += 20; let mut decoder = flate2::read::ZlibDecoder::new(&data[*pos..]);
let mut delta = Vec::new();
let _ = decoder.read_to_end(&mut delta);
*pos += decoder.total_in() as usize;
}
_ => {
return Err(format!("Unknown pack object type: {}", obj_type).into());
}
}
Ok(())
}