this repo has no description

Create PRs, handle commits in between branches

+353 -92
+122 -1
Cargo.lock
··· 33 33 ] 34 34 35 35 [[package]] 36 + name = "anstream" 37 + version = "0.6.19" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 40 + dependencies = [ 41 + "anstyle", 42 + "anstyle-parse", 43 + "anstyle-query", 44 + "anstyle-wincon", 45 + "colorchoice", 46 + "is_terminal_polyfill", 47 + "utf8parse", 48 + ] 49 + 50 + [[package]] 51 + name = "anstyle" 52 + version = "1.0.11" 53 + source = "registry+https://github.com/rust-lang/crates.io-index" 54 + checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 55 + 56 + [[package]] 57 + name = "anstyle-parse" 58 + version = "0.2.7" 59 + source = "registry+https://github.com/rust-lang/crates.io-index" 60 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 61 + dependencies = [ 62 + "utf8parse", 63 + ] 64 + 65 + [[package]] 66 + name = "anstyle-query" 67 + version = "1.1.3" 68 + source = "registry+https://github.com/rust-lang/crates.io-index" 69 + checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 70 + dependencies = [ 71 + "windows-sys 0.59.0", 72 + ] 73 + 74 + [[package]] 75 + name = "anstyle-wincon" 76 + version = "3.0.9" 77 + source = "registry+https://github.com/rust-lang/crates.io-index" 78 + checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 79 + dependencies = [ 80 + "anstyle", 81 + "once_cell_polyfill", 82 + "windows-sys 0.59.0", 83 + ] 84 + 85 + [[package]] 36 86 name = "arc-swap" 37 87 version = "1.7.1" 38 88 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 125 175 ] 126 176 127 177 [[package]] 178 + name = "clap" 179 + version = "4.5.41" 180 + source = "registry+https://github.com/rust-lang/crates.io-index" 181 + checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" 182 + dependencies = [ 183 + "clap_builder", 184 + "clap_derive", 185 + ] 186 + 187 + [[package]] 188 + name = "clap_builder" 189 + version = "4.5.41" 190 + source = "registry+https://github.com/rust-lang/crates.io-index" 191 + checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" 192 + dependencies = [ 193 + "anstream", 194 + "anstyle", 195 + "clap_lex", 196 + "strsim", 197 + ] 198 + 199 + [[package]] 200 + name = "clap_derive" 201 + version = "4.5.41" 202 + source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" 204 + dependencies = [ 205 + "heck", 206 + "proc-macro2", 207 + "quote", 208 + "syn", 209 + ] 210 + 211 + [[package]] 212 + name = "clap_lex" 213 + version = "0.7.5" 214 + source = "registry+https://github.com/rust-lang/crates.io-index" 215 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 216 + 217 + [[package]] 128 218 name = "color-eyre" 129 219 version = "0.6.5" 130 220 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 152 242 ] 153 243 154 244 [[package]] 245 + name = "colorchoice" 246 + version = "1.0.4" 247 + source = "registry+https://github.com/rust-lang/crates.io-index" 248 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 249 + 250 + [[package]] 155 251 name = "core-foundation" 156 252 version = "0.10.1" 157 253 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 602 698 ] 603 699 604 700 [[package]] 701 + name = "is_terminal_polyfill" 702 + version = "1.70.1" 703 + source = "registry+https://github.com/rust-lang/crates.io-index" 704 + checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 705 + 706 + [[package]] 605 707 name = "itoa" 606 708 version = "1.0.15" 607 709 source = "registry+https://github.com/rust-lang/crates.io-index" 608 710 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 609 711 610 712 [[package]] 611 - name = "jj-create-prs" 713 + name = "jj-sync-prs" 612 714 version = "0.1.0" 613 715 dependencies = [ 716 + "clap", 614 717 "color-eyre", 615 718 "futures", 616 719 "octocrab", ··· 795 898 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 796 899 797 900 [[package]] 901 + name = "once_cell_polyfill" 902 + version = "1.70.1" 903 + source = "registry+https://github.com/rust-lang/crates.io-index" 904 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 905 + 906 + [[package]] 798 907 name = "openssl-probe" 799 908 version = "0.1.6" 800 909 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1185 1294 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1186 1295 1187 1296 [[package]] 1297 + name = "strsim" 1298 + version = "0.11.1" 1299 + source = "registry+https://github.com/rust-lang/crates.io-index" 1300 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1301 + 1302 + [[package]] 1188 1303 name = "subtle" 1189 1304 version = "2.6.1" 1190 1305 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1479 1594 version = "1.0.4" 1480 1595 source = "registry+https://github.com/rust-lang/crates.io-index" 1481 1596 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1597 + 1598 + [[package]] 1599 + name = "utf8parse" 1600 + version = "0.2.2" 1601 + source = "registry+https://github.com/rust-lang/crates.io-index" 1602 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1482 1603 1483 1604 [[package]] 1484 1605 name = "valuable"
+2 -1
Cargo.toml
··· 1 1 [package] 2 - name = "jj-create-prs" 2 + name = "jj-sync-prs" 3 3 version = "0.1.0" 4 4 edition = "2024" 5 5 6 6 [dependencies] 7 + clap = { version = "4.5.41", features = ["derive"] } 7 8 color-eyre = "0.6.5" 8 9 futures = "0.3.31" 9 10 octocrab = { version = "0.44.1", features = ["stream"] }
+27
commands
··· 1 + jj log --no-graph -r 'bookmarks()' -T 'bookmarks ++ "\n"' 2 + 3 + jj log --no-graph -r 'children(push-wtqmqortkzry) & bookmarks()' -T 'bookmarks ++ "\n"' 4 + 5 + jj log --no-graph -r '..push-wtqmqortkzry & ..ng-3088-button-to-create-windows-from-window-notes' 6 + 7 + jj log --no-graph -r '..bookmarks()' 8 + 9 + jj log --no-graph -r 'root().. & bookmarks()' 10 + 11 + jj -r 'main---::' 12 + 13 + jj log --no-graph -T 'change_id ++ "\n"' 14 + 15 + jj log --no-graph -r 'children(ztzxrsrskxmyopropwxolytvzwlmlylu, 1)' -T 'change_id ++ " " ++ bookmarks ++ "\n"' 16 + 17 + jj log --no-graph -r 'ztzxrsrskxmyopropwxolytvzwlmlylu' -T 'change_id ++ " " ++ bookmarks ++ "\n"' 18 + 19 + ztzxrsrskxmyopropwxolytvzwlmlylu 20 + 21 + push-wtqmqortkzry 22 + ng-3088-button-to-create-windows-from-window-notes 23 + david/debug-menu-for-camera-layers 24 + david/port-ruler-to-new-snapping-25-07-04 25 + david/move-snapping-into-core-state 26 + david/simplify-coordinate-spaces 27 + main
+15
graph.dot
··· 1 + digraph Workflow { 2 + 0 [label="main"]; 3 + 1 [label="david/simplify-coordinate-spaces"]; 4 + 2 [label="david/move-snapping-into-core-state"]; 5 + 3 [label="david/port-ruler-to-new-snapping-25-07-04"]; 6 + 4 [label="david/debug-menu-for-camera-layers"]; 7 + 5 [label="ng-3088-button-to-create-windows-from-window-notes"]; 8 + 6 [label="push-wtqmqortkzry"]; 9 + 0 -> 1; 10 + 1 -> 2; 11 + 2 -> 3; 12 + 3 -> 4; 13 + 4 -> 5; 14 + 5 -> 6; 15 + }
+22 -1
src/graph.rs
··· 1 - use std::collections::{BTreeMap, BTreeSet}; 1 + use std::{ 2 + collections::{BTreeMap, BTreeSet}, 3 + fmt::Write as _, 4 + }; 2 5 3 6 #[derive(Debug, Default)] 4 7 pub struct Graph { ··· 6 9 edges: BTreeMap<usize, BTreeSet<usize>>, 7 10 } 8 11 12 + #[allow(dead_code)] 9 13 impl Graph { 10 14 pub fn get_or_insert(&mut self, node: &str) -> usize { 11 15 for (idx, n) in self.nodes.iter().enumerate() { ··· 51 55 (&**node, child) 52 56 }) 53 57 }) 58 + } 59 + 60 + pub fn to_dot(&self) -> String { 61 + let mut out = String::new(); 62 + 63 + writeln!(&mut out, "digraph Workflow {{").unwrap(); 64 + for (i, node) in self.nodes.iter().enumerate() { 65 + writeln!(&mut out, " {i} [label=\"{node}\"];").unwrap(); 66 + } 67 + for (from, edges) in &self.edges { 68 + for to in edges { 69 + writeln!(&mut out, " {from} -> {to};").unwrap(); 70 + } 71 + } 72 + writeln!(&mut out, "}}").unwrap(); 73 + 74 + out 54 75 } 55 76 }
+165 -89
src/main.rs
··· 1 1 use std::fmt::Write; 2 + use std::pin::pin; 2 3 use std::{ffi::OsStr, path::Path}; 3 4 5 + use clap::Parser; 6 + use color_eyre::eyre::ContextCompat; 4 7 use futures::TryStreamExt as _; 5 8 use octocrab::{Octocrab, models::pulls::PullRequest}; 6 9 use serde::Deserialize; ··· 9 12 10 13 mod graph; 11 14 15 + #[derive(Parser)] 16 + struct Cli { 17 + #[arg(short, long)] 18 + create_new: bool, 19 + } 20 + 12 21 #[tokio::main] 13 22 async fn main() -> color_eyre::Result<()> { 23 + let cli = Cli::parse(); 24 + 14 25 let graph = build_branch_graph()?; 15 26 16 27 let repo_info = repo_info()?; ··· 20 31 let octocrab = octocrab::OctocrabBuilder::default() 21 32 .personal_token(token) 22 33 .build()?; 23 - let pulls = octocrab 34 + let mut pulls = octocrab 24 35 .pulls(&repo_info.owner, &repo_info.name) 25 36 .list() 26 37 .send() ··· 29 40 .try_collect::<Vec<_>>() 30 41 .await?; 31 42 43 + let mut commands = Vec::new(); 44 + 32 45 for stack_root in graph.iter_edges_from("main") { 33 46 let mut comment_lines = Vec::new(); 34 47 write_pr_comment(&graph, stack_root, 0, &mut comment_lines); 35 48 36 - process_branch( 37 - stack_root, 38 - &graph, 39 - &pulls, 40 - &comment_lines, 41 - &octocrab, 42 - &repo_info, 43 - ) 44 - .await?; 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 + } 45 56 } 46 57 47 58 Ok(()) 48 59 } 49 60 50 61 fn build_branch_graph() -> color_eyre::Result<Graph> { 51 - let mut graph = Graph::default(); 52 - 53 - // jj log --no-graph -r 'bookmarks()' -T 'bookmarks ++ "\n"' 54 - // jj log --no-graph -r 'children(test) & bookmarks()' -T 'bookmarks ++ "\n"' 55 - 56 - let mut branches = command( 57 - "jj", 58 - [ 59 - "log", 60 - "--no-graph", 61 - "-r", 62 - "bookmarks()", 63 - "-T", 64 - "bookmarks ++ \"\\n\"", 65 - ], 66 - )? 67 - .lines() 68 - .map(|s| s.to_owned()) 69 - .collect::<Vec<_>>(); 70 - branches.reverse(); 71 - 72 - for parent in branches { 73 - let parent_node = graph.get_or_insert(&parent); 74 - 75 - let children = command( 62 + fn go(graph: &mut Graph, change: &str, parent_branch: &str) -> color_eyre::Result<()> { 63 + let output = command( 76 64 "jj", 77 65 [ 78 66 "log", 79 67 "--no-graph", 80 68 "-r", 81 - &format!("children({parent}) & bookmarks()"), 69 + &format!("children({change}, 1)"), 82 70 "-T", 83 - "bookmarks ++ \"\\n\"", 71 + "change_id ++ \" \" ++ bookmarks ++ \"\n\"", 84 72 ], 85 73 )?; 86 74 87 - for child in children.lines() { 88 - let child_node = graph.get_or_insert(child); 89 - graph.add_edge(parent_node, child_node); 75 + for line in output.lines() { 76 + let (change, branch) = if let Some((change, branch)) = line.trim().split_once(' ') { 77 + (change, Some(branch.trim_matches('*'))) 78 + } else { 79 + (line, None) 80 + }; 81 + 82 + if let Some(branch) = branch { 83 + if parent_branch != branch { 84 + let parent_branch_node = graph.get_or_insert(parent_branch); 85 + let branch_node = graph.get_or_insert(branch); 86 + graph.add_edge(parent_branch_node, branch_node); 87 + } 88 + go(graph, change, branch)?; 89 + } else { 90 + go(graph, change, parent_branch)?; 91 + } 90 92 } 93 + 94 + Ok(()) 91 95 } 96 + 97 + let mut graph = Graph::default(); 98 + 99 + let output = command("jj", ["log", "--no-graph", "-T", "change_id ++ \"\n\""])?; 100 + let mut output = output.lines(); 101 + let common_ancestor = output.next_back().context("no lines")?; 102 + 103 + go(&mut graph, common_ancestor, "main")?; 92 104 93 105 Ok(graph) 94 106 } ··· 145 157 } 146 158 } 147 159 148 - #[derive(Debug)] 160 + #[derive(Debug, Clone)] 149 161 struct CommentLine { 150 162 branch: String, 151 163 indent: usize, ··· 154 166 impl CommentLine { 155 167 fn format( 156 168 &self, 157 - branch: &str, 169 + head_branch: &str, 158 170 pulls: &[PullRequest], 159 171 out: &mut String, 160 172 ) -> color_eyre::Result<()> { 161 - let Some((pull_title, pull_url)) = pulls 173 + let (pull_title, pull_url) = pulls 162 174 .iter() 163 175 .find(|pull| pull.head.ref_field == self.branch) 164 176 .and_then(|pull| { ··· 166 178 let title = pull.title.as_deref()?; 167 179 Some((title, url)) 168 180 }) 169 - else { 170 - color_eyre::eyre::bail!("no pr"); 171 - }; 181 + .with_context(|| format!("PR from {} not found", self.branch))?; 172 182 173 183 for c in std::iter::repeat_n(' ', self.indent) { 174 184 write!(out, "{c}").unwrap(); ··· 176 186 write!(out, "- ").unwrap(); 177 187 178 188 write!(out, "[{pull_title}]({pull_url})").unwrap(); 179 - if branch == self.branch { 189 + if head_branch == self.branch { 180 190 write!(out, " 👈 you are here").unwrap(); 181 191 } 182 192 ··· 186 196 187 197 const ID: &str = "e39f85cc-4589-41f7-9bae-d491c1ee2eda"; 188 198 189 - async fn process_branch( 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>, 190 213 branch: &str, 214 + target: &str, 191 215 graph: &Graph, 192 - pulls: &[PullRequest], 193 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 + } 228 + 229 + for child in graph.iter_edges_from(branch) { 230 + process_branch(commands, child, branch, graph, comment_lines); 231 + } 232 + } 233 + 234 + async fn run_command( 235 + command: &Commands, 236 + pulls: &mut Vec<PullRequest>, 194 237 octocrab: &Octocrab, 195 238 repo_info: &RepoInfo, 239 + cli: &Cli, 196 240 ) -> color_eyre::Result<()> { 197 - if let Some(pull) = pulls.iter().find(|pull| pull.head.ref_field == branch) { 198 - let mut comment = "This pull request is part of a stack:\n".to_owned(); 199 - for line in comment_lines { 200 - if line.format(branch, pulls, &mut comment).is_ok() { 201 - comment.push('\n'); 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}"); 202 272 } 203 273 } 204 - comment.push_str("-------\n"); 205 - write!(comment, "_This comment was auto-generated (id: {ID})_").unwrap(); 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"))?; 206 282 207 - let mut stream = std::pin::pin!( 208 - octocrab 283 + let comment = finalize_comment(branch, comment_lines, pulls)?; 284 + 285 + let comment_stream = octocrab 209 286 .issues(&repo_info.owner, &repo_info.name) 210 287 .list_comments(pull.number) 211 288 .send() ··· 213 290 .into_stream(octocrab) 214 291 .try_filter(|comment| { 215 292 std::future::ready(comment.body.as_ref().is_some_and(|body| body.contains(ID))) 216 - }) 217 - ); 293 + }); 218 294 219 - if let Some(existing_comment) = stream.try_next().await? { 220 - if existing_comment.body.is_none_or(|body| body != comment) { 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 { 221 306 octocrab 222 - .issues("lun-energy", "web-main") 223 - .update_comment(existing_comment.id, comment) 307 + .issues(&repo_info.owner, &repo_info.name) 308 + .create_comment(pull.number, comment) 224 309 .await?; 225 310 if let Some(url) = &pull.html_url { 226 - println!("Updated comment on {url}"); 311 + eprintln!("created comment on {url}"); 227 312 } 228 - } else if let Some(url) = &pull.html_url { 229 - println!("{url} is up to date"); 230 - } 231 - } else { 232 - octocrab 233 - .issues("lun-energy", "web-main") 234 - .create_comment(pull.number, comment) 235 - .await?; 236 - if let Some(url) = &pull.html_url { 237 - println!("Created comment on {url}"); 238 313 } 239 314 } 240 - } else { 241 - println!("`{branch}` has no pull request"); 242 315 } 243 316 244 - for child in graph.iter_edges_from(branch) { 245 - Box::pin(process_branch( 246 - child, 247 - graph, 248 - pulls, 249 - comment_lines, 250 - octocrab, 251 - repo_info, 252 - )) 253 - .await?; 317 + Ok(()) 318 + } 319 + 320 + fn finalize_comment( 321 + branch: &str, 322 + comment_lines: &[CommentLine], 323 + 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'); 254 329 } 255 - 256 - Ok(()) 330 + comment.push_str("-------\n"); 331 + write!(comment, "_This comment was auto-generated (id: {ID})_").unwrap(); 332 + Ok(comment) 257 333 }
workflow.png

This is a binary file and will not be displayed.