Skip to main content

lit/commands/
clone.rs

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    // Determine target directory
19    let dir_name = if let Some(d) = directory {
20        d
21    } else {
22        // Extract a sensible name from the URL
23        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    // Initialize the new repo
46    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    // Set up remote
58    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    // Get remote branches and download objects
73    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    // Create remote-tracking refs
81    for (branch_name, hash) in &remote_branches {
82        transport::update_remote_tracking_ref(&target, "origin", branch_name, hash)?;
83    }
84
85    // Determine default branch from remote HEAD
86    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    // Set up local branch matching default
97    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
125/// Checkout working tree from a commit
126fn 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}