use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use anyhow::{Context, Result, bail};
use clap::{Args as ClapArgs, Subcommand};
use uuid::Uuid;
#[derive(Debug, ClapArgs)]
pub(super) struct Args {
#[command(subcommand)]
command: CommandArgs,
}
#[derive(Debug, Subcommand)]
enum CommandArgs {
Fetch(FetchArgs),
}
#[derive(Debug, ClapArgs)]
struct FetchArgs {
repository: String,
#[arg(long, default_value = ".devclean.toml")]
file: PathBuf,
#[arg(long, default_value = ".devclean.toml")]
output: PathBuf,
#[arg(long)]
force: bool,
#[arg(long)]
dry_run: bool,
}
pub(super) fn run(arguments: &Args) -> Result<()> {
match &arguments.command {
CommandArgs::Fetch(arguments) => fetch(arguments),
}
}
fn fetch(arguments: &FetchArgs) -> Result<()> {
validate_relative_file(&arguments.file)?;
if arguments.output.exists() && !arguments.force {
bail!(
"{} already exists; pass --force to replace it",
arguments.output.display()
);
}
if arguments.dry_run {
println!(
"would clone {} and validate {} before writing {}",
arguments.repository,
arguments.file.display(),
arguments.output.display()
);
return Ok(());
}
let checkout = std::env::temp_dir().join(format!("devclean-config-{}", Uuid::new_v4()));
let result = fetch_into(arguments, &checkout);
let _ = fs::remove_dir_all(&checkout);
result
}
fn fetch_into(arguments: &FetchArgs, checkout: &Path) -> Result<()> {
let status = Command::new("git")
.args(["clone", "--depth", "1", "--"])
.arg(&arguments.repository)
.arg(checkout)
.stdin(Stdio::null())
.status()
.context("failed to run git clone for shared config")?;
if !status.success() {
bail!("git clone failed for shared config repository");
}
let source = checkout.join(&arguments.file);
devclean::load_config(Some(&source)).context("shared config did not pass strict validation")?;
if let Some(parent) = arguments
.output
.parent()
.filter(|path| !path.as_os_str().is_empty())
{
fs::create_dir_all(parent)?;
}
fs::copy(&source, &arguments.output).with_context(|| {
format!(
"failed to copy validated config to {}",
arguments.output.display()
)
})?;
println!(
"installed validated config at {}",
arguments.output.display()
);
Ok(())
}
fn validate_relative_file(path: &Path) -> Result<()> {
anyhow::ensure!(
!path.is_absolute()
&& path
.components()
.all(|component| matches!(component, std::path::Component::Normal(_))),
"--file must be a repository-relative path without parent traversal"
);
Ok(())
}