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