1use std::collections::HashMap;
2use std::time::{Duration, Instant};
3use tokio::process::Command;
4
5const GIT_TIMEOUT: Duration = Duration::from_secs(5);
7
8#[derive(Debug, Clone)]
10pub struct GitInfo {
11 pub branch: String,
13 pub dirty: bool,
15 pub is_worktree: bool,
17}
18
19pub struct GitCache {
21 cache: HashMap<String, (Option<GitInfo>, Instant)>,
22 ttl_secs: u64,
23}
24
25impl Default for GitCache {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl GitCache {
32 pub fn new() -> Self {
34 Self {
35 cache: HashMap::new(),
36 ttl_secs: 10,
37 }
38 }
39
40 pub async fn get_info(&mut self, dir: &str) -> Option<GitInfo> {
43 if let Some((info, ts)) = self.cache.get(dir) {
45 if ts.elapsed().as_secs() < self.ttl_secs {
46 return info.clone();
47 }
48 }
49
50 let info = fetch_git_info(dir).await;
52 self.cache
53 .insert(dir.to_string(), (info.clone(), Instant::now()));
54 info
55 }
56
57 pub fn get_cached(&self, dir: &str) -> Option<GitInfo> {
60 if let Some((info, ts)) = self.cache.get(dir) {
61 if ts.elapsed().as_secs() < self.ttl_secs {
62 return info.clone();
63 }
64 }
65 None
66 }
67
68 pub fn cleanup(&mut self) {
70 self.cache
71 .retain(|_, (_, ts)| ts.elapsed().as_secs() < self.ttl_secs * 3);
72 }
73}
74
75async fn fetch_git_info(dir: &str) -> Option<GitInfo> {
77 let branch = fetch_branch(dir).await?;
78 let (dirty, is_worktree) = tokio::join!(fetch_dirty(dir), fetch_is_worktree(dir));
80 Some(GitInfo {
81 branch,
82 dirty,
83 is_worktree,
84 })
85}
86
87async fn fetch_branch(dir: &str) -> Option<String> {
89 let output = tokio::time::timeout(
90 GIT_TIMEOUT,
91 Command::new("git")
92 .args(["-C", dir, "rev-parse", "--abbrev-ref", "HEAD"])
93 .output(),
94 )
95 .await
96 .ok()?
97 .ok()?;
98 if output.status.success() {
99 Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
100 } else {
101 None
102 }
103}
104
105async fn fetch_dirty(dir: &str) -> bool {
107 let output = tokio::time::timeout(
108 GIT_TIMEOUT,
109 Command::new("git")
110 .args(["-C", dir, "status", "--porcelain"])
111 .output(),
112 )
113 .await;
114 match output {
115 Ok(Ok(o)) => !o.stdout.is_empty(),
116 _ => false,
117 }
118}
119
120async fn fetch_is_worktree(dir: &str) -> bool {
122 let results = tokio::join!(
123 tokio::time::timeout(
124 GIT_TIMEOUT,
125 Command::new("git")
126 .args(["-C", dir, "rev-parse", "--git-dir"])
127 .output(),
128 ),
129 tokio::time::timeout(
130 GIT_TIMEOUT,
131 Command::new("git")
132 .args(["-C", dir, "rev-parse", "--git-common-dir"])
133 .output(),
134 ),
135 );
136 match results {
137 (Ok(Ok(gd)), Ok(Ok(cd))) => {
138 let gd_str = String::from_utf8_lossy(&gd.stdout).trim().to_string();
139 let cd_str = String::from_utf8_lossy(&cd.stdout).trim().to_string();
140 gd_str != cd_str
141 }
142 _ => false,
143 }
144}