f677d627 Andy McCall 23 days ago ↶
1
2
3
4
5
6
7
8
9
10
use crate :: auth:: roles;
11
use crate :: db:: { self , accounts:: Account, repos:: RepoRow, Pool} ;
12
use crate :: repos:: RepoName;
13
use crate :: Shared;
14
use oxy_core:: hash:: ChangeHash;
15
use serde:: Serialize;
16
17
18
pub const LEARN_DAYS : i64 = 90 ;
19
20
21
22
23
pub async fn target ( pool : & Pool, row : & RepoRow) -> String {
24
let since = db:: now_ms( ) - LEARN_DAYS * 24 * 3600 * 1000 ;
25
let learned: Option< String> = sqlx:: query_scalar(
26
" SELECT target FROM proposals WHERE repo_id = ? AND state = 'landed' AND landed_ms >= ?
27
GROUP BY target ORDER BY COUNT(*) DESC, MAX(landed_ms) DESC LIMIT 1" ,
28
)
29
.bind ( row.id)
30
.bind ( since)
31
.fetch_optional ( pool)
32
.await
33
.ok ( )
34
.flatten ( ) ;
35
learned.filter ( | t | ! t.is_empty ( ) ) .unwrap_or_else ( | | row.default_bookmark.clone ( ) )
36
}
37
38
39
40
pub async fn followed_bookmarks ( pool : & Pool, row : & RepoRow) -> Vec< String> {
41
let mut out = vec! [ row.default_bookmark.clone ( ) ] ;
42
let t = target ( pool, row) .await;
43
if ! out.contains ( & t) {
44
out.push ( t) ;
45
}
46
out
47
}
48
49
50
pub async fn record ( app : & Shared, row : & RepoRow, bookmark : & str , heads : & [ChangeHash]) {
51
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { return } ;
52
let pins = if heads.is_empty ( ) { Vec :: new( ) } else { crate :: flow:: pins_in( app, name, heads.to_vec ( ) ) .await } ;
53
let now = db:: now_ms( ) ;
54
let Ok ( mut tx) = app.db.begin ( ) .await else { return } ;
55
let mut ok = sqlx:: query( " DELETE FROM diffusion_pins WHERE consumer_id = ? AND bookmark = ?" )
56
.bind ( row.id)
57
.bind ( bookmark)
58
.execute ( & mut * tx)
59
.await
60
.is_ok ( ) ;
61
for ( path, id, pinned) in & pins {
62
let heads: Vec< String> = pinned.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ;
63
ok & =
64
sqlx:: query( " INSERT INTO diffusion_pins (consumer_id, bookmark, path, upstream, heads, updated_ms) VALUES (?, ?, ?, ?, ?, ?)" )
65
.bind ( row.id)
66
.bind ( bookmark)
67
.bind ( path)
68
.bind ( hex:: encode( id) )
69
.bind ( heads.join ( " +" ) )
70
.bind ( now)
71
.execute ( & mut * tx)
72
.await
73
.is_ok ( ) ;
74
}
75
if ok {
76
tx.commit ( ) .await.ok ( ) ;
77
}
78
}
79
80
81
pub async fn after_push ( app : & Shared, row : & RepoRow, moved : & [( String , String , Vec <ChangeHash >) ]) {
82
let followed = followed_bookmarks ( & app.db, row) .await;
83
for ( bookmark, _ , heads) in moved {
84
if followed.contains ( bookmark) {
85
record ( app, row, bookmark, heads) .await;
86
}
87
}
88
}
89
90
91
92
pub async fn backfill ( app : & Shared) {
93
let Ok ( rows) = db:: repos:: list( & app.db) .await else { return } ;
94
for row in rows {
95
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { continue } ;
96
for bookmark in followed_bookmarks ( & app.db, & row) .await {
97
let b = bookmark.clone ( ) ;
98
let heads = app.repos.read ( & name, move | r, _ | heads_of ( r, & b) ) .await;
99
if let Ok ( heads) = heads {
100
record ( app, & row, & bookmark, & heads) .await;
101
}
102
}
103
}
104
}
105
106
107
108
fn heads_of ( r : & oxy_core:: repo:: Repo, bookmark : & str ) -> Result< Vec< ChangeHash> , crate:: repos:: RepoError> {
109
Ok ( r.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) )
110
}
111
112
113
# [ derive ( Clone, Debug, Serialize ) ]
114
pub struct Consumer {
115
pub owner : String,
116
pub name : String,
117
pub bookmark : String,
118
pub path : String,
119
120
pub pinned : Vec< String> ,
121
122
123
pub behind : Option< usize > ,
124
}
125
126
127
128
pub async fn consumers ( app : & Shared, row : & RepoRow, account : Option< & Account> ) -> Result< Vec< Consumer> , crate:: repos:: RepoError> {
129
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { return Ok ( Vec :: new( ) ) } ;
130
let id = app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await? ;
131
let found: Vec< ( i64 , String, String, String) > = sqlx:: query_as(
132
" SELECT consumer_id, bookmark, path, heads FROM diffusion_pins WHERE upstream = ? ORDER BY consumer_id, bookmark, path" ,
133
)
134
.bind ( hex:: encode( id) )
135
.fetch_all ( & app.db)
136
.await
137
.map_err ( | e | crate :: repos:: RepoError:: Io( std:: io:: Error:: other( e) ) ) ? ;
138
let mut out = Vec :: new( ) ;
139
let mut pinned_sets = Vec :: new( ) ;
140
for ( consumer_id, bookmark, path, heads) in found {
141
let Ok ( Some ( c) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
142
if ! roles:: allows( roles:: role_for( & app.db, account, & c) .await.ok ( ) .flatten ( ) , crate :: auth:: Access:: Read) {
143
continue ;
144
}
145
let pinned: Vec< String> = heads.split ( ' +' ) .filter ( | h | ! h.is_empty ( ) ) .map ( String :: from) .collect ( ) ;
146
pinned_sets.push ( pinned.iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect:: < Vec< _ > > ( ) ) ;
147
out.push ( Consumer { owner: c.owner, name: c.name, bookmark, path, pinned, behind: None } ) ;
148
}
149
if out.is_empty ( ) {
150
return Ok ( out) ;
151
}
152
let default = row.default_bookmark.clone ( ) ;
153
let behind = app
154
.repos
155
.read ( & name, move | r, cache| {
156
let tip = heads_of ( r, & default) ? ;
157
let tip = cache.summary ( r, & tip) ? ;
158
Ok ( pinned_sets
159
.iter ( )
160
.map ( | p | cache.summary ( r, p) .ok ( ) .map ( | s | tip.changes.iter ( ) .filter ( | c | ! s.changes.contains ( * c) ) .count ( ) ) )
161
.collect:: < Vec< _ > > ( ) )
162
} )
163
.await? ;
164
for ( c, b) in out.iter_mut ( ) .zip ( behind) {
165
c.behind = b;
166
}
167
Ok ( out)
168
}
e7158350 Andy McCall 23 days ago ↶
169
1d634f69 Andy McCall 23 days ago ↶
170
171
pub fn bump_bookmark ( path : & str ) -> String {
172
format! ( " diffusion/{} " , path.trim_matches ( ' /' ) )
173
}
174
175
176
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
177
pub enum Bumped {
178
f01cb865 Andy McCall 23 days ago ↶
179
Moved { bookmark: String , change: ChangeHash, heads: Vec< ChangeHash> , from: Vec< ChangeHash> , state: String } ,
1d634f69 Andy McCall 23 days ago ↶
180
181
UpToDate,
182
9aaa88b2 Andy McCall 22 days ago ↶
183
Already { from: Vec< ChangeHash> , to: Vec< ChangeHash> } ,
7a14bdae Andy McCall 23 days ago ↶
184
185
Unknown,
186
187
188
Conflicts,
1d634f69 Andy McCall 23 days ago ↶
189
190
191
NoPin,
192
}
193
7a14bdae Andy McCall 23 days ago ↶
194
195
196
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
197
pub enum Plan {
198
199
To( Vec< ChangeHash> , usize ) ,
200
201
UpToDate,
202
203
204
Unknown,
205
206
Conflicts,
207
}
208
209
210
211
212
213
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 ↶
214
app.repos
215
.read ( upstream, move | r, cache| {
7a14bdae Andy McCall 23 days ago ↶
216
let Ok ( o) = cache.summary ( r, & old) else { return Ok ( Plan:: Unknown) } ;
217
let n = cache.summary ( r, & new) ? ;
218
if o.changes.iter ( ) .all ( | c | n.changes.contains ( c) ) {
219
let added = n.changes.len ( ) - o.changes.len ( ) ;
220
return Ok ( if added = = 0 { Plan:: UpToDate } else { Plan:: To( new, added) } ) ;
221
}
222
let mut both = new.clone ( ) ;
223
both.extend ( old.iter ( ) .copied ( ) ) ;
224
let heads = r.load_state ( & both) ? .heads ( ) ;
225
let b = cache.summary ( r, & heads) ? ;
226
if b.conflicted & & ! o.conflicted {
227
return Ok ( Plan:: Conflicts) ;
1d634f69 Andy McCall 23 days ago ↶
228
}
7a14bdae Andy McCall 23 days ago ↶
229
let added = b.changes.len ( ) - o.changes.len ( ) ;
230
Ok ( if added = = 0 { Plan:: UpToDate } else { Plan:: To( heads, added) } )
1d634f69 Andy McCall 23 days ago ↶
231
} )
232
.await
233
}
234
235
236
237
238
pub async fn bump (
239
app : & Shared,
240
consumer : & RepoRow,
241
target : & str ,
242
path : & str ,
243
upstream : & RepoRow,
244
new_heads : Vec< ChangeHash> ,
245
) -> Result< Bumped, crate:: repos:: RepoError> {
246
use oxy_core:: identity:: Domain;
247
use oxy_core:: subrepo:: Pin;
248
let ( Some ( cname) , Some ( uname) ) = ( RepoName:: new( & consumer.owner, & consumer.name) , RepoName:: new( & upstream.owner, & upstream.name) )
249
else {
250
return Ok ( Bumped:: NoPin) ;
251
} ;
252
let upstream_id = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await? ;
253
254
255
let ( t, p) = ( target.to_string ( ) , path.trim_matches ( ' /' ) .to_string ( ) ) ;
256
let ( current, bumped) = app
257
.repos
258
.read ( & cname, move | r, cache| {
259
let pin_on = | bookmark : & str | -> Result< Option< Pin> , crate:: repos:: RepoError> {
260
let heads = heads_of ( r, bookmark) ? ;
261
if heads.is_empty ( ) {
262
return Ok ( None ) ;
263
}
264
let loaded = cache.load ( r, & heads) ? ;
265
let Some ( f) = loaded.snapshot.files.get ( & p) else { return Ok ( None ) } ;
266
let oxy_core:: state:: FileContent:: Opaque { blobs } = & f.content else { return Ok ( None ) } ;
267
let Some ( b) = blobs.first ( ) else { return Ok ( None ) } ;
268
Ok ( Pin:: decode( & r.read_blob ( b) ? ) .ok ( ) )
269
} ;
270
Ok ( ( pin_on ( & t) ? , pin_on ( & bump_bookmark ( & p) ) ? ) )
271
} )
272
.await? ;
273
let Some ( Pin:: Oxy { repo, heads: old, bookmark } ) = current else { return Ok ( Bumped:: NoPin) } ;
7a14bdae Andy McCall 23 days ago ↶
274
if repo ! = upstream_id {
275
return Ok ( Bumped:: NoPin) ;
276
}
277
let new_heads = match plan ( app, & uname, old.clone ( ) , new_heads.clone ( ) ) .await? {
278
Plan:: To( heads, _ ) => heads,
279
Plan:: UpToDate => return Ok ( Bumped:: UpToDate) ,
280
Plan:: Unknown => return Ok ( Bumped:: Unknown) ,
281
Plan:: Conflicts => return Ok ( Bumped:: Conflicts) ,
282
} ;
1d634f69 Andy McCall 23 days ago ↶
283
if let Some ( Pin:: Oxy { heads: b, .. } ) = & bumped {
284
let ( mut b, mut n) = ( b.clone ( ) , new_heads.clone ( ) ) ;
285
b.sort ( ) ;
286
n.sort ( ) ;
287
if b = = n {
9aaa88b2 Andy McCall 22 days ago ↶
288
return Ok ( Bumped:: Already { from: old, to: new_heads } ) ;
1d634f69 Andy McCall 23 days ago ↶
289
}
290
}
291
let ( key, secret) =
292
crate :: keys:: load_secret( & app.config.keys_dir ( ) ) .map_err ( | e | crate :: repos:: RepoError:: Io( std:: io:: Error:: other( e.to_string ( ) ) ) ) ? ;
293
let identity = app.hub_key.identity.clone ( ) ;
294
let upstream_label = format! ( " {} /{} " , upstream.owner, upstream.name) ;
295
let ( t, p) = ( target.to_string ( ) , path.trim_matches ( ' /' ) .to_string ( ) ) ;
296
let pin = Pin:: Oxy { repo, heads: new_heads.clone ( ) , bookmark } ;
a7e6e8e6 Andy McCall 23 days ago ↶
297
let old = old.clone ( ) ;
1d634f69 Andy McCall 23 days ago ↶
298
let short: Vec< String> = new_heads.iter ( ) .map ( | h | h.to_hex ( ) [ .. 12 ] .to_string ( ) ) .collect ( ) ;
299
let message = format! ( " Diffusion: move {p} to {upstream_label} {} " , short.join ( " +" ) ) ;
300
app.repos
301
.write ( & cname, move | r, _ | {
302
let base = heads_of ( r, & t) ? ;
303
let change = r.build_pin_change ( & base, & p, & pin, & message, key) ? ;
304
let hash = change.hash ( ) ;
305
let sig = secret.sign ( Domain:: Change, & key, & hash) ;
306
let mut state = r.load_state ( & base) ? ;
307
state.add ( change.clone ( ) ) .map_err ( | e | crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( e.to_string ( ) ) ) ) ? ;
308
let mut candidates = base.clone ( ) ;
309
candidates.push ( hash) ;
310
let heads = state.maximal ( & candidates) ;
311
let bookmark = bump_bookmark ( & p) ;
312
313
314
let mut update = r.host_ref_update ( & bookmark, & heads, Some ( ( & key, & secret) ) ) ? ;
315
update.force = true ;
f01cb865 Andy McCall 23 days ago ↶
316
let state = update.new_state.to_hex ( ) ;
1d634f69 Andy McCall 23 days ago ↶
317
let report = r.apply_hosted ( Some ( ( change, vec! [ sig] ) ) , & [ identity] , update) ? ;
318
if let Some ( refused) = report.results.iter ( ) .find ( | x | ! x.ok) {
319
return Err ( crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( format! (
320
" the bump was refused: {} " ,
321
refused.reason
322
) ) ) ) ;
323
}
f01cb865 Andy McCall 23 days ago ↶
324
Ok ( Bumped:: Moved { bookmark, change: hash, heads, from: old, state } )
1d634f69 Andy McCall 23 days ago ↶
325
} )
326
.await
327
}
328
a7e6e8e6 Andy McCall 23 days ago ↶
329
330
pub const ACCOUNT : & str = " diffusion" ;
331
332
333
pub async fn account ( pool : & Pool) -> Option< Account> {
334
if let Ok ( Some ( a) ) = db:: accounts:: account_by_name( pool, ACCOUNT ) .await {
335
return Some ( a) ;
336
}
337
db:: accounts:: create_account( pool, ACCOUNT , " diffusion@localhost" , " !" , " " , false ) .await.ok ( )
338
}
339
c4e432da Andy McCall 22 days ago ↶
340
341
342
343
344
pub fn watched ( pattern : & str , path : & str ) -> bool {
345
fn m ( p : & [u8 ], s : & [u8 ]) -> bool {
346
match ( p.first ( ) , s.first ( ) ) {
347
( None , None ) => true ,
348
( Some ( b ' *' ) , _ ) if p.get ( 1 ) = = Some ( & b ' *' ) => ( 0 .. = s.len ( ) ) .any ( | i | m ( & p[ 2 .. ] , & s[ i.. ] ) ) ,
349
( Some ( b ' *' ) , _ ) => ( 0 .. = s.len ( ) ) .take_while ( | & i | i = = 0 | | s[ i - 1 ] ! = b ' /' ) .any ( | i | m ( & p[ 1 .. ] , & s[ i.. ] ) ) ,
350
( Some ( b ' ?' ) , Some ( c) ) if * c ! = b ' /' => m ( & p[ 1 .. ] , & s[ 1 .. ] ) ,
351
( Some ( a) , Some ( b) ) if a = = b => m ( & p[ 1 .. ] , & s[ 1 .. ] ) ,
352
_ => false ,
353
}
354
}
355
let p = pattern.trim ( ) .trim_matches ( ' /' ) ;
356
if p.is_empty ( ) {
357
return false ;
358
}
359
m ( p.as_bytes ( ) , path.as_bytes ( ) ) | | ( ! p.contains ( [ ' *' , ' ?' ] ) & & path.starts_with ( & format! ( " {p} /" ) ) )
360
}
361
362
363
364
365
pub struct Changelog {
366
pub lines : Vec< String> ,
367
pub watched : Vec< String> ,
368
}
369
370
pub async fn changelog ( app : & Shared, upstream : & RepoRow, old : Vec< ChangeHash> , new : Vec< ChangeHash> , watch : Vec< String> ) -> Changelog {
371
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { return Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } } ;
a7e6e8e6 Andy McCall 23 days ago ↶
372
let linked: std:: collections:: HashMap< String, String> =
373
db:: accounts:: identity_accounts( & app.db) .await.unwrap_or_default ( ) .into_iter ( ) .collect ( ) ;
374
let base = format! ( " /{} /{} " , upstream.owner, upstream.name) ;
375
app.repos
376
.read ( & name, move | r, cache| {
377
let ( o, n) = ( cache.summary ( r, & old) ? , cache.summary ( r, & new) ? ) ;
c4e432da Andy McCall 22 days ago ↶
378
let mut added: Vec< ( u64 , ChangeHash, String, String, Vec< String> ) > = Vec :: new( ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
379
for h in n.changes.iter ( ) .filter ( | c | ! o.changes.contains ( * c) ) {
380
let Ok ( c) = r.store ( ) .get_change ( h) else { continue } ;
381
let key = c.author.to_hex ( ) ;
382
let who = linked.get ( & key) .cloned ( ) .unwrap_or_else ( | | key[ .. 12 ] .to_string ( ) ) ;
383
let first = c.message.lines ( ) .next ( ) .unwrap_or ( " " ) .trim ( ) .to_string ( ) ;
c4e432da Andy McCall 22 days ago ↶
384
385
let hits: Vec< String> = if watch.is_empty ( ) {
386
Vec :: new( )
387
} else {
388
crate :: repos:: view:: change( r, cache, h)
389
.map ( | d | d.files.into_iter ( ) .map ( | f | f.path ) .filter ( | p | watch.iter ( ) .any ( | w | watched ( w, p) ) ) .collect ( ) )
390
.unwrap_or_default ( )
391
} ;
392
added.push ( ( c.hlc.wall_ms, * h, first, who, hits) ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
393
}
394
added.sort_by_key ( | a | std:: cmp:: Reverse( a.0 ) ) ;
c4e432da Andy McCall 22 days ago ↶
395
let line = | h : & ChangeHash, msg : & str , who : & str , flag : bool | {
396
format! (
397
" - {} [`{} `]({base} /change/{} ) {} ({who} )" ,
398
if flag { " ⚑ " } else { " " } ,
399
& h.to_hex ( ) [ .. 12 ] ,
400
h.to_hex ( ) ,
401
msg.replace ( ' \n ' , " " )
402
)
403
} ;
404
let watched = added
405
.iter ( )
406
.filter ( | a | ! a.4. is_empty ( ) )
407
.map ( | ( _, h , msg , who , hits ) | {
408
format! (
409
" {} — touches {} " ,
410
line ( h, msg, who, true ) ,
411
hits.iter ( ) .map ( | p | format! ( " `{p} `" ) ) .collect:: < Vec< _ > > ( ) .join ( " , " )
412
)
a7e6e8e6 Andy McCall 23 days ago ↶
413
} )
c4e432da Andy McCall 22 days ago ↶
414
.collect ( ) ;
415
let lines = added.iter ( ) .map ( | ( _, h , msg , who , hits ) | line ( h, msg, who, ! hits.is_empty ( ) ) ) .collect ( ) ;
416
Ok ( Changelog { lines, watched } )
a7e6e8e6 Andy McCall 23 days ago ↶
417
} )
418
.await
c4e432da Andy McCall 22 days ago ↶
419
.unwrap_or ( Changelog { lines: Vec :: new( ) , watched: Vec :: new( ) } )
a7e6e8e6 Andy McCall 23 days ago ↶
420
}
421
9aaa88b2 Andy McCall 22 days ago ↶
422
423
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
424
pub enum Why {
425
426
Green,
427
428
Moved,
429
430
431
Unverified,
432
}
433
434
435
436
437
a7e6e8e6 Andy McCall 23 days ago ↶
438
pub async fn propose (
439
app : & Shared,
440
consumer : & RepoRow,
441
target : & str ,
442
path : & str ,
443
upstream : & RepoRow,
9aaa88b2 Andy McCall 22 days ago ↶
444
( from , to ) : ( Vec< ChangeHash> , Vec< ChangeHash> ) ,
c4e432da Andy McCall 22 days ago ↶
445
( why , watch ) : ( Why, Vec< String> ) ,
a7e6e8e6 Andy McCall 23 days ago ↶
446
) -> Option< i64 > {
447
use crate :: db:: proposals as pdb;
448
let bookmark = bump_bookmark ( path) ;
449
let bookmark = bookmark.as_str ( ) ;
450
let me = account ( & app.db) .await? ;
c4e432da Andy McCall 22 days ago ↶
451
let log = changelog ( app, upstream, from, to.clone ( ) , watch.clone ( ) ) .await;
452
let lines = log.lines;
453
let flagged = if log.watched.is_empty ( ) {
454
String :: new( )
455
} else {
456
format! (
457
" \n \n **Touches what you watch** ({} ):\n \n {} " ,
458
watch.iter ( ) .map ( | w | format! ( " `{w} `" ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ,
459
log.watched.join ( " \n " )
460
)
461
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
462
let slug = format! ( " {} /{} " , upstream.owner, upstream.name) ;
463
let short: Vec< String> = to.iter ( ) .map ( | h | h.to_hex ( ) [ .. 12 ] .to_string ( ) ) .collect ( ) ;
1b6536f0 Andy McCall 23 days ago ↶
464
let ub = & upstream.default_bookmark;
465
let count = if lines.len ( ) = = 1 { " 1 change" .to_string ( ) } else { format! ( " {} changes" , lines.len ( ) ) } ;
466
let title = format! ( " Diffusion: {path} to {slug} {ub} ({count} )" ) ;
9aaa88b2 Andy McCall 22 days ago ↶
467
let when = match why {
468
Why:: Green => " at its newest state whose checks passed" ,
469
Why:: Moved => " as it is now" ,
470
Why:: Unverified => " as it is now" ,
471
} ;
472
let amber = if why = = Why:: Unverified {
473
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." )
474
} else {
475
String :: new( )
476
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
477
let body = format! (
c4e432da Andy McCall 22 days ago ↶
478
" Moves the subrepo `{path} ` to [{slug} ](/{slug} )'s `{ub} `, {when} (`{} `). Only the pin changes; this repository's own checks test it.{amber} {flagged} \n \n **What it brings** ({count} ):\n \n {} \n \n Opened and kept up to date by Diffusion (docs/diffusion.md). It never lands by itself." ,
1b6536f0 Andy McCall 23 days ago ↶
479
short.join ( " +" ) ,
a7e6e8e6 Andy McCall 23 days ago ↶
480
lines.join ( " \n " )
481
) ;
482
let open = pdb:: list( & app.db, consumer.id, & [ " open" , " draft" ] ) .await.ok ( ) ? .into_iter ( ) .find ( | p | p.source_bookmark = = bookmark ) ;
9aaa88b2 Andy McCall 22 days ago ↶
483
let ( number, id) = match open {
a7e6e8e6 Andy McCall 23 days ago ↶
484
Some ( p) => {
9aaa88b2 Andy McCall 22 days ago ↶
485
if p.title ! = title | | p.body ! = body {
486
pdb:: edit( & app.db, p.id, & title, & body, target) .await.ok ( ) ? ;
487
pdb:: add_event( & app.db, p.id, ACCOUNT , " updated" , & short.join ( " +" ) ) .await.ok ( ) ;
488
}
489
( p.number, p.id)
a7e6e8e6 Andy McCall 23 days ago ↶
490
}
491
None => {
492
let p = pdb:: create(
493
& app.db,
494
pdb:: NewProposal {
495
repo_id: consumer.id,
496
title: & title,
497
body: & body,
498
author_id: Some ( me.id) ,
499
author: ACCOUNT ,
500
draft: false ,
501
target,
502
source_kind: " bookmark" ,
503
source_repo_id: None ,
504
source_bookmark: bookmark,
505
source_changes: " " ,
506
} ,
507
)
508
.await
509
.ok ( ) ? ;
510
pdb:: add_event( & app.db, p.id, ACCOUNT , " opened" , " " ) .await.ok ( ) ;
511
db:: notify:: index( & app.db, " proposal" , consumer.id, p.id, p.number, & p.title, & p.body) .await.ok ( ) ;
512
if let Ok ( label) =
513
db:: issues:: upsert_label( & app.db, consumer.id, " dependencies" , " #0072ff" , " A subrepo moves (Diffusion)" ) .await
514
{
515
db:: issues:: set_proposal_label( & app.db, p.id, label.id, true ) .await.ok ( ) ;
516
}
9aaa88b2 Andy McCall 22 days ago ↶
517
( p.number, p.id)
a7e6e8e6 Andy McCall 23 days ago ↶
518
}
519
} ;
9aaa88b2 Andy McCall 22 days ago ↶
520
521
let unverified = db:: issues:: label_by_name( & app.db, consumer.id, " upstream unverified" ) .await.ok ( ) .flatten ( ) ;
522
match ( why = = Why:: Unverified, unverified) {
523
( true , l) => {
524
let l = match l {
525
Some ( l) => Some ( l) ,
526
None => db:: issues:: upsert_label(
527
& app.db,
528
consumer.id,
529
" upstream unverified" ,
530
" #e3a008" ,
531
" The upstream has no checks (Diffusion)" ,
532
)
533
.await
534
.ok ( ) ,
535
} ;
536
if let Some ( l) = l {
537
db:: issues:: set_proposal_label( & app.db, id, l.id, true ) .await.ok ( ) ;
538
}
539
}
540
( false , Some ( l) ) => {
541
db:: issues:: set_proposal_label( & app.db, id, l.id, false ) .await.ok ( ) ;
542
}
543
( false , None ) => { }
544
}
a7e6e8e6 Andy McCall 23 days ago ↶
545
Some ( number)
546
}
547
f01cb865 Andy McCall 23 days ago ↶
548
549
550
551
fn start_runs (
552
app : Shared,
553
row : RepoRow,
554
bookmark : String,
555
state : String,
556
heads : Vec< ChangeHash> ,
557
) -> std:: pin:: Pin< Box< dyn std:: future:: Future< Output = ( ) > + Send> > {
558
Box :: pin( async move { crate :: flow:: on_push( & app, & row, & bookmark, & state, & heads, ACCOUNT ) .await } )
559
}
560
a7e6e8e6 Andy McCall 23 days ago ↶
561
562
async fn oxymodules ( app : & Shared, name : & RepoName, bookmark : & str ) -> Option< String> {
563
let b = bookmark.to_string ( ) ;
564
app.repos
565
.read ( name, move | r, cache| {
566
let heads = heads_of ( r, & b) ? ;
567
if heads.is_empty ( ) {
568
return Ok ( None ) ;
569
}
570
let loaded = cache.load ( r, & heads) ? ;
571
Ok ( match loaded.snapshot.files.get ( " .oxymodules" ) .map ( | f | & f.content) {
572
Some ( oxy_core:: state:: FileContent:: Text { bytes, .. } ) => Some ( String :: from_utf8_lossy( bytes) .into_owned ( ) ) ,
573
_ => None ,
574
} )
575
} )
576
.await
577
.ok ( )
578
.flatten ( )
579
}
580
581
582
583
584
pub async fn propagate ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool ) {
3776b70d Andy McCall 23 days ago ↶
585
propagate_to ( app, upstream, heads, green, None ) .await
586
}
587
588
589
async fn propagate_to ( app : & Shared, upstream : & RepoRow, heads : Vec< ChangeHash> , green : bool , only : Option< i64 > ) {
a7e6e8e6 Andy McCall 23 days ago ↶
590
let Some ( uname) = RepoName:: new( & upstream.owner, & upstream.name) else { return } ;
591
let Ok ( id) = app.repos.read ( & uname, | r , _| Ok ( r.id ( ) ? ) ) .await else { return } ;
592
let rows: Vec< ( i64 , String, String) > = sqlx:: query_as( " SELECT consumer_id, bookmark, path FROM diffusion_pins WHERE upstream = ?" )
593
.bind ( hex:: encode( id) )
594
.fetch_all ( & app.db)
595
.await
596
.unwrap_or_default ( ) ;
9aaa88b2 Andy McCall 22 days ago ↶
597
let unverified = unverified ( app, upstream) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
598
for ( consumer_id, indexed, path) in rows {
3776b70d Andy McCall 23 days ago ↶
599
if only.is_some_and ( | o | o ! = consumer_id ) {
600
continue ;
601
}
a7e6e8e6 Andy McCall 23 days ago ↶
602
let Ok ( Some ( consumer) ) = db:: repos:: get_by_id( & app.db, consumer_id) .await else { continue } ;
603
let Some ( cname) = RepoName:: new( & consumer.owner, & consumer.name) else { continue } ;
604
let learned = target ( & app.db, & consumer) .await;
605
let modules = oxymodules ( app, & cname, & learned) .await;
606
let r = rule ( & app.config.diffusion.follow, & consumer, modules.as_deref ( ) , & path) ;
607
let target = r.target.clone ( ) .unwrap_or ( learned) ;
608
if indexed ! = target {
609
continue ;
610
}
9aaa88b2 Andy McCall 22 days ago ↶
611
612
a7e6e8e6 Andy McCall 23 days ago ↶
613
let go = match r.follow {
614
Follow:: Off => false ,
9aaa88b2 Andy McCall 22 days ago ↶
615
Follow:: Green => green | | unverified,
a7e6e8e6 Andy McCall 23 days ago ↶
616
Follow:: Any => ! green,
617
} ;
9aaa88b2 Andy McCall 22 days ago ↶
618
let why = if green {
619
Why:: Green
620
} else if unverified {
621
Why:: Unverified
622
} else {
623
Why:: Moved
624
} ;
a7e6e8e6 Andy McCall 23 days ago ↶
625
if ! go {
626
continue ;
627
}
628
match bump ( app, & consumer, & target, & path, upstream, heads.clone ( ) ) .await {
f01cb865 Andy McCall 23 days ago ↶
629
Ok ( Bumped:: Moved { from, bookmark, heads: bumped, state, .. } ) => {
630
631
tokio:: spawn( start_runs ( app.clone ( ) , consumer.clone ( ) , bookmark, state, bumped) ) ;
c4e432da Andy McCall 22 days ago ↶
632
let n = propose ( app, & consumer, & target, & path, upstream, ( from, heads.clone ( ) ) , ( why, r.watch.clone ( ) ) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
633
tracing:: info! (
634
" diffusion: {}/{} {path} bumped to {}/{} (proposal {n:?})" ,
635
consumer.owner,
636
consumer.name,
637
upstream.owner,
638
upstream.name
639
) ;
9aaa88b2 Andy McCall 22 days ago ↶
640
}
641
Ok ( Bumped:: Already { from, to } ) => {
642
643
c4e432da Andy McCall 22 days ago ↶
644
propose ( app, & consumer, & target, & path, upstream, ( from, to) , ( why, r.watch.clone ( ) ) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
645
}
7a14bdae Andy McCall 23 days ago ↶
646
Ok ( other) => tracing:: info! ( " diffusion: {}/{} {path}: no bump ({other:?})" , consumer.owner, consumer.name) ,
a7e6e8e6 Andy McCall 23 days ago ↶
647
Err ( e) => tracing:: warn! ( " diffusion: bump {}/{} {path}: {e}" , consumer.owner, consumer.name) ,
3776b70d Andy McCall 23 days ago ↶
648
}
649
}
9aaa88b2 Andy McCall 22 days ago ↶
650
}
651
652
653
654
655
pub async fn unverified ( app : & Shared, upstream : & RepoRow) -> bool {
656
if ! crate :: flow:: enabled( app, upstream) {
657
return true ;
658
}
659
let runs: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM flow_runs WHERE repo_id = ? AND bookmark = ?" )
660
.bind ( upstream.id)
661
.bind ( & upstream.default_bookmark)
662
.fetch_one ( & app.db)
663
.await
664
.unwrap_or ( 0 ) ;
665
runs = = 0
3776b70d Andy McCall 23 days ago ↶
666
}
667
668
669
async fn repo_by_oxy_id ( app : & Shared, id : & str ) -> Option< RepoRow> {
670
for row in db:: repos:: list( & app.db) .await.ok ( ) ? {
671
let Some ( name) = RepoName:: new( & row.owner, & row.name) else { continue } ;
672
if app.repos.read ( & name, | r , _| Ok ( r.id ( ) ? ) ) .await.ok ( ) .map ( hex:: encode) .as_deref ( ) = = Some ( id) {
673
return Some ( row) ;
674
}
675
}
676
None
677
}
678
679
680
async fn last_green ( app : & Shared, upstream : & RepoRow) -> Option< Vec< ChangeHash> > {
681
let heads: String =
682
sqlx:: query_scalar( " SELECT heads FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND status = 'passed' ORDER BY id DESC LIMIT 1" )
683
.bind ( upstream.id)
684
.bind ( & upstream.default_bookmark)
685
.fetch_optional ( & app.db)
686
.await
687
.ok ( )
688
.flatten ( ) ? ;
689
let h: Vec< ChangeHash> = heads.split ( ' +' ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
690
( ! h.is_empty ( ) ) .then_some ( h)
691
}
692
693
694
695
696
pub async fn catch_up ( app : & Shared, consumer : & RepoRow) {
697
let upstreams: Vec< String> = sqlx:: query_scalar( " SELECT DISTINCT upstream FROM diffusion_pins WHERE consumer_id = ?" )
698
.bind ( consumer.id)
699
.fetch_all ( & app.db)
700
.await
701
.unwrap_or_default ( ) ;
702
for id in upstreams {
703
let Some ( upstream) = repo_by_oxy_id ( app, & id) .await else { continue } ;
704
if let Some ( h) = last_green ( app, & upstream) .await {
705
propagate_to ( app, & upstream, h, true , Some ( consumer.id) ) .await;
706
}
707
let Some ( name) = RepoName:: new( & upstream.owner, & upstream.name) else { continue } ;
708
let b = upstream.default_bookmark.clone ( ) ;
709
if let Ok ( h) = app.repos.read ( & name, move | r, _ | heads_of ( r, & b) ) .await {
710
propagate_to ( app, & upstream, h, false , Some ( consumer.id) ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
711
}
712
}
713
}
714
e7158350 Andy McCall 23 days ago ↶
715
716
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize ) ]
717
# [ serde ( rename_all = " lowercase" ) ]
718
pub enum Follow {
719
Off,
720
721
Green,
722
723
Any,
724
}
725
726
impl Follow {
727
pub fn parse ( s : & str ) -> Option< Follow> {
728
match s.trim ( ) {
729
" off" => Some ( Follow:: Off) ,
730
" green" => Some ( Follow:: Green) ,
731
" any" => Some ( Follow:: Any) ,
732
_ => None ,
733
}
734
}
735
}
736
737
738
739
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize ) ]
740
pub struct Rule {
741
pub follow : Follow,
742
743
pub land_auto : bool ,
744
745
pub target : Option< String> ,
746
747
748
pub watch : Vec< String> ,
749
750
pub from : & 'static str ,
751
}
752
753
754
755
pub fn module_keys ( text : & str , path : & str ) -> ( Option< Follow> , Option< bool > , Option< String> , Vec< String> ) {
756
let ( mut follow, mut land, mut target, mut watch) = ( None , None , None , Vec :: new( ) ) ;
757
let mut here = false ;
758
for line in text.lines ( ) {
759
let line = line.trim ( ) ;
760
if line.is_empty ( ) | | line.starts_with ( ' #' ) {
761
continue ;
762
}
763
if let Some ( inner) = line.strip_prefix ( ' [' ) .and_then ( | l | l.strip_suffix ( ' ]' ) ) {
764
here = inner.trim ( ) .trim_matches ( ' "' ) .trim_end_matches ( ' /' ) = = path.trim_end_matches ( ' /' ) ;
765
continue ;
766
}
767
let ( true , Some ( ( k, v) ) ) = ( here, line.split_once ( ' =' ) ) else { continue } ;
768
let v = v.trim ( ) ;
769
let unquoted = v.trim_matches ( ' "' ) .trim ( ) ;
770
match k.trim ( ) {
771
" follow" => follow = Follow:: parse( unquoted) ,
772
" land" => land = Some ( unquoted = = " auto" ) ,
773
" target" if ! unquoted.is_empty ( ) => target = Some ( unquoted.to_string ( ) ) ,
774
" watch" => {
775
watch = v
776
.trim_start_matches ( ' [' )
777
.trim_end_matches ( ' ]' )
778
.split ( ' ,' )
779
.map ( | w | w.trim ( ) .trim_matches ( ' "' ) .trim ( ) .to_string ( ) )
780
.filter ( | w | ! w.is_empty ( ) )
781
.collect ( )
782
}
783
_ => { }
784
}
785
}
786
( follow, land, target, watch)
787
}
788
789
790
pub fn rule ( site_default : & str , row : & RepoRow, oxymodules : Option< & str > , path : & str ) -> Rule {
791
let ( follow, land, target, watch) = oxymodules.map ( | t | module_keys ( t, path) ) .unwrap_or_default ( ) ;
792
let ( follow, from) = match follow {
793
Some ( f) => ( f, " .oxymodules" ) ,
794
None => match Follow:: parse( & row.diffusion) {
795
Some ( f) => ( f, " setting" ) ,
796
None => ( Follow:: parse( site_default) .unwrap_or ( Follow:: Off) , " site" ) ,
797
} ,
798
} ;
799
Rule { follow, land_auto: land.unwrap_or ( false ) , target, watch, from }
800
}
801
802
# [ cfg ( test ) ]
803
mod tests {
804
use super :: * ;
805
806
const MODULES : & str = r # "
807
[rstar-api]
808
url = https://oxygenhub.io/doomsdayone/rstar-api
809
follow = green
810
land = "propose"
811
target = develop
812
watch = ["src/x16/irq*", "src/x16/frame*"]
813
814
[other]
815
url = https://example/other
816
follow = any
817
"# ;
818
819
fn row ( diffusion : & str ) -> RepoRow {
820
RepoRow {
821
id: 1 ,
822
owner: " o" .into ( ) ,
823
name: " n" .into ( ) ,
824
description: String :: new( ) ,
825
default_bookmark: " main" .into ( ) ,
826
visibility: " public" .into ( ) ,
827
created_ms: 0 ,
828
updated_ms: 0 ,
829
git_mirror: false ,
830
git_mirror_state: String :: new( ) ,
831
forked_from: None ,
832
topics: String :: new( ) ,
833
flow: String :: new( ) ,
834
diffusion: diffusion.into ( ) ,
835
.. Default :: default( )
836
}
c4e432da Andy McCall 22 days ago ↶
837
}
838
839
# [ test ]
840
fn watch_patterns_match_paths ( ) {
841
assert! ( watched ( " src/x16/irq*" , " src/x16/irq.s" ) ) ;
842
assert! ( watched ( " src/x16/irq*" , " src/x16/irq_wait.c" ) ) ;
843
assert! ( ! watched ( " src/x16/irq*" , " src/x16/sub/irq.s" ) , " one segment" ) ;
844
assert! ( watched ( " src/**/frame*.c" , " src/x16/floor/frame_wait.c" ) ) ;
845
assert! ( watched ( " src/x16" , " src/x16/bank.s" ) , " a directory" ) ;
846
assert! ( ! watched ( " src/x16" , " src/x160/bank.s" ) ) ;
847
assert! ( watched ( " include/r?m.h" , " include/rim.h" ) ) ;
848
assert! ( ! watched ( " " , " anything" ) ) ;
e7158350 Andy McCall 23 days ago ↶
849
}
850
851
# [ test ]
852
fn oxymodules_keys_are_read_per_entry ( ) {
853
let ( f, land, target, watch) = module_keys ( MODULES , " rstar-api" ) ;
854
assert_eq! ( ( f, land, target.as_deref ( ) ) , ( Some ( Follow:: Green) , Some ( false ) , Some ( " develop" ) ) ) ;
855
assert_eq! ( watch, vec! [ " src/x16/irq*" , " src/x16/frame*" ] ) ;
856
assert_eq! ( module_keys ( MODULES , " other" ) .0 , Some ( Follow:: Any) ) ;
857
assert_eq! ( module_keys ( MODULES , " missing" ) , ( None , None , None , vec! [ ] ) ) ;
858
assert_eq! ( module_keys ( " [a]\n watch = x, y\n " , " a" ) .3 , vec! [ " x" , " y" ] ) ;
859
}
860
861
# [ test ]
862
fn oxymodules_wins_then_the_setting_then_the_site ( ) {
863
let r = rule ( " off" , & row ( " any" ) , Some ( MODULES ) , " rstar-api" ) ;
864
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " .oxymodules" ) ) ;
865
let r = rule ( " off" , & row ( " any" ) , Some ( " [rstar-api]\n url = x\n " ) , " rstar-api" ) ;
866
assert_eq! ( ( r.follow, r.from) , ( Follow:: Any, " setting" ) ) ;
867
let r = rule ( " green" , & row ( " " ) , None , " rstar-api" ) ;
868
assert_eq! ( ( r.follow, r.from) , ( Follow:: Green, " site" ) ) ;
869
assert_eq! ( rule ( " nonsense" , & row ( " " ) , None , " x" ) .follow, Follow:: Off) ;
870
}
871
}