this repo has no description

fix some bugs

+176 -129
+176 -129
src/main.rs
··· 3 3 use std::{ffi::OsStr, path::Path}; 4 4 5 5 use clap::Parser; 6 - use color_eyre::eyre::ContextCompat; 6 + use color_eyre::eyre::{Context as _, ContextCompat}; 7 7 use futures::TryStreamExt as _; 8 8 use octocrab::{Octocrab, models::pulls::PullRequest}; 9 9 use serde::Deserialize; ··· 12 12 13 13 mod graph; 14 14 15 - #[derive(Parser)] 15 + #[derive(Parser, Debug)] 16 16 struct Cli { 17 17 #[arg(short, long)] 18 18 create_new: bool, 19 19 } 20 20 21 - #[tokio::main] 21 + #[tokio::main(flavor = "current_thread")] 22 22 async fn main() -> color_eyre::Result<()> { 23 + color_eyre::install()?; 24 + 23 25 let cli = Cli::parse(); 24 26 25 - let graph = build_branch_graph()?; 27 + let graph = build_branch_graph().context("failed to build graph")?; 26 28 27 - let repo_info = repo_info()?; 29 + let repo_info = repo_info().context("failed to find repo info")?; 28 30 29 - let token = command("gh", ["auth", "token"])?; 31 + let token = command("gh", ["auth", "token"]).context("failed to find github auth token")?; 30 32 let token = token.trim().to_owned(); 31 33 let octocrab = octocrab::OctocrabBuilder::default() 32 34 .personal_token(token) 33 - .build()?; 35 + .build() 36 + .context("failed to build github client")?; 34 37 let mut pulls = octocrab 35 38 .pulls(&repo_info.owner, &repo_info.name) 36 39 .list() 37 40 .send() 38 - .await? 41 + .await 42 + .context("failed to fetch pull requests")? 39 43 .into_stream(&octocrab) 40 44 .try_collect::<Vec<_>>() 41 - .await?; 45 + .await 46 + .context("failed to fetch all pull requests")?; 42 47 43 - let mut commands = Vec::new(); 48 + for stack_root in graph.iter_edges_from("main") { 49 + find_or_create_prs( 50 + stack_root, "main", &graph, &repo_info, &octocrab, &cli, &mut pulls, 51 + ) 52 + .await 53 + .context("failed to sync prs")?; 54 + } 44 55 45 56 for stack_root in graph.iter_edges_from("main") { 46 57 let mut comment_lines = Vec::new(); 47 58 write_pr_comment(&graph, stack_root, 0, &mut comment_lines); 48 - 49 - process_branch(&mut commands, stack_root, "main", &graph, &comment_lines); 50 - } 51 - 52 - for command in commands { 53 - if let Err(err) = run_command(&command, &mut pulls, &octocrab, &repo_info, &cli).await { 54 - eprintln!("❌ {command:?} failed: {err:#}"); 55 - } 59 + create_or_update_comments( 60 + &comment_lines, 61 + stack_root, 62 + &graph, 63 + &pulls, 64 + &octocrab, 65 + &repo_info, 66 + ) 67 + .await 68 + .context("failed to sync stack comment")?; 56 69 } 57 70 58 71 Ok(()) ··· 124 137 } 125 138 126 139 let output = command("gh", ["repo", "view", "--json", "name,owner"])?; 127 - let output = serde_json::from_str::<Output>(&output)?; 140 + let output = 141 + serde_json::from_str::<Output>(&output).context("failed to parse json output from gh")?; 128 142 129 143 Ok(RepoInfo { 130 144 owner: output.owner.login, ··· 143 157 } 144 158 let output = cmd.output()?; 145 159 color_eyre::eyre::ensure!(output.status.success(), "{cmd:?} failed"); 146 - Ok(String::from_utf8(output.stdout)?) 160 + String::from_utf8(output.stdout).context("command returned invalid utf-8") 147 161 } 148 162 149 163 fn write_pr_comment(graph: &Graph, branch: &str, indent: usize, out: &mut Vec<CommentLine>) { ··· 196 210 197 211 const ID: &str = "e39f85cc-4589-41f7-9bae-d491c1ee2eda"; 198 212 199 - #[derive(Debug)] 200 - enum Commands { 201 - FindOrCreatePr { 202 - target: String, 203 - branch: String, 204 - }, 205 - CreateOrUpdateComment { 206 - comment_lines: Vec<CommentLine>, 207 - branch: String, 208 - }, 209 - } 210 - 211 - fn process_branch( 212 - commands: &mut Vec<Commands>, 213 + async fn find_or_create_prs( 213 214 branch: &str, 214 215 target: &str, 215 216 graph: &Graph, 216 - comment_lines: &[CommentLine], 217 - ) { 218 - commands.push(Commands::FindOrCreatePr { 219 - branch: branch.to_owned(), 220 - target: target.to_owned(), 221 - }); 222 - if comment_lines.len() > 1 { 223 - commands.push(Commands::CreateOrUpdateComment { 224 - branch: branch.to_owned(), 225 - comment_lines: comment_lines.to_vec(), 226 - }); 227 - } 217 + repo_info: &RepoInfo, 218 + octocrab: &Octocrab, 219 + cli: &Cli, 220 + pulls: &mut Vec<PullRequest>, 221 + ) -> color_eyre::Result<()> { 222 + find_or_create_pr(target, branch, pulls, octocrab, repo_info, cli) 223 + .await 224 + .with_context(|| format!("failed to find or create pr from {branch} into {target}"))?; 228 225 229 226 for child in graph.iter_edges_from(branch) { 230 - process_branch(commands, child, branch, graph, comment_lines); 227 + Box::pin(find_or_create_prs( 228 + child, branch, graph, repo_info, octocrab, cli, pulls, 229 + )) 230 + .await 231 + .with_context(|| format!("failed to find or create pr from {branch} into {target}"))?; 231 232 } 233 + 234 + Ok(()) 232 235 } 233 236 234 - async fn run_command( 235 - command: &Commands, 237 + fn finalize_comment( 238 + branch: &str, 239 + comment_lines: &[CommentLine], 240 + pulls: &[PullRequest], 241 + ) -> color_eyre::Result<String> { 242 + let mut comment = "This pull request is part of a stack:\n".to_owned(); 243 + for line in comment_lines { 244 + line.format(branch, pulls, &mut comment)?; 245 + comment.push('\n'); 246 + } 247 + comment.push_str("-------\n"); 248 + write!(comment, "_This comment was auto-generated (id: {ID})_").unwrap(); 249 + Ok(comment) 250 + } 251 + 252 + async fn find_or_create_pr( 253 + target: &str, 254 + branch: &str, 236 255 pulls: &mut Vec<PullRequest>, 237 256 octocrab: &Octocrab, 238 257 repo_info: &RepoInfo, 239 258 cli: &Cli, 240 259 ) -> color_eyre::Result<()> { 241 - match command { 242 - Commands::FindOrCreatePr { target, branch } => { 243 - if let Some((idx, pull)) = pulls 244 - .iter() 245 - .enumerate() 246 - .find(|(_, pull)| pull.head.ref_field == **branch) 247 - { 248 - if pull.base.ref_field != **target { 249 - eprintln!( 250 - "updating target of PR from {branch} from {} to {target}", 251 - pull.base.ref_field 252 - ); 253 - let updated = octocrab 254 - .pulls(&repo_info.owner, &repo_info.name) 255 - .update(pull.number) 256 - .base(target) 257 - .send() 258 - .await?; 259 - pulls[idx] = updated; 260 - } 261 - } else if cli.create_new { 262 - eprintln!("creating PR from {branch} into {target}"); 263 - let pull = octocrab 264 - .pulls(&repo_info.owner, &repo_info.name) 265 - .create(&**branch, target, &**branch) 266 - .draft(true) 267 - .send() 268 - .await?; 269 - pulls.push(pull); 270 - } else { 271 - eprintln!("skipping creating PR from {branch} into {target}"); 272 - } 260 + if let Some((idx, pull)) = pulls 261 + .iter() 262 + .enumerate() 263 + .find(|(_, pull)| pull.head.ref_field == branch) 264 + { 265 + if pull.base.ref_field != target { 266 + eprintln!( 267 + "updating target of PR #{number} from {prev_target}<-{branch} to {new_target}<-{branch}", 268 + number = pull.number, 269 + prev_target = pull.base.ref_field, 270 + new_target = target, 271 + ); 272 + let updated = octocrab 273 + .pulls(&repo_info.owner, &repo_info.name) 274 + .update(pull.number) 275 + .base(target) 276 + .send() 277 + .await 278 + .with_context(|| { 279 + format!( 280 + "failed updating target of PR #{number} from {prev_target}<-{branch} to {new_target}<-{branch}", 281 + number = pull.number, 282 + prev_target = pull.base.ref_field, 283 + new_target = target, 284 + ) 285 + })?; 286 + pulls[idx] = updated; 273 287 } 274 - Commands::CreateOrUpdateComment { 275 - comment_lines, 276 - branch, 277 - } => { 278 - let pull = pulls 279 - .iter() 280 - .find(|pull| pull.head.ref_field == *branch) 281 - .with_context(|| format!("PR from {branch} not found"))?; 288 + } else if cli.create_new { 289 + eprintln!("creating PR from {target}<-{branch}"); 290 + let pull = octocrab 291 + .pulls(&repo_info.owner, &repo_info.name) 292 + .create(branch, branch, target) 293 + .draft(true) 294 + .send() 295 + .await 296 + .with_context(|| format!("failed to create PR from {target}<-{branch}"))?; 297 + pulls.push(pull); 298 + } else { 299 + eprintln!("skipping creating PR from {target}<-{branch}"); 300 + } 282 301 283 - let comment = finalize_comment(branch, comment_lines, pulls)?; 302 + Ok(()) 303 + } 284 304 285 - let comment_stream = octocrab 286 - .issues(&repo_info.owner, &repo_info.name) 287 - .list_comments(pull.number) 288 - .send() 289 - .await? 290 - .into_stream(octocrab) 291 - .try_filter(|comment| { 292 - std::future::ready(comment.body.as_ref().is_some_and(|body| body.contains(ID))) 293 - }); 305 + async fn create_or_update_comments( 306 + comment_lines: &[CommentLine], 307 + branch: &str, 308 + graph: &Graph, 309 + pulls: &[PullRequest], 310 + octocrab: &Octocrab, 311 + repo_info: &RepoInfo, 312 + ) -> color_eyre::Result<()> { 313 + create_or_update_comment(comment_lines, branch, pulls, octocrab, repo_info).await?; 294 314 295 - if let Some(existing_comment) = pin!(comment_stream).try_next().await? { 296 - if existing_comment.body.is_none_or(|body| body != comment) { 297 - octocrab 298 - .issues(&repo_info.owner, &repo_info.name) 299 - .update_comment(existing_comment.id, comment) 300 - .await?; 301 - if let Some(url) = &pull.html_url { 302 - eprintln!("updated comment on {url}"); 303 - } 304 - } 305 - } else { 306 - octocrab 307 - .issues(&repo_info.owner, &repo_info.name) 308 - .create_comment(pull.number, comment) 309 - .await?; 310 - if let Some(url) = &pull.html_url { 311 - eprintln!("created comment on {url}"); 312 - } 313 - } 314 - } 315 + for child in graph.iter_edges_from(branch) { 316 + Box::pin(create_or_update_comments( 317 + comment_lines, 318 + child, 319 + graph, 320 + pulls, 321 + octocrab, 322 + repo_info, 323 + )) 324 + .await 325 + .context("failed to sync stack comment")?; 315 326 } 316 327 317 328 Ok(()) 318 329 } 319 330 320 - fn finalize_comment( 321 - branch: &str, 331 + async fn create_or_update_comment( 322 332 comment_lines: &[CommentLine], 333 + branch: &str, 323 334 pulls: &[PullRequest], 324 - ) -> color_eyre::Result<String> { 325 - let mut comment = "This pull request is part of a stack:\n".to_owned(); 326 - for line in comment_lines { 327 - line.format(branch, pulls, &mut comment)?; 328 - comment.push('\n'); 335 + octocrab: &Octocrab, 336 + repo_info: &RepoInfo, 337 + ) -> color_eyre::Result<()> { 338 + let pull = pulls 339 + .iter() 340 + .find(|pull| pull.head.ref_field == *branch) 341 + .with_context(|| format!("PR from {branch} not found"))?; 342 + 343 + let comment = 344 + finalize_comment(branch, comment_lines, pulls).context("failed to finalize comment")?; 345 + 346 + let comment_stream = octocrab 347 + .issues(&repo_info.owner, &repo_info.name) 348 + .list_comments(pull.number) 349 + .send() 350 + .await 351 + .context("failed to fetch comments")? 352 + .into_stream(octocrab) 353 + .try_filter(|comment| { 354 + std::future::ready(comment.body.as_ref().is_some_and(|body| body.contains(ID))) 355 + }); 356 + 357 + if let Some(existing_comment) = pin!(comment_stream).try_next().await? { 358 + if existing_comment.body.is_none_or(|body| body != comment) { 359 + octocrab 360 + .issues(&repo_info.owner, &repo_info.name) 361 + .update_comment(existing_comment.id, comment) 362 + .await 363 + .context("failed to update comment")?; 364 + if let Some(url) = &pull.html_url { 365 + eprintln!("updated comment on {url}"); 366 + } 367 + } 368 + } else { 369 + octocrab 370 + .issues(&repo_info.owner, &repo_info.name) 371 + .create_comment(pull.number, comment) 372 + .await 373 + .context("failed to create comment")?; 374 + if let Some(url) = &pull.html_url { 375 + eprintln!("created comment on {url}"); 376 + } 329 377 } 330 - comment.push_str("-------\n"); 331 - write!(comment, "_This comment was auto-generated (id: {ID})_").unwrap(); 332 - Ok(comment) 378 + 379 + Ok(()) 333 380 }