use std::path::{Path, PathBuf};
use std::process::Command;
use crate::types::AgentKind;
use super::RunOpts;
pub(crate) use super::cargo_target::BranchTargetSeedOutcome;
const CARGO_TARGET_DIR_ENV: &str = "CARGO_TARGET_DIR";
const CARGO_MANIFEST_NAME: &str = "Cargo.toml";
const BASE_TARGET_DIR_NAME: &str = "_base";
const SHARED_TARGET_DIR_NAME: &str = "cargo-target";
struct CargoTargetLayout {
source: PathBuf,
branch_root: PathBuf,
}
pub fn agent_has_fs_access(_kind: &AgentKind) -> bool {
true }
pub fn shared_target_dir() -> Option<String> {
Some(target_layout()?.source.to_string_lossy().into_owned())
}
fn target_layout() -> Option<CargoTargetLayout> {
if let Some(branch_root) = std::env::var_os(CARGO_TARGET_DIR_ENV).map(PathBuf::from) {
let source = branch_root.join(BASE_TARGET_DIR_NAME);
return Some(CargoTargetLayout { source, branch_root });
}
let branch_root = crate::paths::aid_dir().join(SHARED_TARGET_DIR_NAME);
let source = branch_root.join(BASE_TARGET_DIR_NAME);
Some(CargoTargetLayout { source, branch_root })
}
pub fn target_dir_for_worktree(worktree_branch: Option<&str>) -> Option<String> {
let layout = target_layout()?;
match worktree_branch {
Some(branch) => {
let target = target_dir_for_branch(&layout.branch_root, branch);
Some(target.to_string_lossy().into_owned())
}
None => shared_target_dir(),
}
}
pub(crate) fn branch_target_root() -> Option<PathBuf> {
Some(target_layout()?.branch_root)
}
pub(crate) fn branch_target_name(branch: &str) -> String {
branch.replace('/', "-")
}
pub(crate) fn seed_branch_target_dir(
worktree_branch: &str,
) -> Option<BranchTargetSeedOutcome> {
let layout = target_layout()?;
let target = target_dir_for_branch(&layout.branch_root, worktree_branch);
let outcome = if layout.source.is_dir() {
super::cargo_target::seed_branch_target_from_source(&layout.source, &target)
} else {
super::cargo_target::seed_branch_target_from_root_entries(&layout.branch_root, &target)
};
Some(outcome)
}
pub(crate) fn remove_branch_target_dir_if_unused(repo_dir: &Path, branch: &str) -> anyhow::Result<bool> {
let Some(layout) = target_layout() else {
return Ok(false);
};
let target = target_dir_for_branch(&layout.branch_root, branch);
if target.file_name().and_then(|name| name.to_str()) == Some(BASE_TARGET_DIR_NAME) {
return Ok(false);
}
if branch_has_live_worktree(repo_dir, branch)? {
return Ok(false);
}
super::cargo_target::remove_branch_target_dir(&target)
}
pub fn apply_rust_build_cache_env(
cmd: &mut Command,
project_dir: Option<&str>,
worktree_branch: Option<&str>,
) {
if !is_rust_project(project_dir) {
return;
}
if let Some(target_dir) = target_dir_for_worktree(worktree_branch) {
apply_cargo_target_env(cmd, Some(target_dir.as_str()));
}
}
pub fn apply_cargo_target_env(cmd: &mut Command, cargo_target_dir: Option<&str>) {
if let Some(target_dir) = cargo_target_dir {
cmd.env(CARGO_TARGET_DIR_ENV, target_dir);
}
}
pub fn cargo_target_env_arg(cargo_target_dir: &str) -> String {
format!("{CARGO_TARGET_DIR_ENV}={cargo_target_dir}")
}
fn target_dir_for_branch(base: &Path, branch: &str) -> PathBuf {
base.join(branch_target_name(branch))
}
fn branch_has_live_worktree(repo_dir: &Path, branch: &str) -> anyhow::Result<bool> {
let output = Command::new("git")
.args(["-C", &repo_dir.to_string_lossy(), "worktree", "list", "--porcelain"])
.output()?;
if !output.status.success() {
return Ok(true);
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(worktree_list_has_branch(&stdout, branch))
}
fn worktree_list_has_branch(output: &str, branch: &str) -> bool {
output.split("\n\n").any(|block| {
!block.lines().any(|line| line.starts_with("prunable "))
&& block.lines().any(|line| {
line.strip_prefix("branch refs/heads/") == Some(branch)
})
})
}
pub fn is_rust_project(dir: Option<&str>) -> bool {
resolve_project_dir(dir).join(CARGO_MANIFEST_NAME).is_file()
}
fn resolve_project_dir(dir: Option<&str>) -> PathBuf {
dir.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok())
.unwrap_or_else(|| PathBuf::from("."))
}
pub fn set_git_ceiling(cmd: &mut Command, dir: &str) {
let path = std::path::Path::new(dir);
if let Some(parent) = path.parent() {
cmd.env("GIT_CEILING_DIRECTORIES", parent);
}
}
pub fn apply_run_env(cmd: &mut Command, opts: &RunOpts) {
cmd.env("AID_HOME", crate::paths::aid_dir());
if let Some(env) = opts.env.as_ref() {
for (key, value) in env {
cmd.env(key, value);
}
}
if let Some(env_forward) = opts.env_forward.as_ref() {
for name in env_forward {
if let Ok(value) = std::env::var(name) {
cmd.env(name, value);
}
}
}
}
pub(crate) fn which_exists(name: &str) -> bool {
Command::new("which")
.arg(name)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(test)]
#[path = "env_tests.rs"]
mod tests;