use std::path::PathBuf;
use git2::Repository;
use crate::error::{Error, Result};
#[derive(Debug)]
pub struct WorktreeStatus {
pub name: String,
pub path: PathBuf,
pub branch: Option<String>,
pub unmerged_commits: usize,
pub has_uncommitted_changes: bool,
pub removable: bool,
}
pub fn analyze_worktrees(repo: &Repository) -> Result<Vec<WorktreeStatus>> {
let repo_path = repo
.workdir()
.ok_or_else(|| Error::NotRepo(PathBuf::from("bare repository")))?;
let worktree_names = repo.worktrees().map_err(|e| Error::Git {
path: repo_path.to_path_buf(),
source: e,
})?;
let main_oid = resolve_main_oid(repo, repo_path)?;
let mut results = Vec::new();
for name in worktree_names.iter().flatten() {
let wt = match repo.find_worktree(name) {
Ok(wt) => wt,
Err(_) => continue,
};
let wt_path = wt.path().to_path_buf();
let wt_repo = match Repository::open(&wt_path) {
Ok(r) => r,
Err(_) => continue,
};
let branch = current_branch_name(&wt_repo);
let unmerged_commits = count_unmerged_commits(&wt_repo, main_oid);
let has_uncommitted_changes = has_changes(&wt_repo);
let removable = unmerged_commits == 0 && !has_uncommitted_changes;
results.push(WorktreeStatus {
name: name.to_string(),
path: wt_path,
branch,
unmerged_commits,
has_uncommitted_changes,
removable,
});
}
Ok(results)
}
pub fn remove_worktree(repo: &Repository, name: &str) -> Result<()> {
let repo_path = repo.workdir().unwrap_or_else(|| repo.path()).to_path_buf();
let wt = repo.find_worktree(name).map_err(|e| Error::Git {
path: repo_path.clone(),
source: e,
})?;
let wt_path = wt.path().to_path_buf();
if wt_path.is_dir() {
std::fs::remove_dir_all(&wt_path)?;
}
wt.prune(Some(
&mut git2::WorktreePruneOptions::new()
.working_tree(true)
.valid(false),
))
.map_err(|e| Error::Git {
path: repo_path,
source: e,
})?;
Ok(())
}
fn resolve_main_oid(repo: &Repository, repo_path: &std::path::Path) -> Result<Option<git2::Oid>> {
for branch_name in &["main", "master"] {
if let Ok(reference) = repo.find_branch(branch_name, git2::BranchType::Local) {
if let Some(oid) = reference.get().target() {
return Ok(Some(oid));
}
}
}
match repo.head() {
Ok(head) => Ok(head.target()),
Err(e) => Err(Error::Git {
path: repo_path.to_path_buf(),
source: e,
}),
}
}
fn current_branch_name(repo: &Repository) -> Option<String> {
let head = repo.head().ok()?;
if head.is_branch() {
head.shorthand().map(|s| s.to_string())
} else {
Some(format!(
"(detached {})",
head.target()
.map(|oid| oid.to_string()[..7].to_string())
.unwrap_or_else(|| "?".to_string())
))
}
}
fn count_unmerged_commits(wt_repo: &Repository, main_oid: Option<git2::Oid>) -> usize {
let main_oid = match main_oid {
Some(oid) => oid,
None => return 0,
};
let head_oid = match wt_repo.head().ok().and_then(|h| h.target()) {
Some(oid) => oid,
None => return 0,
};
if head_oid == main_oid {
return 0;
}
let merge_base = match wt_repo.merge_base(main_oid, head_oid) {
Ok(oid) => oid,
Err(_) => return 1, };
if merge_base == head_oid {
return 0;
}
let mut revwalk = match wt_repo.revwalk() {
Ok(rw) => rw,
Err(_) => return 1,
};
if revwalk.push(head_oid).is_err() {
return 1;
}
if revwalk.hide(merge_base).is_err() {
return 1;
}
revwalk.count()
}
fn has_changes(repo: &Repository) -> bool {
let statuses = match repo.statuses(Some(
git2::StatusOptions::new()
.include_untracked(true)
.recurse_untracked_dirs(false),
)) {
Ok(s) => s,
Err(_) => return true, };
!statuses.is_empty()
}