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;
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
34 .list()
35 .map_err(|e| format!("Failed to list objects: {}", e))?;
36
37 for lit_hash in &all_objects {
38 match export_object(&store, lit_hash, &dest_path, &mut hash_map) {
39 Ok(_) => objects_exported += 1,
40 Err(e) => {
41 eprintln!("Warning: skipping object {}: {}", lit_hash.short(), e);
42 }
43 }
44 }
45
46 let branches =
49 list_refs(&repo_root, "heads").map_err(|e| format!("Failed to list branches: {}", e))?;
50 for branch_ref in &branches {
51 if let Some(git_hash) = hash_map.get(&branch_ref.hash) {
52 let ref_path = dest_path.join("refs").join("heads").join(&branch_ref.name);
53 if let Some(parent) = ref_path.parent() {
54 let _ = fs::create_dir_all(parent);
55 }
56 fs::write(&ref_path, format!("{}\n", git_hash))
57 .map_err(|e| format!("Failed to write ref: {}", e))?;
58 refs_exported += 1;
59 }
60 }
61
62 let tags = list_refs(&repo_root, "tags").map_err(|e| format!("Failed to list tags: {}", e))?;
64 for tag_ref in &tags {
65 if let Some(git_hash) = hash_map.get(&tag_ref.hash) {
66 let ref_path = dest_path.join("refs").join("tags").join(&tag_ref.name);
67 if let Some(parent) = ref_path.parent() {
68 let _ = fs::create_dir_all(parent);
69 }
70 fs::write(&ref_path, format!("{}\n", git_hash))
71 .map_err(|e| format!("Failed to write ref: {}", e))?;
72 refs_exported += 1;
73 }
74 }
75
76 let remotes = list_refs(&repo_root, "remotes").unwrap_or_default();
78 for remote_ref in &remotes {
79 if let Some(git_hash) = hash_map.get(&remote_ref.hash) {
80 let ref_path = dest_path
81 .join("refs")
82 .join("remotes")
83 .join(&remote_ref.name);
84 if let Some(parent) = ref_path.parent() {
85 let _ = fs::create_dir_all(parent);
86 }
87 fs::write(&ref_path, format!("{}\n", git_hash))
88 .map_err(|e| format!("Failed to write remote ref: {}", e))?;
89 refs_exported += 1;
90 }
91 }
92
93 let mut packed_refs = String::from("# pack-refs with: peeled fully-peeled sorted \n");
95 let mut has_packed = false;
96 for branch_ref in &branches {
97 if let Some(git_hash) = hash_map.get(&branch_ref.hash) {
98 packed_refs.push_str(&format!("{} refs/heads/{}\n", git_hash, branch_ref.name));
99 has_packed = true;
100 }
101 }
102 for tag_ref in &tags {
103 if let Some(git_hash) = hash_map.get(&tag_ref.hash) {
104 packed_refs.push_str(&format!("{} refs/tags/{}\n", git_hash, tag_ref.name));
105 has_packed = true;
106 }
107 }
108 if has_packed {
109 fs::write(dest_path.join("packed-refs"), &packed_refs)
110 .map_err(|e| format!("Failed to write packed-refs: {}", e))?;
111 }
112
113 let head = read_head(&repo_root).unwrap_or_else(|_| "main".to_string());
115 if head.contains('/') || head.len() > 50 {
116 if let Some(git_hash) = hash_map.get(&head) {
118 fs::write(dest_path.join("HEAD"), format!("{}\n", git_hash))
119 .map_err(|e| format!("Failed to write HEAD: {}", e))?;
120 }
121 } else {
122 fs::write(
123 dest_path.join("HEAD"),
124 format!("ref: refs/heads/{}\n", head),
125 )
126 .map_err(|e| format!("Failed to write HEAD: {}", e))?;
127 }
128
129 let litignore = repo_root.join(".litignore");
131 let gitignore = dest_path.parent().unwrap_or(&dest_path).join(".gitignore");
132 if litignore.exists() && !gitignore.exists() {
133 let _ = fs::copy(&litignore, &gitignore);
134 }
135
136 Ok(ExportGitResponse {
137 destination: destination.clone(),
138 objects_exported,
139 refs_exported,
140 message: format!(
141 "Exported {} objects and {} refs to Git repository",
142 objects_exported, refs_exported
143 ),
144 })
145}
146
147fn export_object(
149 store: &ObjectStore,
150 lit_hash: &ObjectHash,
151 dest: &Path,
152 hash_map: &mut HashMap<String, String>,
153) -> Result<(), crate::errors::LitError> {
154 let obj = store.read(lit_hash)?;
155
156 let (type_name, content) = match &obj {
157 Object::Blob(blob) => ("blob", blob.content.clone()),
158 Object::Tree(tree) => {
159 let content = serialize_git_tree(tree, hash_map)?;
160 ("tree", content)
161 }
162 Object::Commit(commit) => {
163 let content = serialize_git_commit(commit, hash_map)?;
164 ("commit", content)
165 }
166 Object::Tag(tag) => {
167 let content = serialize_git_tag(tag, hash_map)?;
168 ("tag", content)
169 }
170 };
171
172 let header = format!("{} {}\0", type_name, content.len());
174 let mut sha1 = sha1::Sha1::new();
175 sha1.update(header.as_bytes());
176 sha1.update(&content);
177 let git_hash = hex::encode(sha1.finalize());
178
179 let obj_dir = dest.join("objects").join(&git_hash[..2]);
181 let obj_path = obj_dir.join(&git_hash[2..]);
182 fs::create_dir_all(&obj_dir).map_err(|e| format!("Failed to create object dir: {}", e))?;
183
184 let mut raw = Vec::new();
185 raw.extend_from_slice(header.as_bytes());
186 raw.extend_from_slice(&content);
187
188 let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
189 encoder
190 .write_all(&raw)
191 .map_err(|e| format!("Compress error: {}", e))?;
192 let compressed = encoder
193 .finish()
194 .map_err(|e| format!("Compress finish error: {}", e))?;
195
196 fs::write(&obj_path, &compressed).map_err(|e| format!("Failed to write object: {}", e))?;
197
198 hash_map.insert(lit_hash.as_str().to_string(), git_hash);
199 Ok(())
200}
201
202fn serialize_git_tree(
204 tree: &crate::core::Tree,
205 hash_map: &HashMap<String, String>,
206) -> Result<Vec<u8>, crate::errors::LitError> {
207 let mut buf = Vec::new();
208 for entry in &tree.entries {
209 buf.extend_from_slice(entry.mode.as_bytes());
211 buf.push(b' ');
212 buf.extend_from_slice(entry.name.as_bytes());
213 buf.push(0);
214
215 let git_hex = if let Some(gh) = hash_map.get(entry.hash.as_str()) {
217 gh.clone()
218 } else {
219 entry.hash.as_str()[..40.min(entry.hash.len())].to_string()
221 };
222 let sha1_bytes = hex::decode(&git_hex).map_err(|e| format!("Invalid hash hex: {}", e))?;
223 buf.extend_from_slice(&sha1_bytes[..20.min(sha1_bytes.len())]);
224 while sha1_bytes.len() < 20 && buf.len() < (buf.len() + 20 - sha1_bytes.len()) {
226 buf.push(0);
227 }
228 }
229 Ok(buf)
230}
231
232fn serialize_git_commit(
234 commit: &crate::core::Commit,
235 hash_map: &HashMap<String, String>,
236) -> Result<Vec<u8>, crate::errors::LitError> {
237 let mut lines = Vec::new();
238
239 let tree_hash = hash_map
241 .get(commit.tree.as_str())
242 .cloned()
243 .unwrap_or_else(|| commit.tree.as_str()[..40.min(commit.tree.len())].to_string());
244 lines.push(format!("tree {}", tree_hash));
245
246 for parent in &commit.parents {
248 let parent_hash = hash_map
249 .get(parent.as_str())
250 .cloned()
251 .unwrap_or_else(|| parent.as_str()[..40.min(parent.len())].to_string());
252 lines.push(format!("parent {}", parent_hash));
253 }
254
255 lines.push(format!(
257 "author {} {} +0000",
258 commit.author, commit.timestamp
259 ));
260 lines.push(format!(
261 "committer {} {} +0000",
262 commit.committer, commit.timestamp
263 ));
264
265 let mut message = commit.message.clone();
267 if let Some(ref meta) = commit.metadata {
268 message.push_str(&format!("\n\nLit-Metadata: {}", meta));
269 }
270
271 lines.push(String::new()); lines.push(message);
273
274 Ok(lines.join("\n").into_bytes())
275}
276
277fn serialize_git_tag(
279 tag: &crate::core::Tag,
280 hash_map: &HashMap<String, String>,
281) -> Result<Vec<u8>, crate::errors::LitError> {
282 let target_hash = hash_map
283 .get(tag.target.as_str())
284 .cloned()
285 .unwrap_or_else(|| tag.target.as_str()[..40.min(tag.target.len())].to_string());
286
287 let mut lines = Vec::new();
288 lines.push(format!("object {}", target_hash));
289 lines.push(format!("type {}", tag.target_type));
290 lines.push(format!("tag {}", tag.tag_name));
291 lines.push(format!("tagger {} {} +0000", tag.tagger, tag.timestamp));
292 lines.push(String::new());
293 lines.push(tag.message.clone());
294
295 Ok(lines.join("\n").into_bytes())
296}