Skip to main content

lit/network/
transport.rs

1/// File-based transport for local and file:// repositories
2///
3/// Handles object and ref transfer between local repositories.
4/// Supports direct paths and file:// URLs. For HTTPS, SSH, and lit://
5/// URLs, delegates to the corresponding transport module.
6use crate::core::{Object, ObjectHash};
7use crate::storage::ObjectStore;
8use std::cell::RefCell;
9use std::collections::HashSet;
10use std::fs;
11use std::path::{Path, PathBuf};
12
13/// Transport protocol type
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum TransportKind {
16    Local,
17    Https,
18    Ssh,
19    Lit,
20}
21
22/// Detect the transport protocol from a URL
23pub fn detect_transport(url: &str) -> TransportKind {
24    if crate::network::https::is_https_url(url) {
25        TransportKind::Https
26    } else if crate::network::ssh::is_ssh_url(url) {
27        TransportKind::Ssh
28    } else if crate::network::lit_protocol::is_lit_url(url) {
29        TransportKind::Lit
30    } else {
31        TransportKind::Local
32    }
33}
34
35/// Validate and resolve a remote URL, returning a local path for file-based
36/// transports. For network transports (HTTPS, SSH, lit://), use
37/// `RemoteRepo::open()` instead — this function only resolves local paths.
38pub fn resolve_url(url: &str) -> Result<PathBuf, String> {
39    match detect_transport(url) {
40        TransportKind::Local => resolve_path(url),
41        TransportKind::Https => Err(format!(
42            "HTTPS URLs cannot be resolved to a local path. \
43             Use RemoteRepo::open() for '{}' instead.",
44            url
45        )),
46        TransportKind::Ssh => Err(format!(
47            "SSH URLs cannot be resolved to a local path. \
48             Use RemoteRepo::open() for '{}' instead.",
49            url
50        )),
51        TransportKind::Lit => Err(format!(
52            "lit:// URLs cannot be resolved to a local path. \
53             Use RemoteRepo::open() for '{}' instead.",
54            url
55        )),
56    }
57}
58
59/// Resolve a remote URL to a local path
60pub fn resolve_path(url: &str) -> Result<PathBuf, String> {
61    let path = if let Some(stripped) = url.strip_prefix("file://") {
62        PathBuf::from(stripped)
63    } else {
64        PathBuf::from(url)
65    };
66
67    let canonical = path
68        .canonicalize()
69        .map_err(|e| format!("Cannot resolve remote path '{}': {}", url, e))?;
70
71    if !canonical.join(".lit").exists() {
72        return Err(format!(
73            "'{}' does not appear to be a Lit repository",
74            canonical.display()
75        ));
76    }
77
78    Ok(canonical)
79}
80
81/// Find all objects reachable from a commit (commits, trees, blobs)
82pub fn walk_commit_graph(
83    store: &ObjectStore,
84    start: &ObjectHash,
85    known: &HashSet<String>,
86) -> Result<Vec<ObjectHash>, String> {
87    let mut to_visit = vec![start.clone()];
88    let mut visited: HashSet<String> = known.clone();
89    let mut result = Vec::new();
90
91    while let Some(hash) = to_visit.pop() {
92        if visited.contains(hash.as_str()) {
93            continue;
94        }
95        visited.insert(hash.as_str().to_string());
96
97        let obj = store.read(&hash)?;
98        result.push(hash.clone());
99
100        match &obj {
101            Object::Commit(commit) => {
102                to_visit.push(commit.tree.clone());
103                for parent in &commit.parents {
104                    to_visit.push(parent.clone());
105                }
106            }
107            Object::Tree(tree) => {
108                for entry in &tree.entries {
109                    to_visit.push(entry.hash.clone());
110                }
111            }
112            Object::Tag(tag) => {
113                to_visit.push(tag.target.clone());
114            }
115            Object::Blob(_) => {}
116        }
117    }
118
119    Ok(result)
120}
121
122/// Copy objects from source store to destination store, skipping those already present
123pub fn transfer_objects(
124    src_store: &ObjectStore,
125    dst_store: &ObjectStore,
126    objects: &[ObjectHash],
127) -> Result<usize, String> {
128    let mut count = 0;
129    for hash in objects {
130        if !dst_store.exists(hash) {
131            let obj = src_store.read(hash)?;
132            dst_store.write(&obj)?;
133            count += 1;
134        }
135    }
136    Ok(count)
137}
138
139/// List all ref hashes that a destination already has (for negotiation)
140pub fn collect_known_hashes(repo_path: &Path) -> HashSet<String> {
141    let mut known = HashSet::new();
142
143    // Collect from heads
144    if let Ok(refs) = crate::core::refs::list_refs(repo_path, "heads") {
145        for r in refs {
146            known.insert(r.hash);
147        }
148    }
149
150    // Collect from remotes
151    let remotes_dir = repo_path.join(".lit").join("refs").join("remotes");
152    if remotes_dir.exists() {
153        if let Ok(entries) = fs::read_dir(&remotes_dir) {
154            for entry in entries.flatten() {
155                if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
156                    let remote_name = entry.file_name().to_string_lossy().to_string();
157                    if let Ok(refs) =
158                        crate::core::refs::list_refs(repo_path, &format!("remotes/{}", remote_name))
159                    {
160                        for r in refs {
161                            known.insert(r.hash);
162                        }
163                    }
164                }
165            }
166        }
167    }
168
169    known
170}
171
172/// Read a remote's ref (branch tip)
173pub fn read_remote_ref(remote_path: &Path, branch: &str) -> Result<String, String> {
174    crate::core::refs::read_ref(remote_path, &format!("heads/{}", branch))
175}
176
177/// List all branches on a remote
178pub fn list_remote_branches(remote_path: &Path) -> Result<Vec<(String, String)>, String> {
179    let refs = crate::core::refs::list_refs(remote_path, "heads")?;
180    Ok(refs.into_iter().map(|r| (r.name, r.hash)).collect())
181}
182
183/// Update a remote-tracking ref
184pub fn update_remote_tracking_ref(
185    repo_path: &Path,
186    remote_name: &str,
187    branch: &str,
188    hash: &str,
189) -> Result<(), String> {
190    crate::core::refs::write_ref(
191        repo_path,
192        &format!("remotes/{}/{}", remote_name, branch),
193        hash,
194    )
195}
196
197/// Update a ref on the remote repository
198pub fn update_remote_branch_ref(
199    remote_path: &Path,
200    branch: &str,
201    hash: &str,
202) -> Result<(), String> {
203    crate::core::refs::write_ref(remote_path, &format!("heads/{}", branch), hash)
204}
205
206/// Check if a push would be a fast-forward
207pub fn is_fast_forward_push(
208    store: &ObjectStore,
209    new_hash: &ObjectHash,
210    old_hash: &ObjectHash,
211) -> Result<bool, String> {
212    crate::core::merge::is_ancestor(store, old_hash, new_hash)
213}
214
215/// Read HEAD from a remote repo (for clone)
216pub fn read_remote_head(remote_path: &Path) -> Result<String, String> {
217    let head_path = remote_path.join(".lit").join("HEAD");
218    if !head_path.exists() {
219        return Err("Remote HEAD not found".to_string());
220    }
221    let content =
222        fs::read_to_string(&head_path).map_err(|e| format!("Failed to read remote HEAD: {}", e))?;
223    Ok(content.trim().to_string())
224}
225
226// ── RemoteRepo — unified abstraction for file and HTTP remotes ──
227
228/// A reference on a remote (name + hash)
229#[derive(Debug, Clone)]
230pub struct RemoteRef {
231    pub kind: String,
232    pub name: String,
233    pub hash: String,
234}
235
236/// A remote repository that can be accessed via filesystem, HTTP, SSH, or lit://
237pub enum RemoteRepo {
238    File {
239        path: PathBuf,
240    },
241    Http {
242        base_url: String,
243        token: Option<String>,
244    },
245    Ssh {
246        pipe: RefCell<crate::network::ssh::SshPipe>,
247    },
248    Lit {
249        conn: RefCell<crate::network::lit_protocol::LitConnection>,
250    },
251}
252
253impl RemoteRepo {
254    /// Create a RemoteRepo from a URL, auto-detecting transport type
255    pub fn open(url: &str) -> Result<Self, String> {
256        match detect_transport(url) {
257            TransportKind::Local => {
258                let path = resolve_path(url)?;
259                Ok(RemoteRepo::File { path })
260            }
261            TransportKind::Https => {
262                // Strip trailing slash
263                let base = url.trim_end_matches('/').to_string();
264                // Check for token in LIT_TOKEN env var
265                let token = std::env::var("LIT_TOKEN").ok();
266                Ok(RemoteRepo::Http {
267                    base_url: base,
268                    token,
269                })
270            }
271            TransportKind::Ssh => {
272                let parsed = crate::network::ssh::parse_ssh_url(url)?;
273                let pipe = crate::network::ssh::SshPipe::open(&parsed)?;
274                Ok(RemoteRepo::Ssh {
275                    pipe: RefCell::new(pipe),
276                })
277            }
278            TransportKind::Lit => {
279                let parsed = crate::network::lit_protocol::parse_lit_url(url)?;
280                let conn = crate::network::lit_protocol::LitConnection::open(&parsed)?;
281                Ok(RemoteRepo::Lit {
282                    conn: RefCell::new(conn),
283                })
284            }
285        }
286    }
287
288    /// List refs (branches and/or tags) on the remote
289    pub fn list_refs(&self, kind: &str) -> Result<Vec<RemoteRef>, String> {
290        match self {
291            RemoteRepo::File { path } => {
292                let mut result = Vec::new();
293                if kind == "all" || kind == "heads" {
294                    if let Ok(refs) = crate::core::refs::list_refs(path, "heads") {
295                        for r in refs {
296                            result.push(RemoteRef {
297                                kind: "heads".into(),
298                                name: r.name,
299                                hash: r.hash,
300                            });
301                        }
302                    }
303                }
304                if kind == "all" || kind == "tags" {
305                    if let Ok(refs) = crate::core::refs::list_refs(path, "tags") {
306                        for r in refs {
307                            result.push(RemoteRef {
308                                kind: "tags".into(),
309                                name: r.name,
310                                hash: r.hash,
311                            });
312                        }
313                    }
314                }
315                Ok(result)
316            }
317            RemoteRepo::Http { base_url, token } => {
318                crate::network::https::list_refs_http(base_url, kind, token.as_deref())
319            }
320            RemoteRepo::Ssh { pipe } => {
321                crate::network::ssh::list_refs_ssh(&mut pipe.borrow_mut(), kind)
322            }
323            RemoteRepo::Lit { conn } => {
324                crate::network::lit_protocol::list_refs_lit(&mut conn.borrow_mut(), kind)
325            }
326        }
327    }
328
329    /// List branches on the remote (convenience method)
330    pub fn list_branches(&self) -> Result<Vec<(String, String)>, String> {
331        let refs = self.list_refs("heads")?;
332        Ok(refs.into_iter().map(|r| (r.name, r.hash)).collect())
333    }
334
335    /// Read a branch ref on the remote
336    pub fn read_branch_ref(&self, branch: &str) -> Result<String, String> {
337        match self {
338            RemoteRepo::File { path } => read_remote_ref(path, branch),
339            RemoteRepo::Http { base_url, token } => {
340                crate::network::https::read_ref_http(base_url, branch, token.as_deref())
341            }
342            RemoteRepo::Ssh { pipe } => {
343                crate::network::ssh::read_ref_ssh(&mut pipe.borrow_mut(), branch)
344            }
345            RemoteRepo::Lit { conn } => {
346                crate::network::lit_protocol::read_ref_lit(&mut conn.borrow_mut(), branch)
347            }
348        }
349    }
350
351    /// Read HEAD from the remote
352    pub fn read_head(&self) -> Result<String, String> {
353        match self {
354            RemoteRepo::File { path } => read_remote_head(path),
355            RemoteRepo::Http { base_url, token } => {
356                crate::network::https::read_head_http(base_url, token.as_deref())
357            }
358            RemoteRepo::Ssh { pipe } => crate::network::ssh::read_head_ssh(&mut pipe.borrow_mut()),
359            RemoteRepo::Lit { conn } => {
360                crate::network::lit_protocol::read_head_lit(&mut conn.borrow_mut())
361            }
362        }
363    }
364
365    /// Update a branch ref on the remote
366    pub fn update_branch_ref(&self, branch: &str, hash: &str, force: bool) -> Result<(), String> {
367        match self {
368            RemoteRepo::File { path } => update_remote_branch_ref(path, branch, hash),
369            RemoteRepo::Http { base_url, token } => crate::network::https::update_ref_http(
370                base_url,
371                branch,
372                hash,
373                force,
374                token.as_deref(),
375            ),
376            RemoteRepo::Ssh { pipe } => {
377                crate::network::ssh::update_ref_ssh(&mut pipe.borrow_mut(), branch, hash, force)
378            }
379            RemoteRepo::Lit { conn } => crate::network::lit_protocol::update_ref_lit(
380                &mut conn.borrow_mut(),
381                branch,
382                hash,
383                force,
384            ),
385        }
386    }
387
388    /// Negotiate which objects need to be transferred (returns hashes of needed objects)
389    pub fn negotiate_download(
390        &self,
391        local_store: &ObjectStore,
392        wants: &[String],
393    ) -> Result<Vec<ObjectHash>, String> {
394        match self {
395            RemoteRepo::File { path } => {
396                let remote_store = ObjectStore::new(path);
397                let known = collect_known_hashes_from_store(local_store);
398                let mut all = Vec::new();
399                for want in wants {
400                    let hash = ObjectHash::from_hex(want.clone());
401                    let needed = walk_commit_graph(&remote_store, &hash, &known)?;
402                    for h in needed {
403                        if !all.iter().any(|x: &ObjectHash| x.as_str() == h.as_str()) {
404                            all.push(h);
405                        }
406                    }
407                }
408                Ok(all)
409            }
410            RemoteRepo::Http { base_url, token } => {
411                let known = collect_known_hashes_from_store(local_store);
412                let haves: Vec<String> = known.into_iter().collect();
413                crate::network::https::negotiate_http(base_url, wants, &haves, token.as_deref())
414            }
415            RemoteRepo::Ssh { pipe } => {
416                let known = collect_known_hashes_from_store(local_store);
417                let haves: Vec<String> = known.into_iter().collect();
418                crate::network::ssh::negotiate_ssh(&mut pipe.borrow_mut(), wants, &haves)
419            }
420            RemoteRepo::Lit { conn } => {
421                let known = collect_known_hashes_from_store(local_store);
422                let haves: Vec<String> = known.into_iter().collect();
423                crate::network::lit_protocol::negotiate_lit(&mut conn.borrow_mut(), wants, &haves)
424            }
425        }
426    }
427
428    /// Download objects from remote into local store
429    pub fn download_objects(
430        &self,
431        local_store: &ObjectStore,
432        hashes: &[ObjectHash],
433    ) -> Result<usize, String> {
434        match self {
435            RemoteRepo::File { path } => {
436                let remote_store = ObjectStore::new(path);
437                transfer_objects(&remote_store, local_store, hashes)
438            }
439            RemoteRepo::Http { base_url, token } => crate::network::https::download_objects_http(
440                base_url,
441                local_store,
442                hashes,
443                token.as_deref(),
444            ),
445            RemoteRepo::Ssh { pipe } => crate::network::ssh::download_objects_ssh(
446                &mut pipe.borrow_mut(),
447                local_store,
448                hashes,
449            ),
450            RemoteRepo::Lit { conn } => crate::network::lit_protocol::download_objects_lit(
451                &mut conn.borrow_mut(),
452                local_store,
453                hashes,
454            ),
455        }
456    }
457
458    /// Upload objects from local store to remote
459    pub fn upload_objects(
460        &self,
461        local_store: &ObjectStore,
462        hashes: &[ObjectHash],
463    ) -> Result<usize, String> {
464        match self {
465            RemoteRepo::File { path } => {
466                let remote_store = ObjectStore::new(path);
467                transfer_objects(local_store, &remote_store, hashes)
468            }
469            RemoteRepo::Http { base_url, token } => crate::network::https::upload_objects_http(
470                base_url,
471                local_store,
472                hashes,
473                token.as_deref(),
474            ),
475            RemoteRepo::Ssh { pipe } => {
476                crate::network::ssh::upload_objects_ssh(&mut pipe.borrow_mut(), local_store, hashes)
477            }
478            RemoteRepo::Lit { conn } => crate::network::lit_protocol::upload_objects_lit(
479                &mut conn.borrow_mut(),
480                local_store,
481                hashes,
482            ),
483        }
484    }
485
486    /// Negotiate which objects need to be uploaded (walk local graph, exclude known remote objects)
487    pub fn negotiate_upload(
488        &self,
489        local_store: &ObjectStore,
490        wants: &[String],
491        remote_has: &HashSet<String>,
492    ) -> Result<Vec<ObjectHash>, String> {
493        let mut all = Vec::new();
494        for want in wants {
495            let hash = ObjectHash::from_hex(want.clone());
496            let needed = walk_commit_graph(local_store, &hash, remote_has)?;
497            for h in needed {
498                if !all.iter().any(|x: &ObjectHash| x.as_str() == h.as_str()) {
499                    all.push(h);
500                }
501            }
502        }
503        Ok(all)
504    }
505
506    /// Check if a push would be fast-forward
507    pub fn check_fast_forward(
508        &self,
509        local_store: &ObjectStore,
510        new_hash: &ObjectHash,
511        old_hash: &ObjectHash,
512    ) -> Result<bool, String> {
513        // For both file and HTTP, we check locally since we should have
514        // downloaded the remote's objects first (or they share an ancestor)
515        is_fast_forward_push(local_store, new_hash, old_hash)
516    }
517}
518
519/// Collect known hashes from a local ObjectStore (for negotiation)
520fn collect_known_hashes_from_store(store: &ObjectStore) -> HashSet<String> {
521    store
522        .list()
523        .unwrap_or_default()
524        .into_iter()
525        .map(|h| h.as_str().to_string())
526        .collect()
527}