use std::fs;
use std::path::{Path, PathBuf};
use tokio::task::spawn_blocking;
use super::error::GitError;
use super::repo::{main_repo_root_sync, resolve_git_dir, run_git_command_sync};
pub(crate) async fn detect_git_info(dir: PathBuf) -> Option<String> {
spawn_blocking(move || detect_git_info_sync(&dir))
.await
.ok()
.flatten()
}
pub(crate) async fn find_git_repo_root(dir: PathBuf) -> Option<PathBuf> {
spawn_blocking(move || find_git_repo_root_sync(&dir))
.await
.ok()
.flatten()
}
pub(crate) async fn create_worktree(
repo_path: PathBuf,
worktree_path: PathBuf,
branch_name: String,
start_ref: String,
) -> Result<(), GitError> {
spawn_blocking(move || {
let worktree_path = worktree_path.to_string_lossy().to_string();
run_git_command_sync(
&repo_path,
&[
"worktree",
"add",
"-b",
branch_name.as_str(),
worktree_path.as_str(),
start_ref.as_str(),
],
"Git worktree command failed",
)?;
Ok(())
})
.await?
}
pub(crate) async fn remove_worktree(worktree_path: PathBuf) -> Result<(), GitError> {
spawn_blocking(move || {
let repo_root = main_repo_root_sync(&worktree_path)?;
let worktree_path = worktree_path.to_string_lossy().to_string();
run_git_command_sync(
&repo_root,
&["worktree", "remove", "--force", worktree_path.as_str()],
"Git worktree command failed",
)?;
Ok(())
})
.await?
}
pub(super) fn detect_git_info_sync(dir: &Path) -> Option<String> {
let repo_dir = find_git_repo(dir)?;
get_git_branch(&repo_dir)
}
fn find_git_repo(dir: &Path) -> Option<PathBuf> {
find_git_repo_root_sync(dir)
}
fn find_git_repo_root_sync(dir: &Path) -> Option<PathBuf> {
let mut current = dir.to_path_buf();
loop {
let git_dir = current.join(".git");
if git_dir.exists() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
fn get_git_branch(repo_dir: &Path) -> Option<String> {
let git_dir = resolve_git_dir(repo_dir)?;
let head_path = git_dir.join("HEAD");
let content = fs::read_to_string(head_path).ok()?;
let content = content.trim();
if let Some(branch_ref) = content.strip_prefix("ref: refs/heads/") {
return Some(branch_ref.to_string());
}
if content.len() >= 7
&& content
.chars()
.all(|character| character.is_ascii_hexdigit())
{
return Some(format!("HEAD@{}", &content[..7]));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_git_repo_root_sync_finds_repo_at_current_dir() {
let temp_dir = tempfile::tempdir().expect("should create temp dir");
fs::create_dir(temp_dir.path().join(".git")).expect("should create .git");
let result = find_git_repo_root_sync(temp_dir.path());
assert_eq!(result, Some(temp_dir.path().to_path_buf()));
}
#[test]
fn find_git_repo_root_sync_walks_up_to_parent() {
let temp_dir = tempfile::tempdir().expect("should create temp dir");
fs::create_dir(temp_dir.path().join(".git")).expect("should create .git");
let nested = temp_dir.path().join("src").join("lib");
fs::create_dir_all(&nested).expect("should create nested dirs");
let result = find_git_repo_root_sync(&nested);
assert_eq!(result, Some(temp_dir.path().to_path_buf()));
}
#[test]
fn find_git_repo_root_sync_returns_none_when_no_git_dir() {
let temp_dir = tempfile::tempdir().expect("should create temp dir");
let result = find_git_repo_root_sync(temp_dir.path());
assert!(result.is_none());
}
#[test]
fn get_git_branch_returns_branch_from_ref() {
let temp_dir = tempfile::tempdir().expect("should create temp dir");
let git_dir = temp_dir.path().join(".git");
fs::create_dir(&git_dir).expect("should create .git");
fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").expect("should write HEAD");
let result = get_git_branch(temp_dir.path());
assert_eq!(result, Some("main".to_string()));
}
#[test]
fn get_git_branch_returns_detached_head_for_commit_hash() {
let temp_dir = tempfile::tempdir().expect("should create temp dir");
let git_dir = temp_dir.path().join(".git");
fs::create_dir(&git_dir).expect("should create .git");
fs::write(git_dir.join("HEAD"), "abc1234def5678\n").expect("should write HEAD");
let result = get_git_branch(temp_dir.path());
assert_eq!(result, Some("HEAD@abc1234".to_string()));
}
#[test]
fn get_git_branch_returns_none_for_unrecognized_content() {
let temp_dir = tempfile::tempdir().expect("should create temp dir");
let git_dir = temp_dir.path().join(".git");
fs::create_dir(&git_dir).expect("should create .git");
fs::write(git_dir.join("HEAD"), "unknown\n").expect("should write HEAD");
let result = get_git_branch(temp_dir.path());
assert!(result.is_none());
}
#[test]
fn get_git_branch_returns_none_when_no_git_dir() {
let temp_dir = tempfile::tempdir().expect("should create temp dir");
let result = get_git_branch(temp_dir.path());
assert!(result.is_none());
}
#[test]
fn detect_git_info_sync_returns_branch_for_repo() {
let temp_dir = tempfile::tempdir().expect("should create temp dir");
let git_dir = temp_dir.path().join(".git");
fs::create_dir(&git_dir).expect("should create .git");
fs::write(git_dir.join("HEAD"), "ref: refs/heads/feature\n").expect("should write HEAD");
let result = detect_git_info_sync(temp_dir.path());
assert_eq!(result, Some("feature".to_string()));
}
#[test]
fn detect_git_info_sync_returns_none_for_non_repo() {
let temp_dir = tempfile::tempdir().expect("should create temp dir");
let result = detect_git_info_sync(temp_dir.path());
assert!(result.is_none());
}
}