use dialoguer::{FuzzySelect, Input};
use log::debug;
use miette::{bail, IntoDiagnostic, Result, WrapErr};
use crate::cli::New;
use crate::hooks::execute_post_create_hooks;
use crate::output;
use workon::{
add_worktree, copy_untracked, get_repo, get_worktrees, workon_root, BranchType, CopyOptions,
WorktreeDescriptor,
};
use super::Run;
impl Run for New {
fn run(&self) -> Result<Option<WorktreeDescriptor>> {
let name = match &self.name {
Some(name) => name.clone(),
None => {
if self.no_interactive {
bail!("No worktree name provided. Specify a name or remove --no-interactive.");
}
let name: String = Input::new()
.with_prompt("Branch name")
.interact_text()
.into_diagnostic()
.wrap_err("Failed to read branch name")?;
if name.trim().is_empty() {
bail!("Branch name cannot be empty");
}
name.trim().to_string()
}
};
let repo = get_repo(None).wrap_err("Failed to find git repository")?;
let config = workon::WorkonConfig::new(&repo)?;
let pr_info = if !self.orphan && !self.detach && self.base.is_none() {
let info = workon::parse_pr_reference(&name)?;
if info.is_some() {
debug!("Detected PR reference in '{}'", name);
}
info
} else {
debug!("Skipping PR detection (conflicting flags)");
None
};
let (worktree_name, base_branch, branch_type) = if let Some(pr) = pr_info {
let pr_format = config.pr_format(None)?;
let pb = output::create_spinner();
pb.set_message(format!("Fetching PR #{} metadata...", pr.number));
let (worktree_name, remote_ref, base_ref) =
workon::prepare_pr_worktree(&repo, pr.number, &pr_format)
.wrap_err(format!("Failed to prepare PR #{} worktree", pr.number))
.inspect_err(|_| pb.finish_and_clear())?;
pb.finish_and_clear();
let pb = output::create_spinner();
pb.set_message("Creating worktree...");
let worktree =
add_worktree(&repo, &worktree_name, BranchType::Normal, Some(&remote_ref))
.inspect_err(|_| pb.finish_and_clear())?;
pb.finish_and_clear();
let parts: Vec<&str> = remote_ref.split('/').collect();
let remote_name = parts.first().copied().unwrap_or("origin");
let branch_name = parts[1..].join("/"); let branch_ref = format!("refs/heads/{}", branch_name);
workon::set_upstream_tracking(&worktree, remote_name, &branch_ref)
.wrap_err("Failed to set upstream tracking for PR branch")?;
let copy_override = if self.copy_untracked {
Some(true)
} else if self.no_copy_untracked {
Some(false)
} else {
None
};
if config.auto_copy_untracked(copy_override)? {
if let Err(e) = copy_untracked_files(
&repo,
&worktree,
Some(&base_ref),
&config,
self.copy_ignored,
) {
output::warn(&format!("Failed to copy untracked files: {}", e));
}
}
if !self.no_hooks {
if let Err(e) = execute_post_create_hooks(&worktree, Some(&base_ref), &config) {
output::warn(&format!("Post-create hook failed: {}", e));
}
}
return Ok(Some(worktree));
} else {
let base_branch = if let Some(base) = &self.base {
debug!("Using explicit base branch: {}", base);
config.default_branch(Some(base))?
} else if !self.no_interactive && self.name.is_none() {
debug!("Prompting for base branch (interactive mode)");
prompt_for_base_branch(&repo, &config)?
} else {
debug!("Using default base branch from config");
config.default_branch(None)?
};
let branch_type = if self.orphan {
BranchType::Orphan
} else if self.detach {
BranchType::Detached
} else {
BranchType::Normal
};
(name, base_branch, branch_type)
};
let worktree = add_worktree(&repo, &worktree_name, branch_type, base_branch.as_deref())
.wrap_err(format!("Failed to create worktree '{}'", worktree_name))?;
let copy_override = if self.copy_untracked {
Some(true)
} else if self.no_copy_untracked {
Some(false)
} else {
None
};
if config.auto_copy_untracked(copy_override)? {
debug!("Auto-copy enabled, copying from base worktree");
if let Err(e) = copy_untracked_files(
&repo,
&worktree,
base_branch.as_deref(),
&config,
self.copy_ignored,
) {
output::warn(&format!("Failed to copy untracked files: {}", e));
}
} else {
debug!("Auto-copy disabled");
}
if !self.no_hooks {
debug!("Executing post-create hooks");
if let Err(e) = execute_post_create_hooks(&worktree, base_branch.as_deref(), &config) {
output::warn(&format!("Post-create hook failed: {}", e));
}
} else {
debug!("Hooks skipped (--no-hooks)");
}
Ok(Some(worktree))
}
}
fn prompt_for_base_branch(
repo: &git2::Repository,
config: &workon::WorkonConfig,
) -> Result<Option<String>> {
let branches = repo
.branches(Some(git2::BranchType::Local))
.into_diagnostic()?;
let branch_names: Vec<String> = branches
.filter_map(|b| {
b.ok()
.and_then(|(branch, _)| branch.name().ok().flatten().map(|s| s.to_string()))
})
.collect();
if branch_names.is_empty() {
return config.default_branch(None).map_err(Into::into);
}
let default_branch = config
.default_branch(None)?
.unwrap_or_else(|| "main".to_string());
let mut items = vec![format!("<default: {}>", default_branch)];
items.extend(branch_names.iter().cloned());
let selection = FuzzySelect::new()
.with_prompt("Base branch")
.items(&items)
.default(0)
.interact()
.into_diagnostic()
.wrap_err("Failed to select base branch")?;
if selection == 0 {
Ok(Some(default_branch))
} else {
Ok(Some(branch_names[selection - 1].clone()))
}
}
fn copy_untracked_files(
repo: &git2::Repository,
worktree: &WorktreeDescriptor,
base_branch: Option<&str>,
config: &workon::WorkonConfig,
include_ignored: bool,
) -> Result<()> {
let patterns = config.copy_patterns()?;
let excludes = config.copy_excludes()?;
let include_ignored = config.copy_include_ignored(Some(include_ignored).filter(|&v| v))?;
let source_branch_name = if let Some(base) = base_branch {
base.to_string()
} else {
match repo.head() {
Ok(head) => match head.shorthand() {
Some(s) => s.to_string(),
None => return Ok(()), },
Err(_) => return Ok(()), }
};
let source_path = find_worktree_path(repo, &source_branch_name)?;
let Some(source_path) = source_path else {
return Ok(());
};
let dest_path = worktree.path().to_path_buf();
let json_mode = output::is_json_mode();
let pb = output::create_spinner();
pb.set_message("Copying files...");
let mut count = 0usize;
let pb_copied = pb.clone();
let copied = copy_untracked(
&source_path,
&dest_path,
CopyOptions {
patterns: &patterns,
excludes: &excludes,
include_ignored,
on_copied: Box::new(move |rel_path| {
if !json_mode {
count += 1;
pb_copied.println(format!(
" {} {}",
output::style::green_bold("Copied"),
rel_path.display()
));
pb_copied.set_message(format!("Copying files... ({} copied)", count));
}
}),
..Default::default()
},
)?;
pb.finish_and_clear();
if !copied.is_empty() {
output::success(&format!(
"Copied {} file(s) from base worktree",
copied.len()
));
}
Ok(())
}
fn find_worktree_path(
repo: &git2::Repository,
branch_name: &str,
) -> Result<Option<std::path::PathBuf>> {
let local_name = branch_name
.split_once('/')
.map(|(_, b)| b)
.unwrap_or(branch_name);
if let Ok(worktrees) = get_worktrees(repo) {
for wt in worktrees {
if wt.name() == Some(branch_name) || wt.name() == Some(local_name) {
return Ok(Some(wt.path().to_path_buf()));
}
if let Ok(Some(branch)) = wt.branch() {
if branch == branch_name || branch == local_name {
return Ok(Some(wt.path().to_path_buf()));
}
}
}
}
let root = workon_root(repo)?;
let path = root.join(local_name);
Ok(path.exists().then_some(path))
}