use anyhow::{Context, Result, anyhow};
use regex::Regex;
use tracing::{info, warn};
use crate::config::MuxMode;
use crate::multiplexer::util::prefixed;
use crate::state::StateStore;
use crate::util::canon_or_self;
use crate::{git, naming};
use super::context::WorkflowContext;
use super::types::RenameResult;
pub fn rename(
user_target: &str,
new_name: &str,
rename_branch: bool,
context: &WorkflowContext,
) -> Result<RenameResult> {
let (old_path, branch_name) = git::find_worktree(user_target).with_context(|| {
format!(
"Worktree '{}' not found. Use 'workmux list' to see available worktrees.",
user_target
)
})?;
let old_handle = old_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| {
anyhow!(
"Could not derive handle from worktree path: {}",
old_path.display()
)
})?
.to_string();
if old_path == context.main_worktree_root {
return Err(anyhow!("Cannot rename the main worktree"));
}
let new_handle = naming::derive_handle(&branch_name, Some(new_name), &context.config)?;
if new_handle == old_handle && !rename_branch {
return Err(anyhow!(
"Nothing to rename: new name '{}' matches current handle",
new_handle
));
}
if rename_branch && branch_name == "(detached)" {
return Err(anyhow!(
"Cannot rename the branch of a detached HEAD worktree. \
Omit --branch to rename only the worktree and tmux window."
));
}
let parent = old_path
.parent()
.ok_or_else(|| anyhow!("Cannot determine parent directory of worktree"))?;
let new_path = parent.join(&new_handle);
let new_branch = if rename_branch {
Some(new_handle.clone())
} else {
None
};
if new_handle != old_handle {
if new_path.exists() {
return Err(anyhow!(
"Target path already exists: {}",
new_path.display()
));
}
if git::find_worktree(&new_handle).is_ok() {
return Err(anyhow!(
"Another worktree with handle '{}' already exists",
new_handle
));
}
}
if let Some(ref b) = new_branch
&& b != &branch_name
&& git::branch_exists(b).unwrap_or(false)
{
return Err(anyhow!("Branch '{}' already exists", b));
}
let mode = git::get_worktree_mode(&old_handle);
let old_full = prefixed(&context.prefix, &old_handle);
let new_full = prefixed(&context.prefix, &new_handle);
let mux_running = context.mux.is_running().unwrap_or(false);
if mux_running && new_handle != old_handle {
match mode {
MuxMode::Session => {
if context.mux.session_exists(&new_full)? {
return Err(anyhow!("tmux session '{}' already exists", new_full));
}
}
MuxMode::Window => {
let all = context.mux.get_all_window_names()?;
let re = duplicate_name_regex(&new_full);
if all.iter().any(|w| re.is_match(w)) {
return Err(anyhow!(
"tmux window '{}' (or a numbered duplicate) already exists",
new_full
));
}
}
}
}
info!(
old_handle = %old_handle,
new_handle = %new_handle,
rename_branch,
"rename:starting"
);
let old_canonical = canon_or_self(&old_path);
context.chdir_to_main_worktree()?;
if new_handle != old_handle {
git::move_worktree(&old_path, &new_path)
.context("Failed to move worktree (is the directory in use?)")?;
info!(from = %old_path.display(), to = %new_path.display(), "rename:worktree moved");
}
if let Some(ref nb) = new_branch
&& nb != &branch_name
{
git::rename_branch(&branch_name, nb)?;
info!(old = branch_name, new = nb, "rename:branch renamed");
}
if new_handle != old_handle
&& let Err(e) = git::migrate_worktree_meta(&old_handle, &new_handle)
{
warn!(error = %e, "rename:failed to migrate worktree metadata");
}
let mut tmux_renamed = 0;
if mux_running && new_handle != old_handle {
match mode {
MuxMode::Session => {
if context.mux.session_exists(&old_full).unwrap_or(false) {
match context.mux.rename_session(&old_full, &new_full) {
Ok(()) => {
tmux_renamed += 1;
info!(old = %old_full, new = %new_full, "rename:session renamed");
}
Err(e) => {
warn!(error = %e, "rename:failed to rename tmux session");
}
}
}
}
MuxMode::Window => {
let all = context.mux.get_all_window_names().unwrap_or_default();
let re = duplicate_name_regex(&old_full);
let matches: Vec<String> = all.into_iter().filter(|w| re.is_match(w)).collect();
for old_name in &matches {
let new_name = remap_duplicate_name(old_name, &old_full, &new_full);
if let Err(e) = context.mux.rename_window(old_name, &new_name) {
warn!(window = old_name, error = %e, "rename:tmux rename_window failed");
} else {
tmux_renamed += 1;
info!(old = old_name, new = new_name, "rename:window renamed");
}
}
}
}
}
let agents_migrated = match StateStore::new() {
Ok(store) => {
let migrated = store
.migrate_worktree_paths(&old_canonical, &new_path, &old_full, &new_full)
.unwrap_or_else(|e| {
warn!(error = %e, "rename:failed to migrate agent state");
0
});
if new_handle != old_handle
&& let Err(e) = store.migrate_container_handle(&old_handle, &new_handle)
{
warn!(error = %e, "rename:failed to migrate container markers");
}
migrated
}
Err(e) => {
warn!(error = %e, "rename:state store unavailable, skipping state migration");
0
}
};
Ok(RenameResult {
old_path,
new_path,
old_handle,
new_handle,
old_branch: branch_name,
new_branch,
tmux_renamed,
agents_migrated,
})
}
fn duplicate_name_regex(base: &str) -> Regex {
let pattern = format!(r"^{}(-\d+)?$", regex::escape(base));
Regex::new(&pattern).expect("static regex pattern")
}
fn remap_duplicate_name(name: &str, old_base: &str, new_base: &str) -> String {
if name == old_base {
return new_base.to_string();
}
if let Some(suffix) = name.strip_prefix(&format!("{}-", old_base))
&& !suffix.is_empty()
&& suffix.chars().all(|c| c.is_ascii_digit())
{
return format!("{}-{}", new_base, suffix);
}
name.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remap_exact_match() {
assert_eq!(remap_duplicate_name("wm-old", "wm-old", "wm-new"), "wm-new");
}
#[test]
fn remap_numeric_suffix() {
assert_eq!(
remap_duplicate_name("wm-old-2", "wm-old", "wm-new"),
"wm-new-2"
);
assert_eq!(
remap_duplicate_name("wm-old-42", "wm-old", "wm-new"),
"wm-new-42"
);
}
#[test]
fn remap_non_matching_unchanged() {
assert_eq!(remap_duplicate_name("other", "wm-old", "wm-new"), "other");
assert_eq!(
remap_duplicate_name("wm-old-abc", "wm-old", "wm-new"),
"wm-old-abc"
);
}
#[test]
fn duplicate_regex_matches_base_and_suffixes() {
let re = duplicate_name_regex("wm-feature");
assert!(re.is_match("wm-feature"));
assert!(re.is_match("wm-feature-2"));
assert!(re.is_match("wm-feature-99"));
assert!(!re.is_match("wm-feature-abc"));
assert!(!re.is_match("wm-feature-x"));
assert!(!re.is_match("wm-feature2"));
assert!(!re.is_match("other"));
}
}