14530604 Andy McCall 1 month ago ↶
1
2
3
4
5
6
use super :: settings:: { csrf, require} ;
7
use super :: { lookup_role, Error, Page, RepoShell, Shell} ;
8
use crate :: auth:: { Access, Viewer} ;
9
use crate :: db:: { self , accounts} ;
10
use crate :: repos:: edits:: { self , Signer} ;
11
use crate :: repos:: view:: { self , FileBody} ;
12
use crate :: Shared;
13
use askama:: Template;
14
use axum:: extract:: { Form, Path, Query, State} ;
15
use axum:: response:: { IntoResponse, Redirect, Response} ;
b43a9ab0 Andy McCall 1 month ago ↶
16
use oxy_core:: identity:: { Identity, Scope} ;
14530604 Andy McCall 1 month ago ↶
17
use oxy_core:: repo:: FileEdit;
18
use serde:: Deserialize;
19
20
21
pub async fn delegated_identity ( app : & Shared, account_id : i64 ) -> Option< Identity> {
22
let linked = accounts:: identities_of( & app.db, account_id) .await.ok ( ) ? ;
b43a9ab0 Andy McCall 1 month ago ↶
23
24
25
let now = crate :: db:: now_ms( ) as u64 ;
26
linked
27
.into_iter ( )
28
.filter_map ( | l | Identity:: decode( & l.object) .ok ( ) )
29
.find ( | i | i.delegates_to ( & app.hub_key.key, Scope:: CHANGES | Scope:: REFS , now) )
14530604 Andy McCall 1 month ago ↶
30
}
31
32
33
pub async fn signer_for ( app : & Shared, account_id : i64 ) -> Result< Signer, String> {
34
let author = delegated_identity ( app, account_id) .await.ok_or_else ( | | {
b43a9ab0 Andy McCall 1 month ago ↶
35
" You have not delegated to this hub's key yet. From a checkout with your identity: download the hub key from /.well-known/oxyhub-key, run `oxy identity delegate add oxyhub-key.identity --scope changes,refs`, then link the new identity under Settings › Keys (`oxy identity export`)." .to_string ( )
14530604 Andy McCall 1 month ago ↶
36
} ) ? ;
37
let ( hub_key, hub_secret) = crate :: keys:: load_secret( & app.config.keys_dir ( ) ) .map_err ( | e | format! ( " the hub key is unavailable: {e} " ) ) ? ;
38
Ok ( Signer { author, hub_key, hub_secret, hub_identity: app.hub_key.identity.clone ( ) } )
39
}
40
41
# [ derive ( Template ) ]
42
# [ template ( path = " edit.html" ) ]
43
pub struct EditPage {
44
shell : Shell,
45
repo : RepoShell,
46
r : String,
47
path : String,
48
content : String,
49
new_file : bool ,
50
is_bookmark : bool ,
51
conflicted : bool ,
52
delegation : Option< String> ,
53
error : Option< String> ,
54
message : String,
55
}
56
57
# [ derive ( Deserialize ) ]
58
pub struct NewQuery {
59
#[serde(default)]
60
dir : String,
61
}
62
63
64
# [ allow ( clippy::too_many_arguments ) ]
65
async fn page (
66
app : & Shared,
67
viewer : & Viewer,
68
owner : & str ,
69
name : & str ,
70
r : & str ,
71
path : Option< & str > ,
72
dir : & str ,
73
error : Option< String> ,
74
content : Option< String> ,
75
) -> Result< Page< EditPage> , Error> {
76
let shell = Shell:: new( app, & format! ( " /{owner} /{name} /edit/{r} " ) , viewer) ;
77
let me = require ( viewer, & shell) ? ;
78
let ( repo, row, role) = lookup_role ( app, & shell, viewer, owner, name, Access:: Write) .await? ;
79
let limit = app.config.limits.render_limit.0 ;
80
let spec = r.to_string ( ) ;
81
let p = path.map ( String :: from) ;
82
let ( is_bookmark, file) = app
83
.repos
84
.read ( & repo, move | repo, cache| {
85
let target = view:: resolve( repo, cache, & spec) ? ;
86
let is_bookmark = target.kind = = view:: RefKind:: Bookmark;
87
let file = match & p {
88
Some ( p) => Some ( view:: file( repo, cache, target, p, limit) ? ) ,
89
None => None ,
90
} ;
91
Ok ( ( is_bookmark, file) )
92
} )
93
.await
94
.map_err ( | e | Error:: from_repo( shell.clone ( ) , e) ) ? ;
95
let ( existing, conflicted) = match & file {
96
Some ( f) => match & f.body {
97
FileBody:: Text( t) => ( t.clone ( ) , f.conflicted) ,
98
_ => return Err ( Error:: bad( shell, " Only text files within the render limit can be edited in the browser." ) ) ,
99
} ,
100
None => ( String :: new( ) , false ) ,
101
} ;
102
let delegation = signer_for ( app, me.account.id) .await.err ( ) ;
103
let new_file = path.is_none ( ) ;
104
let default_path = if dir.is_empty ( ) { String :: new( ) } else { format! ( " {} /" , dir.trim_matches ( ' /' ) ) } ;
105
Ok ( Page( EditPage {
106
shell,
107
repo: RepoShell:: new( app, & row, " code" ) .with_role ( role) ,
108
r: r.to_string ( ) ,
109
path: path.map ( String :: from) .unwrap_or ( default_path) ,
110
content: content.unwrap_or ( existing) ,
111
new_file,
112
is_bookmark,
113
conflicted,
114
delegation,
115
error,
116
message: String :: new( ) ,
117
} ) )
118
}
119
120
pub async fn edit_form (
121
State( app ) : State< Shared> ,
122
viewer : Viewer,
123
Path( (owner , name , r , path ) ) : Path< ( String, String, String, String) > ,
124
) -> Result< Page< EditPage> , Error> {
125
page ( & app, & viewer, & owner, & name, & r, Some ( & path) , " " , None , None ) .await
126
}
127
128
pub async fn new_form (
129
State( app ) : State< Shared> ,
130
viewer : Viewer,
131
Path( (owner , name , r ) ) : Path< ( String, String, String) > ,
132
Query( q) : Query< NewQuery> ,
133
) -> Result< Page< EditPage> , Error> {
134
page ( & app, & viewer, & owner, & name, & r, None , & q.dir, None , None ) .await
135
}
136
137
# [ derive ( Deserialize ) ]
138
pub struct SaveForm {
139
#[serde(rename = "_csrf")]
140
csrf_token : String,
141
path : String,
142
#[serde(default)]
143
original : String,
144
#[serde(default)]
145
content : String,
146
message : String,
147
148
#[serde(default = "commit_mode")]
149
mode : String,
150
#[serde(default)]
151
new_bookmark : String,
152
#[serde(default)]
153
delete : Option< String> ,
154
}
155
156
fn commit_mode ( ) -> String {
157
" commit" .into ( )
158
}
159
160
pub async fn save (
161
State( app ) : State< Shared> ,
162
viewer : Viewer,
163
Path( (owner , name , r ) ) : Path< ( String, String, String) > ,
164
Form( f) : Form< SaveForm> ,
165
) -> Result< Response, Error> {
166
let shell = Shell:: new( & app, & format! ( " /{owner} /{name} /edit/{r} " ) , & viewer) ;
167
let me = require ( & viewer, & shell) ? ;
168
csrf ( & viewer, & f.csrf_token, & shell) ? ;
169
let ( repo, row, _ ) = lookup_role ( & app, & shell, & viewer, & owner, & name, Access:: Write) .await? ;
170
let again = | msg : String| {
171
let ( app, viewer, owner, name, r) = ( app.clone ( ) , viewer.clone ( ) , owner.clone ( ) , name.clone ( ) , r.clone ( ) ) ;
172
let ( orig, content) = ( f.original.clone ( ) , f.content.clone ( ) ) ;
173
async move {
174
page ( & app, & viewer, & owner, & name, & r, if orig.is_empty ( ) { None } else { Some ( orig.as_str ( ) ) } , " " , Some ( msg) , Some ( content) )
175
.await
176
.map ( IntoResponse:: into_response)
177
}
178
} ;
179
let signer = match signer_for ( & app, me.account.id) .await {
180
Ok ( s) => s,
181
Err ( msg) => return again ( msg) .await,
182
} ;
183
let path = f.path.trim ( ) .trim_matches ( ' /' ) .to_string ( ) ;
184
if path.is_empty ( ) {
185
return again ( " Name the file." .into ( ) ) .await;
186
}
187
let message = f.message.trim ( ) .to_string ( ) ;
188
if message.is_empty ( ) {
189
return again ( " Describe the change." .into ( ) ) .await;
190
}
191
192
let content = f.content.replace ( " \r \n " , " \n " ) ;
193
let mut changes = vec! [ ] ;
194
if f.delete.is_some ( ) {
195
changes.push ( FileEdit {
196
path: if f.original.is_empty ( ) { path.clone ( ) } else { f.original.trim_matches ( ' /' ) .to_string ( ) } ,
197
content: None ,
198
} ) ;
199
} else {
200
if ! f.original.is_empty ( ) & & f.original.trim_matches ( ' /' ) ! = path {
201
202
changes.push ( FileEdit { path: f.original.trim_matches ( ' /' ) .to_string ( ) , content: None } ) ;
203
}
204
changes.push ( FileEdit { path: path.clone ( ) , content: Some ( content.into_bytes ( ) ) } ) ;
205
}
206
let target = if f.mode = = " propose" {
207
let b = f.new_bookmark.trim ( ) ;
208
let b = if b.is_empty ( ) { format! ( " {} -patch-{} " , me.account.username, crate :: auth:: random_hex( 3 ) ) } else { b.to_string ( ) } ;
209
if ! crate :: repos:: names:: valid_segment( & b) {
210
return again ( " A bookmark name is letters, digits, dots, dashes and underscores." .into ( ) ) .await;
211
}
212
b
213
} else {
214
r.clone ( )
215
} ;
216
let done = match edits:: commit( & app.repos, & repo, r.clone ( ) , target.clone ( ) , changes, message.clone ( ) , signer) .await {
217
Ok ( d) => d,
218
Err ( e) => return again ( e.to_string ( ) ) .await,
219
} ;
c5f3bde4 Andy McCall 1 month ago ↶
220
after_web_commit ( & app, & me.account, & row, & owner, & name, & done, & message) .await;
221
if f.mode = = " propose" {
222
match super :: proposals:: open_proposal( & app, & repo, & row, & me.account, & message, " " , & r, & done.bookmark, false ) .await {
223
Ok ( p) => return Ok ( Redirect:: to( & format! ( " /{owner} /{name} /proposal/{} " , p.number) ) .into_response ( ) ) ,
224
Err ( msg) => {
225
return Ok ( Redirect:: to( & format! ( " /{owner} /{name} /tree/{} ?notice={} " , done.bookmark, super :: settings:: urlencode( & msg) ) )
226
.into_response ( ) )
227
}
228
}
229
}
230
let to = if f.delete.is_some ( ) {
231
format! ( " /{owner} /{name} /tree/{} " , done.bookmark)
232
} else {
233
format! ( " /{owner} /{name} /file/{} /{} " , done.bookmark, path)
234
} ;
235
Ok ( Redirect:: to( & to) .into_response ( ) )
236
}
237
238
239
240
241
pub ( super ) async fn after_web_commit (
242
app : & Shared,
243
account : & accounts:: Account,
244
row : & db:: repos:: RepoRow,
245
owner : & str ,
246
name : & str ,
247
done : & edits:: Committed,
248
message : & str ,
249
) {
14530604 Andy McCall 1 month ago ↶
250
let outcome = db:: repos:: PushOutcome {
251
reference: done.bookmark.clone ( ) ,
252
ok: true ,
253
reason: " web edit" .into ( ) ,
254
old_state: done.old_state.clone ( ) ,
255
new_state: done.new_state.clone ( ) ,
256
heads: done.heads.len ( ) as i64 ,
257
changes: 1 ,
258
} ;
c5f3bde4 Andy McCall 1 month ago ↶
259
db:: repos:: record_push( & app.db, row.id, Some ( ( account.id, & account.username) ) , & [ outcome] ) .await.ok ( ) ;
260
crate :: hooks:: emit( app, row, " push" , & account.username, serde_json:: json! ( { " updates" : [ { " reference" : done.bookmark, " old_state" : done.old_state, " new_state" : done.new_state, " heads" : done.heads.len ( ) , " changes" : 1 } ] , " via" : " web" } ) ) .await;
261
crate :: flow:: on_push( app, row, & done.bookmark, & done.new_state, & done.heads, & account.username) .await;
14530604 Andy McCall 1 month ago ↶
262
if done.bookmark = = row.default_bookmark {
c5f3bde4 Andy McCall 1 month ago ↶
263
super :: issues:: close_from_messages( app, row, & [ ( done.change.to_hex ( ) , message.to_string ( ) ) ] ) .await;
264
crate :: mirror:: refresh( app, row) .await;
14530604 Andy McCall 1 month ago ↶
265
}
266
accounts:: audit(
267
& app.db,
c5f3bde4 Andy McCall 1 month ago ↶
268
Some ( account) ,
14530604 Andy McCall 1 month ago ↶
269
" web.edit" ,
270
& format! ( " {owner} /{name} " ) ,
271
& format! ( " {} {} " , done.bookmark, done.change.short ( 12 ) ) ,
272
)
273
.await
274
.ok ( ) ;
275
}