de505df5 Andy McCall 1 month ago ↶
1
2
3
4
use super :: history:: { render_files, FileRows} ;
5
use super :: settings:: { csrf, require} ;
6
use super :: { filters, lookup, lookup_role, Error, Page, RepoShell, Shell} ;
7
use crate :: auth:: { Access, Role, Viewer} ;
8
use crate :: db:: { self , accounts, proposals as pdb} ;
9
use crate :: render:: { self , diff} ;
10
use crate :: repos:: proposals:: { self as pl, Projection, Source} ;
11
use crate :: repos:: view:: { self , ChangeView, ConflictView} ;
12
use crate :: repos:: RepoName;
13
use crate :: Shared;
14
use askama:: Template;
15
use axum:: extract:: { Form, Path, Query, State} ;
16
use axum:: response:: { IntoResponse, Redirect, Response} ;
17
use hub_api:: { Approval, ProposalSource, ProposalState} ;
18
use oxy_core:: hash:: ChangeHash;
19
use serde:: Deserialize;
20
use std:: collections:: { BTreeMap, HashMap} ;
21
22
23
24
# [ derive ( Template ) ]
25
# [ template ( path = " proposals.html" ) ]
26
pub struct ProposalsPage {
27
shell : Shell,
28
repo : RepoShell,
29
rows : Vec< pdb:: ProposalRow> ,
30
filter : String,
31
counts : ( usize , usize , usize ) ,
32
bookmarks : Vec< String> ,
1ac84c62 Andy McCall 1 month ago ↶
33
q : String,
34
sort : String,
35
pager : super:: Pager,
36
labels : std:: collections:: HashMap< i64 , Vec< db:: issues:: Label> > ,
de505df5 Andy McCall 1 month ago ↶
37
}
38
1ac84c62 Andy McCall 1 month ago ↶
39
impl ProposalsPage {
40
fn labels_of ( & self , id : & i64 ) -> & [ db:: issues:: Label] {
41
self .labels.get ( id) .map ( Vec :: as_slice) .unwrap_or ( & [ ] )
42
}
43
}
44
de505df5 Andy McCall 1 month ago ↶
45
# [ derive ( Deserialize ) ]
46
pub struct ListQuery {
47
state : Option< String> ,
1ac84c62 Andy McCall 1 month ago ↶
48
q : Option< String> ,
49
sort : Option< String> ,
50
page : Option< usize > ,
de505df5 Andy McCall 1 month ago ↶
51
}
52
1ac84c62 Andy McCall 1 month ago ↶
53
54
pub const SORTS : & [ ( & str , & str ) ] = & [ ( " updated" , " recently updated" ) , ( " newest" , " newest" ) , ( " oldest" , " oldest" ) ] ;
55
de505df5 Andy McCall 1 month ago ↶
56
pub async fn list (
57
State( app ) : State< Shared> ,
58
viewer : Viewer,
59
Path( (owner , name ) ) : Path< ( String, String) > ,
60
Query( q) : Query< ListQuery> ,
61
) -> Result< Page< ProposalsPage> , Error> {
62
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposals" ) , & viewer) ;
63
let ( repo, row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
64
let all = pdb:: list( & app.db, row.id, & [ ] ) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
65
let counts = (
66
all.iter ( ) .filter ( | p | p.is_open ( ) ) .count ( ) ,
67
all.iter ( ) .filter ( | p | p.state = = " landed" ) .count ( ) ,
68
all.iter ( ) .filter ( | p | p.state = = " closed" ) .count ( ) ,
69
) ;
70
let filter = q.state.unwrap_or_else ( | | " open" .into ( ) ) ;
1ac84c62 Andy McCall 1 month ago ↶
71
let mut labels: std:: collections:: HashMap< i64 , Vec< db:: issues:: Label> > = std:: collections:: HashMap:: new( ) ;
72
for ( id, l) in db:: issues:: labels_by_proposal( & app.db, row.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? {
73
labels.entry ( id) .or_default ( ) .push ( l) ;
74
}
75
76
let qtext = q.q.clone ( ) .unwrap_or_default ( ) ;
77
let ( mut authors, mut wanted, mut targets, mut words) = ( vec! [ ] , vec! [ ] , vec! [ ] , vec! [ ] ) ;
78
for tok in qtext.split_whitespace ( ) {
79
match tok.split_once ( ' :' ) {
80
Some ( ( " author" , v) ) => authors.push ( v.to_lowercase ( ) ) ,
81
Some ( ( " label" , v) ) => wanted.push ( v.to_lowercase ( ) ) ,
82
Some ( ( " target" , v) ) => targets.push ( v.to_string ( ) ) ,
83
_ => words.push ( tok.to_lowercase ( ) ) ,
84
}
85
}
86
let mut rows: Vec< pdb:: ProposalRow> = all
de505df5 Andy McCall 1 month ago ↶
87
.into_iter ( )
88
.filter ( | p | match filter.as_str ( ) {
89
" open" => p.is_open ( ) ,
90
" landed" => p.state = = " landed" ,
91
" closed" => p.state = = " closed" ,
92
_ => true ,
93
} )
1ac84c62 Andy McCall 1 month ago ↶
94
.filter ( | p | authors.is_empty ( ) | | authors.contains ( & p.author.to_lowercase ( ) ) )
95
.filter ( | p | targets.is_empty ( ) | | targets.contains ( & p.target) )
96
.filter ( | p | wanted.iter ( ) .all ( | w | labels.get ( & p.id) .is_some_and ( | ls | ls.iter ( ) .any ( | l | l.name.to_lowercase ( ) = = * w ) ) ) )
97
.filter ( | p | words.iter ( ) .all ( | w | p.title.to_lowercase ( ) .contains ( w) | | p.body.to_lowercase ( ) .contains ( w) ) )
de505df5 Andy McCall 1 month ago ↶
98
.collect ( ) ;
1ac84c62 Andy McCall 1 month ago ↶
99
let sort = q.sort.filter ( | s | SORTS .iter ( ) .any ( | ( k , _) | k = = s ) ) .unwrap_or_else ( | | " updated" .into ( ) ) ;
100
match sort.as_str ( ) {
101
" newest" => rows.sort_by_key ( | p | std:: cmp:: Reverse( p.number) ) ,
102
" oldest" => rows.sort_by_key ( | p | p.number ) ,
103
_ => rows.sort_by_key ( | p | std:: cmp:: Reverse( p.updated_ms) ) ,
104
}
105
let pager = super :: Pager:: new( rows.len ( ) , q.page, & [ ( " state" , & filter) , ( " q" , & qtext) , ( " sort" , & sort) ] ) ;
106
let rows = pager.slice ( rows) ;
de505df5 Andy McCall 1 month ago ↶
107
let bookmarks = app
108
.repos
109
.read ( & repo, | r , _| Ok ( r.bookmarks ( ) ? .into_iter ( ) .map ( | b | b.name ) .collect:: < Vec< _ > > ( ) ) )
110
.await
111
.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
1ac84c62 Andy McCall 1 month ago ↶
112
Ok ( Page( ProposalsPage {
113
shell,
114
repo: RepoShell:: new( & app, & row, " proposals" ) .with_role ( role) ,
115
rows,
116
filter,
117
counts,
118
bookmarks,
119
q: qtext,
120
sort,
121
pager,
122
labels,
123
} ) )
de505df5 Andy McCall 1 month ago ↶
124
}
125
126
127
128
# [ derive ( Template ) ]
129
# [ template ( path = " proposal_new.html" ) ]
130
pub struct NewProposalPage {
131
shell : Shell,
132
repo : RepoShell,
133
bookmarks : Vec< String> ,
134
source : String,
135
target : String,
136
error : Option< String> ,
137
138
preview : Vec< ChangeView> ,
139
conflicted : bool ,
7d2eb590 Andy McCall 1 month ago ↶
140
141
title : String,
142
body : String,
143
144
assistants : Vec< String> ,
de505df5 Andy McCall 1 month ago ↶
145
}
146
147
# [ derive ( Deserialize ) ]
148
pub struct NewQuery {
149
source : Option< String> ,
150
target : Option< String> ,
151
}
152
153
0b20f1ec Andy McCall 1 month ago ↶
154
155
156
de505df5 Andy McCall 1 month ago ↶
157
fn parse_source ( app : & Shared, repo : & RepoName, source : & str ) -> Option< ( Option< RepoName> , String) > {
0b20f1ec Andy McCall 1 month ago ↶
158
let plain = | b : & str | ! b.is_empty ( ) & & ! b.chars ( ) .any ( | c | c.is_control ( ) | | c = = ' :' ) ;
de505df5 Andy McCall 1 month ago ↶
159
if let Some ( ( slug, bookmark) ) = source.split_once ( ' :' ) {
160
let fork = RepoName:: parse( slug) ? ;
0b20f1ec Andy McCall 1 month ago ↶
161
if ! plain ( bookmark) {
de505df5 Andy McCall 1 month ago ↶
162
return None ;
0b20f1ec Andy McCall 1 month ago ↶
163
}
164
if fork = = * repo {
165
return Some ( ( None , bookmark.to_string ( ) ) ) ;
de505df5 Andy McCall 1 month ago ↶
166
}
0b20f1ec Andy McCall 1 month ago ↶
167
if ! app.repos.exists ( & fork) {
168
return None ;
169
}
de505df5 Andy McCall 1 month ago ↶
170
return Some ( ( Some ( fork) , bookmark.to_string ( ) ) ) ;
171
}
0b20f1ec Andy McCall 1 month ago ↶
172
plain ( source) .then ( | | ( None , source.to_string ( ) ) )
de505df5 Andy McCall 1 month ago ↶
173
}
174
175
async fn preview_for (
176
app : & Shared,
177
repo : & RepoName,
178
target : & str ,
179
source : & str ,
180
) -> Result< ( Vec< ChangeView> , bool ) , crate:: repos:: RepoError> {
181
let Some ( ( fork, bookmark) ) = parse_source ( app, repo, source) else { return Ok ( ( vec! [ ] , false ) ) } ;
182
let target = target.to_string ( ) ;
183
let fork_dir = fork.as_ref ( ) .map ( | f | app.repos.dir ( f) ) ;
184
app.repos
185
.read ( repo, move | r, cache| {
186
let projection = match & fork_dir {
187
Some ( dir) => {
188
let fork_repo = oxy_core:: repo:: Repo:: open( dir) ? ;
189
let heads = fork_repo.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) ;
190
pl:: project( r, cache, & target, Source:: Fork { fork: & fork_repo, heads } ) ?
191
}
192
None => {
193
let heads = r.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) ;
194
pl:: project( r, cache, & target, Source:: Bookmark( heads) ) ?
195
}
196
} ;
197
Ok ( ( projection.changes, projection.conflicted) )
198
} )
199
.await
200
}
201
202
pub async fn new_form (
203
State( app ) : State< Shared> ,
204
viewer : Viewer,
205
Path( (owner , name ) ) : Path< ( String, String) > ,
206
Query( q) : Query< NewQuery> ,
207
) -> Result< Page< NewProposalPage> , Error> {
208
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposals/new" ) , & viewer) ;
209
require ( & viewer, & shell) ? ;
210
let ( repo, row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
211
let bookmarks = app
212
.repos
213
.read ( & repo, | r , _| Ok ( r.bookmarks ( ) ? .into_iter ( ) .map ( | b | b.name ) .collect:: < Vec< _ > > ( ) ) )
214
.await
215
.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
216
let target = q.target.unwrap_or_else ( | | row.default_bookmark.clone ( ) ) ;
217
let source = q.source.unwrap_or_default ( ) ;
218
let ( preview, conflicted) =
219
if source.is_empty ( ) { ( vec! [ ] , false ) } else { preview_for ( & app, & repo, & target, & source) .await.unwrap_or ( ( vec! [ ] , false ) ) } ;
7d2eb590 Andy McCall 1 month ago ↶
220
let assistants = if preview.is_empty ( ) { vec! [ ] } else { assistant_names ( & app, & row) .await } ;
de505df5 Andy McCall 1 month ago ↶
221
Ok ( Page( NewProposalPage {
222
shell,
223
repo: RepoShell:: new( & app, & row, " proposals" ) .with_role ( role) ,
224
bookmarks,
225
source,
226
target,
227
error: None ,
228
preview,
229
conflicted,
7d2eb590 Andy McCall 1 month ago ↶
230
title: String :: new( ) ,
231
body: String :: new( ) ,
232
assistants,
de505df5 Andy McCall 1 month ago ↶
233
} ) )
234
}
235
236
# [ derive ( Deserialize ) ]
237
pub struct NewForm {
238
#[serde(rename = "_csrf")]
239
csrf_token : String,
240
title : String,
241
#[serde(default)]
242
body : String,
243
target : String,
244
source : String,
245
#[serde(default)]
246
draft : Option< String> ,
7d2eb590 Andy McCall 1 month ago ↶
247
248
#[serde(default)]
249
assistant : String,
de505df5 Andy McCall 1 month ago ↶
250
}
251
b64e2537 Andy McCall 1 month ago ↶
252
253
# [ allow ( clippy::too_many_arguments ) ]
254
pub async fn open_proposal (
255
app : & Shared,
256
repo : & RepoName,
257
row : & db:: repos:: RepoRow,
258
me : & accounts:: Account,
259
title : & str ,
260
body : & str ,
261
target : & str ,
262
source : & str ,
263
draft : bool ,
264
) -> Result< pdb:: ProposalRow, String> {
265
if title.trim ( ) .is_empty ( ) {
266
return Err ( " Give the proposal a title." .into ( ) ) ;
de505df5 Andy McCall 1 month ago ↶
267
}
b64e2537 Andy McCall 1 month ago ↶
268
let bookmarks =
269
app.repos.read ( repo, | r , _| Ok ( r.bookmarks ( ) ? .into_iter ( ) .map ( | b | b.name ) .collect:: < Vec< _ > > ( ) ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
270
if ! bookmarks.iter ( ) .any ( | b | b = = target ) {
271
return Err ( " Pick a target bookmark of this repository." .into ( ) ) ;
de505df5 Andy McCall 1 month ago ↶
272
}
b64e2537 Andy McCall 1 month ago ↶
273
let Some ( ( fork, bookmark) ) = parse_source ( app, repo, source.trim ( ) ) else {
274
return Err ( " The source is a bookmark of this repository, or owner/name:bookmark of a fork." .into ( ) ) ;
de505df5 Andy McCall 1 month ago ↶
275
} ;
0b20f1ec Andy McCall 1 month ago ↶
276
if fork.is_none ( ) & & ! bookmarks.contains ( & bookmark) {
277
return Err ( format! ( " There is no bookmark {bookmark} in this repository." ) ) ;
278
}
b64e2537 Andy McCall 1 month ago ↶
279
if fork.is_none ( ) & & bookmark = = target {
280
return Err ( " The source and the target are the same bookmark." .into ( ) ) ;
de505df5 Andy McCall 1 month ago ↶
281
}
b64e2537 Andy McCall 1 month ago ↶
282
let ( preview, _ ) = preview_for ( app, repo, target, source.trim ( ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
de505df5 Andy McCall 1 month ago ↶
283
if preview.is_empty ( ) {
b64e2537 Andy McCall 1 month ago ↶
284
return Err ( " The source adds nothing to the target." .into ( ) ) ;
de505df5 Andy McCall 1 month ago ↶
285
}
286
let source_repo_id = match & fork {
b64e2537 Andy McCall 1 month ago ↶
287
Some ( fk) => db:: repos:: get( & app.db, & fk.owner, & fk.name) .await.map_err ( | e | e.to_string ( ) ) ? .map ( | r | r.id ) ,
de505df5 Andy McCall 1 month ago ↶
288
None => None ,
289
} ;
290
let created = pdb:: create(
291
& app.db,
292
pdb:: NewProposal {
293
repo_id: row.id,
b64e2537 Andy McCall 1 month ago ↶
294
title: title.trim ( ) ,
295
body: body.trim ( ) ,
296
author_id: Some ( me.id) ,
297
author: & me.username,
298
draft,
299
target,
de505df5 Andy McCall 1 month ago ↶
300
source_kind: if fork.is_some ( ) { " fork" } else { " bookmark" } ,
301
source_repo_id,
302
source_bookmark: & bookmark,
303
source_changes: " " ,
304
} ,
305
)
306
.await
b64e2537 Andy McCall 1 month ago ↶
307
.map_err ( | e | e.to_string ( ) ) ? ;
308
pdb:: add_event( & app.db, created.id, & me.username, " opened" , " " ) .await.ok ( ) ;
309
accounts:: audit( & app.db, Some ( me) , " proposal.opened" , & format! ( " {} #{} " , row.slug ( ) , created.number) , title) .await.ok ( ) ;
18e381ea Andy McCall 1 month ago ↶
310
db:: notify:: index( & app.db, " proposal" , row.id, created.id, created.number, & created.title, & created.body) .await.ok ( ) ;
b64e2537 Andy McCall 1 month ago ↶
311
let url = format! ( " /{} /proposal/{} " , row.slug ( ) , created.number) ;
18e381ea Andy McCall 1 month ago ↶
312
crate :: notify:: deliver(
b64e2537 Andy McCall 1 month ago ↶
313
app,
18e381ea Andy McCall 1 month ago ↶
314
crate :: notify:: Event {
315
kind: " proposal.opened" ,
b64e2537 Andy McCall 1 month ago ↶
316
title: format! ( " {} #{} : {} " , row.slug ( ) , created.number, created.title) ,
317
url,
318
actor: me,
18e381ea Andy McCall 1 month ago ↶
319
repo_id: row.id,
320
thread: Some ( ( " proposal" , created.id) ) ,
b64e2537 Andy McCall 1 month ago ↶
321
mention_text: body,
18e381ea Andy McCall 1 month ago ↶
322
direct: vec! [ ] ,
323
} ,
324
)
325
.await;
b64e2537 Andy McCall 1 month ago ↶
326
crate :: hooks:: emit(
327
app,
328
row,
329
" proposal.opened" ,
330
& me.username,
331
serde_json:: json! ( { " number" : created.number, " title" : created.title, " target" : target, " source" : source } ) ,
332
)
333
.await;
334
Ok ( created)
335
}
336
337
pub async fn create (
338
State( app ) : State< Shared> ,
339
viewer : Viewer,
340
Path( (owner , name ) ) : Path< ( String, String) > ,
341
Form( f) : Form< NewForm> ,
342
) -> Result< Response, Error> {
343
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposals/new" ) , & viewer) ;
344
let me = require ( & viewer, & shell) ? ;
345
csrf ( & viewer, & f.csrf_token, & shell) ? ;
346
let ( repo, row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
d0fbea2e Andy McCall 1 month ago ↶
347
super :: writable( & row, & shell) ? ;
b64e2537 Andy McCall 1 month ago ↶
348
match open_proposal ( & app, & repo, & row, & me.account, & f.title, & f.body, & f.target, & f.source, f.draft.is_some ( ) ) .await {
349
Ok ( created) => Ok ( Redirect:: to( & format! ( " /{owner} /{name} /proposal/{} " , created.number) ) .into_response ( ) ) ,
350
Err ( msg) => {
351
let bookmarks =
352
app.repos.read ( & repo, | r , _| Ok ( r.bookmarks ( ) ? .into_iter ( ) .map ( | b | b.name ) .collect:: < Vec< _ > > ( ) ) ) .await.unwrap_or_default ( ) ;
353
Ok ( Page( NewProposalPage {
354
shell,
355
repo: RepoShell:: new( & app, & row, " proposals" ) .with_role ( role) ,
356
bookmarks,
357
source: f.source.clone ( ) ,
358
target: f.target.clone ( ) ,
359
error: Some ( msg) ,
360
preview: vec! [ ] ,
361
conflicted: false ,
7d2eb590 Andy McCall 1 month ago ↶
362
title: f.title.clone ( ) ,
363
body: f.body.clone ( ) ,
364
assistants: vec! [ ] ,
b64e2537 Andy McCall 1 month ago ↶
365
} )
366
.into_response ( ) )
367
}
368
}
de505df5 Andy McCall 1 month ago ↶
369
}
370
371
372
373
374
pub struct CommentView {
375
pub row : pdb:: CommentRow,
376
pub body_html : String,
377
pub line : Option< usize > ,
378
}
379
a1b98695 Andy McCall 1 month ago ↶
380
381
382
pub struct Entry {
383
pub kind : & 'static str ,
384
pub index : usize ,
385
}
386
de505df5 Andy McCall 1 month ago ↶
387
pub struct ReviewView {
388
pub row : pdb:: ReviewRow,
389
pub body_html : String,
390
pub sealed : bool ,
391
}
392
393
394
pub struct Loaded {
395
pub row : pdb:: ProposalRow,
396
pub changes : Vec< ChangeView> ,
397
pub hashes : Vec< ChangeHash> ,
398
pub heads : Vec< ChangeHash> ,
3f02e5ea Andy McCall 1 month ago ↶
399
pub target_heads : Vec< ChangeHash> ,
de505df5 Andy McCall 1 month ago ↶
400
pub state : String,
401
pub target_state : String,
402
pub conflicted : bool ,
403
pub conflicts : Vec< ConflictView> ,
dd0788b7 Andy McCall 9 days ago ↶
404
405
406
407
pub union : Option< Vec< ( String, Vec< u8 > ) > > ,
de505df5 Andy McCall 1 month ago ↶
408
pub files : Vec< FileRows> ,
9df855f2 Andy McCall 1 month ago ↶
409
410
pub diffs : Vec< oxy_core:: repo:: FileDiff> ,
411
pub bases : std:: collections:: BTreeMap< String, Vec< u8 > > ,
de505df5 Andy McCall 1 month ago ↶
412
413
pub atoms : HashMap< ( String, usize ) , ( String, u32 ) > ,
414
pub sealing_keys : Vec< String> ,
415
pub touched : Vec< String> ,
416
pub owners : Vec< String> ,
417
pub landed_already : bool ,
ac713df7 Andy McCall 23 days ago ↶
418
419
420
421
pub source_has_changes : bool ,
668b2ad7 Andy McCall 9 days ago ↶
422
423
424
pub since_landing : Vec< ChangeView> ,
de505df5 Andy McCall 1 month ago ↶
425
}
426
427
428
pub async fn load ( app : & Shared, repo : & RepoName, row : pdb:: ProposalRow) -> Result< Loaded, crate:: repos:: RepoError> {
3571c519 Andy McCall 16 days ago ↶
429
load_with ( app, repo, row, true ) .await
430
}
431
432
433
434
435
436
pub async fn load_summary ( app : & Shared, repo : & RepoName, row : pdb:: ProposalRow) -> Result< Loaded, crate:: repos:: RepoError> {
437
load_with ( app, repo, row, false ) .await
438
}
439
d7e2c275 Andy McCall 9 days ago ↶
440
441
442
443
444
pub async fn load_for_api ( app : & Shared, repo : & RepoName, row : pdb:: ProposalRow) -> Result< Loaded, crate:: repos:: RepoError> {
445
if row.is_open ( ) {
446
return load_summary ( app, repo, row) .await;
447
}
448
let hashes: Vec< ChangeHash> = row.projected_changes ( ) .iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect ( ) ;
449
let heads: Vec< ChangeHash> = row.projected_heads ( ) .iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect ( ) ;
450
let h2 = hashes.clone ( ) ;
451
let ( changes, sealing_keys) = app
452
.repos
453
.read ( repo, move | r, _ | {
454
let labels = HashMap:: new( ) ;
455
let changes: Vec< ChangeView> =
456
h2.iter ( ) .filter_map ( | h | Some ( view:: change_view_pub( r, h, & r.store ( ) .get_change_meta ( h) .ok ( ) ? , false , & labels) ) ) .collect ( ) ;
457
let sealing = pl:: sealing_keys( r, & h2) ? .into_iter ( ) .map ( | k | k.to_hex ( ) ) .collect ( ) ;
458
Ok ( ( changes, sealing) )
459
} )
460
.await? ;
461
Ok ( Loaded {
462
changes,
463
heads,
464
target_heads: Vec :: new( ) ,
465
state: row.projected_state.clone ( ) ,
466
target_state: row.projected_target.clone ( ) ,
467
conflicted: row.conflicted,
468
conflicts: Vec :: new( ) ,
469
union: None ,
470
files: Vec :: new( ) ,
471
diffs: Vec :: new( ) ,
472
bases: BTreeMap:: new( ) ,
473
atoms: HashMap:: new( ) ,
474
sealing_keys,
475
touched: Vec :: new( ) ,
476
owners: Vec :: new( ) ,
477
landed_already: row.state = = " landed" ,
478
source_has_changes: ! hashes.is_empty ( ) ,
668b2ad7 Andy McCall 9 days ago ↶
479
since_landing: Vec :: new( ) ,
d7e2c275 Andy McCall 9 days ago ↶
480
hashes,
481
row,
482
} )
483
}
484
3571c519 Andy McCall 16 days ago ↶
485
async fn load_with ( app : & Shared, repo : & RepoName, row : pdb:: ProposalRow, render : bool ) -> Result< Loaded, crate:: repos:: RepoError> {
de505df5 Andy McCall 1 month ago ↶
486
let fork_dir = match ( row.source_kind.as_str ( ) , row.source_repo_id) {
487
( " fork" , Some ( id) ) => {
488
db:: repos:: get_by_id( & app.db, id) .await.ok ( ) .flatten ( ) .and_then ( | r | RepoName:: new( & r.owner, & r.name) ) .map ( | n | app.repos.dir ( & n) )
489
}
490
_ => None ,
491
} ;
492
let r2 = row.clone ( ) ;
493
let loaded = app
494
.repos
495
.read ( repo, move | r, cache| {
496
let source = match ( r2.source_kind.as_str ( ) , & fork_dir) {
497
( " changes" , _ ) => Source:: Changes( r2.source_changes ( ) .iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect ( ) ) ,
498
( " fork" , Some ( dir) ) => {
499
let fork_repo = oxy_core:: repo:: Repo:: open( dir) ? ;
500
let heads =
501
fork_repo.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = r2.source_bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) ;
502
503
let fork_state = fork_repo.load_state ( & heads) ? ;
504
let missing: Vec< ChangeHash> = fork_state.changes ( ) .map ( | ( h , _) | * h) .filter ( | h | ! r.store ( ) .has ( h) ) .collect ( ) ;
505
if ! missing.is_empty ( ) {
506
r.import_bundle ( & fork_repo.bundle_for ( & missing) ? , r.current_policy ( ) ? .is_some ( ) ) ? ;
507
}
508
Source:: Changes( heads)
509
}
510
_ => {
511
let heads = r.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = r2.source_bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) ;
512
Source:: Bookmark( heads)
513
}
514
} ;
ac713df7 Andy McCall 23 days ago ↶
515
let source_has_changes = match & source {
516
Source:: Changes( h) | Source:: Bookmark( h) | Source:: Fork { heads: h, .. } => ! h.is_empty ( ) ,
517
} ;
de505df5 Andy McCall 1 month ago ↶
518
let p: Projection = pl:: project( r, cache, & r2.target, source) ? ;
519
let target_loaded = cache.load ( r, & p.target_heads) ? ;
3571c519 Andy McCall 16 days ago ↶
520
let files = if render { render_files ( & p.files, & p.bases) } else { Vec :: new( ) } ;
de505df5 Andy McCall 1 month ago ↶
521
let mut atoms = HashMap:: new( ) ;
522
for f in & files {
3571c519 Andy McCall 16 days ago ↶
523
524
let Some ( live) = pl:: atoms_of( & p.loaded, & f.path) else { continue } ;
de505df5 Andy McCall 1 month ago ↶
525
for l in & f.lines {
3571c519 Andy McCall 16 days ago ↶
526
if let Some ( ( h, i) ) = l.new_no.and_then ( | n | live.get ( ( n as usize ) .checked_sub ( 1 ) ? ) ) {
527
atoms.insert ( ( f.path.clone ( ) , l.new_no.unwrap_or ( 0 ) as usize ) , ( h.to_hex ( ) , * i) ) ;
de505df5 Andy McCall 1 month ago ↶
528
}
529
}
530
}
531
let touched: Vec< String> = p.files.iter ( ) .map ( | f | f.path.clone ( ) ) .collect ( ) ;
532
let owners = pl:: owners_for( & target_loaded, & touched) ;
533
let sealing = pl:: sealing_keys( r, & p.hashes) ? .into_iter ( ) .map ( | k | k.to_hex ( ) ) .collect ( ) ;
534
let landed_already = ! p.hashes.is_empty ( ) & & p.hashes.iter ( ) .all ( | h | target_loaded.state.contains ( h) ) ;
0b20f1ec Andy McCall 1 month ago ↶
535
668b2ad7 Andy McCall 9 days ago ↶
536
537
538
539
540
541
let recorded: Vec< ChangeHash> = if r2.state = = " landed" {
542
r2.projected_changes ( ) .iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect ( )
543
} else {
544
Vec :: new( )
545
} ;
546
let ( changes, hashes, since_landing) = if r2.state = = " landed" {
0b20f1ec Andy McCall 1 month ago ↶
547
let labels = HashMap:: new( ) ;
668b2ad7 Andy McCall 9 days ago ↶
548
let changes = recorded
0b20f1ec Andy McCall 1 month ago ↶
549
.iter ( )
550
.filter_map ( | h | Some ( view:: change_view_pub( r, h, & r.store ( ) .get_change_meta ( h) .ok ( ) ? , false , & labels) ) )
551
.collect ( ) ;
668b2ad7 Andy McCall 9 days ago ↶
552
let since: Vec< ChangeView> = p.changes.into_iter ( ) .filter ( | c | ! recorded.iter ( ) .any ( | h | h.to_hex ( ) = = c.hash ) ) .collect ( ) ;
553
( changes, recorded, since)
0b20f1ec Andy McCall 1 month ago ↶
554
} else {
668b2ad7 Andy McCall 9 days ago ↶
555
( p.changes, p.hashes, Vec :: new( ) )
0b20f1ec Andy McCall 1 month ago ↶
556
} ;
de505df5 Andy McCall 1 month ago ↶
557
Ok ( Loaded {
558
row: r2,
0b20f1ec Andy McCall 1 month ago ↶
559
changes,
560
hashes,
de505df5 Andy McCall 1 month ago ↶
561
heads: p.heads,
3f02e5ea Andy McCall 1 month ago ↶
562
target_heads: p.target_heads.clone ( ) ,
de505df5 Andy McCall 1 month ago ↶
563
state: p.state.to_hex ( ) ,
564
target_state: p.target_state.to_hex ( ) ,
565
conflicted: p.conflicted,
566
conflicts: p.conflicts,
dd0788b7 Andy McCall 9 days ago ↶
567
union: if p.conflicted { oxy_core:: repo:: Repo:: union_resolution( & p.loaded.state, & p.loaded.snapshot) } else { None } ,
de505df5 Andy McCall 1 month ago ↶
568
files,
9df855f2 Andy McCall 1 month ago ↶
569
diffs: p.files.clone ( ) ,
570
bases: p.bases.clone ( ) ,
de505df5 Andy McCall 1 month ago ↶
571
atoms,
572
sealing_keys: sealing,
573
touched,
574
owners,
575
landed_already,
ac713df7 Andy McCall 23 days ago ↶
576
source_has_changes,
668b2ad7 Andy McCall 9 days ago ↶
577
since_landing,
de505df5 Andy McCall 1 month ago ↶
578
} )
579
} )
580
.await? ;
ac713df7 Andy McCall 23 days ago ↶
581
582
583
584
585
let mut loaded = loaded;
586
if loaded.row.is_open ( )
587
& & loaded.source_has_changes
588
& & loaded.hashes.is_empty ( )
589
& & pdb:: set_landed( & app.db, loaded.row.id, & loaded.target_state, " " , " push" ) .await.is_ok ( )
590
{
591
loaded.row.state = " landed" .into ( ) ;
592
loaded.row.landed_via = " push" .into ( ) ;
593
loaded.row.landed_state = loaded.target_state.clone ( ) ;
594
return Ok ( loaded) ;
595
}
0b20f1ec Andy McCall 1 month ago ↶
596
597
598
599
if ! loaded.row.is_open ( ) {
600
return Ok ( loaded) ;
601
}
de505df5 Andy McCall 1 month ago ↶
602
let heads_csv = loaded.heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect:: < Vec< _ > > ( ) .join ( " ," ) ;
603
let changes_csv = loaded.hashes.iter ( ) .map ( | h | h.to_hex ( ) ) .collect:: < Vec< _ > > ( ) .join ( " ," ) ;
604
pdb:: set_projection( & app.db, loaded.row.id, & loaded.state, & heads_csv, & loaded.target_state, & changes_csv, loaded.conflicted)
605
.await
606
.ok ( ) ;
607
Ok ( loaded)
ac713df7 Andy McCall 23 days ago ↶
608
}
609
610
611
612
pub async fn settle_after_push ( app : & Shared, row : & db:: repos:: RepoRow, bookmark : & str , actor : & str ) {
613
let Some ( repo) = RepoName:: new( & row.owner, & row.name) else { return } ;
614
let Ok ( open) = pdb:: list( & app.db, row.id, & [ " open" , " draft" ] ) .await else { return } ;
615
for p in open.into_iter ( ) .filter ( | p | p.target = = bookmark ) {
616
let id = p.id;
d943160d Andy McCall 22 days ago ↶
617
let number = p.number;
3571c519 Andy McCall 16 days ago ↶
618
if let Ok ( l) = load_summary ( app, & repo, p) .await {
ac713df7 Andy McCall 23 days ago ↶
619
if l.row.state = = " landed" & & l.row.landed_by.is_empty ( ) {
620
pdb:: set_landed( & app.db, id, & l.target_state, actor, " push" ) .await.ok ( ) ;
2abea308 Andy McCall 17 days ago ↶
621
crate :: flow:: moot_after_landing( app, row, & l.row) .await;
d943160d Andy McCall 22 days ago ↶
622
} else if l.row.is_open ( ) & & ! l.hashes.is_empty ( ) & & crate :: flow:: enabled( app, row) {
623
624
625
5d90e0e0 Andy McCall 19 days ago
626
let ran: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM flow_runs WHERE repo_id = $1 AND state = $2" )
d943160d Andy McCall 22 days ago ↶
627
.bind ( row.id)
628
.bind ( & l.state)
629
.fetch_one ( & app.db)
630
.await
631
.unwrap_or ( 1 ) ;
632
if ran = = 0 {
633
let label = format! ( " proposal/{number} " ) ;
634
match crate :: flow:: start_run_scoped( app, row, & label, & l.state, & l.heads, actor, " proposal" , None ) .await {
635
Ok ( Some ( n) ) => tracing:: info! ( " proposal {number}: its target moved; run #{n} checks its new projected state" ) ,
636
Ok ( None ) => { }
637
Err ( e) => tracing:: warn! ( " proposal {number}: a run of its new projected state did not start: {e}" ) ,
638
}
639
}
ac713df7 Andy McCall 23 days ago ↶
640
}
641
}
642
}
de505df5 Andy McCall 1 month ago ↶
643
}
644
645
646
pub struct Verdict {
647
pub reviews : Vec< ReviewView> ,
648
pub approvals : Vec< Approval> ,
649
pub blockers : Vec< String> ,
2a8b6216 Andy McCall 17 days ago ↶
650
651
652
pub checks_blocker : Option< String> ,
de505df5 Andy McCall 1 month ago ↶
653
pub depends_on : Vec< i64 > ,
654
pub requirements : pdb:: Requirements,
655
}
656
657
pub async fn verdict ( app : & Shared, loaded : & Loaded, repo_id : i64 ) -> Result< Verdict, sqlx:: Error> {
658
let rows = pdb:: reviews( & app.db, loaded.row.id) .await? ;
659
let links = accounts:: identity_accounts( & app.db) .await? ;
660
let sealed_accounts: Vec< String> = links.iter ( ) .filter ( | ( k , _) | loaded.sealing_keys.contains ( k) ) .map ( | ( _, u ) | u.clone ( ) ) .collect ( ) ;
661
662
let mut latest: BTreeMap< String, & pdb:: ReviewRow> = BTreeMap:: new( ) ;
7d2eb590 Andy McCall 1 month ago ↶
663
for r in rows.iter ( ) .filter ( | r | r.via.is_empty ( ) ) {
664
de505df5 Andy McCall 1 month ago ↶
665
latest.insert ( r.author.clone ( ) , r) ;
666
}
667
let mut approvals = Vec :: new( ) ;
668
for ( who, r) in & latest {
669
if r.verdict = = " approve" {
670
let sealed = sealed_accounts.contains ( who) ;
671
let key = links.iter ( ) .find ( | ( k , u ) | u = = who & & loaded.sealing_keys.contains ( k) ) .map ( | ( k , _) | k.clone ( ) ) ;
672
approvals.push ( Approval { account: who.clone ( ) , sealed, key } ) ;
673
}
674
}
675
let reviews = rows
676
.iter ( )
677
.map ( | r | ReviewView {
678
row: r.clone ( ) ,
679
body_html: render:: markdown:: render( & r.body, " /" , " /" ) ,
680
sealed: r.verdict = = " approve" & & sealed_accounts.contains ( & r.author) ,
681
} )
682
.collect ( ) ;
683
let requirements = pdb:: requirements( & app.db, repo_id) .await? ;
684
let mut blockers = Vec :: new( ) ;
0b20f1ec Andy McCall 1 month ago ↶
685
686
if ! loaded.row.is_open ( ) {
2a8b6216 Andy McCall 17 days ago ↶
687
return Ok ( Verdict { reviews, approvals, blockers, checks_blocker: None , depends_on: vec! [ ] , requirements } ) ;
0b20f1ec Andy McCall 1 month ago ↶
688
}
de505df5 Andy McCall 1 month ago ↶
689
if loaded.row.state = = " draft" {
690
blockers.push ( " the proposal is a draft" .into ( ) ) ;
691
}
692
if loaded.hashes.is_empty ( ) {
693
blockers.push ( " the proposal adds no changes" .into ( ) ) ;
694
}
695
let sealed_count = approvals.iter ( ) .filter ( | a | a.sealed ) .count ( ) as i64 ;
696
if sealed_count < requirements.required_approvals {
697
blockers.push ( format! ( " {sealed_count} of {} required sealed approvals" , requirements.required_approvals) ) ;
698
}
699
if requirements.require_owners {
700
let missing: Vec< & String> = loaded.owners.iter ( ) .filter ( | o | ! approvals.iter ( ) .any ( | a | a.sealed & & & a.account = = * o ) ) .collect ( ) ;
701
if ! missing.is_empty ( ) {
702
blockers.push ( format! ( " code owners still to approve: {} " , missing.iter ( ) .map ( | s | s.as_str ( ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ) ) ;
703
}
704
}
dd0788b7 Andy McCall 9 days ago ↶
705
if loaded.conflicted & & loaded.union.is_none ( ) & & ! requirements.allow_conflicts {
1e23d010 Andy McCall 1 month ago ↶
706
blockers.push ( format! (
707
" landing would carry conflicts, which this repository refuses: resolve them on the source (oxy merge {} there, fix the marked files, oxy commit, push)" ,
708
loaded.row.target
709
) ) ;
de505df5 Andy McCall 1 month ago ↶
710
}
2a8b6216 Andy McCall 17 days ago ↶
711
let mut checks_blocker = None ;
b64e2537 Andy McCall 1 month ago ↶
712
let required = db:: hooks:: required_checks( & app.db, repo_id) .await? ;
713
if ! required.is_empty ( ) {
714
let checks = db:: hooks:: checks_for_state( & app.db, repo_id, & loaded.state) .await? ;
715
let missing: Vec< String> =
716
required.iter ( ) .filter ( | n | ! checks.iter ( ) .any ( | c | & c.name = = * n & & c.status = = " passed" ) ) .cloned ( ) .collect ( ) ;
717
if ! missing.is_empty ( ) {
2a8b6216 Andy McCall 17 days ago ↶
718
let b = format! ( " required checks not passed on the projected state: {} " , missing.join ( " , " ) ) ;
719
checks_blocker = Some ( b.clone ( ) ) ;
720
blockers.push ( b) ;
b64e2537 Andy McCall 1 month ago ↶
721
}
722
}
de505df5 Andy McCall 1 month ago ↶
723
let requesting: Vec< & String> = latest.iter ( ) .filter ( | ( _, r ) | r.verdict = = " changes" ) .map ( | ( who , _) | who ) .collect ( ) ;
724
if ! requesting.is_empty ( ) {
725
blockers.push ( format! ( " changes requested by {} " , requesting.iter ( ) .map ( | s | s.as_str ( ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ) ) ;
726
}
727
728
let mut depends_on = Vec :: new( ) ;
729
for other in pdb:: list( & app.db, repo_id, & [ " open" , " draft" ] ) .await? {
730
if other.id = = loaded.row.id | | other.number > loaded.row.number {
731
continue ;
732
}
733
let theirs = other.projected_changes ( ) ;
734
if ! theirs.is_empty ( ) & & theirs.iter ( ) .all ( | h | loaded.hashes.iter ( ) .any ( | m | m.to_hex ( ) = = * h ) ) {
735
depends_on.push ( other.number) ;
736
}
737
}
738
if ! depends_on.is_empty ( ) {
739
blockers.push ( format! ( " depends on {} " , depends_on.iter ( ) .map ( | n | format! ( " #{n} " ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ) ) ;
740
}
2a8b6216 Andy McCall 17 days ago ↶
741
Ok ( Verdict { reviews, approvals, blockers, checks_blocker, depends_on, requirements } )
de505df5 Andy McCall 1 month ago ↶
742
}
743
744
# [ derive ( Template ) ]
745
# [ template ( path = " proposal.html" ) ]
746
pub struct ProposalPage {
747
shell : Shell,
748
repo : RepoShell,
749
p : pdb:: ProposalRow,
b5a0a69b Andy McCall 20 days ago ↶
750
751
752
live_topic : String,
753
repo_topic : String,
754
755
pending : Vec< String> ,
38833ed7 Andy McCall 1 month ago ↶
756
757
closes : Vec< super:: chain:: Link> ,
a1b98695 Andy McCall 1 month ago ↶
758
759
project_choices : Option< Vec< db:: projects:: Project> > ,
760
761
timeline : Vec< Entry> ,
762
763
can_triage : bool ,
de505df5 Andy McCall 1 month ago ↶
764
body_html : String,
765
tab : String,
766
changes : Vec< ChangeView> ,
767
files : Vec< FileRows> ,
9df855f2 Andy McCall 1 month ago ↶
768
diffs : Vec< oxy_core:: repo:: FileDiff> ,
769
bases : std:: collections:: BTreeMap< String, Vec< u8 > > ,
770
771
ignore_ws : bool ,
de505df5 Andy McCall 1 month ago ↶
772
conflicts : Vec< ConflictView> ,
773
conflicted : bool ,
774
comments : Vec< CommentView> ,
775
776
placed : HashMap< ( String, usize ) , Vec< usize > > ,
777
outdated : Vec< usize > ,
778
reviews : Vec< ReviewView> ,
779
approvals : Vec< Approval> ,
780
blockers : Vec< String> ,
781
depends_on : Vec< i64 > ,
782
events : Vec< pdb:: EventRow> ,
783
owners : Vec< String> ,
784
requirements : pdb:: Requirements,
785
landed_already : bool ,
786
can_edit : bool ,
4f37afcc Andy McCall 1 month ago ↶
787
788
789
deletable_source : Option< String> ,
668b2ad7 Andy McCall 9 days ago ↶
790
791
since_landing : Vec< ChangeView> ,
de505df5 Andy McCall 1 month ago ↶
792
can_land : bool ,
2a8b6216 Andy McCall 17 days ago ↶
793
can_land_over_checks : bool ,
217814c2 Andy McCall 8 days ago ↶
794
795
can_resolve : bool ,
86a69b53 Andy McCall 17 days ago ↶
796
can_auto_land : bool ,
de505df5 Andy McCall 1 month ago ↶
797
hub_can_land : bool ,
798
notice : Option< String> ,
799
error : Option< String> ,
800
heads : Vec< String> ,
801
state_hash : String,
802
source_label : String,
803
api_url : String,
18e381ea Andy McCall 1 month ago ↶
804
labels : Vec< db:: issues:: Label> ,
805
all_labels : Vec< db:: issues:: Label> ,
b64e2537 Andy McCall 1 month ago ↶
806
checks : Vec< db:: hooks:: Check> ,
807
required_checks : Vec< String> ,
e1ec9fbf Andy McCall 1 month ago ↶
808
809
projects : Vec< ( db:: projects:: Project, String) > ,
cc1cd197 Andy McCall 1 month ago ↶
810
811
requested : Vec< String> ,
8e96499f Andy McCall 1 month ago ↶
812
813
reviewable : Vec< String> ,
cc1cd197 Andy McCall 1 month ago ↶
814
815
can_request : bool ,
7d2eb590 Andy McCall 1 month ago ↶
816
817
assistants : Vec< String> ,
818
819
draft : Option< String> ,
de505df5 Andy McCall 1 month ago ↶
820
}
821
822
# [ derive ( Deserialize ) ]
823
pub struct DetailQuery {
824
tab : Option< String> ,
a1b98695 Andy McCall 1 month ago ↶
825
9df855f2 Andy McCall 1 month ago ↶
826
#[serde(default)]
a1b98695 Andy McCall 1 month ago ↶
827
notice : Option< String> ,
828
#[serde(default)]
829
error : Option< String> ,
830
#[serde(default)]
9df855f2 Andy McCall 1 month ago ↶
831
w : Option< String> ,
b5a0a69b Andy McCall 20 days ago ↶
832
833
#[serde(default)]
834
fragment : Option< String> ,
de505df5 Andy McCall 1 month ago ↶
835
}
836
837
838
839
# [ allow ( clippy::too_many_arguments ) ]
840
async fn detail (
841
app : & Shared,
842
shell : Shell,
843
viewer : & Viewer,
844
owner : & str ,
845
name : & str ,
846
number : i64 ,
847
tab : String,
848
notice : Option< String> ,
849
error : Option< String> ,
850
) -> Result< Page< ProposalPage> , Error> {
851
let ( repo, row, role) = lookup ( app, & shell, viewer, owner, name) .await? ;
852
let p = pdb:: get( & app.db, row.id, number)
853
.await
854
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
855
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
b5a0a69b Andy McCall 20 days ago ↶
856
let p_id = p.id;
de505df5 Andy McCall 1 month ago ↶
857
let loaded = load ( app, & repo, p) .await.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
858
859
if loaded.landed_already & & loaded.row.is_open ( ) {
860
pdb:: set_state( & app.db, loaded.row.id, " landed" , & loaded.target_state) .await.ok ( ) ;
861
pdb:: add_event( & app.db, loaded.row.id, " " , " landed" , " every change reached the target" ) .await.ok ( ) ;
2abea308 Andy McCall 17 days ago ↶
862
crate :: flow:: moot_after_landing( app, & row, & loaded.row) .await;
de505df5 Andy McCall 1 month ago ↶
863
}
864
let p = pdb:: get( & app.db, row.id, number) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? .unwrap_or ( loaded.row.clone ( ) ) ;
865
let v = verdict ( app, & loaded, row.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
866
let comment_rows = pdb:: comments( & app.db, loaded.row.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
867
let events = pdb:: events( & app.db, loaded.row.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
868
869
let mut by_atom: HashMap< ( String, String, u32 ) , usize > = HashMap:: new( ) ;
870
for ( ( path, line) , ( h, i) ) in & loaded.atoms {
871
by_atom.insert ( ( path.clone ( ) , h.clone ( ) , * i) , * line) ;
872
}
873
let mut comments = Vec :: new( ) ;
874
let mut placed: HashMap< ( String, usize ) , Vec< usize > > = HashMap:: new( ) ;
875
let mut outdated = Vec :: new( ) ;
876
for ( idx, c) in comment_rows.into_iter ( ) .enumerate ( ) {
877
let line = if c.anchor_change.is_empty ( ) {
878
None
879
} else {
880
by_atom.get ( & ( c.path.clone ( ) , c.anchor_change.clone ( ) , c.anchor_index as u32 ) ) .copied ( )
881
} ;
882
if ! c.anchor_change.is_empty ( ) {
883
match line {
884
Some ( l) => placed.entry ( ( c.path.clone ( ) , l) ) .or_default ( ) .push ( idx) ,
885
None => outdated.push ( idx) ,
886
}
887
}
888
comments.push ( CommentView { body_html: render:: markdown:: render( & c.body, " /" , " /" ) , row: c, line } ) ;
889
}
890
let mut changes = loaded.changes.clone ( ) ;
891
super :: attribute( app, & mut changes.iter_mut ( ) .collect:: < Vec< _ > > ( ) ) .await;
892
let me = viewer.account ( ) ;
893
let can_edit = me.is_some_and ( | a | Some ( a.id) = = p.author_id | | role > = Role:: Maintain) ;
668b2ad7 Andy McCall 9 days ago ↶
894
895
let deletable_source = if p.state = = " landed"
896
& & p.source_kind = = " bookmark"
897
& & role > = Role:: Write
898
& & p.source_bookmark ! = row.default_bookmark
899
& & loaded.since_landing.is_empty ( )
900
{
901
let b = p.source_bookmark.clone ( ) ;
902
app.repos
903
.read ( & repo, move | r, _ | Ok ( r.view ( ) ? .bookmarks.contains_key ( & b) ) )
904
.await
905
.unwrap_or ( false )
906
.then ( | | p.source_bookmark.clone ( ) )
907
} else {
908
None
909
} ;
de505df5 Andy McCall 1 month ago ↶
910
let can_land = role > = Role:: Write & & p.is_open ( ) & & v.blockers.is_empty ( ) ;
86a69b53 Andy McCall 17 days ago ↶
911
912
913
let can_auto_land = role > = Role:: Write & & p.is_open ( ) & & ! v.blockers.is_empty ( ) ;
2a8b6216 Andy McCall 17 days ago ↶
914
217814c2 Andy McCall 8 days ago ↶
915
let can_resolve = role > = Role:: Write & & p.is_open ( ) & & p.source_kind = = " bookmark" ;
2a8b6216 Andy McCall 17 days ago ↶
916
let can_land_over_checks = role > = Role:: Maintain & & p.is_open ( ) & & v.checks_blocker.is_some ( ) & & v.blockers.len ( ) = = 1 ;
cc1cd197 Andy McCall 1 month ago ↶
917
let can_request = p.is_open ( ) & & me.is_some_and ( | a | Some ( a.id) = = p.author_id | | role > = Role:: Write) ;
8e96499f Andy McCall 1 month ago ↶
918
let requested = pdb:: review_requests( & app.db, p.id) .await.unwrap_or_default ( ) ;
919
let mut reviewable: Vec< String> = if can_request {
920
accounts:: assignable( & app.db, row.id, & row.owner) .await.unwrap_or_default ( ) .into_iter ( ) .filter ( | u | * u ! = p.author) .collect ( )
921
} else {
922
Vec :: new( )
923
} ;
924
for r in & requested {
925
if ! reviewable.iter ( ) .any ( | x | x.eq_ignore_ascii_case ( r) ) {
926
reviewable.push ( r.clone ( ) ) ;
927
}
928
}
7d2eb590 Andy McCall 1 month ago ↶
929
let assistants: Vec< String> = if ( can_request | | can_edit) & & p.is_open ( ) { assistant_names ( app, & row) .await } else { Vec :: new( ) } ;
14530604 Andy McCall 1 month ago ↶
930
let delegated = match viewer.account ( ) {
931
Some ( a) => super :: edit:: delegated_identity( app, a.id) .await.is_some ( ) ,
932
None => false ,
933
} ;
934
let hub_can_land = delegated | | app.repos.read ( & repo, | r , _| Ok ( r.current_policy ( ) ? .is_none ( ) ) ) .await.unwrap_or ( false ) ;
de505df5 Andy McCall 1 month ago ↶
935
let source_label = match p.source_kind.as_str ( ) {
936
" fork" => format! (
937
" fork {} " ,
938
db:: repos:: get_by_id( & app.db, p.source_repo_id.unwrap_or ( 0 ) )
939
.await
940
.ok ( )
941
.flatten ( )
942
.map ( | r | format! ( " {} :{} " , r.slug ( ) , p.source_bookmark) )
943
.unwrap_or_default ( )
944
) ,
945
" changes" => format! ( " {} changes" , p.source_changes ( ) .len ( ) ) ,
946
_ => p.source_bookmark.clone ( ) ,
947
} ;
e1ec9fbf Andy McCall 1 month ago ↶
948
let projects = super :: projects:: placements_for( app, viewer, & row, p.number) .await;
38833ed7 Andy McCall 1 month ago ↶
949
let texts: Vec< String> = std:: iter:: once( p.body.clone ( ) ) .chain ( changes.iter ( ) .map ( | c | format! ( " {} \n {} " , c.title, c.body) ) ) .collect ( ) ;
950
let closes = super :: chain:: issues_closed_by( app, & row, & texts.iter ( ) .map ( String :: as_str) .collect:: < Vec< _ > > ( ) ) .await;
a1b98695 Andy McCall 1 month ago ↶
951
let project_choices = super :: projects:: project_choices( app, viewer, & row, & projects) .await;
952
let mut dated: Vec< ( i64 , Entry) > = Vec :: new( ) ;
953
for ( index, c) in comments.iter ( ) .enumerate ( ) {
954
if c.row.anchor_change.is_empty ( ) {
955
dated.push ( ( c.row.created_ms, Entry { kind: " comment" , index } ) ) ;
956
}
957
}
958
for ( index, r) in v.reviews.iter ( ) .enumerate ( ) {
959
dated.push ( ( r.row.created_ms, Entry { kind: " review" , index } ) ) ;
960
}
961
for ( index, e) in events.iter ( ) .enumerate ( ) {
962
dated.push ( ( e.at_ms, Entry { kind: " event" , index } ) ) ;
963
}
964
dated.sort_by_key ( | ( at , _) | * at) ;
965
let timeline: Vec< Entry> = dated.into_iter ( ) .map ( | ( _, e ) | e ) .collect ( ) ;
b5a0a69b Andy McCall 20 days ago ↶
966
let pending = app.assistant_gate.lock ( ) .unwrap_or_else ( | e | e.into_inner ( ) ) .answering ( p_id) ;
de505df5 Andy McCall 1 month ago ↶
967
Ok ( Page( ProposalPage {
b5a0a69b Andy McCall 20 days ago ↶
968
live_topic: crate :: events:: topic:: proposal( row.id, number) ,
969
repo_topic: crate :: events:: topic:: repo( row.id) ,
970
pending,
a1b98695 Andy McCall 1 month ago ↶
971
can_triage: role > = Role:: Triage,
972
timeline,
38833ed7 Andy McCall 1 month ago ↶
973
closes,
a1b98695 Andy McCall 1 month ago ↶
974
project_choices,
e1ec9fbf Andy McCall 1 month ago ↶
975
projects,
de505df5 Andy McCall 1 month ago ↶
976
shell,
ffb67a81 Andy McCall 21 days ago
977
repo: RepoShell:: new( app, & row, " proposals" ) .with_role ( role) .with_sky ( app, & row) .await,
c79a1111 Andy McCall 1 month ago ↶
978
body_html: super :: issues:: markdown( app, & row, & row.default_bookmark, & p.body) .await,
de505df5 Andy McCall 1 month ago ↶
979
p,
980
tab,
981
changes,
982
files: loaded.files,
9df855f2 Andy McCall 1 month ago ↶
983
diffs: loaded.diffs,
984
bases: loaded.bases,
985
ignore_ws: false ,
de505df5 Andy McCall 1 month ago ↶
986
conflicts: loaded.conflicts,
987
conflicted: loaded.conflicted,
668b2ad7 Andy McCall 9 days ago ↶
988
since_landing: loaded.since_landing,
de505df5 Andy McCall 1 month ago ↶
989
comments,
990
placed,
991
outdated,
992
reviews: v.reviews,
993
approvals: v.approvals,
994
blockers: v.blockers,
995
depends_on: v.depends_on,
996
events,
997
owners: loaded.owners,
998
requirements: v.requirements,
999
landed_already: loaded.landed_already,
1000
can_edit,
4f37afcc Andy McCall 1 month ago ↶
1001
deletable_source,
de505df5 Andy McCall 1 month ago ↶
1002
can_land,
2a8b6216 Andy McCall 17 days ago ↶
1003
can_land_over_checks,
217814c2 Andy McCall 8 days ago ↶
1004
can_resolve,
86a69b53 Andy McCall 17 days ago ↶
1005
can_auto_land,
de505df5 Andy McCall 1 month ago ↶
1006
hub_can_land,
1007
notice,
1008
error,
1009
heads: loaded.heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ,
b64e2537 Andy McCall 1 month ago ↶
1010
state_hash: loaded.state.clone ( ) ,
de505df5 Andy McCall 1 month ago ↶
1011
source_label,
1012
api_url: format! ( " {} /api/v1/repos/{owner} /{name} /proposals/{number} " , app.config.base_url ( ) ) ,
18e381ea Andy McCall 1 month ago ↶
1013
labels: db:: issues:: proposal_labels( & app.db, loaded.row.id) .await.unwrap_or_default ( ) ,
1014
all_labels: db:: issues:: labels( & app.db, row.id) .await.unwrap_or_default ( ) ,
b64e2537 Andy McCall 1 month ago ↶
1015
checks: db:: hooks:: checks_for_state( & app.db, row.id, & loaded.state) .await.unwrap_or_default ( ) ,
1016
required_checks: db:: hooks:: required_checks( & app.db, row.id) .await.unwrap_or_default ( ) ,
8e96499f Andy McCall 1 month ago ↶
1017
requested,
1018
reviewable,
7d2eb590 Andy McCall 1 month ago ↶
1019
assistants,
1020
draft: None ,
cc1cd197 Andy McCall 1 month ago ↶
1021
can_request,
de505df5 Andy McCall 1 month ago ↶
1022
} ) )
1023
}
1024
a1b98695 Andy McCall 1 month ago ↶
1025
1026
1027
fn back ( owner : & str , name : & str , number : i64 , tab : & str , notice : Option< String> , error : Option< String> ) -> Response {
1028
let mut url = format! ( " /{owner} /{name} /proposal/{number} ?tab={tab} " ) ;
1029
if let Some ( n) = notice {
1030
url.push_str ( & format! ( " ¬ice={} " , super :: settings:: urlencode( & n) ) ) ;
1031
}
1032
if let Some ( e) = error {
1033
url.push_str ( & format! ( " &error={} " , super :: settings:: urlencode( & e) ) ) ;
1034
}
1035
Redirect:: to( & url) .into_response ( )
1036
}
1037
de505df5 Andy McCall 1 month ago ↶
1038
pub async fn show (
1039
State( app ) : State< Shared> ,
1040
viewer : Viewer,
1041
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1042
Query( q) : Query< DetailQuery> ,
b5a0a69b Andy McCall 20 days ago ↶
1043
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
1044
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
a1b98695 Andy McCall 1 month ago ↶
1045
let mut page =
1046
detail ( & app, shell, & viewer, & owner, & name, number, q.tab.unwrap_or_else ( | | " conversation" .into ( ) ) , q.notice, q.error) .await? ;
9df855f2 Andy McCall 1 month ago ↶
1047
if q.w.as_deref ( ) = = Some ( " 1" ) {
1048
1049
page.0. files =
1050
super :: history:: render_files_with( & page.0. diffs, & page.0. bases, crate :: render:: diff:: Options { ignore_whitespace: true } ) ;
1051
page.0. ignore_ws = true ;
b5a0a69b Andy McCall 20 days ago ↶
1052
}
1053
1054
1055
if let Some ( name) = q.fragment.as_deref ( ) {
1056
let html = page.0. render ( ) .map_err ( | e | Error:: bad( page.0. shell.clone ( ) , e.to_string ( ) ) ) ? ;
1057
return Ok ( match super :: fragment( & html, name) {
1058
Some ( part) => ( [ ( axum:: http:: header:: CACHE_CONTROL , " no-store" ) ] , axum:: response:: Html( part) ) .into_response ( ) ,
1059
None => ( axum:: http:: StatusCode:: NOT_FOUND , " no such fragment" ) .into_response ( ) ,
1060
} ) ;
9df855f2 Andy McCall 1 month ago ↶
1061
}
b5a0a69b Andy McCall 20 days ago ↶
1062
Ok ( page.into_response ( ) )
de505df5 Andy McCall 1 month ago ↶
1063
}
1064
1065
1066
1067
# [ derive ( Deserialize ) ]
1068
pub struct CommentForm {
1069
#[serde(rename = "_csrf")]
1070
csrf_token : String,
1071
body : String,
1072
#[serde(default)]
1073
path : String,
1074
#[serde(default)]
1075
line : Option< usize > ,
1076
}
1077
1078
pub async fn comment (
1079
State( app ) : State< Shared> ,
1080
viewer : Viewer,
1081
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1082
Form( f) : Form< CommentForm> ,
a1b98695 Andy McCall 1 month ago ↶
1083
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
1084
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1085
let me = require ( & viewer, & shell) ? ;
1086
csrf ( & viewer, & f.csrf_token, & shell) ? ;
1087
let ( repo, row, _ ) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
d0fbea2e Andy McCall 1 month ago ↶
1088
super :: writable( & row, & shell) ? ;
de505df5 Andy McCall 1 month ago ↶
1089
let p = pdb:: get( & app.db, row.id, number)
1090
.await
1091
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1092
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1093
if f.body.trim ( ) .is_empty ( ) {
a1b98695 Andy McCall 1 month ago ↶
1094
return Ok ( back ( & owner, & name, number, " conversation" , None , Some ( " An empty comment says nothing." .into ( ) ) ) ) ;
de505df5 Andy McCall 1 month ago ↶
1095
}
1096
let anchor = match ( f.path.trim ( ) , f.line) {
1097
( " " , _ ) | ( _ , None ) => None ,
1098
( path, Some ( line) ) => {
1099
let loaded = load ( & app, & repo, p.clone ( ) ) .await.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
1100
loaded.atoms.get ( & ( path.to_string ( ) , line) ) .cloned ( )
1101
}
1102
} ;
1103
let tab = if anchor.is_some ( ) { " files" } else { " conversation" } ;
1104
pdb:: add_comment(
1105
& app.db,
1106
p.id,
1107
Some ( me.account.id) ,
1108
& me.account.username,
1109
f.body.trim ( ) ,
1110
f.path.trim ( ) ,
1111
anchor.as_ref ( ) .map ( | ( h , i ) | ( h.as_str ( ) , * i as i64 ) ) ,
1112
)
1113
.await
1114
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
33d98147 Andy McCall 16 days ago
1115
app.bus.live ( & crate :: events:: topic:: proposal( row.id, number) , " comment" ) ;
a1b98695 Andy McCall 1 month ago ↶
1116
Ok ( back ( & owner, & name, number, tab, None , None ) )
de505df5 Andy McCall 1 month ago ↶
1117
}
1118
1119
# [ derive ( Deserialize ) ]
1120
pub struct ReviewForm {
1121
#[serde(rename = "_csrf")]
1122
csrf_token : String,
1123
verdict : String,
1124
#[serde(default)]
1125
body : String,
1126
}
1127
1128
pub async fn review (
1129
State( app ) : State< Shared> ,
1130
viewer : Viewer,
1131
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1132
Form( f) : Form< ReviewForm> ,
a1b98695 Andy McCall 1 month ago ↶
1133
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
1134
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1135
let me = require ( & viewer, & shell) ? ;
1136
csrf ( & viewer, & f.csrf_token, & shell) ? ;
1137
let ( _ , row, _ ) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
d0fbea2e Andy McCall 1 month ago ↶
1138
super :: writable( & row, & shell) ? ;
de505df5 Andy McCall 1 month ago ↶
1139
let p = pdb:: get( & app.db, row.id, number)
1140
.await
1141
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1142
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1143
if ! matches! ( f.verdict.as_str ( ) , " comment" | " changes" | " approve" ) {
1144
return Err ( Error:: bad( shell, " a review is comment, changes or approve" ) ) ;
1145
}
1146
if f.verdict = = " approve" & & Some ( me.account.id) = = p.author_id {
a1b98695 Andy McCall 1 month ago ↶
1147
return Ok ( back ( & owner, & name, number, " conversation" , None , Some ( " You cannot approve your own proposal." .into ( ) ) ) ) ;
de505df5 Andy McCall 1 month ago ↶
1148
}
1149
pdb:: add_review( & app.db, p.id, me.account.id, & me.account.username, & f.verdict, f.body.trim ( ) )
1150
.await
1151
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
1152
pdb:: add_event( & app.db, p.id, & me.account.username, & format! ( " review.{} " , f.verdict) , " " ) .await.ok ( ) ;
cc1cd197 Andy McCall 1 month ago ↶
1153
reviewed ( & app, & row, & p, & me.account, & f.verdict) .await;
b64e2537 Andy McCall 1 month ago ↶
1154
crate :: hooks:: emit( & app, & row, " review" , & me.account.username, serde_json:: json! ( { " number" : number, " verdict" : f.verdict } ) ) .await;
de505df5 Andy McCall 1 month ago ↶
1155
let notice = if f.verdict = = " approve" {
1156
Some ( format! (
1157
" Approval recorded. It counts once you seal the changes: oxy seal --proposal {} /{owner} /{name} /proposal/{number} " ,
1158
app.config.base_url ( )
1159
) )
1160
} else {
1161
None
1162
} ;
a1b98695 Andy McCall 1 month ago ↶
1163
Ok ( back ( & owner, & name, number, " conversation" , notice, None ) )
de505df5 Andy McCall 1 month ago ↶
1164
}
1165
1166
# [ derive ( Deserialize ) ]
1167
pub struct EditForm {
1168
#[serde(rename = "_csrf")]
1169
csrf_token : String,
1170
title : String,
1171
#[serde(default)]
1172
body : String,
1173
target : String,
1174
}
1175
1176
pub async fn edit (
1177
State( app ) : State< Shared> ,
1178
viewer : Viewer,
1179
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1180
Form( f) : Form< EditForm> ,
a1b98695 Andy McCall 1 month ago ↶
1181
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
1182
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1183
let me = require ( & viewer, & shell) ? ;
1184
csrf ( & viewer, & f.csrf_token, & shell) ? ;
1185
let ( _ , row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
1186
let p = pdb:: get( & app.db, row.id, number)
1187
.await
1188
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1189
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1190
if Some ( me.account.id) ! = p.author_id & & role < Role:: Maintain {
1191
return Err ( Error:: not_found( shell) ) ;
1192
}
1193
let target = if crate :: repos:: names:: valid_segment( f.target.trim ( ) ) { f.target.trim ( ) } else { & p.target } ;
1194
pdb:: edit( & app.db, p.id, f.title.trim ( ) , f.body.trim ( ) , target) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
1195
pdb:: add_event( & app.db, p.id, & me.account.username, " edited" , " " ) .await.ok ( ) ;
a1b98695 Andy McCall 1 month ago ↶
1196
Ok ( back ( & owner, & name, number, " conversation" , Some ( " Saved." .into ( ) ) , None ) )
de505df5 Andy McCall 1 month ago ↶
1197
}
1198
1199
# [ derive ( Deserialize ) ]
1200
pub struct StateForm {
1201
#[serde(rename = "_csrf")]
1202
csrf_token : String,
1203
action : String,
1204
}
1205
1206
pub async fn state (
1207
State( app ) : State< Shared> ,
1208
viewer : Viewer,
1209
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1210
Form( f) : Form< StateForm> ,
a1b98695 Andy McCall 1 month ago ↶
1211
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
1212
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1213
let me = require ( & viewer, & shell) ? ;
1214
csrf ( & viewer, & f.csrf_token, & shell) ? ;
1215
let ( _ , row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
1216
let p = pdb:: get( & app.db, row.id, number)
1217
.await
1218
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1219
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1220
if Some ( me.account.id) ! = p.author_id & & role < Role:: Triage {
1221
return Err ( Error:: not_found( shell) ) ;
1222
}
1223
let new_state = match ( f.action.as_str ( ) , p.state.as_str ( ) ) {
1224
( " close" , " open" | " draft" ) => " closed" ,
1225
( " reopen" , " closed" ) => " open" ,
1226
( " ready" , " draft" ) => " open" ,
1227
( " draft" , " open" ) => " draft" ,
1228
_ => return Err ( Error:: bad( shell, " that transition is not possible" ) ) ,
1229
} ;
1230
pdb:: set_state( & app.db, p.id, new_state, " " ) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
1231
pdb:: add_event( & app.db, p.id, & me.account.username, new_state, " " ) .await.ok ( ) ;
b64e2537 Andy McCall 1 month ago ↶
1232
crate :: hooks:: emit( & app, & row, & format! ( " proposal.{new_state} " ) , & me.account.username, serde_json:: json! ( { " number" : number } ) ) .await;
a1b98695 Andy McCall 1 month ago ↶
1233
Ok ( back ( & owner, & name, number, " conversation" , None , None ) )
de505df5 Andy McCall 1 month ago ↶
1234
}
1235
1236
pub async fn resolve_comment (
1237
State( app ) : State< Shared> ,
1238
viewer : Viewer,
1239
Path( (owner , name , number , id ) ) : Path< ( String, String, i64 , i64 ) > ,
1240
Form( f) : Form< super:: session:: CsrfForm> ,
a1b98695 Andy McCall 1 month ago ↶
1241
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
1242
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1243
require ( & viewer, & shell) ? ;
1244
csrf ( & viewer, & f.csrf, & shell) ? ;
1245
let ( _ , row, _ ) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
1246
let p = pdb:: get( & app.db, row.id, number)
1247
.await
1248
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1249
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
5d90e0e0 Andy McCall 19 days ago
1250
let current: Option< bool > = sqlx:: query_scalar( " SELECT resolved FROM proposal_comments WHERE id = $1 AND proposal_id = $2" )
de505df5 Andy McCall 1 month ago ↶
1251
.bind ( id)
1252
.bind ( p.id)
1253
.fetch_optional ( & app.db)
1254
.await
1255
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
1256
if let Some ( r) = current {
1257
pdb:: set_comment_resolved( & app.db, id, ! r) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
1258
}
a1b98695 Andy McCall 1 month ago ↶
1259
Ok ( back ( & owner, & name, number, " files" , None , None ) )
de505df5 Andy McCall 1 month ago ↶
1260
}
1261
1262
1263
2a8b6216 Andy McCall 17 days ago ↶
1264
# [ derive ( serde::Deserialize ) ]
1265
pub struct LandForm {
1266
#[serde(rename = "_csrf")]
1267
csrf : String,
1268
1269
#[serde(default)]
1270
over_checks : Option< String> ,
1271
}
1272
de505df5 Andy McCall 1 month ago ↶
1273
pub async fn land (
1274
State( app ) : State< Shared> ,
1275
viewer : Viewer,
1276
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
2a8b6216 Andy McCall 17 days ago ↶
1277
Form( f) : Form< LandForm> ,
a1b98695 Andy McCall 1 month ago ↶
1278
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
1279
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1280
let me = require ( & viewer, & shell) ? ;
1281
csrf ( & viewer, & f.csrf, & shell) ? ;
2a8b6216 Andy McCall 17 days ago ↶
1282
let need = if f.over_checks.is_some ( ) { Access:: Maintain } else { Access:: Write } ;
1283
let ( repo, row, _ ) = lookup_role ( & app, & shell, & viewer, & owner, & name, need) .await? ;
1284
match land_as ( & app, & repo, & row, & me.account, number, " web" , false , f.over_checks.as_deref ( ) ) .await {
a1b98695 Andy McCall 1 month ago ↶
1285
Ok ( _ ) => Ok ( back ( & owner, & name, number, " conversation" , Some ( " Landed." .into ( ) ) , None ) ) ,
1f1eb06a Andy McCall 1 month ago ↶
1286
Err ( Landing:: NotFound) => Err ( Error:: not_found( shell) ) ,
a1b98695 Andy McCall 1 month ago ↶
1287
Err ( Landing:: Refused( m) ) => Ok ( back ( & owner, & name, number, " conversation" , None , Some ( m) ) ) ,
1f1eb06a Andy McCall 1 month ago ↶
1288
}
1289
}
1290
86a69b53 Andy McCall 17 days ago ↶
1291
# [ derive ( serde::Deserialize ) ]
1292
pub struct AutoLandForm {
1293
#[serde(rename = "_csrf")]
1294
csrf : String,
1295
1296
#[serde(default)]
1297
on : String,
1298
}
1299
1300
1301
1302
pub async fn auto_land (
1303
State( app ) : State< Shared> ,
1304
viewer : Viewer,
1305
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1306
Form( f) : Form< AutoLandForm> ,
1307
) -> Result< Response, Error> {
1308
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1309
let me = require ( & viewer, & shell) ? ;
1310
csrf ( & viewer, & f.csrf, & shell) ? ;
1311
let ( _ , row, _ ) = lookup_role ( & app, & shell, & viewer, & owner, & name, Access:: Write) .await? ;
1312
let p = pdb:: get( & app.db, row.id, number)
1313
.await
1314
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1315
.filter ( | p | p.is_open ( ) )
1316
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1317
let on = f.on = = " 1" ;
1318
crate :: autoland:: set( & app, & row, & p, & me.account, on) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
1319
let note = if on { " It will land when its checks pass." } else { " It will not land by itself." } ;
1320
Ok ( back ( & owner, & name, number, " conversation" , Some ( note.into ( ) ) , None ) )
1321
}
1322
217814c2 Andy McCall 8 days ago ↶
1323
1324
1325
1326
1327
1328
1329
pub async fn resolve_on_web (
1330
State( app ) : State< Shared> ,
1331
viewer : Viewer,
1332
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1333
Form( f) : Form< LandForm> ,
1334
) -> Result< Response, Error> {
1335
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1336
let me = require ( & viewer, & shell) ? ;
1337
csrf ( & viewer, & f.csrf, & shell) ? ;
1338
let ( repo, row, _ ) = lookup_role ( & app, & shell, & viewer, & owner, & name, Access:: Write) .await? ;
1339
let p = pdb:: get( & app.db, row.id, number)
1340
.await
1341
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1342
.filter ( | p | p.is_open ( ) )
1343
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1344
if p.source_kind ! = " bookmark" {
1345
let msg = " Its source is not a bookmark of this repository: resolve it there, then push." .to_string ( ) ;
1346
return Ok ( back ( & owner, & name, number, " conversation" , None , Some ( msg) ) ) ;
1347
}
1348
let ( source, target) = ( p.source_bookmark.clone ( ) , p.target.clone ( ) ) ;
1349
let ( s, t) = ( source.clone ( ) , target.clone ( ) ) ;
1350
let union = app
1351
.repos
1352
.read ( & repo, move | r, _ | {
1353
let view = r.view ( ) ? ;
1354
let ( Some ( ours) , Some ( theirs) ) = ( view.bookmarks.get ( & s) .copied ( ) , view.bookmarks.get ( & t) .copied ( ) ) else { return Ok ( None ) } ;
1355
let mut all = r.store ( ) .get_state ( & ours) ? .heads;
1356
all.extend ( r.store ( ) .get_state ( & theirs) ? .heads) ;
1357
all.sort ( ) ;
1358
all.dedup ( ) ;
1359
let state = r.load_state ( & all) ? ;
1360
Ok ( Some ( state.maximal ( & all) ) )
1361
} )
1362
.await
1363
.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
1364
let Some ( union) = union else {
1365
return Ok ( back ( & owner, & name, number, " conversation" , None , Some ( format! ( " {source} or {target} is not a bookmark any more." ) ) ) ) ;
1366
} ;
1367
if let Err ( why) = super :: refs:: set_bookmark( & app, & repo, me.account.id, & source, union.clone ( ) ) .await {
1368
return Ok ( back ( & owner, & name, number, " conversation" , None , Some ( format! ( " Could not bring {target} into {source} : {why} " ) ) ) ) ;
1369
}
1370
let detail = format! ( " {target} into {source} for proposal #{number} " ) ;
1371
accounts:: audit( & app.db, Some ( & me.account) , " proposal.merged_target" , & format! ( " {owner} /{name} " ) , & detail) .await.ok ( ) ;
1372
let s = source.clone ( ) ;
1373
let first = app
1374
.repos
1375
.read ( & repo, move | r, cache| Ok ( view:: conflicts( r, cache) ? .into_iter ( ) .find ( | c | c.bookmark = = s ) .map ( | c | c.path ) ) )
1376
.await
1377
.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
1378
Ok ( match first {
1379
Some ( path) => Redirect:: to( & format! ( " /{owner} /{name} /resolve/{source} /{path} " ) ) .into_response ( ) ,
1380
None => {
1381
back ( & owner, & name, number, " conversation" , Some ( format! ( " Brought {target} into {source} : nothing left to resolve." ) ) , None )
1382
}
1383
} )
1384
}
1385
1f1eb06a Andy McCall 1 month ago ↶
1386
1387
pub enum Landing {
1388
NotFound,
1389
Refused( String ) ,
1390
}
1391
1392
1393
1394
0b20f1ec Andy McCall 1 month ago ↶
1395
f4d6ef41 Andy McCall 22 days ago ↶
1396
2a8b6216 Andy McCall 17 days ago ↶
1397
1398
1399
1400
1401
# [ allow ( clippy::too_many_arguments ) ]
1f1eb06a Andy McCall 1 month ago ↶
1402
pub async fn land_as (
1403
app : & Shared,
1404
repo : & RepoName,
1405
row : & db:: repos:: RepoRow,
1406
me : & accounts:: Account,
1407
number : i64 ,
0b20f1ec Andy McCall 1 month ago ↶
1408
via : & str ,
f4d6ef41 Andy McCall 22 days ago ↶
1409
as_hub : bool ,
2a8b6216 Andy McCall 17 days ago ↶
1410
over_checks : Option< & str > ,
1f1eb06a Andy McCall 1 month ago ↶
1411
) -> Result< String, Landing> {
1412
let ( owner, name) = ( row.owner.clone ( ) , row.name.clone ( ) ) ;
1413
let refused = | e : String| Landing:: Refused( e) ;
1414
let p = pdb:: get( & app.db, row.id, number) .await.map_err ( | e | refused ( e.to_string ( ) ) ) ? .ok_or ( Landing:: NotFound) ? ;
1415
let loaded = load ( app, repo, p.clone ( ) ) .await.map_err ( | e | refused ( e.to_string ( ) ) ) ? ;
1416
let v = verdict ( app, & loaded, row.id) .await.map_err ( | e | refused ( e.to_string ( ) ) ) ? ;
1417
if ! p.is_open ( ) {
1418
return Err ( refused ( format! ( " Cannot land: the proposal is {} " , p.state) ) ) ;
2a8b6216 Andy McCall 17 days ago ↶
1419
}
1420
let reason = over_checks.map ( str :: trim) ;
1421
if reason = = Some ( " " ) {
1422
return Err ( refused ( " Landing over checks needs a reason: say why it can't wait for them" .into ( ) ) ) ;
1f1eb06a Andy McCall 1 month ago ↶
1423
}
2a8b6216 Andy McCall 17 days ago ↶
1424
let blocking: Vec< & String> = v.blockers.iter ( ) .filter ( | b | ! ( reason.is_some ( ) & & v.checks_blocker.as_ref ( ) = = Some ( * b) ) ) .collect ( ) ;
1425
if ! blocking.is_empty ( ) {
1426
let list: Vec< & str > = blocking.iter ( ) .map ( | b | b.as_str ( ) ) .collect ( ) ;
1427
return Err ( refused ( format! ( " Cannot land: {} " , list.join ( " ; " ) ) ) ) ;
de505df5 Andy McCall 1 month ago ↶
1428
}
2a8b6216 Andy McCall 17 days ago ↶
1429
1430
let over = reason.filter ( | _| v.checks_blocker.is_some ( ) ) ;
1431
let via_owned = match over {
1432
Some ( r) => format! ( " {via} , over checks: {r} " ) ,
1433
None => via.to_string ( ) ,
1434
} ;
1435
let via = via_owned.as_str ( ) ;
de505df5 Andy McCall 1 month ago ↶
1436
let target = p.target.clone ( ) ;
1437
let heads = loaded.heads.clone ( ) ;
14530604 Andy McCall 1 month ago ↶
1438
1439
1440
1441
f4d6ef41 Andy McCall 22 days ago ↶
1442
let signer = if as_hub { super :: edit:: hub_signer( app) } else { super :: edit:: signer_for( app, me.id) .await } ;
dd0788b7 Andy McCall 9 days ago ↶
1443
1444
1445
let union = loaded.union.clone ( ) .filter ( | _| loaded.conflicted ) ;
1446
let mut heads = heads;
1447
let result = match ( signer, union) {
1448
( Ok ( signer) , Some ( files) ) => {
1449
let names: Vec< & str > = files.iter ( ) .map ( | ( p , _) | p.as_str ( ) ) .collect ( ) ;
1450
let message = format! ( " Keep both sides in {} (merge=union), landing #{number} " , names.join ( " , " ) ) ;
1451
let edits = files.into_iter ( ) .map ( | ( path , text ) | oxy_core:: repo:: FileEdit { path, content: Some ( text) } ) .collect ( ) ;
1452
crate :: repos:: edits:: land_resolved( & app.repos, repo, target.clone ( ) , heads.clone ( ) , edits, message, signer) .await.map ( | h | {
1453
heads = h;
1454
pl:: state_ref( & heads) .to_hex ( )
1455
} )
1456
}
1457
( Err ( why) , Some ( _ ) ) => {
1458
return Err ( refused ( format! (
1459
" Cannot land: its changelog-style conflicts are settled by a resolution the hub signs for you, and it cannot: {why} "
1460
) ) )
1461
}
1462
( signer, None ) => match signer {
1463
Ok ( signer) => crate :: repos:: edits:: move_bookmark( & app.repos, repo, target.clone ( ) , heads.clone ( ) , signer)
1464
.await
1465
.map ( | _| pl:: state_ref( & heads) .to_hex ( ) ) ,
1466
Err ( why) => {
1467
let ( target, heads) = ( target.clone ( ) , heads.clone ( ) ) ;
1468
app.repos
1f1eb06a Andy McCall 1 month ago ↶
1469
.write ( repo, move | r, _ | {
14530604 Andy McCall 1 month ago ↶
1470
if r.current_policy ( ) ? .is_some ( ) {
1471
return Err ( crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( format! (
1472
" this repository has a trust policy, so the landing must be signed by a key it authorises. Land from a client with oxy proposal land, or delegate to the hub. {why} "
1473
) ) ) ) ;
1474
}
1475
r.bookmark_set ( & target, & heads) ? ;
1476
Ok ( pl:: state_ref( & heads) .to_hex ( ) )
1477
} )
1478
.await
dd0788b7 Andy McCall 9 days ago ↶
1479
}
1480
} ,
14530604 Andy McCall 1 month ago ↶
1481
} ;
1f1eb06a Andy McCall 1 month ago ↶
1482
let state = result.map_err ( | e | refused ( e.to_string ( ) ) ) ? ;
0b20f1ec Andy McCall 1 month ago ↶
1483
pdb:: set_landed( & app.db, p.id, & state, & me.username, via) .await.map_err ( | e | refused ( e.to_string ( ) ) ) ? ;
2abea308 Andy McCall 17 days ago ↶
1484
crate :: flow:: moot_after_landing( app, row, & p) .await;
0b20f1ec Andy McCall 1 month ago ↶
1485
let how = if v.approvals.is_empty ( ) { format! ( " {via} , with no approval" ) } else { via.to_string ( ) } ;
1486
pdb:: add_event( & app.db, p.id, & me.username, " landed" , & how) .await.ok ( ) ;
1f1eb06a Andy McCall 1 month ago ↶
1487
crate :: hooks:: emit(
1488
app,
1489
row,
1490
" proposal.landed" ,
1491
& me.username,
1492
serde_json:: json! ( { " number" : number, " target" : p.target, " state" : state } ) ,
1493
)
1494
.await;
1495
crate :: notify:: deliver(
1496
app,
1497
crate :: notify:: Event {
1498
kind: " proposal.landed" ,
1499
title: format! ( " {owner} /{name} #{number} : {} landed" , p.title) ,
1500
url: format! ( " /{owner} /{name} /proposal/{number} " ) ,
1501
actor: me,
1502
repo_id: row.id,
1503
thread: Some ( ( " proposal" , p.id) ) ,
1504
mention_text: " " ,
1505
direct: p.author_id.into_iter ( ) .collect ( ) ,
1506
} ,
1507
)
1508
.await;
1509
if p.target = = row.default_bookmark {
1510
let messages: Vec< ( String, String) > = loaded.changes.iter ( ) .map ( | c | ( c.hash.clone ( ) , format! ( " {} \n {} " , c.title, c.body) ) ) .collect ( ) ;
1511
super :: issues:: close_from_messages( app, row, & messages) .await;
de505df5 Andy McCall 1 month ago ↶
1512
}
0b20f1ec Andy McCall 1 month ago ↶
1513
accounts:: audit( & app.db, Some ( me) , " proposal.landed" , & format! ( " {owner} /{name} #{number} " ) , & how) .await.ok ( ) ;
2a8b6216 Andy McCall 17 days ago ↶
1514
if let ( Some ( r) , Some ( checks) ) = ( over, & v.checks_blocker) {
1515
accounts:: audit( & app.db, Some ( me) , " proposal.land_over_checks" , & format! ( " {owner} /{name} #{number} " ) , & format! ( " {r} ({checks} )" ) )
1516
.await
1517
.ok ( ) ;
1518
}
1f1eb06a Andy McCall 1 month ago ↶
1519
db:: repos:: record_push(
1520
& app.db,
1521
row.id,
1522
Some ( ( me.id, me.username.as_str ( ) ) ) ,
1523
& [ db:: repos:: PushOutcome {
1524
reference: p.target.clone ( ) ,
1525
ok: true ,
0b20f1ec Andy McCall 1 month ago ↶
1526
reason: format! ( " landed #{number} ({how} )" ) ,
1f1eb06a Andy McCall 1 month ago ↶
1527
old_state: loaded.target_state.clone ( ) ,
1528
new_state: state.clone ( ) ,
1529
heads: loaded.heads.len ( ) as i64 ,
1530
changes: loaded.hashes.len ( ) as i64 ,
1531
} ] ,
1532
)
1533
.await
1534
.ok ( ) ;
d943160d Andy McCall 22 days ago ↶
1535
1536
settle_after_push ( app, row, & p.target, & me.username) .await;
f7bcee49 Andy McCall 21 days ago ↶
1537
1538
1539
dd0788b7 Andy McCall 9 days ago ↶
1540
crate :: flow:: on_push( app, row, & p.target, & state, & heads, & me.username) .await;
acc85ac0 Andy McCall 22 days ago ↶
1541
1542
1543
if p.target = = row.default_bookmark {
1544
let body = ( format! ( " proposal #{number} " ) , p.body.clone ( ) ) ;
dd0788b7 Andy McCall 9 days ago ↶
1545
crate :: web:: issues:: close_from_arrived( app, row, loaded.target_heads.clone ( ) , heads.clone ( ) , Some ( body) ) .await;
acc85ac0 Andy McCall 22 days ago ↶
1546
}
757195ad Andy McCall 22 days ago ↶
1547
1548
1549
dd0788b7 Andy McCall 9 days ago ↶
1550
let heads = heads.clone ( ) ;
757195ad Andy McCall 22 days ago ↶
1551
let moved = vec! [ ( p.target.clone ( ) , state.clone ( ) , heads.clone ( ) ) ] ;
1552
crate :: diffusion:: after_push( app, row, & moved) .await;
1553
if p.target = = row.default_bookmark {
1554
let ( app, row) = ( app.clone ( ) , row.clone ( ) ) ;
1555
tokio:: spawn( async move { crate :: diffusion:: propagate( & app, & row, heads, false ) .await } ) ;
1556
}
1f1eb06a Andy McCall 1 month ago ↶
1557
Ok ( state)
de505df5 Andy McCall 1 month ago ↶
1558
}
1559
18e381ea Andy McCall 1 month ago ↶
1560
# [ derive ( Deserialize ) ]
1561
pub struct LabelsForm {
1562
#[serde(rename = "_csrf")]
1563
csrf_token : String,
1564
#[serde(default)]
1565
label : Vec< i64 > ,
1566
}
1567
1568
pub async fn set_labels (
1569
State( app ) : State< Shared> ,
1570
viewer : Viewer,
1571
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1572
axum_extra:: extract:: Form( f) : axum_extra:: extract:: Form< LabelsForm> ,
a1b98695 Andy McCall 1 month ago ↶
1573
) -> Result< Response, Error> {
18e381ea Andy McCall 1 month ago ↶
1574
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1575
require ( & viewer, & shell) ? ;
1576
csrf ( & viewer, & f.csrf_token, & shell) ? ;
1577
let ( _ , row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
1578
if role < Role:: Triage {
1579
return Err ( Error:: not_found( shell) ) ;
1580
}
1581
let p = pdb:: get( & app.db, row.id, number)
1582
.await
1583
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1584
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1585
for l in db:: issues:: labels( & app.db, row.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? {
1586
db:: issues:: set_proposal_label( & app.db, p.id, l.id, f.label.contains ( & l.id) ) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
1587
}
a1b98695 Andy McCall 1 month ago ↶
1588
Ok ( back ( & owner, & name, number, " conversation" , None , None ) )
18e381ea Andy McCall 1 month ago ↶
1589
}
1590
b64e2537 Andy McCall 1 month ago ↶
1591
1592
1593
pub async fn events (
1594
State( app ) : State< Shared> ,
1595
viewer : Viewer,
1596
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1597
) -> Result< axum:: response:: Sse< impl futures:: Stream< Item = Result< axum:: response:: sse:: Event, std:: convert:: Infallible> > > , Error> {
1598
use futures:: StreamExt;
1599
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1600
let ( _ , row, _ ) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
33d98147 Andy McCall 16 days ago
1601
let rx = app.bus.subscribe ( crate :: bus:: Topic:: Proposals) ;
b64e2537 Andy McCall 1 month ago ↶
1602
let repo_id = row.id;
1603
let stream = tokio_stream:: wrappers:: BroadcastStream:: new( rx) .filter_map ( move | item| async move {
1604
match item {
33d98147 Andy McCall 16 days ago
1605
Ok ( crate :: bus:: Event:: Proposal { repo, event: ev } ) if repo = = repo_id => {
b64e2537 Andy McCall 1 month ago ↶
1606
Some ( Ok ( axum:: response:: sse:: Event:: default( ) .event ( ev.kind.clone ( ) ) .data ( serde_json:: to_string( & ev) .unwrap_or_default ( ) ) ) )
1607
}
1608
_ => None ,
1609
}
1610
} ) ;
c92a9949 Andy McCall 19 days ago ↶
1611
Ok ( axum:: response:: Sse:: new( futures:: StreamExt:: take_until( stream, crate :: shutdown:: stopping( ) ) )
1612
.keep_alive ( axum:: response:: sse:: KeepAlive:: default( ) ) )
b64e2537 Andy McCall 1 month ago ↶
1613
}
1614
de505df5 Andy McCall 1 month ago ↶
1615
1616
1617
pub async fn to_api ( app : & Shared, owner : & str , name : & str , loaded : & Loaded, v : & Verdict, source_url : Option< String> ) -> hub_api:: Proposal {
1618
let p = & loaded.row;
1619
hub_api:: Proposal {
1620
number: p.number,
1621
title: p.title.clone ( ) ,
1622
body: p.body.clone ( ) ,
1623
author: p.author.clone ( ) ,
1624
state: ProposalState:: parse( & p.state) .unwrap_or ( ProposalState:: Open) ,
1625
target: p.target.clone ( ) ,
1626
source: match p.source_kind.as_str ( ) {
1627
" fork" => {
1628
let ( o, n) = source_url
1629
.as_deref ( )
1630
.and_then ( | u | u.rsplit_once ( ' /' ) )
1631
.and_then ( | ( b , n ) | b.rsplit_once ( ' /' ) .map ( | ( _, o ) | ( o.to_string ( ) , n.to_string ( ) ) ) )
1632
.unwrap_or_default ( ) ;
1633
ProposalSource:: Fork { owner: o, name: n, bookmark: p.source_bookmark.clone ( ) }
1634
}
1635
" changes" => ProposalSource:: Changes { hashes: p.source_changes ( ) } ,
1636
_ => ProposalSource:: Bookmark { bookmark: p.source_bookmark.clone ( ) } ,
1637
} ,
1638
changes: loaded
1639
.changes
1640
.iter ( )
1641
.map ( | c | hub_api:: ChangeSummary {
1642
hash: c.hash.clone ( ) ,
1643
message: if c.body.is_empty ( ) { c.title.clone ( ) } else { format! ( " {} \n \n {} " , c.title, c.body) } ,
1644
author: hub_api:: Author {
1645
name: c.author.name.clone ( ) ,
1646
email: c.author.email.clone ( ) ,
1647
key: c.author.key.clone ( ) ,
1648
account: c.author.account.clone ( ) ,
760a7dd3 Andy McCall 1 month ago ↶
1649
note: hub_api:: Author:: note_for( & c.author.key, & c.author.account) ,
de505df5 Andy McCall 1 month ago ↶
1650
} ,
1651
when_ms: c.when_ms,
1652
deps: c.deps.clone ( ) ,
1653
head: c.head,
8933faff Andy McCall 1 month ago ↶
1654
via: c.via.as_ref ( ) .map ( | v | hub_api:: Via {
1655
name: v.name.clone ( ) ,
1656
key: v.key.clone ( ) ,
1657
scope: v.scope.clone ( ) ,
1658
expires_ms: v.expires_ms,
1659
revoked: v.revoked,
1660
} ) ,
de505df5 Andy McCall 1 month ago ↶
1661
signature: match & c.signature {
1662
view:: SigView:: Unsigned => hub_api:: Signature:: Unsigned,
1663
view:: SigView:: Valid { key, sealed, revoked, .. } => {
1664
hub_api:: Signature:: Valid { key: key.clone ( ) , sealed: * sealed, revoked: * revoked }
1665
}
1666
view:: SigView:: Invalid( why) => hub_api:: Signature:: Invalid { reason: why.clone ( ) } ,
1667
} ,
1668
} )
1669
.collect ( ) ,
1670
projected_heads: loaded.heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ,
1671
projected_state: Some ( loaded.state.clone ( ) ) ,
1672
conflicted: loaded.conflicted,
1673
approvals: v.approvals.clone ( ) ,
1674
landable: p.is_open ( ) & & v.blockers.is_empty ( ) ,
1675
blockers: v.blockers.clone ( ) ,
1676
created_ms: p.created_ms,
1677
updated_ms: p.updated_ms,
1678
url: format! ( " {} /{owner} /{name} /proposal/{} " , app.config.base_url ( ) , p.number) ,
1679
repository_url: format! ( " {} /{owner} /{name} " , app.config.base_url ( ) ) ,
1680
source_url,
0b20f1ec Andy McCall 1 month ago ↶
1681
landed: ( p.state = = " landed" & & p.landed_ms > 0 ) .then ( | | hub_api:: Landed {
1682
by: p.landed_by.clone ( ) ,
1683
via: p.landed_via.clone ( ) ,
1684
when_ms: p.landed_ms,
1685
} ) ,
86a69b53 Andy McCall 17 days ago ↶
1686
auto_land: ( p.is_open ( ) & & ! p.auto_land_by.is_empty ( ) ) .then ( | | p.auto_land_by.clone ( ) ) ,
cc1cd197 Andy McCall 1 month ago ↶
1687
requested_reviewers: pdb:: review_requests( & app.db, p.id) .await.unwrap_or_default ( ) ,
2291f8cc Andy McCall 1 month ago ↶
1688
labels: db:: issues:: proposal_labels( & app.db, p.id)
1689
.await
1690
.unwrap_or_default ( )
1691
.into_iter ( )
1692
.map ( | l | hub_api:: Label { name: l.name, colour: l.colour, description: l.description } )
1693
.collect ( ) ,
de505df5 Andy McCall 1 month ago ↶
1694
}
1695
}
1696
1697
1698
pub fn key ( path : & str , line : impl filters:: Num) -> ( String, usize ) {
1699
( path.to_string ( ) , line.as_u64 ( ) as usize )
1700
}
1701
1702
# [ allow ( dead_code ) ]
1703
fn _unused ( _ : & diff:: Line, _ : & ConflictView, _ : & Role) { }
cc1cd197 Andy McCall 1 month ago ↶
1704
1705
1706
1707
1708
1709
1710
1711
pub async fn request_reviewers (
1712
app : & Shared,
1713
row : & db:: repos:: RepoRow,
1714
p : & pdb:: ProposalRow,
1715
by : & accounts:: Account,
1716
names : & [String],
1717
) -> Result< Vec< String> , String> {
1718
let mut found = Vec :: new( ) ;
1719
for name in names.iter ( ) .map ( | n | n.trim ( ) .trim_start_matches ( ' @' ) ) .filter ( | n | ! n.is_empty ( ) ) {
1720
let Some ( a) = accounts:: account_by_name( & app.db, name) .await.ok ( ) .flatten ( ) else {
1721
return Err ( format! ( " There is no account {name} ." ) ) ;
1722
} ;
1723
if Some ( a.id) = = p.author_id {
1724
return Err ( format! ( " {name} wrote this proposal, and cannot review it." ) ) ;
1725
}
1726
let role = crate :: auth:: roles:: role_for( & app.db, Some ( & a) , row) .await.ok ( ) .flatten ( ) ;
1727
if ! crate :: auth:: roles:: allows( role, crate :: auth:: Access:: Read) {
1728
return Err ( format! ( " {name} cannot read this repository." ) ) ;
1729
}
1730
found.push ( a) ;
1731
}
1732
let mut asked = Vec :: new( ) ;
1733
for a in found {
1734
if ! pdb:: request_review( & app.db, p.id, a.id, & by.username) .await.map_err ( | e | e.to_string ( ) ) ? {
1735
continue ;
1736
}
1737
pdb:: add_event( & app.db, p.id, & by.username, " review requested" , & a.username) .await.ok ( ) ;
1738
crate :: notify:: deliver(
1739
app,
1740
crate :: notify:: Event {
1741
kind: " proposal.review_requested" ,
1742
title: format! ( " {} /{} #{} : {} (review requested from {} )" , row.owner, row.name, p.number, p.title, a.username) ,
1743
url: format! ( " /{} /{} /proposal/{} " , row.owner, row.name, p.number) ,
1744
actor: by,
1745
repo_id: row.id,
1746
thread: None ,
1747
mention_text: " " ,
1748
direct: vec! [ a.id] ,
1749
} ,
1750
)
1751
.await;
1752
asked.push ( a.username) ;
1753
}
1754
Ok ( asked)
1755
}
1756
1757
1758
1759
pub async fn reviewed ( app : & Shared, row : & db:: repos:: RepoRow, p : & pdb:: ProposalRow, by : & accounts:: Account, verdict : & str ) {
1760
pdb:: unrequest_review( & app.db, p.id, by.id) .await.ok ( ) ;
1761
let what = match verdict {
1762
" approve" => " approved" ,
1763
" changes" => " changes requested" ,
1764
_ => " reviewed" ,
1765
} ;
1766
crate :: notify:: deliver(
1767
app,
1768
crate :: notify:: Event {
1769
kind: " proposal.review" ,
1770
title: format! ( " {} /{} #{} : {} ({what} by {} )" , row.owner, row.name, p.number, p.title, by.username) ,
1771
url: format! ( " /{} /{} /proposal/{} " , row.owner, row.name, p.number) ,
1772
actor: by,
1773
repo_id: row.id,
1774
thread: Some ( ( " proposal" , p.id) ) ,
1775
mention_text: " " ,
1776
direct: p.author_id.into_iter ( ) .collect ( ) ,
1777
} ,
1778
)
1779
.await;
1780
}
1781
1782
# [ derive ( Deserialize ) ]
1783
pub struct ReviewersForm {
1784
#[serde(rename = "_csrf")]
1785
csrf_token : String,
8e96499f Andy McCall 1 month ago ↶
1786
cc1cd197 Andy McCall 1 month ago ↶
1787
action : String,
8e96499f Andy McCall 1 month ago ↶
1788
#[serde(default)]
1789
username : Vec< String> ,
cc1cd197 Andy McCall 1 month ago ↶
1790
}
1791
1792
1793
1794
pub async fn reviewers (
1795
State( app ) : State< Shared> ,
1796
viewer : Viewer,
1797
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
8e96499f Andy McCall 1 month ago ↶
1798
axum_extra:: extract:: Form( f) : axum_extra:: extract:: Form< ReviewersForm> ,
cc1cd197 Andy McCall 1 month ago ↶
1799
) -> Result< Response, Error> {
1800
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1801
let me = require ( & viewer, & shell) ? ;
1802
csrf ( & viewer, & f.csrf_token, & shell) ? ;
1803
let ( _ , row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
1804
let p = pdb:: get( & app.db, row.id, number)
1805
.await
1806
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1807
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1808
if Some ( me.account.id) ! = p.author_id & & role < Role:: Write {
1809
return Err ( Error:: not_found( shell) ) ;
1810
}
8e96499f Andy McCall 1 month ago ↶
1811
let names: Vec< String> = f.username.iter ( ) .map ( | n | n.trim ( ) .trim_start_matches ( ' @' ) .to_string ( ) ) .filter ( | n | ! n.is_empty ( ) ) .collect ( ) ;
1812
let current = pdb:: review_requests( & app.db, p.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
1813
1814
let withdraw: Vec< String> = match f.action.as_str ( ) {
1815
" remove" => names.clone ( ) ,
1816
" set" => current.iter ( ) .filter ( | c | ! names.iter ( ) .any ( | n | n.eq_ignore_ascii_case ( c) ) ) .cloned ( ) .collect ( ) ,
1817
_ => Vec :: new( ) ,
1818
} ;
1819
for who in & withdraw {
1820
if let Some ( a) = accounts:: account_by_name( & app.db, who) .await.ok ( ) .flatten ( ) {
cc1cd197 Andy McCall 1 month ago ↶
1821
if pdb:: unrequest_review( & app.db, p.id, a.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? {
1822
pdb:: add_event( & app.db, p.id, & me.account.username, " review request withdrawn" , & a.username) .await.ok ( ) ;
1823
}
1824
}
8e96499f Andy McCall 1 month ago ↶
1825
}
1826
let ask: Vec< String> = match f.action.as_str ( ) {
1827
" add" => names,
1828
" set" => names.into_iter ( ) .filter ( | n | ! current.iter ( ) .any ( | c | c.eq_ignore_ascii_case ( n) ) ) .collect ( ) ,
1829
_ => return Ok ( back ( & owner, & name, number, " conversation" , None , None ) ) ,
1830
} ;
1831
if ask.is_empty ( ) {
cc1cd197 Andy McCall 1 month ago ↶
1832
return Ok ( back ( & owner, & name, number, " conversation" , None , None ) ) ;
1833
}
8e96499f Andy McCall 1 month ago ↶
1834
match request_reviewers ( & app, & row, & p, & me.account, & ask) .await {
cc1cd197 Andy McCall 1 month ago ↶
1835
Ok ( asked) if asked.is_empty ( ) => Ok ( back ( & owner, & name, number, " conversation" , Some ( " Already asked." .into ( ) ) , None ) ) ,
1836
Ok ( asked) => Ok ( back ( & owner, & name, number, " conversation" , Some ( format! ( " Asked {} to review." , asked.join ( " , " ) ) ) , None ) ) ,
1837
Err ( e) => Ok ( back ( & owner, & name, number, " conversation" , None , Some ( e) ) ) ,
7d2eb590 Andy McCall 1 month ago ↶
1838
}
1839
}
1840
1841
# [ derive ( Deserialize ) ]
1842
pub struct AssistantForm {
1843
#[serde(rename = "_csrf")]
1844
csrf_token : String,
1845
assistant : String,
ffb67a81 Andy McCall 21 days ago
1846
1847
#[serde(default)]
1848
task : String,
7d2eb590 Andy McCall 1 month ago ↶
1849
}
1850
1851
1852
1853
1854
pub async fn assistant (
1855
State( app ) : State< Shared> ,
1856
viewer : Viewer,
1857
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1858
Form( f) : Form< AssistantForm> ,
1859
) -> Result< Response, Error> {
1860
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1861
let me = require ( & viewer, & shell) ? ;
1862
csrf ( & viewer, & f.csrf_token, & shell) ? ;
1863
let ( _ , row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
d0fbea2e Andy McCall 1 month ago ↶
1864
super :: writable( & row, & shell) ? ;
7d2eb590 Andy McCall 1 month ago ↶
1865
let p = pdb:: get( & app.db, row.id, number)
1866
.await
1867
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1868
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1869
if Some ( me.account.id) ! = p.author_id & & role < Role:: Write {
1870
return Err ( Error:: not_found( shell) ) ;
1871
}
1872
if ! p.is_open ( ) {
1873
return Ok ( back ( & owner, & name, number, " conversation" , None , Some ( " The proposal is not open." .into ( ) ) ) ) ;
1874
}
1875
let Some ( a) = crate :: assistants:: enabled_for_repo( & app.db, & row)
1876
.await
1877
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1878
.into_iter ( )
1879
.find ( | a | a.name.eq_ignore_ascii_case ( f.assistant.trim ( ) ) )
1880
else {
1881
return Ok ( back ( & owner, & name, number, " conversation" , None , Some ( format! ( " No assistant called {} here." , f.assistant.trim ( ) ) ) ) ) ;
1882
} ;
1883
let who = a.name.clone ( ) ;
ffb67a81 Andy McCall 21 days ago
1884
let summary = f.task = = " summary" ;
1885
let asked = if summary {
1886
crate :: assistants:: request_summary( & app, & row, & p, & me.account, a) .await
1887
} else {
1888
crate :: assistants:: request_review( & app, & row, & p, & me.account, a) .await
1889
} ;
1890
match asked {
7d2eb590 Andy McCall 1 month ago ↶
1891
Ok ( ( ) ) => Ok ( back (
1892
& owner,
1893
& name,
1894
number,
1895
" conversation" ,
ffb67a81 Andy McCall 21 days ago
1896
Some ( if summary {
1897
format! ( " Asked {who} for a summary; its comment appears here when it comes." )
1898
} else {
1899
format! ( " Asked {who} to review; its answer appears here when it comes." )
1900
} ) ,
7d2eb590 Andy McCall 1 month ago ↶
1901
None ,
1902
) ) ,
1903
Err ( e) => Ok ( back ( & owner, & name, number, " conversation" , None , Some ( format! ( " Not asked: {e} ." ) ) ) ) ,
1904
}
1905
}
1906
1907
1908
async fn assistant_names ( app : & Shared, row : & db:: repos:: RepoRow) -> Vec< String> {
1909
crate :: assistants:: enabled_for_repo( & app.db, row) .await.unwrap_or_default ( ) .into_iter ( ) .map ( | a | a.name ) .collect ( )
1910
}
1911
1912
1913
1914
async fn preview_diff (
1915
app : & Shared,
1916
repo : & RepoName,
1917
target : & str ,
1918
source : & str ,
1919
) -> Result< ( Vec< ChangeView> , Vec< oxy_core:: repo:: FileDiff> ) , crate:: repos:: RepoError> {
1920
let Some ( ( fork, bookmark) ) = parse_source ( app, repo, source) else { return Ok ( ( vec! [ ] , vec! [ ] ) ) } ;
1921
let target = target.to_string ( ) ;
1922
let fork_dir = fork.as_ref ( ) .map ( | f | app.repos.dir ( f) ) ;
1923
app.repos
1924
.read ( repo, move | r, cache| {
1925
let projection = match & fork_dir {
1926
Some ( dir) => {
1927
let fork_repo = oxy_core:: repo:: Repo:: open( dir) ? ;
1928
let heads = fork_repo.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) ;
1929
pl:: project( r, cache, & target, Source:: Fork { fork: & fork_repo, heads } ) ?
1930
}
1931
None => {
1932
let heads = r.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) ;
1933
pl:: project( r, cache, & target, Source:: Bookmark( heads) ) ?
1934
}
1935
} ;
1936
Ok ( ( projection.changes, projection.files) )
1937
} )
1938
.await
1939
}
1940
1941
1942
1943
1944
pub async fn new_draft (
1945
State( app ) : State< Shared> ,
1946
viewer : Viewer,
1947
Path( (owner , name ) ) : Path< ( String, String) > ,
1948
Form( f) : Form< NewForm> ,
1949
) -> Result< Page< NewProposalPage> , Error> {
1950
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposals/new" ) , & viewer) ;
1951
let me = require ( & viewer, & shell) ? ;
1952
csrf ( & viewer, & f.csrf_token, & shell) ? ;
1953
let ( repo, row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
d0fbea2e Andy McCall 1 month ago ↶
1954
super :: writable( & row, & shell) ? ;
7d2eb590 Andy McCall 1 month ago ↶
1955
let bookmarks =
1956
app.repos.read ( & repo, | r , _| Ok ( r.bookmarks ( ) ? .into_iter ( ) .map ( | b | b.name ) .collect:: < Vec< _ > > ( ) ) ) .await.unwrap_or_default ( ) ;
1957
let ( preview, conflicted) = preview_for ( & app, & repo, & f.target, & f.source) .await.unwrap_or ( ( vec! [ ] , false ) ) ;
1958
let assistants = if preview.is_empty ( ) { vec! [ ] } else { assistant_names ( & app, & row) .await } ;
1959
let mut page = NewProposalPage {
1960
shell,
1961
repo: RepoShell:: new( & app, & row, " proposals" ) .with_role ( role) ,
1962
bookmarks,
1963
source: f.source.clone ( ) ,
1964
target: f.target.clone ( ) ,
1965
error: None ,
1966
preview,
1967
conflicted,
1968
title: f.title.clone ( ) ,
1969
body: f.body.clone ( ) ,
1970
assistants,
1971
} ;
1972
let assistant = crate :: assistants:: enabled_for_repo( & app.db, & row)
1973
.await
1974
.unwrap_or_default ( )
1975
.into_iter ( )
1976
.find ( | a | a.name.eq_ignore_ascii_case ( f.assistant.trim ( ) ) ) ;
1977
let Some ( a) = assistant else {
1978
page.error = Some ( format! ( " No assistant called {} here." , f.assistant.trim ( ) ) ) ;
1979
return Ok ( Page( page) ) ;
1980
} ;
1981
if page.preview.is_empty ( ) {
1982
page.error = Some ( " Nothing to describe: the source adds nothing to the target." .into ( ) ) ;
1983
return Ok ( Page( page) ) ;
1984
}
1985
let ( changes, diffs) = match preview_diff ( & app, & repo, & f.target, & f.source) .await {
1986
Ok ( x) => x,
1987
Err ( e) => {
1988
page.error = Some ( e.to_string ( ) ) ;
1989
return Ok ( Page( page) ) ;
1990
}
1991
} ;
c43e4f63 Andy McCall 10 days ago ↶
1992
let d =
1993
crate :: assistants:: Draft { slot: - row.id, title: & f.title, target: & f.target, changes: & changes, diffs: & diffs, with_title: true } ;
7d2eb590 Andy McCall 1 month ago ↶
1994
match crate :: assistants:: draft_description( & app, & row, & me.account, & a, d) .await {
c43e4f63 Andy McCall 10 days ago ↶
1995
Ok ( text) => {
1996
let ( title, body) = crate :: assistants:: split_title( & text) ;
1997
page.body = body;
1998
1999
2000
let suggested = if page.preview.len ( ) = = 1 { page.preview[ 0 ] .title.clone ( ) } else { String :: new( ) } ;
2001
if let Some ( t) = title.filter ( | _| page.title.trim ( ) .is_empty ( ) | | page.title.trim ( ) = = suggested.trim ( ) ) {
2002
page.title = t;
2003
}
2004
}
7d2eb590 Andy McCall 1 month ago ↶
2005
Err ( why) => page.error = Some ( format! ( " {} did not answer: {why} ." , a.name) ) ,
2006
}
2007
Ok ( Page( page) )
2008
}
2009
2010
# [ derive ( Deserialize ) ]
2011
pub struct DraftForm {
2012
#[serde(rename = "_csrf")]
2013
csrf_token : String,
2014
assistant : String,
2015
#[serde(default)]
2016
title : String,
2017
}
2018
2019
2020
2021
2022
pub async fn draft (
2023
State( app ) : State< Shared> ,
2024
viewer : Viewer,
2025
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
2026
Form( f) : Form< DraftForm> ,
2027
) -> Result< Response, Error> {
2028
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
2029
let me = require ( & viewer, & shell) ? ;
2030
csrf ( & viewer, & f.csrf_token, & shell) ? ;
2031
let ( repo, row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
d0fbea2e Andy McCall 1 month ago ↶
2032
super :: writable( & row, & shell) ? ;
7d2eb590 Andy McCall 1 month ago ↶
2033
let p = pdb:: get( & app.db, row.id, number)
2034
.await
2035
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
2036
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
2037
if Some ( me.account.id) ! = p.author_id & & role < Role:: Maintain {
2038
return Err ( Error:: not_found( shell) ) ;
2039
}
2040
let Some ( a) = crate :: assistants:: enabled_for_repo( & app.db, & row)
2041
.await
2042
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
2043
.into_iter ( )
2044
.find ( | a | a.name.eq_ignore_ascii_case ( f.assistant.trim ( ) ) )
2045
else {
2046
return Ok ( back ( & owner, & name, number, " conversation" , None , Some ( format! ( " No assistant called {} here." , f.assistant.trim ( ) ) ) ) ) ;
2047
} ;
2048
let loaded = crate :: web:: proposals:: load( & app, & repo, p.clone ( ) ) .await.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
2049
let title = if f.title.trim ( ) .is_empty ( ) { p.title.clone ( ) } else { f.title.clone ( ) } ;
c43e4f63 Andy McCall 10 days ago ↶
2050
let d = crate :: assistants:: Draft {
2051
slot: p.id,
2052
title: & title,
2053
target: & p.target,
2054
changes: & loaded.changes,
2055
diffs: & loaded.diffs,
2056
with_title: false ,
2057
} ;
7d2eb590 Andy McCall 1 month ago ↶
2058
let draft = crate :: assistants:: draft_description( & app, & row, & me.account, & a, d) .await;
2059
match draft {
2060
Ok ( text) => {
2061
let mut page = detail ( & app, shell, & viewer, & owner, & name, number, " conversation" .into ( ) , None , None ) .await? ;
2062
page.0. draft = Some ( text) ;
2063
Ok ( page.into_response ( ) )
2064
}
2065
Err ( why) => Ok ( back ( & owner, & name, number, " conversation" , None , Some ( format! ( " {} did not answer: {why} ." , a.name) ) ) ) ,
cc1cd197 Andy McCall 1 month ago ↶
2066
}
2067
}
119fd17d Andy McCall 18 days ago ↶
2068
2069
2070
2071
2072
2073
2074
type DraftEvents = axum:: response:: Sse<
2075
axum:: response:: sse:: KeepAliveStream<
2076
tokio_stream:: wrappers:: ReceiverStream< Result< axum:: response:: sse:: Event, std:: convert:: Infallible> > ,
2077
> ,
2078
> ;
2079
2080
fn draft_events ( rx : tokio:: sync:: mpsc:: Receiver< Result< axum:: response:: sse:: Event, std:: convert:: Infallible> > ) -> DraftEvents {
2081
axum:: response:: Sse:: new( tokio_stream:: wrappers:: ReceiverStream:: new( rx) )
2082
.keep_alive ( axum:: response:: sse:: KeepAlive:: new( ) .interval ( std:: time:: Duration:: from_secs( 15 ) ) )
2083
}
2084
2085
fn draft_event ( kind : & str , text : & str ) -> Result< axum:: response:: sse:: Event, std:: convert:: Infallible> {
2086
Ok ( axum:: response:: sse:: Event:: default( ) .event ( kind) .data ( serde_json:: to_string( text) .unwrap_or_default ( ) ) )
2087
}
2088
2089
2090
fn draft_refused ( why : String) -> DraftEvents {
2091
let ( tx, rx) = tokio:: sync:: mpsc:: channel( 1 ) ;
2092
let _ = tx.try_send ( draft_event ( " error" , & why) ) ;
2093
draft_events ( rx)
2094
}
2095
2096
2097
struct DraftJob {
2098
row : db:: repos:: RepoRow,
2099
by : accounts:: Account,
2100
a : crate:: assistants:: Assistant,
2101
slot : i64 ,
2102
title : String,
2103
target : String,
2104
changes : Vec< ChangeView> ,
2105
diffs : Vec< oxy_core:: repo:: FileDiff> ,
c43e4f63 Andy McCall 10 days ago ↶
2106
2107
with_title : bool ,
119fd17d Andy McCall 18 days ago ↶
2108
}
2109
2110
2111
fn draft_in_background ( app : Shared, job : DraftJob) -> DraftEvents {
2112
let ( tx, rx) = tokio:: sync:: mpsc:: channel( 64 ) ;
2113
tokio:: spawn( async move {
c43e4f63 Andy McCall 10 days ago ↶
2114
let DraftJob { row, by, a, slot, title, target, changes, diffs, with_title } = job;
2115
let d = crate :: assistants:: Draft { slot, title: & title, target: & target, changes: & changes, diffs: & diffs, with_title } ;
119fd17d Andy McCall 18 days ago ↶
2116
let parts = tx.clone ( ) ;
2117
let result = crate :: assistants:: draft_description_streaming( & app, & row, & by, & a, d, move | p: & str | {
2118
2119
let _ = parts.try_send ( draft_event ( " part" , p) ) ;
2120
} )
2121
.await;
2122
let last = match result {
c43e4f63 Andy McCall 10 days ago ↶
2123
Ok ( text) if with_title => {
2124
2125
let ( t, body) = crate :: assistants:: split_title( & text) ;
2126
if let Some ( t) = t {
2127
let _ = tx.send ( draft_event ( " title" , & t) ) .await;
2128
}
2129
draft_event ( " done" , & body)
2130
}
119fd17d Andy McCall 18 days ago ↶
2131
Ok ( text) => draft_event ( " done" , text.trim ( ) ) ,
2132
Err ( why) => draft_event ( " error" , & format! ( " {} did not answer: {why} ." , a.name) ) ,
2133
} ;
2134
let _ = tx.send ( last) .await;
2135
} ) ;
2136
draft_events ( rx)
2137
}
2138
2139
2140
2141
pub async fn draft_stream (
2142
State( app ) : State< Shared> ,
2143
viewer : Viewer,
2144
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
2145
Form( f) : Form< DraftForm> ,
2146
) -> Result< DraftEvents, Error> {
2147
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
2148
let me = require ( & viewer, & shell) ? ;
2149
csrf ( & viewer, & f.csrf_token, & shell) ? ;
2150
let ( repo, row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
2151
super :: writable( & row, & shell) ? ;
2152
let p = pdb:: get( & app.db, row.id, number)
2153
.await
2154
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
2155
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
2156
if Some ( me.account.id) ! = p.author_id & & role < Role:: Maintain {
2157
return Err ( Error:: not_found( shell) ) ;
2158
}
2159
let Some ( a) = crate :: assistants:: enabled_for_repo( & app.db, & row)
2160
.await
2161
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
2162
.into_iter ( )
2163
.find ( | a | a.name.eq_ignore_ascii_case ( f.assistant.trim ( ) ) )
2164
else {
2165
return Ok ( draft_refused ( format! ( " No assistant called {} here." , f.assistant.trim ( ) ) ) ) ;
2166
} ;
2167
let loaded = load ( & app, & repo, p.clone ( ) ) .await.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
2168
let title = if f.title.trim ( ) .is_empty ( ) { p.title.clone ( ) } else { f.title.clone ( ) } ;
2169
let job = DraftJob {
2170
row,
2171
by: me.account.clone ( ) ,
2172
a,
2173
slot: p.id,
2174
title,
2175
target: p.target.clone ( ) ,
2176
changes: loaded.changes,
2177
diffs: loaded.diffs,
c43e4f63 Andy McCall 10 days ago ↶
2178
with_title: false ,
119fd17d Andy McCall 18 days ago ↶
2179
} ;
2180
Ok ( draft_in_background ( app.clone ( ) , job) )
2181
}
2182
2183
2184
2185
pub async fn new_draft_stream (
2186
State( app ) : State< Shared> ,
2187
viewer : Viewer,
2188
Path( (owner , name ) ) : Path< ( String, String) > ,
2189
Form( f) : Form< NewForm> ,
2190
) -> Result< DraftEvents, Error> {
2191
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposals/new" ) , & viewer) ;
2192
let me = require ( & viewer, & shell) ? ;
2193
csrf ( & viewer, & f.csrf_token, & shell) ? ;
2194
let ( repo, row, _role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
2195
super :: writable( & row, & shell) ? ;
2196
let Some ( a) = crate :: assistants:: enabled_for_repo( & app.db, & row)
2197
.await
2198
.unwrap_or_default ( )
2199
.into_iter ( )
2200
.find ( | a | a.name.eq_ignore_ascii_case ( f.assistant.trim ( ) ) )
2201
else {
2202
return Ok ( draft_refused ( format! ( " No assistant called {} here." , f.assistant.trim ( ) ) ) ) ;
2203
} ;
2204
let ( changes, diffs) = match preview_diff ( & app, & repo, & f.target, & f.source) .await {
2205
Ok ( x) => x,
2206
Err ( e) => return Ok ( draft_refused ( e.to_string ( ) ) ) ,
2207
} ;
2208
if changes.is_empty ( ) {
2209
return Ok ( draft_refused ( " Nothing to describe: the source adds nothing to the target." .into ( ) ) ) ;
2210
}
c43e4f63 Andy McCall 10 days ago ↶
2211
let job = DraftJob {
2212
slot: - row.id,
2213
row,
2214
by: me.account.clone ( ) ,
2215
a,
2216
title: f.title.clone ( ) ,
2217
target: f.target.clone ( ) ,
2218
changes,
2219
diffs,
2220
with_title: true ,
2221
} ;
119fd17d Andy McCall 18 days ago ↶
2222
Ok ( draft_in_background ( app.clone ( ) , job) )
2223
}