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(
26
" SELECT target FROM proposals WHERE repo_id = ? AND state = 'landed' AND landed_ms >= ?
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 20 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 23 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 20 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} " ) ) ? ;
127
sqlx:: query( " DELETE FROM diffusion_pins WHERE consumer_id = ? AND bookmark = ?" )
f677d627 Andy McCall 23 days ago ↶
128
.bind ( row.id)
129
.bind ( bookmark)
130
.execute ( & mut * tx)
131
.await
5d54cf74 Andy McCall 20 days ago ↶
132
.map_err ( | e | format! ( " delete: {e} " ) ) ? ;
757195ad Andy McCall 23 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 20 days ago ↶
135
sqlx:: query(
757195ad Andy McCall 23 days ago ↶
136
" INSERT INTO diffusion_pins (consumer_id, bookmark, path, upstream, heads, updated_ms, upstream_repo_id) VALUES (?, ?, ?, ?, ?, ?, ?)" ,
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 20 days ago ↶
147
.map_err ( | e | format! ( " insert {path} : {e} " ) ) ? ;
f677d627 Andy McCall 23 days ago ↶
148
}
0e04dfc2 Andy McCall 20 days ago ↶
149
sqlx:: query( " INSERT OR REPLACE INTO diffusion_indexed (consumer_id, bookmark, heads, updated_ms) VALUES (?, ?, ?, ?)" )
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 20 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 20 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 23 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 23 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 20 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 } ;
250
let indexed: Option< String> = sqlx:: query_scalar( " SELECT heads FROM diffusion_indexed WHERE consumer_id = ? AND bookmark = ?" )
251
.bind ( row.id)
252
.bind ( & bookmark)
253
.fetch_optional ( & app.db)
254
.await
255
.ok ( )
256
.flatten ( ) ;
257
if indexed.as_deref ( ) ! = Some ( heads_key ( & heads) .as_str ( ) ) {
258
tracing:: info! ( " diffusion: {}/{} {bookmark}: the index was not at the bookmark; re-indexing" , row.owner, row.name) ;
259
record ( app, row, & bookmark, & heads) .await;
260
repairs.reindexed + = 1 ;
f677d627 Andy McCall 23 days ago ↶
261
}
0e04dfc2 Andy McCall 20 days ago ↶
262
}
263
}
264
265
let pins: Vec< ( i64 , String, String, String) > =
266
sqlx:: query_as( " SELECT consumer_id, path, upstream, heads FROM diffusion_pins WHERE upstream_repo_id IS NOT NULL" )
267
.fetch_all ( & app.db)
268
.await
269
.unwrap_or_default ( ) ;
270
let mut caught = std:: collections:: HashSet:: new( ) ;
271
for ( consumer_id, path, upstream, pinned) in pins {
272
if caught.contains ( & consumer_id) {
273
continue ;
274
}
275
let Some ( up) = repo_by_oxy_id ( app, & upstream) .await else { continue } ;
276
let Some ( green) = last_green ( app, & up) .await else { continue } ;
277
if heads_key ( & green) = = heads_key ( & pinned.split ( ' +' ) .filter_map ( ChangeHash:: from_hex) .collect:: < Vec< _ > > ( ) ) {
278
continue ;
279
}
280
let Ok ( Some ( consumer) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
281
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { continue } ;
282
let learned = target ( & app.db, & consumer) .await;
283
let modules = oxymodules ( app, & cname, & learned) .await;
284
if rule ( & app.config.diffusion.follow, & consumer, modules.as_deref ( ) , & path) .follow = = Follow:: Off {
285
continue ;
286
}
287
288
289
290
let bump_bm = bump_bookmark ( & path) ;
291
let newest = | | async {
292
db:: proposals:: list( & app.db, consumer_id, & [ ] )
293
.await
294
.unwrap_or_default ( )
295
.into_iter ( )
296
.filter ( | p | p.source_bookmark = = bump_bm )
297
.max_by_key ( | p | p.number )
298
.map ( | p | p.state )
299
} ;
300
let has_bump = | | async { matches! ( newest ( ) .await.as_deref ( ) , Some ( " open" | " draft" ) ) } ;
301
if matches! ( newest ( ) .await.as_deref ( ) , Some ( " open" | " draft" | " closed" ) ) {
302
continue ;
303
}
304
caught.insert ( consumer_id) ;
305
catch_up ( app, & consumer) .await;
306
if has_bump ( ) .await {
307
tracing:: info! (
308
" diffusion: {}/{} {path} was behind {}/{} with no bump; bumped" ,
309
consumer.owner,
310
consumer.name,
311
up.owner,
312
up.name
313
) ;
314
repairs.caught_up + = 1 ;
f677d627 Andy McCall 23 days ago ↶
315
}
316
}
0e04dfc2 Andy McCall 20 days ago ↶
317
repairs
f677d627 Andy McCall 23 days ago ↶
318
}
319
320
321
322
fn heads_of ( r : & oxy_core:: repo:: Repo, bookmark : & str ) -> Result< Vec< ChangeHash> , crate:: repos:: RepoError> {
323
Ok ( r.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) )
324
}
325
326
327
# [ derive ( Clone, Debug, Serialize ) ]
328
pub struct Consumer {
329
pub owner : String,
330
pub name : String,
331
pub bookmark : String,
332
pub path : String,
333
334
pub pinned : Vec< String> ,
335
336
337
pub behind : Option< usize > ,
757195ad Andy McCall 23 days ago ↶
338
339
pub follow : Follow,
340
341
pub bump : Option< i64 > ,
342
343
pub run : Option< ( i64 , String) > ,
344
345
pub landed : Option< ( i64 , i64 ) > ,
346
347
pub colour : Colour,
73ef9f76 Andy McCall 23 days ago ↶
348
349
pub upstream : Half,
350
pub downstream : Half,
757195ad Andy McCall 23 days ago ↶
351
}
352
eb2a3502 Andy McCall 23 days ago ↶
353
impl Consumer {
354
355
pub fn api ( & self , upstream : & str ) -> hub_api:: DiffusionPin {
356
let half = | h : & Half| hub_api:: DiffusionHalf {
357
tone: h.class.trim_start_matches ( " df-" ) .to_string ( ) ,
358
word: h.word.to_string ( ) ,
359
title: h.title.clone ( ) ,
360
} ;
361
hub_api:: DiffusionPin {
362
upstream: upstream.to_string ( ) ,
363
consumer: format! ( " {} /{} " , self .owner, self .name) ,
364
path: self .path.clone ( ) ,
365
bookmark: self .bookmark.clone ( ) ,
366
behind: self .behind.map ( | n | n as u64 ) ,
367
follow: self .follow.as_str ( ) .to_string ( ) ,
368
bump: self .bump,
369
run: self .run.as_ref ( ) .map ( | r | r.0 ) ,
370
run_status: self .run.as_ref ( ) .map ( | r | r.1. clone ( ) ) ,
371
upstream_state: half ( & self .upstream) ,
372
consumer_state: half ( & self .downstream) ,
373
}
374
}
375
}
376
73ef9f76 Andy McCall 23 days ago ↶
377
378
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize ) ]
379
pub struct Half {
380
pub class : & 'static str ,
381
pub word : & 'static str ,
382
pub title : String,
383
}
384
385
impl Half {
386
387
pub fn upstream ( unverified : bool , run : Option< & str > , slug : & str ) -> Half {
388
let ( class, word, title) = match ( unverified, run) {
389
( true , _ ) => ( " df-amber" , " untested" , format! ( " {slug} has no checks of its own: nothing has tested it" ) ) ,
390
( false , Some ( " passed" ) ) => ( " df-green" , " checked" , format! ( " {slug} 's newest run passed" ) ) ,
391
( false , Some ( " failed" | " error" | " cancelled" ) ) => ( " df-red" , " failing" , format! ( " {slug} 's newest run failed" ) ) ,
392
( false , _ ) => ( " df-grey" , " testing" , format! ( " {slug} 's newest run has not finished" ) ) ,
393
} ;
394
Half { class, word, title }
395
}
396
397
398
pub fn downstream ( follow : Follow, behind : Option< usize > , bump : Option< i64 > , run : Option< & str > , name : & str ) -> Half {
399
let ( class, word, title) = if follow = = Follow:: Off {
400
( " df-off" , " not following" , format! ( " {name} does not follow it" ) )
401
} else {
402
match ( bump, run) {
403
( Some ( _ ) , Some ( " passed" ) ) => ( " df-green" , " passes" , format! ( " {name} 's run on the bump passed" ) ) ,
404
( Some ( _ ) , Some ( " failed" | " error" | " cancelled" ) ) => ( " df-red" , " fails" , format! ( " {name} 's run on the bump failed" ) ) ,
405
( Some ( _ ) , Some ( _ ) ) => ( " df-grey" , " testing" , format! ( " {name} 's run on the bump has not finished" ) ) ,
406
( Some ( _ ) , None ) => ( " df-grey" , " waiting" , format! ( " {name} 's bump has no run yet" ) ) ,
407
( None , _ ) if behind = = Some ( 0 ) => ( " df-green" , " up to date" , format! ( " {name} pins the newest state" ) ) ,
408
( None , _ ) => ( " df-grey" , " waiting" , format! ( " {name} is behind; its bump has not been made yet" ) ) ,
409
}
410
} ;
411
Half { class, word, title }
412
}
413
}
414
757195ad Andy McCall 23 days ago ↶
415
416
417
418
419
420
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize ) ]
421
# [ serde ( rename_all = " kebab-case" ) ]
422
pub enum Colour {
423
Green,
424
Amber,
425
Red,
426
Grey,
427
UpToDate,
428
Off,
f677d627 Andy McCall 23 days ago ↶
429
}
430
757195ad Andy McCall 23 days ago ↶
431
impl Colour {
432
pub fn class ( & self ) -> & 'static str {
433
match self {
434
Colour:: Green => " df-green" ,
435
Colour:: Amber => " df-amber" ,
436
Colour:: Red => " df-red" ,
437
Colour:: Grey => " df-grey" ,
438
Colour:: UpToDate => " df-current" ,
439
Colour:: Off => " df-off" ,
440
}
441
}
442
443
pub fn label ( & self ) -> & 'static str {
444
match self {
445
Colour:: Green => " green" ,
446
Colour:: Amber => " amber: upstream unverified" ,
447
Colour:: Red => " red: its run failed" ,
448
Colour:: Grey => " testing" ,
449
Colour:: UpToDate => " up to date" ,
450
Colour:: Off => " not following" ,
451
}
452
}
453
454
455
pub fn of ( follow : Follow, behind : Option< usize > , bump : Option< i64 > , run : Option< & str > , unverified : bool ) -> Colour {
456
if follow = = Follow:: Off {
457
return Colour:: Off;
458
}
459
match ( bump, run) {
460
( Some ( _ ) , Some ( " passed" ) ) if unverified => Colour:: Amber,
461
( Some ( _ ) , Some ( " passed" ) ) => Colour:: Green,
462
( Some ( _ ) , Some ( " failed" | " error" | " cancelled" ) ) => Colour:: Red,
463
( Some ( _ ) , _ ) => Colour:: Grey,
464
( None , _ ) if behind = = Some ( 0 ) => Colour:: UpToDate,
465
( None , _ ) => Colour:: Grey,
466
}
467
}
468
}
469
f677d627 Andy McCall 23 days ago ↶
470
471
472
pub async fn consumers ( app : & Shared, row : & RepoRow, account : Option< & Account> ) -> Result< Vec< Consumer> , crate:: repos:: RepoError> {
473
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { return Ok ( Vec :: new( ) ) } ;
474
let id = app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await? ;
475
let found: Vec< ( i64 , String, String, String) > = sqlx:: query_as(
476
" SELECT consumer_id, bookmark, path, heads FROM diffusion_pins WHERE upstream = ? ORDER BY consumer_id, bookmark, path" ,
477
)
478
.bind ( hex:: encode( id) )
479
.fetch_all ( & app.db)
480
.await
481
.map_err ( | e | crate :: repos:: RepoError:: Io( std:: io:: Error:: other( e) ) ) ? ;
482
let mut out = Vec :: new( ) ;
483
let mut pinned_sets = Vec :: new( ) ;
757195ad Andy McCall 23 days ago ↶
484
let unverified = unverified ( app, row) .await;
f677d627 Andy McCall 23 days ago ↶
485
for ( consumer_id, bookmark, path, heads) in found {
486
let Ok ( Some ( c) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
487
if ! roles:: allows( roles:: role_for( & app.db, account, & c) .await.ok ( ) .flatten ( ) , crate :: auth:: Access:: Read) {
757195ad Andy McCall 23 days ago ↶
488
continue ;
489
}
490
491
if bookmark ! = target ( & app.db, & c) .await {
f677d627 Andy McCall 23 days ago ↶
492
continue ;
493
}
494
let pinned: Vec< String> = heads.split ( ' +' ) .filter ( | h | ! h.is_empty ( ) ) .map ( String :: from) .collect ( ) ;
495
pinned_sets.push ( pinned.iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect:: < Vec< _ > > ( ) ) ;
757195ad Andy McCall 23 days ago ↶
496
let bump_bm = bump_bookmark ( & path) ;
497
let mine: Vec< db:: proposals:: ProposalRow> = db:: proposals:: list( & app.db, c.id, & [ ] )
498
.await
499
.unwrap_or_default ( )
500
.into_iter ( )
501
.filter ( | p | p.source_bookmark = = bump_bm )
502
.collect ( ) ;
503
let bump = mine.iter ( ) .find ( | p | p.state = = " open" | | p.state = = " draft" ) .map ( | p | p.number ) ;
504
let landed = mine.iter ( ) .filter ( | p | p.state = = " landed" ) .max_by_key ( | p | p.landed_ms ) .map ( | p | ( p.number, p.landed_ms) ) ;
505
let run: Option< ( i64 , String) > =
506
sqlx:: query_as( " SELECT number, status FROM flow_runs WHERE repo_id = ? AND bookmark = ? ORDER BY id DESC LIMIT 1" )
507
.bind ( c.id)
508
.bind ( & bump_bm)
509
.fetch_optional ( & app.db)
510
.await
511
.ok ( )
512
.flatten ( ) ;
513
let follow = rule ( & app.config.diffusion.follow, & c, None , & path) .follow;
514
out.push ( Consumer {
515
owner: c.owner,
516
name: c.name,
517
bookmark,
518
path,
519
pinned,
520
behind: None ,
521
follow,
522
bump,
523
run,
524
landed,
525
colour: Colour:: Off,
73ef9f76 Andy McCall 23 days ago ↶
526
upstream: Half { class: " df-grey" , word: " " , title: String :: new( ) } ,
527
downstream: Half { class: " df-grey" , word: " " , title: String :: new( ) } ,
757195ad Andy McCall 23 days ago ↶
528
} ) ;
f677d627 Andy McCall 23 days ago ↶
529
}
530
if out.is_empty ( ) {
531
return Ok ( out) ;
532
}
533
let default = row.default_bookmark.clone ( ) ;
534
let behind = app
535
.repos
536
.read ( & name, move | r, cache| {
537
let tip = heads_of ( r, & default) ? ;
538
let tip = cache.summary ( r, & tip) ? ;
539
Ok ( pinned_sets
540
.iter ( )
541
.map ( | p | cache.summary ( r, p) .ok ( ) .map ( | s | tip.changes.iter ( ) .filter ( | c | ! s.changes.contains ( * c) ) .count ( ) ) )
542
.collect:: < Vec< _ > > ( ) )
543
} )
544
.await? ;
73ef9f76 Andy McCall 23 days ago ↶
545
let up_run: Option< String> =
546
sqlx:: query_scalar( " SELECT status FROM flow_runs WHERE repo_id = ? AND bookmark = ? ORDER BY id DESC LIMIT 1" )
547
.bind ( row.id)
548
.bind ( & row.default_bookmark)
549
.fetch_optional ( & app.db)
550
.await
551
.ok ( )
552
.flatten ( ) ;
553
let slug = format! ( " {} /{} " , row.owner, row.name) ;
f677d627 Andy McCall 23 days ago ↶
554
for ( c, b) in out.iter_mut ( ) .zip ( behind) {
555
c.behind = b;
73ef9f76 Andy McCall 23 days ago ↶
556
let run = c.run.as_ref ( ) .map ( | r | r.1. clone ( ) ) ;
557
c.colour = Colour:: of( c.follow, b, c.bump, run.as_deref ( ) , unverified) ;
558
c.upstream = Half:: upstream( unverified, up_run.as_deref ( ) , & slug) ;
559
c.downstream = Half:: downstream( c.follow, b, c.bump, run.as_deref ( ) , & format! ( " {} /{} " , c.owner, c.name) ) ;
f677d627 Andy McCall 23 days ago ↶
560
}
561
Ok ( out)
757195ad Andy McCall 23 days ago ↶
562
}
563
564
565
566
pub async fn follows ( app : & Shared, consumer : & RepoRow, account : Option< & Account> ) -> Vec< ( RepoRow, Consumer) > {
567
let ups: Vec< i64 > =
568
sqlx:: query_scalar( " SELECT DISTINCT upstream_repo_id FROM diffusion_pins WHERE consumer_id = ? AND upstream_repo_id IS NOT NULL" )
569
.bind ( consumer.id)
570
.fetch_all ( & app.db)
571
.await
572
.unwrap_or_default ( ) ;
573
let mut out = Vec :: new( ) ;
574
for id in ups {
575
let Ok ( Some ( up) ) = db:: repos:: get_by_id( & app.db, id) .await else { continue } ;
576
if ! roles:: allows( roles:: role_for( & app.db, account, & up) .await.ok ( ) .flatten ( ) , crate :: auth:: Access:: Read) {
577
continue ;
578
}
579
for c in consumers ( app, & up, account) .await.unwrap_or_default ( ) {
580
if c.owner = = consumer.owner & & c.name = = consumer.name {
581
out.push ( ( up.clone ( ) , c) ) ;
582
}
583
}
584
}
585
out
586
}
587
588
589
pub async fn everything ( app : & Shared, account : Option< & Account> ) -> Vec< ( RepoRow, Vec< Consumer> , bool ) > {
590
let ups: Vec< i64 > = sqlx:: query_scalar( " SELECT DISTINCT upstream_repo_id FROM diffusion_pins WHERE upstream_repo_id IS NOT NULL" )
591
.fetch_all ( & app.db)
592
.await
593
.unwrap_or_default ( ) ;
594
let mut out = Vec :: new( ) ;
595
for id in ups {
596
let Ok ( Some ( up) ) = db:: repos:: get_by_id( & app.db, id) .await else { continue } ;
597
let list = consumers ( app, & up, account) .await.unwrap_or_default ( ) ;
598
let unverified = unverified ( app, & up) .await;
599
out.push ( ( up, list, unverified) ) ;
600
}
601
out
f677d627 Andy McCall 23 days ago ↶
602
}
e7158350 Andy McCall 23 days ago ↶
603
1d634f69 Andy McCall 23 days ago ↶
604
605
pub fn bump_bookmark ( path : & str ) -> String {
606
format! ( " diffusion/{} " , path.trim_matches ( ' /' ) )
607
}
608
609
610
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
611
pub enum Bumped {
612
f01cb865 Andy McCall 23 days ago ↶
613
Moved { bookmark: String , change: ChangeHash, heads: Vec< ChangeHash> , from: Vec< ChangeHash> , state: String } ,
1d634f69 Andy McCall 23 days ago ↶
614
615
UpToDate,
616
9aaa88b2 Andy McCall 23 days ago ↶
617
Already { from: Vec< ChangeHash> , to: Vec< ChangeHash> } ,
7a14bdae Andy McCall 23 days ago ↶
618
619
Unknown,
620
621
622
Conflicts,
1d634f69 Andy McCall 23 days ago ↶
623
624
625
NoPin,
626
}
627
7a14bdae Andy McCall 23 days ago ↶
628
629
630
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
631
pub enum Plan {
632
633
To( Vec< ChangeHash> , usize ) ,
634
635
UpToDate,
636
637
638
Unknown,
639
640
Conflicts,
641
}
642
643
644
645
646
647
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 ↶
648
app.repos
649
.read ( upstream, move | r, cache| {
7a14bdae Andy McCall 23 days ago ↶
650
let Ok ( o) = cache.summary ( r, & old) else { return Ok ( Plan:: Unknown) } ;
651
let n = cache.summary ( r, & new) ? ;
652
if o.changes.iter ( ) .all ( | c | n.changes.contains ( c) ) {
653
let added = n.changes.len ( ) - o.changes.len ( ) ;
654
return Ok ( if added = = 0 { Plan:: UpToDate } else { Plan:: To( new, added) } ) ;
655
}
656
let mut both = new.clone ( ) ;
657
both.extend ( old.iter ( ) .copied ( ) ) ;
658
let heads = r.load_state ( & both) ? .heads ( ) ;
659
let b = cache.summary ( r, & heads) ? ;
660
if b.conflicted & & ! o.conflicted {
661
return Ok ( Plan:: Conflicts) ;
1d634f69 Andy McCall 23 days ago ↶
662
}
7a14bdae Andy McCall 23 days ago ↶
663
let added = b.changes.len ( ) - o.changes.len ( ) ;
664
Ok ( if added = = 0 { Plan:: UpToDate } else { Plan:: To( heads, added) } )
1d634f69 Andy McCall 23 days ago ↶
665
} )
666
.await
667
}
668
669
670
671
672
pub async fn bump (
673
app : & Shared,
674
consumer : & RepoRow,
675
target : & str ,
676
path : & str ,
677
upstream : & RepoRow,
678
new_heads : Vec< ChangeHash> ,
679
) -> Result< Bumped, crate:: repos:: RepoError> {
680
use oxy_core:: identity:: Domain;
681
use oxy_core:: subrepo:: Pin;
682
let ( Some ( cname) , Some ( uname) ) = ( RepoName:: new( & consumer.owner, & consumer.name) , RepoName:: new( & upstream.owner, & upstream.name) )
683
else {
684
return Ok ( Bumped:: NoPin) ;
685
} ;
686
let upstream_id = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await? ;
687
688
689
let ( t, p) = ( target.to_string ( ) , path.trim_matches ( ' /' ) .to_string ( ) ) ;
690
let ( current, bumped) = app
691
.repos
692
.read ( & cname, move | r, cache| {
693
let pin_on = | bookmark : & str | -> Result< Option< Pin> , crate:: repos:: RepoError> {
694
let heads = heads_of ( r, bookmark) ? ;
695
if heads.is_empty ( ) {
696
return Ok ( None ) ;
697
}
698
let loaded = cache.load ( r, & heads) ? ;
699
let Some ( f) = loaded.snapshot.files.get ( & p) else { return Ok ( None ) } ;
700
let oxy_core:: state:: FileContent:: Opaque { blobs } = & f.content else { return Ok ( None ) } ;
701
let Some ( b) = blobs.first ( ) else { return Ok ( None ) } ;
702
Ok ( Pin:: decode( & r.read_blob ( b) ? ) .ok ( ) )
703
} ;
704
Ok ( ( pin_on ( & t) ? , pin_on ( & bump_bookmark ( & p) ) ? ) )
705
} )
706
.await? ;
707
let Some ( Pin:: Oxy { repo, heads: old, bookmark } ) = current else { return Ok ( Bumped:: NoPin) } ;
7a14bdae Andy McCall 23 days ago ↶
708
if repo ! = upstream_id {
709
return Ok ( Bumped:: NoPin) ;
710
}
711
let new_heads = match plan ( app, & uname, old.clone ( ) , new_heads.clone ( ) ) .await? {
712
Plan:: To( heads, _ ) => heads,
713
Plan:: UpToDate => return Ok ( Bumped:: UpToDate) ,
714
Plan:: Unknown => return Ok ( Bumped:: Unknown) ,
715
Plan:: Conflicts => return Ok ( Bumped:: Conflicts) ,
716
} ;
1d634f69 Andy McCall 23 days ago ↶
717
if let Some ( Pin:: Oxy { heads: b, .. } ) = & bumped {
718
let ( mut b, mut n) = ( b.clone ( ) , new_heads.clone ( ) ) ;
719
b.sort ( ) ;
720
n.sort ( ) ;
721
if b = = n {
9aaa88b2 Andy McCall 23 days ago ↶
722
return Ok ( Bumped:: Already { from: old, to: new_heads } ) ;
1d634f69 Andy McCall 23 days ago ↶
723
}
724
}
725
let ( key, secret) =
726
crate :: keys:: load_secret( & app.config.keys_dir ( ) ) .map_err ( | e | crate :: repos:: RepoError:: Io( std:: io:: Error:: other( e.to_string ( ) ) ) ) ? ;
727
let identity = app.hub_key.identity.clone ( ) ;
728
let upstream_label = format! ( " {} /{} " , upstream.owner, upstream.name) ;
729
let ( t, p) = ( target.to_string ( ) , path.trim_matches ( ' /' ) .to_string ( ) ) ;
730
let pin = Pin:: Oxy { repo, heads: new_heads.clone ( ) , bookmark } ;
a7e6e8e6 Andy McCall 23 days ago ↶
731
let old = old.clone ( ) ;
1d634f69 Andy McCall 23 days ago ↶
732
let short: Vec< String> = new_heads.iter ( ) .map ( | h | h.to_hex ( ) [ .. 12 ] .to_string ( ) ) .collect ( ) ;
733
let message = format! ( " Diffusion: move {p} to {upstream_label} {} " , short.join ( " +" ) ) ;
734
app.repos
735
.write ( & cname, move | r, _ | {
736
let base = heads_of ( r, & t) ? ;
737
let change = r.build_pin_change ( & base, & p, & pin, & message, key) ? ;
738
let hash = change.hash ( ) ;
739
let sig = secret.sign ( Domain:: Change, & key, & hash) ;
740
let mut state = r.load_state ( & base) ? ;
741
state.add ( change.clone ( ) ) .map_err ( | e | crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( e.to_string ( ) ) ) ) ? ;
742
let mut candidates = base.clone ( ) ;
743
candidates.push ( hash) ;
744
let heads = state.maximal ( & candidates) ;
745
let bookmark = bump_bookmark ( & p) ;
746
747
748
let mut update = r.host_ref_update ( & bookmark, & heads, Some ( ( & key, & secret) ) ) ? ;
749
update.force = true ;
f01cb865 Andy McCall 23 days ago ↶
750
let state = update.new_state.to_hex ( ) ;
1d634f69 Andy McCall 23 days ago ↶
751
let report = r.apply_hosted ( Some ( ( change, vec! [ sig] ) ) , & [ identity] , update) ? ;
752
if let Some ( refused) = report.results.iter ( ) .find ( | x | ! x.ok) {
753
return Err ( crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( format! (
754
" the bump was refused: {} " ,
755
refused.reason
756
) ) ) ) ;
757
}
f01cb865 Andy McCall 23 days ago ↶
758
Ok ( Bumped:: Moved { bookmark, change: hash, heads, from: old, state } )
1d634f69 Andy McCall 23 days ago ↶
759
} )
760
.await
761
}
762
a7e6e8e6 Andy McCall 23 days ago ↶
763
764
pub const ACCOUNT : & str = " diffusion" ;
765
766
767
pub async fn account ( pool : & Pool) -> Option< Account> {
768
if let Ok ( Some ( a) ) = db:: accounts:: account_by_name( pool, ACCOUNT ) .await {
769
return Some ( a) ;
770
}
771
db:: accounts:: create_account( pool, ACCOUNT , " diffusion@localhost" , " !" , " " , false ) .await.ok ( )
772
}
773
c4e432da Andy McCall 23 days ago ↶
774
775
776
777
778
pub fn watched ( pattern : & str , path : & str ) -> bool {
779
fn m ( p : & [u8 ], s : & [u8 ]) -> bool {
780
match ( p.first ( ) , s.first ( ) ) {
781
( None , None ) => true ,
782
( Some ( b ' *' ) , _ ) if p.get ( 1 ) = = Some ( & b ' *' ) => ( 0 .. = s.len ( ) ) .any ( | i | m ( & p[ 2 .. ] , & s[ i.. ] ) ) ,
783
( Some ( b ' *' ) , _ ) => ( 0 .. = s.len ( ) ) .take_while ( | & i | i = = 0 | | s[ i - 1 ] ! = b ' /' ) .any ( | i | m ( & p[ 1 .. ] , & s[ i.. ] ) ) ,
784
( Some ( b ' ?' ) , Some ( c) ) if * c ! = b ' /' => m ( & p[ 1 .. ] , & s[ 1 .. ] ) ,
785
( Some ( a) , Some ( b) ) if a = = b => m ( & p[ 1 .. ] , & s[ 1 .. ] ) ,
786
_ => false ,
787
}
788
}
789
let p = pattern.trim ( ) .trim_matches ( ' /' ) ;
790
if p.is_empty ( ) {
791
return false ;
792
}
793
m ( p.as_bytes ( ) , path.as_bytes ( ) ) | | ( ! p.contains ( [ ' *' , ' ?' ] ) & & path.starts_with ( & format! ( " {p} /" ) ) )
794
}
795
796
797
798
799
pub struct Changelog {
800
pub lines : Vec< String> ,
801
pub watched : Vec< String> ,
802
}
803
804
pub async fn changelog ( app : & Shared, upstream : & RepoRow, old : Vec< ChangeHash> , new : Vec< ChangeHash> , watch : Vec< String> ) -> Changelog {
805
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { return Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } } ;
a7e6e8e6 Andy McCall 23 days ago ↶
806
let linked: std:: collections:: HashMap< String, String> =
807
db:: accounts:: identity_accounts( & app.db) .await.unwrap_or_default ( ) .into_iter ( ) .collect ( ) ;
808
let base = format! ( " /{} /{} " , upstream.owner, upstream.name) ;
809
app.repos
810
.read ( & name, move | r, cache| {
811
let ( o, n) = ( cache.summary ( r, & old) ? , cache.summary ( r, & new) ? ) ;
c4e432da Andy McCall 23 days ago ↶
812
let mut added: Vec< ( u64 , ChangeHash, String, String, Vec< String> ) > = Vec :: new( ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
813
for h in n.changes.iter ( ) .filter ( | c | ! o.changes.contains ( * c) ) {
814
let Ok ( c) = r.store ( ) .get_change ( h) else { continue } ;
815
let key = c.author.to_hex ( ) ;
816
let who = linked.get ( & key) .cloned ( ) .unwrap_or_else ( | | key[ .. 12 ] .to_string ( ) ) ;
817
let first = c.message.lines ( ) .next ( ) .unwrap_or ( " " ) .trim ( ) .to_string ( ) ;
c4e432da Andy McCall 23 days ago ↶
818
819
let hits: Vec< String> = if watch.is_empty ( ) {
820
Vec :: new( )
821
} else {
822
crate :: repos:: view:: change( r, cache, h)
823
.map ( | d | d.files.into_iter ( ) .map ( | f | f.path ) .filter ( | p | watch.iter ( ) .any ( | w | watched ( w, p) ) ) .collect ( ) )
824
.unwrap_or_default ( )
825
} ;
826
added.push ( ( c.hlc.wall_ms, * h, first, who, hits) ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
827
}
828
added.sort_by_key ( | a | std:: cmp:: Reverse( a.0 ) ) ;
c4e432da Andy McCall 23 days ago ↶
829
let line = | h : & ChangeHash, msg : & str , who : & str , flag : bool | {
830
format! (
831
" - {} [`{} `]({base} /change/{} ) {} ({who} )" ,
832
if flag { " ⚑ " } else { " " } ,
833
& h.to_hex ( ) [ .. 12 ] ,
834
h.to_hex ( ) ,
835
msg.replace ( ' \n ' , " " )
836
)
837
} ;
838
let watched = added
839
.iter ( )
840
.filter ( | a | ! a.4. is_empty ( ) )
841
.map ( | ( _, h , msg , who , hits ) | {
842
format! (
843
" {} — touches {} " ,
844
line ( h, msg, who, true ) ,
845
hits.iter ( ) .map ( | p | format! ( " `{p} `" ) ) .collect:: < Vec< _ > > ( ) .join ( " , " )
846
)
a7e6e8e6 Andy McCall 23 days ago ↶
847
} )
c4e432da Andy McCall 23 days ago ↶
848
.collect ( ) ;
849
let lines = added.iter ( ) .map ( | ( _, h , msg , who , hits ) | line ( h, msg, who, ! hits.is_empty ( ) ) ) .collect ( ) ;
850
Ok ( Changelog { lines, watched } )
a7e6e8e6 Andy McCall 23 days ago ↶
851
} )
852
.await
c4e432da Andy McCall 23 days ago ↶
853
.unwrap_or ( Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } )
a7e6e8e6 Andy McCall 23 days ago ↶
854
}
855
9aaa88b2 Andy McCall 23 days ago ↶
856
857
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
858
pub enum Why {
859
860
Green,
861
862
Moved,
863
864
865
Unverified,
866
}
867
868
869
870
871
a7e6e8e6 Andy McCall 23 days ago ↶
872
pub async fn propose (
873
app : & Shared,
874
consumer : & RepoRow,
875
target : & str ,
876
path : & str ,
877
upstream : & RepoRow,
9aaa88b2 Andy McCall 23 days ago ↶
878
( from , to ) : ( Vec< ChangeHash> , Vec< ChangeHash> ) ,
c4e432da Andy McCall 23 days ago ↶
879
( why , watch ) : ( Why, Vec< String> ) ,
a7e6e8e6 Andy McCall 23 days ago ↶
880
) -> Option< i64 > {
881
use crate :: db:: proposals as pdb;
882
let bookmark = bump_bookmark ( path) ;
883
let bookmark = bookmark.as_str ( ) ;
884
let me = account ( & app.db) .await? ;
c4e432da Andy McCall 23 days ago ↶
885
let log = changelog ( app, upstream, from, to.clone ( ) , watch.clone ( ) ) .await;
886
let lines = log.lines;
887
let flagged = if log.watched.is_empty ( ) {
888
String :: new( )
889
} else {
890
format! (
891
" \n \n **Touches what you watch** ({} ):\n \n {} " ,
892
watch.iter ( ) .map ( | w | format! ( " `{w} `" ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ,
893
log.watched.join ( " \n " )
894
)
895
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
896
let slug = format! ( " {} /{} " , upstream.owner, upstream.name) ;
897
let short: Vec< String> = to.iter ( ) .map ( | h | h.to_hex ( ) [ .. 12 ] .to_string ( ) ) .collect ( ) ;
1b6536f0 Andy McCall 23 days ago ↶
898
let ub = & upstream.default_bookmark;
899
let count = if lines.len ( ) = = 1 { " 1 change" .to_string ( ) } else { format! ( " {} changes" , lines.len ( ) ) } ;
900
let title = format! ( " Diffusion: {path} to {slug} {ub} ({count} )" ) ;
9aaa88b2 Andy McCall 23 days ago ↶
901
let when = match why {
902
Why:: Green => " at its newest state whose checks passed" ,
903
Why:: Moved => " as it is now" ,
904
Why:: Unverified => " as it is now" ,
905
} ;
906
let amber = if why = = Why:: Unverified {
907
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." )
908
} else {
909
String :: new( )
910
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
911
let body = format! (
c4e432da Andy McCall 23 days ago ↶
912
" 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). It never lands by itself." ,
1b6536f0 Andy McCall 23 days ago ↶
913
short.join ( " +" ) ,
a7e6e8e6 Andy McCall 23 days ago ↶
914
lines.join ( " \n " )
915
) ;
916
let open = pdb:: list( & app.db, consumer.id, & [ " open" , " draft" ] ) .await.ok ( ) ? .into_iter ( ) .find ( | p | p.source_bookmark = = bookmark ) ;
9aaa88b2 Andy McCall 23 days ago ↶
917
let ( number, id) = match open {
a7e6e8e6 Andy McCall 23 days ago ↶
918
Some ( p) => {
9aaa88b2 Andy McCall 23 days ago ↶
919
if p.title ! = title | | p.body ! = body {
920
pdb:: edit( & app.db, p.id, & title, & body, target) .await.ok ( ) ? ;
921
pdb:: add_event( & app.db, p.id, ACCOUNT , " updated" , & short.join ( " +" ) ) .await.ok ( ) ;
922
}
923
( p.number, p.id)
a7e6e8e6 Andy McCall 23 days ago ↶
924
}
925
None => {
926
let p = pdb:: create(
927
& app.db,
928
pdb:: NewProposal {
929
repo_id: consumer.id,
930
title: & title,
931
body: & body,
932
author_id: Some ( me.id) ,
933
author: ACCOUNT ,
934
draft: false ,
935
target,
936
source_kind: " bookmark" ,
937
source_repo_id: None ,
938
source_bookmark: bookmark,
939
source_changes: " " ,
940
} ,
941
)
942
.await
943
.ok ( ) ? ;
944
pdb:: add_event( & app.db, p.id, ACCOUNT , " opened" , " " ) .await.ok ( ) ;
945
db:: notify:: index( & app.db, " proposal" , consumer.id, p.id, p.number, & p.title, & p.body) .await.ok ( ) ;
946
if let Ok ( label) =
947
db:: issues:: upsert_label( & app.db, consumer.id, " dependencies" , " #0072ff" , " A subrepo moves (Diffusion)" ) .await
948
{
949
db:: issues:: set_proposal_label( & app.db, p.id, label.id, true ) .await.ok ( ) ;
950
}
9aaa88b2 Andy McCall 23 days ago ↶
951
( p.number, p.id)
a7e6e8e6 Andy McCall 23 days ago ↶
952
}
953
} ;
9aaa88b2 Andy McCall 23 days ago ↶
954
955
let unverified = db:: issues:: label_by_name( & app.db, consumer.id, " upstream unverified" ) .await.ok ( ) .flatten ( ) ;
956
match ( why = = Why:: Unverified, unverified) {
957
( true , l) => {
958
let l = match l {
959
Some ( l) => Some ( l) ,
960
None => db:: issues:: upsert_label(
961
& app.db,
962
consumer.id,
963
" upstream unverified" ,
964
" #e3a008" ,
965
" The upstream has no checks (Diffusion)" ,
966
)
967
.await
968
.ok ( ) ,
969
} ;
970
if let Some ( l) = l {
971
db:: issues:: set_proposal_label( & app.db, id, l.id, true ) .await.ok ( ) ;
972
}
973
}
974
( false , Some ( l) ) => {
975
db:: issues:: set_proposal_label( & app.db, id, l.id, false ) .await.ok ( ) ;
976
}
977
( false , None ) => { }
978
}
a7e6e8e6 Andy McCall 23 days ago ↶
979
Some ( number)
980
}
981
f01cb865 Andy McCall 23 days ago ↶
982
983
984
985
fn start_runs (
986
app : Shared,
987
row : RepoRow,
988
bookmark : String,
989
state : String,
990
heads : Vec< ChangeHash> ,
991
) -> std:: pin:: Pin< Box< dyn std:: future:: Future< Output = ( ) > + Send> > {
992
Box :: pin( async move { crate :: flow:: on_push( & app, & row, & bookmark, & state, & heads, ACCOUNT ) .await } )
993
}
994
a7e6e8e6 Andy McCall 23 days ago ↶
995
996
async fn oxymodules ( app : & Shared, name : & RepoName, bookmark : & str ) -> Option< String> {
997
let b = bookmark.to_string ( ) ;
998
app.repos
999
.read ( name, move | r, cache| {
1000
let heads = heads_of ( r, & b) ? ;
1001
if heads.is_empty ( ) {
1002
return Ok ( None ) ;
1003
}
1004
let loaded = cache.load ( r, & heads) ? ;
1005
Ok ( match loaded.snapshot.files.get ( " .oxymodules" ) .map ( | f | & f.content) {
1006
Some ( oxy_core:: state:: FileContent:: Text { bytes, .. } ) => Some ( String :: from_utf8_lossy( bytes) .into_owned ( ) ) ,
1007
_ => None ,
1008
} )
1009
} )
1010
.await
1011
.ok ( )
1012
.flatten ( )
1013
}
1014
1015
1016
1017
1018
pub async fn propagate ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool ) {
3776b70d Andy McCall 23 days ago ↶
1019
propagate_to ( app, upstream, heads, green, None ) .await
1020
}
1021
1022
1023
async fn propagate_to ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool , only : Option< i64 > ) {
a7e6e8e6 Andy McCall 23 days ago ↶
1024
let Some ( uname) = RepoName:: new( & upstream.owner, & upstream.name) else { return } ;
1025
let Ok ( id) = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await else { return } ;
1026
let rows: Vec< ( i64 , String, String) > = sqlx:: query_as( " SELECT consumer_id, bookmark, path FROM diffusion_pins WHERE upstream = ?" )
1027
.bind ( hex:: encode( id) )
1028
.fetch_all ( & app.db)
1029
.await
1030
.unwrap_or_default ( ) ;
9aaa88b2 Andy McCall 23 days ago ↶
1031
let unverified = unverified ( app, upstream) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
1032
for ( consumer_id, indexed, path) in rows {
3776b70d Andy McCall 23 days ago ↶
1033
if only.is_some_and ( | o | o ! = consumer_id ) {
1034
continue ;
1035
}
a7e6e8e6 Andy McCall 23 days ago ↶
1036
let Ok ( Some ( consumer) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
1037
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { continue } ;
1038
let learned = target ( & app.db, & consumer) .await;
1039
let modules = oxymodules ( app, & cname, & learned) .await;
1040
let r = rule ( & app.config.diffusion.follow, & consumer, modules.as_deref ( ) , & path) ;
1041
let target = r.target.clone ( ) .unwrap_or ( learned) ;
1042
if indexed ! = target {
1043
continue ;
1044
}
9aaa88b2 Andy McCall 23 days ago ↶
1045
1046
a7e6e8e6 Andy McCall 23 days ago ↶
1047
let go = match r.follow {
1048
Follow:: Off => false ,
9aaa88b2 Andy McCall 23 days ago ↶
1049
Follow:: Green => green | | unverified,
a7e6e8e6 Andy McCall 23 days ago ↶
1050
Follow:: Any => ! green,
1051
} ;
9aaa88b2 Andy McCall 23 days ago ↶
1052
let why = if green {
1053
Why:: Green
1054
} else if unverified {
1055
Why:: Unverified
1056
} else {
1057
Why:: Moved
1058
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
1059
if ! go {
1060
continue ;
1061
}
1062
match bump ( app, & consumer, & target, & path, upstream, heads.clone ( ) ) .await {
f01cb865 Andy McCall 23 days ago ↶
1063
Ok ( Bumped:: Moved { from, bookmark, heads: bumped, state, .. } ) => {
1064
1065
tokio:: spawn( start_runs ( app.clone ( ) , consumer.clone ( ) , bookmark, state, bumped) ) ;
c4e432da Andy McCall 23 days ago ↶
1066
let n = propose ( app, & consumer, & target, & path, upstream, ( from, heads.clone ( ) ) , ( why, r.watch.clone ( ) ) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
1067
tracing:: info! (
1068
" diffusion: {}/{} {path} bumped to {}/{} (proposal {n:?})" ,
1069
consumer.owner,
1070
consumer.name,
1071
upstream.owner,
1072
upstream.name
1073
) ;
9aaa88b2 Andy McCall 23 days ago ↶
1074
}
1075
Ok ( Bumped:: Already { from, to } ) => {
1076
1077
c4e432da Andy McCall 23 days ago ↶
1078
propose ( app, & consumer, & target, & path, upstream, ( from, to) , ( why, r.watch.clone ( ) ) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
1079
}
7a14bdae Andy McCall 23 days ago ↶
1080
Ok ( other) => tracing:: info! ( " diffusion: {}/{} {path}: no bump ({other:?})" , consumer.owner, consumer.name) ,
a7e6e8e6 Andy McCall 23 days ago ↶
1081
Err ( e) => tracing:: warn! ( " diffusion: bump {}/{} {path}: {e}" , consumer.owner, consumer.name) ,
3776b70d Andy McCall 23 days ago ↶
1082
}
1083
}
9aaa88b2 Andy McCall 23 days ago ↶
1084
}
1085
1086
1087
1088
1089
pub async fn unverified ( app : & Shared, upstream : & RepoRow) -> bool {
1090
if ! crate :: flow:: enabled( app, upstream) {
1091
return true ;
1092
}
1093
let runs: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM flow_runs WHERE repo_id = ? AND bookmark = ?" )
1094
.bind ( upstream.id)
1095
.bind ( & upstream.default_bookmark)
1096
.fetch_one ( & app.db)
1097
.await
1098
.unwrap_or ( 0 ) ;
1099
runs = = 0
3776b70d Andy McCall 23 days ago ↶
1100
}
1101
1102
1103
async fn repo_by_oxy_id ( app : & Shared, id : & str ) -> Option< RepoRow> {
1104
for row in db:: repos:: list( & app.db) .await.ok ( ) ? {
1105
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { continue } ;
1106
if app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await.ok ( ) .map ( hex:: encode) .as_deref ( ) = = Some ( id) {
1107
return Some ( row) ;
1108
}
1109
}
1110
None
1111
}
1112
1113
1114
async fn last_green ( app : & Shared, upstream : & RepoRow) -> Option< Vec< ChangeHash> > {
1115
let heads: String =
1116
sqlx:: query_scalar( " SELECT heads FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND status = 'passed' ORDER BY id DESC LIMIT 1" )
1117
.bind ( upstream.id)
1118
.bind ( & upstream.default_bookmark)
1119
.fetch_optional ( & app.db)
1120
.await
1121
.ok ( )
1122
.flatten ( ) ? ;
1123
let h: Vec< ChangeHash> = heads.split ( ' +' ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
1124
( ! h.is_empty ( ) ) .then_some ( h)
1125
}
1126
1127
1128
1129
1130
pub async fn catch_up ( app : & Shared, consumer : & RepoRow) {
1131
let upstreams: Vec< String> = sqlx:: query_scalar( " SELECT DISTINCT upstream FROM diffusion_pins WHERE consumer_id = ?" )
1132
.bind ( consumer.id)
1133
.fetch_all ( & app.db)
1134
.await
1135
.unwrap_or_default ( ) ;
1136
for id in upstreams {
1137
let Some ( upstream) = repo_by_oxy_id ( app, & id) .await else { continue } ;
1138
if let Some ( h) = last_green ( app, & upstream) .await {
1139
propagate_to ( app, & upstream, h, true , Some ( consumer.id) ) .await;
1140
}
1141
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { continue } ;
1142
let b = upstream.default_bookmark.clone ( ) ;
1143
if let Ok ( h) = app.repos.read ( & name, move | r, _ | heads_of ( r, & b) ) .await {
1144
propagate_to ( app, & upstream, h, false , Some ( consumer.id) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
1145
}
1146
}
1147
}
1148
e7158350 Andy McCall 23 days ago ↶
1149
1150
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize ) ]
1151
# [ serde ( rename_all = " lowercase" ) ]
1152
pub enum Follow {
1153
Off,
1154
1155
Green,
1156
1157
Any,
1158
}
1159
1160
impl Follow {
1161
pub fn parse ( s : & str ) -> Option< Follow> {
1162
match s.trim ( ) {
1163
" off" => Some ( Follow:: Off) ,
1164
" green" => Some ( Follow:: Green) ,
1165
" any" => Some ( Follow:: Any) ,
1166
_ => None ,
eb2a3502 Andy McCall 23 days ago ↶
1167
}
1168
}
1169
1170
pub fn as_str ( self ) -> & 'static str {
1171
match self {
1172
Follow:: Off => " off" ,
1173
Follow:: Green => " green" ,
1174
Follow:: Any => " any" ,
e7158350 Andy McCall 23 days ago ↶
1175
}
1176
}
1177
}
1178
1179
1180
1181
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize ) ]
1182
pub struct Rule {
1183
pub follow : Follow,
1184
1185
pub land_auto : bool ,
1186
1187
pub target : Option< String> ,
1188
1189
1190
pub watch : Vec< String> ,
1191
1192
pub from : & 'static str ,
1193
}
1194
1195
1196
1197
pub fn module_keys ( text : & str , path : & str ) -> ( Option< Follow> , Option< bool > , Option< String> , Vec< String> ) {
1198
let ( mut follow, mut land, mut target, mut watch) = ( None , None , None , Vec :: new( ) ) ;
1199
let mut here = false ;
1200
for line in text.lines ( ) {
1201
let line = line.trim ( ) ;
1202
if line.is_empty ( ) | | line.starts_with ( ' #' ) {
1203
continue ;
1204
}
1205
if let Some ( inner) = line.strip_prefix ( ' [' ) .and_then ( | l | l.strip_suffix ( ' ]' ) ) {
1206
here = inner.trim ( ) .trim_matches ( ' "' ) .trim_end_matches ( ' /' ) = = path.trim_end_matches ( ' /' ) ;
1207
continue ;
1208
}
1209
let ( true , Some ( ( k, v) ) ) = ( here, line.split_once ( ' =' ) ) else { continue } ;
1210
let v = v.trim ( ) ;
1211
let unquoted = v.trim_matches ( ' "' ) .trim ( ) ;
1212
match k.trim ( ) {
1213
" follow" => follow = Follow:: parse( unquoted) ,
1214
" land" => land = Some ( unquoted = = " auto" ) ,
1215
" target" if ! unquoted.is_empty ( ) => target = Some ( unquoted.to_string ( ) ) ,
1216
" watch" => {
1217
watch = v
1218
.trim_start_matches ( ' [' )
1219
.trim_end_matches ( ' ]' )
1220
.split ( ' ,' )
1221
.map ( | w | w.trim ( ) .trim_matches ( ' "' ) .trim ( ) .to_string ( ) )
1222
.filter ( | w | ! w.is_empty ( ) )
1223
.collect ( )
1224
}
1225
_ => { }
1226
}
1227
}
1228
( follow, land, target, watch)
1229
}
1230
1231
1232
pub fn rule ( site_default : & str , row : & RepoRow, oxymodules : Option< & str > , path : & str ) -> Rule {
1233
let ( follow, land, target, watch) = oxymodules.map ( | t | module_keys ( t, path) ) .unwrap_or_default ( ) ;
1234
let ( follow, from) = match follow {
1235
Some ( f) => ( f, " .oxymodules" ) ,
1236
None => match Follow:: parse( & row.diffusion) {
1237
Some ( f) => ( f, " setting" ) ,
1238
None => ( Follow:: parse( site_default) .unwrap_or ( Follow:: Off) , " site" ) ,
1239
} ,
1240
} ;
1241
Rule { follow, land_auto: land.unwrap_or ( false ) , target, watch, from }
1242
}
1243
1244
# [ cfg ( test ) ]
1245
mod tests {
1246
use super :: * ;
1247
1248
const MODULES : & str = r # "
1249
[rstar-api]
1250
url = https://oxygenhub.io/doomsdayone/rstar-api
1251
follow = green
1252
land = "propose"
1253
target = develop
1254
watch = ["src/x16/irq*", "src/x16/frame*"]
1255
1256
[other]
1257
url = https://example/other
1258
follow = any
1259
"# ;
1260
1261
fn row ( diffusion : & str ) -> RepoRow {
1262
RepoRow {
1263
id: 1 ,
1264
owner: " o" .into ( ) ,
1265
name: " n" .into ( ) ,
1266
description: String :: new( ) ,
1267
default_bookmark: " main" .into ( ) ,
1268
visibility: " public" .into ( ) ,
1269
created_ms: 0 ,
1270
updated_ms: 0 ,
1271
git_mirror: false ,
1272
git_mirror_state: String :: new( ) ,
1273
forked_from: None ,
1274
topics: String :: new( ) ,
1275
flow: String :: new( ) ,
1276
diffusion: diffusion.into ( ) ,
1277
.. Default :: default( )
1278
}
c4e432da Andy McCall 23 days ago ↶
1279
}
1280
1281
# [ test ]
1282
fn watch_patterns_match_paths ( ) {
1283
assert! ( watched ( " src/x16/irq*" , " src/x16/irq.s" ) ) ;
1284
assert! ( watched ( " src/x16/irq*" , " src/x16/irq_wait.c" ) ) ;
1285
assert! ( ! watched ( " src/x16/irq*" , " src/x16/sub/irq.s" ) , " one segment" ) ;
1286
assert! ( watched ( " src/**/frame*.c" , " src/x16/floor/frame_wait.c" ) ) ;
1287
assert! ( watched ( " src/x16" , " src/x16/bank.s" ) , " a directory" ) ;
1288
assert! ( ! watched ( " src/x16" , " src/x160/bank.s" ) ) ;
1289
assert! ( watched ( " include/r?m.h" , " include/rim.h" ) ) ;
1290
assert! ( ! watched ( " " , " anything" ) ) ;
e7158350 Andy McCall 23 days ago ↶
1291
}
1292
1293
# [ test ]
1294
fn oxymodules_keys_are_read_per_entry ( ) {
1295
let ( f, land, target, watch) = module_keys ( MODULES , " rstar-api" ) ;
1296
assert_eq! ( ( f, land, target.as_deref ( ) ) , ( Some ( Follow:: Green) , Some ( false ) , Some ( " develop" ) ) ) ;
1297
assert_eq! ( watch, vec! [ " src/x16/irq*" , " src/x16/frame*" ] ) ;
1298
assert_eq! ( module_keys ( MODULES , " other" ) .0 , Some ( Follow:: Any) ) ;
1299
assert_eq! ( module_keys ( MODULES , " missing" ) , ( None , None , None , vec! [ ] ) ) ;
1300
assert_eq! ( module_keys ( " [a]\n watch = x, y\n " , " a" ) .3 , vec! [ " x" , " y" ] ) ;
1301
}
1302
1303
# [ test ]
1304
fn oxymodules_wins_then_the_setting_then_the_site ( ) {
1305
let r = rule ( " off" , & row ( " any" ) , Some ( MODULES ) , " rstar-api" ) ;
1306
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " .oxymodules" ) ) ;
1307
let r = rule ( " off" , & row ( " any" ) , Some ( " [rstar-api]\n url = x\n " ) , " rstar-api" ) ;
1308
assert_eq! ( ( r.follow, r.from) , ( Follow:: Any, " setting" ) ) ;
1309
let r = rule ( " green" , & row ( " " ) , None , " rstar-api" ) ;
1310
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " site" ) ) ;
1311
assert_eq! ( rule ( " nonsense" , & row ( " " ) , None , " x" ) .follow, Follow:: Off) ;
1312
}
1313
}