search_issue_refs/
main.rs1use 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::{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, clippy::unwrap_in_result)]
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})")?;
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 .ok_or_else(|| eyre!("paths from read_dir should be valid unicode"))?
170 .to_string();
171
172 let column = captures
173 .get(1)
174 .ok_or_else(|| eyre!("matches should have 2 captures"))?
175 .start()
176 + 1;
177
178 let potential_issue_ref = captures
179 .get(2)
180 .ok_or_else(|| eyre!("matches should have 2 captures"))?;
181 let matching_text = potential_issue_ref.as_str();
182
183 let id = matching_text[matching_text.len().checked_sub(4).unwrap_or(1)..]
184 .to_string();
185
186 let issue_entry = possible_issue_refs.entry(id).or_default();
187
188 if issue_entry.iter().all(|issue_ref| {
189 issue_ref.line_number != line_number || issue_ref.file_path != file_path
190 }) {
191 num_possible_issue_refs += 1;
192
193 issue_entry.push(PossibleIssueRef {
194 file_path,
195 line_number,
196 column,
197 });
198 }
199 }
200 }
201 }
202
203 let num_possible_issues = possible_issue_refs.len();
204
205 println!(
206 "\nFound {num_possible_issue_refs} possible references to {num_possible_issues} issues, checking statuses on Github..\n"
207 );
208
209 possible_issue_refs
210 };
211
212 let Some((_, github_token)) = env::vars().find(|(key, _)| key == GITHUB_TOKEN_ENV_KEY) else {
215 println!(
216 "Can't find {GITHUB_TOKEN_ENV_KEY} in env vars, printing all found possible issue refs, \
217see https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token \
218to create a github token."
219 );
220
221 for (
222 id,
223 PossibleIssueRef {
224 file_path,
225 line_number,
226 column,
227 },
228 ) in possible_issue_refs
229 .into_iter()
230 .flat_map(|(issue_id, issue_refs)| {
231 issue_refs
232 .into_iter()
233 .map(move |issue_ref| (issue_id.clone(), issue_ref))
234 })
235 {
236 let github_url = github_issue_url(&id);
237 let github_file_ref = github_remote_file_ref(&file_path, line_number);
238
239 println!("\n--------------------------------------");
240 println!("Found possible reference to closed issue #{id}: {file_path}:{line_number}:{column}");
241 println!("{github_file_ref}");
242 println!("{github_url}");
243 }
244
245 return Ok(());
246 };
247
248 let mut headers = HeaderMap::new();
249 let mut auth_value = HeaderValue::from_str(&format!("Bearer {github_token}"))?;
250 let accept_value = HeaderValue::from_static("application/vnd.github+json");
251 let github_api_version_value = HeaderValue::from_static("2022-11-28");
252 let user_agent_value = HeaderValue::from_static("search-issue-refs");
253
254 auth_value.set_sensitive(true);
255
256 headers.insert(header::AUTHORIZATION, auth_value);
257 headers.insert(header::ACCEPT, accept_value);
258 headers.insert("X-GitHub-Api-Version", github_api_version_value);
259 headers.insert(header::USER_AGENT, user_agent_value);
260
261 let client = ClientBuilder::new().default_headers(headers).build()?;
262
263 let latest_commit_json: serde_json::Value = serde_json::from_str::<serde_json::Value>(
266 &client
267 .get(github_ref_api_url("heads/main"))
268 .send()
269 .await?
270 .text()
271 .await?,
272 )?;
273
274 let latest_commit_sha = latest_commit_json["object"]["sha"]
275 .as_str()
276 .ok_or_else(|| eyre!("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}