use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use crate::communication::{AgentMessage, CommunicationHub, GitOperationType};
use crate::resource_checker::ResourceChecker;
use crate::resource_locks::{ResourceLockGuard, ResourceLockManager, ResourceScope, ResourceType};
pub mod git_tools {
pub const STATUS: &str = "git_status";
pub const DIFF: &str = "git_diff";
pub const LOG: &str = "git_log";
pub const SEARCH: &str = "git_search";
pub const FETCH: &str = "git_fetch";
pub const STAGE: &str = "git_stage";
pub const UNSTAGE: &str = "git_unstage";
pub const COMMIT: &str = "git_commit";
pub const PUSH: &str = "git_push";
pub const PULL: &str = "git_pull";
pub const BRANCH: &str = "git_branch";
pub const DISCARD: &str = "git_discard";
}
#[derive(Debug, Clone)]
pub struct GitLockRequirements {
pub resource_types: Vec<ResourceType>,
pub check_file_conflicts: bool,
pub check_build_conflicts: bool,
pub operation_type: GitOperationType,
pub description: &'static str,
}
impl GitLockRequirements {
pub fn is_read_only(&self) -> bool {
self.resource_types.is_empty()
}
}
pub fn get_lock_requirements(tool_name: &str) -> GitLockRequirements {
match tool_name {
git_tools::STATUS | git_tools::DIFF | git_tools::LOG | git_tools::SEARCH => {
GitLockRequirements {
resource_types: vec![],
check_file_conflicts: false,
check_build_conflicts: false,
operation_type: GitOperationType::ReadOnly,
description: "Read-only git operation",
}
}
git_tools::FETCH => GitLockRequirements {
resource_types: vec![],
check_file_conflicts: false,
check_build_conflicts: false,
operation_type: GitOperationType::ReadOnly,
description: "Fetch from remote",
},
git_tools::STAGE | git_tools::UNSTAGE => GitLockRequirements {
resource_types: vec![ResourceType::GitIndex],
check_file_conflicts: true, check_build_conflicts: false,
operation_type: GitOperationType::Staging,
description: "Staging area modification",
},
git_tools::COMMIT => GitLockRequirements {
resource_types: vec![ResourceType::GitIndex, ResourceType::GitCommit],
check_file_conflicts: true, check_build_conflicts: true, operation_type: GitOperationType::Commit,
description: "Create commit",
},
git_tools::PUSH => GitLockRequirements {
resource_types: vec![ResourceType::GitRemoteWrite],
check_file_conflicts: false,
check_build_conflicts: true, operation_type: GitOperationType::RemoteWrite,
description: "Push to remote",
},
git_tools::PULL => GitLockRequirements {
resource_types: vec![ResourceType::GitRemoteMerge, ResourceType::GitIndex],
check_file_conflicts: true, check_build_conflicts: true, operation_type: GitOperationType::RemoteMerge,
description: "Pull from remote",
},
git_tools::BRANCH => GitLockRequirements {
resource_types: vec![ResourceType::GitBranch],
check_file_conflicts: false,
check_build_conflicts: false,
operation_type: GitOperationType::Branch,
description: "Branch operation",
},
git_tools::DISCARD => GitLockRequirements {
resource_types: vec![ResourceType::GitDestructive, ResourceType::GitIndex],
check_file_conflicts: true, check_build_conflicts: true, operation_type: GitOperationType::Destructive,
description: "Discard changes (destructive)",
},
_ => GitLockRequirements {
resource_types: vec![],
check_file_conflicts: false,
check_build_conflicts: false,
operation_type: GitOperationType::ReadOnly,
description: "Unknown git operation",
},
}
}
pub struct GitCoordinator {
resource_locks: Arc<ResourceLockManager>,
resource_checker: Option<Arc<ResourceChecker>>,
communication_hub: Option<Arc<CommunicationHub>>,
project_root: PathBuf,
}
impl GitCoordinator {
pub fn new(resource_locks: Arc<ResourceLockManager>, project_root: PathBuf) -> Self {
Self {
resource_locks,
resource_checker: None,
communication_hub: None,
project_root,
}
}
pub fn with_full_integration(
resource_locks: Arc<ResourceLockManager>,
resource_checker: Arc<ResourceChecker>,
communication_hub: Arc<CommunicationHub>,
project_root: PathBuf,
) -> Self {
Self {
resource_locks,
resource_checker: Some(resource_checker),
communication_hub: Some(communication_hub),
project_root,
}
}
pub fn project_scope(&self) -> ResourceScope {
ResourceScope::Project(self.project_root.clone())
}
#[tracing::instrument(name = "agent.git.acquire", skip(self))]
pub async fn acquire_for_git_op(
&self,
agent_id: &str,
tool_name: &str,
) -> Result<GitOperationLocks> {
let requirements = get_lock_requirements(tool_name);
if requirements.is_read_only() {
return Ok(GitOperationLocks {
guards: vec![],
operation_type: requirements.operation_type,
description: requirements.description.to_string(),
});
}
let scope = self.project_scope();
if let Some(checker) = &self.resource_checker {
if requirements.check_file_conflicts {
let git_op_type = match requirements.operation_type {
GitOperationType::Staging => ResourceType::GitIndex,
GitOperationType::Commit => ResourceType::GitCommit,
GitOperationType::RemoteWrite => ResourceType::GitRemoteWrite,
GitOperationType::RemoteMerge => ResourceType::GitRemoteMerge,
GitOperationType::Branch => ResourceType::GitBranch,
GitOperationType::Destructive => ResourceType::GitDestructive,
GitOperationType::ReadOnly => ResourceType::GitIndex, };
let conflict_check = checker
.can_start_git_operation(git_op_type, &scope, agent_id)
.await;
if conflict_check.is_blocked() {
let conflicts: Vec<String> = conflict_check
.conflicts()
.iter()
.map(|c| format!("{}: {} by {}", c.resource, c.status, c.holder_agent))
.collect();
return Err(anyhow::anyhow!(
"Git operation blocked by conflicts: {}",
conflicts.join(", ")
));
}
}
}
if let Some(hub) = &self.communication_hub {
let _ = hub
.broadcast(
agent_id.to_string(),
AgentMessage::GitOperationStarted {
agent_id: agent_id.to_string(),
git_op: requirements.operation_type,
branch: None, description: requirements.description.to_string(),
},
)
.await;
}
let mut guards = Vec::new();
for resource_type in &requirements.resource_types {
let guard = self
.resource_locks
.acquire_resource(
agent_id,
*resource_type,
scope.clone(),
requirements.description,
)
.await?;
guards.push(guard);
}
Ok(GitOperationLocks {
guards,
operation_type: requirements.operation_type,
description: requirements.description.to_string(),
})
}
pub async fn can_perform_git_op(&self, agent_id: &str, tool_name: &str) -> bool {
let requirements = get_lock_requirements(tool_name);
if requirements.is_read_only() {
return true;
}
let scope = self.project_scope();
for resource_type in &requirements.resource_types {
if !self
.resource_locks
.can_acquire(agent_id, *resource_type, &scope)
.await
{
return false;
}
}
if let Some(checker) = &self.resource_checker
&& requirements.check_file_conflicts
{
let git_op_type = match requirements.operation_type {
GitOperationType::Staging => ResourceType::GitIndex,
GitOperationType::Commit => ResourceType::GitCommit,
GitOperationType::RemoteWrite => ResourceType::GitRemoteWrite,
GitOperationType::RemoteMerge => ResourceType::GitRemoteMerge,
GitOperationType::Branch => ResourceType::GitBranch,
GitOperationType::Destructive => ResourceType::GitDestructive,
GitOperationType::ReadOnly => return true,
};
let check = checker
.can_start_git_operation(git_op_type, &scope, agent_id)
.await;
if check.is_blocked() {
return false;
}
}
true
}
pub async fn broadcast_completion(
&self,
agent_id: &str,
operation_type: GitOperationType,
success: bool,
summary: &str,
) {
if let Some(hub) = &self.communication_hub {
let _ = hub
.broadcast(
agent_id.to_string(),
AgentMessage::GitOperationCompleted {
agent_id: agent_id.to_string(),
git_op: operation_type,
success,
summary: summary.to_string(),
},
)
.await;
}
}
}
pub struct GitOperationLocks {
guards: Vec<ResourceLockGuard>,
pub operation_type: GitOperationType,
pub description: String,
}
impl GitOperationLocks {
pub fn is_read_only(&self) -> bool {
self.guards.is_empty()
}
pub fn lock_count(&self) -> usize {
self.guards.len()
}
}
#[async_trait::async_trait]
pub trait GitOperationRunner {
async fn run_with_locks<F, T>(
&self,
agent_id: &str,
tool_name: &str,
operation: F,
) -> Result<T>
where
F: FnOnce() -> Result<T> + Send,
T: Send;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_only_operations() {
assert!(get_lock_requirements(git_tools::STATUS).is_read_only());
assert!(get_lock_requirements(git_tools::DIFF).is_read_only());
assert!(get_lock_requirements(git_tools::LOG).is_read_only());
assert!(get_lock_requirements(git_tools::SEARCH).is_read_only());
assert!(get_lock_requirements(git_tools::FETCH).is_read_only());
}
#[test]
fn test_staging_operations() {
let stage_req = get_lock_requirements(git_tools::STAGE);
assert!(!stage_req.is_read_only());
assert!(stage_req.resource_types.contains(&ResourceType::GitIndex));
assert!(matches!(
stage_req.operation_type,
GitOperationType::Staging
));
let unstage_req = get_lock_requirements(git_tools::UNSTAGE);
assert!(!unstage_req.is_read_only());
assert!(unstage_req.resource_types.contains(&ResourceType::GitIndex));
}
#[test]
fn test_commit_operation() {
let req = get_lock_requirements(git_tools::COMMIT);
assert!(!req.is_read_only());
assert!(req.resource_types.contains(&ResourceType::GitIndex));
assert!(req.resource_types.contains(&ResourceType::GitCommit));
assert!(req.check_file_conflicts);
assert!(req.check_build_conflicts);
assert!(matches!(req.operation_type, GitOperationType::Commit));
}
#[test]
fn test_push_operation() {
let req = get_lock_requirements(git_tools::PUSH);
assert!(!req.is_read_only());
assert!(req.resource_types.contains(&ResourceType::GitRemoteWrite));
assert!(!req.check_file_conflicts);
assert!(req.check_build_conflicts);
assert!(matches!(req.operation_type, GitOperationType::RemoteWrite));
}
#[test]
fn test_pull_operation() {
let req = get_lock_requirements(git_tools::PULL);
assert!(!req.is_read_only());
assert!(req.resource_types.contains(&ResourceType::GitRemoteMerge));
assert!(req.resource_types.contains(&ResourceType::GitIndex));
assert!(req.check_file_conflicts);
assert!(req.check_build_conflicts);
assert!(matches!(req.operation_type, GitOperationType::RemoteMerge));
}
#[test]
fn test_destructive_operation() {
let req = get_lock_requirements(git_tools::DISCARD);
assert!(!req.is_read_only());
assert!(req.resource_types.contains(&ResourceType::GitDestructive));
assert!(req.resource_types.contains(&ResourceType::GitIndex));
assert!(req.check_file_conflicts);
assert!(req.check_build_conflicts);
assert!(matches!(req.operation_type, GitOperationType::Destructive));
}
#[test]
fn test_unknown_operation() {
let req = get_lock_requirements("unknown_git_tool");
assert!(req.is_read_only());
}
#[tokio::test]
async fn test_coordinator_read_only_no_locks() {
let resource_locks = Arc::new(ResourceLockManager::new());
let coordinator = GitCoordinator::new(resource_locks, PathBuf::from("/test/project"));
let locks = coordinator
.acquire_for_git_op("agent-1", git_tools::STATUS)
.await
.unwrap();
assert!(locks.is_read_only());
assert_eq!(locks.lock_count(), 0);
}
#[tokio::test]
async fn test_coordinator_staging_acquires_index_lock() {
let resource_locks = Arc::new(ResourceLockManager::new());
let coordinator =
GitCoordinator::new(resource_locks.clone(), PathBuf::from("/test/project"));
let locks = coordinator
.acquire_for_git_op("agent-1", git_tools::STAGE)
.await
.unwrap();
assert!(!locks.is_read_only());
assert_eq!(locks.lock_count(), 1);
assert!(matches!(locks.operation_type, GitOperationType::Staging));
let scope = ResourceScope::Project(PathBuf::from("/test/project"));
assert!(
!resource_locks
.can_acquire("agent-2", ResourceType::GitIndex, &scope)
.await
);
}
#[tokio::test]
async fn test_coordinator_commit_acquires_multiple_locks() {
let resource_locks = Arc::new(ResourceLockManager::new());
let coordinator =
GitCoordinator::new(resource_locks.clone(), PathBuf::from("/test/project"));
let locks = coordinator
.acquire_for_git_op("agent-1", git_tools::COMMIT)
.await
.unwrap();
assert!(!locks.is_read_only());
assert_eq!(locks.lock_count(), 2); assert!(matches!(locks.operation_type, GitOperationType::Commit));
}
#[tokio::test]
async fn test_can_perform_git_op() {
let resource_locks = Arc::new(ResourceLockManager::new());
let coordinator =
GitCoordinator::new(resource_locks.clone(), PathBuf::from("/test/project"));
assert!(
coordinator
.can_perform_git_op("agent-1", git_tools::STATUS)
.await
);
assert!(
coordinator
.can_perform_git_op("agent-1", git_tools::STAGE)
.await
);
let _locks = coordinator
.acquire_for_git_op("agent-1", git_tools::STAGE)
.await
.unwrap();
assert!(
!coordinator
.can_perform_git_op("agent-2", git_tools::STAGE)
.await
);
assert!(
coordinator
.can_perform_git_op("agent-1", git_tools::STAGE)
.await
);
}
}