//! Commands that talk to remotes, and the reference server. use super::format::plural; use super::open; use super::ui; use anyhow::{Context, Result}; use oxy_core::repo::Repo; use std::path::PathBuf; pub fn clone(url: &str, dest: Option, max_blob: Option<&str>, sparse: &[String], depth: Option) -> Result { let dest = dest.unwrap_or_else(|| { let base = url.trim_end_matches('/').rsplit('/').next().unwrap_or("repo"); PathBuf::from(base) }); let max_blob = max_blob.map(oxy_core::repo::parse_size).transpose()?; let sp = ui::spinner(&format!("Cloning {url}…")); let repo = match depth { Some(d) => Repo::clone_shallow(url, &dest, d, max_blob, sparse)?, None => Repo::clone_with(url, &dest, max_blob, sparse)?, }; let heads = repo.bookmark_heads()?; sp.done(&format!("{} into {} {}", ui::done("Cloned"), ui::path(repo.root().display()), ui::dim(format!("({url}, {} with {})", ui::bookmark(&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!("{} remote {} {} {url}", ui::done("Added"), ui::bookmark(name), ui::dim(ui::glyph("→", "->"))); Ok(0) } pub fn remote_remove(name: &str) -> Result { let mut repo = open()?; repo.config.set(&format!("remote.{name}.url"), "")?; repo.save_config()?; println!("{} remote {}", ui::done("Removed"), ui::bookmark(name)); Ok(0) } pub fn remote_list() -> Result { let repo = open()?; let view = repo.view()?; if repo.config.remotes.is_empty() { println!("{}", ui::dim("No remotes; add one with oxy remote add .")); } for (name, url) in &repo.config.remotes { println!("{} {}{}{}", ui::ok(ui::glyph("⇄", "<>")), ui::bookmark(name), ui::sep(), ui::dim(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!(" {} {}{}{}", ui::dim(ui::glyph("↳", "->")), ui::id(format!("{name}/{bm}")), ui::sep(), ui::dim(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 sp = ui::spinner(&format!("Fetching from {remote}…")); let report = repo.fetch(&remote)?; sp.clear(); if report.refs.is_empty() { println!("{} {}", ui::done("Fetched"), ui::dim(format!("nothing new from {remote}"))); } for (name, heads, n) in &report.refs { let what = if *n == 0 { ui::dim("up to date") } else { ui::ok(plural(*n, "new change")) }; println!("{} {}{}{}{}{what}", ui::done("Fetched"), ui::id(format!("{remote}/{name}")), ui::sep(), ui::dim(plural(heads.len(), "head")), ui::sep()); } if report.new_tags > 0 { println!(" {} {}", ui::note(), ui::dim(format!("{} adopted", plural(report.new_tags, "tag")))); } if report.unverified { eprintln!("{} {remote} has no trust policy; nothing fetched from it was verified", ui::caution()); } Ok(0) } pub fn pull(remote: Option<&str>) -> Result { let repo = open()?; let remote = remote_name(remote); let sp = ui::spinner(&format!("Pulling from {remote}…")); let r = repo.pull(&remote)?; if !r.changed { sp.done(&format!("{} {}", ui::ok(ui::glyph("✔", "*")), ui::dim("Already up to date."))); return Ok(0); } sp.done(&format!( "{} {} now has {}{}", ui::done("Pulled:"), ui::bookmark(&repo.config.bookmark), plural(r.heads.len(), "head"), ui::dim(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!(" {} {}", ui::caution(), ui::warn("the result has conflicts; see oxy status")); } Ok(0) } pub fn push(remote: Option<&str>, force: bool, bookmark: Option<&str>) -> Result { let repo = open()?; let remote = remote_name(remote); let sp = ui::spinner(&format!("Pushing to {remote}…")); let results = match bookmark { Some(b) => repo.push_bookmark(&remote, b, force)?, None => repo.push(&remote, force)?, }; sp.clear(); let mut code = 0; for r in results { if r.ok { println!("{} {}{}{}", ui::done("Pushed"), ui::id(format!("{remote}/{}", r.name)), ui::sep(), ui::dim(if r.reason.is_empty() { "updated" } else { r.reason.as_str() })); } else { println!("{} {} {}{}{}", ui::err(ui::glyph("✖", "x")), ui::err("Rejected"), ui::id(format!("{remote}/{}", r.name)), ui::sep(), 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, branch: Option<&str>) -> Result { let repo = open()?; let sp = ui::spinner("Exporting to Git…"); let r = match branch { Some(b) => repo.git_export_as(git_dir, b)?, None => repo.git_export(git_dir)?, }; sp.clear(); println!( "{} {} to {} as branch {} {}", ui::done("Exported"), plural(r.exported, "new state"), ui::path(git_dir.display()), ui::bookmark(&r.branch), ui::dim(format!("(currently {}; {} already exported)", plural(r.heads, "head"), r.skipped)) ); Ok(0) } pub fn git_fetch(url: &str, branch: &str) -> Result { let repo = open()?; let sp = ui::spinner(&format!("Fetching {url} {branch}…")); let r = repo.git_fetch(url, branch)?; sp.clear(); if r.commits == 0 { println!("{} {}", ui::done("Fetched"), ui::dim(format!("nothing new from {url} {branch}"))); } else { println!( "{} {} ({} merges) from {} {}{}{} now has {}", ui::done("Fetched"), plural(r.commits, "new commit"), r.merges, ui::path(url), ui::bookmark(branch), ui::sep(), ui::bookmark(&repo.config.bookmark), plural(r.heads, "head") ); if !repo.snapshot("oxy git fetch")?.changed { println!(" {} {}", ui::note(), ui::dim("working copy updated")); } else { println!(" {} {}", ui::note(), ui::dim("run oxy checkout to update the working copy")); } } if r.submodules > 0 { println!(" {} {}", ui::note(), ui::dim(format!("{} submodule {} imported as subrepos with Git pins", r.submodules, if r.submodules == 1 { "entry" } else { "entries" }))); } Ok(0) } pub fn git_push(url: &str, branch: &str) -> Result { let repo = open()?; let sp = ui::spinner(&format!("Pushing to {url} {branch}…")); let r = repo.git_push(url, branch)?; sp.clear(); println!( "{} {} to {} {} {}", ui::done("Pushed"), plural(r.exported, "new commit"), ui::path(url), ui::bookmark(&r.branch), ui::dim(format!("({} already there)", 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 sp = ui::spinner(&format!("Importing {} {branch}…", git_dir.display())); let r = repo.import_git(git_dir, branch)?; sp.clear(); println!( "{} {} ({} merges) from {} {} into {} {}", ui::done("Imported"), plural(r.commits, "commit"), r.merges, ui::path(git_dir.display()), ui::bookmark(branch), ui::path(repo.root().display()), ui::dim(format!("in {:.1?}; {} now has {}", started.elapsed(), repo.config.bookmark, plural(r.heads, "head"))) ); if r.submodules > 0 { println!(" {} {}", ui::note(), ui::dim(format!("{} submodule {} imported as subrepos with Git pins", r.submodules, if r.submodules == 1 { "entry" } else { "entries" }))); } Ok(0) } pub fn submodule_add(url: &str, path: &str, bookmark: Option<&str>) -> Result { let repo = open()?; let sp = ui::spinner(&format!("Cloning {url} into {path}…")); let pin = repo.submodule_add(url, path, bookmark)?; sp.done(&format!("{} subrepo {} {} {}", ui::done("Added"), ui::path(path), ui::dim(ui::glyph("→", "->")), ui::dim(pin.summary()))); println!(" {} {}", ui::note(), ui::dim("recorded in .oxymodules; commit to pin it")); Ok(0) } pub fn submodule_update(init: bool, paths: &[String]) -> Result { let repo = open()?; let sp = ui::spinner("Updating subrepos…"); let results = repo.submodule_update(init, paths)?; sp.clear(); if results.is_empty() { println!("{}", ui::dim("No subrepos.")); } let mut code = 0; for (path, r) in results { use oxy_core::repo::SubrepoSync::*; let line = match r { Updated => ui::done("Updated"), UpToDate => ui::ok(format!("{} up to date", ui::glyph("✔", "*"))), Missing => { code = 1; ui::warn(format!("{} missing (pass --init to clone)", ui::glyph("⚠", "!"))) } Dirty => { code = 1; ui::warn(format!("{} has uncommitted changes; left as is", ui::glyph("⚠", "!"))) } Failed(e) => { code = 1; ui::err(format!("{} {e}", ui::glyph("✖", "x"))) } }; println!("{line} {}", ui::path(path)); } Ok(code) } pub fn submodule_status() -> Result { let repo = open()?; let subs = repo.subrepos()?; if subs.is_empty() { println!("{}", ui::dim("No subrepos.")); } for s in subs { let state = match (&s.actual, s.dirty) { (None, _) => ui::warn(format!("{} missing", ui::glyph("⚠", "!"))), (Some(a), dirty) if *a == s.pin => { if dirty { ui::warn(format!("{} at pin, uncommitted changes", ui::glyph("✎", "~"))) } else { ui::ok(format!("{} at pin", ui::glyph("✔", "*"))) } } (Some(a), _) => ui::warn(format!("{} at {}", ui::glyph("↕", "<>"), a.summary())), }; println!("{} {}{}{}{}{state}", ui::ok(ui::glyph("⧉", "#")), ui::path(&s.path), ui::sep(), ui::dim(s.pin.summary()), ui::sep()); if let Some(u) = s.url { println!(" {} {}", ui::dim(ui::glyph("↳", "->")), ui::dim(u)); } } Ok(0) } pub fn submodule_remove(path: &str) -> Result { let repo = open()?; repo.submodule_remove(path)?; println!("{} subrepo {} {}", ui::done("Removed"), ui::path(path), ui::dim("(directory left in place; commit to record the removal)")); Ok(0) } pub fn deepen(remote: &str, depth: u32) -> Result { let repo = open()?; if !repo.is_shallow() { println!("{}", ui::dim("This repository is not shallow; nothing to deepen.")); return Ok(0); } let sp = ui::spinner(&format!("Fetching {depth} more levels from {remote}…")); let n = repo.deepen(remote, depth)?; sp.done(&format!("{} {}; {}", ui::ok("Deepened by"), plural(n, "change"), if repo.is_shallow() { "still shallow".to_string() } else { ui::ok("history is now complete") })); Ok(0) }