1use crate::core::{find_repo_root, list_refs, read_head, Object, ObjectHash};
2use crate::response::ExportGitResponse;
3use crate::storage::ObjectStore;
4use sha1::Digest as Sha1Digest;
5use std::collections::{HashMap, HashSet};
6use std::fs;
7use std::io::Write;
8use std::path::{Path, PathBuf};
9
10pub fn execute(destination: String) -> Result<ExportGitResponse, crate::errors::LitError> {
13 let repo_root = find_repo_root()?;
14 let dest_path = PathBuf::from(&destination);
15
16 fs::create_dir_all(&dest_path).map_err(|e| format!("Failed to create destination: {}", e))?;
18 for dir in &["objects", "refs/heads", "refs/tags"] {
19 fs::create_dir_all(dest_path.join(dir))
20 .map_err(|e| format!("Failed to create {}: {}", dir, e))?;
21 }
22
23 fs::write(dest_path.join("HEAD"), "ref: refs/heads/main\n")
25 .map_err(|e| format!("Failed to write HEAD: {}", e))?;
26
27 let store = ObjectStore::new(&repo_root);
28 let mut hash_map: HashMap<String, String> = HashMap::new(); let mut objects_exported = 0u64;
30 let mut refs_exported = 0u64;
31
32 let all_objects = store
40 .list()
41 .map_err(|e| format!("Failed to list objects: {}", e))?;
42
43 let mut scheduled: HashSet<String> = HashSet::new();
44 for lit_hash in &all_objects {
45 objects_exported +=
46 export_subgraph(&store, lit_hash, &dest_path, &mut hash_map, &mut scheduled)?;
47 }
48
49 let branches =
52 list_refs(&repo_root, "heads").map_err(|e| format!("Failed to list branches: {}", e))?;
53 for branch_ref in &branches {
54 if let Some(git_hash) = hash_map.get(&branch_ref.hash) {
55 let ref_path = dest_path.join("refs").join("heads").join(&branch_ref.name);
56 if let Some(parent) = ref_path.parent() {
57 let _ = fs::create_dir_all(parent);
58 }
59 fs::write(&ref_path, format!("{}\n", git_hash))
60 .map_err(|e| format!("Failed to write ref: {}", e))?;
61 refs_exported += 1;
62 }
63 }
64
65 let tags = list_refs(&repo_root, "tags").map_err(|e| format!("Failed to list tags: {}", e))?;
67 for tag_ref in &tags {
68 if let Some(git_hash) = hash_map.get(&tag_ref.hash) {
69 let ref_path = dest_path.join("refs").join("tags").join(&tag_ref.name);
70 if let Some(parent) = ref_path.parent() {
71 let _ = fs::create_dir_all(parent);
72 }
73 fs::write(&ref_path, format!("{}\n", git_hash))
74 .map_err(|e| format!("Failed to write ref: {}", e))?;
75 refs_exported += 1;
76 }
77 }
78
79 let remotes = list_refs(&repo_root, "remotes").unwrap_or_default();
81 for remote_ref in &remotes {
82 if let Some(git_hash) = hash_map.get(&remote_ref.hash) {
83 let ref_path = dest_path
84 .join("refs")
85 .join("remotes")
86 .join(&remote_ref.name);
87 if let Some(parent) = ref_path.parent() {
88 let _ = fs::create_dir_all(parent);
89 }
90 fs::write(&ref_path, format!("{}\n", git_hash))
91 .map_err(|e| format!("Failed to write remote ref: {}", e))?;
92 refs_exported += 1;
93 }
94 }
95
96 let mut packed_refs = String::from("# pack-refs with: peeled fully-peeled sorted \n");
98 let mut has_packed = false;
99 for branch_ref in &branches {
100 if let Some(git_hash) = hash_map.get(&branch_ref.hash) {
101 packed_refs.push_str(&format!("{} refs/heads/{}\n", git_hash, branch_ref.name));
102 has_packed = true;
103 }
104 }
105 for tag_ref in &tags {
106 if let Some(git_hash) = hash_map.get(&tag_ref.hash) {
107 packed_refs.push_str(&format!("{} refs/tags/{}\n", git_hash, tag_ref.name));
108 has_packed = true;
109 }
110 }
111 if has_packed {
112 fs::write(dest_path.join("packed-refs"), &packed_refs)
113 .map_err(|e| format!("Failed to write packed-refs: {}", e))?;
114 }
115
116 let head = read_head(&repo_root).unwrap_or_else(|_| "main".to_string());
118 if head.contains('/') || head.len() > 50 {
119 if let Some(git_hash) = hash_map.get(&head) {
121 fs::write(dest_path.join("HEAD"), format!("{}\n", git_hash))
122 .map_err(|e| format!("Failed to write HEAD: {}", e))?;
123 }
124 } else {
125 fs::write(
126 dest_path.join("HEAD"),
127 format!("ref: refs/heads/{}\n", head),
128 )
129 .map_err(|e| format!("Failed to write HEAD: {}", e))?;
130 }
131
132 let litignore = repo_root.join(".litignore");
134 let gitignore = dest_path.parent().unwrap_or(&dest_path).join(".gitignore");
135 if litignore.exists() && !gitignore.exists() {
136 let _ = fs::copy(&litignore, &gitignore);
137 }
138
139 Ok(ExportGitResponse {
140 destination: destination.clone(),
141 objects_exported,
142 refs_exported,
143 message: format!(
144 "Exported {} objects and {} refs to Git repository",
145 objects_exported, refs_exported
146 ),
147 })
148}
149
150enum Step {
152 Visit(ObjectHash),
154 Emit(ObjectHash),
156}
157
158fn export_subgraph(
165 store: &ObjectStore,
166 root: &ObjectHash,
167 dest: &Path,
168 hash_map: &mut HashMap<String, String>,
169 scheduled: &mut HashSet<String>,
170) -> Result<u64, crate::errors::LitError> {
171 let mut exported = 0u64;
172 let mut stack = vec![Step::Visit(root.clone())];
173
174 while let Some(step) = stack.pop() {
175 match step {
176 Step::Visit(hash) => {
177 if hash_map.contains_key(hash.as_str())
178 || !scheduled.insert(hash.as_str().to_string())
179 {
180 continue;
181 }
182 let deps = match dependencies_of(store, &hash) {
183 Ok(deps) => deps,
184 Err(e) => {
185 eprintln!("Warning: skipping object {}: {}", hash.short(), e);
190 continue;
191 }
192 };
193 stack.push(Step::Emit(hash));
194 for dep in deps {
195 stack.push(Step::Visit(dep));
196 }
197 }
198 Step::Emit(hash) => {
199 export_object(store, &hash, dest, hash_map)?;
200 exported += 1;
201 }
202 }
203 }
204
205 Ok(exported)
206}
207
208fn dependencies_of(
210 store: &ObjectStore,
211 hash: &ObjectHash,
212) -> Result<Vec<ObjectHash>, crate::errors::LitError> {
213 Ok(match store.read(hash)? {
214 Object::Blob(_) => Vec::new(),
215 Object::Tree(tree) => tree.entries.iter().map(|e| e.hash.clone()).collect(),
216 Object::Commit(commit) => std::iter::once(commit.tree.clone())
217 .chain(commit.parents.iter().cloned())
218 .collect(),
219 Object::Tag(tag) => vec![tag.target.clone()],
220 })
221}
222
223fn lookup_git_hash(
229 hash_map: &HashMap<String, String>,
230 lit_hash: &ObjectHash,
231 context: &str,
232) -> Result<String, crate::errors::LitError> {
233 hash_map.get(lit_hash.as_str()).cloned().ok_or_else(|| {
234 crate::errors::LitError::general(format!(
235 "Cannot export {}: referenced object {} is missing from this repository",
236 context,
237 lit_hash.short()
238 ))
239 })
240}
241
242fn decode_sha1(git_hex: &str) -> Result<Vec<u8>, crate::errors::LitError> {
244 let bytes = hex::decode(git_hex).map_err(|e| format!("Invalid Git hash hex: {}", e))?;
245 if bytes.len() != 20 {
246 return Err(format!(
247 "Expected a 20-byte Git SHA-1, got {} bytes from '{}'",
248 bytes.len(),
249 git_hex
250 )
251 .into());
252 }
253 Ok(bytes)
254}
255
256fn export_object(
258 store: &ObjectStore,
259 lit_hash: &ObjectHash,
260 dest: &Path,
261 hash_map: &mut HashMap<String, String>,
262) -> Result<(), crate::errors::LitError> {
263 let obj = store.read(lit_hash)?;
264
265 let (type_name, content) = match &obj {
266 Object::Blob(blob) => ("blob", blob.content.clone()),
267 Object::Tree(tree) => {
268 let content = serialize_git_tree(tree, hash_map)?;
269 ("tree", content)
270 }
271 Object::Commit(commit) => {
272 let content = serialize_git_commit(commit, hash_map)?;
273 ("commit", content)
274 }
275 Object::Tag(tag) => {
276 let content = serialize_git_tag(tag, hash_map)?;
277 ("tag", content)
278 }
279 };
280
281 let header = format!("{} {}\0", type_name, content.len());
283 let mut sha1 = sha1::Sha1::new();
284 sha1.update(header.as_bytes());
285 sha1.update(&content);
286 let git_hash = hex::encode(sha1.finalize());
287
288 let obj_dir = dest.join("objects").join(&git_hash[..2]);
290 let obj_path = obj_dir.join(&git_hash[2..]);
291 fs::create_dir_all(&obj_dir).map_err(|e| format!("Failed to create object dir: {}", e))?;
292
293 let mut raw = Vec::new();
294 raw.extend_from_slice(header.as_bytes());
295 raw.extend_from_slice(&content);
296
297 let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
298 encoder
299 .write_all(&raw)
300 .map_err(|e| format!("Compress error: {}", e))?;
301 let compressed = encoder
302 .finish()
303 .map_err(|e| format!("Compress finish error: {}", e))?;
304
305 fs::write(&obj_path, &compressed).map_err(|e| format!("Failed to write object: {}", e))?;
306
307 hash_map.insert(lit_hash.as_str().to_string(), git_hash);
308 Ok(())
309}
310
311fn serialize_git_tree(
313 tree: &crate::core::Tree,
314 hash_map: &HashMap<String, String>,
315) -> Result<Vec<u8>, crate::errors::LitError> {
316 let mut buf = Vec::new();
317 for entry in &tree.entries {
318 buf.extend_from_slice(entry.mode.as_bytes());
320 buf.push(b' ');
321 buf.extend_from_slice(entry.name.as_bytes());
322 buf.push(0);
323
324 let git_hex = lookup_git_hash(
326 hash_map,
327 &entry.hash,
328 &format!("tree entry '{}'", entry.name),
329 )?;
330 buf.extend_from_slice(&decode_sha1(&git_hex)?);
331 }
332 Ok(buf)
333}
334
335fn serialize_git_commit(
337 commit: &crate::core::Commit,
338 hash_map: &HashMap<String, String>,
339) -> Result<Vec<u8>, crate::errors::LitError> {
340 let mut lines = Vec::new();
341
342 let tree_hash = lookup_git_hash(hash_map, &commit.tree, "commit tree")?;
344 lines.push(format!("tree {}", tree_hash));
345
346 for parent in &commit.parents {
348 let parent_hash = lookup_git_hash(hash_map, parent, "commit parent")?;
349 lines.push(format!("parent {}", parent_hash));
350 }
351
352 let timezone = commit.timezone.as_deref().unwrap_or("+0000");
355 lines.push(format!(
356 "author {} {} {}",
357 commit.author, commit.timestamp, timezone
358 ));
359 lines.push(format!(
360 "committer {} {} {}",
361 commit.committer, commit.timestamp, timezone
362 ));
363
364 let mut message = commit.message.clone();
366 if let Some(ref meta) = commit.metadata {
367 message.push_str(&format!("\n\nLit-Metadata: {}", meta));
368 }
369
370 lines.push(String::new()); lines.push(message);
372
373 Ok(lines.join("\n").into_bytes())
374}
375
376fn serialize_git_tag(
378 tag: &crate::core::Tag,
379 hash_map: &HashMap<String, String>,
380) -> Result<Vec<u8>, crate::errors::LitError> {
381 let target_hash = lookup_git_hash(hash_map, &tag.target, "tag target")?;
382
383 let timezone = tag.timezone.as_deref().unwrap_or("+0000");
386
387 let mut lines = Vec::new();
388 lines.push(format!("object {}", target_hash));
389 lines.push(format!("type {}", tag.target_type));
390 lines.push(format!("tag {}", tag.tag_name));
391 lines.push(format!(
392 "tagger {} {} {}",
393 tag.tagger, tag.timestamp, timezone
394 ));
395 lines.push(String::new());
396 lines.push(tag.message.clone());
397
398 Ok(lines.join("\n").into_bytes())
399}