use std::path::{Path, PathBuf};
use tracing::{debug, warn};
pub async fn get_worktrees(
repo_root: &Path,
) -> crate::error::Result<Vec<crate::tui::types::WorktreeInfo>> {
let worktrees_data = crate::vcs::git::commands::list_worktrees(repo_root).await?;
let mut worktrees: Vec<crate::tui::types::WorktreeInfo> = worktrees_data
.into_iter()
.map(
|(path, head, branch, is_detached, is_main)| crate::tui::types::WorktreeInfo {
path: PathBuf::from(path),
head,
branch: branch.clone(),
is_detached,
is_main,
merge_conflict: None,
has_commits_ahead: false,
is_merging: false,
},
)
.collect();
let base_branch = if let Some(main_wt) = worktrees.iter().find(|wt| wt.is_main) {
main_wt.branch.clone()
} else {
match crate::vcs::git::commands::get_current_branch(repo_root).await? {
Some(branch) => branch,
None => {
return Ok(worktrees);
}
}
};
let mut tasks = tokio::task::JoinSet::new();
for (idx, worktree) in worktrees.iter().enumerate() {
if worktree.is_main || worktree.is_detached || worktree.branch.is_empty() {
continue;
}
let wt_path = worktree.path.clone();
let branch_name = worktree.branch.clone();
let base_branch_clone = base_branch.clone();
tasks.spawn(async move {
let conflict_result =
crate::vcs::git::commands::check_merge_conflicts(&wt_path, &base_branch_clone)
.await;
let ahead_result = crate::vcs::git::commands::count_commits_ahead(
&wt_path,
&base_branch_clone,
&branch_name,
)
.await;
(idx, conflict_result, ahead_result)
});
}
while let Some(result) = tasks.join_next().await {
match result {
Ok((idx, conflict_result, ahead_result)) => {
match conflict_result {
Ok(conflict_files_opt) => {
if let Some(conflict_files) = conflict_files_opt {
worktrees[idx].merge_conflict =
Some(crate::tui::types::MergeConflictInfo { conflict_files });
}
}
Err(e) => {
debug!(
"Conflict check failed for worktree {}: {}",
worktrees[idx].path.display(),
e
);
}
}
match ahead_result {
Ok(count) => {
worktrees[idx].has_commits_ahead = count > 0;
}
Err(e) => {
debug!(
"Commits ahead check failed for worktree {}: {}",
worktrees[idx].path.display(),
e
);
}
}
}
Err(e) => {
warn!("Worktree check task panicked: {}", e);
}
}
}
Ok(worktrees)
}
pub async fn can_delete_worktree(
worktree: &crate::tui::types::WorktreeInfo,
) -> (bool, Option<String>) {
if worktree.is_main {
return (false, Some("Cannot delete main worktree".to_string()));
}
match crate::vcs::git::commands::has_uncommitted_changes(&worktree.path).await {
Ok((has_changes, _)) if has_changes => {
return (false, Some("Worktree has uncommitted changes".to_string()));
}
Err(e) => {
warn!("Failed to check uncommitted changes: {}", e);
}
_ => {}
}
if worktree.has_commits_ahead {
return (
false,
Some("Worktree has unmerged commits ahead of base branch".to_string()),
);
}
(true, None)
}
pub fn can_merge_worktree(worktree: &crate::tui::types::WorktreeInfo) -> (bool, Option<String>) {
if worktree.is_main {
return (false, Some("Cannot merge main worktree".to_string()));
}
if worktree.merge_conflict.is_some() {
return (false, Some("Worktree has merge conflicts".to_string()));
}
if !worktree.has_commits_ahead {
return (
false,
Some("Worktree has no commits ahead of base".to_string()),
);
}
(true, None)
}
pub async fn worktree_exists(repo_root: &Path, branch_name: &str) -> crate::error::Result<bool> {
let worktrees = get_worktrees(repo_root).await?;
Ok(worktrees.iter().any(|wt| wt.branch == branch_name))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_can_delete_main_worktree() {
let worktree = crate::tui::types::WorktreeInfo {
path: PathBuf::from("/repo"),
head: "abc123".to_string(),
branch: "main".to_string(),
is_detached: false,
is_main: true,
merge_conflict: None,
has_commits_ahead: false,
is_merging: false,
};
let (can_delete, reason) = can_delete_worktree(&worktree).await;
assert!(!can_delete);
assert!(reason.is_some());
assert!(reason.unwrap().contains("main worktree"));
}
#[test]
fn test_can_merge_main_worktree() {
let worktree = crate::tui::types::WorktreeInfo {
path: PathBuf::from("/repo"),
head: "abc123".to_string(),
branch: "main".to_string(),
is_detached: false,
is_main: true,
merge_conflict: None,
has_commits_ahead: true,
is_merging: false,
};
let (can_merge, reason) = can_merge_worktree(&worktree);
assert!(!can_merge);
assert!(reason.is_some());
assert!(reason.unwrap().contains("main worktree"));
}
#[test]
fn test_can_merge_with_conflicts() {
let worktree = crate::tui::types::WorktreeInfo {
path: PathBuf::from("/repo/wt1"),
head: "abc123".to_string(),
branch: "feature-1".to_string(),
is_detached: false,
is_main: false,
merge_conflict: Some(crate::tui::types::MergeConflictInfo {
conflict_files: vec!["file.txt".to_string()],
}),
has_commits_ahead: true,
is_merging: false,
};
let (can_merge, reason) = can_merge_worktree(&worktree);
assert!(!can_merge);
assert!(reason.is_some());
assert!(reason.unwrap().contains("merge conflicts"));
}
#[test]
fn test_can_merge_no_commits_ahead() {
let worktree = crate::tui::types::WorktreeInfo {
path: PathBuf::from("/repo/wt1"),
head: "abc123".to_string(),
branch: "feature-1".to_string(),
is_detached: false,
is_main: false,
merge_conflict: None,
has_commits_ahead: false,
is_merging: false,
};
let (can_merge, reason) = can_merge_worktree(&worktree);
assert!(!can_merge);
assert!(reason.is_some());
assert!(reason.unwrap().contains("no commits ahead"));
}
#[test]
fn test_can_merge_valid() {
let worktree = crate::tui::types::WorktreeInfo {
path: PathBuf::from("/repo/wt1"),
head: "abc123".to_string(),
branch: "feature-1".to_string(),
is_detached: false,
is_main: false,
merge_conflict: None,
has_commits_ahead: true,
is_merging: false,
};
let (can_merge, reason) = can_merge_worktree(&worktree);
assert!(can_merge);
assert!(reason.is_none());
}
}