1use crate::core::ObjectHash;
2use crate::network::transport::{self, RemoteRepo};
3use crate::network::AirgapValidator;
4use crate::response::CloneResponse;
5use crate::storage::ObjectStore;
6use std::fs;
7use std::path::Path;
8
9pub fn execute(
10 url: String,
11 directory: Option<String>,
12) -> Result<CloneResponse, crate::errors::LitError> {
13 let validator = AirgapValidator::new()?;
14 validator.validate_transport(&url)?;
15
16 let remote_repo = RemoteRepo::open(&url)?;
17
18 let dir_name = if let Some(d) = directory {
20 d
21 } else {
22 let name = url
24 .trim_end_matches('/')
25 .rsplit('/')
26 .next()
27 .unwrap_or("repo")
28 .trim_end_matches(".git")
29 .trim_end_matches(".lit");
30 if name.is_empty() {
31 "repo".to_string()
32 } else {
33 name.to_string()
34 }
35 };
36
37 let target = std::env::current_dir()
38 .map_err(|e| format!("Failed to get current directory: {}", e))?
39 .join(&dir_name);
40
41 if target.exists() {
42 return Err(format!("Directory '{}' already exists", dir_name).into());
43 }
44
45 fs::create_dir_all(&target).map_err(|e| format!("Failed to create directory: {}", e))?;
47
48 let original_dir =
49 std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?;
50 std::env::set_current_dir(&target).map_err(|e| format!("Failed to change directory: {}", e))?;
51
52 let init_result = crate::commands::init::execute(false, None);
53 std::env::set_current_dir(&original_dir)
54 .map_err(|e| format!("Failed to restore directory: {}", e))?;
55 init_result?;
56
57 let remotes_config = serde_json::json!({
59 "remotes": {
60 "origin": {
61 "url": url
62 }
63 }
64 });
65 fs::write(
66 target.join(".lit").join("remotes"),
67 serde_json::to_string_pretty(&remotes_config)
68 .map_err(|e| format!("Failed to serialize remotes: {}", e))?,
69 )
70 .map_err(|e| format!("Failed to write remotes config: {}", e))?;
71
72 let local_store = ObjectStore::new(&target);
74 let remote_branches = remote_repo.list_branches()?;
75
76 let wants: Vec<String> = remote_branches.iter().map(|(_, h)| h.clone()).collect();
77 let needed = remote_repo.negotiate_download(&local_store, &wants)?;
78 let total_transferred = remote_repo.download_objects(&local_store, &needed)?;
79
80 for (branch_name, hash) in &remote_branches {
82 transport::update_remote_tracking_ref(&target, "origin", branch_name, hash)?;
83 }
84
85 let remote_head = remote_repo.read_head()?;
87 let default_branch = if remote_head.starts_with("ref: refs/heads/") {
88 remote_head
89 .strip_prefix("ref: refs/heads/")
90 .unwrap()
91 .to_string()
92 } else {
93 "main".to_string()
94 };
95
96 if let Some((_, hash)) = remote_branches
98 .iter()
99 .find(|(name, _)| name == &default_branch)
100 {
101 crate::core::refs::write_ref(&target, &format!("heads/{}", default_branch), hash)?;
102 fs::write(
103 target.join(".lit").join("HEAD"),
104 format!("ref: refs/heads/{}\n", default_branch),
105 )
106 .map_err(|e| format!("Failed to write HEAD: {}", e))?;
107
108 checkout_tree(&target, &ObjectHash::from_hex(hash.clone()), &local_store)?;
109 }
110
111 Ok(CloneResponse {
112 url: url.clone(),
113 directory: dir_name.clone(),
114 branches_cloned: remote_branches.iter().map(|(n, _)| n.clone()).collect(),
115 objects_transferred: total_transferred,
116 message: format!(
117 "Cloned into '{}'\n {} objects, {} branches",
118 dir_name,
119 total_transferred,
120 remote_branches.len()
121 ),
122 })
123}
124
125fn checkout_tree(
127 repo_path: &Path,
128 commit_hash: &ObjectHash,
129 store: &ObjectStore,
130) -> Result<(), crate::errors::LitError> {
131 use crate::core::Object;
132
133 let commit_obj = store.read(commit_hash)?;
134 let commit = match commit_obj {
135 Object::Commit(c) => c,
136 _ => return Err("Expected commit object".into()),
137 };
138
139 let tree_obj = store.read(&commit.tree)?;
140 let tree = match tree_obj {
141 Object::Tree(t) => t,
142 _ => return Err("Expected tree object".into()),
143 };
144
145 checkout_tree_recursive(repo_path, &tree, store, repo_path)
146}
147
148fn checkout_tree_recursive(
149 base_path: &Path,
150 tree: &crate::core::Tree,
151 store: &ObjectStore,
152 _repo_path: &Path,
153) -> Result<(), crate::errors::LitError> {
154 use crate::core::Object;
155
156 for entry in &tree.entries {
157 let entry_path = base_path.join(&entry.name);
158
159 let obj = store.read(&entry.hash)?;
160 match obj {
161 Object::Blob(blob) => {
162 if let Some(parent) = entry_path.parent() {
163 fs::create_dir_all(parent)
164 .map_err(|e| format!("Failed to create directory: {}", e))?;
165 }
166 fs::write(&entry_path, &blob.content)
167 .map_err(|e| format!("Failed to write file '{}': {}", entry.name, e))?;
168 }
169 Object::Tree(subtree) => {
170 fs::create_dir_all(&entry_path)
171 .map_err(|e| format!("Failed to create directory: {}", e))?;
172 checkout_tree_recursive(&entry_path, &subtree, store, _repo_path)?;
173 }
174 _ => {}
175 }
176 }
177 Ok(())
178}