| 1 |
|
| 2 |
|
| 3 |
use super::format::plural; |
| 4 |
use super::open; |
| 5 |
use super::ui; |
| 6 |
use anyhow::{Context, Result}; |
| 7 |
use oxy_core::repo::Repo; |
| 8 |
use std::path::PathBuf; |
| 9 |
|
| 10 |
pub fn clone(url: &str, dest: Option<PathBuf>, max_blob: Option<&str>, sparse: &[String], depth: Option<u32>) -> Result<i32> { |
| 11 |
let dest = dest.unwrap_or_else(|| { |
| 12 |
let base = url.trim_end_matches('/').rsplit('/').next().unwrap_or("repo"); |
| 13 |
PathBuf::from(base) |
| 14 |
}); |
| 15 |
let max_blob = max_blob.map(oxy_core::repo::parse_size).transpose()?; |
| 16 |
let sp = ui::spinner(&format!("Cloning {url}…")); |
| 17 |
let repo = match depth { |
| 18 |
Some(d) => Repo::clone_shallow(url, &dest, d, max_blob, sparse)?, |
| 19 |
None => Repo::clone_with(url, &dest, max_blob, sparse)?, |
| 20 |
}; |
| 21 |
let heads = repo.bookmark_heads()?; |
| 22 |
sp.done(&format!("{} into {} ({} on {}: {})", ui::ok("Cloned"), ui::path(repo.root().display()), url, ui::id(&repo.config.bookmark), plural(heads.len(), "head"))); |
| 23 |
Ok(0) |
| 24 |
} |
| 25 |
|
| 26 |
pub fn remote_add(name: &str, url: &str) -> Result<i32> { |
| 27 |
let mut repo = open()?; |
| 28 |
repo.config.set(&format!("remote.{name}.url"), url)?; |
| 29 |
repo.save_config()?; |
| 30 |
println!("Added remote {name} = {url}"); |
| 31 |
Ok(0) |
| 32 |
} |
| 33 |
|
| 34 |
pub fn remote_remove(name: &str) -> Result<i32> { |
| 35 |
let mut repo = open()?; |
| 36 |
repo.config.set(&format!("remote.{name}.url"), "")?; |
| 37 |
repo.save_config()?; |
| 38 |
println!("Removed remote {name}"); |
| 39 |
Ok(0) |
| 40 |
} |
| 41 |
|
| 42 |
pub fn remote_list() -> Result<i32> { |
| 43 |
let repo = open()?; |
| 44 |
let view = repo.view()?; |
| 45 |
for (name, url) in &repo.config.remotes { |
| 46 |
println!("{name}\t{url}"); |
| 47 |
if let Some(bms) = view.remotes.get(name) { |
| 48 |
for (bm, st) in bms { |
| 49 |
let heads = repo.store().get_state(st).map(|s| s.heads.len()).unwrap_or(0); |
| 50 |
println!(" {bm}: {}", plural(heads, "head")); |
| 51 |
} |
| 52 |
} |
| 53 |
} |
| 54 |
Ok(0) |
| 55 |
} |
| 56 |
|
| 57 |
fn remote_name(remote: Option<&str>) -> String { |
| 58 |
remote.unwrap_or("origin").to_string() |
| 59 |
} |
| 60 |
|
| 61 |
pub fn fetch(remote: Option<&str>) -> Result<i32> { |
| 62 |
let repo = open()?; |
| 63 |
let remote = remote_name(remote); |
| 64 |
let sp = ui::spinner(&format!("Fetching from {remote}…")); |
| 65 |
let report = repo.fetch(&remote)?; |
| 66 |
sp.clear(); |
| 67 |
for (name, heads, n) in &report.refs { |
| 68 |
println!("{} {}, {}", ui::id(format!("{remote}/{name}")), plural(heads.len(), "head"), plural(*n, "new change")); |
| 69 |
} |
| 70 |
if report.unverified { |
| 71 |
eprintln!("{} {remote} has no trust policy; nothing fetched from it was verified", ui::warn("warning:")); |
| 72 |
} |
| 73 |
Ok(0) |
| 74 |
} |
| 75 |
|
| 76 |
pub fn pull(remote: Option<&str>) -> Result<i32> { |
| 77 |
let repo = open()?; |
| 78 |
let remote = remote_name(remote); |
| 79 |
let sp = ui::spinner(&format!("Pulling from {remote}…")); |
| 80 |
let r = repo.pull(&remote)?; |
| 81 |
if !r.changed { |
| 82 |
sp.done(&ui::dim("Already up to date.")); |
| 83 |
return Ok(0); |
| 84 |
} |
| 85 |
sp.done(&format!( |
| 86 |
"{} {} now has {}{}", |
| 87 |
ui::ok("Pulled:"), |
| 88 |
ui::id(&repo.config.bookmark), |
| 89 |
plural(r.heads.len(), "head"), |
| 90 |
if r.checked_out { "; working copy updated" } else { "; run `oxy checkout` to update the working copy" } |
| 91 |
)); |
| 92 |
let snap = repo.snapshot("oxy pull")?; |
| 93 |
if snap.base_snapshot.is_conflicted() { |
| 94 |
println!(" {}", ui::warn("the result has conflicts; see oxy status")); |
| 95 |
} |
| 96 |
Ok(0) |
| 97 |
} |
| 98 |
|
| 99 |
pub fn push(remote: Option<&str>, force: bool) -> Result<i32> { |
| 100 |
let repo = open()?; |
| 101 |
let remote = remote_name(remote); |
| 102 |
let sp = ui::spinner(&format!("Pushing to {remote}…")); |
| 103 |
let results = repo.push(&remote, force)?; |
| 104 |
sp.clear(); |
| 105 |
let mut code = 0; |
| 106 |
for r in results { |
| 107 |
if r.ok { |
| 108 |
println!("{} {}", ui::id(format!("{remote}/{}", r.name)), ui::ok(if r.reason.is_empty() { "updated" } else { r.reason.as_str() })); |
| 109 |
} else { |
| 110 |
println!("{} {} {}", ui::id(format!("{remote}/{}", r.name)), ui::err("rejected:"), r.reason); |
| 111 |
code = 1; |
| 112 |
} |
| 113 |
} |
| 114 |
Ok(code) |
| 115 |
} |
| 116 |
|
| 117 |
pub fn serve(root: PathBuf, bind: &str, tokens: Vec<String>, tls: Option<(PathBuf, PathBuf)>) -> Result<i32> { |
| 118 |
let listener = std::net::TcpListener::bind(bind).with_context(|| format!("binding {bind}"))?; |
| 119 |
let root = root.canonicalize()?; |
| 120 |
let tls = tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key }); |
| 121 |
println!( |
| 122 |
"Serving repositories under {} on {}://{bind}/<repo>/oxy/v1/ ({})", |
| 123 |
root.display(), |
| 124 |
if tls.is_some() { "https" } else { "http" }, |
| 125 |
if tokens.is_empty() { "open: no push tokens configured" } else { "push requires a token" } |
| 126 |
); |
| 127 |
if tls.is_none() && !bind.starts_with("127.") && !bind.starts_with("localhost") && !bind.starts_with("[::1]") { |
| 128 |
eprintln!("warning: serving plain HTTP on a non-loopback address; put a TLS terminator in front or pass --tls-cert/--tls-key"); |
| 129 |
} |
| 130 |
oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens, tls })?; |
| 131 |
Ok(0) |
| 132 |
} |
| 133 |
|
| 134 |
pub fn git_export(git_dir: &std::path::Path) -> Result<i32> { |
| 135 |
let repo = open()?; |
| 136 |
let sp = ui::spinner("Exporting to Git…"); |
| 137 |
let r = repo.git_export(git_dir)?; |
| 138 |
sp.clear(); |
| 139 |
println!( |
| 140 |
"Exported {} to {} (branch {}: {}{}); {} already exported.", |
| 141 |
plural(r.exported, "change"), |
| 142 |
git_dir.display(), |
| 143 |
r.branch, |
| 144 |
plural(r.heads, "head"), |
| 145 |
if r.heads > 1 { ", joined by a synthetic merge commit" } else { "" }, |
| 146 |
r.skipped |
| 147 |
); |
| 148 |
Ok(0) |
| 149 |
} |
| 150 |
|
| 151 |
pub fn git_import(git_dir: &std::path::Path, dest: &std::path::Path, branch: &str) -> Result<i32> { |
| 152 |
let repo = Repo::init(dest)?; |
| 153 |
let started = std::time::Instant::now(); |
| 154 |
let sp = ui::spinner(&format!("Importing {} {branch}…", git_dir.display())); |
| 155 |
let r = repo.import_git(git_dir, branch)?; |
| 156 |
sp.clear(); |
| 157 |
println!( |
| 158 |
"Imported {} ({} merges) from {} {branch} into {} in {:.1?}: {} now has {}.{}", |
| 159 |
plural(r.commits, "commit"), |
| 160 |
r.merges, |
| 161 |
git_dir.display(), |
| 162 |
repo.root().display(), |
| 163 |
started.elapsed(), |
| 164 |
repo.config.bookmark, |
| 165 |
plural(r.heads, "head"), |
| 166 |
if r.skipped_submodules > 0 { format!(" Skipped {} submodule entries.", r.skipped_submodules) } else { String::new() } |
| 167 |
); |
| 168 |
Ok(0) |
| 169 |
} |
| 170 |
|
| 171 |
pub fn deepen(remote: &str, depth: u32) -> Result<i32> { |
| 172 |
let repo = open()?; |
| 173 |
if !repo.is_shallow() { |
| 174 |
println!("{}", ui::dim("This repository is not shallow; nothing to deepen.")); |
| 175 |
return Ok(0); |
| 176 |
} |
| 177 |
let sp = ui::spinner(&format!("Fetching {depth} more levels from {remote}…")); |
| 178 |
let n = repo.deepen(remote, depth)?; |
| 179 |
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") })); |
| 180 |
Ok(0) |
| 181 |
} |