ai_agent/utils/plugins/git_availability.rs
1// Source: ~/claudecode/openclaudecode/src/utils/plugins/gitAvailability.ts
2
3use std::sync::atomic::{AtomicBool, Ordering};
4
5/// Check if a command is available in PATH.
6///
7/// Uses which to find the actual executable without executing it.
8/// This is a security best practice to avoid executing arbitrary code
9/// in untrusted directories.
10async fn is_command_available(command: &str) -> bool {
11 crate::utils::which(command).await.is_some()
12}
13
14/// Atomic flag for git availability cache.
15static GIT_AVAILABLE: AtomicBool = AtomicBool::new(true);
16static GIT_AVAILABLE_INITIALIZED: AtomicBool = AtomicBool::new(false);
17
18/// Check if git is available on the system.
19///
20/// This is memoized so repeated calls within a session return the cached result.
21/// Git availability is unlikely to change during a single CLI session.
22///
23/// Only checks PATH -- does not exec git. On macOS this means the /usr/bin/git
24/// xcrun shim passes even without Xcode CLT installed; callers that hit
25/// `xcrun: error:` at exec time should call mark_git_unavailable() so the rest
26/// of the session behaves as though git is absent.
27pub async fn check_git_available() -> bool {
28 // Check if cache has been set
29 if GIT_AVAILABLE_INITIALIZED.load(Ordering::SeqCst) {
30 return GIT_AVAILABLE.load(Ordering::SeqCst);
31 }
32
33 let available = is_command_available("git").await;
34 GIT_AVAILABLE.store(available, Ordering::SeqCst);
35 GIT_AVAILABLE_INITIALIZED.store(true, Ordering::SeqCst);
36 available
37}
38
39/// Force the memoized git-availability check to return false for the rest of
40/// the session.
41///
42/// Call this when a git invocation fails in a way that indicates the binary
43/// exists on PATH but cannot actually run -- the macOS xcrun shim being the
44/// main case (`xcrun: error: invalid active developer path`). Subsequent
45/// check_git_available() calls then short-circuit to false, so downstream code
46/// that guards on git availability skips cleanly instead of failing repeatedly
47/// with the same exec error.
48pub fn mark_git_unavailable() {
49 GIT_AVAILABLE.store(false, Ordering::SeqCst);
50 GIT_AVAILABLE_INITIALIZED.store(true, Ordering::SeqCst);
51}
52
53/// Clear the git availability cache.
54/// Used for testing purposes.
55pub fn clear_git_availability_cache() {
56 GIT_AVAILABLE.store(true, Ordering::SeqCst);
57 GIT_AVAILABLE_INITIALIZED.store(false, Ordering::SeqCst);
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[tokio::test]
65 async fn test_clear_cache() {
66 clear_git_availability_cache();
67 assert!(!GIT_AVAILABLE_INITIALIZED.load(Ordering::SeqCst));
68 }
69
70 #[test]
71 fn test_mark_unavailable() {
72 mark_git_unavailable();
73 assert!(!GIT_AVAILABLE.load(Ordering::SeqCst));
74 assert!(GIT_AVAILABLE_INITIALIZED.load(Ordering::SeqCst));
75 clear_git_availability_cache();
76 }
77}