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,
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 branch_exists(
runner: &dyn ProcessRunner,
project_root: &Path,
branch: &str,
) -> CoreResult<bool> {
let request = ProcessRequest::new("git")
.args(["rev-parse", "--verify", branch])
.current_dir(project_root);
let output = runner.run(&request).map_err(|err| {
CoreError::process(format!(
"Cannot check whether branch '{branch}' exists.\n\
Git command failed to run: {err}\n\
Fix: ensure git is installed and '{project_root}' is a git repository.",
project_root = project_root.display(),
))
})?;
Ok(output.success)
}
fn create_change_worktree(
runner: &dyn ProcessRunner,
project_root: &Path,
change_id: &str,
base_branch: &str,
target_path: &Path,
) -> CoreResult<()> {
let target_str = target_path.to_string_lossy();
let branch_already_exists = branch_exists(runner, project_root, change_id)?;
let request = if branch_already_exists {
ProcessRequest::new("git")
.args(["worktree", "add", target_str.as_ref(), change_id])
.current_dir(project_root)
} else {
ProcessRequest::new("git")
.args([
"worktree",
"add",
target_str.as_ref(),
"-b",
change_id,
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\
Git command failed to run: {err}\n\
Fix: ensure git is installed and '{project_root}' is a git repository.",
target = target_path.display(),
project_root = project_root.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\
Git reported: {detail}\n\
Fix: ensure the base branch '{base_branch}' exists and the target path \
does not already exist. If the branch is already checked out in another \
worktree, run `git worktree list` to inspect.",
target = target_path.display(),
)))
}
#[cfg(test)]
#[path = "worktree_ensure_tests.rs"]
mod worktree_ensure_tests;