1use crate::{
2 core::{
3 common::{collect_owners, collect_tags, get_repo_hash},
4 parse::parse_repo,
5 resolver::find_owners_and_tags_for_file,
6 types::{
7 codeowners_entry_to_matcher, CacheEncoding, CodeownersCache, CodeownersEntry,
8 CodeownersEntryMatcher, FileEntry,
9 },
10 },
11 utils::{
12 error::{Error, Result},
13 output,
14 },
15};
16use rayon::{iter::ParallelIterator, slice::ParallelSlice};
17use std::{
18 io::{Read, Write},
19 path::{Path, PathBuf},
20};
21
22pub fn build_cache(
24 entries: Vec<CodeownersEntry>, files: Vec<PathBuf>, hash: [u8; 32],
25) -> Result<CodeownersCache> {
26 let mut owners_map = std::collections::HashMap::new();
27 let mut tags_map = std::collections::HashMap::new();
28
29 let matched_entries: Vec<CodeownersEntryMatcher> = entries
30 .iter()
31 .map(|entry| codeowners_entry_to_matcher(entry))
32 .collect();
33
34 let total_files = files.len();
36 let processed_count = std::sync::atomic::AtomicUsize::new(0);
37
38 let file_entries: Vec<FileEntry> = files
39 .par_chunks(100)
40 .flat_map(|chunk| {
41 chunk
42 .iter()
43 .map(|file_path| {
44 let current =
45 processed_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
46
47 let file_display = file_path.display().to_string();
49 let truncated_file = if file_display.len() > 60 {
50 format!("...{}", &file_display[file_display.len() - 57..])
51 } else {
52 file_display
53 };
54
55 output::print(&format!(
56 "\r\x1b[K📁 Processing [{}/{}] {}",
57 current, total_files, truncated_file
58 ));
59
60 let (owners, tags) =
61 find_owners_and_tags_for_file(file_path, &matched_entries).unwrap();
62
63 FileEntry {
65 path: file_path.clone(),
66 owners: owners.clone(),
67 tags: tags.clone(),
68 }
69 })
70 .collect::<Vec<FileEntry>>()
71 })
72 .collect();
73
74 output::println(&format!("\r\x1b[K✅ Processed {} files successfully", total_files));
76
77 let owners = collect_owners(&entries);
79 owners.iter().for_each(|owner| {
80 let paths = owners_map.entry(owner.clone()).or_insert_with(Vec::new);
81 for file_entry in &file_entries {
82 if file_entry.owners.contains(owner) {
83 paths.push(file_entry.path.clone());
84 }
85 }
86 });
87
88 let tags = collect_tags(&entries);
90 tags.iter().for_each(|tag| {
91 let paths = tags_map.entry(tag.clone()).or_insert_with(Vec::new);
92 for file_entry in &file_entries {
93 if file_entry.tags.contains(tag) {
94 paths.push(file_entry.path.clone());
95 }
96 }
97 });
98
99 Ok(CodeownersCache {
100 hash,
101 entries,
102 files: file_entries,
103 owners_map,
104 tags_map,
105 })
106}
107
108pub fn store_cache(cache: &CodeownersCache, path: &Path, encoding: CacheEncoding) -> Result<()> {
110 let parent = path
111 .parent()
112 .ok_or_else(|| Error::new("Invalid cache path"))?;
113 std::fs::create_dir_all(parent)?;
114
115 let file = std::fs::File::create(path)?;
116 let mut writer = std::io::BufWriter::new(file);
117
118 match encoding {
119 CacheEncoding::Bincode => {
120 bincode::serde::encode_into_std_write(cache, &mut writer, bincode::config::standard())
121 .map_err(|e| Error::new(&format!("Failed to serialize cache: {}", e)))?;
122 }
123 CacheEncoding::Json => {
124 serde_json::to_writer_pretty(&mut writer, cache)
125 .map_err(|e| Error::new(&format!("Failed to serialize cache to JSON: {}", e)))?;
126 }
127 }
128
129 writer.flush()?;
130
131 Ok(())
132}
133
134pub fn load_cache(path: &Path) -> Result<CodeownersCache> {
136 let mut file = std::fs::File::open(path)
138 .map_err(|e| Error::new(&format!("Failed to open cache file: {}", e)))?;
139
140 let mut first_byte = [0u8; 1];
141 let read_result = file.read_exact(&mut first_byte);
142
143 drop(file);
145
146 if read_result.is_ok() && first_byte[0] == b'{' {
147 let file = std::fs::File::open(path)
149 .map_err(|e| Error::new(&format!("Failed to open cache file: {}", e)))?;
150 let reader = std::io::BufReader::new(file);
151
152 return serde_json::from_reader(reader)
153 .map_err(|e| Error::new(&format!("Failed to deserialize JSON cache: {}", e)));
154 }
155
156 let file = std::fs::File::open(path)
158 .map_err(|e| Error::new(&format!("Failed to open cache file: {}", e)))?;
159 let mut reader = std::io::BufReader::new(file);
160
161 match bincode::serde::decode_from_std_read(&mut reader, bincode::config::standard()) {
162 Ok(cache) => Ok(cache),
163 Err(_) => {
164 let file = std::fs::File::open(path)
166 .map_err(|e| Error::new(&format!("Failed to open cache file: {}", e)))?;
167 let reader = std::io::BufReader::new(file);
168
169 serde_json::from_reader(reader).map_err(|e| {
170 Error::new(&format!(
171 "Failed to deserialize cache in any supported format: {}",
172 e
173 ))
174 })
175 }
176 }
177}
178
179pub fn sync_cache(
180 repo: &std::path::Path, cache_file: Option<&std::path::Path>,
181) -> Result<CodeownersCache> {
182 let config_cache_file = crate::utils::app_config::AppConfig::fetch()?
183 .cache_file
184 .clone();
185
186 let cache_file: &std::path::Path = match cache_file {
187 Some(file) => file.into(),
188 None => std::path::Path::new(&config_cache_file),
189 };
190
191 if !repo.join(cache_file).exists() {
193 return parse_repo(&repo, &cache_file);
195 }
196
197 let cache = load_cache(&repo.join(cache_file)).map_err(|e| {
199 crate::utils::error::Error::new(&format!(
200 "Failed to load cache from {}: {}",
201 cache_file.display(),
202 e
203 ))
204 })?;
205
206 let current_hash = get_repo_hash(repo)?;
208 let cache_hash = cache.hash;
209
210 if cache_hash != current_hash {
211 return parse_repo(&repo, &cache_file);
213 } else {
214 return Ok(cache);
215 }
216}