mergegate/crates/miyabi-core/src/git.rs
Shunsuke Hayashi 48dfa915c7 feat: Add hooks, workflow, MCP support and core modules
Phase 2: Hooks System
- Event-driven execution with HookEvent/HookAction types
- HookManager for registration and execution

Phase 3: Multi-Agent Workflow
- Workflow orchestration with dependency graphs
- WorkflowStep with conditions and retry support

Phase 4: MCP (Model Context Protocol) Support
- McpServer for external tool servers
- McpManager for multiple server management

Also includes core modules: cache, error_policy, feature_flags,
git, logger, plugin, retry, rules

662 tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:37:14 +09:00

211 lines
6.6 KiB
Rust

//! Git utilities for repository discovery and validation
//!
//! Provides utilities for working with Git repositories:
//! - Repository root discovery from any subdirectory
//! - Repository validation
//! - Branch detection
use crate::error::{Error, Result};
use std::path::{Path, PathBuf};
/// Find the Git repository root from the current directory or any subdirectory
///
/// This function uses Git's discovery mechanism to walk up the directory tree
/// until it finds a `.git` directory.
///
/// # Examples
///
/// ```no_run
/// use miyabi_core::git::find_git_root;
///
/// let root = find_git_root(None)?;
/// println!("Git root: {:?}", root);
/// # Ok::<(), miyabi_core::error::Error>(())
/// ```
pub fn find_git_root(start_path: Option<&Path>) -> Result<PathBuf> {
let search_path = match start_path {
Some(p) => p.to_path_buf(),
None => std::env::current_dir()?,
};
match git2::Repository::discover(&search_path) {
Ok(repo) => repo
.workdir()
.map(|p| p.to_path_buf())
.ok_or_else(|| {
Error::Git(
"Repository is bare (no working directory). Miyabi requires a non-bare repository.".to_string()
)
}),
Err(e) => Err(Error::Git(format!(
"Not in a Git repository. Please run miyabi from within a Git repository.\n\
Searched from: {:?}\n\
Git error: {}\n\n\
To initialize a new repository, run: git init",
search_path, e
))),
}
}
/// Validate that a path is a valid Git repository
///
/// # Arguments
/// * `path` - Path to validate
///
/// # Returns
/// `true` if the path is a valid Git repository with a working directory
pub fn is_valid_repository(path: impl AsRef<Path>) -> bool {
match git2::Repository::open(path.as_ref()) {
Ok(repo) => repo.workdir().is_some(),
Err(_) => false,
}
}
/// Check if a path is within a Git repository
///
/// # Arguments
/// * `path` - Path to check
///
/// # Returns
/// `true` if the path is within a Git repository
pub fn is_in_git_repo(path: impl AsRef<Path>) -> bool {
git2::Repository::discover(path.as_ref()).is_ok()
}
/// Get the current branch name of a repository
///
/// # Arguments
/// * `repo_path` - Path to the repository
///
/// # Returns
/// The name of the currently checked out branch
///
/// # Errors
/// Returns an error if the repository is in a detached HEAD state or cannot be opened
pub fn get_current_branch(repo_path: impl AsRef<Path>) -> Result<String> {
let repo = git2::Repository::open(repo_path.as_ref())
.map_err(|e| Error::Git(format!("Failed to open repository: {}", e)))?;
let head = repo
.head()
.map_err(|e| Error::Git(format!("Failed to get HEAD: {}", e)))?;
if !head.is_branch() {
return Err(Error::Git(
"Repository is in detached HEAD state. Please checkout a branch.".to_string(),
));
}
head.shorthand()
.map(|s| s.to_string())
.ok_or_else(|| Error::Git("Failed to get branch name".to_string()))
}
/// Get the main branch name (tries 'main' then 'master')
///
/// # Arguments
/// * `repo_path` - Path to the repository
///
/// # Returns
/// The name of the main branch ('main' or 'master')
///
/// # Errors
/// Returns an error if neither 'main' nor 'master' branch exists
pub fn get_main_branch(repo_path: impl AsRef<Path>) -> Result<String> {
let repo = git2::Repository::open(repo_path.as_ref())
.map_err(|e| Error::Git(format!("Failed to open repository: {}", e)))?;
if repo.find_branch("main", git2::BranchType::Local).is_ok() {
Ok("main".to_string())
} else if repo.find_branch("master", git2::BranchType::Local).is_ok() {
Ok("master".to_string())
} else {
Err(Error::Git(
"Neither 'main' nor 'master' branch found. Please create a main branch.".to_string(),
))
}
}
/// Check if the repository has uncommitted changes
///
/// # Arguments
/// * `repo_path` - Path to the repository
///
/// # Returns
/// `true` if there are uncommitted changes in the working directory or index
pub fn has_uncommitted_changes(repo_path: impl AsRef<Path>) -> Result<bool> {
let repo = git2::Repository::open(repo_path.as_ref())
.map_err(|e| Error::Git(format!("Failed to open repository: {}", e)))?;
let statuses = repo
.statuses(None)
.map_err(|e| Error::Git(format!("Failed to get repository status: {}", e)))?;
Ok(!statuses.is_empty())
}
/// Get the short hash of HEAD
pub fn get_head_short_hash(repo_path: impl AsRef<Path>) -> Result<String> {
let repo = git2::Repository::open(repo_path.as_ref())
.map_err(|e| Error::Git(format!("Failed to open repository: {}", e)))?;
let head = repo
.head()
.map_err(|e| Error::Git(format!("Failed to get HEAD: {}", e)))?;
let commit = head
.peel_to_commit()
.map_err(|e| Error::Git(format!("Failed to get commit: {}", e)))?;
Ok(commit.id().to_string()[..7].to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn setup_test_repo() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path().to_path_buf();
git2::Repository::init(&repo_path).unwrap();
(temp_dir, repo_path)
}
#[test]
fn test_find_git_root_from_root() {
let (_temp, repo_path) = setup_test_repo();
let found_root = find_git_root(Some(&repo_path)).unwrap();
let canonical_found = fs::canonicalize(&found_root).unwrap();
let canonical_expected = fs::canonicalize(&repo_path).unwrap();
assert_eq!(canonical_found, canonical_expected);
}
#[test]
fn test_find_git_root_from_subdirectory() {
let (_temp, repo_path) = setup_test_repo();
let sub_dir = repo_path.join("src").join("nested");
fs::create_dir_all(&sub_dir).unwrap();
let found_root = find_git_root(Some(&sub_dir)).unwrap();
let canonical_found = fs::canonicalize(&found_root).unwrap();
let canonical_expected = fs::canonicalize(&repo_path).unwrap();
assert_eq!(canonical_found, canonical_expected);
}
#[test]
fn test_is_valid_repository() {
let (_temp, repo_path) = setup_test_repo();
assert!(is_valid_repository(&repo_path));
let temp_dir = TempDir::new().unwrap();
assert!(!is_valid_repository(temp_dir.path()));
}
#[test]
fn test_is_in_git_repo() {
let (_temp, repo_path) = setup_test_repo();
assert!(is_in_git_repo(&repo_path));
let temp_dir = TempDir::new().unwrap();
assert!(!is_in_git_repo(temp_dir.path()));
}
}