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
73ef9f76 Andy McCall 23 days ago ↶
166
167
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize ) ]
168
pub struct Half {
169
pub class : & 'static str ,
170
pub word : & 'static str ,
171
pub title : String,
172
}
173
174
impl Half {
175
176
pub fn upstream ( unverified : bool , run : Option< & str > , slug : & str ) -> Half {
177
let ( class, word, title) = match ( unverified, run) {
178
( true , _ ) => ( " df-amber" , " untested" , format! ( " {slug} has no checks of its own: nothing has tested it" ) ) ,
179
( false , Some ( " passed" ) ) => ( " df-green" , " checked" , format! ( " {slug} 's newest run passed" ) ) ,
180
( false , Some ( " failed" | " error" | " cancelled" ) ) => ( " df-red" , " failing" , format! ( " {slug} 's newest run failed" ) ) ,
181
( false , _ ) => ( " df-grey" , " testing" , format! ( " {slug} 's newest run has not finished" ) ) ,
182
} ;
183
Half { class, word, title }
184
}
185
186
187
pub fn downstream ( follow : Follow, behind : Option< usize > , bump : Option< i64 > , run : Option< & str > , name : & str ) -> Half {
188
let ( class, word, title) = if follow = = Follow:: Off {
189
( " df-off" , " not following" , format! ( " {name} does not follow it" ) )
190
} else {
191
match ( bump, run) {
192
( Some ( _ ) , Some ( " passed" ) ) => ( " df-green" , " passes" , format! ( " {name} 's run on the bump passed" ) ) ,
193
( Some ( _ ) , Some ( " failed" | " error" | " cancelled" ) ) => ( " df-red" , " fails" , format! ( " {name} 's run on the bump failed" ) ) ,
194
( Some ( _ ) , Some ( _ ) ) => ( " df-grey" , " testing" , format! ( " {name} 's run on the bump has not finished" ) ) ,
195
( Some ( _ ) , None ) => ( " df-grey" , " waiting" , format! ( " {name} 's bump has no run yet" ) ) ,
196
( None , _ ) if behind = = Some ( 0 ) => ( " df-green" , " up to date" , format! ( " {name} pins the newest state" ) ) ,
197
( None , _ ) => ( " df-grey" , " waiting" , format! ( " {name} is behind; its bump has not been made yet" ) ) ,
198
}
199
} ;
200
Half { class, word, title }
201
}
202
}
203
757195ad Andy McCall 23 days ago ↶
204
205
206
207
208
209
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize ) ]
210
# [ serde ( rename_all = " kebab-case" ) ]
211
pub enum Colour {
212
Green,
213
Amber,
214
Red,
215
Grey,
216
UpToDate,
217
Off,
f677d627 Andy McCall 24 days ago ↶
218
}
219
757195ad Andy McCall 23 days ago ↶
220
impl Colour {
221
pub fn class ( & self ) -> & 'static str {
222
match self {
223
Colour:: Green => " df-green" ,
224
Colour:: Amber => " df-amber" ,
225
Colour:: Red => " df-red" ,
226
Colour:: Grey => " df-grey" ,
227
Colour:: UpToDate => " df-current" ,
228
Colour:: Off => " df-off" ,
229
}
230
}
231
232
pub fn label ( & self ) -> & 'static str {
233
match self {
234
Colour:: Green => " green" ,
235
Colour:: Amber => " amber: upstream unverified" ,
236
Colour:: Red => " red: its run failed" ,
237
Colour:: Grey => " testing" ,
238
Colour:: UpToDate => " up to date" ,
239
Colour:: Off => " not following" ,
240
}
241
}
242
243
244
pub fn of ( follow : Follow, behind : Option< usize > , bump : Option< i64 > , run : Option< & str > , unverified : bool ) -> Colour {
245
if follow = = Follow:: Off {
246
return Colour:: Off;
247
}
248
match ( bump, run) {
249
( Some ( _ ) , Some ( " passed" ) ) if unverified => Colour:: Amber,
250
( Some ( _ ) , Some ( " passed" ) ) => Colour:: Green,
251
( Some ( _ ) , Some ( " failed" | " error" | " cancelled" ) ) => Colour:: Red,
252
( Some ( _ ) , _ ) => Colour:: Grey,
253
( None , _ ) if behind = = Some ( 0 ) => Colour:: UpToDate,
254
( None , _ ) => Colour:: Grey,
255
}
256
}
257
}
258
f677d627 Andy McCall 24 days ago ↶
259
260
261
pub async fn consumers ( app : & Shared, row : & RepoRow, account : Option< & Account> ) -> Result< Vec< Consumer> , crate:: repos:: RepoError> {
262
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { return Ok ( Vec :: new( ) ) } ;
263
let id = app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await? ;
264
let found: Vec< ( i64 , String, String, String) > = sqlx:: query_as(
265
" SELECT consumer_id, bookmark, path, heads FROM diffusion_pins WHERE upstream = ? ORDER BY consumer_id, bookmark, path" ,
266
)
267
.bind ( hex:: encode( id) )
268
.fetch_all ( & app.db)
269
.await
270
.map_err ( | e | crate :: repos:: RepoError:: Io( std:: io:: Error:: other( e) ) ) ? ;
271
let mut out = Vec :: new( ) ;
272
let mut pinned_sets = Vec :: new( ) ;
757195ad Andy McCall 23 days ago ↶
273
let unverified = unverified ( app, row) .await;
f677d627 Andy McCall 24 days ago ↶
274
for ( consumer_id, bookmark, path, heads) in found {
275
let Ok ( Some ( c) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
276
if ! roles:: allows( roles:: role_for( & app.db, account, & c) .await.ok ( ) .flatten ( ) , crate :: auth:: Access:: Read) {
757195ad Andy McCall 23 days ago ↶
277
continue ;
278
}
279
280
if bookmark ! = target ( & app.db, & c) .await {
f677d627 Andy McCall 24 days ago ↶
281
continue ;
282
}
283
let pinned: Vec< String> = heads.split ( ' +' ) .filter ( | h | ! h.is_empty ( ) ) .map ( String :: from) .collect ( ) ;
284
pinned_sets.push ( pinned.iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect:: < Vec< _ > > ( ) ) ;
757195ad Andy McCall 23 days ago ↶
285
let bump_bm = bump_bookmark ( & path) ;
286
let mine: Vec< db:: proposals:: ProposalRow> = db:: proposals:: list( & app.db, c.id, & [ ] )
287
.await
288
.unwrap_or_default ( )
289
.into_iter ( )
290
.filter ( | p | p.source_bookmark = = bump_bm )
291
.collect ( ) ;
292
let bump = mine.iter ( ) .find ( | p | p.state = = " open" | | p.state = = " draft" ) .map ( | p | p.number ) ;
293
let landed = mine.iter ( ) .filter ( | p | p.state = = " landed" ) .max_by_key ( | p | p.landed_ms ) .map ( | p | ( p.number, p.landed_ms) ) ;
294
let run: Option< ( i64 , String) > =
295
sqlx:: query_as( " SELECT number, status FROM flow_runs WHERE repo_id = ? AND bookmark = ? ORDER BY id DESC LIMIT 1" )
296
.bind ( c.id)
297
.bind ( & bump_bm)
298
.fetch_optional ( & app.db)
299
.await
300
.ok ( )
301
.flatten ( ) ;
302
let follow = rule ( & app.config.diffusion.follow, & c, None , & path) .follow;
303
out.push ( Consumer {
304
owner: c.owner,
305
name: c.name,
306
bookmark,
307
path,
308
pinned,
309
behind: None ,
310
follow,
311
bump,
312
run,
313
landed,
314
colour: Colour:: Off,
73ef9f76 Andy McCall 23 days ago ↶
315
upstream: Half { class: " df-grey" , word: " " , title: String :: new( ) } ,
316
downstream: Half { class: " df-grey" , word: " " , title: String :: new( ) } ,
757195ad Andy McCall 23 days ago ↶
317
} ) ;
f677d627 Andy McCall 24 days ago ↶
318
}
319
if out.is_empty ( ) {
320
return Ok ( out) ;
321
}
322
let default = row.default_bookmark.clone ( ) ;
323
let behind = app
324
.repos
325
.read ( & name, move | r, cache| {
326
let tip = heads_of ( r, & default) ? ;
327
let tip = cache.summary ( r, & tip) ? ;
328
Ok ( pinned_sets
329
.iter ( )
330
.map ( | p | cache.summary ( r, p) .ok ( ) .map ( | s | tip.changes.iter ( ) .filter ( | c | ! s.changes.contains ( * c) ) .count ( ) ) )
331
.collect:: < Vec< _ > > ( ) )
332
} )
333
.await? ;
73ef9f76 Andy McCall 23 days ago ↶
334
let up_run: Option< String> =
335
sqlx:: query_scalar( " SELECT status FROM flow_runs WHERE repo_id = ? AND bookmark = ? ORDER BY id DESC LIMIT 1" )
336
.bind ( row.id)
337
.bind ( & row.default_bookmark)
338
.fetch_optional ( & app.db)
339
.await
340
.ok ( )
341
.flatten ( ) ;
342
let slug = format! ( " {} /{} " , row.owner, row.name) ;
f677d627 Andy McCall 24 days ago ↶
343
for ( c, b) in out.iter_mut ( ) .zip ( behind) {
344
c.behind = b;
73ef9f76 Andy McCall 23 days ago ↶
345
let run = c.run.as_ref ( ) .map ( | r | r.1. clone ( ) ) ;
346
c.colour = Colour:: of( c.follow, b, c.bump, run.as_deref ( ) , unverified) ;
347
c.upstream = Half:: upstream( unverified, up_run.as_deref ( ) , & slug) ;
348
c.downstream = Half:: downstream( c.follow, b, c.bump, run.as_deref ( ) , & format! ( " {} /{} " , c.owner, c.name) ) ;
f677d627 Andy McCall 24 days ago ↶
349
}
350
Ok ( out)
757195ad Andy McCall 23 days ago ↶
351
}
352
353
354
355
pub async fn follows ( app : & Shared, consumer : & RepoRow, account : Option< & Account> ) -> Vec< ( RepoRow, Consumer) > {
356
let ups: Vec< i64 > =
357
sqlx:: query_scalar( " SELECT DISTINCT upstream_repo_id FROM diffusion_pins WHERE consumer_id = ? AND upstream_repo_id IS NOT NULL" )
358
.bind ( consumer.id)
359
.fetch_all ( & app.db)
360
.await
361
.unwrap_or_default ( ) ;
362
let mut out = Vec :: new( ) ;
363
for id in ups {
364
let Ok ( Some ( up) ) = db:: repos:: get_by_id( & app.db, id) .await else { continue } ;
365
if ! roles:: allows( roles:: role_for( & app.db, account, & up) .await.ok ( ) .flatten ( ) , crate :: auth:: Access:: Read) {
366
continue ;
367
}
368
for c in consumers ( app, & up, account) .await.unwrap_or_default ( ) {
369
if c.owner = = consumer.owner & & c.name = = consumer.name {
370
out.push ( ( up.clone ( ) , c) ) ;
371
}
372
}
373
}
374
out
375
}
376
377
378
pub async fn everything ( app : & Shared, account : Option< & Account> ) -> Vec< ( RepoRow, Vec< Consumer> , bool ) > {
379
let ups: Vec< i64 > = sqlx:: query_scalar( " SELECT DISTINCT upstream_repo_id FROM diffusion_pins WHERE upstream_repo_id IS NOT NULL" )
380
.fetch_all ( & app.db)
381
.await
382
.unwrap_or_default ( ) ;
383
let mut out = Vec :: new( ) ;
384
for id in ups {
385
let Ok ( Some ( up) ) = db:: repos:: get_by_id( & app.db, id) .await else { continue } ;
386
let list = consumers ( app, & up, account) .await.unwrap_or_default ( ) ;
387
let unverified = unverified ( app, & up) .await;
388
out.push ( ( up, list, unverified) ) ;
389
}
390
out
f677d627 Andy McCall 24 days ago ↶
391
}
e7158350 Andy McCall 24 days ago ↶
392
1d634f69 Andy McCall 24 days ago ↶
393
394
pub fn bump_bookmark ( path : & str ) -> String {
395
format! ( " diffusion/{} " , path.trim_matches ( ' /' ) )
396
}
397
398
399
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
400
pub enum Bumped {
401
f01cb865 Andy McCall 24 days ago ↶
402
Moved { bookmark: String , change: ChangeHash, heads: Vec< ChangeHash> , from: Vec< ChangeHash> , state: String } ,
1d634f69 Andy McCall 24 days ago ↶
403
404
UpToDate,
405
9aaa88b2 Andy McCall 24 days ago ↶
406
Already { from: Vec< ChangeHash> , to: Vec< ChangeHash> } ,
7a14bdae Andy McCall 24 days ago ↶
407
408
Unknown,
409
410
411
Conflicts,
1d634f69 Andy McCall 24 days ago ↶
412
413
414
NoPin,
415
}
416
7a14bdae Andy McCall 24 days ago ↶
417
418
419
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
420
pub enum Plan {
421
422
To( Vec< ChangeHash> , usize ) ,
423
424
UpToDate,
425
426
427
Unknown,
428
429
Conflicts,
430
}
431
432
433
434
435
436
pub async fn plan ( app : & Shared, upstream : & RepoName, old : Vec< ChangeHash> , new : Vec< ChangeHash> ) -> Result< Plan, crate:: repos:: RepoError> {
1d634f69 Andy McCall 24 days ago ↶
437
app.repos
438
.read ( upstream, move | r, cache| {
7a14bdae Andy McCall 24 days ago ↶
439
let Ok ( o) = cache.summary ( r, & old) else { return Ok ( Plan:: Unknown) } ;
440
let n = cache.summary ( r, & new) ? ;
441
if o.changes.iter ( ) .all ( | c | n.changes.contains ( c) ) {
442
let added = n.changes.len ( ) - o.changes.len ( ) ;
443
return Ok ( if added = = 0 { Plan:: UpToDate } else { Plan:: To( new, added) } ) ;
444
}
445
let mut both = new.clone ( ) ;
446
both.extend ( old.iter ( ) .copied ( ) ) ;
447
let heads = r.load_state ( & both) ? .heads ( ) ;
448
let b = cache.summary ( r, & heads) ? ;
449
if b.conflicted & & ! o.conflicted {
450
return Ok ( Plan:: Conflicts) ;
1d634f69 Andy McCall 24 days ago ↶
451
}
7a14bdae Andy McCall 24 days ago ↶
452
let added = b.changes.len ( ) - o.changes.len ( ) ;
453
Ok ( if added = = 0 { Plan:: UpToDate } else { Plan:: To( heads, added) } )
1d634f69 Andy McCall 24 days ago ↶
454
} )
455
.await
456
}
457
458
459
460
461
pub async fn bump (
462
app : & Shared,
463
consumer : & RepoRow,
464
target : & str ,
465
path : & str ,
466
upstream : & RepoRow,
467
new_heads : Vec< ChangeHash> ,
468
) -> Result< Bumped, crate:: repos:: RepoError> {
469
use oxy_core:: identity:: Domain;
470
use oxy_core:: subrepo:: Pin;
471
let ( Some ( cname) , Some ( uname) ) = ( RepoName:: new( & consumer.owner, & consumer.name) , RepoName:: new( & upstream.owner, & upstream.name) )
472
else {
473
return Ok ( Bumped:: NoPin) ;
474
} ;
475
let upstream_id = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await? ;
476
477
478
let ( t, p) = ( target.to_string ( ) , path.trim_matches ( ' /' ) .to_string ( ) ) ;
479
let ( current, bumped) = app
480
.repos
481
.read ( & cname, move | r, cache| {
482
let pin_on = | bookmark : & str | -> Result< Option< Pin> , crate:: repos:: RepoError> {
483
let heads = heads_of ( r, bookmark) ? ;
484
if heads.is_empty ( ) {
485
return Ok ( None ) ;
486
}
487
let loaded = cache.load ( r, & heads) ? ;
488
let Some ( f) = loaded.snapshot.files.get ( & p) else { return Ok ( None ) } ;
489
let oxy_core:: state:: FileContent:: Opaque { blobs } = & f.content else { return Ok ( None ) } ;
490
let Some ( b) = blobs.first ( ) else { return Ok ( None ) } ;
491
Ok ( Pin:: decode( & r.read_blob ( b) ? ) .ok ( ) )
492
} ;
493
Ok ( ( pin_on ( & t) ? , pin_on ( & bump_bookmark ( & p) ) ? ) )
494
} )
495
.await? ;
496
let Some ( Pin:: Oxy { repo, heads: old, bookmark } ) = current else { return Ok ( Bumped:: NoPin) } ;
7a14bdae Andy McCall 24 days ago ↶
497
if repo ! = upstream_id {
498
return Ok ( Bumped:: NoPin) ;
499
}
500
let new_heads = match plan ( app, & uname, old.clone ( ) , new_heads.clone ( ) ) .await? {
501
Plan:: To( heads, _ ) => heads,
502
Plan:: UpToDate => return Ok ( Bumped:: UpToDate) ,
503
Plan:: Unknown => return Ok ( Bumped:: Unknown) ,
504
Plan:: Conflicts => return Ok ( Bumped:: Conflicts) ,
505
} ;
1d634f69 Andy McCall 24 days ago ↶
506
if let Some ( Pin:: Oxy { heads: b, .. } ) = & bumped {
507
let ( mut b, mut n) = ( b.clone ( ) , new_heads.clone ( ) ) ;
508
b.sort ( ) ;
509
n.sort ( ) ;
510
if b = = n {
9aaa88b2 Andy McCall 24 days ago ↶
511
return Ok ( Bumped:: Already { from: old, to: new_heads } ) ;
1d634f69 Andy McCall 24 days ago ↶
512
}
513
}
514
let ( key, secret) =
515
crate :: keys:: load_secret( & app.config.keys_dir ( ) ) .map_err ( | e | crate :: repos:: RepoError:: Io( std:: io:: Error:: other( e.to_string ( ) ) ) ) ? ;
516
let identity = app.hub_key.identity.clone ( ) ;
517
let upstream_label = format! ( " {} /{} " , upstream.owner, upstream.name) ;
518
let ( t, p) = ( target.to_string ( ) , path.trim_matches ( ' /' ) .to_string ( ) ) ;
519
let pin = Pin:: Oxy { repo, heads: new_heads.clone ( ) , bookmark } ;
a7e6e8e6 Andy McCall 24 days ago ↶
520
let old = old.clone ( ) ;
1d634f69 Andy McCall 24 days ago ↶
521
let short: Vec< String> = new_heads.iter ( ) .map ( | h | h.to_hex ( ) [ .. 12 ] .to_string ( ) ) .collect ( ) ;
522
let message = format! ( " Diffusion: move {p} to {upstream_label} {} " , short.join ( " +" ) ) ;
523
app.repos
524
.write ( & cname, move | r, _ | {
525
let base = heads_of ( r, & t) ? ;
526
let change = r.build_pin_change ( & base, & p, & pin, & message, key) ? ;
527
let hash = change.hash ( ) ;
528
let sig = secret.sign ( Domain:: Change, & key, & hash) ;
529
let mut state = r.load_state ( & base) ? ;
530
state.add ( change.clone ( ) ) .map_err ( | e | crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( e.to_string ( ) ) ) ) ? ;
531
let mut candidates = base.clone ( ) ;
532
candidates.push ( hash) ;
533
let heads = state.maximal ( & candidates) ;
534
let bookmark = bump_bookmark ( & p) ;
535
536
537
let mut update = r.host_ref_update ( & bookmark, & heads, Some ( ( & key, & secret) ) ) ? ;
538
update.force = true ;
f01cb865 Andy McCall 24 days ago ↶
539
let state = update.new_state.to_hex ( ) ;
1d634f69 Andy McCall 24 days ago ↶
540
let report = r.apply_hosted ( Some ( ( change, vec! [ sig] ) ) , & [ identity] , update) ? ;
541
if let Some ( refused) = report.results.iter ( ) .find ( | x | ! x.ok) {
542
return Err ( crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( format! (
543
" the bump was refused: {} " ,
544
refused.reason
545
) ) ) ) ;
546
}
f01cb865 Andy McCall 24 days ago ↶
547
Ok ( Bumped:: Moved { bookmark, change: hash, heads, from: old, state } )
1d634f69 Andy McCall 24 days ago ↶
548
} )
549
.await
550
}
551
a7e6e8e6 Andy McCall 24 days ago ↶
552
553
pub const ACCOUNT : & str = " diffusion" ;
554
555
556
pub async fn account ( pool : & Pool) -> Option< Account> {
557
if let Ok ( Some ( a) ) = db:: accounts:: account_by_name( pool, ACCOUNT ) .await {
558
return Some ( a) ;
559
}
560
db:: accounts:: create_account( pool, ACCOUNT , " diffusion@localhost" , " !" , " " , false ) .await.ok ( )
561
}
562
c4e432da Andy McCall 23 days ago ↶
563
564
565
566
567
pub fn watched ( pattern : & str , path : & str ) -> bool {
568
fn m ( p : & [u8 ], s : & [u8 ]) -> bool {
569
match ( p.first ( ) , s.first ( ) ) {
570
( None , None ) => true ,
571
( Some ( b ' *' ) , _ ) if p.get ( 1 ) = = Some ( & b ' *' ) => ( 0 .. = s.len ( ) ) .any ( | i | m ( & p[ 2 .. ] , & s[ i.. ] ) ) ,
572
( Some ( b ' *' ) , _ ) => ( 0 .. = s.len ( ) ) .take_while ( | & i | i = = 0 | | s[ i - 1 ] ! = b ' /' ) .any ( | i | m ( & p[ 1 .. ] , & s[ i.. ] ) ) ,
573
( Some ( b ' ?' ) , Some ( c) ) if * c ! = b ' /' => m ( & p[ 1 .. ] , & s[ 1 .. ] ) ,
574
( Some ( a) , Some ( b) ) if a = = b => m ( & p[ 1 .. ] , & s[ 1 .. ] ) ,
575
_ => false ,
576
}
577
}
578
let p = pattern.trim ( ) .trim_matches ( ' /' ) ;
579
if p.is_empty ( ) {
580
return false ;
581
}
582
m ( p.as_bytes ( ) , path.as_bytes ( ) ) | | ( ! p.contains ( [ ' *' , ' ?' ] ) & & path.starts_with ( & format! ( " {p} /" ) ) )
583
}
584
585
586
587
588
pub struct Changelog {
589
pub lines : Vec< String> ,
590
pub watched : Vec< String> ,
591
}
592
593
pub async fn changelog ( app : & Shared, upstream : & RepoRow, old : Vec< ChangeHash> , new : Vec< ChangeHash> , watch : Vec< String> ) -> Changelog {
594
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { return Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } } ;
a7e6e8e6 Andy McCall 24 days ago ↶
595
let linked: std:: collections:: HashMap< String, String> =
596
db:: accounts:: identity_accounts( & app.db) .await.unwrap_or_default ( ) .into_iter ( ) .collect ( ) ;
597
let base = format! ( " /{} /{} " , upstream.owner, upstream.name) ;
598
app.repos
599
.read ( & name, move | r, cache| {
600
let ( o, n) = ( cache.summary ( r, & old) ? , cache.summary ( r, & new) ? ) ;
c4e432da Andy McCall 23 days ago ↶
601
let mut added: Vec< ( u64 , ChangeHash, String, String, Vec< String> ) > = Vec :: new( ) ;
a7e6e8e6 Andy McCall 24 days ago ↶
602
for h in n.changes.iter ( ) .filter ( | c | ! o.changes.contains ( * c) ) {
603
let Ok ( c) = r.store ( ) .get_change ( h) else { continue } ;
604
let key = c.author.to_hex ( ) ;
605
let who = linked.get ( & key) .cloned ( ) .unwrap_or_else ( | | key[ .. 12 ] .to_string ( ) ) ;
606
let first = c.message.lines ( ) .next ( ) .unwrap_or ( " " ) .trim ( ) .to_string ( ) ;
c4e432da Andy McCall 23 days ago ↶
607
608
let hits: Vec< String> = if watch.is_empty ( ) {
609
Vec :: new( )
610
} else {
611
crate :: repos:: view:: change( r, cache, h)
612
.map ( | d | d.files.into_iter ( ) .map ( | f | f.path ) .filter ( | p | watch.iter ( ) .any ( | w | watched ( w, p) ) ) .collect ( ) )
613
.unwrap_or_default ( )
614
} ;
615
added.push ( ( c.hlc.wall_ms, * h, first, who, hits) ) ;
a7e6e8e6 Andy McCall 24 days ago ↶
616
}
617
added.sort_by_key ( | a | std:: cmp:: Reverse( a.0 ) ) ;
c4e432da Andy McCall 23 days ago ↶
618
let line = | h : & ChangeHash, msg : & str , who : & str , flag : bool | {
619
format! (
620
" - {} [`{} `]({base} /change/{} ) {} ({who} )" ,
621
if flag { " ⚑ " } else { " " } ,
622
& h.to_hex ( ) [ .. 12 ] ,
623
h.to_hex ( ) ,
624
msg.replace ( ' \n ' , " " )
625
)
626
} ;
627
let watched = added
628
.iter ( )
629
.filter ( | a | ! a.4. is_empty ( ) )
630
.map ( | ( _, h , msg , who , hits ) | {
631
format! (
632
" {} — touches {} " ,
633
line ( h, msg, who, true ) ,
634
hits.iter ( ) .map ( | p | format! ( " `{p} `" ) ) .collect:: < Vec< _ > > ( ) .join ( " , " )
635
)
a7e6e8e6 Andy McCall 24 days ago ↶
636
} )
c4e432da Andy McCall 23 days ago ↶
637
.collect ( ) ;
638
let lines = added.iter ( ) .map ( | ( _, h , msg , who , hits ) | line ( h, msg, who, ! hits.is_empty ( ) ) ) .collect ( ) ;
639
Ok ( Changelog { lines, watched } )
a7e6e8e6 Andy McCall 24 days ago ↶
640
} )
641
.await
c4e432da Andy McCall 23 days ago ↶
642
.unwrap_or ( Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } )
a7e6e8e6 Andy McCall 24 days ago ↶
643
}
644
9aaa88b2 Andy McCall 24 days ago ↶
645
646
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
647
pub enum Why {
648
649
Green,
650
651
Moved,
652
653
654
Unverified,
655
}
656
657
658
659
660
a7e6e8e6 Andy McCall 24 days ago ↶
661
pub async fn propose (
662
app : & Shared,
663
consumer : & RepoRow,
664
target : & str ,
665
path : & str ,
666
upstream : & RepoRow,
9aaa88b2 Andy McCall 24 days ago ↶
667
( from , to ) : ( Vec< ChangeHash> , Vec< ChangeHash> ) ,
c4e432da Andy McCall 23 days ago ↶
668
( why , watch ) : ( Why, Vec< String> ) ,
a7e6e8e6 Andy McCall 24 days ago ↶
669
) -> Option< i64 > {
670
use crate :: db:: proposals as pdb;
671
let bookmark = bump_bookmark ( path) ;
672
let bookmark = bookmark.as_str ( ) ;
673
let me = account ( & app.db) .await? ;
c4e432da Andy McCall 23 days ago ↶
674
let log = changelog ( app, upstream, from, to.clone ( ) , watch.clone ( ) ) .await;
675
let lines = log.lines;
676
let flagged = if log.watched.is_empty ( ) {
677
String :: new( )
678
} else {
679
format! (
680
" \n \n **Touches what you watch** ({} ):\n \n {} " ,
681
watch.iter ( ) .map ( | w | format! ( " `{w} `" ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ,
682
log.watched.join ( " \n " )
683
)
684
} ;
a7e6e8e6 Andy McCall 24 days ago ↶
685
let slug = format! ( " {} /{} " , upstream.owner, upstream.name) ;
686
let short: Vec< String> = to.iter ( ) .map ( | h | h.to_hex ( ) [ .. 12 ] .to_string ( ) ) .collect ( ) ;
1b6536f0 Andy McCall 24 days ago ↶
687
let ub = & upstream.default_bookmark;
688
let count = if lines.len ( ) = = 1 { " 1 change" .to_string ( ) } else { format! ( " {} changes" , lines.len ( ) ) } ;
689
let title = format! ( " Diffusion: {path} to {slug} {ub} ({count} )" ) ;
9aaa88b2 Andy McCall 24 days ago ↶
690
let when = match why {
691
Why:: Green => " at its newest state whose checks passed" ,
692
Why:: Moved => " as it is now" ,
693
Why:: Unverified => " as it is now" ,
694
} ;
695
let amber = if why = = Why:: Unverified {
696
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." )
697
} else {
698
String :: new( )
699
} ;
a7e6e8e6 Andy McCall 24 days ago ↶
700
let body = format! (
c4e432da Andy McCall 23 days ago ↶
701
" 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 24 days ago ↶
702
short.join ( " +" ) ,
a7e6e8e6 Andy McCall 24 days ago ↶
703
lines.join ( " \n " )
704
) ;
705
let open = pdb:: list( & app.db, consumer.id, & [ " open" , " draft" ] ) .await.ok ( ) ? .into_iter ( ) .find ( | p | p.source_bookmark = = bookmark ) ;
9aaa88b2 Andy McCall 24 days ago ↶
706
let ( number, id) = match open {
a7e6e8e6 Andy McCall 24 days ago ↶
707
Some ( p) => {
9aaa88b2 Andy McCall 24 days ago ↶
708
if p.title ! = title | | p.body ! = body {
709
pdb:: edit( & app.db, p.id, & title, & body, target) .await.ok ( ) ? ;
710
pdb:: add_event( & app.db, p.id, ACCOUNT , " updated" , & short.join ( " +" ) ) .await.ok ( ) ;
711
}
712
( p.number, p.id)
a7e6e8e6 Andy McCall 24 days ago ↶
713
}
714
None => {
715
let p = pdb:: create(
716
& app.db,
717
pdb:: NewProposal {
718
repo_id: consumer.id,
719
title: & title,
720
body: & body,
721
author_id: Some ( me.id) ,
722
author: ACCOUNT ,
723
draft: false ,
724
target,
725
source_kind: " bookmark" ,
726
source_repo_id: None ,
727
source_bookmark: bookmark,
728
source_changes: " " ,
729
} ,
730
)
731
.await
732
.ok ( ) ? ;
733
pdb:: add_event( & app.db, p.id, ACCOUNT , " opened" , " " ) .await.ok ( ) ;
734
db:: notify:: index( & app.db, " proposal" , consumer.id, p.id, p.number, & p.title, & p.body) .await.ok ( ) ;
735
if let Ok ( label) =
736
db:: issues:: upsert_label( & app.db, consumer.id, " dependencies" , " #0072ff" , " A subrepo moves (Diffusion)" ) .await
737
{
738
db:: issues:: set_proposal_label( & app.db, p.id, label.id, true ) .await.ok ( ) ;
739
}
9aaa88b2 Andy McCall 24 days ago ↶
740
( p.number, p.id)
a7e6e8e6 Andy McCall 24 days ago ↶
741
}
742
} ;
9aaa88b2 Andy McCall 24 days ago ↶
743
744
let unverified = db:: issues:: label_by_name( & app.db, consumer.id, " upstream unverified" ) .await.ok ( ) .flatten ( ) ;
745
match ( why = = Why:: Unverified, unverified) {
746
( true , l) => {
747
let l = match l {
748
Some ( l) => Some ( l) ,
749
None => db:: issues:: upsert_label(
750
& app.db,
751
consumer.id,
752
" upstream unverified" ,
753
" #e3a008" ,
754
" The upstream has no checks (Diffusion)" ,
755
)
756
.await
757
.ok ( ) ,
758
} ;
759
if let Some ( l) = l {
760
db:: issues:: set_proposal_label( & app.db, id, l.id, true ) .await.ok ( ) ;
761
}
762
}
763
( false , Some ( l) ) => {
764
db:: issues:: set_proposal_label( & app.db, id, l.id, false ) .await.ok ( ) ;
765
}
766
( false , None ) => { }
767
}
a7e6e8e6 Andy McCall 24 days ago ↶
768
Some ( number)
769
}
770
f01cb865 Andy McCall 24 days ago ↶
771
772
773
774
fn start_runs (
775
app : Shared,
776
row : RepoRow,
777
bookmark : String,
778
state : String,
779
heads : Vec< ChangeHash> ,
780
) -> std:: pin:: Pin< Box< dyn std:: future:: Future< Output = ( ) > + Send> > {
781
Box :: pin( async move { crate :: flow:: on_push( & app, & row, & bookmark, & state, & heads, ACCOUNT ) .await } )
782
}
783
a7e6e8e6 Andy McCall 24 days ago ↶
784
785
async fn oxymodules ( app : & Shared, name : & RepoName, bookmark : & str ) -> Option< String> {
786
let b = bookmark.to_string ( ) ;
787
app.repos
788
.read ( name, move | r, cache| {
789
let heads = heads_of ( r, & b) ? ;
790
if heads.is_empty ( ) {
791
return Ok ( None ) ;
792
}
793
let loaded = cache.load ( r, & heads) ? ;
794
Ok ( match loaded.snapshot.files.get ( " .oxymodules" ) .map ( | f | & f.content) {
795
Some ( oxy_core:: state:: FileContent:: Text { bytes, .. } ) => Some ( String :: from_utf8_lossy( bytes) .into_owned ( ) ) ,
796
_ => None ,
797
} )
798
} )
799
.await
800
.ok ( )
801
.flatten ( )
802
}
803
804
805
806
807
pub async fn propagate ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool ) {
3776b70d Andy McCall 24 days ago ↶
808
propagate_to ( app, upstream, heads, green, None ) .await
809
}
810
811
812
async fn propagate_to ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool , only : Option< i64 > ) {
a7e6e8e6 Andy McCall 24 days ago ↶
813
let Some ( uname) = RepoName:: new( & upstream.owner, & upstream.name) else { return } ;
814
let Ok ( id) = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await else { return } ;
815
let rows: Vec< ( i64 , String, String) > = sqlx:: query_as( " SELECT consumer_id, bookmark, path FROM diffusion_pins WHERE upstream = ?" )
816
.bind ( hex:: encode( id) )
817
.fetch_all ( & app.db)
818
.await
819
.unwrap_or_default ( ) ;
9aaa88b2 Andy McCall 24 days ago ↶
820
let unverified = unverified ( app, upstream) .await;
a7e6e8e6 Andy McCall 24 days ago ↶
821
for ( consumer_id, indexed, path) in rows {
3776b70d Andy McCall 24 days ago ↶
822
if only.is_some_and ( | o | o ! = consumer_id ) {
823
continue ;
824
}
a7e6e8e6 Andy McCall 24 days ago ↶
825
let Ok ( Some ( consumer) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
826
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { continue } ;
827
let learned = target ( & app.db, & consumer) .await;
828
let modules = oxymodules ( app, & cname, & learned) .await;
829
let r = rule ( & app.config.diffusion.follow, & consumer, modules.as_deref ( ) , & path) ;
830
let target = r.target.clone ( ) .unwrap_or ( learned) ;
831
if indexed ! = target {
832
continue ;
833
}
9aaa88b2 Andy McCall 24 days ago ↶
834
835
a7e6e8e6 Andy McCall 24 days ago ↶
836
let go = match r.follow {
837
Follow:: Off => false ,
9aaa88b2 Andy McCall 24 days ago ↶
838
Follow:: Green => green | | unverified,
a7e6e8e6 Andy McCall 24 days ago ↶
839
Follow:: Any => ! green,
840
} ;
9aaa88b2 Andy McCall 24 days ago ↶
841
let why = if green {
842
Why:: Green
843
} else if unverified {
844
Why:: Unverified
845
} else {
846
Why:: Moved
847
} ;
a7e6e8e6 Andy McCall 24 days ago ↶
848
if ! go {
849
continue ;
850
}
851
match bump ( app, & consumer, & target, & path, upstream, heads.clone ( ) ) .await {
f01cb865 Andy McCall 24 days ago ↶
852
Ok ( Bumped:: Moved { from, bookmark, heads: bumped, state, .. } ) => {
853
854
tokio:: spawn( start_runs ( app.clone ( ) , consumer.clone ( ) , bookmark, state, bumped) ) ;
c4e432da Andy McCall 23 days ago ↶
855
let n = propose ( app, & consumer, & target, & path, upstream, ( from, heads.clone ( ) ) , ( why, r.watch.clone ( ) ) ) .await;
a7e6e8e6 Andy McCall 24 days ago ↶
856
tracing:: info! (
857
" diffusion: {}/{} {path} bumped to {}/{} (proposal {n:?})" ,
858
consumer.owner,
859
consumer.name,
860
upstream.owner,
861
upstream.name
862
) ;
9aaa88b2 Andy McCall 24 days ago ↶
863
}
864
Ok ( Bumped:: Already { from, to } ) => {
865
866
c4e432da Andy McCall 23 days ago ↶
867
propose ( app, & consumer, & target, & path, upstream, ( from, to) , ( why, r.watch.clone ( ) ) ) .await;
a7e6e8e6 Andy McCall 24 days ago ↶
868
}
7a14bdae Andy McCall 24 days ago ↶
869
Ok ( other) => tracing:: info! ( " diffusion: {}/{} {path}: no bump ({other:?})" , consumer.owner, consumer.name) ,
a7e6e8e6 Andy McCall 24 days ago ↶
870
Err ( e) => tracing:: warn! ( " diffusion: bump {}/{} {path}: {e}" , consumer.owner, consumer.name) ,
3776b70d Andy McCall 24 days ago ↶
871
}
872
}
9aaa88b2 Andy McCall 24 days ago ↶
873
}
874
875
876
877
878
pub async fn unverified ( app : & Shared, upstream : & RepoRow) -> bool {
879
if ! crate :: flow:: enabled( app, upstream) {
880
return true ;
881
}
882
let runs: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM flow_runs WHERE repo_id = ? AND bookmark = ?" )
883
.bind ( upstream.id)
884
.bind ( & upstream.default_bookmark)
885
.fetch_one ( & app.db)
886
.await
887
.unwrap_or ( 0 ) ;
888
runs = = 0
3776b70d Andy McCall 24 days ago ↶
889
}
890
891
892
async fn repo_by_oxy_id ( app : & Shared, id : & str ) -> Option< RepoRow> {
893
for row in db:: repos:: list( & app.db) .await.ok ( ) ? {
894
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { continue } ;
895
if app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await.ok ( ) .map ( hex:: encode) .as_deref ( ) = = Some ( id) {
896
return Some ( row) ;
897
}
898
}
899
None
900
}
901
902
903
async fn last_green ( app : & Shared, upstream : & RepoRow) -> Option< Vec< ChangeHash> > {
904
let heads: String =
905
sqlx:: query_scalar( " SELECT heads FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND status = 'passed' ORDER BY id DESC LIMIT 1" )
906
.bind ( upstream.id)
907
.bind ( & upstream.default_bookmark)
908
.fetch_optional ( & app.db)
909
.await
910
.ok ( )
911
.flatten ( ) ? ;
912
let h: Vec< ChangeHash> = heads.split ( ' +' ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
913
( ! h.is_empty ( ) ) .then_some ( h)
914
}
915
916
917
918
919
pub async fn catch_up ( app : & Shared, consumer : & RepoRow) {
920
let upstreams: Vec< String> = sqlx:: query_scalar( " SELECT DISTINCT upstream FROM diffusion_pins WHERE consumer_id = ?" )
921
.bind ( consumer.id)
922
.fetch_all ( & app.db)
923
.await
924
.unwrap_or_default ( ) ;
925
for id in upstreams {
926
let Some ( upstream) = repo_by_oxy_id ( app, & id) .await else { continue } ;
927
if let Some ( h) = last_green ( app, & upstream) .await {
928
propagate_to ( app, & upstream, h, true , Some ( consumer.id) ) .await;
929
}
930
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { continue } ;
931
let b = upstream.default_bookmark.clone ( ) ;
932
if let Ok ( h) = app.repos.read ( & name, move | r, _ | heads_of ( r, & b) ) .await {
933
propagate_to ( app, & upstream, h, false , Some ( consumer.id) ) .await;
a7e6e8e6 Andy McCall 24 days ago ↶
934
}
935
}
936
}
937
e7158350 Andy McCall 24 days ago ↶
938
939
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize ) ]
940
# [ serde ( rename_all = " lowercase" ) ]
941
pub enum Follow {
942
Off,
943
944
Green,
945
946
Any,
947
}
948
949
impl Follow {
950
pub fn parse ( s : & str ) -> Option< Follow> {
951
match s.trim ( ) {
952
" off" => Some ( Follow:: Off) ,
953
" green" => Some ( Follow:: Green) ,
954
" any" => Some ( Follow:: Any) ,
955
_ => None ,
956
}
957
}
958
}
959
960
961
962
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize ) ]
963
pub struct Rule {
964
pub follow : Follow,
965
966
pub land_auto : bool ,
967
968
pub target : Option< String> ,
969
970
971
pub watch : Vec< String> ,
972
973
pub from : & 'static str ,
974
}
975
976
977
978
pub fn module_keys ( text : & str , path : & str ) -> ( Option< Follow> , Option< bool > , Option< String> , Vec< String> ) {
979
let ( mut follow, mut land, mut target, mut watch) = ( None , None , None , Vec :: new( ) ) ;
980
let mut here = false ;
981
for line in text.lines ( ) {
982
let line = line.trim ( ) ;
983
if line.is_empty ( ) | | line.starts_with ( ' #' ) {
984
continue ;
985
}
986
if let Some ( inner) = line.strip_prefix ( ' [' ) .and_then ( | l | l.strip_suffix ( ' ]' ) ) {
987
here = inner.trim ( ) .trim_matches ( ' "' ) .trim_end_matches ( ' /' ) = = path.trim_end_matches ( ' /' ) ;
988
continue ;
989
}
990
let ( true , Some ( ( k, v) ) ) = ( here, line.split_once ( ' =' ) ) else { continue } ;
991
let v = v.trim ( ) ;
992
let unquoted = v.trim_matches ( ' "' ) .trim ( ) ;
993
match k.trim ( ) {
994
" follow" => follow = Follow:: parse( unquoted) ,
995
" land" => land = Some ( unquoted = = " auto" ) ,
996
" target" if ! unquoted.is_empty ( ) => target = Some ( unquoted.to_string ( ) ) ,
997
" watch" => {
998
watch = v
999
.trim_start_matches ( ' [' )
1000
.trim_end_matches ( ' ]' )
1001
.split ( ' ,' )
1002
.map ( | w | w.trim ( ) .trim_matches ( ' "' ) .trim ( ) .to_string ( ) )
1003
.filter ( | w | ! w.is_empty ( ) )
1004
.collect ( )
1005
}
1006
_ => { }
1007
}
1008
}
1009
( follow, land, target, watch)
1010
}
1011
1012
1013
pub fn rule ( site_default : & str , row : & RepoRow, oxymodules : Option< & str > , path : & str ) -> Rule {
1014
let ( follow, land, target, watch) = oxymodules.map ( | t | module_keys ( t, path) ) .unwrap_or_default ( ) ;
1015
let ( follow, from) = match follow {
1016
Some ( f) => ( f, " .oxymodules" ) ,
1017
None => match Follow:: parse( & row.diffusion) {
1018
Some ( f) => ( f, " setting" ) ,
1019
None => ( Follow:: parse( site_default) .unwrap_or ( Follow:: Off) , " site" ) ,
1020
} ,
1021
} ;
1022
Rule { follow, land_auto: land.unwrap_or ( false ) , target, watch, from }
1023
}
1024
1025
# [ cfg ( test ) ]
1026
mod tests {
1027
use super :: * ;
1028
1029
const MODULES : & str = r # "
1030
[rstar-api]
1031
url = https://oxygenhub.io/doomsdayone/rstar-api
1032
follow = green
1033
land = "propose"
1034
target = develop
1035
watch = ["src/x16/irq*", "src/x16/frame*"]
1036
1037
[other]
1038
url = https://example/other
1039
follow = any
1040
"# ;
1041
1042
fn row ( diffusion : & str ) -> RepoRow {
1043
RepoRow {
1044
id: 1 ,
1045
owner: " o" .into ( ) ,
1046
name: " n" .into ( ) ,
1047
description: String :: new( ) ,
1048
default_bookmark: " main" .into ( ) ,
1049
visibility: " public" .into ( ) ,
1050
created_ms: 0 ,
1051
updated_ms: 0 ,
1052
git_mirror: false ,
1053
git_mirror_state: String :: new( ) ,
1054
forked_from: None ,
1055
topics: String :: new( ) ,
1056
flow: String :: new( ) ,
1057
diffusion: diffusion.into ( ) ,
1058
.. Default :: default( )
1059
}
c4e432da Andy McCall 23 days ago ↶
1060
}
1061
1062
# [ test ]
1063
fn watch_patterns_match_paths ( ) {
1064
assert! ( watched ( " src/x16/irq*" , " src/x16/irq.s" ) ) ;
1065
assert! ( watched ( " src/x16/irq*" , " src/x16/irq_wait.c" ) ) ;
1066
assert! ( ! watched ( " src/x16/irq*" , " src/x16/sub/irq.s" ) , " one segment" ) ;
1067
assert! ( watched ( " src/**/frame*.c" , " src/x16/floor/frame_wait.c" ) ) ;
1068
assert! ( watched ( " src/x16" , " src/x16/bank.s" ) , " a directory" ) ;
1069
assert! ( ! watched ( " src/x16" , " src/x160/bank.s" ) ) ;
1070
assert! ( watched ( " include/r?m.h" , " include/rim.h" ) ) ;
1071
assert! ( ! watched ( " " , " anything" ) ) ;
e7158350 Andy McCall 24 days ago ↶
1072
}
1073
1074
# [ test ]
1075
fn oxymodules_keys_are_read_per_entry ( ) {
1076
let ( f, land, target, watch) = module_keys ( MODULES , " rstar-api" ) ;
1077
assert_eq! ( ( f, land, target.as_deref ( ) ) , ( Some ( Follow:: Green) , Some ( false ) , Some ( " develop" ) ) ) ;
1078
assert_eq! ( watch, vec! [ " src/x16/irq*" , " src/x16/frame*" ] ) ;
1079
assert_eq! ( module_keys ( MODULES , " other" ) .0 , Some ( Follow:: Any) ) ;
1080
assert_eq! ( module_keys ( MODULES , " missing" ) , ( None , None , None , vec! [ ] ) ) ;
1081
assert_eq! ( module_keys ( " [a]\n watch = x, y\n " , " a" ) .3 , vec! [ " x" , " y" ] ) ;
1082
}
1083
1084
# [ test ]
1085
fn oxymodules_wins_then_the_setting_then_the_site ( ) {
1086
let r = rule ( " off" , & row ( " any" ) , Some ( MODULES ) , " rstar-api" ) ;
1087
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " .oxymodules" ) ) ;
1088
let r = rule ( " off" , & row ( " any" ) , Some ( " [rstar-api]\n url = x\n " ) , " rstar-api" ) ;
1089
assert_eq! ( ( r.follow, r.from) , ( Follow:: Any, " setting" ) ) ;
1090
let r = rule ( " green" , & row ( " " ) , None , " rstar-api" ) ;
1091
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " site" ) ) ;
1092
assert_eq! ( rule ( " nonsense" , & row ( " " ) , None , " x" ) .follow, Follow:: Off) ;
1093
}
1094
}