Skip to main content

codeinput/core/
cache.rs

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
22/// Create a cache from parsed CODEOWNERS entries and files
23pub 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    // Process each file to find owners and tags
35    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                    // Limit filename display length and clear the line properly
48                    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                    // Build file entry
64                    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    // Print newline after processing is complete
75    output::println(&format!("\r\x1b[K✅ Processed {} files successfully", total_files));
76
77    // Process each owner
78    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    // Process each tag
89    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
108/// Store Cache
109pub 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
134/// Load Cache from file, automatically detecting whether it's JSON or Bincode format
135pub fn load_cache(path: &Path) -> Result<CodeownersCache> {
136    // Read the first byte to make an educated guess about the format
137    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    // Close the file handle and reopen for full reading
144    drop(file);
145
146    if read_result.is_ok() && first_byte[0] == b'{' {
147        // First byte is '{', likely JSON
148        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    // Try bincode first since it's not JSON
157    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            // If bincode fails and it's not obviously JSON, still try JSON as a fallback
165            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    // Verify that the cache file exists
192    if !repo.join(cache_file).exists() {
193        // parse the codeowners files and build the cache
194        return parse_repo(&repo, &cache_file);
195    }
196
197    // Load the cache from the specified file
198    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    // verify the hash of the cache matches the current repo hash
207    let current_hash = get_repo_hash(repo)?;
208    let cache_hash = cache.hash;
209
210    if cache_hash != current_hash {
211        // parse the codeowners files and build the cache
212        return parse_repo(&repo, &cache_file);
213    } else {
214        return Ok(cache);
215    }
216}