use std::path::{Path, PathBuf};
use ito_config::types::ItoConfig;
use crate::coordination_worktree::repair_current_worktree_coordination_links;
use crate::errors::{CoreError, CoreResult};
use crate::process::{ProcessRequest, ProcessRunner, SystemProcessRunner};
use crate::repo_paths::{ResolvedEnv, ResolvedWorktreePaths, WorktreeFeature, WorktreeSelector};
use crate::worktree_init;
const INIT_MARKER: &str = "ito-initialized";
pub fn ensure_worktree(
change_id: &str,
config: &ItoConfig,
env: &ResolvedEnv,
worktree_paths: &ResolvedWorktreePaths,
cwd: &Path,
) -> CoreResult<PathBuf> {
let runner = SystemProcessRunner;
ensure_worktree_with_runner(&runner, change_id, config, env, worktree_paths, cwd)
}
pub(crate) fn ensure_worktree_with_runner(
runner: &dyn ProcessRunner,
change_id: &str,
config: &ItoConfig,
env: &ResolvedEnv,
worktree_paths: &ResolvedWorktreePaths,
cwd: &Path,
) -> CoreResult<PathBuf> {
validate_change_id(change_id)?;
let WorktreeFeature::Enabled = worktree_paths.feature else {
return Ok(cwd.to_path_buf());
};
let selector = WorktreeSelector::Change(change_id.to_string());
let worktree_path = worktree_paths.path_for_selector(&selector).ok_or_else(|| {
CoreError::validation(format!(
"Cannot resolve worktree path for change '{change_id}'.\n\
Worktrees are enabled but the worktrees root could not be determined.\n\
Fix: check 'worktrees.strategy' and 'worktrees.layout' in .ito/config.json.",
))
})?;
if worktree_path.is_dir() {
let git_entry = worktree_path.join(".git");
let has_git = git_entry.exists();
let ito_path = worktree_path.join(".ito");
let has_marker = has_git && {
resolve_gitdir(&git_entry)
.map(|gitdir| gitdir.join(INIT_MARKER).exists())
.unwrap_or(false)
};
if has_git && has_marker {
repair_current_worktree_coordination_links(&env.project_root, &ito_path, config)?;
return Ok(worktree_path);
}
if has_git {
repair_current_worktree_coordination_links(&env.project_root, &ito_path, config)?;
let source_root = worktree_paths.main_worktree_root.as_deref().unwrap_or(cwd);
worktree_init::init_worktree_with_runner(
runner,
source_root,
&worktree_path,
&config.worktrees,
)?;
write_init_marker(&worktree_path)?;
return Ok(worktree_path);
}
}
if let Some(parent) = worktree_path.parent() {
std::fs::create_dir_all(parent).map_err(|err| {
CoreError::io(
format!(
"Cannot create worktrees directory '{}'.\n\
Fix: ensure the path is writable.",
parent.display(),
),
err,
)
})?;
}
let default_branch = &config.worktrees.default_branch;
create_change_worktree(
runner,
&env.project_root,
&env.ito_root,
change_id,
default_branch,
&worktree_path,
)?;
let source_root = worktree_paths.main_worktree_root.as_deref().unwrap_or(cwd);
let ito_path = worktree_path.join(".ito");
repair_current_worktree_coordination_links(&env.project_root, &ito_path, config)?;
worktree_init::init_worktree_with_runner(
runner,
source_root,
&worktree_path,
&config.worktrees,
)?;
write_init_marker(&worktree_path)?;
Ok(worktree_path)
}
fn resolve_gitdir(git_entry: &Path) -> Option<PathBuf> {
if git_entry.is_dir() {
return Some(git_entry.to_path_buf());
}
let content = std::fs::read_to_string(git_entry).ok()?;
let line = content.lines().next()?;
let pointer = line.strip_prefix("gitdir:")?;
let pointer = pointer.trim();
if pointer.is_empty() {
return None;
}
let parent = git_entry.parent()?;
let gitdir = parent.join(pointer);
gitdir.canonicalize().ok()
}
fn write_init_marker(worktree_path: &Path) -> CoreResult<()> {
let git_entry = worktree_path.join(".git");
let gitdir = resolve_gitdir(&git_entry).ok_or_else(|| {
CoreError::validation(format!(
"Cannot resolve gitdir for worktree at '{}'.\n\
Fix: ensure the worktree has a valid .git file or directory.",
worktree_path.display(),
))
})?;
let marker_path = gitdir.join(INIT_MARKER);
std::fs::write(&marker_path, "initialized\n").map_err(|err| {
CoreError::io(
format!(
"Cannot write initialization marker at '{}'.\n\
Fix: ensure the gitdir path is writable.",
marker_path.display(),
),
err,
)
})
}
fn validate_change_id(change_id: &str) -> CoreResult<()> {
if change_id.is_empty() {
return Err(CoreError::validation(
"Change ID must not be empty.\n\
Fix: provide a valid change ID (e.g. '012-05_my-change').",
));
}
if change_id.starts_with('-') {
return Err(CoreError::validation(format!(
"Change ID '{change_id}' must not start with '-'.\n\
A leading dash could be misinterpreted as a git flag.\n\
Fix: use a change ID that starts with an alphanumeric character.",
)));
}
if change_id.contains("..") {
return Err(CoreError::validation(format!(
"Change ID '{change_id}' must not contain '..'.\n\
This could enable path traversal.\n\
Fix: use a change ID without '..' components.",
)));
}
if change_id.contains('/') || change_id.contains('\\') || change_id.contains('\0') {
return Err(CoreError::validation(format!(
"Change ID '{change_id}' contains invalid characters (/, \\, or NUL).\n\
Fix: use a change ID with only alphanumeric characters, dashes, and underscores.",
)));
}
Ok(())
}
fn create_change_worktree(
runner: &dyn ProcessRunner,
project_root: &Path,
ito_root: &Path,
change_id: &str,
base_branch: &str,
target_path: &Path,
) -> CoreResult<()> {
let config_path = write_worktrunk_path_config(ito_root, target_path)?;
let config_arg = config_path.to_string_lossy().to_string();
let project_root_arg = project_root.to_string_lossy().to_string();
let request = ProcessRequest::new("wt")
.args([
"--config",
&config_arg,
"-C",
&project_root_arg,
"--yes",
"switch",
"--create",
change_id,
"--base",
base_branch,
])
.current_dir(project_root);
let output = runner.run(&request).map_err(|err| {
CoreError::process(format!(
"Cannot create worktree for change '{change_id}' at '{target}'.\n\
Worktrunk command failed to run: {err}\n\
Command context: wt switch --create {change_id} --base {base_branch}\n\
Fix: install Worktrunk and ensure `wt` is available on PATH, or create the worktree manually at the target path.",
target = target_path.display(),
))
})?;
if output.success {
return Ok(());
}
let detail = if !output.stderr.trim().is_empty() {
output.stderr.trim().to_string()
} else if !output.stdout.trim().is_empty() {
output.stdout.trim().to_string()
} else {
"no command output".to_string()
};
Err(CoreError::process(format!(
"Cannot create worktree for change '{change_id}' at '{target}'.\n\
Worktrunk reported: {detail}\n\
Command context: wt switch --create {change_id} --base {base_branch}\n\
Fix: ensure Worktrunk can access base branch '{base_branch}', the target path is free, and the local Worktrunk path config points at the Ito worktree root.",
target = target_path.display(),
)))
}
fn write_worktrunk_path_config(ito_root: &Path, target_path: &Path) -> CoreResult<PathBuf> {
let parent = target_path.parent().ok_or_else(|| {
CoreError::validation(format!(
"Cannot derive Worktrunk path config for '{}'.\n\
Fix: configure worktrees so the target path has a parent directory.",
target_path.display(),
))
})?;
let config_dir = ito_root.join("worktrunk");
std::fs::create_dir_all(&config_dir).map_err(|err| {
CoreError::io(
format!(
"Cannot create Worktrunk config directory '{}'.\n\
Fix: ensure the Ito directory is writable.",
config_dir.display(),
),
err,
)
})?;
let config_path = config_dir.join("worktree-path.toml");
let template = parent.join("{{ branch | sanitize }}");
let contents = format!(
"worktree-path = \"{}\"\n",
template
.to_string_lossy()
.replace('\\', "\\\\")
.replace('"', "\\\"")
);
std::fs::write(&config_path, contents).map_err(|err| {
CoreError::io(
format!(
"Cannot write Worktrunk path config '{}'.\n\
Fix: ensure the Ito directory is writable.",
config_path.display(),
),
err,
)
})?;
Ok(config_path)
}
#[cfg(test)]
#[path = "worktree_ensure_tests.rs"]
mod worktree_ensure_tests;