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 ,
140
}
141
142
# [ derive ( Deserialize ) ]
143
pub struct NewQuery {
144
source : Option< String> ,
145
target : Option< String> ,
146
}
147
148
0b20f1ec Andy McCall 1 month ago ↶
149
150
151
de505df5 Andy McCall 1 month ago ↶
152
fn parse_source ( app : & Shared, repo : & RepoName, source : & str ) -> Option< ( Option< RepoName> , String) > {
0b20f1ec Andy McCall 1 month ago ↶
153
let plain = | b : & str | ! b.is_empty ( ) & & ! b.chars ( ) .any ( | c | c.is_control ( ) | | c = = ' :' ) ;
de505df5 Andy McCall 1 month ago ↶
154
if let Some ( ( slug, bookmark) ) = source.split_once ( ' :' ) {
155
let fork = RepoName:: parse( slug) ? ;
0b20f1ec Andy McCall 1 month ago ↶
156
if ! plain ( bookmark) {
de505df5 Andy McCall 1 month ago ↶
157
return None ;
0b20f1ec Andy McCall 1 month ago ↶
158
}
159
if fork = = * repo {
160
return Some ( ( None , bookmark.to_string ( ) ) ) ;
de505df5 Andy McCall 1 month ago ↶
161
}
0b20f1ec Andy McCall 1 month ago ↶
162
if ! app.repos.exists ( & fork) {
163
return None ;
164
}
de505df5 Andy McCall 1 month ago ↶
165
return Some ( ( Some ( fork) , bookmark.to_string ( ) ) ) ;
166
}
0b20f1ec Andy McCall 1 month ago ↶
167
plain ( source) .then ( | | ( None , source.to_string ( ) ) )
de505df5 Andy McCall 1 month ago ↶
168
}
169
170
async fn preview_for (
171
app : & Shared,
172
repo : & RepoName,
173
target : & str ,
174
source : & str ,
175
) -> Result< ( Vec< ChangeView> , bool ) , crate:: repos:: RepoError> {
176
let Some ( ( fork, bookmark) ) = parse_source ( app, repo, source) else { return Ok ( ( vec! [ ] , false ) ) } ;
177
let target = target.to_string ( ) ;
178
let fork_dir = fork.as_ref ( ) .map ( | f | app.repos.dir ( f) ) ;
179
app.repos
180
.read ( repo, move | r, cache| {
181
let projection = match & fork_dir {
182
Some ( dir) => {
183
let fork_repo = oxy_core:: repo:: Repo:: open( dir) ? ;
184
let heads = fork_repo.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) ;
185
pl:: project( r, cache, & target, Source:: Fork { fork: & fork_repo, heads } ) ?
186
}
187
None => {
188
let heads = r.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) ;
189
pl:: project( r, cache, & target, Source:: Bookmark( heads) ) ?
190
}
191
} ;
192
Ok ( ( projection.changes, projection.conflicted) )
193
} )
194
.await
195
}
196
197
pub async fn new_form (
198
State( app ) : State< Shared> ,
199
viewer : Viewer,
200
Path( (owner , name ) ) : Path< ( String, String) > ,
201
Query( q) : Query< NewQuery> ,
202
) -> Result< Page< NewProposalPage> , Error> {
203
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposals/new" ) , & viewer) ;
204
require ( & viewer, & shell) ? ;
205
let ( repo, row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
206
let bookmarks = app
207
.repos
208
.read ( & repo, | r , _| Ok ( r.bookmarks ( ) ? .into_iter ( ) .map ( | b | b.name ) .collect:: < Vec< _ > > ( ) ) )
209
.await
210
.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
211
let target = q.target.unwrap_or_else ( | | row.default_bookmark.clone ( ) ) ;
212
let source = q.source.unwrap_or_default ( ) ;
213
let ( preview, conflicted) =
214
if source.is_empty ( ) { ( vec! [ ] , false ) } else { preview_for ( & app, & repo, & target, & source) .await.unwrap_or ( ( vec! [ ] , false ) ) } ;
215
Ok ( Page( NewProposalPage {
216
shell,
217
repo: RepoShell:: new( & app, & row, " proposals" ) .with_role ( role) ,
218
bookmarks,
219
source,
220
target,
221
error: None ,
222
preview,
223
conflicted,
224
} ) )
225
}
226
227
# [ derive ( Deserialize ) ]
228
pub struct NewForm {
229
#[serde(rename = "_csrf")]
230
csrf_token : String,
231
title : String,
232
#[serde(default)]
233
body : String,
234
target : String,
235
source : String,
236
#[serde(default)]
237
draft : Option< String> ,
238
}
239
b64e2537 Andy McCall 1 month ago ↶
240
241
# [ allow ( clippy::too_many_arguments ) ]
242
pub async fn open_proposal (
243
app : & Shared,
244
repo : & RepoName,
245
row : & db:: repos:: RepoRow,
246
me : & accounts:: Account,
247
title : & str ,
248
body : & str ,
249
target : & str ,
250
source : & str ,
251
draft : bool ,
252
) -> Result< pdb:: ProposalRow, String> {
253
if title.trim ( ) .is_empty ( ) {
254
return Err ( " Give the proposal a title." .into ( ) ) ;
de505df5 Andy McCall 1 month ago ↶
255
}
b64e2537 Andy McCall 1 month ago ↶
256
let bookmarks =
257
app.repos.read ( repo, | r , _| Ok ( r.bookmarks ( ) ? .into_iter ( ) .map ( | b | b.name ) .collect:: < Vec< _ > > ( ) ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
258
if ! bookmarks.iter ( ) .any ( | b | b = = target ) {
259
return Err ( " Pick a target bookmark of this repository." .into ( ) ) ;
de505df5 Andy McCall 1 month ago ↶
260
}
b64e2537 Andy McCall 1 month ago ↶
261
let Some ( ( fork, bookmark) ) = parse_source ( app, repo, source.trim ( ) ) else {
262
return Err ( " The source is a bookmark of this repository, or owner/name:bookmark of a fork." .into ( ) ) ;
de505df5 Andy McCall 1 month ago ↶
263
} ;
0b20f1ec Andy McCall 1 month ago ↶
264
if fork.is_none ( ) & & ! bookmarks.contains ( & bookmark) {
265
return Err ( format! ( " There is no bookmark {bookmark} in this repository." ) ) ;
266
}
b64e2537 Andy McCall 1 month ago ↶
267
if fork.is_none ( ) & & bookmark = = target {
268
return Err ( " The source and the target are the same bookmark." .into ( ) ) ;
de505df5 Andy McCall 1 month ago ↶
269
}
b64e2537 Andy McCall 1 month ago ↶
270
let ( preview, _ ) = preview_for ( app, repo, target, source.trim ( ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
de505df5 Andy McCall 1 month ago ↶
271
if preview.is_empty ( ) {
b64e2537 Andy McCall 1 month ago ↶
272
return Err ( " The source adds nothing to the target." .into ( ) ) ;
de505df5 Andy McCall 1 month ago ↶
273
}
274
let source_repo_id = match & fork {
b64e2537 Andy McCall 1 month ago ↶
275
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 ↶
276
None => None ,
277
} ;
278
let created = pdb:: create(
279
& app.db,
280
pdb:: NewProposal {
281
repo_id: row.id,
b64e2537 Andy McCall 1 month ago ↶
282
title: title.trim ( ) ,
283
body: body.trim ( ) ,
284
author_id: Some ( me.id) ,
285
author: & me.username,
286
draft,
287
target,
de505df5 Andy McCall 1 month ago ↶
288
source_kind: if fork.is_some ( ) { " fork" } else { " bookmark" } ,
289
source_repo_id,
290
source_bookmark: & bookmark,
291
source_changes: " " ,
292
} ,
293
)
294
.await
b64e2537 Andy McCall 1 month ago ↶
295
.map_err ( | e | e.to_string ( ) ) ? ;
296
pdb:: add_event( & app.db, created.id, & me.username, " opened" , " " ) .await.ok ( ) ;
297
accounts:: audit( & app.db, Some ( me) , " proposal.opened" , & format! ( " {} #{} " , row.slug ( ) , created.number) , title) .await.ok ( ) ;
18e381ea Andy McCall 1 month ago ↶
298
db:: notify:: index( & app.db, " proposal" , row.id, created.id, created.number, & created.title, & created.body) .await.ok ( ) ;
b64e2537 Andy McCall 1 month ago ↶
299
let url = format! ( " /{} /proposal/{} " , row.slug ( ) , created.number) ;
18e381ea Andy McCall 1 month ago ↶
300
crate :: notify:: deliver(
b64e2537 Andy McCall 1 month ago ↶
301
app,
18e381ea Andy McCall 1 month ago ↶
302
crate :: notify:: Event {
303
kind: " proposal.opened" ,
b64e2537 Andy McCall 1 month ago ↶
304
title: format! ( " {} #{} : {} " , row.slug ( ) , created.number, created.title) ,
305
url,
306
actor: me,
18e381ea Andy McCall 1 month ago ↶
307
repo_id: row.id,
308
thread: Some ( ( " proposal" , created.id) ) ,
b64e2537 Andy McCall 1 month ago ↶
309
mention_text: body,
18e381ea Andy McCall 1 month ago ↶
310
direct: vec! [ ] ,
311
} ,
312
)
313
.await;
b64e2537 Andy McCall 1 month ago ↶
314
crate :: hooks:: emit(
315
app,
316
row,
317
" proposal.opened" ,
318
& me.username,
319
serde_json:: json! ( { " number" : created.number, " title" : created.title, " target" : target, " source" : source } ) ,
320
)
321
.await;
322
Ok ( created)
323
}
324
325
pub async fn create (
326
State( app ) : State< Shared> ,
327
viewer : Viewer,
328
Path( (owner , name ) ) : Path< ( String, String) > ,
329
Form( f) : Form< NewForm> ,
330
) -> Result< Response, Error> {
331
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposals/new" ) , & viewer) ;
332
let me = require ( & viewer, & shell) ? ;
333
csrf ( & viewer, & f.csrf_token, & shell) ? ;
334
let ( repo, row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
335
match open_proposal ( & app, & repo, & row, & me.account, & f.title, & f.body, & f.target, & f.source, f.draft.is_some ( ) ) .await {
336
Ok ( created) => Ok ( Redirect:: to( & format! ( " /{owner} /{name} /proposal/{} " , created.number) ) .into_response ( ) ) ,
337
Err ( msg) => {
338
let bookmarks =
339
app.repos.read ( & repo, | r , _| Ok ( r.bookmarks ( ) ? .into_iter ( ) .map ( | b | b.name ) .collect:: < Vec< _ > > ( ) ) ) .await.unwrap_or_default ( ) ;
340
Ok ( Page( NewProposalPage {
341
shell,
342
repo: RepoShell:: new( & app, & row, " proposals" ) .with_role ( role) ,
343
bookmarks,
344
source: f.source.clone ( ) ,
345
target: f.target.clone ( ) ,
346
error: Some ( msg) ,
347
preview: vec! [ ] ,
348
conflicted: false ,
349
} )
350
.into_response ( ) )
351
}
352
}
de505df5 Andy McCall 1 month ago ↶
353
}
354
355
356
357
358
pub struct CommentView {
359
pub row : pdb:: CommentRow,
360
pub body_html : String,
361
pub line : Option< usize > ,
362
}
363
a1b98695 Andy McCall 1 month ago ↶
364
365
366
pub struct Entry {
367
pub kind : & 'static str ,
368
pub index : usize ,
369
}
370
de505df5 Andy McCall 1 month ago ↶
371
pub struct ReviewView {
372
pub row : pdb:: ReviewRow,
373
pub body_html : String,
374
pub sealed : bool ,
375
}
376
377
378
pub struct Loaded {
379
pub row : pdb:: ProposalRow,
380
pub changes : Vec< ChangeView> ,
381
pub hashes : Vec< ChangeHash> ,
382
pub heads : Vec< ChangeHash> ,
3f02e5ea Andy McCall 1 month ago ↶
383
pub target_heads : Vec< ChangeHash> ,
de505df5 Andy McCall 1 month ago ↶
384
pub state : String,
385
pub target_state : String,
386
pub conflicted : bool ,
387
pub conflicts : Vec< ConflictView> ,
388
pub files : Vec< FileRows> ,
9df855f2 Andy McCall 1 month ago ↶
389
390
pub diffs : Vec< oxy_core:: repo:: FileDiff> ,
391
pub bases : std:: collections:: BTreeMap< String, Vec< u8 > > ,
de505df5 Andy McCall 1 month ago ↶
392
393
pub atoms : HashMap< ( String, usize ) , ( String, u32 ) > ,
394
pub sealing_keys : Vec< String> ,
395
pub touched : Vec< String> ,
396
pub owners : Vec< String> ,
397
pub landed_already : bool ,
398
}
399
400
401
pub async fn load ( app : & Shared, repo : & RepoName, row : pdb:: ProposalRow) -> Result< Loaded, crate:: repos:: RepoError> {
402
let fork_dir = match ( row.source_kind.as_str ( ) , row.source_repo_id) {
403
( " fork" , Some ( id) ) => {
404
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) )
405
}
406
_ => None ,
407
} ;
408
let r2 = row.clone ( ) ;
409
let loaded = app
410
.repos
411
.read ( repo, move | r, cache| {
412
let source = match ( r2.source_kind.as_str ( ) , & fork_dir) {
413
( " changes" , _ ) => Source:: Changes( r2.source_changes ( ) .iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect ( ) ) ,
414
( " fork" , Some ( dir) ) => {
415
let fork_repo = oxy_core:: repo:: Repo:: open( dir) ? ;
416
let heads =
417
fork_repo.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = r2.source_bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) ;
418
419
let fork_state = fork_repo.load_state ( & heads) ? ;
420
let missing: Vec< ChangeHash> = fork_state.changes ( ) .map ( | ( h , _) | * h) .filter ( | h | ! r.store ( ) .has ( h) ) .collect ( ) ;
421
if ! missing.is_empty ( ) {
422
r.import_bundle ( & fork_repo.bundle_for ( & missing) ? , r.current_policy ( ) ? .is_some ( ) ) ? ;
423
}
424
Source:: Changes( heads)
425
}
426
_ => {
427
let heads = r.bookmarks ( ) ? .into_iter ( ) .find ( | b | b.name = = r2.source_bookmark ) .map ( | b | b.heads ) .unwrap_or_default ( ) ;
428
Source:: Bookmark( heads)
429
}
430
} ;
431
let p: Projection = pl:: project( r, cache, & r2.target, source) ? ;
432
let target_loaded = cache.load ( r, & p.target_heads) ? ;
433
let files = render_files ( & p.files, & p.bases) ;
434
let mut atoms = HashMap:: new( ) ;
435
for f in & files {
436
for l in & f.lines {
437
if let Some ( n) = l.new_no {
438
if let Some ( ( h, i) ) = pl:: atom_at( & p.loaded, & f.path, n as usize ) {
439
atoms.insert ( ( f.path.clone ( ) , n as usize ) , ( h.to_hex ( ) , i) ) ;
440
}
441
}
442
}
443
}
444
let touched: Vec< String> = p.files.iter ( ) .map ( | f | f.path.clone ( ) ) .collect ( ) ;
445
let owners = pl:: owners_for( & target_loaded, & touched) ;
446
let sealing = pl:: sealing_keys( r, & p.hashes) ? .into_iter ( ) .map ( | k | k.to_hex ( ) ) .collect ( ) ;
447
let landed_already = ! p.hashes.is_empty ( ) & & p.hashes.iter ( ) .all ( | h | target_loaded.state.contains ( h) ) ;
0b20f1ec Andy McCall 1 month ago ↶
448
449
450
let ( changes, hashes) = if r2.state = = " landed" & & p.hashes.is_empty ( ) {
451
let hashes: Vec< ChangeHash> = r2.projected_changes ( ) .iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect ( ) ;
452
let labels = HashMap:: new( ) ;
453
let changes = hashes
454
.iter ( )
455
.filter_map ( | h | Some ( view:: change_view_pub( r, h, & r.store ( ) .get_change_meta ( h) .ok ( ) ? , false , & labels) ) )
456
.collect ( ) ;
457
( changes, hashes)
458
} else {
459
( p.changes, p.hashes)
460
} ;
de505df5 Andy McCall 1 month ago ↶
461
Ok ( Loaded {
462
row: r2,
0b20f1ec Andy McCall 1 month ago ↶
463
changes,
464
hashes,
de505df5 Andy McCall 1 month ago ↶
465
heads: p.heads,
3f02e5ea Andy McCall 1 month ago ↶
466
target_heads: p.target_heads.clone ( ) ,
de505df5 Andy McCall 1 month ago ↶
467
state: p.state.to_hex ( ) ,
468
target_state: p.target_state.to_hex ( ) ,
469
conflicted: p.conflicted,
470
conflicts: p.conflicts,
471
files,
9df855f2 Andy McCall 1 month ago ↶
472
diffs: p.files.clone ( ) ,
473
bases: p.bases.clone ( ) ,
de505df5 Andy McCall 1 month ago ↶
474
atoms,
475
sealing_keys: sealing,
476
touched,
477
owners,
478
landed_already,
479
} )
480
} )
481
.await? ;
0b20f1ec Andy McCall 1 month ago ↶
482
483
484
485
if ! loaded.row.is_open ( ) {
486
return Ok ( loaded) ;
487
}
de505df5 Andy McCall 1 month ago ↶
488
let heads_csv = loaded.heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect:: < Vec< _ > > ( ) .join ( " ," ) ;
489
let changes_csv = loaded.hashes.iter ( ) .map ( | h | h.to_hex ( ) ) .collect:: < Vec< _ > > ( ) .join ( " ," ) ;
490
pdb:: set_projection( & app.db, loaded.row.id, & loaded.state, & heads_csv, & loaded.target_state, & changes_csv, loaded.conflicted)
491
.await
492
.ok ( ) ;
493
Ok ( loaded)
494
}
495
496
497
pub struct Verdict {
498
pub reviews : Vec< ReviewView> ,
499
pub approvals : Vec< Approval> ,
500
pub blockers : Vec< String> ,
501
pub depends_on : Vec< i64 > ,
502
pub requirements : pdb:: Requirements,
503
}
504
505
pub async fn verdict ( app : & Shared, loaded : & Loaded, repo_id : i64 ) -> Result< Verdict, sqlx:: Error> {
506
let rows = pdb:: reviews( & app.db, loaded.row.id) .await? ;
507
let links = accounts:: identity_accounts( & app.db) .await? ;
508
let sealed_accounts: Vec< String> = links.iter ( ) .filter ( | ( k , _) | loaded.sealing_keys.contains ( k) ) .map ( | ( _, u ) | u.clone ( ) ) .collect ( ) ;
509
510
let mut latest: BTreeMap< String, & pdb:: ReviewRow> = BTreeMap:: new( ) ;
511
for r in & rows {
512
latest.insert ( r.author.clone ( ) , r) ;
513
}
514
let mut approvals = Vec :: new( ) ;
515
for ( who, r) in & latest {
516
if r.verdict = = " approve" {
517
let sealed = sealed_accounts.contains ( who) ;
518
let key = links.iter ( ) .find ( | ( k , u ) | u = = who & & loaded.sealing_keys.contains ( k) ) .map ( | ( k , _) | k.clone ( ) ) ;
519
approvals.push ( Approval { account: who.clone ( ) , sealed, key } ) ;
520
}
521
}
522
let reviews = rows
523
.iter ( )
524
.map ( | r | ReviewView {
525
row: r.clone ( ) ,
526
body_html: render:: markdown:: render( & r.body, " /" , " /" ) ,
527
sealed: r.verdict = = " approve" & & sealed_accounts.contains ( & r.author) ,
528
} )
529
.collect ( ) ;
530
let requirements = pdb:: requirements( & app.db, repo_id) .await? ;
531
let mut blockers = Vec :: new( ) ;
0b20f1ec Andy McCall 1 month ago ↶
532
533
if ! loaded.row.is_open ( ) {
534
return Ok ( Verdict { reviews, approvals, blockers, depends_on: vec! [ ] , requirements } ) ;
535
}
de505df5 Andy McCall 1 month ago ↶
536
if loaded.row.state = = " draft" {
537
blockers.push ( " the proposal is a draft" .into ( ) ) ;
538
}
539
if loaded.hashes.is_empty ( ) {
540
blockers.push ( " the proposal adds no changes" .into ( ) ) ;
541
}
542
let sealed_count = approvals.iter ( ) .filter ( | a | a.sealed ) .count ( ) as i64 ;
543
if sealed_count < requirements.required_approvals {
544
blockers.push ( format! ( " {sealed_count} of {} required sealed approvals" , requirements.required_approvals) ) ;
545
}
546
if requirements.require_owners {
547
let missing: Vec< & String> = loaded.owners.iter ( ) .filter ( | o | ! approvals.iter ( ) .any ( | a | a.sealed & & & a.account = = * o ) ) .collect ( ) ;
548
if ! missing.is_empty ( ) {
549
blockers.push ( format! ( " code owners still to approve: {} " , missing.iter ( ) .map ( | s | s.as_str ( ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ) ) ;
550
}
551
}
552
if loaded.conflicted & & ! requirements.allow_conflicts {
553
blockers.push ( " landing would carry conflicts, which this repository refuses" .into ( ) ) ;
554
}
b64e2537 Andy McCall 1 month ago ↶
555
let required = db:: hooks:: required_checks( & app.db, repo_id) .await? ;
556
if ! required.is_empty ( ) {
557
let checks = db:: hooks:: checks_for_state( & app.db, repo_id, & loaded.state) .await? ;
558
let missing: Vec< String> =
559
required.iter ( ) .filter ( | n | ! checks.iter ( ) .any ( | c | & c.name = = * n & & c.status = = " passed" ) ) .cloned ( ) .collect ( ) ;
560
if ! missing.is_empty ( ) {
561
blockers.push ( format! ( " required checks not passed on the projected state: {} " , missing.join ( " , " ) ) ) ;
562
}
563
}
de505df5 Andy McCall 1 month ago ↶
564
let requesting: Vec< & String> = latest.iter ( ) .filter ( | ( _, r ) | r.verdict = = " changes" ) .map ( | ( who , _) | who ) .collect ( ) ;
565
if ! requesting.is_empty ( ) {
566
blockers.push ( format! ( " changes requested by {} " , requesting.iter ( ) .map ( | s | s.as_str ( ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ) ) ;
567
}
568
569
let mut depends_on = Vec :: new( ) ;
570
for other in pdb:: list( & app.db, repo_id, & [ " open" , " draft" ] ) .await? {
571
if other.id = = loaded.row.id | | other.number > loaded.row.number {
572
continue ;
573
}
574
let theirs = other.projected_changes ( ) ;
575
if ! theirs.is_empty ( ) & & theirs.iter ( ) .all ( | h | loaded.hashes.iter ( ) .any ( | m | m.to_hex ( ) = = * h ) ) {
576
depends_on.push ( other.number) ;
577
}
578
}
579
if ! depends_on.is_empty ( ) {
580
blockers.push ( format! ( " depends on {} " , depends_on.iter ( ) .map ( | n | format! ( " #{n} " ) ) .collect:: < Vec< _ > > ( ) .join ( " , " ) ) ) ;
581
}
582
Ok ( Verdict { reviews, approvals, blockers, depends_on, requirements } )
583
}
584
585
# [ derive ( Template ) ]
586
# [ template ( path = " proposal.html" ) ]
587
pub struct ProposalPage {
588
shell : Shell,
589
repo : RepoShell,
590
p : pdb:: ProposalRow,
38833ed7 Andy McCall 1 month ago ↶
591
592
closes : Vec< super:: chain:: Link> ,
a1b98695 Andy McCall 1 month ago ↶
593
594
project_choices : Option< Vec< db:: projects:: Project> > ,
595
596
timeline : Vec< Entry> ,
597
598
can_triage : bool ,
de505df5 Andy McCall 1 month ago ↶
599
body_html : String,
600
tab : String,
601
changes : Vec< ChangeView> ,
602
files : Vec< FileRows> ,
9df855f2 Andy McCall 1 month ago ↶
603
diffs : Vec< oxy_core:: repo:: FileDiff> ,
604
bases : std:: collections:: BTreeMap< String, Vec< u8 > > ,
605
606
ignore_ws : bool ,
de505df5 Andy McCall 1 month ago ↶
607
conflicts : Vec< ConflictView> ,
608
conflicted : bool ,
609
comments : Vec< CommentView> ,
610
611
placed : HashMap< ( String, usize ) , Vec< usize > > ,
612
outdated : Vec< usize > ,
613
reviews : Vec< ReviewView> ,
614
approvals : Vec< Approval> ,
615
blockers : Vec< String> ,
616
depends_on : Vec< i64 > ,
617
events : Vec< pdb:: EventRow> ,
618
owners : Vec< String> ,
619
requirements : pdb:: Requirements,
620
landed_already : bool ,
621
can_edit : bool ,
622
can_land : bool ,
623
hub_can_land : bool ,
624
notice : Option< String> ,
625
error : Option< String> ,
626
heads : Vec< String> ,
627
state_hash : String,
628
source_label : String,
629
api_url : String,
18e381ea Andy McCall 1 month ago ↶
630
labels : Vec< db:: issues:: Label> ,
631
all_labels : Vec< db:: issues:: Label> ,
b64e2537 Andy McCall 1 month ago ↶
632
checks : Vec< db:: hooks:: Check> ,
633
required_checks : Vec< String> ,
e1ec9fbf Andy McCall 1 month ago ↶
634
635
projects : Vec< ( db:: projects:: Project, String) > ,
cc1cd197 Andy McCall 1 month ago ↶
636
637
requested : Vec< String> ,
638
639
can_request : bool ,
de505df5 Andy McCall 1 month ago ↶
640
}
641
642
# [ derive ( Deserialize ) ]
643
pub struct DetailQuery {
644
tab : Option< String> ,
a1b98695 Andy McCall 1 month ago ↶
645
9df855f2 Andy McCall 1 month ago ↶
646
#[serde(default)]
a1b98695 Andy McCall 1 month ago ↶
647
notice : Option< String> ,
648
#[serde(default)]
649
error : Option< String> ,
650
#[serde(default)]
9df855f2 Andy McCall 1 month ago ↶
651
w : Option< String> ,
de505df5 Andy McCall 1 month ago ↶
652
}
653
654
655
656
# [ allow ( clippy::too_many_arguments ) ]
657
async fn detail (
658
app : & Shared,
659
shell : Shell,
660
viewer : & Viewer,
661
owner : & str ,
662
name : & str ,
663
number : i64 ,
664
tab : String,
665
notice : Option< String> ,
666
error : Option< String> ,
667
) -> Result< Page< ProposalPage> , Error> {
668
let ( repo, row, role) = lookup ( app, & shell, viewer, owner, name) .await? ;
669
let p = pdb:: get( & app.db, row.id, number)
670
.await
671
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
672
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
673
let loaded = load ( app, & repo, p) .await.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
674
675
if loaded.landed_already & & loaded.row.is_open ( ) {
676
pdb:: set_state( & app.db, loaded.row.id, " landed" , & loaded.target_state) .await.ok ( ) ;
677
pdb:: add_event( & app.db, loaded.row.id, " " , " landed" , " every change reached the target" ) .await.ok ( ) ;
678
}
679
let p = pdb:: get( & app.db, row.id, number) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? .unwrap_or ( loaded.row.clone ( ) ) ;
680
let v = verdict ( app, & loaded, row.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
681
let comment_rows = pdb:: comments( & app.db, loaded.row.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
682
let events = pdb:: events( & app.db, loaded.row.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
683
684
let mut by_atom: HashMap< ( String, String, u32 ) , usize > = HashMap:: new( ) ;
685
for ( ( path, line) , ( h, i) ) in & loaded.atoms {
686
by_atom.insert ( ( path.clone ( ) , h.clone ( ) , * i) , * line) ;
687
}
688
let mut comments = Vec :: new( ) ;
689
let mut placed: HashMap< ( String, usize ) , Vec< usize > > = HashMap:: new( ) ;
690
let mut outdated = Vec :: new( ) ;
691
for ( idx, c) in comment_rows.into_iter ( ) .enumerate ( ) {
692
let line = if c.anchor_change.is_empty ( ) {
693
None
694
} else {
695
by_atom.get ( & ( c.path.clone ( ) , c.anchor_change.clone ( ) , c.anchor_index as u32 ) ) .copied ( )
696
} ;
697
if ! c.anchor_change.is_empty ( ) {
698
match line {
699
Some ( l) => placed.entry ( ( c.path.clone ( ) , l) ) .or_default ( ) .push ( idx) ,
700
None => outdated.push ( idx) ,
701
}
702
}
703
comments.push ( CommentView { body_html: render:: markdown:: render( & c.body, " /" , " /" ) , row: c, line } ) ;
704
}
705
let mut changes = loaded.changes.clone ( ) ;
706
super :: attribute( app, & mut changes.iter_mut ( ) .collect:: < Vec< _ > > ( ) ) .await;
707
let me = viewer.account ( ) ;
708
let can_edit = me.is_some_and ( | a | Some ( a.id) = = p.author_id | | role > = Role:: Maintain) ;
709
let can_land = role > = Role:: Write & & p.is_open ( ) & & v.blockers.is_empty ( ) ;
cc1cd197 Andy McCall 1 month ago ↶
710
let can_request = p.is_open ( ) & & me.is_some_and ( | a | Some ( a.id) = = p.author_id | | role > = Role:: Write) ;
14530604 Andy McCall 1 month ago ↶
711
let delegated = match viewer.account ( ) {
712
Some ( a) => super :: edit:: delegated_identity( app, a.id) .await.is_some ( ) ,
713
None => false ,
714
} ;
715
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 ↶
716
let source_label = match p.source_kind.as_str ( ) {
717
" fork" => format! (
718
" fork {} " ,
719
db:: repos:: get_by_id( & app.db, p.source_repo_id.unwrap_or ( 0 ) )
720
.await
721
.ok ( )
722
.flatten ( )
723
.map ( | r | format! ( " {} :{} " , r.slug ( ) , p.source_bookmark) )
724
.unwrap_or_default ( )
725
) ,
726
" changes" => format! ( " {} changes" , p.source_changes ( ) .len ( ) ) ,
727
_ => p.source_bookmark.clone ( ) ,
728
} ;
e1ec9fbf Andy McCall 1 month ago ↶
729
let projects = super :: projects:: placements_for( app, viewer, & row, p.number) .await;
38833ed7 Andy McCall 1 month ago ↶
730
let texts: Vec< String> = std:: iter:: once( p.body.clone ( ) ) .chain ( changes.iter ( ) .map ( | c | format! ( " {} \n {} " , c.title, c.body) ) ) .collect ( ) ;
731
let closes = super :: chain:: issues_closed_by( app, & row, & texts.iter ( ) .map ( String :: as_str) .collect:: < Vec< _ > > ( ) ) .await;
a1b98695 Andy McCall 1 month ago ↶
732
let project_choices = super :: projects:: project_choices( app, viewer, & row, & projects) .await;
733
let mut dated: Vec< ( i64 , Entry) > = Vec :: new( ) ;
734
for ( index, c) in comments.iter ( ) .enumerate ( ) {
735
if c.row.anchor_change.is_empty ( ) {
736
dated.push ( ( c.row.created_ms, Entry { kind: " comment" , index } ) ) ;
737
}
738
}
739
for ( index, r) in v.reviews.iter ( ) .enumerate ( ) {
740
dated.push ( ( r.row.created_ms, Entry { kind: " review" , index } ) ) ;
741
}
742
for ( index, e) in events.iter ( ) .enumerate ( ) {
743
dated.push ( ( e.at_ms, Entry { kind: " event" , index } ) ) ;
744
}
745
dated.sort_by_key ( | ( at , _) | * at) ;
746
let timeline: Vec< Entry> = dated.into_iter ( ) .map ( | ( _, e ) | e ) .collect ( ) ;
de505df5 Andy McCall 1 month ago ↶
747
Ok ( Page( ProposalPage {
a1b98695 Andy McCall 1 month ago ↶
748
can_triage: role > = Role:: Triage,
749
timeline,
38833ed7 Andy McCall 1 month ago ↶
750
closes,
a1b98695 Andy McCall 1 month ago ↶
751
project_choices,
e1ec9fbf Andy McCall 1 month ago ↶
752
projects,
de505df5 Andy McCall 1 month ago ↶
753
shell,
754
repo: RepoShell:: new( app, & row, " proposals" ) .with_role ( role) ,
c79a1111 Andy McCall 1 month ago ↶
755
body_html: super :: issues:: markdown( app, & row, & row.default_bookmark, & p.body) .await,
de505df5 Andy McCall 1 month ago ↶
756
p,
757
tab,
758
changes,
759
files: loaded.files,
9df855f2 Andy McCall 1 month ago ↶
760
diffs: loaded.diffs,
761
bases: loaded.bases,
762
ignore_ws: false ,
de505df5 Andy McCall 1 month ago ↶
763
conflicts: loaded.conflicts,
764
conflicted: loaded.conflicted,
765
comments,
766
placed,
767
outdated,
768
reviews: v.reviews,
769
approvals: v.approvals,
770
blockers: v.blockers,
771
depends_on: v.depends_on,
772
events,
773
owners: loaded.owners,
774
requirements: v.requirements,
775
landed_already: loaded.landed_already,
776
can_edit,
777
can_land,
778
hub_can_land,
779
notice,
780
error,
781
heads: loaded.heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ,
b64e2537 Andy McCall 1 month ago ↶
782
state_hash: loaded.state.clone ( ) ,
de505df5 Andy McCall 1 month ago ↶
783
source_label,
784
api_url: format! ( " {} /api/v1/repos/{owner} /{name} /proposals/{number} " , app.config.base_url ( ) ) ,
18e381ea Andy McCall 1 month ago ↶
785
labels: db:: issues:: proposal_labels( & app.db, loaded.row.id) .await.unwrap_or_default ( ) ,
786
all_labels: db:: issues:: labels( & app.db, row.id) .await.unwrap_or_default ( ) ,
b64e2537 Andy McCall 1 month ago ↶
787
checks: db:: hooks:: checks_for_state( & app.db, row.id, & loaded.state) .await.unwrap_or_default ( ) ,
788
required_checks: db:: hooks:: required_checks( & app.db, row.id) .await.unwrap_or_default ( ) ,
cc1cd197 Andy McCall 1 month ago ↶
789
requested: pdb:: review_requests( & app.db, loaded.row.id) .await.unwrap_or_default ( ) ,
790
can_request,
de505df5 Andy McCall 1 month ago ↶
791
} ) )
792
}
793
a1b98695 Andy McCall 1 month ago ↶
794
795
796
fn back ( owner : & str , name : & str , number : i64 , tab : & str , notice : Option< String> , error : Option< String> ) -> Response {
797
let mut url = format! ( " /{owner} /{name} /proposal/{number} ?tab={tab} " ) ;
798
if let Some ( n) = notice {
799
url.push_str ( & format! ( " ¬ice={} " , super :: settings:: urlencode( & n) ) ) ;
800
}
801
if let Some ( e) = error {
802
url.push_str ( & format! ( " &error={} " , super :: settings:: urlencode( & e) ) ) ;
803
}
804
Redirect:: to( & url) .into_response ( )
805
}
806
de505df5 Andy McCall 1 month ago ↶
807
pub async fn show (
808
State( app ) : State< Shared> ,
809
viewer : Viewer,
810
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
811
Query( q) : Query< DetailQuery> ,
812
) -> Result< Page< ProposalPage> , Error> {
813
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
a1b98695 Andy McCall 1 month ago ↶
814
let mut page =
815
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 ↶
816
if q.w.as_deref ( ) = = Some ( " 1" ) {
817
818
page.0. files =
819
super :: history:: render_files_with( & page.0. diffs, & page.0. bases, crate :: render:: diff:: Options { ignore_whitespace: true } ) ;
820
page.0. ignore_ws = true ;
821
}
822
Ok ( page)
de505df5 Andy McCall 1 month ago ↶
823
}
824
825
826
827
# [ derive ( Deserialize ) ]
828
pub struct CommentForm {
829
#[serde(rename = "_csrf")]
830
csrf_token : String,
831
body : String,
832
#[serde(default)]
833
path : String,
834
#[serde(default)]
835
line : Option< usize > ,
836
}
837
838
pub async fn comment (
839
State( app ) : State< Shared> ,
840
viewer : Viewer,
841
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
842
Form( f) : Form< CommentForm> ,
a1b98695 Andy McCall 1 month ago ↶
843
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
844
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
845
let me = require ( & viewer, & shell) ? ;
846
csrf ( & viewer, & f.csrf_token, & shell) ? ;
847
let ( repo, row, _ ) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
848
let p = pdb:: get( & app.db, row.id, number)
849
.await
850
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
851
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
852
if f.body.trim ( ) .is_empty ( ) {
a1b98695 Andy McCall 1 month ago ↶
853
return Ok ( back ( & owner, & name, number, " conversation" , None , Some ( " An empty comment says nothing." .into ( ) ) ) ) ;
de505df5 Andy McCall 1 month ago ↶
854
}
855
let anchor = match ( f.path.trim ( ) , f.line) {
856
( " " , _ ) | ( _ , None ) => None ,
857
( path, Some ( line) ) => {
858
let loaded = load ( & app, & repo, p.clone ( ) ) .await.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
859
loaded.atoms.get ( & ( path.to_string ( ) , line) ) .cloned ( )
860
}
861
} ;
862
let tab = if anchor.is_some ( ) { " files" } else { " conversation" } ;
863
pdb:: add_comment(
864
& app.db,
865
p.id,
866
Some ( me.account.id) ,
867
& me.account.username,
868
f.body.trim ( ) ,
869
f.path.trim ( ) ,
870
anchor.as_ref ( ) .map ( | ( h , i ) | ( h.as_str ( ) , * i as i64 ) ) ,
871
)
872
.await
873
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
a1b98695 Andy McCall 1 month ago ↶
874
Ok ( back ( & owner, & name, number, tab, None , None ) )
de505df5 Andy McCall 1 month ago ↶
875
}
876
877
# [ derive ( Deserialize ) ]
878
pub struct ReviewForm {
879
#[serde(rename = "_csrf")]
880
csrf_token : String,
881
verdict : String,
882
#[serde(default)]
883
body : String,
884
}
885
886
pub async fn review (
887
State( app ) : State< Shared> ,
888
viewer : Viewer,
889
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
890
Form( f) : Form< ReviewForm> ,
a1b98695 Andy McCall 1 month ago ↶
891
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
892
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
893
let me = require ( & viewer, & shell) ? ;
894
csrf ( & viewer, & f.csrf_token, & shell) ? ;
895
let ( _ , row, _ ) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
896
let p = pdb:: get( & app.db, row.id, number)
897
.await
898
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
899
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
900
if ! matches! ( f.verdict.as_str ( ) , " comment" | " changes" | " approve" ) {
901
return Err ( Error:: bad( shell, " a review is comment, changes or approve" ) ) ;
902
}
903
if f.verdict = = " approve" & & Some ( me.account.id) = = p.author_id {
a1b98695 Andy McCall 1 month ago ↶
904
return Ok ( back ( & owner, & name, number, " conversation" , None , Some ( " You cannot approve your own proposal." .into ( ) ) ) ) ;
de505df5 Andy McCall 1 month ago ↶
905
}
906
pdb:: add_review( & app.db, p.id, me.account.id, & me.account.username, & f.verdict, f.body.trim ( ) )
907
.await
908
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
909
pdb:: add_event( & app.db, p.id, & me.account.username, & format! ( " review.{} " , f.verdict) , " " ) .await.ok ( ) ;
cc1cd197 Andy McCall 1 month ago ↶
910
reviewed ( & app, & row, & p, & me.account, & f.verdict) .await;
b64e2537 Andy McCall 1 month ago ↶
911
crate :: hooks:: emit( & app, & row, " review" , & me.account.username, serde_json:: json! ( { " number" : number, " verdict" : f.verdict } ) ) .await;
de505df5 Andy McCall 1 month ago ↶
912
let notice = if f.verdict = = " approve" {
913
Some ( format! (
914
" Approval recorded. It counts once you seal the changes: oxy seal --proposal {} /{owner} /{name} /proposal/{number} " ,
915
app.config.base_url ( )
916
) )
917
} else {
918
None
919
} ;
a1b98695 Andy McCall 1 month ago ↶
920
Ok ( back ( & owner, & name, number, " conversation" , notice, None ) )
de505df5 Andy McCall 1 month ago ↶
921
}
922
923
# [ derive ( Deserialize ) ]
924
pub struct EditForm {
925
#[serde(rename = "_csrf")]
926
csrf_token : String,
927
title : String,
928
#[serde(default)]
929
body : String,
930
target : String,
931
}
932
933
pub async fn edit (
934
State( app ) : State< Shared> ,
935
viewer : Viewer,
936
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
937
Form( f) : Form< EditForm> ,
a1b98695 Andy McCall 1 month ago ↶
938
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
939
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
940
let me = require ( & viewer, & shell) ? ;
941
csrf ( & viewer, & f.csrf_token, & shell) ? ;
942
let ( _ , row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
943
let p = pdb:: get( & app.db, row.id, number)
944
.await
945
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
946
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
947
if Some ( me.account.id) ! = p.author_id & & role < Role:: Maintain {
948
return Err ( Error:: not_found( shell) ) ;
949
}
950
let target = if crate :: repos:: names:: valid_segment( f.target.trim ( ) ) { f.target.trim ( ) } else { & p.target } ;
951
pdb:: edit( & app.db, p.id, f.title.trim ( ) , f.body.trim ( ) , target) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
952
pdb:: add_event( & app.db, p.id, & me.account.username, " edited" , " " ) .await.ok ( ) ;
a1b98695 Andy McCall 1 month ago ↶
953
Ok ( back ( & owner, & name, number, " conversation" , Some ( " Saved." .into ( ) ) , None ) )
de505df5 Andy McCall 1 month ago ↶
954
}
955
956
# [ derive ( Deserialize ) ]
957
pub struct StateForm {
958
#[serde(rename = "_csrf")]
959
csrf_token : String,
960
action : String,
961
}
962
963
pub async fn state (
964
State( app ) : State< Shared> ,
965
viewer : Viewer,
966
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
967
Form( f) : Form< StateForm> ,
a1b98695 Andy McCall 1 month ago ↶
968
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
969
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
970
let me = require ( & viewer, & shell) ? ;
971
csrf ( & viewer, & f.csrf_token, & shell) ? ;
972
let ( _ , row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
973
let p = pdb:: get( & app.db, row.id, number)
974
.await
975
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
976
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
977
if Some ( me.account.id) ! = p.author_id & & role < Role:: Triage {
978
return Err ( Error:: not_found( shell) ) ;
979
}
980
let new_state = match ( f.action.as_str ( ) , p.state.as_str ( ) ) {
981
( " close" , " open" | " draft" ) => " closed" ,
982
( " reopen" , " closed" ) => " open" ,
983
( " ready" , " draft" ) => " open" ,
984
( " draft" , " open" ) => " draft" ,
985
_ => return Err ( Error:: bad( shell, " that transition is not possible" ) ) ,
986
} ;
987
pdb:: set_state( & app.db, p.id, new_state, " " ) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
988
pdb:: add_event( & app.db, p.id, & me.account.username, new_state, " " ) .await.ok ( ) ;
b64e2537 Andy McCall 1 month ago ↶
989
crate :: hooks:: emit( & app, & row, & format! ( " proposal.{new_state} " ) , & me.account.username, serde_json:: json! ( { " number" : number } ) ) .await;
a1b98695 Andy McCall 1 month ago ↶
990
Ok ( back ( & owner, & name, number, " conversation" , None , None ) )
de505df5 Andy McCall 1 month ago ↶
991
}
992
993
pub async fn resolve_comment (
994
State( app ) : State< Shared> ,
995
viewer : Viewer,
996
Path( (owner , name , number , id ) ) : Path< ( String, String, i64 , i64 ) > ,
997
Form( f) : Form< super:: session:: CsrfForm> ,
a1b98695 Andy McCall 1 month ago ↶
998
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
999
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1000
require ( & viewer, & shell) ? ;
1001
csrf ( & viewer, & f.csrf, & shell) ? ;
1002
let ( _ , row, _ ) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
1003
let p = pdb:: get( & app.db, row.id, number)
1004
.await
1005
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1006
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1007
let current: Option< bool > = sqlx:: query_scalar( " SELECT resolved FROM proposal_comments WHERE id = ? AND proposal_id = ?" )
1008
.bind ( id)
1009
.bind ( p.id)
1010
.fetch_optional ( & app.db)
1011
.await
1012
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
1013
if let Some ( r) = current {
1014
pdb:: set_comment_resolved( & app.db, id, ! r) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
1015
}
a1b98695 Andy McCall 1 month ago ↶
1016
Ok ( back ( & owner, & name, number, " files" , None , None ) )
de505df5 Andy McCall 1 month ago ↶
1017
}
1018
1019
1020
1021
pub async fn land (
1022
State( app ) : State< Shared> ,
1023
viewer : Viewer,
1024
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1025
Form( f) : Form< super:: session:: CsrfForm> ,
a1b98695 Andy McCall 1 month ago ↶
1026
) -> Result< Response, Error> {
de505df5 Andy McCall 1 month ago ↶
1027
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1028
let me = require ( & viewer, & shell) ? ;
1029
csrf ( & viewer, & f.csrf, & shell) ? ;
1030
let ( repo, row, _ ) = lookup_role ( & app, & shell, & viewer, & owner, & name, Access:: Write) .await? ;
0b20f1ec Andy McCall 1 month ago ↶
1031
match land_as ( & app, & repo, & row, & me.account, number, " web" ) .await {
a1b98695 Andy McCall 1 month ago ↶
1032
Ok ( _ ) => Ok ( back ( & owner, & name, number, " conversation" , Some ( " Landed." .into ( ) ) , None ) ) ,
1f1eb06a Andy McCall 1 month ago ↶
1033
Err ( Landing:: NotFound) => Err ( Error:: not_found( shell) ) ,
a1b98695 Andy McCall 1 month ago ↶
1034
Err ( Landing:: Refused( m) ) => Ok ( back ( & owner, & name, number, " conversation" , None , Some ( m) ) ) ,
1f1eb06a Andy McCall 1 month ago ↶
1035
}
1036
}
1037
1038
1039
pub enum Landing {
1040
NotFound,
1041
Refused( String ) ,
1042
}
1043
1044
1045
1046
0b20f1ec Andy McCall 1 month ago ↶
1047
1048
1f1eb06a Andy McCall 1 month ago ↶
1049
pub async fn land_as (
1050
app : & Shared,
1051
repo : & RepoName,
1052
row : & db:: repos:: RepoRow,
1053
me : & accounts:: Account,
1054
number : i64 ,
0b20f1ec Andy McCall 1 month ago ↶
1055
via : & str ,
1f1eb06a Andy McCall 1 month ago ↶
1056
) -> Result< String, Landing> {
1057
let ( owner, name) = ( row.owner.clone ( ) , row.name.clone ( ) ) ;
1058
let refused = | e : String| Landing:: Refused( e) ;
1059
let p = pdb:: get( & app.db, row.id, number) .await.map_err ( | e | refused ( e.to_string ( ) ) ) ? .ok_or ( Landing:: NotFound) ? ;
1060
let loaded = load ( app, repo, p.clone ( ) ) .await.map_err ( | e | refused ( e.to_string ( ) ) ) ? ;
1061
let v = verdict ( app, & loaded, row.id) .await.map_err ( | e | refused ( e.to_string ( ) ) ) ? ;
1062
if ! p.is_open ( ) {
1063
return Err ( refused ( format! ( " Cannot land: the proposal is {} " , p.state) ) ) ;
1064
}
1065
if ! v.blockers.is_empty ( ) {
1066
return Err ( refused ( format! ( " Cannot land: {} " , v.blockers.join ( " ; " ) ) ) ) ;
de505df5 Andy McCall 1 month ago ↶
1067
}
1068
let target = p.target.clone ( ) ;
1069
let heads = loaded.heads.clone ( ) ;
14530604 Andy McCall 1 month ago ↶
1070
1071
1072
1073
1f1eb06a Andy McCall 1 month ago ↶
1074
let result = match super :: edit:: signer_for( app, me.id) .await {
1075
Ok ( signer) => crate :: repos:: edits:: move_bookmark( & app.repos, repo, target.clone ( ) , heads.clone ( ) , signer) .await.map ( | _| pl:: state_ref( & heads) .to_hex ( ) ) ,
14530604 Andy McCall 1 month ago ↶
1076
Err ( why) => {
1077
app.repos
1f1eb06a Andy McCall 1 month ago ↶
1078
.write ( repo, move | r, _ | {
14530604 Andy McCall 1 month ago ↶
1079
if r.current_policy ( ) ? .is_some ( ) {
1080
return Err ( crate :: repos:: RepoError:: Oxy( oxy_core:: repo:: RepoError:: Msg( format! (
1081
" 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} "
1082
) ) ) ) ;
1083
}
1084
r.bookmark_set ( & target, & heads) ? ;
1085
Ok ( pl:: state_ref( & heads) .to_hex ( ) )
1086
} )
1087
.await
1088
}
1089
} ;
1f1eb06a Andy McCall 1 month ago ↶
1090
let state = result.map_err ( | e | refused ( e.to_string ( ) ) ) ? ;
0b20f1ec Andy McCall 1 month ago ↶
1091
pdb:: set_landed( & app.db, p.id, & state, & me.username, via) .await.map_err ( | e | refused ( e.to_string ( ) ) ) ? ;
1092
let how = if v.approvals.is_empty ( ) { format! ( " {via} , with no approval" ) } else { via.to_string ( ) } ;
1093
pdb:: add_event( & app.db, p.id, & me.username, " landed" , & how) .await.ok ( ) ;
1f1eb06a Andy McCall 1 month ago ↶
1094
crate :: hooks:: emit(
1095
app,
1096
row,
1097
" proposal.landed" ,
1098
& me.username,
1099
serde_json:: json! ( { " number" : number, " target" : p.target, " state" : state } ) ,
1100
)
1101
.await;
1102
crate :: notify:: deliver(
1103
app,
1104
crate :: notify:: Event {
1105
kind: " proposal.landed" ,
1106
title: format! ( " {owner} /{name} #{number} : {} landed" , p.title) ,
1107
url: format! ( " /{owner} /{name} /proposal/{number} " ) ,
1108
actor: me,
1109
repo_id: row.id,
1110
thread: Some ( ( " proposal" , p.id) ) ,
1111
mention_text: " " ,
1112
direct: p.author_id.into_iter ( ) .collect ( ) ,
1113
} ,
1114
)
1115
.await;
1116
if p.target = = row.default_bookmark {
1117
let messages: Vec< ( String, String) > = loaded.changes.iter ( ) .map ( | c | ( c.hash.clone ( ) , format! ( " {} \n {} " , c.title, c.body) ) ) .collect ( ) ;
1118
super :: issues:: close_from_messages( app, row, & messages) .await;
de505df5 Andy McCall 1 month ago ↶
1119
}
0b20f1ec Andy McCall 1 month ago ↶
1120
accounts:: audit( & app.db, Some ( me) , " proposal.landed" , & format! ( " {owner} /{name} #{number} " ) , & how) .await.ok ( ) ;
1f1eb06a Andy McCall 1 month ago ↶
1121
db:: repos:: record_push(
1122
& app.db,
1123
row.id,
1124
Some ( ( me.id, me.username.as_str ( ) ) ) ,
1125
& [ db:: repos:: PushOutcome {
1126
reference: p.target.clone ( ) ,
1127
ok: true ,
0b20f1ec Andy McCall 1 month ago ↶
1128
reason: format! ( " landed #{number} ({how} )" ) ,
1f1eb06a Andy McCall 1 month ago ↶
1129
old_state: loaded.target_state.clone ( ) ,
1130
new_state: state.clone ( ) ,
1131
heads: loaded.heads.len ( ) as i64 ,
1132
changes: loaded.hashes.len ( ) as i64 ,
1133
} ] ,
1134
)
1135
.await
1136
.ok ( ) ;
1137
Ok ( state)
de505df5 Andy McCall 1 month ago ↶
1138
}
1139
18e381ea Andy McCall 1 month ago ↶
1140
# [ derive ( Deserialize ) ]
1141
pub struct LabelsForm {
1142
#[serde(rename = "_csrf")]
1143
csrf_token : String,
1144
#[serde(default)]
1145
label : Vec< i64 > ,
1146
}
1147
1148
pub async fn set_labels (
1149
State( app ) : State< Shared> ,
1150
viewer : Viewer,
1151
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1152
axum_extra:: extract:: Form( f) : axum_extra:: extract:: Form< LabelsForm> ,
a1b98695 Andy McCall 1 month ago ↶
1153
) -> Result< Response, Error> {
18e381ea Andy McCall 1 month ago ↶
1154
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1155
require ( & viewer, & shell) ? ;
1156
csrf ( & viewer, & f.csrf_token, & shell) ? ;
1157
let ( _ , row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
1158
if role < Role:: Triage {
1159
return Err ( Error:: not_found( shell) ) ;
1160
}
1161
let p = pdb:: get( & app.db, row.id, number)
1162
.await
1163
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1164
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1165
for l in db:: issues:: labels( & app.db, row.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? {
1166
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) ) ? ;
1167
}
a1b98695 Andy McCall 1 month ago ↶
1168
Ok ( back ( & owner, & name, number, " conversation" , None , None ) )
18e381ea Andy McCall 1 month ago ↶
1169
}
1170
b64e2537 Andy McCall 1 month ago ↶
1171
1172
1173
pub async fn events (
1174
State( app ) : State< Shared> ,
1175
viewer : Viewer,
1176
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1177
) -> Result< axum:: response:: Sse< impl futures:: Stream< Item = Result< axum:: response:: sse:: Event, std:: convert:: Infallible> > > , Error> {
1178
use futures:: StreamExt;
1179
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1180
let ( _ , row, _ ) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
1181
let rx = app.events.proposals.subscribe ( ) ;
1182
let repo_id = row.id;
1183
let stream = tokio_stream:: wrappers:: BroadcastStream:: new( rx) .filter_map ( move | item| async move {
1184
match item {
1185
Ok ( ( id, ev) ) if id = = repo_id => {
1186
Some ( Ok ( axum:: response:: sse:: Event:: default( ) .event ( ev.kind.clone ( ) ) .data ( serde_json:: to_string( & ev) .unwrap_or_default ( ) ) ) )
1187
}
1188
_ => None ,
1189
}
1190
} ) ;
1191
Ok ( axum:: response:: Sse:: new( stream) .keep_alive ( axum:: response:: sse:: KeepAlive:: default( ) ) )
1192
}
1193
de505df5 Andy McCall 1 month ago ↶
1194
1195
1196
pub async fn to_api ( app : & Shared, owner : & str , name : & str , loaded : & Loaded, v : & Verdict, source_url : Option< String> ) -> hub_api:: Proposal {
1197
let p = & loaded.row;
1198
hub_api:: Proposal {
1199
number: p.number,
1200
title: p.title.clone ( ) ,
1201
body: p.body.clone ( ) ,
1202
author: p.author.clone ( ) ,
1203
state: ProposalState:: parse( & p.state) .unwrap_or ( ProposalState:: Open) ,
1204
target: p.target.clone ( ) ,
1205
source: match p.source_kind.as_str ( ) {
1206
" fork" => {
1207
let ( o, n) = source_url
1208
.as_deref ( )
1209
.and_then ( | u | u.rsplit_once ( ' /' ) )
1210
.and_then ( | ( b , n ) | b.rsplit_once ( ' /' ) .map ( | ( _, o ) | ( o.to_string ( ) , n.to_string ( ) ) ) )
1211
.unwrap_or_default ( ) ;
1212
ProposalSource:: Fork { owner: o, name: n, bookmark: p.source_bookmark.clone ( ) }
1213
}
1214
" changes" => ProposalSource:: Changes { hashes: p.source_changes ( ) } ,
1215
_ => ProposalSource:: Bookmark { bookmark: p.source_bookmark.clone ( ) } ,
1216
} ,
1217
changes: loaded
1218
.changes
1219
.iter ( )
1220
.map ( | c | hub_api:: ChangeSummary {
1221
hash: c.hash.clone ( ) ,
1222
message: if c.body.is_empty ( ) { c.title.clone ( ) } else { format! ( " {} \n \n {} " , c.title, c.body) } ,
1223
author: hub_api:: Author {
1224
name: c.author.name.clone ( ) ,
1225
email: c.author.email.clone ( ) ,
1226
key: c.author.key.clone ( ) ,
1227
account: c.author.account.clone ( ) ,
760a7dd3 Andy McCall 1 month ago ↶
1228
note: hub_api:: Author:: note_for( & c.author.key, & c.author.account) ,
de505df5 Andy McCall 1 month ago ↶
1229
} ,
1230
when_ms: c.when_ms,
1231
deps: c.deps.clone ( ) ,
1232
head: c.head,
1233
signature: match & c.signature {
1234
view:: SigView:: Unsigned => hub_api:: Signature:: Unsigned,
1235
view:: SigView:: Valid { key, sealed, revoked, .. } => {
1236
hub_api:: Signature:: Valid { key: key.clone ( ) , sealed: * sealed, revoked: * revoked }
1237
}
1238
view:: SigView:: Invalid( why) => hub_api:: Signature:: Invalid { reason: why.clone ( ) } ,
1239
} ,
1240
} )
1241
.collect ( ) ,
1242
projected_heads: loaded.heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ,
1243
projected_state: Some ( loaded.state.clone ( ) ) ,
1244
conflicted: loaded.conflicted,
1245
approvals: v.approvals.clone ( ) ,
1246
landable: p.is_open ( ) & & v.blockers.is_empty ( ) ,
1247
blockers: v.blockers.clone ( ) ,
1248
created_ms: p.created_ms,
1249
updated_ms: p.updated_ms,
1250
url: format! ( " {} /{owner} /{name} /proposal/{} " , app.config.base_url ( ) , p.number) ,
1251
repository_url: format! ( " {} /{owner} /{name} " , app.config.base_url ( ) ) ,
1252
source_url,
0b20f1ec Andy McCall 1 month ago ↶
1253
landed: ( p.state = = " landed" & & p.landed_ms > 0 ) .then ( | | hub_api:: Landed {
1254
by: p.landed_by.clone ( ) ,
1255
via: p.landed_via.clone ( ) ,
1256
when_ms: p.landed_ms,
1257
} ) ,
cc1cd197 Andy McCall 1 month ago ↶
1258
requested_reviewers: pdb:: review_requests( & app.db, p.id) .await.unwrap_or_default ( ) ,
de505df5 Andy McCall 1 month ago ↶
1259
}
1260
}
1261
1262
1263
pub fn key ( path : & str , line : impl filters:: Num) -> ( String, usize ) {
1264
( path.to_string ( ) , line.as_u64 ( ) as usize )
1265
}
1266
1267
# [ allow ( dead_code ) ]
1268
fn _unused ( _ : & diff:: Line, _ : & ConflictView, _ : & Role) { }
cc1cd197 Andy McCall 1 month ago ↶
1269
1270
1271
1272
1273
1274
1275
1276
pub async fn request_reviewers (
1277
app : & Shared,
1278
row : & db:: repos:: RepoRow,
1279
p : & pdb:: ProposalRow,
1280
by : & accounts:: Account,
1281
names : & [String],
1282
) -> Result< Vec< String> , String> {
1283
let mut found = Vec :: new( ) ;
1284
for name in names.iter ( ) .map ( | n | n.trim ( ) .trim_start_matches ( ' @' ) ) .filter ( | n | ! n.is_empty ( ) ) {
1285
let Some ( a) = accounts:: account_by_name( & app.db, name) .await.ok ( ) .flatten ( ) else {
1286
return Err ( format! ( " There is no account {name} ." ) ) ;
1287
} ;
1288
if Some ( a.id) = = p.author_id {
1289
return Err ( format! ( " {name} wrote this proposal, and cannot review it." ) ) ;
1290
}
1291
let role = crate :: auth:: roles:: role_for( & app.db, Some ( & a) , row) .await.ok ( ) .flatten ( ) ;
1292
if ! crate :: auth:: roles:: allows( role, crate :: auth:: Access:: Read) {
1293
return Err ( format! ( " {name} cannot read this repository." ) ) ;
1294
}
1295
found.push ( a) ;
1296
}
1297
let mut asked = Vec :: new( ) ;
1298
for a in found {
1299
if ! pdb:: request_review( & app.db, p.id, a.id, & by.username) .await.map_err ( | e | e.to_string ( ) ) ? {
1300
continue ;
1301
}
1302
pdb:: add_event( & app.db, p.id, & by.username, " review requested" , & a.username) .await.ok ( ) ;
1303
crate :: notify:: deliver(
1304
app,
1305
crate :: notify:: Event {
1306
kind: " proposal.review_requested" ,
1307
title: format! ( " {} /{} #{} : {} (review requested from {} )" , row.owner, row.name, p.number, p.title, a.username) ,
1308
url: format! ( " /{} /{} /proposal/{} " , row.owner, row.name, p.number) ,
1309
actor: by,
1310
repo_id: row.id,
1311
thread: None ,
1312
mention_text: " " ,
1313
direct: vec! [ a.id] ,
1314
} ,
1315
)
1316
.await;
1317
asked.push ( a.username) ;
1318
}
1319
Ok ( asked)
1320
}
1321
1322
1323
1324
pub async fn reviewed ( app : & Shared, row : & db:: repos:: RepoRow, p : & pdb:: ProposalRow, by : & accounts:: Account, verdict : & str ) {
1325
pdb:: unrequest_review( & app.db, p.id, by.id) .await.ok ( ) ;
1326
let what = match verdict {
1327
" approve" => " approved" ,
1328
" changes" => " changes requested" ,
1329
_ => " reviewed" ,
1330
} ;
1331
crate :: notify:: deliver(
1332
app,
1333
crate :: notify:: Event {
1334
kind: " proposal.review" ,
1335
title: format! ( " {} /{} #{} : {} ({what} by {} )" , row.owner, row.name, p.number, p.title, by.username) ,
1336
url: format! ( " /{} /{} /proposal/{} " , row.owner, row.name, p.number) ,
1337
actor: by,
1338
repo_id: row.id,
1339
thread: Some ( ( " proposal" , p.id) ) ,
1340
mention_text: " " ,
1341
direct: p.author_id.into_iter ( ) .collect ( ) ,
1342
} ,
1343
)
1344
.await;
1345
}
1346
1347
# [ derive ( Deserialize ) ]
1348
pub struct ReviewersForm {
1349
#[serde(rename = "_csrf")]
1350
csrf_token : String,
1351
1352
action : String,
1353
username : String,
1354
}
1355
1356
1357
1358
pub async fn reviewers (
1359
State( app ) : State< Shared> ,
1360
viewer : Viewer,
1361
Path( (owner , name , number ) ) : Path< ( String, String, i64 ) > ,
1362
Form( f) : Form< ReviewersForm> ,
1363
) -> Result< Response, Error> {
1364
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /proposal/{number} " ) , & viewer) ;
1365
let me = require ( & viewer, & shell) ? ;
1366
csrf ( & viewer, & f.csrf_token, & shell) ? ;
1367
let ( _ , row, role) = lookup ( & app, & shell, & viewer, & owner, & name) .await? ;
1368
let p = pdb:: get( & app.db, row.id, number)
1369
.await
1370
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
1371
.ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
1372
if Some ( me.account.id) ! = p.author_id & & role < Role:: Write {
1373
return Err ( Error:: not_found( shell) ) ;
1374
}
1375
let who = f.username.trim ( ) .trim_start_matches ( ' @' ) .to_string ( ) ;
1376
if f.action = = " remove" {
1377
if let Some ( a) = accounts:: account_by_name( & app.db, & who) .await.ok ( ) .flatten ( ) {
1378
if pdb:: unrequest_review( & app.db, p.id, a.id) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? {
1379
pdb:: add_event( & app.db, p.id, & me.account.username, " review request withdrawn" , & a.username) .await.ok ( ) ;
1380
}
1381
}
1382
return Ok ( back ( & owner, & name, number, " conversation" , None , None ) ) ;
1383
}
1384
match request_reviewers ( & app, & row, & p, & me.account, & [ who] ) .await {
1385
Ok ( asked) if asked.is_empty ( ) => Ok ( back ( & owner, & name, number, " conversation" , Some ( " Already asked." .into ( ) ) , None ) ) ,
1386
Ok ( asked) => Ok ( back ( & owner, & name, number, " conversation" , Some ( format! ( " Asked {} to review." , asked.join ( " , " ) ) ) , None ) ) ,
1387
Err ( e) => Ok ( back ( & owner, & name, number, " conversation" , None , Some ( e) ) ) ,
1388
}
1389
}