//! Commands that talk to remotes, and the reference server. use super::format::plural; use super::open; use anyhow::{Context, Result}; use oxy_core::repo::Repo; use std::path::PathBuf; pub fn clone(url: &str, dest: Option) -> Result { let dest = dest.unwrap_or_else(|| { let base = url.trim_end_matches('/').rsplit('/').next().unwrap_or("repo"); PathBuf::from(base) }); let repo = Repo::clone(url, &dest)?; let heads = repo.bookmark_heads()?; println!("Cloned into {} ({url} on {}: {})", repo.root().display(), repo.config.bookmark, plural(heads.len(), "head")); Ok(0) } pub fn remote_add(name: &str, url: &str) -> Result { let mut repo = open()?; repo.config.set(&format!("remote.{name}.url"), url)?; repo.save_config()?; println!("Added remote {name} = {url}"); Ok(0) } pub fn remote_remove(name: &str) -> Result { let mut repo = open()?; repo.config.set(&format!("remote.{name}.url"), "")?; repo.save_config()?; println!("Removed remote {name}"); Ok(0) } pub fn remote_list() -> Result { let repo = open()?; let view = repo.view()?; for (name, url) in &repo.config.remotes { println!("{name}\t{url}"); if let Some(bms) = view.remotes.get(name) { for (bm, st) in bms { let heads = repo.store().get_state(st).map(|s| s.heads.len()).unwrap_or(0); println!(" {bm}: {}", plural(heads, "head")); } } } Ok(0) } fn remote_name(remote: Option<&str>) -> String { remote.unwrap_or("origin").to_string() } pub fn fetch(remote: Option<&str>) -> Result { let repo = open()?; let remote = remote_name(remote); let report = repo.fetch(&remote)?; for (name, heads, n) in &report.refs { println!("{remote}/{name}: {}, {}", plural(heads.len(), "head"), plural(*n, "new change")); } if report.unverified { eprintln!("warning: {remote} has no trust policy; nothing fetched from it was verified"); } Ok(0) } pub fn pull(remote: Option<&str>) -> Result { let repo = open()?; let remote = remote_name(remote); let r = repo.pull(&remote)?; if !r.changed { println!("Already up to date."); return Ok(0); } println!( "{} now has {}{}", repo.config.bookmark, plural(r.heads.len(), "head"), if r.checked_out { "; working copy updated" } else { "; run `oxy checkout` to update the working copy" } ); let snap = repo.snapshot("oxy pull")?; if snap.base_snapshot.is_conflicted() { println!("The result has conflicts; see oxy status."); } Ok(0) } pub fn push(remote: Option<&str>, force: bool) -> Result { let repo = open()?; let remote = remote_name(remote); let mut code = 0; for r in repo.push(&remote, force)? { if r.ok { println!("{remote}/{}: {}", r.name, if r.reason.is_empty() { "updated" } else { r.reason.as_str() }); } else { println!("{remote}/{}: rejected: {}", r.name, r.reason); code = 1; } } Ok(code) } pub fn serve(root: PathBuf, bind: &str, tokens: Vec, tls: Option<(PathBuf, PathBuf)>) -> Result { let listener = std::net::TcpListener::bind(bind).with_context(|| format!("binding {bind}"))?; let root = root.canonicalize()?; let tls = tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key }); println!( "Serving repositories under {} on {}://{bind}//oxy/v1/ ({})", root.display(), if tls.is_some() { "https" } else { "http" }, if tokens.is_empty() { "open: no push tokens configured" } else { "push requires a token" } ); if tls.is_none() && !bind.starts_with("127.") && !bind.starts_with("localhost") && !bind.starts_with("[::1]") { eprintln!("warning: serving plain HTTP on a non-loopback address; put a TLS terminator in front or pass --tls-cert/--tls-key"); } oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens, tls })?; Ok(0) } pub fn git_export(git_dir: &std::path::Path) -> Result { let repo = open()?; let r = repo.git_export(git_dir)?; println!( "Exported {} to {} (branch {}: {}{}); {} already exported.", plural(r.exported, "change"), git_dir.display(), r.branch, plural(r.heads, "head"), if r.heads > 1 { ", joined by a synthetic merge commit" } else { "" }, r.skipped ); Ok(0) } pub fn git_import(git_dir: &std::path::Path, dest: &std::path::Path, branch: &str) -> Result { let repo = Repo::init(dest)?; let started = std::time::Instant::now(); let r = repo.import_git(git_dir, branch)?; println!( "Imported {} ({} merges) from {} {branch} into {} in {:.1?}: {} now has {}.{}", plural(r.commits, "commit"), r.merges, git_dir.display(), repo.root().display(), started.elapsed(), repo.config.bookmark, plural(r.heads, "head"), if r.skipped_submodules > 0 { format!(" Skipped {} submodule entries.", r.skipped_submodules) } else { String::new() } ); Ok(0) }