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