use std::path::Path;
use std::sync::atomic::AtomicBool;
use anyhow::{Context, Result, bail};
use inquire::Text;
use owo_colors::OwoColorize;
use crate::cli::CloneArgs;
use crate::commands::ShokaContext;
use crate::config::{ShokaConfig, VcsDefault};
use crate::remote::parse_clone_input;
use crate::state::{Repo, Shelf};
pub async fn run(ctx: &ShokaContext, args: CloneArgs) -> Result<()> {
let input = match args.url {
Some(s) => s,
None => prompt_for_input()?,
};
let cfg = ShokaConfig::load(&ctx.paths)?.resolve(ctx.profile_override.as_deref())?;
let (parts, url) = parse_clone_input(&input, &cfg.default_host, cfg.default_protocol)?;
let repo = Repo::new(parts.host, parts.owner, parts.name);
let dest = cfg.clone_path_for_one(&repo)?;
let target = cfg.resolve_target(&repo.slug());
if dest_is_occupied(&dest)? {
bail!(
"destination {} already exists and is not empty — \
refusing to clone over existing content",
dest.display()
);
}
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating parent dir {}", parent.display()))?;
}
println!("{} {} → {}", "clone:".bold(), repo.slug(), dest.display());
match target.default_vcs {
VcsDefault::Jj => {
let url_str = url.to_string();
spawn_jj_clone(&url_str, &dest).await?;
}
VcsDefault::Git | VcsDefault::Auto => {
let dest_owned = dest.clone();
let url_owned = url.clone();
tokio::task::spawn_blocking(move || gix_clone(url_owned, &dest_owned))
.await
.context("gix clone task panicked")?
.with_context(|| format!("cloning {} via gix", url))?;
}
}
let mut shelf = Shelf::load(&ctx.paths)?;
if shelf.find(&repo.host, &repo.owner, &repo.name).is_some() {
tracing::warn!(
target: "shoka",
"shelf already had {} — leaving the existing entry's metadata",
repo.slug()
);
} else {
shelf.add(repo).context("recording cloned repo on shelf")?;
shelf.save(&ctx.paths)?;
}
println!("{} done", "clone:".bold());
Ok(())
}
fn prompt_for_input() -> Result<String> {
let answer = Text::new("URL or owner/name to clone:")
.with_help_message("e.g. https://github.com/foo/bar or foo/bar")
.prompt()
.context("clone input prompt cancelled")?;
if answer.trim().is_empty() {
bail!("empty clone input");
}
Ok(answer)
}
fn dest_is_occupied(dest: &Path) -> Result<bool> {
match std::fs::read_dir(dest) {
Ok(mut it) => Ok(it.next().is_some()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e).with_context(|| format!("inspecting {}", dest.display())),
}
}
fn gix_clone(url: gix::Url, dest: &Path) -> Result<()> {
let interrupt = AtomicBool::new(false);
let mut prepare_fetch = gix::prepare_clone(url, dest).context("preparing clone")?;
let (mut prepare_checkout, _outcome) = prepare_fetch
.fetch_then_checkout(gix::progress::Discard, &interrupt)
.context("fetching from remote")?;
let (_repo, _outcome) = prepare_checkout
.main_worktree(gix::progress::Discard, &interrupt)
.context("checking out main worktree")?;
Ok(())
}
async fn spawn_jj_clone(url: &str, dest: &Path) -> Result<()> {
let jj = which::which("jj").context("`jj` not found on PATH (required for vcs = jj)")?;
let mut cmd = tokio::process::Command::new(&jj);
cmd.arg("git").arg("clone").arg(url).arg(dest);
#[cfg(windows)]
cmd.creation_flags(crate::silent_creation_flags());
let status = cmd
.status()
.await
.with_context(|| format!("spawning `{} git clone`", jj.display()))?;
if !status.success() {
bail!("jj git clone exited with status {status}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dest_is_occupied_missing_dir_is_free() {
let tmp = tempfile::TempDir::new().unwrap();
let absent = tmp.path().join("not-there");
assert!(!dest_is_occupied(&absent).unwrap());
}
#[test]
fn dest_is_occupied_empty_dir_is_free() {
let tmp = tempfile::TempDir::new().unwrap();
let empty = tmp.path().join("empty");
std::fs::create_dir_all(&empty).unwrap();
assert!(!dest_is_occupied(&empty).unwrap());
}
#[test]
fn dest_is_occupied_nonempty_dir_is_blocked() {
let tmp = tempfile::TempDir::new().unwrap();
let occupied = tmp.path().join("occupied");
std::fs::create_dir_all(&occupied).unwrap();
std::fs::write(occupied.join("leftover.txt"), "hi").unwrap();
assert!(dest_is_occupied(&occupied).unwrap());
}
}