f677d627 Andy McCall 23 days ago ↶
1
2
3
4
5
6
7
8
9
10
use crate :: auth:: roles;
11
use crate :: db:: { self , accounts:: Account, repos:: RepoRow, Pool} ;
12
use crate :: repos:: RepoName;
13
use crate :: Shared;
14
use oxy_core:: hash:: ChangeHash;
15
use serde:: Serialize;
16
17
18
pub const LEARN_DAYS : i64 = 90 ;
19
20
21
22
23
pub async fn target ( pool : & Pool, row : & RepoRow) -> String {
24
let since = db:: now_ms( ) - LEARN_DAYS * 24 * 3600 * 1000 ;
25
let learned: Option< String> = sqlx:: query_scalar(
5d90e0e0 Andy McCall 19 days ago
26
" SELECT target FROM proposals WHERE repo_id = $1 AND state = 'landed' AND landed_ms >= $2
f677d627 Andy McCall 23 days ago ↶
27
GROUP BY target ORDER BY COUNT(*) DESC, MAX(landed_ms) DESC LIMIT 1" ,
28
)
29
.bind ( row.id)
30
.bind ( since)
31
.fetch_optional ( pool)
32
.await
33
.ok ( )
34
.flatten ( ) ;
35
learned.filter ( | t | ! t.is_empty ( ) ) .unwrap_or_else ( | | row.default_bookmark.clone ( ) )
36
}
37
38
39
40
pub async fn followed_bookmarks ( pool : & Pool, row : & RepoRow) -> Vec< String> {
41
let mut out = vec! [ row.default_bookmark.clone ( ) ] ;
42
let t = target ( pool, row) .await;
43
if ! out.contains ( & t) {
44
out.push ( t) ;
45
}
46
out
47
}
48
49
50
pub async fn record ( app : & Shared, row : & RepoRow, bookmark : & str , heads : & [ChangeHash]) {
5d54cf74 Andy McCall 19 days ago ↶
51
52
53
let repo = format! ( " {} /{} " , row.owner, row.name) ;
54
for attempt in 1 .. = 2 {
55
match try_record ( app, row, bookmark, heads) .await {
56
Ok ( ( ) ) => {
57
if attempt > 1 {
58
tracing:: info! ( " diffusion: indexed {repo} {bookmark} on the second try" ) ;
59
}
60
return ;
61
}
62
Err ( why) if attempt = = 1 => {
63
tracing:: warn! ( " diffusion: indexing {repo} {bookmark} failed ({why}); trying again" ) ;
64
tokio:: time:: sleep( RETRY_AFTER ) .await;
65
}
66
Err ( why) => {
67
FAILURES .fetch_add ( 1 , std:: sync:: atomic:: Ordering:: Relaxed) ;
68
tracing:: error! ( " diffusion: indexing {repo} {bookmark} failed again ({why}); its pins stay as they were" ) ;
69
}
70
}
71
}
72
}
73
74
75
const RETRY_AFTER : std:: time:: Duration = std:: time:: Duration:: from_millis( 250 ) ;
76
77
78
pub static FAILURES : std:: sync:: atomic:: AtomicU64 = std:: sync:: atomic:: AtomicU64:: new( 0 ) ;
79
80
81
static FORCED : std:: sync:: Mutex< Option< std:: collections:: HashMap< String, u32 > > > = std:: sync:: Mutex:: new( None ) ;
82
83
84
85
# [ doc ( hidden ) ]
86
pub fn fail_next_records ( app : & Shared, repo_id : i64 , n : u32 ) {
87
FORCED .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) .get_or_insert_with ( Default :: default) .insert ( key ( app, repo_id) , n) ;
88
}
89
90
91
# [ doc ( hidden ) ]
92
pub fn forced_left ( app : & Shared, repo_id : i64 ) -> u32 {
93
FORCED .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) .as_ref ( ) .and_then ( | m | m.get ( & key ( app, repo_id) ) .copied ( ) ) .unwrap_or ( 0 )
94
}
95
96
fn forced ( app : & Shared, repo_id : i64 ) -> bool {
97
let mut forced = FORCED .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) ;
98
match forced.as_mut ( ) .and_then ( | m | m.get_mut ( & key ( app, repo_id) ) ) {
99
Some ( n) if * n > 0 => {
100
* n - = 1 ;
101
true
102
}
103
_ => false ,
104
}
105
}
106
107
108
109
110
async fn try_record ( app : & Shared, row : & RepoRow, bookmark : & str , heads : & [ChangeHash]) -> Result< ( ) , String> {
111
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { return Ok ( ( ) ) } ;
112
let pins = if heads.is_empty ( ) {
113
Vec :: new( )
114
} else {
115
crate :: flow:: pins_in( app, name, heads.to_vec ( ) ) .await.map_err ( | e | format! ( " reading the pins: {e} " ) ) ?
116
} ;
f677d627 Andy McCall 23 days ago ↶
117
let now = db:: now_ms( ) ;
757195ad Andy McCall 22 days ago ↶
118
119
let mut upstreams = Vec :: new( ) ;
120
for ( _ , id, _ ) in & pins {
121
upstreams.push ( repo_by_oxy_id ( app, & hex:: encode( id) ) .await.map ( | r | r.id ) ) ;
122
}
5d54cf74 Andy McCall 19 days ago ↶
123
if forced ( app, row.id) {
124
return Err ( " begin: forced for a test" .into ( ) ) ;
125
}
126
let mut tx = app.db.begin ( ) .await.map_err ( | e | format! ( " begin: {e} " ) ) ? ;
5d90e0e0 Andy McCall 19 days ago
127
sqlx:: query( " DELETE FROM diffusion_pins WHERE consumer_id = $1 AND bookmark = $2" )
f677d627 Andy McCall 23 days ago ↶
128
.bind ( row.id)
129
.bind ( bookmark)
130
.execute ( & mut * tx)
131
.await
5d54cf74 Andy McCall 19 days ago ↶
132
.map_err ( | e | format! ( " delete: {e} " ) ) ? ;
757195ad Andy McCall 22 days ago ↶
133
for ( ( path, id, pinned) , upstream_row) in pins.iter ( ) .zip ( & upstreams) {
f677d627 Andy McCall 23 days ago ↶
134
let heads: Vec< String> = pinned.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ;
5d54cf74 Andy McCall 19 days ago ↶
135
sqlx:: query(
5d90e0e0 Andy McCall 19 days ago
136
" INSERT INTO diffusion_pins (consumer_id, bookmark, path, upstream, heads, updated_ms, upstream_repo_id) VALUES ($1, $2, $3, $4, $5, $6, $7)" ,
757195ad Andy McCall 22 days ago ↶
137
)
138
.bind ( row.id)
139
.bind ( bookmark)
140
.bind ( path)
141
.bind ( hex:: encode( id) )
142
.bind ( heads.join ( " +" ) )
143
.bind ( now)
144
.bind ( upstream_row)
145
.execute ( & mut * tx)
146
.await
5d54cf74 Andy McCall 19 days ago ↶
147
.map_err ( | e | format! ( " insert {path} : {e} " ) ) ? ;
f677d627 Andy McCall 23 days ago ↶
148
}
d1bf2d69 Andy McCall 19 days ago ↶
149
sqlx:: query( " INSERT INTO diffusion_indexed (consumer_id, bookmark, heads, updated_ms) VALUES ($1, $2, $3, $4) ON CONFLICT (consumer_id, bookmark) DO UPDATE SET heads = excluded.heads, updated_ms = excluded.updated_ms" )
0e04dfc2 Andy McCall 19 days ago ↶
150
.bind ( row.id)
151
.bind ( bookmark)
152
.bind ( heads_key ( heads) )
153
.bind ( now)
154
.execute ( & mut * tx)
155
.await
156
.map_err ( | e | format! ( " noting the state: {e} " ) ) ? ;
5d54cf74 Andy McCall 19 days ago ↶
157
tx.commit ( ) .await.map_err ( | e | format! ( " commit: {e} " ) ) ? ;
158
for u in upstreams.iter ( ) .flatten ( ) {
159
mark ( app, row.id) ;
160
mark ( app, * u) ;
f677d627 Andy McCall 23 days ago ↶
161
}
5d54cf74 Andy McCall 19 days ago ↶
162
Ok ( ( ) )
f677d627 Andy McCall 23 days ago ↶
163
}
164
165
166
pub async fn after_push ( app : & Shared, row : & RepoRow, moved : & [( String , String , Vec <ChangeHash >) ]) {
167
let followed = followed_bookmarks ( & app.db, row) .await;
168
for ( bookmark, _ , heads) in moved {
169
if followed.contains ( bookmark) {
170
record ( app, row, bookmark, heads) .await;
171
}
172
}
757195ad Andy McCall 22 days ago ↶
173
}
174
175
static INVOLVED : std:: sync:: Mutex< Option< std:: collections:: HashSet< String> > > = std:: sync:: Mutex:: new( None ) ;
176
177
fn key ( app : & Shared, repo_id : i64 ) -> String {
178
format! ( " {} #{repo_id} " , app.repos.root ( ) .display ( ) )
179
}
180
181
fn mark ( app : & Shared, repo_id : i64 ) {
182
INVOLVED .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) .get_or_insert_with ( Default :: default) .insert ( key ( app, repo_id) ) ;
f677d627 Andy McCall 23 days ago ↶
183
}
184
757195ad Andy McCall 22 days ago ↶
185
186
187
pub fn involved ( app : & Shared, repo_id : i64 ) -> bool {
188
INVOLVED .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) .as_ref ( ) .is_some_and ( | s | s.contains ( & key ( app, repo_id) ) )
189
}
190
f677d627 Andy McCall 23 days ago ↶
191
192
193
pub async fn backfill ( app : & Shared) {
194
let Ok ( rows) = db:: repos:: list( & app.db) .await else { return } ;
195
for row in rows {
196
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { continue } ;
197
for bookmark in followed_bookmarks ( & app.db, & row) .await {
198
let b = bookmark.clone ( ) ;
199
let heads = app.repos.read ( & name, move | r, _ | heads_of ( r, & b) ) .await;
200
if let Ok ( heads) = heads {
201
record ( app, & row, & bookmark, & heads) .await;
0e04dfc2 Andy McCall 19 days ago ↶
202
}
203
}
204
}
205
}
206
207
208
fn heads_key ( heads : & [ChangeHash]) -> String {
209
let mut h: Vec< String> = heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ;
210
h.sort ( ) ;
211
h.join ( " +" )
212
}
213
214
215
pub const RECONCILE_EVERY : std:: time:: Duration = std:: time:: Duration:: from_secs( 10 * 60 ) ;
216
217
218
# [ derive ( Debug, Default, PartialEq, Eq ) ]
219
pub struct Repairs {
220
221
pub reindexed : usize ,
222
223
224
pub caught_up : usize ,
225
}
226
227
228
229
pub async fn reconciler ( app : Shared) {
230
loop {
231
tokio:: time:: sleep( RECONCILE_EVERY ) .await;
232
let r = reconcile ( & app) .await;
233
if r ! = Repairs:: default( ) {
234
tracing:: warn! ( " diffusion: reconciled: {} bookmarks re-indexed, {} consumers caught up" , r.reindexed, r.caught_up) ;
235
}
236
}
237
}
238
239
240
241
242
pub async fn reconcile ( app : & Shared) -> Repairs {
243
let mut repairs = Repairs:: default( ) ;
244
let Ok ( rows) = db:: repos:: list( & app.db) .await else { return repairs } ;
245
for row in & rows {
246
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { continue } ;
247
for bookmark in followed_bookmarks ( & app.db, row) .await {
248
let b = bookmark.clone ( ) ;
249
let Ok ( heads) = app.repos.read ( & name, move | r, _ | heads_of ( r, & b) ) .await else { continue } ;
5d90e0e0 Andy McCall 19 days ago
250
let indexed: Option< String> =
251
sqlx:: query_scalar( " SELECT heads FROM diffusion_indexed WHERE consumer_id = $1 AND bookmark = $2" )
252
.bind ( row.id)
253
.bind ( & bookmark)
254
.fetch_optional ( & app.db)
255
.await
256
.ok ( )
257
.flatten ( ) ;
0e04dfc2 Andy McCall 19 days ago ↶
258
if indexed.as_deref ( ) ! = Some ( heads_key ( & heads) .as_str ( ) ) {
259
tracing:: info! ( " diffusion: {}/{} {bookmark}: the index was not at the bookmark; re-indexing" , row.owner, row.name) ;
260
record ( app, row, & bookmark, & heads) .await;
261
repairs.reindexed + = 1 ;
f677d627 Andy McCall 23 days ago ↶
262
}
0e04dfc2 Andy McCall 19 days ago ↶
263
}
264
}
265
266
let pins: Vec< ( i64 , String, String, String) > =
267
sqlx:: query_as( " SELECT consumer_id, path, upstream, heads FROM diffusion_pins WHERE upstream_repo_id IS NOT NULL" )
268
.fetch_all ( & app.db)
269
.await
270
.unwrap_or_default ( ) ;
271
let mut caught = std:: collections:: HashSet:: new( ) ;
272
for ( consumer_id, path, upstream, pinned) in pins {
273
if caught.contains ( & consumer_id) {
274
continue ;
275
}
276
let Some ( up) = repo_by_oxy_id ( app, & upstream) .await else { continue } ;
277
let Some ( green) = last_green ( app, & up) .await else { continue } ;
278
if heads_key ( & green) = = heads_key ( & pinned.split ( ' +' ) .filter_map ( ChangeHash:: from_hex) .collect:: < Vec< _ > > ( ) ) {
279
continue ;
280
}
281
let Ok ( Some ( consumer) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
282
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { continue } ;
283
let learned = target ( & app.db, & consumer) .await;
284
let modules = oxymodules ( app, & cname, & learned) .await;
285
if rule ( & app.config.diffusion.follow, & consumer, modules.as_deref ( ) , & path) .follow = = Follow:: Off {
286
continue ;
287
}
288
289
290
291
let bump_bm = bump_bookmark ( & path) ;
292
let newest = | | async {
293
db:: proposals:: list( & app.db, consumer_id, & [ ] )
294
.await
295
.unwrap_or_default ( )
296
.into_iter ( )
297
.filter ( | p | p.source_bookmark = = bump_bm )
298
.max_by_key ( | p | p.number )
299
.map ( | p | p.state )
300
} ;
301
let has_bump = | | async { matches! ( newest ( ) .await.as_deref ( ) , Some ( " open" | " draft" ) ) } ;
302
if matches! ( newest ( ) .await.as_deref ( ) , Some ( " open" | " draft" | " closed" ) ) {
303
continue ;
304
}
305
caught.insert ( consumer_id) ;
306
catch_up ( app, & consumer) .await;
307
if has_bump ( ) .await {
308
tracing:: info! (
309
" diffusion: {}/{} {path} was behind {}/{} with no bump; bumped" ,
310
consumer.owner,
311
consumer.name,
312
up.owner,
313
up.name
314
) ;
315
repairs.caught_up + = 1 ;
f677d627 Andy McCall 23 days ago ↶
316
}
317
}
0e04dfc2 Andy McCall 19 days ago ↶
318
repairs
f677d627 Andy McCall 23 days ago ↶
319
}
320
321
322
323
fn heads_of ( r : & oxy_core:: repo:: Repo, bookmark : & str ) -> Result< Vec< ChangeHash> , crate:: repos:: RepoError> {
324
Ok ( r.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) )
325
}
326
327
328
# [ derive ( Clone, Debug, Serialize ) ]
329
pub struct Consumer {
330
pub owner : String,
331
pub name : String,
332
pub bookmark : String,
333
pub path : String,
334
335
pub pinned : Vec< String> ,
336
337
338
pub behind : Option< usize > ,
757195ad Andy McCall 22 days ago ↶
339
340
pub follow : Follow,
341
342
pub bump : Option< i64 > ,
343
344
pub run : Option< ( i64 , String) > ,
345
346
pub landed : Option< ( i64 , i64 ) > ,
347
348
pub colour : Colour,
73ef9f76 Andy McCall 22 days ago ↶
349
350
pub upstream : Half,
351
pub downstream : Half,
757195ad Andy McCall 22 days ago ↶
352
}
353
eb2a3502 Andy McCall 22 days ago ↶
354
impl Consumer {
355
356
pub fn api ( & self , upstream : & str ) -> hub_api:: DiffusionPin {
357
let half = | h : & Half| hub_api:: DiffusionHalf {
358
tone: h.class.trim_start_matches ( " df-" ) .to_string ( ) ,
359
word: h.word.to_string ( ) ,
360
title: h.title.clone ( ) ,
361
} ;
362
hub_api:: DiffusionPin {
363
upstream: upstream.to_string ( ) ,
364
consumer: format! ( " {} /{} " , self .owner, self .name) ,
365
path: self .path.clone ( ) ,
366
bookmark: self .bookmark.clone ( ) ,
367
behind: self .behind.map ( | n | n as u64 ) ,
368
follow: self .follow.as_str ( ) .to_string ( ) ,
369
bump: self .bump,
370
run: self .run.as_ref ( ) .map ( | r | r.0 ) ,
371
run_status: self .run.as_ref ( ) .map ( | r | r.1. clone ( ) ) ,
372
upstream_state: half ( & self .upstream) ,
373
consumer_state: half ( & self .downstream) ,
374
}
375
}
376
}
377
73ef9f76 Andy McCall 22 days ago ↶
378
379
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize ) ]
380
pub struct Half {
381
pub class : & 'static str ,
382
pub word : & 'static str ,
383
pub title : String,
384
}
385
386
impl Half {
387
388
pub fn upstream ( unverified : bool , run : Option< & str > , slug : & str ) -> Half {
389
let ( class, word, title) = match ( unverified, run) {
390
( true , _ ) => ( " df-amber" , " untested" , format! ( " {slug} has no checks of its own: nothing has tested it" ) ) ,
391
( false , Some ( " passed" ) ) => ( " df-green" , " checked" , format! ( " {slug} 's newest run passed" ) ) ,
392
( false , Some ( " failed" | " error" | " cancelled" ) ) => ( " df-red" , " failing" , format! ( " {slug} 's newest run failed" ) ) ,
393
( false , _ ) => ( " df-grey" , " testing" , format! ( " {slug} 's newest run has not finished" ) ) ,
394
} ;
395
Half { class, word, title }
396
}
397
398
399
pub fn downstream ( follow : Follow, behind : Option< usize > , bump : Option< i64 > , run : Option< & str > , name : & str ) -> Half {
400
let ( class, word, title) = if follow = = Follow:: Off {
401
( " df-off" , " not following" , format! ( " {name} does not follow it" ) )
402
} else {
403
match ( bump, run) {
404
( Some ( _ ) , Some ( " passed" ) ) => ( " df-green" , " passes" , format! ( " {name} 's run on the bump passed" ) ) ,
405
( Some ( _ ) , Some ( " failed" | " error" | " cancelled" ) ) => ( " df-red" , " fails" , format! ( " {name} 's run on the bump failed" ) ) ,
406
( Some ( _ ) , Some ( _ ) ) => ( " df-grey" , " testing" , format! ( " {name} 's run on the bump has not finished" ) ) ,
407
( Some ( _ ) , None ) => ( " df-grey" , " waiting" , format! ( " {name} 's bump has no run yet" ) ) ,
408
( None , _ ) if behind = = Some ( 0 ) => ( " df-green" , " up to date" , format! ( " {name} pins the newest state" ) ) ,
409
( None , _ ) => ( " df-grey" , " waiting" , format! ( " {name} is behind; its bump has not been made yet" ) ) ,
410
}
411
} ;
412
Half { class, word, title }
413
}
414
}
415
757195ad Andy McCall 22 days ago ↶
416
417
418
419
420
421
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize ) ]
422
# [ serde ( rename_all = " kebab-case" ) ]
423
pub enum Colour {
424
Green,
425
Amber,
426
Red,
427
Grey,
428
UpToDate,
429
Off,
f677d627 Andy McCall 23 days ago ↶
430
}
431
757195ad Andy McCall 22 days ago ↶
432
impl Colour {
433
pub fn class ( & self ) -> & 'static str {
434
match self {
435
Colour:: Green => " df-green" ,
436
Colour:: Amber => " df-amber" ,
437
Colour:: Red => " df-red" ,
438
Colour:: Grey => " df-grey" ,
439
Colour:: UpToDate => " df-current" ,
440
Colour:: Off => " df-off" ,
441
}
442
}
443
444
pub fn label ( & self ) -> & 'static str {
445
match self {
446
Colour:: Green => " green" ,
447
Colour:: Amber => " amber: upstream unverified" ,
448
Colour:: Red => " red: its run failed" ,
449
Colour:: Grey => " testing" ,
450
Colour:: UpToDate => " up to date" ,
451
Colour:: Off => " not following" ,
452
}
453
}
454
455
456
pub fn of ( follow : Follow, behind : Option< usize > , bump : Option< i64 > , run : Option< & str > , unverified : bool ) -> Colour {
457
if follow = = Follow:: Off {
458
return Colour:: Off;
459
}
460
match ( bump, run) {
461
( Some ( _ ) , Some ( " passed" ) ) if unverified => Colour:: Amber,
462
( Some ( _ ) , Some ( " passed" ) ) => Colour:: Green,
463
( Some ( _ ) , Some ( " failed" | " error" | " cancelled" ) ) => Colour:: Red,
464
( Some ( _ ) , _ ) => Colour:: Grey,
465
( None , _ ) if behind = = Some ( 0 ) => Colour:: UpToDate,
466
( None , _ ) => Colour:: Grey,
467
}
468
}
469
}
470
f677d627 Andy McCall 23 days ago ↶
471
472
473
pub async fn consumers ( app : & Shared, row : & RepoRow, account : Option< & Account> ) -> Result< Vec< Consumer> , crate:: repos:: RepoError> {
474
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { return Ok ( Vec :: new( ) ) } ;
475
let id = app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await? ;
476
let found: Vec< ( i64 , String, String, String) > = sqlx:: query_as(
5d90e0e0 Andy McCall 19 days ago
477
" SELECT consumer_id, bookmark, path, heads FROM diffusion_pins WHERE upstream = $1 ORDER BY consumer_id, bookmark, path" ,
f677d627 Andy McCall 23 days ago ↶
478
)
479
.bind ( hex:: encode( id) )
480
.fetch_all ( & app.db)
481
.await
482
.map_err ( | e | crate :: repos:: RepoError:: Io( std:: io:: Error:: other( e) ) ) ? ;
483
let mut out = Vec :: new( ) ;
484
let mut pinned_sets = Vec :: new( ) ;
757195ad Andy McCall 22 days ago ↶
485
let unverified = unverified ( app, row) .await;
f677d627 Andy McCall 23 days ago ↶
486
for ( consumer_id, bookmark, path, heads) in found {
487
let Ok ( Some ( c) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
488
if ! roles:: allows( roles:: role_for( & app.db, account, & c) .await.ok ( ) .flatten ( ) , crate :: auth:: Access:: Read) {
757195ad Andy McCall 22 days ago ↶
489
continue ;
490
}
491
492
if bookmark ! = target ( & app.db, & c) .await {
f677d627 Andy McCall 23 days ago ↶
493
continue ;
494
}
495
let pinned: Vec< String> = heads.split ( ' +' ) .filter ( | h | ! h.is_empty ( ) ) .map ( String :: from) .collect ( ) ;
496
pinned_sets.push ( pinned.iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect:: < Vec< _ > > ( ) ) ;
757195ad Andy McCall 22 days ago ↶
497
let bump_bm = bump_bookmark ( & path) ;
498
let mine: Vec< db:: proposals:: ProposalRow> = db:: proposals:: list( & app.db, c.id, & [ ] )
499
.await
500
.unwrap_or_default ( )
501
.into_iter ( )
502
.filter ( | p | p.source_bookmark = = bump_bm )
503
.collect ( ) ;
504
let bump = mine.iter ( ) .find ( | p | p.state = = " open" | | p.state = = " draft" ) .map ( | p | p.number ) ;
505
let landed = mine.iter ( ) .filter ( | p | p.state = = " landed" ) .max_by_key ( | p | p.landed_ms ) .map ( | p | ( p.number, p.landed_ms) ) ;
506
let run: Option< ( i64 , String) > =
5d90e0e0 Andy McCall 19 days ago
507
sqlx:: query_as( " SELECT number, status FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 ORDER BY id DESC LIMIT 1" )
757195ad Andy McCall 22 days ago ↶
508
.bind ( c.id)
509
.bind ( & bump_bm)
510
.fetch_optional ( & app.db)
511
.await
512
.ok ( )
513
.flatten ( ) ;
514
let follow = rule ( & app.config.diffusion.follow, & c, None , & path) .follow;
515
out.push ( Consumer {
516
owner: c.owner,
517
name: c.name,
518
bookmark,
519
path,
520
pinned,
521
behind: None ,
522
follow,
523
bump,
524
run,
525
landed,
526
colour: Colour:: Off,
73ef9f76 Andy McCall 22 days ago ↶
527
upstream: Half { class: " df-grey" , word: " " , title: String :: new( ) } ,
528
downstream: Half { class: " df-grey" , word: " " , title: String :: new( ) } ,
757195ad Andy McCall 22 days ago ↶
529
} ) ;
f677d627 Andy McCall 23 days ago ↶
530
}
531
if out.is_empty ( ) {
532
return Ok ( out) ;
533
}
534
let default = row.default_bookmark.clone ( ) ;
535
let behind = app
536
.repos
537
.read ( & name, move | r, cache| {
538
let tip = heads_of ( r, & default) ? ;
539
let tip = cache.summary ( r, & tip) ? ;
540
Ok ( pinned_sets
541
.iter ( )
542
.map ( | p | cache.summary ( r, p) .ok ( ) .map ( | s | tip.changes.iter ( ) .filter ( | c | ! s.changes.contains ( * c) ) .count ( ) ) )
543
.collect:: < Vec< _ > > ( ) )
544
} )
545
.await? ;
73ef9f76 Andy McCall 22 days ago ↶
546
let up_run: Option< String> =
5d90e0e0 Andy McCall 19 days ago
547
sqlx:: query_scalar( " SELECT status FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 ORDER BY id DESC LIMIT 1" )
73ef9f76 Andy McCall 22 days ago ↶
548
.bind ( row.id)
549
.bind ( & row.default_bookmark)
550
.fetch_optional ( & app.db)
551
.await
552
.ok ( )
553
.flatten ( ) ;
554
let slug = format! ( " {} /{} " , row.owner, row.name) ;
f677d627 Andy McCall 23 days ago ↶
555
for ( c, b) in out.iter_mut ( ) .zip ( behind) {
556
c.behind = b;
73ef9f76 Andy McCall 22 days ago ↶
557
let run = c.run.as_ref ( ) .map ( | r | r.1. clone ( ) ) ;
558
c.colour = Colour:: of( c.follow, b, c.bump, run.as_deref ( ) , unverified) ;
559
c.upstream = Half:: upstream( unverified, up_run.as_deref ( ) , & slug) ;
560
c.downstream = Half:: downstream( c.follow, b, c.bump, run.as_deref ( ) , & format! ( " {} /{} " , c.owner, c.name) ) ;
f677d627 Andy McCall 23 days ago ↶
561
}
562
Ok ( out)
757195ad Andy McCall 22 days ago ↶
563
}
564
565
566
567
pub async fn follows ( app : & Shared, consumer : & RepoRow, account : Option< & Account> ) -> Vec< ( RepoRow, Consumer) > {
568
let ups: Vec< i64 > =
5d90e0e0 Andy McCall 19 days ago
569
sqlx:: query_scalar( " SELECT DISTINCT upstream_repo_id FROM diffusion_pins WHERE consumer_id = $1 AND upstream_repo_id IS NOT NULL" )
757195ad Andy McCall 22 days ago ↶
570
.bind ( consumer.id)
571
.fetch_all ( & app.db)
572
.await
573
.unwrap_or_default ( ) ;
574
let mut out = Vec :: new( ) ;
575
for id in ups {
576
let Ok ( Some ( up) ) = db:: repos:: get_by_id( & app.db, id) .await else { continue } ;
577
if ! roles:: allows( roles:: role_for( & app.db, account, & up) .await.ok ( ) .flatten ( ) , crate :: auth:: Access:: Read) {
578
continue ;
579
}
580
for c in consumers ( app, & up, account) .await.unwrap_or_default ( ) {
581
if c.owner = = consumer.owner & & c.name = = consumer.name {
582
out.push ( ( up.clone ( ) , c) ) ;
583
}
584
}
585
}
586
out
587
}
588
589
590
pub async fn everything ( app : & Shared, account : Option< & Account> ) -> Vec< ( RepoRow, Vec< Consumer> , bool ) > {
591
let ups: Vec< i64 > = sqlx:: query_scalar( " SELECT DISTINCT upstream_repo_id FROM diffusion_pins WHERE upstream_repo_id IS NOT NULL" )
592
.fetch_all ( & app.db)
593
.await
594
.unwrap_or_default ( ) ;
595
let mut out = Vec :: new( ) ;
596
for id in ups {
597
let Ok ( Some ( up) ) = db:: repos:: get_by_id( & app.db, id) .await else { continue } ;
598
let list = consumers ( app, & up, account) .await.unwrap_or_default ( ) ;
599
let unverified = unverified ( app, & up) .await;
600
out.push ( ( up, list, unverified) ) ;
601
}
602
out
f677d627 Andy McCall 23 days ago ↶
603
}
e7158350 Andy McCall 23 days ago ↶
604
1d634f69 Andy McCall 23 days ago ↶
605
62cdf34d Andy McCall 19 days ago ↶
606
607
608
609
pub fn bump_message ( path : & str , upstream : & str , heads : & [String]) -> String {
610
let first = heads.first ( ) .map ( String :: as_str) .unwrap_or ( " " ) ;
611
match heads.len ( ) {
612
0 | 1 => format! ( " Diffusion: move {path} to {upstream} {first} " ) ,
613
n => format! ( " Diffusion: move {path} to {upstream} {first} (+{} heads)\n \n Pins {upstream} at {} " , n - 1 , heads.join ( " +" ) ) ,
614
}
615
}
616
1d634f69 Andy McCall 23 days ago ↶
617
pub fn bump_bookmark ( path : & str ) -> String {
618
format! ( " diffusion/{} " , path.trim_matches ( ' /' ) )
619
}
620
621
622
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
623
pub enum Bumped {
624
f01cb865 Andy McCall 22 days ago ↶
625
Moved { bookmark: String , change: ChangeHash, heads: Vec< ChangeHash> , from: Vec< ChangeHash> , state: String } ,
1d634f69 Andy McCall 23 days ago ↶
626
627
UpToDate,
628
9aaa88b2 Andy McCall 22 days ago ↶
629
Already { from: Vec< ChangeHash> , to: Vec< ChangeHash> } ,
7a14bdae Andy McCall 22 days ago ↶
630
631
Unknown,
632
633
634
Conflicts,
1d634f69 Andy McCall 23 days ago ↶
635
636
637
NoPin,
638
}
639
7a14bdae Andy McCall 22 days ago ↶
640
641
642
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
643
pub enum Plan {
644
645
To( Vec< ChangeHash> , usize ) ,
646
647
UpToDate,
648
649
650
Unknown,
651
652
Conflicts,
653
}
654
655
656
657
658
659
pub async fn plan ( app : & Shared, upstream : & RepoName, old : Vec< ChangeHash> , new : Vec< ChangeHash> ) -> Result< Plan, crate:: repos:: RepoError> {
1d634f69 Andy McCall 23 days ago ↶
660
app.repos
661
.read ( upstream, move | r, cache| {
7a14bdae Andy McCall 22 days ago ↶
662
let Ok ( o) = cache.summary ( r, & old) else { return Ok ( Plan:: Unknown) } ;
663
let n = cache.summary ( r, & new) ? ;
664
if o.changes.iter ( ) .all ( | c | n.changes.contains ( c) ) {
665
let added = n.changes.len ( ) - o.changes.len ( ) ;
666
return Ok ( if added = = 0 { Plan:: UpToDate } else { Plan:: To( new, added) } ) ;
667
}
668
let mut both = new.clone ( ) ;
669
both.extend ( old.iter ( ) .copied ( ) ) ;
670
let heads = r.load_state ( & both) ? .heads ( ) ;
671
let b = cache.summary ( r, & heads) ? ;
672
if b.conflicted & & ! o.conflicted {
673
return Ok ( Plan:: Conflicts) ;
1d634f69 Andy McCall 23 days ago ↶
674
}
7a14bdae Andy McCall 22 days ago ↶
675
let added = b.changes.len ( ) - o.changes.len ( ) ;
676
Ok ( if added = = 0 { Plan:: UpToDate } else { Plan:: To( heads, added) } )
1d634f69 Andy McCall 23 days ago ↶
677
} )
678
.await
679
}
680
681
682
683
684
pub async fn bump (
685
app : & Shared,
686
consumer : & RepoRow,
687
target : & str ,
688
path : & str ,
689
upstream : & RepoRow,
690
new_heads : Vec< ChangeHash> ,
691
) -> Result< Bumped, crate:: repos:: RepoError> {
692
use oxy_core:: identity:: Domain;
693
use oxy_core:: subrepo:: Pin;
694
let ( Some ( cname) , Some ( uname) ) = ( RepoName:: new( & consumer.owner, & consumer.name) , RepoName:: new( & upstream.owner, & upstream.name) )
695
else {
696
return Ok ( Bumped:: NoPin) ;
697
} ;
698
let upstream_id = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await? ;
699
700
701
let ( t, p) = ( target.to_string ( ) , path.trim_matches ( ' /' ) .to_string ( ) ) ;
702
let ( current, bumped) = app
703
.repos
704
.read ( & cname, move | r, cache| {
705
let pin_on = | bookmark : & str | -> Result< Option< Pin> , crate:: repos:: RepoError> {
706
let heads = heads_of ( r, bookmark) ? ;
707
if heads.is_empty ( ) {
708
return Ok ( None ) ;
709
}
710
let loaded = cache.load ( r, & heads) ? ;
711
let Some ( f) = loaded.snapshot.files.get ( & p) else { return Ok ( None ) } ;
712
let oxy_core:: state:: FileContent:: Opaque { blobs } = & f.content else { return Ok ( None ) } ;
713
let Some ( b) = blobs.first ( ) else { return Ok ( None ) } ;
714
Ok ( Pin:: decode( & r.read_blob ( b) ? ) .ok ( ) )
715
} ;
716
Ok ( ( pin_on ( & t) ? , pin_on ( & bump_bookmark ( & p) ) ? ) )
717
} )
718
.await? ;
719
let Some ( Pin:: Oxy { repo, heads: old, bookmark } ) = current else { return Ok ( Bumped:: NoPin) } ;
7a14bdae Andy McCall 22 days ago ↶
720
if repo ! = upstream_id {
721
return Ok ( Bumped:: NoPin) ;
722
}
723
let new_heads = match plan ( app, & uname, old.clone ( ) , new_heads.clone ( ) ) .await? {
724
Plan:: To( heads, _ ) => heads,
725
Plan:: UpToDate => return Ok ( Bumped:: UpToDate) ,
726
Plan:: Unknown => return Ok ( Bumped:: Unknown) ,
727
Plan:: Conflicts => return Ok ( Bumped:: Conflicts) ,
728
} ;
1d634f69 Andy McCall 23 days ago ↶
729
if let Some ( Pin:: Oxy { heads: b, .. } ) = & bumped {
730
let ( mut b, mut n) = ( b.clone ( ) , new_heads.clone ( ) ) ;
731
b.sort ( ) ;
732
n.sort ( ) ;
733
if b = = n {
9aaa88b2 Andy McCall 22 days ago ↶
734
return Ok ( Bumped:: Already { from: old, to: new_heads } ) ;
1d634f69 Andy McCall 23 days ago ↶
735
}
736
}
737
let ( key, secret) =
738
crate :: keys:: load_secret( & app.config.keys_dir ( ) ) .map_err ( | e | crate :: repos:: RepoError:: Io( std:: io:: Error:: other( e.to_string ( ) ) ) ) ? ;
739
let identity = app.hub_key.identity.clone ( ) ;
740
let upstream_label = format! ( " {} /{} " , upstream.owner, upstream.name) ;
741
let ( t, p) = ( target.to_string ( ) , path.trim_matches ( ' /' ) .to_string ( ) ) ;
742
let pin = Pin:: Oxy { repo, heads: new_heads.clone ( ) , bookmark } ;
a7e6e8e6 Andy McCall 22 days ago ↶
743
let old = old.clone ( ) ;
1d634f69 Andy McCall 23 days ago ↶
744
let short: Vec< String> = new_heads.iter ( ) .map ( | h | h.to_hex ( ) [ .. 12 ] .to_string ( ) ) .collect ( ) ;
62cdf34d Andy McCall 19 days ago ↶
745
let message = bump_message ( & p, & upstream_label, & short) ;
1d634f69 Andy McCall 23 days ago ↶
746
app.repos
747
.write ( & cname, move | r, _ | {
748
let base = heads_of ( r, & t) ? ;
749
let change = r.build_pin_change ( & base, & p, & pin, & message, key) ? ;
750
let hash = change.hash ( ) ;
751
let sig = secret.sign ( Domain:: Change, & key, & hash) ;
752
let mut state = r.load_state ( & base) ? ;
753
state.add ( change.clone ( ) ) .map_err ( | e | crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( e.to_string ( ) ) ) ) ? ;
754
let mut candidates = base.clone ( ) ;
755
candidates.push ( hash) ;
756
let heads = state.maximal ( & candidates) ;
757
let bookmark = bump_bookmark ( & p) ;
758
759
760
let mut update = r.host_ref_update ( & bookmark, & heads, Some ( ( & key, & secret) ) ) ? ;
761
update.force = true ;
f01cb865 Andy McCall 22 days ago ↶
762
let state = update.new_state.to_hex ( ) ;
1d634f69 Andy McCall 23 days ago ↶
763
let report = r.apply_hosted ( Some ( ( change, vec! [ sig] ) ) , & [ identity] , update) ? ;
764
if let Some ( refused) = report.results.iter ( ) .find ( | x | ! x.ok) {
765
return Err ( crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( format! (
766
" the bump was refused: {} " ,
767
refused.reason
768
) ) ) ) ;
769
}
f01cb865 Andy McCall 22 days ago ↶
770
Ok ( Bumped:: Moved { bookmark, change: hash, heads, from: old, state } )
1d634f69 Andy McCall 23 days ago ↶
771
} )
772
.await
773
}
774
a7e6e8e6 Andy McCall 22 days ago ↶
775
776
pub const ACCOUNT : & str = " diffusion" ;
777
778
779
pub async fn account ( pool : & Pool) -> Option< Account> {
780
if let Ok ( Some ( a) ) = db:: accounts:: account_by_name( pool, ACCOUNT ) .await {
781
return Some ( a) ;
782
}
783
db:: accounts:: create_account( pool, ACCOUNT , " diffusion@localhost" , " !" , " " , false ) .await.ok ( )
784
}
785
c4e432da Andy McCall 22 days ago ↶
786
787
788
789
790
pub fn watched ( pattern : & str , path : & str ) -> bool {
791
fn m ( p : & [u8 ], s : & [u8 ]) -> bool {
792
match ( p.first ( ) , s.first ( ) ) {
793
( None , None ) => true ,
794
( Some ( b ' *' ) , _ ) if p.get ( 1 ) = = Some ( & b ' *' ) => ( 0 .. = s.len ( ) ) .any ( | i | m ( & p[ 2 .. ] , & s[ i.. ] ) ) ,
795
( Some ( b ' *' ) , _ ) => ( 0 .. = s.len ( ) ) .take_while ( | & i | i = = 0 | | s[ i - 1 ] ! = b ' /' ) .any ( | i | m ( & p[ 1 .. ] , & s[ i.. ] ) ) ,
796
( Some ( b ' ?' ) , Some ( c) ) if * c ! = b ' /' => m ( & p[ 1 .. ] , & s[ 1 .. ] ) ,
797
( Some ( a) , Some ( b) ) if a = = b => m ( & p[ 1 .. ] , & s[ 1 .. ] ) ,
798
_ => false ,
799
}
800
}
801
let p = pattern.trim ( ) .trim_matches ( ' /' ) ;
802
if p.is_empty ( ) {
803
return false ;
804
}
805
m ( p.as_bytes ( ) , path.as_bytes ( ) ) | | ( ! p.contains ( [ ' *' , ' ?' ] ) & & path.starts_with ( & format! ( " {p} /" ) ) )
806
}
807
808
809
810
811
pub struct Changelog {
812
pub lines : Vec< String> ,
813
pub watched : Vec< String> ,
814
}
815
816
pub async fn changelog ( app : & Shared, upstream : & RepoRow, old : Vec< ChangeHash> , new : Vec< ChangeHash> , watch : Vec< String> ) -> Changelog {
817
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { return Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } } ;
a7e6e8e6 Andy McCall 22 days ago ↶
818
let linked: std:: collections:: HashMap< String, String> =
819
db:: accounts:: identity_accounts( & app.db) .await.unwrap_or_default ( ) .into_iter ( ) .collect ( ) ;
820
let base = format! ( " /{} /{} " , upstream.owner, upstream.name) ;
821
app.repos
822
.read ( & name, move | r, cache| {
823
let ( o, n) = ( cache.summary ( r, & old) ? , cache.summary ( r, & new) ? ) ;
c4e432da Andy McCall 22 days ago ↶
824
let mut added: Vec< ( u64 , ChangeHash, String, String, Vec< String> ) > = Vec :: new( ) ;
a7e6e8e6 Andy McCall 22 days ago ↶
825
for h in n.changes.iter ( ) .filter ( | c | ! o.changes.contains ( * c) ) {
826
let Ok ( c) = r.store ( ) .get_change ( h) else { continue } ;
827
let key = c.author.to_hex ( ) ;
828
let who = linked.get ( & key) .cloned ( ) .unwrap_or_else ( | | key[ .. 12 ] .to_string ( ) ) ;
829
let first = c.message.lines ( ) .next ( ) .unwrap_or ( " " ) .trim ( ) .to_string ( ) ;
c4e432da Andy McCall 22 days ago ↶
830
831
let hits: Vec< String> = if watch.is_empty ( ) {
832
Vec :: new( )
833
} else {
834
crate :: repos:: view:: change( r, cache, h)
835
.map ( | d | d.files.into_iter ( ) .map ( | f | f.path ) .filter ( | p | watch.iter ( ) .any ( | w | watched ( w, p) ) ) .collect ( ) )
836
.unwrap_or_default ( )
837
} ;
838
added.push ( ( c.hlc.wall_ms, * h, first, who, hits) ) ;
a7e6e8e6 Andy McCall 22 days ago ↶
839
}
840
added.sort_by_key ( | a | std:: cmp:: Reverse( a.0 ) ) ;
c4e432da Andy McCall 22 days ago ↶
841
let line = | h : & ChangeHash, msg : & str , who : & str , flag : bool | {
842
format! (
843
" - {} [`{} `]({base} /change/{} ) {} ({who} )" ,
844
if flag { " ⚑ " } else { " " } ,
845
& h.to_hex ( ) [ .. 12 ] ,
846
h.to_hex ( ) ,
847
msg.replace ( ' \n ' , " " )
848
)
849
} ;
850
let watched = added
851
.iter ( )
852
.filter ( | a | ! a.4. is_empty ( ) )
853
.map ( | ( _, h , msg , who , hits ) | {
854
format! (
855
" {} — touches {} " ,
856
line ( h, msg, who, true ) ,
857
hits.iter ( ) .map ( | p | format! ( " `{p} `" ) ) .collect:: < Vec< _ > > ( ) .join ( " , " )
858
)
a7e6e8e6 Andy McCall 22 days ago ↶
859
} )
c4e432da Andy McCall 22 days ago ↶
860
.collect ( ) ;
861
let lines = added.iter ( ) .map ( | ( _, h , msg , who , hits ) | line ( h, msg, who, ! hits.is_empty ( ) ) ) .collect ( ) ;
862
Ok ( Changelog { lines, watched } )
a7e6e8e6 Andy McCall 22 days ago ↶
863
} )
864
.await
c4e432da Andy McCall 22 days ago ↶
865
.unwrap_or ( Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } )
a7e6e8e6 Andy McCall 22 days ago ↶
866
}
867
9aaa88b2 Andy McCall 22 days ago ↶
868
869
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
870
pub enum Why {
871
872
Green,
873
874
Moved,
875
876
877
Unverified,
878
}
879
880
881
882
883
a7e6e8e6 Andy McCall 22 days ago ↶
884
pub async fn propose (
885
app : & Shared,
886
consumer : & RepoRow,
887
target : & str ,
888
path : & str ,
889
upstream : & RepoRow,
9aaa88b2 Andy McCall 22 days ago ↶
890
( from , to ) : ( Vec< ChangeHash> , Vec< ChangeHash> ) ,
f4d6ef41 Andy McCall 21 days ago ↶
891
( why , watch , lands ) : ( Why, Vec< String> , bool ) ,
a7e6e8e6 Andy McCall 22 days ago ↶
892
) -> Option< i64 > {
893
use crate :: db:: proposals as pdb;
894
let bookmark = bump_bookmark ( path) ;
895
let bookmark = bookmark.as_str ( ) ;
896
let me = account ( & app.db) .await? ;
c4e432da Andy McCall 22 days ago ↶
897
let log = changelog ( app, upstream, from, to.clone ( ) , watch.clone ( ) ) .await;
898
let lines = log.lines;
899
let flagged = if log.watched.is_empty ( ) {
900
String :: new( )
901
} else {
902
format! (
903
" \n \n **Touches what you watch** ({} ):\n \n {} " ,
904
watch.iter ( ) .map ( | w | format! ( " `{w} `" ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ,
905
log.watched.join ( " \n " )
906
)
907
} ;
a7e6e8e6 Andy McCall 22 days ago ↶
908
let slug = format! ( " {} /{} " , upstream.owner, upstream.name) ;
909
let short: Vec< String> = to.iter ( ) .map ( | h | h.to_hex ( ) [ .. 12 ] .to_string ( ) ) .collect ( ) ;
1b6536f0 Andy McCall 22 days ago ↶
910
let ub = & upstream.default_bookmark;
911
let count = if lines.len ( ) = = 1 { " 1 change" .to_string ( ) } else { format! ( " {} changes" , lines.len ( ) ) } ;
912
let title = format! ( " Diffusion: {path} to {slug} {ub} ({count} )" ) ;
9aaa88b2 Andy McCall 22 days ago ↶
913
let when = match why {
914
Why:: Green => " at its newest state whose checks passed" ,
915
Why:: Moved => " as it is now" ,
916
Why:: Unverified => " as it is now" ,
917
} ;
918
let amber = if why = = Why:: Unverified {
919
format! ( " \n \n > **Upstream unverified.** {slug} has no checks of its own, so nothing has tested this state of it. Only this repository's pipeline tests the move; a later bump from a passing run of {slug} clears this." )
920
} else {
921
String :: new( )
922
} ;
a7e6e8e6 Andy McCall 22 days ago ↶
923
let body = format! (
f4d6ef41 Andy McCall 21 days ago ↶
924
" Moves the subrepo `{path} ` to [{slug} ](/{slug} )'s `{ub} `, {when} (`{} `). Only the pin changes; this repository's own checks test it.{amber} {flagged} \n \n **What it brings** ({count} ):\n \n {} \n \n Opened and kept up to date by Diffusion (docs/diffusion.md). {} " ,
6ae3fa0a Andy McCall 22 days ago ↶
925
crate :: web:: state_label( & short.join ( " +" ) ) ,
f4d6ef41 Andy McCall 21 days ago ↶
926
lines.join ( " \n " ) ,
927
if lands {
928
" This repository lands its bumps itself (`land = auto`): it lands once the upstream state it pins has passed its own checks and this repository's run on it passes, unless its required checks, approvals or trust policy say otherwise. An untested upstream always waits for a person."
929
} else {
930
" It never lands by itself."
931
}
a7e6e8e6 Andy McCall 22 days ago ↶
932
) ;
933
let open = pdb:: list( & app.db, consumer.id, & [ " open" , " draft" ] ) .await.ok ( ) ? .into_iter ( ) .find ( | p | p.source_bookmark = = bookmark ) ;
9aaa88b2 Andy McCall 22 days ago ↶
934
let ( number, id) = match open {
a7e6e8e6 Andy McCall 22 days ago ↶
935
Some ( p) => {
9aaa88b2 Andy McCall 22 days ago ↶
936
if p.title ! = title | | p.body ! = body {
937
pdb:: edit( & app.db, p.id, & title, & body, target) .await.ok ( ) ? ;
6ae3fa0a Andy McCall 22 days ago ↶
938
pdb:: add_event( & app.db, p.id, ACCOUNT , " updated" , & crate :: web:: state_label( & short.join ( " +" ) ) ) .await.ok ( ) ;
9aaa88b2 Andy McCall 22 days ago ↶
939
}
940
( p.number, p.id)
a7e6e8e6 Andy McCall 22 days ago ↶
941
}
942
None => {
943
let p = pdb:: create(
944
& app.db,
945
pdb:: NewProposal {
946
repo_id: consumer.id,
947
title: & title,
948
body: & body,
949
author_id: Some ( me.id) ,
950
author: ACCOUNT ,
951
draft: false ,
952
target,
953
source_kind: " bookmark" ,
954
source_repo_id: None ,
955
source_bookmark: bookmark,
956
source_changes: " " ,
957
} ,
958
)
959
.await
960
.ok ( ) ? ;
961
pdb:: add_event( & app.db, p.id, ACCOUNT , " opened" , " " ) .await.ok ( ) ;
962
db:: notify:: index( & app.db, " proposal" , consumer.id, p.id, p.number, & p.title, & p.body) .await.ok ( ) ;
bff9106e Andy McCall 21 days ago ↶
963
964
crate :: notify:: deliver(
965
app,
966
crate :: notify:: Event {
967
kind: " proposal.opened" ,
968
title: format! ( " {} /{} #{} : {} " , consumer.owner, consumer.name, p.number, p.title) ,
969
url: format! ( " /{} /{} /proposal/{} " , consumer.owner, consumer.name, p.number) ,
970
actor: & me,
971
repo_id: consumer.id,
972
thread: Some ( ( " proposal" , p.id) ) ,
973
mention_text: " " ,
974
direct: vec! [ ] ,
975
} ,
976
)
977
.await;
a7e6e8e6 Andy McCall 22 days ago ↶
978
if let Ok ( label) =
979
db:: issues:: upsert_label( & app.db, consumer.id, " dependencies" , " #0072ff" , " A subrepo moves (Diffusion)" ) .await
980
{
981
db:: issues:: set_proposal_label( & app.db, p.id, label.id, true ) .await.ok ( ) ;
982
}
9aaa88b2 Andy McCall 22 days ago ↶
983
( p.number, p.id)
a7e6e8e6 Andy McCall 22 days ago ↶
984
}
985
} ;
9aaa88b2 Andy McCall 22 days ago ↶
986
987
let unverified = db:: issues:: label_by_name( & app.db, consumer.id, " upstream unverified" ) .await.ok ( ) .flatten ( ) ;
988
match ( why = = Why:: Unverified, unverified) {
989
( true , l) => {
990
let l = match l {
991
Some ( l) => Some ( l) ,
992
None => db:: issues:: upsert_label(
993
& app.db,
994
consumer.id,
995
" upstream unverified" ,
996
" #e3a008" ,
997
" The upstream has no checks (Diffusion)" ,
998
)
999
.await
1000
.ok ( ) ,
1001
} ;
1002
if let Some ( l) = l {
1003
db:: issues:: set_proposal_label( & app.db, id, l.id, true ) .await.ok ( ) ;
1004
}
1005
}
1006
( false , Some ( l) ) => {
1007
db:: issues:: set_proposal_label( & app.db, id, l.id, false ) .await.ok ( ) ;
1008
}
1009
( false , None ) => { }
1010
}
a7e6e8e6 Andy McCall 22 days ago ↶
1011
Some ( number)
1012
}
1013
f01cb865 Andy McCall 22 days ago ↶
1014
1015
1016
1017
fn start_runs (
1018
app : Shared,
1019
row : RepoRow,
1020
bookmark : String,
1021
state : String,
1022
heads : Vec< ChangeHash> ,
1023
) -> std:: pin:: Pin< Box< dyn std:: future:: Future< Output = ( ) > + Send> > {
1024
Box :: pin( async move { crate :: flow:: on_push( & app, & row, & bookmark, & state, & heads, ACCOUNT ) .await } )
1025
}
1026
a7e6e8e6 Andy McCall 22 days ago ↶
1027
1028
async fn oxymodules ( app : & Shared, name : & RepoName, bookmark : & str ) -> Option< String> {
1029
let b = bookmark.to_string ( ) ;
1030
app.repos
1031
.read ( name, move | r, cache| {
1032
let heads = heads_of ( r, & b) ? ;
1033
if heads.is_empty ( ) {
1034
return Ok ( None ) ;
1035
}
1036
let loaded = cache.load ( r, & heads) ? ;
1037
Ok ( match loaded.snapshot.files.get ( " .oxymodules" ) .map ( | f | & f.content) {
1038
Some ( oxy_core:: state:: FileContent:: Text { bytes, .. } ) => Some ( String :: from_utf8_lossy( bytes) .into_owned ( ) ) ,
1039
_ => None ,
1040
} )
1041
} )
1042
.await
1043
.ok ( )
1044
.flatten ( )
1045
}
1046
1047
1048
1049
1050
pub async fn propagate ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool ) {
3776b70d Andy McCall 22 days ago ↶
1051
propagate_to ( app, upstream, heads, green, None ) .await
1052
}
1053
1054
1055
async fn propagate_to ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool , only : Option< i64 > ) {
a7e6e8e6 Andy McCall 22 days ago ↶
1056
let Some ( uname) = RepoName:: new( & upstream.owner, & upstream.name) else { return } ;
1057
let Ok ( id) = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await else { return } ;
5d90e0e0 Andy McCall 19 days ago
1058
let rows: Vec< ( i64 , String, String) > = sqlx:: query_as( " SELECT consumer_id, bookmark, path FROM diffusion_pins WHERE upstream = $1" )
a7e6e8e6 Andy McCall 22 days ago ↶
1059
.bind ( hex:: encode( id) )
1060
.fetch_all ( & app.db)
1061
.await
1062
.unwrap_or_default ( ) ;
9aaa88b2 Andy McCall 22 days ago ↶
1063
let unverified = unverified ( app, upstream) .await;
a7e6e8e6 Andy McCall 22 days ago ↶
1064
for ( consumer_id, indexed, path) in rows {
3776b70d Andy McCall 22 days ago ↶
1065
if only.is_some_and ( | o | o ! = consumer_id ) {
1066
continue ;
1067
}
a7e6e8e6 Andy McCall 22 days ago ↶
1068
let Ok ( Some ( consumer) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
1069
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { continue } ;
1070
let learned = target ( & app.db, & consumer) .await;
1071
let modules = oxymodules ( app, & cname, & learned) .await;
1072
let r = rule ( & app.config.diffusion.follow, & consumer, modules.as_deref ( ) , & path) ;
1073
let target = r.target.clone ( ) .unwrap_or ( learned) ;
1074
if indexed ! = target {
1075
continue ;
1076
}
9aaa88b2 Andy McCall 22 days ago ↶
1077
1078
a7e6e8e6 Andy McCall 22 days ago ↶
1079
let go = match r.follow {
1080
Follow:: Off => false ,
9aaa88b2 Andy McCall 22 days ago ↶
1081
Follow:: Green => green | | unverified,
a7e6e8e6 Andy McCall 22 days ago ↶
1082
Follow:: Any => ! green,
1083
} ;
9aaa88b2 Andy McCall 22 days ago ↶
1084
let why = if green {
1085
Why:: Green
1086
} else if unverified {
1087
Why:: Unverified
1088
} else {
1089
Why:: Moved
1090
} ;
a7e6e8e6 Andy McCall 22 days ago ↶
1091
if ! go {
1092
continue ;
1093
}
356183b7 Andy McCall 14 days ago ↶
1094
1095
let Ok ( _pin) = crate :: leader:: exclusive( & app.db, & format! ( " diffusion:{} :{path} " , consumer.id) ) .await else { continue } ;
a7e6e8e6 Andy McCall 22 days ago ↶
1096
match bump ( app, & consumer, & target, & path, upstream, heads.clone ( ) ) .await {
f01cb865 Andy McCall 22 days ago ↶
1097
Ok ( Bumped:: Moved { from, bookmark, heads: bumped, state, .. } ) => {
1098
1099
tokio:: spawn( start_runs ( app.clone ( ) , consumer.clone ( ) , bookmark, state, bumped) ) ;
f4d6ef41 Andy McCall 21 days ago ↶
1100
let n = propose ( app, & consumer, & target, & path, upstream, ( from, heads.clone ( ) ) , ( why, r.watch.clone ( ) , r.land_auto) ) .await;
33d98147 Andy McCall 16 days ago
1101
app.bus.live ( & crate :: events:: topic:: diffusion( consumer.id) , " bump" ) ;
1102
app.bus.live ( & crate :: events:: topic:: diffusion( upstream.id) , " bump" ) ;
a7e6e8e6 Andy McCall 22 days ago ↶
1103
tracing:: info! (
1104
" diffusion: {}/{} {path} bumped to {}/{} (proposal {n:?})" ,
1105
consumer.owner,
1106
consumer.name,
1107
upstream.owner,
1108
upstream.name
1109
) ;
9aaa88b2 Andy McCall 22 days ago ↶
1110
}
1111
Ok ( Bumped:: Already { from, to } ) => {
1112
1113
f4d6ef41 Andy McCall 21 days ago ↶
1114
propose ( app, & consumer, & target, & path, upstream, ( from, to) , ( why, r.watch.clone ( ) , r.land_auto) ) .await;
1115
1116
1117
if green & & r.land_auto {
1118
tokio:: spawn( try_land ( app.clone ( ) , consumer.clone ( ) , path.clone ( ) ) ) ;
1119
}
a7e6e8e6 Andy McCall 22 days ago ↶
1120
}
7a14bdae Andy McCall 22 days ago ↶
1121
Ok ( other) => tracing:: info! ( " diffusion: {}/{} {path}: no bump ({other:?})" , consumer.owner, consumer.name) ,
a7e6e8e6 Andy McCall 22 days ago ↶
1122
Err ( e) => tracing:: warn! ( " diffusion: bump {}/{} {path}: {e}" , consumer.owner, consumer.name) ,
3776b70d Andy McCall 22 days ago ↶
1123
}
1124
}
f4d6ef41 Andy McCall 21 days ago ↶
1125
}
1126
1127
1128
# [ derive ( Debug, PartialEq, Eq ) ]
1129
pub enum Held {
1130
1131
NoBump,
1132
1133
Manual,
1134
1135
Unverified,
1136
1137
UpstreamUnchecked,
1138
1139
NotPassed,
1140
1141
Refused( String ) ,
9aaa88b2 Andy McCall 22 days ago ↶
1142
}
1143
f4d6ef41 Andy McCall 21 days ago ↶
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
pub async fn auto_land ( app : & Shared, consumer : & RepoRow, path : & str ) -> Result< i64 , Held> {
1154
let path = path.trim_matches ( ' /' ) ;
1155
let bm = bump_bookmark ( path) ;
1156
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { return Err ( Held:: NoBump) } ;
1157
let bump = db:: proposals:: list( & app.db, consumer.id, & [ " open" ] )
1158
.await
1159
.ok ( )
1160
.and_then ( | ps | ps.into_iter ( ) .find ( | p | p.source_bookmark = = bm & & p.author = = ACCOUNT ) )
1161
.ok_or ( Held:: NoBump) ? ;
1162
let learned = target ( & app.db, consumer) .await;
1163
let modules = oxymodules ( app, & cname, & learned) .await;
1164
if ! rule ( & app.config.diffusion.follow, consumer, modules.as_deref ( ) , path) .land_auto {
1165
return Err ( Held:: Manual) ;
1166
}
1167
let upstream_id: Option< i64 > = sqlx:: query_scalar(
5d90e0e0 Andy McCall 19 days ago
1168
" SELECT upstream_repo_id FROM diffusion_pins WHERE consumer_id = $1 AND path = $2 AND upstream_repo_id IS NOT NULL LIMIT 1" ,
f4d6ef41 Andy McCall 21 days ago ↶
1169
)
1170
.bind ( consumer.id)
1171
.bind ( path)
1172
.fetch_optional ( & app.db)
1173
.await
1174
.ok ( )
1175
.flatten ( ) ;
1176
let Some ( upstream) = ( match upstream_id {
1177
Some ( id) => db:: repos:: get_by_id( & app.db, id) .await.ok ( ) .flatten ( ) ,
1178
None => None ,
1179
} ) else {
1180
return Err ( Held:: UpstreamUnchecked) ;
1181
} ;
1182
if unverified ( app, & upstream) .await {
1183
return Err ( Held:: Unverified) ;
1184
}
1185
1186
let ( p, b) = ( path.to_string ( ) , bm.clone ( ) ) ;
1187
let ( pinned, now) = app
1188
.repos
1189
.read ( & cname, move | r, cache| {
1190
let heads = heads_of ( r, & b) ? ;
1191
if heads.is_empty ( ) {
1192
return Ok ( ( None , heads) ) ;
1193
}
1194
let loaded = cache.load ( r, & heads) ? ;
1195
let pin = match loaded.snapshot.files.get ( & p) .map ( | f | & f.content) {
1196
Some ( oxy_core:: state:: FileContent:: Opaque { blobs } ) => match blobs.first ( ) {
1197
Some ( blob) => oxy_core:: subrepo:: Pin:: decode( & r.read_blob ( blob) ? ) .ok ( ) ,
1198
None => None ,
1199
} ,
1200
_ => None ,
1201
} ;
1202
Ok ( ( pin, heads) )
1203
} )
1204
.await
1205
.map_err ( | e | Held:: Refused( e.to_string ( ) ) ) ? ;
1206
let Some ( oxy_core:: subrepo:: Pin:: Oxy { heads: pinned, .. } ) = pinned else { return Err ( Held:: UpstreamUnchecked) } ;
1207
let same = | text : & str , want : & [ChangeHash]| {
1208
let mut a: Vec< String> = text.split ( ' +' ) .filter ( | h | ! h.is_empty ( ) ) .map ( str :: to_string) .collect ( ) ;
1209
let mut b: Vec< String> = want.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ;
1210
a.sort ( ) ;
1211
b.sort ( ) ;
1212
a = = b
1213
} ;
1214
let upstream_runs: Vec< String> = sqlx:: query_scalar(
5d90e0e0 Andy McCall 19 days ago
1215
" SELECT heads FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 AND status = 'passed' ORDER BY id DESC LIMIT 100" ,
f4d6ef41 Andy McCall 21 days ago ↶
1216
)
1217
.bind ( upstream.id)
1218
.bind ( & upstream.default_bookmark)
1219
.fetch_all ( & app.db)
1220
.await
1221
.unwrap_or_default ( ) ;
1222
if ! upstream_runs.iter ( ) .any ( | h | same ( h, & pinned) ) {
1223
return Err ( Held:: UpstreamUnchecked) ;
1224
}
1225
let run: Option< ( String, String) > =
5d90e0e0 Andy McCall 19 days ago
1226
sqlx:: query_as( " SELECT status, heads FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 ORDER BY id DESC LIMIT 1" )
f4d6ef41 Andy McCall 21 days ago ↶
1227
.bind ( consumer.id)
1228
.bind ( & bm)
1229
.fetch_optional ( & app.db)
1230
.await
1231
.ok ( )
1232
.flatten ( ) ;
1233
if ! run.is_some_and ( | ( status , heads ) | status = = " passed" & & same ( & heads, & now) ) {
1234
return Err ( Held:: NotPassed) ;
1235
}
1236
let me = account ( & app.db) .await.ok_or_else ( | | Held:: Refused( " the diffusion account is missing" .into ( ) ) ) ? ;
1237
let via = " Diffusion, both halves green" ;
2a8b6216 Andy McCall 17 days ago ↶
1238
match crate :: web:: proposals:: land_as( app, & cname, consumer, & me, bump.number, via, true , None ) .await {
f4d6ef41 Andy McCall 21 days ago ↶
1239
Ok ( _ ) => {
1240
tracing:: info! ( " diffusion: {}/{} landed #{} itself (both halves green)" , consumer.owner, consumer.name, bump.number) ;
1241
Ok ( bump.number)
1242
}
1243
Err ( crate :: web:: proposals:: Landing:: NotFound) => Err ( Held:: NoBump) ,
1244
Err ( crate :: web:: proposals:: Landing:: Refused( why) ) => {
1245
1246
let said = db:: proposals:: events( & app.db, bump.id)
1247
.await
1248
.unwrap_or_default ( )
1249
.iter ( )
1250
.any ( | e | e.event = = " not landed by itself" & & e.detail = = why ) ;
1251
if ! said {
1252
db:: proposals:: add_event( & app.db, bump.id, ACCOUNT , " not landed by itself" , & why) .await.ok ( ) ;
1253
}
1254
Err ( Held:: Refused( why) )
1255
}
1256
}
1257
}
1258
1259
1260
1261
pub fn try_land ( app : Shared, consumer : RepoRow, path : String) -> std:: pin:: Pin< Box< dyn std:: future:: Future< Output = ( ) > + Send> > {
1262
Box :: pin( async move {
356183b7 Andy McCall 14 days ago ↶
1263
1264
let Ok ( _pin) = crate :: leader:: exclusive( & app.db, & format! ( " diffusion:{} :{path} " , consumer.id) ) .await else { return } ;
f4d6ef41 Andy McCall 21 days ago ↶
1265
match auto_land ( & app, & consumer, & path) .await {
1266
Ok ( _ ) | Err ( Held:: NoBump | Held:: Manual) => { }
1267
Err ( why) => tracing:: info! ( " diffusion: {}/{} {path} waits for a person: {why:?}" , consumer.owner, consumer.name) ,
1268
}
1269
} )
1270
}
1271
1272
1273
pub async fn land_ready ( app : & Shared, consumer : & RepoRow) {
5d90e0e0 Andy McCall 19 days ago
1274
let paths: Vec< String> = sqlx:: query_scalar( " SELECT DISTINCT path FROM diffusion_pins WHERE consumer_id = $1" )
f4d6ef41 Andy McCall 21 days ago ↶
1275
.bind ( consumer.id)
1276
.fetch_all ( & app.db)
1277
.await
1278
.unwrap_or_default ( ) ;
1279
for path in paths {
1280
tokio:: spawn( try_land ( app.clone ( ) , consumer.clone ( ) , path) ) ;
1281
}
1282
}
1283
9aaa88b2 Andy McCall 22 days ago ↶
1284
1285
1286
1287
pub async fn unverified ( app : & Shared, upstream : & RepoRow) -> bool {
1288
if ! crate :: flow:: enabled( app, upstream) {
1289
return true ;
1290
}
5d90e0e0 Andy McCall 19 days ago
1291
let runs: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM flow_runs WHERE repo_id = $1 AND bookmark = $2" )
9aaa88b2 Andy McCall 22 days ago ↶
1292
.bind ( upstream.id)
1293
.bind ( & upstream.default_bookmark)
1294
.fetch_one ( & app.db)
1295
.await
1296
.unwrap_or ( 0 ) ;
1297
runs = = 0
3776b70d Andy McCall 22 days ago ↶
1298
}
1299
1300
1301
async fn repo_by_oxy_id ( app : & Shared, id : & str ) -> Option< RepoRow> {
1302
for row in db:: repos:: list( & app.db) .await.ok ( ) ? {
1303
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { continue } ;
1304
if app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await.ok ( ) .map ( hex:: encode) .as_deref ( ) = = Some ( id) {
1305
return Some ( row) ;
1306
}
1307
}
1308
None
1309
}
1310
1311
1312
async fn last_green ( app : & Shared, upstream : & RepoRow) -> Option< Vec< ChangeHash> > {
5d90e0e0 Andy McCall 19 days ago
1313
let heads: String = sqlx:: query_scalar(
1314
" SELECT heads FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 AND status = 'passed' ORDER BY id DESC LIMIT 1" ,
1315
)
1316
.bind ( upstream.id)
1317
.bind ( & upstream.default_bookmark)
1318
.fetch_optional ( & app.db)
1319
.await
1320
.ok ( )
1321
.flatten ( ) ? ;
3776b70d Andy McCall 22 days ago ↶
1322
let h: Vec< ChangeHash> = heads.split ( ' +' ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
1323
( ! h.is_empty ( ) ) .then_some ( h)
1324
}
1325
1326
1327
1328
1329
pub async fn catch_up ( app : & Shared, consumer : & RepoRow) {
5d90e0e0 Andy McCall 19 days ago
1330
let upstreams: Vec< String> = sqlx:: query_scalar( " SELECT DISTINCT upstream FROM diffusion_pins WHERE consumer_id = $1" )
3776b70d Andy McCall 22 days ago ↶
1331
.bind ( consumer.id)
1332
.fetch_all ( & app.db)
1333
.await
1334
.unwrap_or_default ( ) ;
1335
for id in upstreams {
1336
let Some ( upstream) = repo_by_oxy_id ( app, & id) .await else { continue } ;
1337
if let Some ( h) = last_green ( app, & upstream) .await {
1338
propagate_to ( app, & upstream, h, true , Some ( consumer.id) ) .await;
1339
}
1340
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { continue } ;
1341
let b = upstream.default_bookmark.clone ( ) ;
1342
if let Ok ( h) = app.repos.read ( & name, move | r, _ | heads_of ( r, & b) ) .await {
1343
propagate_to ( app, & upstream, h, false , Some ( consumer.id) ) .await;
a7e6e8e6 Andy McCall 22 days ago ↶
1344
}
1345
}
1346
}
1347
e7158350 Andy McCall 23 days ago ↶
1348
1349
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize ) ]
1350
# [ serde ( rename_all = " lowercase" ) ]
1351
pub enum Follow {
1352
Off,
1353
1354
Green,
1355
1356
Any,
1357
}
1358
1359
impl Follow {
1360
pub fn parse ( s : & str ) -> Option< Follow> {
1361
match s.trim ( ) {
1362
" off" => Some ( Follow:: Off) ,
1363
" green" => Some ( Follow:: Green) ,
1364
" any" => Some ( Follow:: Any) ,
1365
_ => None ,
eb2a3502 Andy McCall 22 days ago ↶
1366
}
1367
}
1368
1369
pub fn as_str ( self ) -> & 'static str {
1370
match self {
1371
Follow:: Off => " off" ,
1372
Follow:: Green => " green" ,
1373
Follow:: Any => " any" ,
e7158350 Andy McCall 23 days ago ↶
1374
}
1375
}
1376
}
1377
1378
1379
1380
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize ) ]
1381
pub struct Rule {
1382
pub follow : Follow,
f4d6ef41 Andy McCall 21 days ago ↶
1383
1384
e7158350 Andy McCall 23 days ago ↶
1385
pub land_auto : bool ,
1386
1387
pub target : Option< String> ,
1388
1389
1390
pub watch : Vec< String> ,
1391
1392
pub from : & 'static str ,
1393
}
1394
1395
1396
1397
pub fn module_keys ( text : & str , path : & str ) -> ( Option< Follow> , Option< bool > , Option< String> , Vec< String> ) {
1398
let ( mut follow, mut land, mut target, mut watch) = ( None , None , None , Vec :: new( ) ) ;
1399
let mut here = false ;
5360cea3 Andy McCall 20 days ago ↶
1400
let mut lines = text.lines ( ) ;
1401
while let Some ( line) = lines.next ( ) {
e7158350 Andy McCall 23 days ago ↶
1402
let line = line.trim ( ) ;
1403
if line.is_empty ( ) | | line.starts_with ( ' #' ) {
1404
continue ;
1405
}
1406
if let Some ( inner) = line.strip_prefix ( ' [' ) .and_then ( | l | l.strip_suffix ( ' ]' ) ) {
1407
here = inner.trim ( ) .trim_matches ( ' "' ) .trim_end_matches ( ' /' ) = = path.trim_end_matches ( ' /' ) ;
1408
continue ;
1409
}
1410
let ( true , Some ( ( k, v) ) ) = ( here, line.split_once ( ' =' ) ) else { continue } ;
5360cea3 Andy McCall 20 days ago ↶
1411
1412
1413
let mut v = v.trim ( ) .to_string ( ) ;
1414
if v.starts_with ( ' [' ) & & ! v.contains ( ' ]' ) {
1415
for more in lines.by_ref ( ) {
1416
let more = more.trim ( ) ;
1417
if more.starts_with ( ' #' ) {
1418
continue ;
1419
}
1420
v.push_str ( more) ;
1421
if more.contains ( ' ]' ) {
1422
break ;
1423
}
1424
}
1425
}
1426
let v = v.as_str ( ) ;
e7158350 Andy McCall 23 days ago ↶
1427
let unquoted = v.trim_matches ( ' "' ) .trim ( ) ;
1428
match k.trim ( ) {
1429
" follow" => follow = Follow:: parse( unquoted) ,
1430
" land" => land = Some ( unquoted = = " auto" ) ,
1431
" target" if ! unquoted.is_empty ( ) => target = Some ( unquoted.to_string ( ) ) ,
1432
" watch" => {
1433
watch = v
1434
.trim_start_matches ( ' [' )
1435
.trim_end_matches ( ' ]' )
1436
.split ( ' ,' )
1437
.map ( | w | w.trim ( ) .trim_matches ( ' "' ) .trim ( ) .to_string ( ) )
1438
.filter ( | w | ! w.is_empty ( ) )
1439
.collect ( )
1440
}
1441
_ => { }
1442
}
1443
}
1444
( follow, land, target, watch)
1445
}
1446
1447
1448
pub fn rule ( site_default : & str , row : & RepoRow, oxymodules : Option< & str > , path : & str ) -> Rule {
1449
let ( follow, land, target, watch) = oxymodules.map ( | t | module_keys ( t, path) ) .unwrap_or_default ( ) ;
1450
let ( follow, from) = match follow {
1451
Some ( f) => ( f, " .oxymodules" ) ,
1452
None => match Follow:: parse( & row.diffusion) {
1453
Some ( f) => ( f, " setting" ) ,
1454
None => ( Follow:: parse( site_default) .unwrap_or ( Follow:: Off) , " site" ) ,
1455
} ,
1456
} ;
f4d6ef41 Andy McCall 21 days ago ↶
1457
1458
let land_auto = land.unwrap_or ( row.diffusion_land = = " auto" ) ;
1459
Rule { follow, land_auto, target, watch, from }
e7158350 Andy McCall 23 days ago ↶
1460
}
1461
1462
# [ cfg ( test ) ]
1463
mod tests {
1464
use super :: * ;
1465
1466
const MODULES : & str = r # "
1467
[rstar-api]
1468
url = https://oxygenhub.io/doomsdayone/rstar-api
1469
follow = green
1470
land = "propose"
1471
target = develop
1472
watch = ["src/x16/irq*", "src/x16/frame*"]
1473
1474
[other]
1475
url = https://example/other
1476
follow = any
1477
"# ;
1478
1479
fn row ( diffusion : & str ) -> RepoRow {
1480
RepoRow {
1481
id: 1 ,
1482
owner: " o" .into ( ) ,
1483
name: " n" .into ( ) ,
1484
description: String :: new( ) ,
1485
default_bookmark: " main" .into ( ) ,
1486
visibility: " public" .into ( ) ,
1487
created_ms: 0 ,
1488
updated_ms: 0 ,
1489
git_mirror: false ,
1490
git_mirror_state: String :: new( ) ,
1491
forked_from: None ,
1492
topics: String :: new( ) ,
1493
flow: String :: new( ) ,
1494
diffusion: diffusion.into ( ) ,
1495
.. Default :: default( )
1496
}
f4d6ef41 Andy McCall 21 days ago ↶
1497
}
1498
1499
# [ test ]
1500
fn landing_by_itself_is_chosen_per_repository_and_per_entry ( ) {
1501
let mut r = row ( " green" ) ;
1502
assert! ( ! rule ( " off" , & r, None , " rstar-api" ) .land_auto, " never by default" ) ;
1503
r.diffusion_land = " auto" .into ( ) ;
1504
assert! ( rule ( " off" , & r, None , " rstar-api" ) .land_auto, " the setting" ) ;
1505
assert! ( ! rule ( " off" , & r, Some ( MODULES ) , " rstar-api" ) .land_auto, " the entry's `land = propose` wins" ) ;
1506
assert! ( rule ( " off" , & r, Some ( MODULES ) , " other" ) .land_auto, " an entry without `land` takes the setting" ) ;
1507
r.diffusion_land = String :: new( ) ;
1508
let auto = MODULES .replace ( r # "land = "propose""# , " land = auto" ) ;
1509
assert! ( rule ( " off" , & r, Some ( & auto) , " rstar-api" ) .land_auto) ;
c4e432da Andy McCall 22 days ago ↶
1510
}
1511
1512
# [ test ]
1513
fn watch_patterns_match_paths ( ) {
1514
assert! ( watched ( " src/x16/irq*" , " src/x16/irq.s" ) ) ;
1515
assert! ( watched ( " src/x16/irq*" , " src/x16/irq_wait.c" ) ) ;
1516
assert! ( ! watched ( " src/x16/irq*" , " src/x16/sub/irq.s" ) , " one segment" ) ;
1517
assert! ( watched ( " src/**/frame*.c" , " src/x16/floor/frame_wait.c" ) ) ;
1518
assert! ( watched ( " src/x16" , " src/x16/bank.s" ) , " a directory" ) ;
1519
assert! ( ! watched ( " src/x16" , " src/x160/bank.s" ) ) ;
1520
assert! ( watched ( " include/r?m.h" , " include/rim.h" ) ) ;
1521
assert! ( ! watched ( " " , " anything" ) ) ;
e7158350 Andy McCall 23 days ago ↶
1522
}
1523
1524
# [ test ]
1525
fn oxymodules_keys_are_read_per_entry ( ) {
1526
let ( f, land, target, watch) = module_keys ( MODULES , " rstar-api" ) ;
1527
assert_eq! ( ( f, land, target.as_deref ( ) ) , ( Some ( Follow:: Green) , Some ( false ) , Some ( " develop" ) ) ) ;
1528
assert_eq! ( watch, vec! [ " src/x16/irq*" , " src/x16/frame*" ] ) ;
1529
assert_eq! ( module_keys ( MODULES , " other" ) .0 , Some ( Follow:: Any) ) ;
1530
assert_eq! ( module_keys ( MODULES , " missing" ) , ( None , None , None , vec! [ ] ) ) ;
1531
assert_eq! ( module_keys ( " [a]\n watch = x, y\n " , " a" ) .3 , vec! [ " x" , " y" ] ) ;
5360cea3 Andy McCall 20 days ago ↶
1532
1533
1534
let multi = " [rstar-api]\n url = https://hub/x\n \n watch = [\" src/x16/rbm*\" ,\n \" src/x16/rdm.c\" ,\n # a note\n \" src/mega65/rsm.c\" ]\n follow = \" green\" \n " ;
1535
let ( f, _ , _ , watch) = module_keys ( multi, " rstar-api" ) ;
1536
assert_eq! ( watch, vec! [ " src/x16/rbm*" , " src/x16/rdm.c" , " src/mega65/rsm.c" ] ) ;
1537
assert_eq! ( f, Some ( Follow:: Green) , " the key after the list is still read" ) ;
e7158350 Andy McCall 23 days ago ↶
1538
}
1539
1540
# [ test ]
1541
fn oxymodules_wins_then_the_setting_then_the_site ( ) {
1542
let r = rule ( " off" , & row ( " any" ) , Some ( MODULES ) , " rstar-api" ) ;
1543
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " .oxymodules" ) ) ;
1544
let r = rule ( " off" , & row ( " any" ) , Some ( " [rstar-api]\n url = x\n " ) , " rstar-api" ) ;
1545
assert_eq! ( ( r.follow, r.from) , ( Follow:: Any, " setting" ) ) ;
1546
let r = rule ( " green" , & row ( " " ) , None , " rstar-api" ) ;
1547
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " site" ) ) ;
1548
assert_eq! ( rule ( " nonsense" , & row ( " " ) , None , " x" ) .follow, Follow:: Off) ;
62cdf34d Andy McCall 19 days ago ↶
1549
}
1550
}
1551
1552
# [ cfg ( test ) ]
1553
mod message_tests {
1554
use super :: bump_message;
1555
1556
# [ test ]
1557
fn a_bump_of_many_heads_has_a_short_title_and_the_whole_address_below ( ) {
1558
let heads: Vec< String> = ( 0 .. 20 ) .map ( | i | format! ( " {i:02x} a6c9d4746a" ) ) .collect ( ) ;
1559
let m = bump_message ( " rstar-api" , " doomsdayone/rstar-api" , & heads) ;
1560
let ( title, body) = m.split_once ( " \n \n " ) .unwrap ( ) ;
1561
assert_eq! ( title, " Diffusion: move rstar-api to doomsdayone/rstar-api 00a6c9d4746a (+19 heads)" ) ;
1562
assert_eq! ( body, format! ( " Pins doomsdayone/rstar-api at {} " , heads.join ( " +" ) ) ) ;
1563
assert_eq! ( bump_message ( " api" , " andy/api" , & heads[ .. 1 ] ) , " Diffusion: move api to andy/api 00a6c9d4746a" ) ;
e7158350 Andy McCall 23 days ago ↶
1564
}
1565
}