use anyhow::{Context, Result};
use std::path::{Component, Path, PathBuf};
use std::process::Command;
pub fn get_main_repo_root() -> Result<PathBuf> {
let output = Command::new("git")
.args(["rev-parse", "--git-common-dir"])
.output()
.context("Failed to execute git rev-parse")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"Not in a git repository. Please run ofsht from within a git repository.\nGit error: {}",
stderr.trim()
);
}
let git_dir = String::from_utf8_lossy(&output.stdout).trim().to_string();
let git_path = PathBuf::from(&git_dir);
let abs_git_path = if git_path.is_absolute() {
git_path
} else {
std::env::current_dir()?.join(git_path).canonicalize()?
};
let repo_root = abs_git_path
.parent()
.map(PathBuf::from)
.unwrap_or(abs_git_path);
Ok(repo_root)
}
pub fn find_worktree_by_branch(output: &str, branch_name: &str) -> Option<String> {
let mut current_path: Option<String> = None;
let mut worktree_index = 0;
for line in output.lines() {
if line.starts_with("worktree ") {
current_path = line.strip_prefix("worktree ").map(String::from);
worktree_index += 1;
} else if line.starts_with("branch ") {
if let Some(branch_ref) = line.strip_prefix("branch ") {
let branch = branch_ref.strip_prefix("refs/heads/").unwrap_or(branch_ref);
if worktree_index > 1 && branch == branch_name {
return current_path;
}
}
} else if line.is_empty() {
current_path = None;
}
}
None
}
pub fn find_worktree_by_path(output: &str, target_path: &Path) -> Option<String> {
let mut worktree_index = 0;
for line in output.lines() {
if line.starts_with("worktree ") {
let current_path = line.strip_prefix("worktree ").map(String::from);
worktree_index += 1;
if worktree_index > 1 {
if let Some(path) = ¤t_path {
let path_buf = PathBuf::from(path);
let canonical_target = canonicalize_allow_missing(target_path);
let canonical_worktree = canonicalize_allow_missing(&path_buf);
if canonical_target == canonical_worktree {
return current_path;
}
}
}
}
}
None
}
#[cfg(test)]
pub fn is_main_worktree(output: &str, path_or_branch: &str) -> bool {
if path_or_branch == "@" {
return true;
}
let mut main_path: Option<String> = None;
let mut main_branch: Option<String> = None;
let mut worktree_index = 0;
for line in output.lines() {
if line.starts_with("worktree ") {
worktree_index += 1;
if worktree_index == 1 {
main_path = line.strip_prefix("worktree ").map(String::from);
}
} else if line.starts_with("branch ") && worktree_index == 1 {
if let Some(branch_ref) = line.strip_prefix("branch ") {
let branch = branch_ref.strip_prefix("refs/heads/").unwrap_or(branch_ref);
main_branch = Some(branch.to_string());
}
}
if worktree_index > 1 && main_path.is_some() {
break;
}
}
main_path.as_deref() == Some(path_or_branch) || main_branch.as_deref() == Some(path_or_branch)
}
pub fn parse_all_worktrees(output: &str) -> (String, Vec<(String, Option<String>)>) {
let mut main_path = String::new();
let mut worktrees = Vec::new();
let mut worktree_index = 0;
let mut current_path: Option<String> = None;
let mut current_branch: Option<String> = None;
for line in output.lines() {
if line.starts_with("worktree ") {
if let Some(path) = current_path.take() {
if worktree_index == 1 {
main_path = path;
} else {
worktrees.push((path, current_branch.take()));
}
}
worktree_index += 1;
current_path = line.strip_prefix("worktree ").map(String::from);
current_branch = None;
} else if line.starts_with("branch ") {
if let Some(branch_ref) = line.strip_prefix("branch ") {
let branch = branch_ref.strip_prefix("refs/heads/").unwrap_or(branch_ref);
current_branch = Some(branch.to_string());
}
} else if line.is_empty() {
if let Some(path) = current_path.take() {
if worktree_index == 1 {
main_path = path;
} else {
worktrees.push((path, current_branch.take()));
}
}
}
}
if let Some(path) = current_path {
if worktree_index == 1 {
main_path = path;
} else {
worktrees.push((path, current_branch));
}
}
(main_path, worktrees)
}
#[must_use]
pub fn canonicalize_allow_missing(path: &Path) -> PathBuf {
let absolute_path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir().map_or_else(|_| path.to_path_buf(), |cwd| cwd.join(path))
};
let mut normalized = PathBuf::new();
for component in absolute_path.components() {
match component {
Component::CurDir => {
}
Component::ParentDir => {
normalized.pop();
}
_ => {
normalized.push(component);
}
}
}
if let Ok(canonical) = normalized.canonicalize() {
return canonical;
}
let mut current = normalized.as_path();
let mut tail_components = Vec::new();
loop {
if let Some(file_name) = current.file_name() {
tail_components.push(file_name);
}
if let Some(parent) = current.parent() {
if parent.exists() {
if let Ok(canonical_parent) = parent.canonicalize() {
let mut result = canonical_parent;
for component in tail_components.iter().rev() {
result = result.join(component);
}
return result;
}
}
current = parent;
} else {
return normalized;
}
}
}
pub fn resolve_worktree_target(
name: &str,
list_stdout: &str,
_repo_root: &Path,
) -> Result<(PathBuf, PathBuf, Option<String>, bool)> {
let is_current_worktree_removal = name == ".";
let current_path_opt = if is_current_worktree_removal {
let current_output = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.context("Failed to get current worktree path")?;
if !current_output.status.success() {
let stderr = String::from_utf8_lossy(¤t_output.stderr);
anyhow::bail!("Not in a git repository: {stderr}");
}
Some(
String::from_utf8_lossy(¤t_output.stdout)
.trim()
.to_string(),
)
} else {
None
};
let (main_path, worktrees) = parse_all_worktrees(list_stdout);
if name == "@" {
anyhow::bail!("Cannot remove main worktree");
}
let worktree_path: PathBuf;
let branch_name: Option<String>;
let canonical_path: PathBuf;
let branch_match = if !is_current_worktree_removal && name != "@" {
find_worktree_by_branch(list_stdout, name)
} else {
None
};
if let Some(current_path) = current_path_opt {
let current_path_buf = PathBuf::from(¤t_path);
let canonical_current = canonicalize_allow_missing(¤t_path_buf);
let main_path_buf = PathBuf::from(&main_path);
let canonical_main = canonicalize_allow_missing(&main_path_buf);
if canonical_current == canonical_main {
anyhow::bail!("Cannot remove main worktree");
}
let mut current_branch: Option<String> = None;
for (path, branch) in &worktrees {
let path_buf = PathBuf::from(path);
let canonical_wt = canonicalize_allow_missing(&path_buf);
if canonical_wt == canonical_current {
current_branch.clone_from(branch);
break;
}
}
worktree_path = PathBuf::from(current_path);
branch_name = current_branch;
canonical_path = canonical_current;
} else if let Some(path) = branch_match {
worktree_path = PathBuf::from(&path);
branch_name = Some(name.to_string());
let path_buf = PathBuf::from(&path);
canonical_path = canonicalize_allow_missing(&path_buf);
} else {
let input_path_buf = PathBuf::from(name);
let canonical_input = canonicalize_allow_missing(&input_path_buf);
let main_path_buf = PathBuf::from(&main_path);
let canonical_main = canonicalize_allow_missing(&main_path_buf);
if canonical_input == canonical_main {
anyhow::bail!("Cannot remove main worktree");
}
let mut found_worktree = None;
for (path, branch) in &worktrees {
let path_buf = PathBuf::from(path);
let canonical_worktree = canonicalize_allow_missing(&path_buf);
if canonical_input == canonical_worktree {
found_worktree = Some((path.clone(), branch.clone()));
break;
}
}
if let Some((path, branch)) = found_worktree {
worktree_path = PathBuf::from(path);
branch_name = branch;
canonical_path = canonical_input;
} else {
anyhow::bail!("Worktree not found: {name}");
}
}
Ok((
canonical_path,
worktree_path,
branch_name,
is_current_worktree_removal,
))
}