1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum TransportKind {
16 Local,
17 Https,
18 Ssh,
19 Lit,
20}
21
22pub 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
35pub 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
59pub 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
81pub 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
122pub 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
139pub fn collect_known_hashes(repo_path: &Path) -> HashSet<String> {
141 let mut known = HashSet::new();
142
143 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 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
172pub 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
177pub 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
183pub 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
197pub 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
206pub 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
215pub 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#[derive(Debug, Clone)]
230pub struct RemoteRef {
231 pub kind: String,
232 pub name: String,
233 pub hash: String,
234}
235
236pub 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 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 let base = url.trim_end_matches('/').to_string();
264 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 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 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 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 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 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 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 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 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 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 pub fn check_fast_forward(
508 &self,
509 local_store: &ObjectStore,
510 new_hash: &ObjectHash,
511 old_hash: &ObjectHash,
512 ) -> Result<bool, String> {
513 is_fast_forward_push(local_store, new_hash, old_hash)
516 }
517}
518
519fn 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}