use dialoguer::{FuzzySelect, Input};
use git2::BranchType as GitBranchType;
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, current_stack, current_worktree, get_repo, get_worktrees,
graphite_trunk, workon_root, BranchType, CopyOptions, StackModel, WorkonConfig,
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 = WorkonConfig::new(&repo)?;
let effective_model = if self.no_stack {
StackModel::None
} else {
config.stack_model(None)?
};
if self.branch.is_some() && (self.orphan || self.detach) {
bail!("--branch cannot be combined with --orphan or --detach");
}
let pr_info =
if !self.orphan && !self.detach && self.base.is_none() && self.branch.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,
None,
BranchType::Normal,
Some(&remote_ref),
self.lock,
)
.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 {
Some(true)
} else if self.no_copy {
Some(false)
} else {
None
};
if config.auto_copy(copy_override)? {
if let Err(e) = copy_untracked_files(
&repo,
&worktree,
Some(&base_ref),
&config,
self.no_copy_ignored,
) {
output::warn(&format!("Failed to copy local 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 if effective_model != StackModel::None {
match current_stack_branch(&repo, effective_model)? {
Some(branch) => {
debug!("Stack-aware: defaulting base to current branch: {}", branch);
Some(branch)
}
None => {
debug!("Not in a stack-worktree, using config default branch");
config.default_branch(None)?
}
}
} 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 (effective_branch, worktree_alias) = match &self.branch {
Some(branch) => {
if self.base.is_some() && repo.find_branch(branch, GitBranchType::Local).is_ok() {
output::warn(&format!(
"Branch '{}' already exists; --base ignored",
branch
));
}
(branch.clone(), Some(worktree_name.clone()))
}
None => {
if self.base.is_some()
&& repo
.find_branch(&worktree_name, GitBranchType::Local)
.is_ok()
{
output::warn(&format!(
"Branch '{}' already exists; --base ignored",
&worktree_name
));
}
(worktree_name.clone(), None)
}
};
let worktree = add_worktree(
&repo,
&effective_branch,
worktree_alias.as_deref(),
branch_type,
base_branch.as_deref(),
self.lock,
)
.wrap_err(format!("Failed to create worktree '{}'", effective_branch))?;
let already_tracked = current_stack(&repo, &effective_branch, effective_model)?.is_some();
if effective_model == StackModel::Graphite
&& !self.no_stack
&& !already_tracked
&& config.gt_auto_track(None)?
{
let parent = base_branch
.as_deref()
.map(String::from)
.or_else(|| graphite_trunk(&repo));
debug!(
"Running: gt track{} in {}",
parent
.as_deref()
.map(|p| format!(" --parent {p}"))
.unwrap_or_default(),
worktree.path().display()
);
let mut cmd = std::process::Command::new("gt");
cmd.arg("track");
if let Some(p) = &parent {
cmd.arg("--parent").arg(p);
}
match cmd.current_dir(worktree.path()).output() {
Ok(out) if out.status.success() => {
debug!("gt track succeeded");
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
output::warn(&format!("gt track failed: {}", stderr.trim()));
}
Err(e) => {
output::warn(&format!("gt track unavailable: {}", e));
}
}
}
let copy_override = if self.copy {
Some(true)
} else if self.no_copy {
Some(false)
} else {
None
};
if config.auto_copy(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.no_copy_ignored,
) {
output::warn(&format!("Failed to copy local 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 current_stack_branch(repo: &git2::Repository, model: StackModel) -> Result<Option<String>> {
let wt = match current_worktree(repo) {
Ok(wt) => wt,
Err(_) => return Ok(None),
};
let branch = match wt.branch()? {
Some(b) => b,
None => return Ok(None),
};
match current_stack(repo, &branch, model)? {
Some(_) => Ok(Some(branch)),
None => Ok(None),
}
}
fn prompt_for_base_branch(
repo: &git2::Repository,
config: &workon::WorkonConfig,
) -> Result<Option<String>> {
let branches = repo
.branches(Some(GitBranchType::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,
no_copy_ignored: bool,
) -> Result<()> {
let patterns = config.copy_patterns()?;
let excludes = config.copy_excludes()?;
let include_ignored = config.copy_include_ignored(no_copy_ignored.then_some(false))?;
let source_branch_name = if let Some(base) = base_branch {
base.to_string()
} else {
match repo.head() {
Ok(head) => match head.shorthand() {
Ok(s) => s.to_string(),
Err(_) => 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))
}