gitlab/api/projects/repository/files/
create.rs1use std::str;
8
9use base64::Engine;
10use derive_builder::Builder;
11use log::warn;
12
13use crate::api::common::{self, NameOrId};
14use crate::api::endpoint_prelude::*;
15use crate::api::ParamValue;
16
17#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum Encoding {
21 #[default]
25 Text,
26 Base64,
30}
31
32impl Encoding {
33 pub(crate) fn is_binary_safe(self) -> bool {
34 match self {
35 Encoding::Text => false,
36 Encoding::Base64 => true,
37 }
38 }
39
40 pub(crate) fn encode<'a>(self, as_string: Option<&'a str>, content: &'a [u8]) -> Cow<'a, str> {
41 match self {
42 Encoding::Text => {
43 if let Some(string) = as_string {
44 string.into()
45 } else {
46 panic!("attempting to encode non-utf8 content using text!");
47 }
48 },
49 Encoding::Base64 => {
50 let engine = base64::engine::general_purpose::STANDARD;
51 engine.encode(content).into()
52 },
53 }
54 }
55
56 pub(crate) fn as_str(self) -> &'static str {
57 match self {
58 Encoding::Text => "text",
59 Encoding::Base64 => "base64",
60 }
61 }
62}
63
64impl ParamValue<'static> for Encoding {
65 fn as_value(&self) -> Cow<'static, str> {
66 self.as_str().into()
67 }
68}
69
70#[derive(Debug, Builder, Clone)]
72#[builder(setter(strip_option))]
73pub struct CreateFile<'a> {
74 #[builder(setter(into))]
76 project: NameOrId<'a>,
77 #[builder(setter(into))]
81 file_path: Cow<'a, str>,
82 #[builder(setter(into))]
84 branch: Cow<'a, str>,
85 #[builder(setter(into))]
89 content: Cow<'a, [u8]>,
90 #[builder(setter(into))]
92 commit_message: Cow<'a, str>,
93
94 #[builder(setter(into), default)]
96 start_branch: Option<Cow<'a, str>>,
97 #[builder(setter(into), default)]
102 encoding: Option<Encoding>,
103 #[builder(setter(into), default)]
105 author_email: Option<Cow<'a, str>>,
106 #[builder(setter(into), default)]
108 author_name: Option<Cow<'a, str>>,
109
110 #[builder(default)]
112 execute_filemode: Option<bool>,
113}
114
115impl<'a> CreateFile<'a> {
116 pub fn builder() -> CreateFileBuilder<'a> {
118 CreateFileBuilder::default()
119 }
120}
121
122const SAFE_ENCODING: Encoding = Encoding::Base64;
123
124impl Endpoint for CreateFile<'_> {
125 fn method(&self) -> Method {
126 Method::POST
127 }
128
129 fn endpoint(&self) -> Cow<'static, str> {
130 format!(
131 "projects/{}/repository/files/{}",
132 self.project,
133 common::path_escaped(&self.file_path),
134 )
135 .into()
136 }
137
138 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
139 let mut params = FormParams::default();
140
141 params
142 .push("branch", &self.branch)
143 .push("commit_message", &self.commit_message)
144 .push_opt("start_branch", self.start_branch.as_ref())
145 .push_opt("author_email", self.author_email.as_ref())
146 .push_opt("author_name", self.author_name.as_ref())
147 .push_opt("execute_filemode", self.execute_filemode);
148
149 let content = str::from_utf8(&self.content);
150 let needs_encoding = content.is_err();
151 let encoding = self.encoding.unwrap_or_default();
152 let actual_encoding = if needs_encoding && !encoding.is_binary_safe() {
153 warn!(
154 "forcing the encoding to {} due to utf-8 unsafe content",
155 SAFE_ENCODING.as_str(),
156 );
157 SAFE_ENCODING
158 } else {
159 encoding
160 };
161 params.push(
162 "content",
163 actual_encoding.encode(content.ok(), &self.content),
164 );
165 self.encoding
166 .map(|_| actual_encoding)
168 .or_else(|| {
170 if actual_encoding != Encoding::default() {
171 Some(actual_encoding)
172 } else {
173 None
174 }
175 })
176 .map(|value| params.push("encoding", value));
177
178 params.into_body()
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use http::Method;
185
186 use crate::api::projects::repository::files::{CreateFile, CreateFileBuilderError, Encoding};
187 use crate::api::{self, Query};
188 use crate::test::client::{ExpectedUrl, SingleTestClient};
189
190 #[test]
191 fn encoding_default() {
192 assert_eq!(Encoding::default(), Encoding::Text);
193 }
194
195 #[test]
196 fn encoding_is_binary_safe() {
197 let items = &[(Encoding::Text, false), (Encoding::Base64, true)];
198
199 for (i, s) in items {
200 assert_eq!(i.is_binary_safe(), *s);
201 }
202 }
203
204 #[test]
205 fn encoding_encode_text() {
206 let encoding = Encoding::Text;
207 assert_eq!(encoding.encode(Some("foo"), b"foo"), "foo");
208 }
209
210 #[test]
211 #[should_panic = "attempting to encode non-utf8 content using text!"]
212 fn encoding_encode_text_bad() {
213 let encoding = Encoding::Text;
214 encoding.encode(None, b"\xff");
215 }
216
217 #[test]
218 fn encoding_encode_base64() {
219 let encoding = Encoding::Base64;
220 assert_eq!(encoding.encode(None, b"foo"), "Zm9v");
221 }
222
223 #[test]
224 fn encoding_as_str() {
225 let items = &[(Encoding::Text, "text"), (Encoding::Base64, "base64")];
226
227 for (i, s) in items {
228 assert_eq!(i.as_str(), *s);
229 }
230 }
231
232 #[test]
233 fn all_parameters_are_needed() {
234 let err = CreateFile::builder().build().unwrap_err();
235 crate::test::assert_missing_field!(err, CreateFileBuilderError, "project");
236 }
237
238 #[test]
239 fn project_is_required() {
240 let err = CreateFile::builder()
241 .file_path("new/file")
242 .branch("master")
243 .commit_message("commit message")
244 .content(&b"contents"[..])
245 .build()
246 .unwrap_err();
247 crate::test::assert_missing_field!(err, CreateFileBuilderError, "project");
248 }
249
250 #[test]
251 fn file_path_is_required() {
252 let err = CreateFile::builder()
253 .project(1)
254 .branch("master")
255 .commit_message("commit message")
256 .content(&b"contents"[..])
257 .build()
258 .unwrap_err();
259 crate::test::assert_missing_field!(err, CreateFileBuilderError, "file_path");
260 }
261
262 #[test]
263 fn branch_is_required() {
264 let err = CreateFile::builder()
265 .project(1)
266 .file_path("new/file")
267 .commit_message("commit message")
268 .content(&b"contents"[..])
269 .build()
270 .unwrap_err();
271 crate::test::assert_missing_field!(err, CreateFileBuilderError, "branch");
272 }
273
274 #[test]
275 fn commit_message_is_required() {
276 let err = CreateFile::builder()
277 .project(1)
278 .file_path("new/file")
279 .branch("master")
280 .content(&b"contents"[..])
281 .build()
282 .unwrap_err();
283 crate::test::assert_missing_field!(err, CreateFileBuilderError, "commit_message");
284 }
285
286 #[test]
287 fn content_is_required() {
288 let err = CreateFile::builder()
289 .project(1)
290 .file_path("new/file")
291 .branch("master")
292 .commit_message("commit message")
293 .build()
294 .unwrap_err();
295 crate::test::assert_missing_field!(err, CreateFileBuilderError, "content");
296 }
297
298 #[test]
299 fn sufficient_parameters() {
300 CreateFile::builder()
301 .project(1)
302 .file_path("new/file")
303 .branch("master")
304 .commit_message("commit message")
305 .content(&b"contents"[..])
306 .build()
307 .unwrap();
308 }
309
310 #[test]
311 fn endpoint() {
312 let endpoint = ExpectedUrl::builder()
313 .method(Method::POST)
314 .endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
315 .content_type("application/x-www-form-urlencoded")
316 .body_str(concat!(
317 "branch=branch",
318 "&commit_message=commit+message",
319 "&content=file+contents",
320 ))
321 .build()
322 .unwrap();
323 let client = SingleTestClient::new_raw(endpoint, "");
324
325 let endpoint = CreateFile::builder()
326 .project("simple/project")
327 .file_path("path/to/file")
328 .branch("branch")
329 .content(&b"file contents"[..])
330 .commit_message("commit message")
331 .build()
332 .unwrap();
333 api::ignore(endpoint).query(&client).unwrap();
334 }
335
336 #[test]
337 fn endpoint_start_branch() {
338 let endpoint = ExpectedUrl::builder()
339 .method(Method::POST)
340 .endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
341 .content_type("application/x-www-form-urlencoded")
342 .body_str(concat!(
343 "branch=branch",
344 "&commit_message=commit+message",
345 "&start_branch=master",
346 "&content=file+contents",
347 ))
348 .build()
349 .unwrap();
350 let client = SingleTestClient::new_raw(endpoint, "");
351
352 let endpoint = CreateFile::builder()
353 .project("simple/project")
354 .file_path("path/to/file")
355 .branch("branch")
356 .content(&b"file contents"[..])
357 .commit_message("commit message")
358 .start_branch("master")
359 .build()
360 .unwrap();
361 api::ignore(endpoint).query(&client).unwrap();
362 }
363
364 #[test]
365 fn endpoint_encoding() {
366 let endpoint = ExpectedUrl::builder()
367 .method(Method::POST)
368 .endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
369 .content_type("application/x-www-form-urlencoded")
370 .body_str(concat!(
371 "branch=branch",
372 "&commit_message=commit+message",
373 "&content=ZmlsZSBjb250ZW50cw%3D%3D",
374 "&encoding=base64",
375 ))
376 .build()
377 .unwrap();
378 let client = SingleTestClient::new_raw(endpoint, "");
379
380 let endpoint = CreateFile::builder()
381 .project("simple/project")
382 .file_path("path/to/file")
383 .branch("branch")
384 .content(&b"file contents"[..])
385 .commit_message("commit message")
386 .encoding(Encoding::Base64)
387 .build()
388 .unwrap();
389 api::ignore(endpoint).query(&client).unwrap();
390 }
391
392 #[test]
393 fn endpoint_encoding_upgrade() {
394 let endpoint = ExpectedUrl::builder()
395 .method(Method::POST)
396 .endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
397 .content_type("application/x-www-form-urlencoded")
398 .body_str(concat!(
399 "branch=branch",
400 "&commit_message=commit+message",
401 "&content=%2Fw%3D%3D",
402 "&encoding=base64",
403 ))
404 .build()
405 .unwrap();
406 let client = SingleTestClient::new_raw(endpoint, "");
407
408 let endpoint = CreateFile::builder()
409 .project("simple/project")
410 .file_path("path/to/file")
411 .branch("branch")
412 .content(&b"\xff"[..])
413 .commit_message("commit message")
414 .build()
415 .unwrap();
416 api::ignore(endpoint).query(&client).unwrap();
417 }
418
419 #[test]
420 fn endpoint_author_email() {
421 let endpoint = ExpectedUrl::builder()
422 .method(Method::POST)
423 .endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
424 .content_type("application/x-www-form-urlencoded")
425 .body_str(concat!(
426 "branch=branch",
427 "&commit_message=commit+message",
428 "&author_email=author%40email.invalid",
429 "&content=file+contents",
430 ))
431 .build()
432 .unwrap();
433 let client = SingleTestClient::new_raw(endpoint, "");
434
435 let endpoint = CreateFile::builder()
436 .project("simple/project")
437 .file_path("path/to/file")
438 .branch("branch")
439 .content(&b"file contents"[..])
440 .commit_message("commit message")
441 .author_email("author@email.invalid")
442 .build()
443 .unwrap();
444 api::ignore(endpoint).query(&client).unwrap();
445 }
446
447 #[test]
448 fn endpoint_author_name() {
449 let endpoint = ExpectedUrl::builder()
450 .method(Method::POST)
451 .endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
452 .content_type("application/x-www-form-urlencoded")
453 .body_str(concat!(
454 "branch=branch",
455 "&commit_message=commit+message",
456 "&author_name=Arthur+Developer",
457 "&content=file+contents",
458 ))
459 .build()
460 .unwrap();
461 let client = SingleTestClient::new_raw(endpoint, "");
462
463 let endpoint = CreateFile::builder()
464 .project("simple/project")
465 .file_path("path/to/file")
466 .branch("branch")
467 .content(&b"file contents"[..])
468 .commit_message("commit message")
469 .author_name("Arthur Developer")
470 .build()
471 .unwrap();
472 api::ignore(endpoint).query(&client).unwrap();
473 }
474
475 #[test]
476 fn endpoint_execute_filemode() {
477 let endpoint = ExpectedUrl::builder()
478 .method(Method::POST)
479 .endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
480 .content_type("application/x-www-form-urlencoded")
481 .body_str(concat!(
482 "branch=branch",
483 "&commit_message=commit+message",
484 "&execute_filemode=false",
485 "&content=file+contents",
486 ))
487 .build()
488 .unwrap();
489 let client = SingleTestClient::new_raw(endpoint, "");
490
491 let endpoint = CreateFile::builder()
492 .project("simple/project")
493 .file_path("path/to/file")
494 .branch("branch")
495 .content(&b"file contents"[..])
496 .commit_message("commit message")
497 .execute_filemode(false)
498 .build()
499 .unwrap();
500 api::ignore(endpoint).query(&client).unwrap();
501 }
502}