123 lines
4.1 KiB
Rust
123 lines
4.1 KiB
Rust
//! `clone [<dest>] [--branch <b>] [--depth <n>]` — clone a forge repo
|
|
//! with credentials auto-injected, so agents don't hand-assemble
|
|
//! token-bearing URLs. The repo is the standard `-r/--repo` (default
|
|
//! `HIVE_FORGE_REPO`). Pairs with `pr-create --agit`: clone, edit +
|
|
//! commit normally, then open a PR via the `AGit` ref (closes #1399).
|
|
|
|
use std::process::Command;
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use clap::Args as ClapArgs;
|
|
|
|
use crate::client::Client;
|
|
|
|
#[derive(ClapArgs)]
|
|
pub struct Args {
|
|
/// Destination directory. Defaults to the repo's basename
|
|
/// (e.g. `internal/knowledge` → `knowledge`).
|
|
dest: Option<String>,
|
|
/// Branch to check out after cloning.
|
|
#[arg(long)]
|
|
branch: Option<String>,
|
|
/// Shallow-clone depth (omit for a full clone).
|
|
#[arg(long)]
|
|
depth: Option<u32>,
|
|
}
|
|
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the `git clone` shellout fails (bad repo, auth
|
|
/// rejected, network) or the destination can't be derived.
|
|
pub fn run(client: &Client, args: Args) -> Result<()> {
|
|
let repo = client.repo();
|
|
let dest = match args.dest {
|
|
Some(d) => d,
|
|
None => repo
|
|
.rsplit('/')
|
|
.next()
|
|
.filter(|s| !s.is_empty())
|
|
.map(str::to_owned)
|
|
.with_context(|| format!("clone: cannot derive a destination dir from repo {repo}"))?,
|
|
};
|
|
let url = client.authed_git_url(repo);
|
|
|
|
let mut git_args = vec!["clone".to_owned()];
|
|
if let Some(depth) = args.depth {
|
|
git_args.push(format!("--depth={depth}"));
|
|
}
|
|
if let Some(branch) = &args.branch {
|
|
git_args.push("--branch".to_owned());
|
|
git_args.push(branch.clone());
|
|
}
|
|
git_args.push(url);
|
|
git_args.push(dest.clone());
|
|
|
|
let arg_refs: Vec<&str> = git_args.iter().map(String::as_str).collect();
|
|
let out = Command::new("git")
|
|
.args(&arg_refs)
|
|
.output()
|
|
.context("spawn `git clone`")?;
|
|
if !out.status.success() {
|
|
// Scrub the token from any URL echoed back in git's error.
|
|
let stderr = scrub_credentials(&String::from_utf8_lossy(&out.stderr));
|
|
bail!("clone: git clone {repo} failed:\n{stderr}");
|
|
}
|
|
// Print the destination so callers can `cd` into it.
|
|
println!("{dest}");
|
|
Ok(())
|
|
}
|
|
|
|
/// Redact the `user:token@` userinfo from any URL in `s` so a token
|
|
/// never lands in an error message / log. Replaces the credential span
|
|
/// with `***` while keeping the rest of the URL legible.
|
|
fn scrub_credentials(s: &str) -> String {
|
|
let mut out = String::with_capacity(s.len());
|
|
let mut rest = s;
|
|
while let Some(scheme_at) = rest.find("://") {
|
|
let after_scheme = scheme_at + 3;
|
|
// Userinfo runs from after `://` up to the next `@`, but only if
|
|
// that `@` comes before the next `/` (i.e. it's in the authority).
|
|
let authority = &rest[after_scheme..];
|
|
let at = authority.find('@');
|
|
let slash = authority.find('/');
|
|
match (at, slash) {
|
|
(Some(a), maybe_slash) if maybe_slash.is_none_or(|sl| a < sl) => {
|
|
out.push_str(&rest[..after_scheme]);
|
|
out.push_str("***");
|
|
out.push('@');
|
|
rest = &authority[a + 1..];
|
|
}
|
|
_ => {
|
|
// No credentials in this URL; emit up to here and move on.
|
|
out.push_str(&rest[..after_scheme]);
|
|
rest = authority;
|
|
}
|
|
}
|
|
}
|
|
out.push_str(rest);
|
|
out
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn scrubs_token_userinfo() {
|
|
let s = "fatal: unable to access 'http://damocles:deadbeef@localhost:3000/internal/knowledge.git/'";
|
|
let scrubbed = scrub_credentials(s);
|
|
assert!(!scrubbed.contains("deadbeef"));
|
|
assert!(scrubbed.contains("http://***@localhost:3000/internal/knowledge.git"));
|
|
}
|
|
|
|
#[test]
|
|
fn leaves_credential_free_urls_intact() {
|
|
let s = "Cloning into 'http://localhost:3000/x/y.git'...";
|
|
assert_eq!(scrub_credentials(s), s);
|
|
}
|
|
|
|
#[test]
|
|
fn handles_no_url() {
|
|
assert_eq!(scrub_credentials("plain message"), "plain message");
|
|
}
|
|
}
|