1use std::path::PathBuf;
19use std::sync::Arc;
20
21use anyhow::Result;
22
23use crate::communication::{AgentMessage, CommunicationHub, GitOperationType};
24use crate::resource_checker::ResourceChecker;
25use crate::resource_locks::{ResourceLockGuard, ResourceLockManager, ResourceScope, ResourceType};
26
27pub mod git_tools {
29 pub const STATUS: &str = "git_status";
31 pub const DIFF: &str = "git_diff";
33 pub const LOG: &str = "git_log";
35 pub const SEARCH: &str = "git_search";
37 pub const FETCH: &str = "git_fetch";
39 pub const STAGE: &str = "git_stage";
41 pub const UNSTAGE: &str = "git_unstage";
43 pub const COMMIT: &str = "git_commit";
45 pub const PUSH: &str = "git_push";
47 pub const PULL: &str = "git_pull";
49 pub const BRANCH: &str = "git_branch";
51 pub const DISCARD: &str = "git_discard";
53}
54
55#[derive(Debug, Clone)]
57pub struct GitLockRequirements {
58 pub resource_types: Vec<ResourceType>,
60 pub check_file_conflicts: bool,
62 pub check_build_conflicts: bool,
64 pub operation_type: GitOperationType,
66 pub description: &'static str,
68}
69
70impl GitLockRequirements {
71 pub fn is_read_only(&self) -> bool {
73 self.resource_types.is_empty()
74 }
75}
76
77pub fn get_lock_requirements(tool_name: &str) -> GitLockRequirements {
79 match tool_name {
80 git_tools::STATUS | git_tools::DIFF | git_tools::LOG | git_tools::SEARCH => {
82 GitLockRequirements {
83 resource_types: vec![],
84 check_file_conflicts: false,
85 check_build_conflicts: false,
86 operation_type: GitOperationType::ReadOnly,
87 description: "Read-only git operation",
88 }
89 }
90 git_tools::FETCH => GitLockRequirements {
91 resource_types: vec![],
92 check_file_conflicts: false,
93 check_build_conflicts: false,
94 operation_type: GitOperationType::ReadOnly,
95 description: "Fetch from remote",
96 },
97
98 git_tools::STAGE | git_tools::UNSTAGE => GitLockRequirements {
100 resource_types: vec![ResourceType::GitIndex],
101 check_file_conflicts: true, check_build_conflicts: false,
103 operation_type: GitOperationType::Staging,
104 description: "Staging area modification",
105 },
106
107 git_tools::COMMIT => GitLockRequirements {
109 resource_types: vec![ResourceType::GitIndex, ResourceType::GitCommit],
110 check_file_conflicts: true, check_build_conflicts: true, operation_type: GitOperationType::Commit,
113 description: "Create commit",
114 },
115
116 git_tools::PUSH => GitLockRequirements {
118 resource_types: vec![ResourceType::GitRemoteWrite],
119 check_file_conflicts: false,
120 check_build_conflicts: true, operation_type: GitOperationType::RemoteWrite,
122 description: "Push to remote",
123 },
124
125 git_tools::PULL => GitLockRequirements {
127 resource_types: vec![ResourceType::GitRemoteMerge, ResourceType::GitIndex],
128 check_file_conflicts: true, check_build_conflicts: true, operation_type: GitOperationType::RemoteMerge,
131 description: "Pull from remote",
132 },
133
134 git_tools::BRANCH => GitLockRequirements {
136 resource_types: vec![ResourceType::GitBranch],
137 check_file_conflicts: false,
138 check_build_conflicts: false,
139 operation_type: GitOperationType::Branch,
140 description: "Branch operation",
141 },
142
143 git_tools::DISCARD => GitLockRequirements {
145 resource_types: vec![ResourceType::GitDestructive, ResourceType::GitIndex],
146 check_file_conflicts: true, check_build_conflicts: true, operation_type: GitOperationType::Destructive,
149 description: "Discard changes (destructive)",
150 },
151
152 _ => GitLockRequirements {
154 resource_types: vec![],
155 check_file_conflicts: false,
156 check_build_conflicts: false,
157 operation_type: GitOperationType::ReadOnly,
158 description: "Unknown git operation",
159 },
160 }
161}
162
163pub struct GitCoordinator {
165 resource_locks: Arc<ResourceLockManager>,
166 resource_checker: Option<Arc<ResourceChecker>>,
167 communication_hub: Option<Arc<CommunicationHub>>,
168 project_root: PathBuf,
169}
170
171impl GitCoordinator {
172 pub fn new(resource_locks: Arc<ResourceLockManager>, project_root: PathBuf) -> Self {
174 Self {
175 resource_locks,
176 resource_checker: None,
177 communication_hub: None,
178 project_root,
179 }
180 }
181
182 pub fn with_full_integration(
184 resource_locks: Arc<ResourceLockManager>,
185 resource_checker: Arc<ResourceChecker>,
186 communication_hub: Arc<CommunicationHub>,
187 project_root: PathBuf,
188 ) -> Self {
189 Self {
190 resource_locks,
191 resource_checker: Some(resource_checker),
192 communication_hub: Some(communication_hub),
193 project_root,
194 }
195 }
196
197 pub fn project_scope(&self) -> ResourceScope {
199 ResourceScope::Project(self.project_root.clone())
200 }
201
202 #[tracing::instrument(name = "agent.git.acquire", skip(self))]
207 pub async fn acquire_for_git_op(
208 &self,
209 agent_id: &str,
210 tool_name: &str,
211 ) -> Result<GitOperationLocks> {
212 let requirements = get_lock_requirements(tool_name);
213
214 if requirements.is_read_only() {
216 return Ok(GitOperationLocks {
217 guards: vec![],
218 operation_type: requirements.operation_type,
219 description: requirements.description.to_string(),
220 });
221 }
222
223 let scope = self.project_scope();
224
225 if let Some(checker) = &self.resource_checker {
227 if requirements.check_file_conflicts {
229 let git_op_type = match requirements.operation_type {
230 GitOperationType::Staging => ResourceType::GitIndex,
231 GitOperationType::Commit => ResourceType::GitCommit,
232 GitOperationType::RemoteWrite => ResourceType::GitRemoteWrite,
233 GitOperationType::RemoteMerge => ResourceType::GitRemoteMerge,
234 GitOperationType::Branch => ResourceType::GitBranch,
235 GitOperationType::Destructive => ResourceType::GitDestructive,
236 GitOperationType::ReadOnly => ResourceType::GitIndex, };
238
239 let conflict_check = checker
240 .can_start_git_operation(git_op_type, &scope, agent_id)
241 .await;
242
243 if conflict_check.is_blocked() {
244 let conflicts: Vec<String> = conflict_check
245 .conflicts()
246 .iter()
247 .map(|c| format!("{}: {} by {}", c.resource, c.status, c.holder_agent))
248 .collect();
249
250 return Err(anyhow::anyhow!(
251 "Git operation blocked by conflicts: {}",
252 conflicts.join(", ")
253 ));
254 }
255 }
256 }
257
258 if let Some(hub) = &self.communication_hub {
260 let _ = hub
261 .broadcast(
262 agent_id.to_string(),
263 AgentMessage::GitOperationStarted {
264 agent_id: agent_id.to_string(),
265 git_op: requirements.operation_type,
266 branch: None, description: requirements.description.to_string(),
268 },
269 )
270 .await;
271 }
272
273 let mut guards = Vec::new();
275 for resource_type in &requirements.resource_types {
276 let guard = self
277 .resource_locks
278 .acquire_resource(
279 agent_id,
280 *resource_type,
281 scope.clone(),
282 requirements.description,
283 )
284 .await?;
285 guards.push(guard);
286 }
287
288 Ok(GitOperationLocks {
289 guards,
290 operation_type: requirements.operation_type,
291 description: requirements.description.to_string(),
292 })
293 }
294
295 pub async fn can_perform_git_op(&self, agent_id: &str, tool_name: &str) -> bool {
297 let requirements = get_lock_requirements(tool_name);
298
299 if requirements.is_read_only() {
301 return true;
302 }
303
304 let scope = self.project_scope();
305
306 for resource_type in &requirements.resource_types {
308 if !self
309 .resource_locks
310 .can_acquire(agent_id, *resource_type, &scope)
311 .await
312 {
313 return false;
314 }
315 }
316
317 if let Some(checker) = &self.resource_checker
319 && requirements.check_file_conflicts
320 {
321 let git_op_type = match requirements.operation_type {
322 GitOperationType::Staging => ResourceType::GitIndex,
323 GitOperationType::Commit => ResourceType::GitCommit,
324 GitOperationType::RemoteWrite => ResourceType::GitRemoteWrite,
325 GitOperationType::RemoteMerge => ResourceType::GitRemoteMerge,
326 GitOperationType::Branch => ResourceType::GitBranch,
327 GitOperationType::Destructive => ResourceType::GitDestructive,
328 GitOperationType::ReadOnly => return true,
329 };
330
331 let check = checker
332 .can_start_git_operation(git_op_type, &scope, agent_id)
333 .await;
334
335 if check.is_blocked() {
336 return false;
337 }
338 }
339
340 true
341 }
342
343 pub async fn broadcast_completion(
345 &self,
346 agent_id: &str,
347 operation_type: GitOperationType,
348 success: bool,
349 summary: &str,
350 ) {
351 if let Some(hub) = &self.communication_hub {
352 let _ = hub
353 .broadcast(
354 agent_id.to_string(),
355 AgentMessage::GitOperationCompleted {
356 agent_id: agent_id.to_string(),
357 git_op: operation_type,
358 success,
359 summary: summary.to_string(),
360 },
361 )
362 .await;
363 }
364 }
365}
366
367pub struct GitOperationLocks {
371 guards: Vec<ResourceLockGuard>,
372 pub operation_type: GitOperationType,
374 pub description: String,
376}
377
378impl GitOperationLocks {
379 pub fn is_read_only(&self) -> bool {
381 self.guards.is_empty()
382 }
383
384 pub fn lock_count(&self) -> usize {
386 self.guards.len()
387 }
388}
389
390#[async_trait::async_trait]
392pub trait GitOperationRunner {
393 async fn run_with_locks<F, T>(
395 &self,
396 agent_id: &str,
397 tool_name: &str,
398 operation: F,
399 ) -> Result<T>
400 where
401 F: FnOnce() -> Result<T> + Send,
402 T: Send;
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_read_only_operations() {
411 assert!(get_lock_requirements(git_tools::STATUS).is_read_only());
412 assert!(get_lock_requirements(git_tools::DIFF).is_read_only());
413 assert!(get_lock_requirements(git_tools::LOG).is_read_only());
414 assert!(get_lock_requirements(git_tools::SEARCH).is_read_only());
415 assert!(get_lock_requirements(git_tools::FETCH).is_read_only());
416 }
417
418 #[test]
419 fn test_staging_operations() {
420 let stage_req = get_lock_requirements(git_tools::STAGE);
421 assert!(!stage_req.is_read_only());
422 assert!(stage_req.resource_types.contains(&ResourceType::GitIndex));
423 assert!(matches!(
424 stage_req.operation_type,
425 GitOperationType::Staging
426 ));
427
428 let unstage_req = get_lock_requirements(git_tools::UNSTAGE);
429 assert!(!unstage_req.is_read_only());
430 assert!(unstage_req.resource_types.contains(&ResourceType::GitIndex));
431 }
432
433 #[test]
434 fn test_commit_operation() {
435 let req = get_lock_requirements(git_tools::COMMIT);
436 assert!(!req.is_read_only());
437 assert!(req.resource_types.contains(&ResourceType::GitIndex));
438 assert!(req.resource_types.contains(&ResourceType::GitCommit));
439 assert!(req.check_file_conflicts);
440 assert!(req.check_build_conflicts);
441 assert!(matches!(req.operation_type, GitOperationType::Commit));
442 }
443
444 #[test]
445 fn test_push_operation() {
446 let req = get_lock_requirements(git_tools::PUSH);
447 assert!(!req.is_read_only());
448 assert!(req.resource_types.contains(&ResourceType::GitRemoteWrite));
449 assert!(!req.check_file_conflicts);
450 assert!(req.check_build_conflicts);
451 assert!(matches!(req.operation_type, GitOperationType::RemoteWrite));
452 }
453
454 #[test]
455 fn test_pull_operation() {
456 let req = get_lock_requirements(git_tools::PULL);
457 assert!(!req.is_read_only());
458 assert!(req.resource_types.contains(&ResourceType::GitRemoteMerge));
459 assert!(req.resource_types.contains(&ResourceType::GitIndex));
460 assert!(req.check_file_conflicts);
461 assert!(req.check_build_conflicts);
462 assert!(matches!(req.operation_type, GitOperationType::RemoteMerge));
463 }
464
465 #[test]
466 fn test_destructive_operation() {
467 let req = get_lock_requirements(git_tools::DISCARD);
468 assert!(!req.is_read_only());
469 assert!(req.resource_types.contains(&ResourceType::GitDestructive));
470 assert!(req.resource_types.contains(&ResourceType::GitIndex));
471 assert!(req.check_file_conflicts);
472 assert!(req.check_build_conflicts);
473 assert!(matches!(req.operation_type, GitOperationType::Destructive));
474 }
475
476 #[test]
477 fn test_unknown_operation() {
478 let req = get_lock_requirements("unknown_git_tool");
479 assert!(req.is_read_only());
480 }
481
482 #[tokio::test]
483 async fn test_coordinator_read_only_no_locks() {
484 let resource_locks = Arc::new(ResourceLockManager::new());
485 let coordinator = GitCoordinator::new(resource_locks, PathBuf::from("/test/project"));
486
487 let locks = coordinator
488 .acquire_for_git_op("agent-1", git_tools::STATUS)
489 .await
490 .unwrap();
491
492 assert!(locks.is_read_only());
493 assert_eq!(locks.lock_count(), 0);
494 }
495
496 #[tokio::test]
497 async fn test_coordinator_staging_acquires_index_lock() {
498 let resource_locks = Arc::new(ResourceLockManager::new());
499 let coordinator =
500 GitCoordinator::new(resource_locks.clone(), PathBuf::from("/test/project"));
501
502 let locks = coordinator
503 .acquire_for_git_op("agent-1", git_tools::STAGE)
504 .await
505 .unwrap();
506
507 assert!(!locks.is_read_only());
508 assert_eq!(locks.lock_count(), 1);
509 assert!(matches!(locks.operation_type, GitOperationType::Staging));
510
511 let scope = ResourceScope::Project(PathBuf::from("/test/project"));
513 assert!(
514 !resource_locks
515 .can_acquire("agent-2", ResourceType::GitIndex, &scope)
516 .await
517 );
518 }
519
520 #[tokio::test]
521 async fn test_coordinator_commit_acquires_multiple_locks() {
522 let resource_locks = Arc::new(ResourceLockManager::new());
523 let coordinator =
524 GitCoordinator::new(resource_locks.clone(), PathBuf::from("/test/project"));
525
526 let locks = coordinator
527 .acquire_for_git_op("agent-1", git_tools::COMMIT)
528 .await
529 .unwrap();
530
531 assert!(!locks.is_read_only());
532 assert_eq!(locks.lock_count(), 2); assert!(matches!(locks.operation_type, GitOperationType::Commit));
534 }
535
536 #[tokio::test]
537 async fn test_can_perform_git_op() {
538 let resource_locks = Arc::new(ResourceLockManager::new());
539 let coordinator =
540 GitCoordinator::new(resource_locks.clone(), PathBuf::from("/test/project"));
541
542 assert!(
544 coordinator
545 .can_perform_git_op("agent-1", git_tools::STATUS)
546 .await
547 );
548
549 assert!(
551 coordinator
552 .can_perform_git_op("agent-1", git_tools::STAGE)
553 .await
554 );
555
556 let _locks = coordinator
558 .acquire_for_git_op("agent-1", git_tools::STAGE)
559 .await
560 .unwrap();
561
562 assert!(
564 !coordinator
565 .can_perform_git_op("agent-2", git_tools::STAGE)
566 .await
567 );
568
569 assert!(
571 coordinator
572 .can_perform_git_op("agent-1", git_tools::STAGE)
573 .await
574 );
575 }
576}