use serde_json::json;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
const PRIMARY_WORKTREE_LINK_PATHS: &[&str] = &["apps/web/.dev.vars", "apps/web/wrangler.dev.jsonc"];
pub fn print_worktree_paths(invocation_dir: &Path) -> Result<(), String> {
let repo_root = get_repo_root(invocation_dir)?;
let primary_root = get_primary_worktree_root(invocation_dir)?;
let is_worktree = !path_eq(&repo_root, &primary_root);
let shared_state = primary_root.join(".wrangler").join("state");
println!(
"{}",
serde_json::to_string_pretty(&json!({
"repo_root": repo_root,
"primary_worktree_root": primary_root,
"is_worktree_checkout": is_worktree,
"shared_wrangler_state_path": shared_state,
}))
.map_err(|error| format!("Failed to encode JSON output: {}", error))?
);
Ok(())
}
pub fn link_dev_vars_from_primary_worktree(invocation_dir: &Path) -> Result<(), String> {
let repo_root = get_repo_root(invocation_dir)?;
let primary_root = get_primary_worktree_root(invocation_dir)?;
if path_eq(&repo_root, &primary_root) {
println!("Current checkout is the primary worktree; nothing to link.");
return Ok(());
}
let mut linked = Vec::new();
let mut skipped = Vec::new();
for rel_path in PRIMARY_WORKTREE_LINK_PATHS {
match link_from_primary_worktree(rel_path, &primary_root, &repo_root)? {
LinkOutcome::Linked(path) => linked.push(path),
LinkOutcome::Unchanged(path) => skipped.push(format!("{path} (already linked)")),
LinkOutcome::Skipped(path) => skipped.push(path),
}
}
if !linked.is_empty() {
println!("Linked from primary worktree:");
for file in linked {
println!(" - {}", file);
}
}
if !skipped.is_empty() {
println!("Skipped:");
for file in skipped {
println!(" - {}", file);
}
}
Ok(())
}
pub fn get_repo_root(invocation_dir: &Path) -> Result<PathBuf, String> {
let root = PathBuf::from(run_git(invocation_dir, ["rev-parse", "--show-toplevel"])?);
Ok(root.canonicalize().unwrap_or(root))
}
pub fn get_primary_worktree_root(invocation_dir: &Path) -> Result<PathBuf, String> {
let common_dir = get_git_common_dir(invocation_dir)?;
Ok(common_dir
.parent()
.map(Path::to_path_buf)
.unwrap_or(common_dir))
}
pub fn get_shared_wrangler_state_path(invocation_dir: &Path) -> Result<PathBuf, String> {
Ok(get_primary_worktree_root(invocation_dir)?
.join(".wrangler")
.join("state"))
}
pub fn is_worktree_checkout(invocation_dir: &Path) -> Result<bool, String> {
let repo_root = get_repo_root(invocation_dir)?;
let primary_root = get_primary_worktree_root(invocation_dir)?;
Ok(!path_eq(&repo_root, &primary_root))
}
fn get_git_common_dir(invocation_dir: &Path) -> Result<PathBuf, String> {
match run_git(
invocation_dir,
["rev-parse", "--path-format=absolute", "--git-common-dir"],
) {
Ok(path) => Ok(PathBuf::from(path)),
Err(_) => {
let common_dir_raw = run_git(invocation_dir, ["rev-parse", "--git-common-dir"])?;
let common_dir = PathBuf::from(&common_dir_raw);
if common_dir.is_absolute() {
Ok(common_dir)
} else {
Ok(invocation_dir.join(common_dir))
}
}
}
}
fn run_git<const N: usize>(invocation_dir: &Path, args: [&str; N]) -> Result<String, String> {
let output = Command::new("git")
.args(args)
.current_dir(invocation_dir)
.output()
.map_err(|error| format!("Failed to run git {}: {}", args.join(" "), error))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let message = if stderr.is_empty() {
format!(
"git {} exited with status {}",
args.join(" "),
output.status
)
} else {
stderr
};
return Err(message);
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if stdout.is_empty() {
return Err(format!("git {} returned an empty response", args.join(" ")));
}
Ok(stdout)
}
enum LinkOutcome {
Linked(String),
Unchanged(String),
Skipped(String),
}
fn link_from_primary_worktree(
rel_path: &str,
primary_root: &Path,
repo_root: &Path,
) -> Result<LinkOutcome, String> {
let source = primary_root.join(rel_path);
let target = repo_root.join(rel_path);
let Some(target_dir) = target.parent() else {
return Err(format!(
"Could not resolve parent directory for {}",
target.display()
));
};
if !source.exists() {
return Ok(LinkOutcome::Skipped(format!(
"{} (missing in primary worktree)",
rel_path
)));
}
fs::create_dir_all(target_dir)
.map_err(|error| format!("Failed to create {}: {}", target_dir.display(), error))?;
if let Ok(metadata) = fs::symlink_metadata(&target) {
if metadata.file_type().is_symlink() {
let current_target = fs::read_link(&target)
.map_err(|error| format!("Failed to inspect {}: {}", target.display(), error))?;
let resolved_target = if current_target.is_absolute() {
current_target
} else {
target_dir.join(current_target)
};
if path_eq(&resolved_target, &source) {
return Ok(LinkOutcome::Unchanged(rel_path.to_string()));
}
fs::remove_file(&target)
.map_err(|error| format!("Failed to replace {}: {}", target.display(), error))?;
} else {
return Ok(LinkOutcome::Skipped(format!(
"{} (real file exists)",
rel_path
)));
}
}
create_file_symlink(&source, &target)?;
Ok(LinkOutcome::Linked(rel_path.to_string()))
}
#[cfg(windows)]
fn create_file_symlink(source: &Path, target: &Path) -> Result<(), String> {
std::os::windows::fs::symlink_file(source, target).map_err(|error| {
format!(
"Failed to create symlink {} -> {}: {}",
target.display(),
source.display(),
error
)
})
}
#[cfg(not(windows))]
fn create_file_symlink(source: &Path, target: &Path) -> Result<(), String> {
std::os::unix::fs::symlink(source, target).map_err(|error| {
format!(
"Failed to create symlink {} -> {}: {}",
target.display(),
source.display(),
error
)
})
}
fn path_eq(left: &Path, right: &Path) -> bool {
normalize_path(left) == normalize_path(right)
}
fn normalize_path(path: &Path) -> String {
path.to_string_lossy()
.replace('\\', "/")
.to_ascii_lowercase()
}