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
}
d1bf2d69 Andy McCall 20 days ago ↶
149
sqlx:: query( " INSERT INTO diffusion_indexed (consumer_id, bookmark, heads, updated_ms) VALUES ($1, $2, $3, $4) ON CONFLICT (consumer_id, bookmark) DO UPDATE SET heads = excluded.heads, updated_ms = excluded.updated_ms" )
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
}
3e868a1f Andy McCall 10 days ago ↶
723
let planned = match plan ( app, & uname, old.clone ( ) , new_heads.clone ( ) ) .await? {
7a14bdae Andy McCall 23 days ago ↶
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
} ;
3e868a1f Andy McCall 10 days ago ↶
729
let bookmark = pin_bookmark ( & planned, & new_heads, & upstream.default_bookmark, bookmark) ;
730
let new_heads = planned;
1d634f69 Andy McCall 23 days ago ↶
731
if let Some ( Pin:: Oxy { heads: b, .. } ) = & bumped {
732
let ( mut b, mut n) = ( b.clone ( ) , new_heads.clone ( ) ) ;
733
b.sort ( ) ;
734
n.sort ( ) ;
735
if b = = n {
9aaa88b2 Andy McCall 23 days ago ↶
736
return Ok ( Bumped:: Already { from: old, to: new_heads } ) ;
1d634f69 Andy McCall 23 days ago ↶
737
}
738
}
739
let ( key, secret) =
740
crate :: keys:: load_secret( & app.config.keys_dir ( ) ) .map_err ( | e | crate :: repos:: RepoError:: Io( std:: io:: Error:: other( e.to_string ( ) ) ) ) ? ;
741
let identity = app.hub_key.identity.clone ( ) ;
742
let upstream_label = format! ( " {} /{} " , upstream.owner, upstream.name) ;
743
let ( t, p) = ( target.to_string ( ) , path.trim_matches ( ' /' ) .to_string ( ) ) ;
744
let pin = Pin:: Oxy { repo, heads: new_heads.clone ( ) , bookmark } ;
a7e6e8e6 Andy McCall 23 days ago ↶
745
let old = old.clone ( ) ;
1d634f69 Andy McCall 23 days ago ↶
746
let short: Vec< String> = new_heads.iter ( ) .map ( | h | h.to_hex ( ) [ .. 12 ] .to_string ( ) ) .collect ( ) ;
62cdf34d Andy McCall 20 days ago ↶
747
let message = bump_message ( & p, & upstream_label, & short) ;
1d634f69 Andy McCall 23 days ago ↶
748
app.repos
749
.write ( & cname, move | r, _ | {
750
let base = heads_of ( r, & t) ? ;
751
let change = r.build_pin_change ( & base, & p, & pin, & message, key) ? ;
752
let hash = change.hash ( ) ;
753
let sig = secret.sign ( Domain:: Change, & key, & hash) ;
754
let mut state = r.load_state ( & base) ? ;
755
state.add ( change.clone ( ) ) .map_err ( | e | crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( e.to_string ( ) ) ) ) ? ;
756
let mut candidates = base.clone ( ) ;
757
candidates.push ( hash) ;
758
let heads = state.maximal ( & candidates) ;
759
let bookmark = bump_bookmark ( & p) ;
760
761
762
let mut update = r.host_ref_update ( & bookmark, & heads, Some ( ( & key, & secret) ) ) ? ;
763
update.force = true ;
f01cb865 Andy McCall 23 days ago ↶
764
let state = update.new_state.to_hex ( ) ;
1d634f69 Andy McCall 23 days ago ↶
765
let report = r.apply_hosted ( Some ( ( change, vec! [ sig] ) ) , & [ identity] , update) ? ;
766
if let Some ( refused) = report.results.iter ( ) .find ( | x | ! x.ok) {
767
return Err ( crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( format! (
768
" the bump was refused: {} " ,
769
refused.reason
770
) ) ) ) ;
771
}
f01cb865 Andy McCall 23 days ago ↶
772
Ok ( Bumped:: Moved { bookmark, change: hash, heads, from: old, state } )
1d634f69 Andy McCall 23 days ago ↶
773
} )
774
.await
775
}
776
3e868a1f Andy McCall 10 days ago ↶
777
778
779
780
781
782
783
784
fn pin_bookmark ( planned : & [ChangeHash], new : & [ChangeHash], default : & str , old : String) -> String {
785
if heads_key ( planned) = = heads_key ( new) {
786
default.to_string ( )
787
} else {
788
old
789
}
790
}
791
a7e6e8e6 Andy McCall 23 days ago ↶
792
793
pub const ACCOUNT : & str = " diffusion" ;
794
795
796
pub async fn account ( pool : & Pool) -> Option< Account> {
797
if let Ok ( Some ( a) ) = db:: accounts:: account_by_name( pool, ACCOUNT ) .await {
798
return Some ( a) ;
799
}
800
db:: accounts:: create_account( pool, ACCOUNT , " diffusion@localhost" , " !" , " " , false ) .await.ok ( )
801
}
802
c4e432da Andy McCall 23 days ago ↶
803
804
805
806
807
pub fn watched ( pattern : & str , path : & str ) -> bool {
808
fn m ( p : & [u8 ], s : & [u8 ]) -> bool {
809
match ( p.first ( ) , s.first ( ) ) {
810
( None , None ) => true ,
811
( Some ( b ' *' ) , _ ) if p.get ( 1 ) = = Some ( & b ' *' ) => ( 0 .. = s.len ( ) ) .any ( | i | m ( & p[ 2 .. ] , & s[ i.. ] ) ) ,
812
( Some ( b ' *' ) , _ ) => ( 0 .. = s.len ( ) ) .take_while ( | & i | i = = 0 | | s[ i - 1 ] ! = b ' /' ) .any ( | i | m ( & p[ 1 .. ] , & s[ i.. ] ) ) ,
813
( Some ( b ' ?' ) , Some ( c) ) if * c ! = b ' /' => m ( & p[ 1 .. ] , & s[ 1 .. ] ) ,
814
( Some ( a) , Some ( b) ) if a = = b => m ( & p[ 1 .. ] , & s[ 1 .. ] ) ,
815
_ => false ,
816
}
817
}
818
let p = pattern.trim ( ) .trim_matches ( ' /' ) ;
819
if p.is_empty ( ) {
820
return false ;
821
}
822
m ( p.as_bytes ( ) , path.as_bytes ( ) ) | | ( ! p.contains ( [ ' *' , ' ?' ] ) & & path.starts_with ( & format! ( " {p} /" ) ) )
823
}
824
825
826
827
828
pub struct Changelog {
829
pub lines : Vec< String> ,
830
pub watched : Vec< String> ,
831
}
832
833
pub async fn changelog ( app : & Shared, upstream : & RepoRow, old : Vec< ChangeHash> , new : Vec< ChangeHash> , watch : Vec< String> ) -> Changelog {
834
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { return Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } } ;
a7e6e8e6 Andy McCall 23 days ago ↶
835
let linked: std:: collections:: HashMap< String, String> =
836
db:: accounts:: identity_accounts( & app.db) .await.unwrap_or_default ( ) .into_iter ( ) .collect ( ) ;
837
let base = format! ( " /{} /{} " , upstream.owner, upstream.name) ;
838
app.repos
839
.read ( & name, move | r, cache| {
840
let ( o, n) = ( cache.summary ( r, & old) ? , cache.summary ( r, & new) ? ) ;
c4e432da Andy McCall 23 days ago ↶
841
let mut added: Vec< ( u64 , ChangeHash, String, String, Vec< String> ) > = Vec :: new( ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
842
for h in n.changes.iter ( ) .filter ( | c | ! o.changes.contains ( * c) ) {
843
let Ok ( c) = r.store ( ) .get_change ( h) else { continue } ;
844
let key = c.author.to_hex ( ) ;
845
let who = linked.get ( & key) .cloned ( ) .unwrap_or_else ( | | key[ .. 12 ] .to_string ( ) ) ;
846
let first = c.message.lines ( ) .next ( ) .unwrap_or ( " " ) .trim ( ) .to_string ( ) ;
c4e432da Andy McCall 23 days ago ↶
847
848
let hits: Vec< String> = if watch.is_empty ( ) {
849
Vec :: new( )
850
} else {
851
crate :: repos:: view:: change( r, cache, h)
852
.map ( | d | d.files.into_iter ( ) .map ( | f | f.path ) .filter ( | p | watch.iter ( ) .any ( | w | watched ( w, p) ) ) .collect ( ) )
853
.unwrap_or_default ( )
854
} ;
855
added.push ( ( c.hlc.wall_ms, * h, first, who, hits) ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
856
}
857
added.sort_by_key ( | a | std:: cmp:: Reverse( a.0 ) ) ;
c4e432da Andy McCall 23 days ago ↶
858
let line = | h : & ChangeHash, msg : & str , who : & str , flag : bool | {
859
format! (
860
" - {} [`{} `]({base} /change/{} ) {} ({who} )" ,
861
if flag { " ⚑ " } else { " " } ,
862
& h.to_hex ( ) [ .. 12 ] ,
863
h.to_hex ( ) ,
864
msg.replace ( ' \n ' , " " )
865
)
866
} ;
867
let watched = added
868
.iter ( )
869
.filter ( | a | ! a.4. is_empty ( ) )
870
.map ( | ( _, h , msg , who , hits ) | {
871
format! (
872
" {} — touches {} " ,
873
line ( h, msg, who, true ) ,
874
hits.iter ( ) .map ( | p | format! ( " `{p} `" ) ) .collect:: < Vec< _ > > ( ) .join ( " , " )
875
)
a7e6e8e6 Andy McCall 23 days ago ↶
876
} )
c4e432da Andy McCall 23 days ago ↶
877
.collect ( ) ;
878
let lines = added.iter ( ) .map ( | ( _, h , msg , who , hits ) | line ( h, msg, who, ! hits.is_empty ( ) ) ) .collect ( ) ;
879
Ok ( Changelog { lines, watched } )
a7e6e8e6 Andy McCall 23 days ago ↶
880
} )
881
.await
c4e432da Andy McCall 23 days ago ↶
882
.unwrap_or ( Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } )
a7e6e8e6 Andy McCall 23 days ago ↶
883
}
884
9aaa88b2 Andy McCall 23 days ago ↶
885
886
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
887
pub enum Why {
888
889
Green,
890
891
Moved,
892
893
894
Unverified,
895
}
896
897
898
899
900
a7e6e8e6 Andy McCall 23 days ago ↶
901
pub async fn propose (
902
app : & Shared,
903
consumer : & RepoRow,
904
target : & str ,
905
path : & str ,
906
upstream : & RepoRow,
9aaa88b2 Andy McCall 23 days ago ↶
907
( from , to ) : ( Vec< ChangeHash> , Vec< ChangeHash> ) ,
f4d6ef41 Andy McCall 22 days ago ↶
908
( why , watch , lands ) : ( Why, Vec< String> , bool ) ,
a7e6e8e6 Andy McCall 23 days ago ↶
909
) -> Option< i64 > {
910
use crate :: db:: proposals as pdb;
911
let bookmark = bump_bookmark ( path) ;
912
let bookmark = bookmark.as_str ( ) ;
913
let me = account ( & app.db) .await? ;
c4e432da Andy McCall 23 days ago ↶
914
let log = changelog ( app, upstream, from, to.clone ( ) , watch.clone ( ) ) .await;
915
let lines = log.lines;
916
let flagged = if log.watched.is_empty ( ) {
917
String :: new( )
918
} else {
919
format! (
920
" \n \n **Touches what you watch** ({} ):\n \n {} " ,
921
watch.iter ( ) .map ( | w | format! ( " `{w} `" ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ,
922
log.watched.join ( " \n " )
923
)
924
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
925
let slug = format! ( " {} /{} " , upstream.owner, upstream.name) ;
926
let short: Vec< String> = to.iter ( ) .map ( | h | h.to_hex ( ) [ .. 12 ] .to_string ( ) ) .collect ( ) ;
1b6536f0 Andy McCall 23 days ago ↶
927
let ub = & upstream.default_bookmark;
928
let count = if lines.len ( ) = = 1 { " 1 change" .to_string ( ) } else { format! ( " {} changes" , lines.len ( ) ) } ;
929
let title = format! ( " Diffusion: {path} to {slug} {ub} ({count} )" ) ;
9aaa88b2 Andy McCall 23 days ago ↶
930
let when = match why {
931
Why:: Green => " at its newest state whose checks passed" ,
932
Why:: Moved => " as it is now" ,
933
Why:: Unverified => " as it is now" ,
934
} ;
935
let amber = if why = = Why:: Unverified {
936
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." )
937
} else {
938
String :: new( )
939
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
940
let body = format! (
f4d6ef41 Andy McCall 22 days ago ↶
941
" 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 ↶
942
crate :: web:: state_label( & short.join ( " +" ) ) ,
f4d6ef41 Andy McCall 22 days ago ↶
943
lines.join ( " \n " ) ,
944
if lands {
945
" 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."
946
} else {
947
" It never lands by itself."
948
}
a7e6e8e6 Andy McCall 23 days ago ↶
949
) ;
950
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 ↶
951
let ( number, id) = match open {
a7e6e8e6 Andy McCall 23 days ago ↶
952
Some ( p) => {
9aaa88b2 Andy McCall 23 days ago ↶
953
if p.title ! = title | | p.body ! = body {
954
pdb:: edit( & app.db, p.id, & title, & body, target) .await.ok ( ) ? ;
6ae3fa0a Andy McCall 22 days ago ↶
955
pdb:: add_event( & app.db, p.id, ACCOUNT , " updated" , & crate :: web:: state_label( & short.join ( " +" ) ) ) .await.ok ( ) ;
9aaa88b2 Andy McCall 23 days ago ↶
956
}
957
( p.number, p.id)
a7e6e8e6 Andy McCall 23 days ago ↶
958
}
959
None => {
960
let p = pdb:: create(
961
& app.db,
962
pdb:: NewProposal {
963
repo_id: consumer.id,
964
title: & title,
965
body: & body,
966
author_id: Some ( me.id) ,
967
author: ACCOUNT ,
968
draft: false ,
969
target,
970
source_kind: " bookmark" ,
971
source_repo_id: None ,
972
source_bookmark: bookmark,
973
source_changes: " " ,
974
} ,
975
)
976
.await
977
.ok ( ) ? ;
978
pdb:: add_event( & app.db, p.id, ACCOUNT , " opened" , " " ) .await.ok ( ) ;
979
db:: notify:: index( & app.db, " proposal" , consumer.id, p.id, p.number, & p.title, & p.body) .await.ok ( ) ;
bff9106e Andy McCall 22 days ago ↶
980
981
crate :: notify:: deliver(
982
app,
983
crate :: notify:: Event {
984
kind: " proposal.opened" ,
985
title: format! ( " {} /{} #{} : {} " , consumer.owner, consumer.name, p.number, p.title) ,
986
url: format! ( " /{} /{} /proposal/{} " , consumer.owner, consumer.name, p.number) ,
987
actor: & me,
988
repo_id: consumer.id,
989
thread: Some ( ( " proposal" , p.id) ) ,
990
mention_text: " " ,
991
direct: vec! [ ] ,
992
} ,
993
)
994
.await;
a7e6e8e6 Andy McCall 23 days ago ↶
995
if let Ok ( label) =
996
db:: issues:: upsert_label( & app.db, consumer.id, " dependencies" , " #0072ff" , " A subrepo moves (Diffusion)" ) .await
997
{
998
db:: issues:: set_proposal_label( & app.db, p.id, label.id, true ) .await.ok ( ) ;
999
}
9aaa88b2 Andy McCall 23 days ago ↶
1000
( p.number, p.id)
a7e6e8e6 Andy McCall 23 days ago ↶
1001
}
1002
} ;
9aaa88b2 Andy McCall 23 days ago ↶
1003
1004
let unverified = db:: issues:: label_by_name( & app.db, consumer.id, " upstream unverified" ) .await.ok ( ) .flatten ( ) ;
1005
match ( why = = Why:: Unverified, unverified) {
1006
( true , l) => {
1007
let l = match l {
1008
Some ( l) => Some ( l) ,
1009
None => db:: issues:: upsert_label(
1010
& app.db,
1011
consumer.id,
1012
" upstream unverified" ,
1013
" #e3a008" ,
1014
" The upstream has no checks (Diffusion)" ,
1015
)
1016
.await
1017
.ok ( ) ,
1018
} ;
1019
if let Some ( l) = l {
1020
db:: issues:: set_proposal_label( & app.db, id, l.id, true ) .await.ok ( ) ;
1021
}
1022
}
1023
( false , Some ( l) ) => {
1024
db:: issues:: set_proposal_label( & app.db, id, l.id, false ) .await.ok ( ) ;
1025
}
1026
( false , None ) => { }
1027
}
a7e6e8e6 Andy McCall 23 days ago ↶
1028
Some ( number)
1029
}
1030
f01cb865 Andy McCall 23 days ago ↶
1031
1032
1033
1034
fn start_runs (
1035
app : Shared,
1036
row : RepoRow,
1037
bookmark : String,
1038
state : String,
1039
heads : Vec< ChangeHash> ,
1040
) -> std:: pin:: Pin< Box< dyn std:: future:: Future< Output = ( ) > + Send> > {
1041
Box :: pin( async move { crate :: flow:: on_push( & app, & row, & bookmark, & state, & heads, ACCOUNT ) .await } )
1042
}
1043
a7e6e8e6 Andy McCall 23 days ago ↶
1044
1045
async fn oxymodules ( app : & Shared, name : & RepoName, bookmark : & str ) -> Option< String> {
1046
let b = bookmark.to_string ( ) ;
1047
app.repos
1048
.read ( name, move | r, cache| {
1049
let heads = heads_of ( r, & b) ? ;
1050
if heads.is_empty ( ) {
1051
return Ok ( None ) ;
1052
}
1053
let loaded = cache.load ( r, & heads) ? ;
1054
Ok ( match loaded.snapshot.files.get ( " .oxymodules" ) .map ( | f | & f.content) {
1055
Some ( oxy_core:: state:: FileContent:: Text { bytes, .. } ) => Some ( String :: from_utf8_lossy( bytes) .into_owned ( ) ) ,
1056
_ => None ,
1057
} )
1058
} )
1059
.await
1060
.ok ( )
1061
.flatten ( )
1062
}
1063
1064
1065
1066
1067
pub async fn propagate ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool ) {
3776b70d Andy McCall 23 days ago ↶
1068
propagate_to ( app, upstream, heads, green, None ) .await
1069
}
1070
1071
1072
async fn propagate_to ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool , only : Option< i64 > ) {
a7e6e8e6 Andy McCall 23 days ago ↶
1073
let Some ( uname) = RepoName:: new( & upstream.owner, & upstream.name) else { return } ;
1074
let Ok ( id) = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await else { return } ;
5d90e0e0 Andy McCall 20 days ago
1075
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 ↶
1076
.bind ( hex:: encode( id) )
1077
.fetch_all ( & app.db)
1078
.await
1079
.unwrap_or_default ( ) ;
9aaa88b2 Andy McCall 23 days ago ↶
1080
let unverified = unverified ( app, upstream) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
1081
for ( consumer_id, indexed, path) in rows {
3776b70d Andy McCall 23 days ago ↶
1082
if only.is_some_and ( | o | o ! = consumer_id ) {
1083
continue ;
1084
}
a7e6e8e6 Andy McCall 23 days ago ↶
1085
let Ok ( Some ( consumer) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
1086
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { continue } ;
1087
let learned = target ( & app.db, & consumer) .await;
1088
let modules = oxymodules ( app, & cname, & learned) .await;
1089
let r = rule ( & app.config.diffusion.follow, & consumer, modules.as_deref ( ) , & path) ;
1090
let target = r.target.clone ( ) .unwrap_or ( learned) ;
1091
if indexed ! = target {
1092
continue ;
1093
}
9aaa88b2 Andy McCall 23 days ago ↶
1094
1095
a7e6e8e6 Andy McCall 23 days ago ↶
1096
let go = match r.follow {
1097
Follow:: Off => false ,
9aaa88b2 Andy McCall 23 days ago ↶
1098
Follow:: Green => green | | unverified,
a7e6e8e6 Andy McCall 23 days ago ↶
1099
Follow:: Any => ! green,
1100
} ;
9aaa88b2 Andy McCall 23 days ago ↶
1101
let why = if green {
1102
Why:: Green
1103
} else if unverified {
1104
Why:: Unverified
1105
} else {
1106
Why:: Moved
1107
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
1108
if ! go {
1109
continue ;
1110
}
356183b7 Andy McCall 15 days ago ↶
1111
1112
let Ok ( _pin) = crate :: leader:: exclusive( & app.db, & format! ( " diffusion:{} :{path} " , consumer.id) ) .await else { continue } ;
a7e6e8e6 Andy McCall 23 days ago ↶
1113
match bump ( app, & consumer, & target, & path, upstream, heads.clone ( ) ) .await {
f01cb865 Andy McCall 23 days ago ↶
1114
Ok ( Bumped:: Moved { from, bookmark, heads: bumped, state, .. } ) => {
1115
1116
tokio:: spawn( start_runs ( app.clone ( ) , consumer.clone ( ) , bookmark, state, bumped) ) ;
f4d6ef41 Andy McCall 22 days ago ↶
1117
let n = propose ( app, & consumer, & target, & path, upstream, ( from, heads.clone ( ) ) , ( why, r.watch.clone ( ) , r.land_auto) ) .await;
33d98147 Andy McCall 16 days ago
1118
app.bus.live ( & crate :: events:: topic:: diffusion( consumer.id) , " bump" ) ;
1119
app.bus.live ( & crate :: events:: topic:: diffusion( upstream.id) , " bump" ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
1120
tracing:: info! (
1121
" diffusion: {}/{} {path} bumped to {}/{} (proposal {n:?})" ,
1122
consumer.owner,
1123
consumer.name,
1124
upstream.owner,
1125
upstream.name
1126
) ;
9aaa88b2 Andy McCall 23 days ago ↶
1127
}
1128
Ok ( Bumped:: Already { from, to } ) => {
1129
1130
f4d6ef41 Andy McCall 22 days ago ↶
1131
propose ( app, & consumer, & target, & path, upstream, ( from, to) , ( why, r.watch.clone ( ) , r.land_auto) ) .await;
1132
1133
1134
if green & & r.land_auto {
1135
tokio:: spawn( try_land ( app.clone ( ) , consumer.clone ( ) , path.clone ( ) ) ) ;
1136
}
a7e6e8e6 Andy McCall 23 days ago ↶
1137
}
7a14bdae Andy McCall 23 days ago ↶
1138
Ok ( other) => tracing:: info! ( " diffusion: {}/{} {path}: no bump ({other:?})" , consumer.owner, consumer.name) ,
a7e6e8e6 Andy McCall 23 days ago ↶
1139
Err ( e) => tracing:: warn! ( " diffusion: bump {}/{} {path}: {e}" , consumer.owner, consumer.name) ,
3776b70d Andy McCall 23 days ago ↶
1140
}
1141
}
f4d6ef41 Andy McCall 22 days ago ↶
1142
}
1143
1144
1145
# [ derive ( Debug, PartialEq, Eq ) ]
1146
pub enum Held {
1147
1148
NoBump,
1149
1150
Manual,
1151
1152
Unverified,
1153
1154
UpstreamUnchecked,
1155
1156
NotPassed,
1157
1158
Refused( String ) ,
9aaa88b2 Andy McCall 23 days ago ↶
1159
}
1160
f4d6ef41 Andy McCall 22 days ago ↶
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
pub async fn auto_land ( app : & Shared, consumer : & RepoRow, path : & str ) -> Result< i64 , Held> {
1171
let path = path.trim_matches ( ' /' ) ;
1172
let bm = bump_bookmark ( path) ;
1173
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { return Err ( Held:: NoBump) } ;
1174
let bump = db:: proposals:: list( & app.db, consumer.id, & [ " open" ] )
1175
.await
1176
.ok ( )
1177
.and_then ( | ps | ps.into_iter ( ) .find ( | p | p.source_bookmark = = bm & & p.author = = ACCOUNT ) )
1178
.ok_or ( Held:: NoBump) ? ;
1179
let learned = target ( & app.db, consumer) .await;
1180
let modules = oxymodules ( app, & cname, & learned) .await;
1181
if ! rule ( & app.config.diffusion.follow, consumer, modules.as_deref ( ) , path) .land_auto {
1182
return Err ( Held:: Manual) ;
1183
}
1184
let upstream_id: Option< i64 > = sqlx:: query_scalar(
5d90e0e0 Andy McCall 20 days ago
1185
" 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 ↶
1186
)
1187
.bind ( consumer.id)
1188
.bind ( path)
1189
.fetch_optional ( & app.db)
1190
.await
1191
.ok ( )
1192
.flatten ( ) ;
1193
let Some ( upstream) = ( match upstream_id {
1194
Some ( id) => db:: repos:: get_by_id( & app.db, id) .await.ok ( ) .flatten ( ) ,
1195
None => None ,
1196
} ) else {
1197
return Err ( Held:: UpstreamUnchecked) ;
1198
} ;
1199
if unverified ( app, & upstream) .await {
1200
return Err ( Held:: Unverified) ;
1201
}
1202
1203
let ( p, b) = ( path.to_string ( ) , bm.clone ( ) ) ;
1204
let ( pinned, now) = app
1205
.repos
1206
.read ( & cname, move | r, cache| {
1207
let heads = heads_of ( r, & b) ? ;
1208
if heads.is_empty ( ) {
1209
return Ok ( ( None , heads) ) ;
1210
}
1211
let loaded = cache.load ( r, & heads) ? ;
1212
let pin = match loaded.snapshot.files.get ( & p) .map ( | f | & f.content) {
1213
Some ( oxy_core:: state:: FileContent:: Opaque { blobs } ) => match blobs.first ( ) {
1214
Some ( blob) => oxy_core:: subrepo:: Pin:: decode( & r.read_blob ( blob) ? ) .ok ( ) ,
1215
None => None ,
1216
} ,
1217
_ => None ,
1218
} ;
1219
Ok ( ( pin, heads) )
1220
} )
1221
.await
1222
.map_err ( | e | Held:: Refused( e.to_string ( ) ) ) ? ;
1223
let Some ( oxy_core:: subrepo:: Pin:: Oxy { heads: pinned, .. } ) = pinned else { return Err ( Held:: UpstreamUnchecked) } ;
1224
let same = | text : & str , want : & [ChangeHash]| {
1225
let mut a: Vec< String> = text.split ( ' +' ) .filter ( | h | ! h.is_empty ( ) ) .map ( str :: to_string) .collect ( ) ;
1226
let mut b: Vec< String> = want.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ;
1227
a.sort ( ) ;
1228
b.sort ( ) ;
1229
a = = b
1230
} ;
1231
let upstream_runs: Vec< String> = sqlx:: query_scalar(
5d90e0e0 Andy McCall 20 days ago
1232
" 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 ↶
1233
)
1234
.bind ( upstream.id)
1235
.bind ( & upstream.default_bookmark)
1236
.fetch_all ( & app.db)
1237
.await
1238
.unwrap_or_default ( ) ;
1239
if ! upstream_runs.iter ( ) .any ( | h | same ( h, & pinned) ) {
1240
return Err ( Held:: UpstreamUnchecked) ;
1241
}
1242
let run: Option< ( String, String) > =
5d90e0e0 Andy McCall 20 days ago
1243
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 ↶
1244
.bind ( consumer.id)
1245
.bind ( & bm)
1246
.fetch_optional ( & app.db)
1247
.await
1248
.ok ( )
1249
.flatten ( ) ;
1250
if ! run.is_some_and ( | ( status , heads ) | status = = " passed" & & same ( & heads, & now) ) {
1251
return Err ( Held:: NotPassed) ;
1252
}
1253
let me = account ( & app.db) .await.ok_or_else ( | | Held:: Refused( " the diffusion account is missing" .into ( ) ) ) ? ;
1254
let via = " Diffusion, both halves green" ;
2a8b6216 Andy McCall 18 days ago ↶
1255
match crate :: web:: proposals:: land_as( app, & cname, consumer, & me, bump.number, via, true , None ) .await {
f4d6ef41 Andy McCall 22 days ago ↶
1256
Ok ( _ ) => {
1257
tracing:: info! ( " diffusion: {}/{} landed #{} itself (both halves green)" , consumer.owner, consumer.name, bump.number) ;
1258
Ok ( bump.number)
1259
}
1260
Err ( crate :: web:: proposals:: Landing:: NotFound) => Err ( Held:: NoBump) ,
1261
Err ( crate :: web:: proposals:: Landing:: Refused( why) ) => {
1262
1263
let said = db:: proposals:: events( & app.db, bump.id)
1264
.await
1265
.unwrap_or_default ( )
1266
.iter ( )
1267
.any ( | e | e.event = = " not landed by itself" & & e.detail = = why ) ;
1268
if ! said {
1269
db:: proposals:: add_event( & app.db, bump.id, ACCOUNT , " not landed by itself" , & why) .await.ok ( ) ;
1270
}
1271
Err ( Held:: Refused( why) )
1272
}
1273
}
1274
}
1275
1276
1277
1278
pub fn try_land ( app : Shared, consumer : RepoRow, path : String) -> std:: pin:: Pin< Box< dyn std:: future:: Future< Output = ( ) > + Send> > {
1279
Box :: pin( async move {
356183b7 Andy McCall 15 days ago ↶
1280
1281
let Ok ( _pin) = crate :: leader:: exclusive( & app.db, & format! ( " diffusion:{} :{path} " , consumer.id) ) .await else { return } ;
f4d6ef41 Andy McCall 22 days ago ↶
1282
match auto_land ( & app, & consumer, & path) .await {
1283
Ok ( _ ) | Err ( Held:: NoBump | Held:: Manual) => { }
1284
Err ( why) => tracing:: info! ( " diffusion: {}/{} {path} waits for a person: {why:?}" , consumer.owner, consumer.name) ,
1285
}
1286
} )
1287
}
1288
1289
1290
pub async fn land_ready ( app : & Shared, consumer : & RepoRow) {
5d90e0e0 Andy McCall 20 days ago
1291
let paths: Vec< String> = sqlx:: query_scalar( " SELECT DISTINCT path FROM diffusion_pins WHERE consumer_id = $1" )
f4d6ef41 Andy McCall 22 days ago ↶
1292
.bind ( consumer.id)
1293
.fetch_all ( & app.db)
1294
.await
1295
.unwrap_or_default ( ) ;
1296
for path in paths {
1297
tokio:: spawn( try_land ( app.clone ( ) , consumer.clone ( ) , path) ) ;
1298
}
1299
}
1300
9aaa88b2 Andy McCall 23 days ago ↶
1301
1302
1303
1304
pub async fn unverified ( app : & Shared, upstream : & RepoRow) -> bool {
1305
if ! crate :: flow:: enabled( app, upstream) {
1306
return true ;
1307
}
5d90e0e0 Andy McCall 20 days ago
1308
let runs: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM flow_runs WHERE repo_id = $1 AND bookmark = $2" )
9aaa88b2 Andy McCall 23 days ago ↶
1309
.bind ( upstream.id)
1310
.bind ( & upstream.default_bookmark)
1311
.fetch_one ( & app.db)
1312
.await
1313
.unwrap_or ( 0 ) ;
1314
runs = = 0
3776b70d Andy McCall 23 days ago ↶
1315
}
1316
1317
1318
async fn repo_by_oxy_id ( app : & Shared, id : & str ) -> Option< RepoRow> {
1319
for row in db:: repos:: list( & app.db) .await.ok ( ) ? {
1320
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { continue } ;
1321
if app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await.ok ( ) .map ( hex:: encode) .as_deref ( ) = = Some ( id) {
1322
return Some ( row) ;
1323
}
1324
}
1325
None
1326
}
1327
1328
1329
async fn last_green ( app : & Shared, upstream : & RepoRow) -> Option< Vec< ChangeHash> > {
5d90e0e0 Andy McCall 20 days ago
1330
let heads: String = sqlx:: query_scalar(
1331
" SELECT heads FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 AND status = 'passed' ORDER BY id DESC LIMIT 1" ,
1332
)
1333
.bind ( upstream.id)
1334
.bind ( & upstream.default_bookmark)
1335
.fetch_optional ( & app.db)
1336
.await
1337
.ok ( )
1338
.flatten ( ) ? ;
3776b70d Andy McCall 23 days ago ↶
1339
let h: Vec< ChangeHash> = heads.split ( ' +' ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
1340
( ! h.is_empty ( ) ) .then_some ( h)
1341
}
1342
1343
1344
1345
1346
pub async fn catch_up ( app : & Shared, consumer : & RepoRow) {
5d90e0e0 Andy McCall 20 days ago
1347
let upstreams: Vec< String> = sqlx:: query_scalar( " SELECT DISTINCT upstream FROM diffusion_pins WHERE consumer_id = $1" )
3776b70d Andy McCall 23 days ago ↶
1348
.bind ( consumer.id)
1349
.fetch_all ( & app.db)
1350
.await
1351
.unwrap_or_default ( ) ;
1352
for id in upstreams {
1353
let Some ( upstream) = repo_by_oxy_id ( app, & id) .await else { continue } ;
1354
if let Some ( h) = last_green ( app, & upstream) .await {
1355
propagate_to ( app, & upstream, h, true , Some ( consumer.id) ) .await;
1356
}
1357
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { continue } ;
1358
let b = upstream.default_bookmark.clone ( ) ;
1359
if let Ok ( h) = app.repos.read ( & name, move | r, _ | heads_of ( r, & b) ) .await {
1360
propagate_to ( app, & upstream, h, false , Some ( consumer.id) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
1361
}
1362
}
1363
}
1364
e7158350 Andy McCall 23 days ago ↶
1365
1366
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize ) ]
1367
# [ serde ( rename_all = " lowercase" ) ]
1368
pub enum Follow {
1369
Off,
1370
1371
Green,
1372
1373
Any,
1374
}
1375
1376
impl Follow {
1377
pub fn parse ( s : & str ) -> Option< Follow> {
1378
match s.trim ( ) {
1379
" off" => Some ( Follow:: Off) ,
1380
" green" => Some ( Follow:: Green) ,
1381
" any" => Some ( Follow:: Any) ,
1382
_ => None ,
eb2a3502 Andy McCall 23 days ago ↶
1383
}
1384
}
1385
1386
pub fn as_str ( self ) -> & 'static str {
1387
match self {
1388
Follow:: Off => " off" ,
1389
Follow:: Green => " green" ,
1390
Follow:: Any => " any" ,
e7158350 Andy McCall 23 days ago ↶
1391
}
1392
}
1393
}
1394
1395
1396
1397
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize ) ]
1398
pub struct Rule {
1399
pub follow : Follow,
f4d6ef41 Andy McCall 22 days ago ↶
1400
1401
e7158350 Andy McCall 23 days ago ↶
1402
pub land_auto : bool ,
1403
1404
pub target : Option< String> ,
1405
1406
1407
pub watch : Vec< String> ,
1408
1409
pub from : & 'static str ,
1410
}
1411
1412
1413
1414
pub fn module_keys ( text : & str , path : & str ) -> ( Option< Follow> , Option< bool > , Option< String> , Vec< String> ) {
1415
let ( mut follow, mut land, mut target, mut watch) = ( None , None , None , Vec :: new( ) ) ;
1416
let mut here = false ;
5360cea3 Andy McCall 21 days ago ↶
1417
let mut lines = text.lines ( ) ;
1418
while let Some ( line) = lines.next ( ) {
e7158350 Andy McCall 23 days ago ↶
1419
let line = line.trim ( ) ;
1420
if line.is_empty ( ) | | line.starts_with ( ' #' ) {
1421
continue ;
1422
}
1423
if let Some ( inner) = line.strip_prefix ( ' [' ) .and_then ( | l | l.strip_suffix ( ' ]' ) ) {
1424
here = inner.trim ( ) .trim_matches ( ' "' ) .trim_end_matches ( ' /' ) = = path.trim_end_matches ( ' /' ) ;
1425
continue ;
1426
}
1427
let ( true , Some ( ( k, v) ) ) = ( here, line.split_once ( ' =' ) ) else { continue } ;
5360cea3 Andy McCall 21 days ago ↶
1428
1429
1430
let mut v = v.trim ( ) .to_string ( ) ;
1431
if v.starts_with ( ' [' ) & & ! v.contains ( ' ]' ) {
1432
for more in lines.by_ref ( ) {
1433
let more = more.trim ( ) ;
1434
if more.starts_with ( ' #' ) {
1435
continue ;
1436
}
1437
v.push_str ( more) ;
1438
if more.contains ( ' ]' ) {
1439
break ;
1440
}
1441
}
1442
}
1443
let v = v.as_str ( ) ;
e7158350 Andy McCall 23 days ago ↶
1444
let unquoted = v.trim_matches ( ' "' ) .trim ( ) ;
1445
match k.trim ( ) {
1446
" follow" => follow = Follow:: parse( unquoted) ,
1447
" land" => land = Some ( unquoted = = " auto" ) ,
1448
" target" if ! unquoted.is_empty ( ) => target = Some ( unquoted.to_string ( ) ) ,
1449
" watch" => {
1450
watch = v
1451
.trim_start_matches ( ' [' )
1452
.trim_end_matches ( ' ]' )
1453
.split ( ' ,' )
1454
.map ( | w | w.trim ( ) .trim_matches ( ' "' ) .trim ( ) .to_string ( ) )
1455
.filter ( | w | ! w.is_empty ( ) )
1456
.collect ( )
1457
}
1458
_ => { }
1459
}
1460
}
1461
( follow, land, target, watch)
1462
}
1463
1464
1465
pub fn rule ( site_default : & str , row : & RepoRow, oxymodules : Option< & str > , path : & str ) -> Rule {
1466
let ( follow, land, target, watch) = oxymodules.map ( | t | module_keys ( t, path) ) .unwrap_or_default ( ) ;
1467
let ( follow, from) = match follow {
1468
Some ( f) => ( f, " .oxymodules" ) ,
1469
None => match Follow:: parse( & row.diffusion) {
1470
Some ( f) => ( f, " setting" ) ,
1471
None => ( Follow:: parse( site_default) .unwrap_or ( Follow:: Off) , " site" ) ,
1472
} ,
1473
} ;
f4d6ef41 Andy McCall 22 days ago ↶
1474
1475
let land_auto = land.unwrap_or ( row.diffusion_land = = " auto" ) ;
1476
Rule { follow, land_auto, target, watch, from }
e7158350 Andy McCall 23 days ago ↶
1477
}
1478
1479
# [ cfg ( test ) ]
1480
mod tests {
1481
use super :: * ;
1482
1483
const MODULES : & str = r # "
1484
[rstar-api]
1485
url = https://oxygenhub.io/doomsdayone/rstar-api
1486
follow = green
1487
land = "propose"
1488
target = develop
1489
watch = ["src/x16/irq*", "src/x16/frame*"]
1490
1491
[other]
1492
url = https://example/other
1493
follow = any
1494
"# ;
1495
1496
fn row ( diffusion : & str ) -> RepoRow {
1497
RepoRow {
1498
id: 1 ,
1499
owner: " o" .into ( ) ,
1500
name: " n" .into ( ) ,
1501
description: String :: new( ) ,
1502
default_bookmark: " main" .into ( ) ,
1503
visibility: " public" .into ( ) ,
1504
created_ms: 0 ,
1505
updated_ms: 0 ,
1506
git_mirror: false ,
1507
git_mirror_state: String :: new( ) ,
1508
forked_from: None ,
1509
topics: String :: new( ) ,
1510
flow: String :: new( ) ,
1511
diffusion: diffusion.into ( ) ,
1512
.. Default :: default( )
1513
}
f4d6ef41 Andy McCall 22 days ago ↶
1514
}
1515
1516
# [ test ]
1517
fn landing_by_itself_is_chosen_per_repository_and_per_entry ( ) {
1518
let mut r = row ( " green" ) ;
1519
assert! ( ! rule ( " off" , & r, None , " rstar-api" ) .land_auto, " never by default" ) ;
1520
r.diffusion_land = " auto" .into ( ) ;
1521
assert! ( rule ( " off" , & r, None , " rstar-api" ) .land_auto, " the setting" ) ;
1522
assert! ( ! rule ( " off" , & r, Some ( MODULES ) , " rstar-api" ) .land_auto, " the entry's `land = propose` wins" ) ;
1523
assert! ( rule ( " off" , & r, Some ( MODULES ) , " other" ) .land_auto, " an entry without `land` takes the setting" ) ;
1524
r.diffusion_land = String :: new( ) ;
1525
let auto = MODULES .replace ( r # "land = "propose""# , " land = auto" ) ;
1526
assert! ( rule ( " off" , & r, Some ( & auto) , " rstar-api" ) .land_auto) ;
c4e432da Andy McCall 23 days ago ↶
1527
}
1528
1529
# [ test ]
1530
fn watch_patterns_match_paths ( ) {
1531
assert! ( watched ( " src/x16/irq*" , " src/x16/irq.s" ) ) ;
1532
assert! ( watched ( " src/x16/irq*" , " src/x16/irq_wait.c" ) ) ;
1533
assert! ( ! watched ( " src/x16/irq*" , " src/x16/sub/irq.s" ) , " one segment" ) ;
1534
assert! ( watched ( " src/**/frame*.c" , " src/x16/floor/frame_wait.c" ) ) ;
1535
assert! ( watched ( " src/x16" , " src/x16/bank.s" ) , " a directory" ) ;
1536
assert! ( ! watched ( " src/x16" , " src/x160/bank.s" ) ) ;
1537
assert! ( watched ( " include/r?m.h" , " include/rim.h" ) ) ;
1538
assert! ( ! watched ( " " , " anything" ) ) ;
e7158350 Andy McCall 23 days ago ↶
1539
}
1540
1541
# [ test ]
1542
fn oxymodules_keys_are_read_per_entry ( ) {
1543
let ( f, land, target, watch) = module_keys ( MODULES , " rstar-api" ) ;
1544
assert_eq! ( ( f, land, target.as_deref ( ) ) , ( Some ( Follow:: Green) , Some ( false ) , Some ( " develop" ) ) ) ;
1545
assert_eq! ( watch, vec! [ " src/x16/irq*" , " src/x16/frame*" ] ) ;
1546
assert_eq! ( module_keys ( MODULES , " other" ) .0 , Some ( Follow:: Any) ) ;
1547
assert_eq! ( module_keys ( MODULES , " missing" ) , ( None , None , None , vec! [ ] ) ) ;
1548
assert_eq! ( module_keys ( " [a]\n watch = x, y\n " , " a" ) .3 , vec! [ " x" , " y" ] ) ;
5360cea3 Andy McCall 21 days ago ↶
1549
1550
1551
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 " ;
1552
let ( f, _ , _ , watch) = module_keys ( multi, " rstar-api" ) ;
1553
assert_eq! ( watch, vec! [ " src/x16/rbm*" , " src/x16/rdm.c" , " src/mega65/rsm.c" ] ) ;
1554
assert_eq! ( f, Some ( Follow:: Green) , " the key after the list is still read" ) ;
e7158350 Andy McCall 23 days ago ↶
1555
}
1556
1557
# [ test ]
1558
fn oxymodules_wins_then_the_setting_then_the_site ( ) {
1559
let r = rule ( " off" , & row ( " any" ) , Some ( MODULES ) , " rstar-api" ) ;
1560
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " .oxymodules" ) ) ;
1561
let r = rule ( " off" , & row ( " any" ) , Some ( " [rstar-api]\n url = x\n " ) , " rstar-api" ) ;
1562
assert_eq! ( ( r.follow, r.from) , ( Follow:: Any, " setting" ) ) ;
1563
let r = rule ( " green" , & row ( " " ) , None , " rstar-api" ) ;
1564
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " site" ) ) ;
1565
assert_eq! ( rule ( " nonsense" , & row ( " " ) , None , " x" ) .follow, Follow:: Off) ;
62cdf34d Andy McCall 20 days ago ↶
1566
}
1567
}
1568
1569
# [ cfg ( test ) ]
1570
mod message_tests {
1571
use super :: bump_message;
1572
1573
# [ test ]
1574
fn a_bump_of_many_heads_has_a_short_title_and_the_whole_address_below ( ) {
1575
let heads: Vec< String> = ( 0 .. 20 ) .map ( | i | format! ( " {i:02x} a6c9d4746a" ) ) .collect ( ) ;
1576
let m = bump_message ( " rstar-api" , " doomsdayone/rstar-api" , & heads) ;
1577
let ( title, body) = m.split_once ( " \n \n " ) .unwrap ( ) ;
1578
assert_eq! ( title, " Diffusion: move rstar-api to doomsdayone/rstar-api 00a6c9d4746a (+19 heads)" ) ;
1579
assert_eq! ( body, format! ( " Pins doomsdayone/rstar-api at {} " , heads.join ( " +" ) ) ) ;
1580
assert_eq! ( bump_message ( " api" , " andy/api" , & heads[ .. 1 ] ) , " Diffusion: move api to andy/api 00a6c9d4746a" ) ;
3e868a1f Andy McCall 10 days ago ↶
1581
}
1582
}
1583
1584
# [ cfg ( test ) ]
1585
mod pin_tests {
1586
use super :: pin_bookmark;
1587
use oxy_core:: hash:: ObjectRef;
1588
1589
# [ test ]
1590
fn a_bump_pins_the_upstreams_default_bookmark_unless_it_keeps_other_changes ( ) {
1591
let ( a, b) = ( ObjectRef( [ 1 ; 32 ] ) , ObjectRef( [ 2 ; 32 ] ) ) ;
1592
1593
1594
assert_eq! ( pin_bookmark ( & [ a] , & [ a] , " main" , " x16-tilemap-double" .into ( ) ) , " main" ) ;
1595
assert_eq! ( pin_bookmark ( & [ b, a] , & [ a, b] , " main" , " feature" .into ( ) ) , " main" , " in any order" ) ;
1596
1597
1598
assert_eq! ( pin_bookmark ( & [ a, b] , & [ a] , " main" , " feature" .into ( ) ) , " feature" ) ;
e7158350 Andy McCall 23 days ago ↶
1599
}
1600
}