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