use crate::{audit, manifest::Manifest, paths, proc};
use anyhow::Result;
pub fn https_github_url(origin: &str) -> Option<String> {
let origin = origin.trim();
let path = if let Some(rest) = origin.strip_prefix("https://github.com/") {
rest.to_string()
} else if let Some(rest) = origin.strip_prefix("ssh://") {
let rest = rest.split_once('/')?.1;
rest.to_string()
} else if let Some((host, rest)) = origin.split_once(':') {
let host = host.rsplit('@').next().unwrap_or(host);
if host != "github.com" && !host.starts_with("github.com") {
return None;
}
rest.to_string()
} else {
return None;
};
let path = path.strip_suffix(".git").unwrap_or(&path);
let (owner, repo) = path.split_once('/')?;
let repo = repo.split('/').next().unwrap_or(repo);
let safe = |s: &str| {
!s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
};
(safe(owner) && safe(repo)).then(|| format!("https://github.com/{owner}/{repo}.git"))
}
fn exec(name: &str, m: &Manifest, argv: &[&str]) -> Result<(bool, String)> {
let container = m.sandbox_container();
let mut args = vec!["exec", container.as_str()];
args.extend_from_slice(argv);
let (status, out) = proc::run_capture_combined("docker", &args)?;
let logged: Vec<String> = argv.iter().map(|a| a.to_string()).collect();
audit::log_exec(name, m, "pull", &logged, status.code())?;
Ok((status.success(), out))
}
pub fn run(explicit: Option<String>) -> Result<()> {
let name = super::resolve_project_announced(explicit)?;
let m = Manifest::load(&paths::manifest_path(&name)?)?;
crate::secrets::resolve_into_env(&name, &m.secrets);
if super::running_projects(std::slice::from_ref(&name))?.is_empty() {
anyhow::bail!("{name} isn't running.\n\n Next: moor up {name}");
}
let (ok, origin) = exec(&name, &m, &["git", "remote", "get-url", "origin"])?;
if !ok || origin.trim().is_empty() {
anyhow::bail!("the sandbox's repository has no `origin` remote to pull from");
}
let origin = origin.trim();
let Some(https) = https_github_url(origin) else {
anyhow::bail!(
"moor pull only knows how to pull from a github.com remote over HTTPS; origin is:\n {origin}"
);
};
let (ok, branch) = exec(&name, &m, &["git", "rev-parse", "--abbrev-ref", "HEAD"])?;
let branch = branch.trim();
if !ok || branch.is_empty() || branch == "HEAD" {
anyhow::bail!("the sandbox's repository isn't on a branch to pull");
}
let helper = "credential.helper=!f() { echo username=x-access-token; echo \"password=$GITHUB_TOKEN\"; }; f";
println!(
"==> pulling {branch} from {https} (HTTPS, token injected in-container, nothing persisted)"
);
let (ok, out) = exec(
&name,
&m,
&["git", "-c", helper, "pull", "--ff-only", &https, branch],
)?;
let shown = out.trim();
println!("{shown}");
if !ok {
anyhow::bail!(
"pull failed. If the repo is private, make sure GITHUB_TOKEN is set for this project:\n\n \
moor secrets set {name} GITHUB_TOKEN\n moor up {name}\n\nthen try again."
);
}
println!("\n==> {name} is up to date on {branch}.");
Ok(())
}
#[cfg(test)]
mod tests {
use super::https_github_url;
#[test]
fn rewrites_scp_style_with_host_alias() {
assert_eq!(
https_github_url("git@github.com-sbg:Dane-Balia_sbg/filenet-retriever.git"),
Some("https://github.com/Dane-Balia_sbg/filenet-retriever.git".to_string())
);
}
#[test]
fn rewrites_plain_scp_style() {
assert_eq!(
https_github_url("git@github.com:daneb/moor.git"),
Some("https://github.com/daneb/moor.git".to_string())
);
}
#[test]
fn rewrites_ssh_url_form() {
assert_eq!(
https_github_url("ssh://git@github.com/daneb/moor.git"),
Some("https://github.com/daneb/moor.git".to_string())
);
}
#[test]
fn passes_through_https() {
assert_eq!(
https_github_url("https://github.com/daneb/moor.git"),
Some("https://github.com/daneb/moor.git".to_string())
);
}
#[test]
fn adds_git_suffix_when_missing() {
assert_eq!(
https_github_url("git@github.com:daneb/moor"),
Some("https://github.com/daneb/moor.git".to_string())
);
}
#[test]
fn refuses_non_github_host() {
assert_eq!(https_github_url("git@gitlab.com:acme/thing.git"), None);
assert_eq!(https_github_url("https://gitlab.com/acme/thing.git"), None);
}
#[test]
fn refuses_unsafe_owner_or_repo() {
assert_eq!(https_github_url("git@github.com:ow ner/repo.git"), None);
assert_eq!(https_github_url("git@github.com:owner/re;po.git"), None);
}
#[test]
fn refuses_garbage() {
assert_eq!(https_github_url("not a url"), None);
assert_eq!(https_github_url(""), None);
}
}