f677d627 Andy McCall 24 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 23 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 24 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 23 days ago ↶
66
for ( ( path, id, pinned) , upstream_row) in pins.iter ( ) .zip ( & upstreams) {
f677d627 Andy McCall 24 days ago ↶
67
let heads: Vec< String> = pinned.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ;
757195ad Andy McCall 23 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 24 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 23 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 24 days ago ↶
109
}
110
757195ad Andy McCall 23 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 24 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 23 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 23 days ago ↶
161
162
pub upstream : Half,
163
pub downstream : Half,
757195ad Andy McCall 23 days ago ↶
164
}
165
eb2a3502 Andy McCall 23 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 23 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 23 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 24 days ago ↶
242
}
243
757195ad Andy McCall 23 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 24 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 23 days ago ↶
297
let unverified = unverified ( app, row) .await;
f677d627 Andy McCall 24 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 23 days ago ↶
301
continue ;
302
}
303
304
if bookmark ! = target ( & app.db, & c) .await {
f677d627 Andy McCall 24 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 23 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 23 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 23 days ago ↶
341
} ) ;
f677d627 Andy McCall 24 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 23 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 24 days ago ↶
367
for ( c, b) in out.iter_mut ( ) .zip ( behind) {
368
c.behind = b;
73ef9f76 Andy McCall 23 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 24 days ago ↶
373
}
374
Ok ( out)
757195ad Andy McCall 23 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 24 days ago ↶
415
}
e7158350 Andy McCall 24 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> ) ,
c4e432da Andy McCall 23 days ago ↶
692
( why , watch ) : ( Why, Vec< String> ) ,
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! (
c4e432da Andy McCall 23 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). It never lands by itself." ,
1b6536f0 Andy McCall 23 days ago ↶
726
short.join ( " +" ) ,
a7e6e8e6 Andy McCall 23 days ago ↶
727
lines.join ( " \n " )
728
) ;
729
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 ↶
730
let ( number, id) = match open {
a7e6e8e6 Andy McCall 23 days ago ↶
731
Some ( p) => {
9aaa88b2 Andy McCall 23 days ago ↶
732
if p.title ! = title | | p.body ! = body {
733
pdb:: edit( & app.db, p.id, & title, & body, target) .await.ok ( ) ? ;
734
pdb:: add_event( & app.db, p.id, ACCOUNT , " updated" , & short.join ( " +" ) ) .await.ok ( ) ;
735
}
736
( p.number, p.id)
a7e6e8e6 Andy McCall 23 days ago ↶
737
}
738
None => {
739
let p = pdb:: create(
740
& app.db,
741
pdb:: NewProposal {
742
repo_id: consumer.id,
743
title: & title,
744
body: & body,
745
author_id: Some ( me.id) ,
746
author: ACCOUNT ,
747
draft: false ,
748
target,
749
source_kind: " bookmark" ,
750
source_repo_id: None ,
751
source_bookmark: bookmark,
752
source_changes: " " ,
753
} ,
754
)
755
.await
756
.ok ( ) ? ;
757
pdb:: add_event( & app.db, p.id, ACCOUNT , " opened" , " " ) .await.ok ( ) ;
758
db:: notify:: index( & app.db, " proposal" , consumer.id, p.id, p.number, & p.title, & p.body) .await.ok ( ) ;
759
if let Ok ( label) =
760
db:: issues:: upsert_label( & app.db, consumer.id, " dependencies" , " #0072ff" , " A subrepo moves (Diffusion)" ) .await
761
{
762
db:: issues:: set_proposal_label( & app.db, p.id, label.id, true ) .await.ok ( ) ;
763
}
9aaa88b2 Andy McCall 23 days ago ↶
764
( p.number, p.id)
a7e6e8e6 Andy McCall 23 days ago ↶
765
}
766
} ;
9aaa88b2 Andy McCall 23 days ago ↶
767
768
let unverified = db:: issues:: label_by_name( & app.db, consumer.id, " upstream unverified" ) .await.ok ( ) .flatten ( ) ;
769
match ( why = = Why:: Unverified, unverified) {
770
( true , l) => {
771
let l = match l {
772
Some ( l) => Some ( l) ,
773
None => db:: issues:: upsert_label(
774
& app.db,
775
consumer.id,
776
" upstream unverified" ,
777
" #e3a008" ,
778
" The upstream has no checks (Diffusion)" ,
779
)
780
.await
781
.ok ( ) ,
782
} ;
783
if let Some ( l) = l {
784
db:: issues:: set_proposal_label( & app.db, id, l.id, true ) .await.ok ( ) ;
785
}
786
}
787
( false , Some ( l) ) => {
788
db:: issues:: set_proposal_label( & app.db, id, l.id, false ) .await.ok ( ) ;
789
}
790
( false , None ) => { }
791
}
a7e6e8e6 Andy McCall 23 days ago ↶
792
Some ( number)
793
}
794
f01cb865 Andy McCall 23 days ago ↶
795
796
797
798
fn start_runs (
799
app : Shared,
800
row : RepoRow,
801
bookmark : String,
802
state : String,
803
heads : Vec< ChangeHash> ,
804
) -> std:: pin:: Pin< Box< dyn std:: future:: Future< Output = ( ) > + Send> > {
805
Box :: pin( async move { crate :: flow:: on_push( & app, & row, & bookmark, & state, & heads, ACCOUNT ) .await } )
806
}
807
a7e6e8e6 Andy McCall 23 days ago ↶
808
809
async fn oxymodules ( app : & Shared, name : & RepoName, bookmark : & str ) -> Option< String> {
810
let b = bookmark.to_string ( ) ;
811
app.repos
812
.read ( name, move | r, cache| {
813
let heads = heads_of ( r, & b) ? ;
814
if heads.is_empty ( ) {
815
return Ok ( None ) ;
816
}
817
let loaded = cache.load ( r, & heads) ? ;
818
Ok ( match loaded.snapshot.files.get ( " .oxymodules" ) .map ( | f | & f.content) {
819
Some ( oxy_core:: state:: FileContent:: Text { bytes, .. } ) => Some ( String :: from_utf8_lossy( bytes) .into_owned ( ) ) ,
820
_ => None ,
821
} )
822
} )
823
.await
824
.ok ( )
825
.flatten ( )
826
}
827
828
829
830
831
pub async fn propagate ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool ) {
3776b70d Andy McCall 23 days ago ↶
832
propagate_to ( app, upstream, heads, green, None ) .await
833
}
834
835
836
async fn propagate_to ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool , only : Option< i64 > ) {
a7e6e8e6 Andy McCall 23 days ago ↶
837
let Some ( uname) = RepoName:: new( & upstream.owner, & upstream.name) else { return } ;
838
let Ok ( id) = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await else { return } ;
839
let rows: Vec< ( i64 , String, String) > = sqlx:: query_as( " SELECT consumer_id, bookmark, path FROM diffusion_pins WHERE upstream = ?" )
840
.bind ( hex:: encode( id) )
841
.fetch_all ( & app.db)
842
.await
843
.unwrap_or_default ( ) ;
9aaa88b2 Andy McCall 23 days ago ↶
844
let unverified = unverified ( app, upstream) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
845
for ( consumer_id, indexed, path) in rows {
3776b70d Andy McCall 23 days ago ↶
846
if only.is_some_and ( | o | o ! = consumer_id ) {
847
continue ;
848
}
a7e6e8e6 Andy McCall 23 days ago ↶
849
let Ok ( Some ( consumer) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
850
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { continue } ;
851
let learned = target ( & app.db, & consumer) .await;
852
let modules = oxymodules ( app, & cname, & learned) .await;
853
let r = rule ( & app.config.diffusion.follow, & consumer, modules.as_deref ( ) , & path) ;
854
let target = r.target.clone ( ) .unwrap_or ( learned) ;
855
if indexed ! = target {
856
continue ;
857
}
9aaa88b2 Andy McCall 23 days ago ↶
858
859
a7e6e8e6 Andy McCall 23 days ago ↶
860
let go = match r.follow {
861
Follow:: Off => false ,
9aaa88b2 Andy McCall 23 days ago ↶
862
Follow:: Green => green | | unverified,
a7e6e8e6 Andy McCall 23 days ago ↶
863
Follow:: Any => ! green,
864
} ;
9aaa88b2 Andy McCall 23 days ago ↶
865
let why = if green {
866
Why:: Green
867
} else if unverified {
868
Why:: Unverified
869
} else {
870
Why:: Moved
871
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
872
if ! go {
873
continue ;
874
}
875
match bump ( app, & consumer, & target, & path, upstream, heads.clone ( ) ) .await {
f01cb865 Andy McCall 23 days ago ↶
876
Ok ( Bumped:: Moved { from, bookmark, heads: bumped, state, .. } ) => {
877
878
tokio:: spawn( start_runs ( app.clone ( ) , consumer.clone ( ) , bookmark, state, bumped) ) ;
c4e432da Andy McCall 23 days ago ↶
879
let n = propose ( app, & consumer, & target, & path, upstream, ( from, heads.clone ( ) ) , ( why, r.watch.clone ( ) ) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
880
tracing:: info! (
881
" diffusion: {}/{} {path} bumped to {}/{} (proposal {n:?})" ,
882
consumer.owner,
883
consumer.name,
884
upstream.owner,
885
upstream.name
886
) ;
9aaa88b2 Andy McCall 23 days ago ↶
887
}
888
Ok ( Bumped:: Already { from, to } ) => {
889
890
c4e432da Andy McCall 23 days ago ↶
891
propose ( app, & consumer, & target, & path, upstream, ( from, to) , ( why, r.watch.clone ( ) ) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
892
}
7a14bdae Andy McCall 23 days ago ↶
893
Ok ( other) => tracing:: info! ( " diffusion: {}/{} {path}: no bump ({other:?})" , consumer.owner, consumer.name) ,
a7e6e8e6 Andy McCall 23 days ago ↶
894
Err ( e) => tracing:: warn! ( " diffusion: bump {}/{} {path}: {e}" , consumer.owner, consumer.name) ,
3776b70d Andy McCall 23 days ago ↶
895
}
896
}
9aaa88b2 Andy McCall 23 days ago ↶
897
}
898
899
900
901
902
pub async fn unverified ( app : & Shared, upstream : & RepoRow) -> bool {
903
if ! crate :: flow:: enabled( app, upstream) {
904
return true ;
905
}
906
let runs: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM flow_runs WHERE repo_id = ? AND bookmark = ?" )
907
.bind ( upstream.id)
908
.bind ( & upstream.default_bookmark)
909
.fetch_one ( & app.db)
910
.await
911
.unwrap_or ( 0 ) ;
912
runs = = 0
3776b70d Andy McCall 23 days ago ↶
913
}
914
915
916
async fn repo_by_oxy_id ( app : & Shared, id : & str ) -> Option< RepoRow> {
917
for row in db:: repos:: list( & app.db) .await.ok ( ) ? {
918
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { continue } ;
919
if app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await.ok ( ) .map ( hex:: encode) .as_deref ( ) = = Some ( id) {
920
return Some ( row) ;
921
}
922
}
923
None
924
}
925
926
927
async fn last_green ( app : & Shared, upstream : & RepoRow) -> Option< Vec< ChangeHash> > {
928
let heads: String =
929
sqlx:: query_scalar( " SELECT heads FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND status = 'passed' ORDER BY id DESC LIMIT 1" )
930
.bind ( upstream.id)
931
.bind ( & upstream.default_bookmark)
932
.fetch_optional ( & app.db)
933
.await
934
.ok ( )
935
.flatten ( ) ? ;
936
let h: Vec< ChangeHash> = heads.split ( ' +' ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
937
( ! h.is_empty ( ) ) .then_some ( h)
938
}
939
940
941
942
943
pub async fn catch_up ( app : & Shared, consumer : & RepoRow) {
944
let upstreams: Vec< String> = sqlx:: query_scalar( " SELECT DISTINCT upstream FROM diffusion_pins WHERE consumer_id = ?" )
945
.bind ( consumer.id)
946
.fetch_all ( & app.db)
947
.await
948
.unwrap_or_default ( ) ;
949
for id in upstreams {
950
let Some ( upstream) = repo_by_oxy_id ( app, & id) .await else { continue } ;
951
if let Some ( h) = last_green ( app, & upstream) .await {
952
propagate_to ( app, & upstream, h, true , Some ( consumer.id) ) .await;
953
}
954
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { continue } ;
955
let b = upstream.default_bookmark.clone ( ) ;
956
if let Ok ( h) = app.repos.read ( & name, move | r, _ | heads_of ( r, & b) ) .await {
957
propagate_to ( app, & upstream, h, false , Some ( consumer.id) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
958
}
959
}
960
}
961
e7158350 Andy McCall 24 days ago ↶
962
963
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize ) ]
964
# [ serde ( rename_all = " lowercase" ) ]
965
pub enum Follow {
966
Off,
967
968
Green,
969
970
Any,
971
}
972
973
impl Follow {
974
pub fn parse ( s : & str ) -> Option< Follow> {
975
match s.trim ( ) {
976
" off" => Some ( Follow:: Off) ,
977
" green" => Some ( Follow:: Green) ,
978
" any" => Some ( Follow:: Any) ,
979
_ => None ,
eb2a3502 Andy McCall 23 days ago ↶
980
}
981
}
982
983
pub fn as_str ( self ) -> & 'static str {
984
match self {
985
Follow:: Off => " off" ,
986
Follow:: Green => " green" ,
987
Follow:: Any => " any" ,
e7158350 Andy McCall 24 days ago ↶
988
}
989
}
990
}
991
992
993
994
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize ) ]
995
pub struct Rule {
996
pub follow : Follow,
997
998
pub land_auto : bool ,
999
1000
pub target : Option< String> ,
1001
1002
1003
pub watch : Vec< String> ,
1004
1005
pub from : & 'static str ,
1006
}
1007
1008
1009
1010
pub fn module_keys ( text : & str , path : & str ) -> ( Option< Follow> , Option< bool > , Option< String> , Vec< String> ) {
1011
let ( mut follow, mut land, mut target, mut watch) = ( None , None , None , Vec :: new( ) ) ;
1012
let mut here = false ;
1013
for line in text.lines ( ) {
1014
let line = line.trim ( ) ;
1015
if line.is_empty ( ) | | line.starts_with ( ' #' ) {
1016
continue ;
1017
}
1018
if let Some ( inner) = line.strip_prefix ( ' [' ) .and_then ( | l | l.strip_suffix ( ' ]' ) ) {
1019
here = inner.trim ( ) .trim_matches ( ' "' ) .trim_end_matches ( ' /' ) = = path.trim_end_matches ( ' /' ) ;
1020
continue ;
1021
}
1022
let ( true , Some ( ( k, v) ) ) = ( here, line.split_once ( ' =' ) ) else { continue } ;
1023
let v = v.trim ( ) ;
1024
let unquoted = v.trim_matches ( ' "' ) .trim ( ) ;
1025
match k.trim ( ) {
1026
" follow" => follow = Follow:: parse( unquoted) ,
1027
" land" => land = Some ( unquoted = = " auto" ) ,
1028
" target" if ! unquoted.is_empty ( ) => target = Some ( unquoted.to_string ( ) ) ,
1029
" watch" => {
1030
watch = v
1031
.trim_start_matches ( ' [' )
1032
.trim_end_matches ( ' ]' )
1033
.split ( ' ,' )
1034
.map ( | w | w.trim ( ) .trim_matches ( ' "' ) .trim ( ) .to_string ( ) )
1035
.filter ( | w | ! w.is_empty ( ) )
1036
.collect ( )
1037
}
1038
_ => { }
1039
}
1040
}
1041
( follow, land, target, watch)
1042
}
1043
1044
1045
pub fn rule ( site_default : & str , row : & RepoRow, oxymodules : Option< & str > , path : & str ) -> Rule {
1046
let ( follow, land, target, watch) = oxymodules.map ( | t | module_keys ( t, path) ) .unwrap_or_default ( ) ;
1047
let ( follow, from) = match follow {
1048
Some ( f) => ( f, " .oxymodules" ) ,
1049
None => match Follow:: parse( & row.diffusion) {
1050
Some ( f) => ( f, " setting" ) ,
1051
None => ( Follow:: parse( site_default) .unwrap_or ( Follow:: Off) , " site" ) ,
1052
} ,
1053
} ;
1054
Rule { follow, land_auto: land.unwrap_or ( false ) , target, watch, from }
1055
}
1056
1057
# [ cfg ( test ) ]
1058
mod tests {
1059
use super :: * ;
1060
1061
const MODULES : & str = r # "
1062
[rstar-api]
1063
url = https://oxygenhub.io/doomsdayone/rstar-api
1064
follow = green
1065
land = "propose"
1066
target = develop
1067
watch = ["src/x16/irq*", "src/x16/frame*"]
1068
1069
[other]
1070
url = https://example/other
1071
follow = any
1072
"# ;
1073
1074
fn row ( diffusion : & str ) -> RepoRow {
1075
RepoRow {
1076
id: 1 ,
1077
owner: " o" .into ( ) ,
1078
name: " n" .into ( ) ,
1079
description: String :: new( ) ,
1080
default_bookmark: " main" .into ( ) ,
1081
visibility: " public" .into ( ) ,
1082
created_ms: 0 ,
1083
updated_ms: 0 ,
1084
git_mirror: false ,
1085
git_mirror_state: String :: new( ) ,
1086
forked_from: None ,
1087
topics: String :: new( ) ,
1088
flow: String :: new( ) ,
1089
diffusion: diffusion.into ( ) ,
1090
.. Default :: default( )
1091
}
c4e432da Andy McCall 23 days ago ↶
1092
}
1093
1094
# [ test ]
1095
fn watch_patterns_match_paths ( ) {
1096
assert! ( watched ( " src/x16/irq*" , " src/x16/irq.s" ) ) ;
1097
assert! ( watched ( " src/x16/irq*" , " src/x16/irq_wait.c" ) ) ;
1098
assert! ( ! watched ( " src/x16/irq*" , " src/x16/sub/irq.s" ) , " one segment" ) ;
1099
assert! ( watched ( " src/**/frame*.c" , " src/x16/floor/frame_wait.c" ) ) ;
1100
assert! ( watched ( " src/x16" , " src/x16/bank.s" ) , " a directory" ) ;
1101
assert! ( ! watched ( " src/x16" , " src/x160/bank.s" ) ) ;
1102
assert! ( watched ( " include/r?m.h" , " include/rim.h" ) ) ;
1103
assert! ( ! watched ( " " , " anything" ) ) ;
e7158350 Andy McCall 24 days ago ↶
1104
}
1105
1106
# [ test ]
1107
fn oxymodules_keys_are_read_per_entry ( ) {
1108
let ( f, land, target, watch) = module_keys ( MODULES , " rstar-api" ) ;
1109
assert_eq! ( ( f, land, target.as_deref ( ) ) , ( Some ( Follow:: Green) , Some ( false ) , Some ( " develop" ) ) ) ;
1110
assert_eq! ( watch, vec! [ " src/x16/irq*" , " src/x16/frame*" ] ) ;
1111
assert_eq! ( module_keys ( MODULES , " other" ) .0 , Some ( Follow:: Any) ) ;
1112
assert_eq! ( module_keys ( MODULES , " missing" ) , ( None , None , None , vec! [ ] ) ) ;
1113
assert_eq! ( module_keys ( " [a]\n watch = x, y\n " , " a" ) .3 , vec! [ " x" , " y" ] ) ;
1114
}
1115
1116
# [ test ]
1117
fn oxymodules_wins_then_the_setting_then_the_site ( ) {
1118
let r = rule ( " off" , & row ( " any" ) , Some ( MODULES ) , " rstar-api" ) ;
1119
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " .oxymodules" ) ) ;
1120
let r = rule ( " off" , & row ( " any" ) , Some ( " [rstar-api]\n url = x\n " ) , " rstar-api" ) ;
1121
assert_eq! ( ( r.follow, r.from) , ( Follow:: Any, " setting" ) ) ;
1122
let r = rule ( " green" , & row ( " " ) , None , " rstar-api" ) ;
1123
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " site" ) ) ;
1124
assert_eq! ( rule ( " nonsense" , & row ( " " ) , None , " x" ) .follow, Follow:: Off) ;
1125
}
1126
}