use anyhow::{Context, Result, anyhow};
use std::path::{Path, PathBuf};
use crate::cmd::Cmd;
use crate::config::MuxMode;
use super::WorktreeNotFound;
use super::branch::unset_branch_upstream;
pub fn worktree_exists(branch_name: &str) -> Result<bool> {
match get_worktree_path(branch_name) {
Ok(_) => Ok(true),
Err(e) => {
if e.is::<WorktreeNotFound>() {
Ok(false)
} else {
Err(e)
}
}
}
}
pub fn create_worktree(
worktree_path: &Path,
branch_name: &str,
create_branch: bool,
base_branch: Option<&str>,
track_upstream: bool,
) -> Result<()> {
let path_str = worktree_path
.to_str()
.ok_or_else(|| anyhow!("Invalid worktree path"))?;
let mut cmd = Cmd::new("git").arg("worktree").arg("add");
if create_branch {
cmd = cmd.arg("-b").arg(branch_name).arg(path_str);
if let Some(base) = base_branch {
cmd = cmd.arg(base);
}
} else {
cmd = cmd.arg(path_str).arg(branch_name);
}
cmd.run().context("Failed to create worktree")?;
if create_branch && !track_upstream {
unset_branch_upstream(branch_name)?;
}
Ok(())
}
pub fn prune_worktrees_in(git_common_dir: &Path) -> Result<()> {
Cmd::new("git")
.workdir(git_common_dir)
.args(&["worktree", "prune"])
.run()
.context("Failed to prune worktrees")?;
Ok(())
}
pub(super) fn parse_worktree_list_porcelain(output: &str) -> Result<Vec<(PathBuf, String)>> {
let mut worktrees = Vec::new();
for block in output.trim().split("\n\n") {
let mut path: Option<PathBuf> = None;
let mut branch: Option<String> = None;
for line in block.lines() {
if let Some(p) = line.strip_prefix("worktree ") {
path = Some(PathBuf::from(p));
} else if let Some(b) = line.strip_prefix("branch refs/heads/") {
branch = Some(b.to_string());
} else if line.trim() == "detached" {
branch = Some("(detached)".to_string());
}
}
if let (Some(p), Some(b)) = (path, branch) {
worktrees.push((p, b));
}
}
Ok(worktrees)
}
pub fn get_worktree_path(branch_name: &str) -> Result<PathBuf> {
let list_str = Cmd::new("git")
.args(&["worktree", "list", "--porcelain"])
.run_and_capture_stdout()
.context("Failed to list worktrees while locating worktree path")?;
let worktrees = parse_worktree_list_porcelain(&list_str)?;
for (path, branch) in worktrees {
if branch == branch_name {
return Ok(path);
}
}
Err(WorktreeNotFound(branch_name.to_string()).into())
}
pub fn find_worktree(name: &str) -> Result<(PathBuf, String)> {
let list_str = Cmd::new("git")
.args(&["worktree", "list", "--porcelain"])
.run_and_capture_stdout()
.context("Failed to list worktrees")?;
let worktrees = parse_worktree_list_porcelain(&list_str)?;
for (path, branch) in &worktrees {
if let Some(dir_name) = path.file_name()
&& dir_name.to_string_lossy() == name
{
return Ok((path.clone(), branch.clone()));
}
}
for (path, branch) in worktrees {
if branch == name {
return Ok((path, branch));
}
}
Err(WorktreeNotFound(name.to_string()).into())
}
pub fn list_worktrees() -> Result<Vec<(PathBuf, String)>> {
list_worktrees_in(None)
}
pub fn list_worktrees_in(workdir: Option<&Path>) -> Result<Vec<(PathBuf, String)>> {
let cmd = Cmd::new("git").args(&["worktree", "list", "--porcelain"]);
let cmd = match workdir {
Some(path) => cmd.workdir(path),
None => cmd,
};
let list = cmd
.run_and_capture_stdout()
.context("Failed to list worktrees")?;
parse_worktree_list_porcelain(&list)
}
pub fn set_worktree_meta(handle: &str, key: &str, value: &str) -> Result<()> {
Cmd::new("git")
.args(&[
"config",
"--local",
&format!("workmux.worktree.{}.{}", handle, key),
value,
])
.run()
.with_context(|| format!("Failed to set worktree metadata {}.{}", handle, key))?;
Ok(())
}
pub fn get_worktree_meta(handle: &str, key: &str) -> Option<String> {
Cmd::new("git")
.args(&[
"config",
"--local",
"--get",
&format!("workmux.worktree.{}.{}", handle, key),
])
.run_and_capture_stdout()
.ok()
.filter(|s| !s.is_empty())
}
pub fn get_worktree_mode_opt(handle: &str) -> Option<MuxMode> {
match get_worktree_meta(handle, "mode") {
Some(mode) if mode == "session" => Some(MuxMode::Session),
Some(mode) if mode == "window" => Some(MuxMode::Window),
_ => None,
}
}
pub fn get_worktree_mode(handle: &str) -> MuxMode {
get_worktree_mode_opt(handle).unwrap_or(MuxMode::Window)
}
pub fn get_all_worktree_modes_in(
workdir: Option<&Path>,
) -> std::collections::HashMap<String, MuxMode> {
let cmd = Cmd::new("git").args(&[
"config",
"--local",
"--get-regexp",
r"^workmux\.worktree\..*\.mode$",
]);
let cmd = match workdir {
Some(path) => cmd.workdir(path),
None => cmd,
};
let output = cmd.run_and_capture_stdout().unwrap_or_default();
let mut modes = std::collections::HashMap::new();
for line in output.lines() {
let parts: Vec<&str> = line.splitn(2, ' ').collect();
if parts.len() == 2 {
let key = parts[0];
let value = parts[1].trim();
if let Some(rest) = key.strip_prefix("workmux.worktree.")
&& let Some(handle) = rest.strip_suffix(".mode")
{
let mode = if value == "session" {
MuxMode::Session
} else {
MuxMode::Window
};
modes.insert(handle.to_string(), mode);
}
}
}
modes
}
pub fn remove_worktree_meta(handle: &str) -> Result<()> {
let _ = Cmd::new("git")
.args(&[
"config",
"--local",
"--remove-section",
&format!("workmux.worktree.{}", handle),
])
.run();
Ok(())
}
pub fn get_main_worktree_root() -> Result<PathBuf> {
let list_str = Cmd::new("git")
.args(&["worktree", "list", "--porcelain"])
.run_and_capture_stdout()
.context("Failed to list worktrees while locating main worktree")?;
if let Some(first_block) = list_str.trim().split("\n\n").next() {
let mut path: Option<PathBuf> = None;
let mut is_bare = false;
for line in first_block.lines() {
if let Some(p) = line.strip_prefix("worktree ") {
path = Some(PathBuf::from(p));
} else if line.trim() == "bare" {
is_bare = true;
}
}
if is_bare && let Some(p) = path {
return Ok(p);
}
}
let worktrees = parse_worktree_list_porcelain(&list_str)?;
for (path, _) in &worktrees {
if path.exists() {
return Ok(path.clone());
}
}
if let Some((path, _)) = worktrees.first() {
Ok(path.clone())
} else {
Err(anyhow!("No main worktree found"))
}
}