use std::path::PathBuf;
use anyhow::{Context as _, Result};
use crate::cli::AddArgs;
use crate::config::{Hooks, Repo};
use crate::context::Context;
use crate::git;
use crate::hooks::{self, Event};
use crate::worktree;
pub fn run(ctx: &Context, args: &AddArgs) -> Result<()> {
let repo = ctx.repo()?;
let repo_config = ctx.repo_config_optional();
let resolved = resolve_plan(args, repo_config.as_ref())?;
let base_dir = repo_config
.as_ref()
.map_or_else(|| PathBuf::from(".."), |r| r.worktrees_base_dir.clone());
let target = worktree::target_path(&repo, &resolved.name, &base_dir)?;
if args.dry_run {
if ctx.json {
let out = serde_json::json!({
"name": resolved.name,
"path": target.display().to_string(),
"branch": resolved.branch,
"from": resolved.from,
"dry_run": true,
});
println!("{}", serde_json::to_string_pretty(&out)?);
} else if !ctx.quiet {
eprintln!(
"would add worktree: {} at {}",
resolved.name,
target.display()
);
}
return Ok(());
}
let cwd = repo_config
.as_ref()
.map_or_else(|| repo.clone(), |r| r.root.clone());
let event = Event {
cwd: &cwd,
worktree_path: &target,
worktree_name: &resolved.name,
};
hooks::run_required(resolved.hooks.pre_add.as_deref(), "pre_add", &event)?;
ctx.cancel.check()?;
let add_result = worktree::add(
&repo,
&resolved.name,
&base_dir,
worktree::AddOptions {
branch: resolved.branch.as_deref(),
from: resolved.from.as_deref(),
track: args.track,
no_track: args.no_track,
detach: args.detach,
orphan: args.orphan,
lock: args.lock,
reason: args.reason.as_deref(),
force_branch: args.force_branch.as_deref(),
guess_remote: args.guess_remote,
no_guess_remote: args.no_guess_remote,
no_checkout: args.no_checkout,
relative_paths: args.relative_paths,
no_relative_paths: args.no_relative_paths,
quiet: ctx.quiet,
},
);
let created = match add_result {
Ok(path) => path,
Err(e) => {
let target_str = target.to_string_lossy();
let _ = git::run(&repo, &["worktree", "remove", "--force", &target_str]);
let _ = git::run(&repo, &["worktree", "prune"]);
return Err(e);
}
};
hooks::run_best_effort(resolved.hooks.post_add.as_deref(), "post_add", &event);
if ctx.json {
let out = serde_json::json!({
"name": resolved.name,
"path": created.display().to_string(),
"branch": resolved.branch,
"from": resolved.from,
});
println!("{}", serde_json::to_string_pretty(&out)?);
} else if !ctx.quiet {
let ok = crate::style::OK;
anstream::eprintln!("{ok}✓{ok:#} added worktree: {}", created.display());
}
Ok(())
}
struct Plan {
name: String,
branch: Option<String>,
from: Option<String>,
hooks: Hooks,
}
fn resolve_plan(args: &AddArgs, repo_config: Option<&Repo>) -> Result<Plan> {
let explicit_from = args.from.clone().or_else(|| args.base.clone());
if let Some(template_name) = args.template.as_ref() {
let config = repo_config.context(
"--template given but no .limb.toml in this repo; \
try: add `[templates.<name>]` to .limb.toml",
)?;
let template = config.templates.get(template_name).with_context(|| {
let known: Vec<&str> = config.templates.keys().map(String::as_str).collect();
let known = if known.is_empty() {
"no templates defined".to_string()
} else {
format!("known: {}", known.join(", "))
};
format!("unknown template: {template_name}; {known}")
})?;
let name = template
.name_pattern
.as_deref()
.map_or_else(|| args.name.clone(), |p| interpolate(p, &args.name));
Ok(Plan {
name,
branch: args.branch.clone(),
from: explicit_from.or_else(|| template.base_branch.clone()),
hooks: template.hooks.clone(),
})
} else {
Ok(Plan {
name: args.name.clone(),
branch: args.branch.clone(),
from: explicit_from,
hooks: repo_config.map_or_else(Hooks::default, |r| r.hooks.clone()),
})
}
}
fn interpolate(pattern: &str, slug: &str) -> String {
const PLACEHOLDER: &str = "{slug}";
pattern.replace(PLACEHOLDER, slug)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interpolate_replaces_slug() {
assert_eq!(interpolate("feat-{slug}", "auth"), "feat-auth");
assert_eq!(interpolate("{slug}", "x"), "x");
assert_eq!(interpolate("no-placeholder", "x"), "no-placeholder");
}
}