search_issue_refs/
main.rs
1use std::{
36 collections::HashMap,
37 env,
38 ffi::OsStr,
39 fs::{self, File},
40 io::{self, BufRead},
41 path::PathBuf,
42};
43
44use color_eyre::eyre::Result;
45use regex::Regex;
46use reqwest::{
47 header::{self, HeaderMap, HeaderValue},
48 ClientBuilder,
49};
50
51use tokio::task::JoinSet;
52use zebra_utils::init_tracing;
53
54const GITHUB_TOKEN_ENV_KEY: &str = "GITHUB_TOKEN";
55
56const VALID_EXTENSIONS: [&str; 4] = ["rs", "yml", "yaml", "toml"];
57
58fn check_file_ext(ext: &OsStr) -> bool {
59 VALID_EXTENSIONS
60 .into_iter()
61 .any(|valid_extension| valid_extension == ext)
62}
63
64fn search_directory(path: &PathBuf) -> Result<Vec<PathBuf>> {
65 if path.starts_with("/target/") {
66 return Ok(vec![]);
67 }
68
69 Ok(fs::read_dir(path)?
70 .filter_map(|entry| {
71 let path = entry.ok()?.path();
72
73 if path.is_dir() {
74 search_directory(&path).ok()
75 } else if path.is_file() {
76 match path.extension() {
77 Some(ext) if check_file_ext(ext) => Some(vec![path]),
78 _ => None,
79 }
80 } else {
81 None
82 }
83 })
84 .flatten()
85 .collect())
86}
87
88fn github_issue_url(issue_id: &str) -> String {
89 format!("https://github.com/ZcashFoundation/zebra/issues/{issue_id}")
90}
91
92fn github_remote_file_ref(file_path: &str, line: usize) -> String {
93 let file_path = &crate_mod_path(file_path, line);
94 format!("https://github.com/ZcashFoundation/zebra/blob/main/{file_path}")
95}
96
97fn github_permalink(sha: &str, file_path: &str, line: usize) -> String {
98 let file_path = &crate_mod_path(file_path, line);
99 format!("https://github.com/ZcashFoundation/zebra/blob/{sha}/{file_path}")
100}
101
102fn crate_mod_path(file_path: &str, line: usize) -> String {
103 let file_path = &file_path[2..];
104 format!("{file_path}#L{line}")
105}
106
107fn github_issue_api_url(issue_id: &str) -> String {
108 format!("https://api.github.com/repos/ZcashFoundation/zebra/issues/{issue_id}")
109}
110
111fn github_ref_api_url(reference: &str) -> String {
112 format!("https://api.github.com/repos/ZcashFoundation/zebra/git/ref/{reference}")
113}
114
115#[derive(Debug)]
116struct PossibleIssueRef {
117 file_path: String,
118 line_number: usize,
119 column: usize,
120}
121
122impl PossibleIssueRef {
123 #[allow(clippy::print_stdout, clippy::print_stderr)]
124 fn print_paths(issue_refs: &[PossibleIssueRef]) {
125 for PossibleIssueRef {
126 file_path,
127 line_number,
128 column,
129 } in issue_refs
130 {
131 let file_ref = format!("{file_path}:{line_number}:{column}");
132 let github_file_ref = github_remote_file_ref(file_path, *line_number);
133
134 println!("{file_ref}\n{github_file_ref}\n");
135 }
136 }
137}
138
139type IssueId = String;
140
141#[allow(clippy::print_stdout, clippy::print_stderr)]
143#[tokio::main]
144async fn main() -> Result<()> {
145 init_tracing();
146 color_eyre::install()?;
147
148 let possible_issue_refs = {
149 let file_paths = search_directory(&".".into())?;
150
151 let issue_regex =
153 Regex::new(r"(https://github.com/ZcashFoundation/zebra/issues/|#)(\d{1,4})").unwrap();
154
155 let mut possible_issue_refs: HashMap<IssueId, Vec<PossibleIssueRef>> = HashMap::new();
156 let mut num_possible_issue_refs = 0;
157
158 for file_path in file_paths {
159 let file = File::open(&file_path)?;
160 let lines = io::BufReader::new(file).lines();
161
162 for (line_idx, line) in lines.into_iter().enumerate() {
163 let line = line?;
164 let line_number = line_idx + 1;
165
166 for captures in issue_regex.captures_iter(&line) {
167 let file_path = file_path
168 .to_str()
169 .expect("paths from read_dir should be valid unicode")
170 .to_string();
171
172 let column = captures
173 .get(1)
174 .expect("matches should have 2 captures")
175 .start()
176 + 1;
177
178 let potential_issue_ref =
179 captures.get(2).expect("matches should have 2 captures");
180 let matching_text = potential_issue_ref.as_str();
181
182 let id = matching_text[matching_text.len().checked_sub(4).unwrap_or(1)..]
183 .to_string();
184
185 let issue_entry = possible_issue_refs.entry(id).or_default();
186
187 if issue_entry.iter().all(|issue_ref| {
188 issue_ref.line_number != line_number || issue_ref.file_path != file_path
189 }) {
190 num_possible_issue_refs += 1;
191
192 issue_entry.push(PossibleIssueRef {
193 file_path,
194 line_number,
195 column,
196 });
197 }
198 }
199 }
200 }
201
202 let num_possible_issues = possible_issue_refs.len();
203
204 println!(
205 "\nFound {num_possible_issue_refs} possible references to {num_possible_issues} issues, checking statuses on Github..\n"
206 );
207
208 possible_issue_refs
209 };
210
211 let Some((_, github_token)) = env::vars().find(|(key, _)| key == GITHUB_TOKEN_ENV_KEY) else {
214 println!(
215 "Can't find {GITHUB_TOKEN_ENV_KEY} in env vars, printing all found possible issue refs, \
216see https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token \
217to create a github token."
218 );
219
220 for (
221 id,
222 PossibleIssueRef {
223 file_path,
224 line_number,
225 column,
226 },
227 ) in possible_issue_refs
228 .into_iter()
229 .flat_map(|(issue_id, issue_refs)| {
230 issue_refs
231 .into_iter()
232 .map(move |issue_ref| (issue_id.clone(), issue_ref))
233 })
234 {
235 let github_url = github_issue_url(&id);
236 let github_file_ref = github_remote_file_ref(&file_path, line_number);
237
238 println!("\n--------------------------------------");
239 println!("Found possible reference to closed issue #{id}: {file_path}:{line_number}:{column}");
240 println!("{github_file_ref}");
241 println!("{github_url}");
242 }
243
244 return Ok(());
245 };
246
247 let mut headers = HeaderMap::new();
248 let mut auth_value = HeaderValue::from_str(&format!("Bearer {github_token}"))?;
249 let accept_value = HeaderValue::from_static("application/vnd.github+json");
250 let github_api_version_value = HeaderValue::from_static("2022-11-28");
251 let user_agent_value = HeaderValue::from_static("search-issue-refs");
252
253 auth_value.set_sensitive(true);
254
255 headers.insert(header::AUTHORIZATION, auth_value);
256 headers.insert(header::ACCEPT, accept_value);
257 headers.insert("X-GitHub-Api-Version", github_api_version_value);
258 headers.insert(header::USER_AGENT, user_agent_value);
259
260 let client = ClientBuilder::new().default_headers(headers).build()?;
261
262 let latest_commit_json: serde_json::Value = serde_json::from_str::<serde_json::Value>(
265 &client
266 .get(github_ref_api_url("heads/main"))
267 .send()
268 .await?
269 .text()
270 .await?,
271 )
272 .expect("response text should be json");
273
274 let latest_commit_sha = latest_commit_json["object"]["sha"]
275 .as_str()
276 .expect("response.object.sha should be a string");
277
278 let mut github_api_requests = JoinSet::new();
279
280 for (id, issue_refs) in possible_issue_refs {
281 let request = client.get(github_issue_api_url(&id)).send();
282 github_api_requests.spawn(async move { (request.await, id, issue_refs) });
283 }
284
285 let mut num_closed_issue_refs = 0;
288 let mut num_closed_issues = 0;
289
290 while let Some(res) = github_api_requests.join_next().await {
291 let Ok((res, id, issue_refs)) = res else {
292 println!("warning: failed to join api request thread/task");
293 continue;
294 };
295
296 let Ok(res) = res else {
297 println!("warning: no response from github api about issue #{id}");
298 PossibleIssueRef::print_paths(&issue_refs);
299 continue;
300 };
301
302 let Ok(text) = res.text().await else {
303 println!("warning: no response from github api about issue #{id}");
304 PossibleIssueRef::print_paths(&issue_refs);
305 continue;
306 };
307
308 let Ok(json): Result<serde_json::Value, _> = serde_json::from_str(&text) else {
309 println!("warning: no response from github api about issue #{id}");
310 PossibleIssueRef::print_paths(&issue_refs);
311 continue;
312 };
313
314 if json["closed_at"] == serde_json::Value::Null {
315 continue;
316 };
317
318 println!("\n--------------------------------------\n- #{id}\n");
319
320 num_closed_issues += 1;
321
322 for PossibleIssueRef {
323 file_path,
324 line_number,
325 column: _,
326 } in issue_refs
327 {
328 num_closed_issue_refs += 1;
329
330 let github_permalink = github_permalink(latest_commit_sha, &file_path, line_number);
331
332 println!("{github_permalink}");
333 }
334 }
335
336 println!(
337 "\nConfirmed {num_closed_issue_refs} references to {num_closed_issues} closed issues.\n"
338 );
339
340 Ok(())
341}