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 22 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 22 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 22 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 22 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 22 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 22 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 22 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 22 days ago ↶
666
.unwrap_or ( Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } )
a7e6e8e6 Andy McCall 23 days ago ↶
667
}
668
9aaa88b2 Andy McCall 22 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 22 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 22 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 22 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 22 days ago ↶
735
let ( number, id) = match open {
a7e6e8e6 Andy McCall 23 days ago ↶
736
Some ( p) => {
9aaa88b2 Andy McCall 22 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 22 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 ( ) ;
bff9106e Andy McCall 21 days ago ↶
764
765
crate :: notify:: deliver(
766
app,
767
crate :: notify:: Event {
768
kind: " proposal.opened" ,
769
title: format! ( " {} /{} #{} : {} " , consumer.owner, consumer.name, p.number, p.title) ,
770
url: format! ( " /{} /{} /proposal/{} " , consumer.owner, consumer.name, p.number) ,
771
actor: & me,
772
repo_id: consumer.id,
773
thread: Some ( ( " proposal" , p.id) ) ,
774
mention_text: " " ,
775
direct: vec! [ ] ,
776
} ,
777
)
778
.await;
a7e6e8e6 Andy McCall 23 days ago ↶
779
if let Ok ( label) =
780
db:: issues:: upsert_label( & app.db, consumer.id, " dependencies" , " #0072ff" , " A subrepo moves (Diffusion)" ) .await
781
{
782
db:: issues:: set_proposal_label( & app.db, p.id, label.id, true ) .await.ok ( ) ;
783
}
9aaa88b2 Andy McCall 22 days ago ↶
784
( p.number, p.id)
a7e6e8e6 Andy McCall 23 days ago ↶
785
}
786
} ;
9aaa88b2 Andy McCall 22 days ago ↶
787
788
let unverified = db:: issues:: label_by_name( & app.db, consumer.id, " upstream unverified" ) .await.ok ( ) .flatten ( ) ;
789
match ( why = = Why:: Unverified, unverified) {
790
( true , l) => {
791
let l = match l {
792
Some ( l) => Some ( l) ,
793
None => db:: issues:: upsert_label(
794
& app.db,
795
consumer.id,
796
" upstream unverified" ,
797
" #e3a008" ,
798
" The upstream has no checks (Diffusion)" ,
799
)
800
.await
801
.ok ( ) ,
802
} ;
803
if let Some ( l) = l {
804
db:: issues:: set_proposal_label( & app.db, id, l.id, true ) .await.ok ( ) ;
805
}
806
}
807
( false , Some ( l) ) => {
808
db:: issues:: set_proposal_label( & app.db, id, l.id, false ) .await.ok ( ) ;
809
}
810
( false , None ) => { }
811
}
a7e6e8e6 Andy McCall 23 days ago ↶
812
Some ( number)
813
}
814
f01cb865 Andy McCall 23 days ago ↶
815
816
817
818
fn start_runs (
819
app : Shared,
820
row : RepoRow,
821
bookmark : String,
822
state : String,
823
heads : Vec< ChangeHash> ,
824
) -> std:: pin:: Pin< Box< dyn std:: future:: Future< Output = ( ) > + Send> > {
825
Box :: pin( async move { crate :: flow:: on_push( & app, & row, & bookmark, & state, & heads, ACCOUNT ) .await } )
826
}
827
a7e6e8e6 Andy McCall 23 days ago ↶
828
829
async fn oxymodules ( app : & Shared, name : & RepoName, bookmark : & str ) -> Option< String> {
830
let b = bookmark.to_string ( ) ;
831
app.repos
832
.read ( name, move | r, cache| {
833
let heads = heads_of ( r, & b) ? ;
834
if heads.is_empty ( ) {
835
return Ok ( None ) ;
836
}
837
let loaded = cache.load ( r, & heads) ? ;
838
Ok ( match loaded.snapshot.files.get ( " .oxymodules" ) .map ( | f | & f.content) {
839
Some ( oxy_core:: state:: FileContent:: Text { bytes, .. } ) => Some ( String :: from_utf8_lossy( bytes) .into_owned ( ) ) ,
840
_ => None ,
841
} )
842
} )
843
.await
844
.ok ( )
845
.flatten ( )
846
}
847
848
849
850
851
pub async fn propagate ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool ) {
3776b70d Andy McCall 23 days ago ↶
852
propagate_to ( app, upstream, heads, green, None ) .await
853
}
854
855
856
async fn propagate_to ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool , only : Option< i64 > ) {
a7e6e8e6 Andy McCall 23 days ago ↶
857
let Some ( uname) = RepoName:: new( & upstream.owner, & upstream.name) else { return } ;
858
let Ok ( id) = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await else { return } ;
859
let rows: Vec< ( i64 , String, String) > = sqlx:: query_as( " SELECT consumer_id, bookmark, path FROM diffusion_pins WHERE upstream = ?" )
860
.bind ( hex:: encode( id) )
861
.fetch_all ( & app.db)
862
.await
863
.unwrap_or_default ( ) ;
9aaa88b2 Andy McCall 22 days ago ↶
864
let unverified = unverified ( app, upstream) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
865
for ( consumer_id, indexed, path) in rows {
3776b70d Andy McCall 23 days ago ↶
866
if only.is_some_and ( | o | o ! = consumer_id ) {
867
continue ;
868
}
a7e6e8e6 Andy McCall 23 days ago ↶
869
let Ok ( Some ( consumer) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
870
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { continue } ;
871
let learned = target ( & app.db, & consumer) .await;
872
let modules = oxymodules ( app, & cname, & learned) .await;
873
let r = rule ( & app.config.diffusion.follow, & consumer, modules.as_deref ( ) , & path) ;
874
let target = r.target.clone ( ) .unwrap_or ( learned) ;
875
if indexed ! = target {
876
continue ;
877
}
9aaa88b2 Andy McCall 22 days ago ↶
878
879
a7e6e8e6 Andy McCall 23 days ago ↶
880
let go = match r.follow {
881
Follow:: Off => false ,
9aaa88b2 Andy McCall 22 days ago ↶
882
Follow:: Green => green | | unverified,
a7e6e8e6 Andy McCall 23 days ago ↶
883
Follow:: Any => ! green,
884
} ;
9aaa88b2 Andy McCall 22 days ago ↶
885
let why = if green {
886
Why:: Green
887
} else if unverified {
888
Why:: Unverified
889
} else {
890
Why:: Moved
891
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
892
if ! go {
893
continue ;
894
}
895
match bump ( app, & consumer, & target, & path, upstream, heads.clone ( ) ) .await {
f01cb865 Andy McCall 23 days ago ↶
896
Ok ( Bumped:: Moved { from, bookmark, heads: bumped, state, .. } ) => {
897
898
tokio:: spawn( start_runs ( app.clone ( ) , consumer.clone ( ) , bookmark, state, bumped) ) ;
f4d6ef41 Andy McCall 22 days ago ↶
899
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 ↶
900
tracing:: info! (
901
" diffusion: {}/{} {path} bumped to {}/{} (proposal {n:?})" ,
902
consumer.owner,
903
consumer.name,
904
upstream.owner,
905
upstream.name
906
) ;
9aaa88b2 Andy McCall 22 days ago ↶
907
}
908
Ok ( Bumped:: Already { from, to } ) => {
909
910
f4d6ef41 Andy McCall 22 days ago ↶
911
propose ( app, & consumer, & target, & path, upstream, ( from, to) , ( why, r.watch.clone ( ) , r.land_auto) ) .await;
912
913
914
if green & & r.land_auto {
915
tokio:: spawn( try_land ( app.clone ( ) , consumer.clone ( ) , path.clone ( ) ) ) ;
916
}
a7e6e8e6 Andy McCall 23 days ago ↶
917
}
7a14bdae Andy McCall 23 days ago ↶
918
Ok ( other) => tracing:: info! ( " diffusion: {}/{} {path}: no bump ({other:?})" , consumer.owner, consumer.name) ,
a7e6e8e6 Andy McCall 23 days ago ↶
919
Err ( e) => tracing:: warn! ( " diffusion: bump {}/{} {path}: {e}" , consumer.owner, consumer.name) ,
3776b70d Andy McCall 23 days ago ↶
920
}
921
}
f4d6ef41 Andy McCall 22 days ago ↶
922
}
923
924
925
# [ derive ( Debug, PartialEq, Eq ) ]
926
pub enum Held {
927
928
NoBump,
929
930
Manual,
931
932
Unverified,
933
934
UpstreamUnchecked,
935
936
NotPassed,
937
938
Refused( String ) ,
9aaa88b2 Andy McCall 22 days ago ↶
939
}
940
f4d6ef41 Andy McCall 22 days ago ↶
941
942
943
944
945
946
947
948
949
950
pub async fn auto_land ( app : & Shared, consumer : & RepoRow, path : & str ) -> Result< i64 , Held> {
951
let path = path.trim_matches ( ' /' ) ;
952
let bm = bump_bookmark ( path) ;
953
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { return Err ( Held:: NoBump) } ;
954
let bump = db:: proposals:: list( & app.db, consumer.id, & [ " open" ] )
955
.await
956
.ok ( )
957
.and_then ( | ps | ps.into_iter ( ) .find ( | p | p.source_bookmark = = bm & & p.author = = ACCOUNT ) )
958
.ok_or ( Held:: NoBump) ? ;
959
let learned = target ( & app.db, consumer) .await;
960
let modules = oxymodules ( app, & cname, & learned) .await;
961
if ! rule ( & app.config.diffusion.follow, consumer, modules.as_deref ( ) , path) .land_auto {
962
return Err ( Held:: Manual) ;
963
}
964
let upstream_id: Option< i64 > = sqlx:: query_scalar(
965
" SELECT upstream_repo_id FROM diffusion_pins WHERE consumer_id = ? AND path = ? AND upstream_repo_id IS NOT NULL LIMIT 1" ,
966
)
967
.bind ( consumer.id)
968
.bind ( path)
969
.fetch_optional ( & app.db)
970
.await
971
.ok ( )
972
.flatten ( ) ;
973
let Some ( upstream) = ( match upstream_id {
974
Some ( id) => db:: repos:: get_by_id( & app.db, id) .await.ok ( ) .flatten ( ) ,
975
None => None ,
976
} ) else {
977
return Err ( Held:: UpstreamUnchecked) ;
978
} ;
979
if unverified ( app, & upstream) .await {
980
return Err ( Held:: Unverified) ;
981
}
982
983
let ( p, b) = ( path.to_string ( ) , bm.clone ( ) ) ;
984
let ( pinned, now) = app
985
.repos
986
.read ( & cname, move | r, cache| {
987
let heads = heads_of ( r, & b) ? ;
988
if heads.is_empty ( ) {
989
return Ok ( ( None , heads) ) ;
990
}
991
let loaded = cache.load ( r, & heads) ? ;
992
let pin = match loaded.snapshot.files.get ( & p) .map ( | f | & f.content) {
993
Some ( oxy_core:: state:: FileContent:: Opaque { blobs } ) => match blobs.first ( ) {
994
Some ( blob) => oxy_core:: subrepo:: Pin:: decode( & r.read_blob ( blob) ? ) .ok ( ) ,
995
None => None ,
996
} ,
997
_ => None ,
998
} ;
999
Ok ( ( pin, heads) )
1000
} )
1001
.await
1002
.map_err ( | e | Held:: Refused( e.to_string ( ) ) ) ? ;
1003
let Some ( oxy_core:: subrepo:: Pin:: Oxy { heads: pinned, .. } ) = pinned else { return Err ( Held:: UpstreamUnchecked) } ;
1004
let same = | text : & str , want : & [ChangeHash]| {
1005
let mut a: Vec< String> = text.split ( ' +' ) .filter ( | h | ! h.is_empty ( ) ) .map ( str :: to_string) .collect ( ) ;
1006
let mut b: Vec< String> = want.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ;
1007
a.sort ( ) ;
1008
b.sort ( ) ;
1009
a = = b
1010
} ;
1011
let upstream_runs: Vec< String> = sqlx:: query_scalar(
1012
" SELECT heads FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND status = 'passed' ORDER BY id DESC LIMIT 100" ,
1013
)
1014
.bind ( upstream.id)
1015
.bind ( & upstream.default_bookmark)
1016
.fetch_all ( & app.db)
1017
.await
1018
.unwrap_or_default ( ) ;
1019
if ! upstream_runs.iter ( ) .any ( | h | same ( h, & pinned) ) {
1020
return Err ( Held:: UpstreamUnchecked) ;
1021
}
1022
let run: Option< ( String, String) > =
1023
sqlx:: query_as( " SELECT status, heads FROM flow_runs WHERE repo_id = ? AND bookmark = ? ORDER BY id DESC LIMIT 1" )
1024
.bind ( consumer.id)
1025
.bind ( & bm)
1026
.fetch_optional ( & app.db)
1027
.await
1028
.ok ( )
1029
.flatten ( ) ;
1030
if ! run.is_some_and ( | ( status , heads ) | status = = " passed" & & same ( & heads, & now) ) {
1031
return Err ( Held:: NotPassed) ;
1032
}
1033
let me = account ( & app.db) .await.ok_or_else ( | | Held:: Refused( " the diffusion account is missing" .into ( ) ) ) ? ;
1034
let via = " Diffusion, both halves green" ;
1035
match crate :: web:: proposals:: land_as( app, & cname, consumer, & me, bump.number, via, true ) .await {
1036
Ok ( _ ) => {
1037
tracing:: info! ( " diffusion: {}/{} landed #{} itself (both halves green)" , consumer.owner, consumer.name, bump.number) ;
1038
Ok ( bump.number)
1039
}
1040
Err ( crate :: web:: proposals:: Landing:: NotFound) => Err ( Held:: NoBump) ,
1041
Err ( crate :: web:: proposals:: Landing:: Refused( why) ) => {
1042
1043
let said = db:: proposals:: events( & app.db, bump.id)
1044
.await
1045
.unwrap_or_default ( )
1046
.iter ( )
1047
.any ( | e | e.event = = " not landed by itself" & & e.detail = = why ) ;
1048
if ! said {
1049
db:: proposals:: add_event( & app.db, bump.id, ACCOUNT , " not landed by itself" , & why) .await.ok ( ) ;
1050
}
1051
Err ( Held:: Refused( why) )
1052
}
1053
}
1054
}
1055
1056
1057
1058
pub fn try_land ( app : Shared, consumer : RepoRow, path : String) -> std:: pin:: Pin< Box< dyn std:: future:: Future< Output = ( ) > + Send> > {
1059
Box :: pin( async move {
1060
match auto_land ( & app, & consumer, & path) .await {
1061
Ok ( _ ) | Err ( Held:: NoBump | Held:: Manual) => { }
1062
Err ( why) => tracing:: info! ( " diffusion: {}/{} {path} waits for a person: {why:?}" , consumer.owner, consumer.name) ,
1063
}
1064
} )
1065
}
1066
1067
1068
pub async fn land_ready ( app : & Shared, consumer : & RepoRow) {
1069
let paths: Vec< String> = sqlx:: query_scalar( " SELECT DISTINCT path FROM diffusion_pins WHERE consumer_id = ?" )
1070
.bind ( consumer.id)
1071
.fetch_all ( & app.db)
1072
.await
1073
.unwrap_or_default ( ) ;
1074
for path in paths {
1075
tokio:: spawn( try_land ( app.clone ( ) , consumer.clone ( ) , path) ) ;
1076
}
1077
}
1078
9aaa88b2 Andy McCall 22 days ago ↶
1079
1080
1081
1082
pub async fn unverified ( app : & Shared, upstream : & RepoRow) -> bool {
1083
if ! crate :: flow:: enabled( app, upstream) {
1084
return true ;
1085
}
1086
let runs: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM flow_runs WHERE repo_id = ? AND bookmark = ?" )
1087
.bind ( upstream.id)
1088
.bind ( & upstream.default_bookmark)
1089
.fetch_one ( & app.db)
1090
.await
1091
.unwrap_or ( 0 ) ;
1092
runs = = 0
3776b70d Andy McCall 23 days ago ↶
1093
}
1094
1095
1096
async fn repo_by_oxy_id ( app : & Shared, id : & str ) -> Option< RepoRow> {
1097
for row in db:: repos:: list( & app.db) .await.ok ( ) ? {
1098
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { continue } ;
1099
if app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await.ok ( ) .map ( hex:: encode) .as_deref ( ) = = Some ( id) {
1100
return Some ( row) ;
1101
}
1102
}
1103
None
1104
}
1105
1106
1107
async fn last_green ( app : & Shared, upstream : & RepoRow) -> Option< Vec< ChangeHash> > {
1108
let heads: String =
1109
sqlx:: query_scalar( " SELECT heads FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND status = 'passed' ORDER BY id DESC LIMIT 1" )
1110
.bind ( upstream.id)
1111
.bind ( & upstream.default_bookmark)
1112
.fetch_optional ( & app.db)
1113
.await
1114
.ok ( )
1115
.flatten ( ) ? ;
1116
let h: Vec< ChangeHash> = heads.split ( ' +' ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
1117
( ! h.is_empty ( ) ) .then_some ( h)
1118
}
1119
1120
1121
1122
1123
pub async fn catch_up ( app : & Shared, consumer : & RepoRow) {
1124
let upstreams: Vec< String> = sqlx:: query_scalar( " SELECT DISTINCT upstream FROM diffusion_pins WHERE consumer_id = ?" )
1125
.bind ( consumer.id)
1126
.fetch_all ( & app.db)
1127
.await
1128
.unwrap_or_default ( ) ;
1129
for id in upstreams {
1130
let Some ( upstream) = repo_by_oxy_id ( app, & id) .await else { continue } ;
1131
if let Some ( h) = last_green ( app, & upstream) .await {
1132
propagate_to ( app, & upstream, h, true , Some ( consumer.id) ) .await;
1133
}
1134
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { continue } ;
1135
let b = upstream.default_bookmark.clone ( ) ;
1136
if let Ok ( h) = app.repos.read ( & name, move | r, _ | heads_of ( r, & b) ) .await {
1137
propagate_to ( app, & upstream, h, false , Some ( consumer.id) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
1138
}
1139
}
1140
}
1141
e7158350 Andy McCall 23 days ago ↶
1142
1143
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize ) ]
1144
# [ serde ( rename_all = " lowercase" ) ]
1145
pub enum Follow {
1146
Off,
1147
1148
Green,
1149
1150
Any,
1151
}
1152
1153
impl Follow {
1154
pub fn parse ( s : & str ) -> Option< Follow> {
1155
match s.trim ( ) {
1156
" off" => Some ( Follow:: Off) ,
1157
" green" => Some ( Follow:: Green) ,
1158
" any" => Some ( Follow:: Any) ,
1159
_ => None ,
eb2a3502 Andy McCall 22 days ago ↶
1160
}
1161
}
1162
1163
pub fn as_str ( self ) -> & 'static str {
1164
match self {
1165
Follow:: Off => " off" ,
1166
Follow:: Green => " green" ,
1167
Follow:: Any => " any" ,
e7158350 Andy McCall 23 days ago ↶
1168
}
1169
}
1170
}
1171
1172
1173
1174
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize ) ]
1175
pub struct Rule {
1176
pub follow : Follow,
f4d6ef41 Andy McCall 22 days ago ↶
1177
1178
e7158350 Andy McCall 23 days ago ↶
1179
pub land_auto : bool ,
1180
1181
pub target : Option< String> ,
1182
1183
1184
pub watch : Vec< String> ,
1185
1186
pub from : & 'static str ,
1187
}
1188
1189
1190
1191
pub fn module_keys ( text : & str , path : & str ) -> ( Option< Follow> , Option< bool > , Option< String> , Vec< String> ) {
1192
let ( mut follow, mut land, mut target, mut watch) = ( None , None , None , Vec :: new( ) ) ;
1193
let mut here = false ;
5360cea3 Andy McCall 20 days ago ↶
1194
let mut lines = text.lines ( ) ;
1195
while let Some ( line) = lines.next ( ) {
e7158350 Andy McCall 23 days ago ↶
1196
let line = line.trim ( ) ;
1197
if line.is_empty ( ) | | line.starts_with ( ' #' ) {
1198
continue ;
1199
}
1200
if let Some ( inner) = line.strip_prefix ( ' [' ) .and_then ( | l | l.strip_suffix ( ' ]' ) ) {
1201
here = inner.trim ( ) .trim_matches ( ' "' ) .trim_end_matches ( ' /' ) = = path.trim_end_matches ( ' /' ) ;
1202
continue ;
1203
}
1204
let ( true , Some ( ( k, v) ) ) = ( here, line.split_once ( ' =' ) ) else { continue } ;
5360cea3 Andy McCall 20 days ago ↶
1205
1206
1207
let mut v = v.trim ( ) .to_string ( ) ;
1208
if v.starts_with ( ' [' ) & & ! v.contains ( ' ]' ) {
1209
for more in lines.by_ref ( ) {
1210
let more = more.trim ( ) ;
1211
if more.starts_with ( ' #' ) {
1212
continue ;
1213
}
1214
v.push_str ( more) ;
1215
if more.contains ( ' ]' ) {
1216
break ;
1217
}
1218
}
1219
}
1220
let v = v.as_str ( ) ;
e7158350 Andy McCall 23 days ago ↶
1221
let unquoted = v.trim_matches ( ' "' ) .trim ( ) ;
1222
match k.trim ( ) {
1223
" follow" => follow = Follow:: parse( unquoted) ,
1224
" land" => land = Some ( unquoted = = " auto" ) ,
1225
" target" if ! unquoted.is_empty ( ) => target = Some ( unquoted.to_string ( ) ) ,
1226
" watch" => {
1227
watch = v
1228
.trim_start_matches ( ' [' )
1229
.trim_end_matches ( ' ]' )
1230
.split ( ' ,' )
1231
.map ( | w | w.trim ( ) .trim_matches ( ' "' ) .trim ( ) .to_string ( ) )
1232
.filter ( | w | ! w.is_empty ( ) )
1233
.collect ( )
1234
}
1235
_ => { }
1236
}
1237
}
1238
( follow, land, target, watch)
1239
}
1240
1241
1242
pub fn rule ( site_default : & str , row : & RepoRow, oxymodules : Option< & str > , path : & str ) -> Rule {
1243
let ( follow, land, target, watch) = oxymodules.map ( | t | module_keys ( t, path) ) .unwrap_or_default ( ) ;
1244
let ( follow, from) = match follow {
1245
Some ( f) => ( f, " .oxymodules" ) ,
1246
None => match Follow:: parse( & row.diffusion) {
1247
Some ( f) => ( f, " setting" ) ,
1248
None => ( Follow:: parse( site_default) .unwrap_or ( Follow:: Off) , " site" ) ,
1249
} ,
1250
} ;
f4d6ef41 Andy McCall 22 days ago ↶
1251
1252
let land_auto = land.unwrap_or ( row.diffusion_land = = " auto" ) ;
1253
Rule { follow, land_auto, target, watch, from }
e7158350 Andy McCall 23 days ago ↶
1254
}
1255
1256
# [ cfg ( test ) ]
1257
mod tests {
1258
use super :: * ;
1259
1260
const MODULES : & str = r # "
1261
[rstar-api]
1262
url = https://oxygenhub.io/doomsdayone/rstar-api
1263
follow = green
1264
land = "propose"
1265
target = develop
1266
watch = ["src/x16/irq*", "src/x16/frame*"]
1267
1268
[other]
1269
url = https://example/other
1270
follow = any
1271
"# ;
1272
1273
fn row ( diffusion : & str ) -> RepoRow {
1274
RepoRow {
1275
id: 1 ,
1276
owner: " o" .into ( ) ,
1277
name: " n" .into ( ) ,
1278
description: String :: new( ) ,
1279
default_bookmark: " main" .into ( ) ,
1280
visibility: " public" .into ( ) ,
1281
created_ms: 0 ,
1282
updated_ms: 0 ,
1283
git_mirror: false ,
1284
git_mirror_state: String :: new( ) ,
1285
forked_from: None ,
1286
topics: String :: new( ) ,
1287
flow: String :: new( ) ,
1288
diffusion: diffusion.into ( ) ,
1289
.. Default :: default( )
1290
}
f4d6ef41 Andy McCall 22 days ago ↶
1291
}
1292
1293
# [ test ]
1294
fn landing_by_itself_is_chosen_per_repository_and_per_entry ( ) {
1295
let mut r = row ( " green" ) ;
1296
assert! ( ! rule ( " off" , & r, None , " rstar-api" ) .land_auto, " never by default" ) ;
1297
r.diffusion_land = " auto" .into ( ) ;
1298
assert! ( rule ( " off" , & r, None , " rstar-api" ) .land_auto, " the setting" ) ;
1299
assert! ( ! rule ( " off" , & r, Some ( MODULES ) , " rstar-api" ) .land_auto, " the entry's `land = propose` wins" ) ;
1300
assert! ( rule ( " off" , & r, Some ( MODULES ) , " other" ) .land_auto, " an entry without `land` takes the setting" ) ;
1301
r.diffusion_land = String :: new( ) ;
1302
let auto = MODULES .replace ( r # "land = "propose""# , " land = auto" ) ;
1303
assert! ( rule ( " off" , & r, Some ( & auto) , " rstar-api" ) .land_auto) ;
c4e432da Andy McCall 22 days ago ↶
1304
}
1305
1306
# [ test ]
1307
fn watch_patterns_match_paths ( ) {
1308
assert! ( watched ( " src/x16/irq*" , " src/x16/irq.s" ) ) ;
1309
assert! ( watched ( " src/x16/irq*" , " src/x16/irq_wait.c" ) ) ;
1310
assert! ( ! watched ( " src/x16/irq*" , " src/x16/sub/irq.s" ) , " one segment" ) ;
1311
assert! ( watched ( " src/**/frame*.c" , " src/x16/floor/frame_wait.c" ) ) ;
1312
assert! ( watched ( " src/x16" , " src/x16/bank.s" ) , " a directory" ) ;
1313
assert! ( ! watched ( " src/x16" , " src/x160/bank.s" ) ) ;
1314
assert! ( watched ( " include/r?m.h" , " include/rim.h" ) ) ;
1315
assert! ( ! watched ( " " , " anything" ) ) ;
e7158350 Andy McCall 23 days ago ↶
1316
}
1317
1318
# [ test ]
1319
fn oxymodules_keys_are_read_per_entry ( ) {
1320
let ( f, land, target, watch) = module_keys ( MODULES , " rstar-api" ) ;
1321
assert_eq! ( ( f, land, target.as_deref ( ) ) , ( Some ( Follow:: Green) , Some ( false ) , Some ( " develop" ) ) ) ;
1322
assert_eq! ( watch, vec! [ " src/x16/irq*" , " src/x16/frame*" ] ) ;
1323
assert_eq! ( module_keys ( MODULES , " other" ) .0 , Some ( Follow:: Any) ) ;
1324
assert_eq! ( module_keys ( MODULES , " missing" ) , ( None , None , None , vec! [ ] ) ) ;
1325
assert_eq! ( module_keys ( " [a]\n watch = x, y\n " , " a" ) .3 , vec! [ " x" , " y" ] ) ;
5360cea3 Andy McCall 20 days ago ↶
1326
1327
1328
let multi = " [rstar-api]\n url = https://hub/x\n \n watch = [\" src/x16/rbm*\" ,\n \" src/x16/rdm.c\" ,\n # a note\n \" src/mega65/rsm.c\" ]\n follow = \" green\" \n " ;
1329
let ( f, _ , _ , watch) = module_keys ( multi, " rstar-api" ) ;
1330
assert_eq! ( watch, vec! [ " src/x16/rbm*" , " src/x16/rdm.c" , " src/mega65/rsm.c" ] ) ;
1331
assert_eq! ( f, Some ( Follow:: Green) , " the key after the list is still read" ) ;
e7158350 Andy McCall 23 days ago ↶
1332
}
1333
1334
# [ test ]
1335
fn oxymodules_wins_then_the_setting_then_the_site ( ) {
1336
let r = rule ( " off" , & row ( " any" ) , Some ( MODULES ) , " rstar-api" ) ;
1337
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " .oxymodules" ) ) ;
1338
let r = rule ( " off" , & row ( " any" ) , Some ( " [rstar-api]\n url = x\n " ) , " rstar-api" ) ;
1339
assert_eq! ( ( r.follow, r.from) , ( Follow:: Any, " setting" ) ) ;
1340
let r = rule ( " green" , & row ( " " ) , None , " rstar-api" ) ;
1341
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " site" ) ) ;
1342
assert_eq! ( rule ( " nonsense" , & row ( " " ) , None , " x" ) .follow, Follow:: Off) ;
1343
}
1344
}