gitlab/api/projects/repository/commits/
create.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use std::str;
8
9use derive_builder::Builder;
10use log::warn;
11
12use crate::api::common::NameOrId;
13use crate::api::endpoint_prelude::*;
14use crate::api::projects::repository::files::Encoding;
15use crate::api::ParamValue;
16
17/// All actions that can be performed in a commit
18#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum CommitActionType {
21    /// Create a file.
22    #[default]
23    Create,
24    /// delete a file.
25    Delete,
26    /// Move a file.
27    Move,
28    /// Change the contents of a file.
29    Update,
30    /// Change the execution permission on a file.
31    Chmod,
32}
33
34impl CommitActionType {
35    /// The string representation of the visibility level.
36    pub fn as_str(self) -> &'static str {
37        match self {
38            CommitActionType::Create => "create",
39            CommitActionType::Delete => "delete",
40            CommitActionType::Move => "move",
41            CommitActionType::Update => "update",
42            CommitActionType::Chmod => "chmod",
43        }
44    }
45
46    fn validate(self, builder: &CommitActionBuilder) -> Result<(), CommitActionValidationError> {
47        if builder.content.is_some() {
48            Ok(())
49        } else {
50            match self {
51                Self::Create => Err(CommitActionValidationError::ContentRequiredByCreate),
52                Self::Update => Err(CommitActionValidationError::ContentRequiredByUpdate),
53                _ => Ok(()),
54            }
55        }
56    }
57}
58
59impl ParamValue<'static> for CommitActionType {
60    fn as_value(&self) -> Cow<'static, str> {
61        self.as_str().into()
62    }
63}
64
65const SAFE_ENCODING: Encoding = Encoding::Base64;
66
67/// Action that is executed for a commit.
68#[derive(Debug, Clone, Builder)]
69#[builder(setter(strip_option), build_fn(validate = "Self::validate"))]
70pub struct CommitAction<'a> {
71    /// The action to perform.
72    action: CommitActionType,
73    /// The path to the file.
74    #[builder(setter(into))]
75    file_path: Cow<'a, str>,
76    /// Original full path to the file being moved.
77    ///
78    /// Only considered for `Move` action.
79    #[builder(setter(into), default)]
80    previous_path: Option<Cow<'a, str>>,
81    /// File content, required for `Create` and `Update`.
82    ///
83    /// Move actions that do not specify content preserve the existing file content and any other
84    /// value of content overwrites the file content.
85    ///
86    /// This will automatically be encoded according to the `encoding` parameter.
87    #[builder(setter(into), default)]
88    content: Option<Cow<'a, [u8]>>,
89    /// The encoding to use for the content, text is default.
90    ///
91    /// Note that if `text` is requested and `content` contains non-UTF-8 content, a warning will
92    /// be generated and a binary-safe encoding used instead.
93    #[builder(default)]
94    encoding: Option<Encoding>,
95    /// Last known file commit ID.
96    ///
97    /// Only considered in `Update`, `Move`, and `Delete` actions.
98    #[builder(setter(into), default)]
99    last_commit_id: Option<Cow<'a, str>>,
100    /// When true/false enables/disables the execute flag on the file.
101    ///
102    /// Only considered for the `Chmod` action.
103    #[builder(default)]
104    execute_filemode: Option<bool>,
105}
106
107impl<'a> CommitAction<'a> {
108    /// Create a builder for the endpoint.
109    pub fn builder() -> CommitActionBuilder<'a> {
110        CommitActionBuilder::default()
111    }
112
113    // XXX(rust-1.66): `Option<(T, U)>::unzip` is added in 1.66, but the MSRV is 1.63 because
114    // non-`api`-using clients can still use it.
115    #[allow(clippy::incompatible_msrv)]
116    fn add_query<'b>(&'b self, params: &mut FormParams<'b>) {
117        let (actual_encoding, actual_content) = self
118            .content
119            .as_ref()
120            .map(|content| {
121                let str_content = str::from_utf8(content);
122                let needs_encoding = str_content.is_err();
123                let encoding = self.encoding.unwrap_or_default();
124                let actual_encoding = if needs_encoding && !encoding.is_binary_safe() {
125                    warn!(
126                        "forcing the encoding to {} due to utf-8 unsafe content",
127                        SAFE_ENCODING.as_str(),
128                    );
129                    SAFE_ENCODING
130                } else {
131                    encoding
132                };
133
134                (
135                    actual_encoding,
136                    actual_encoding.encode(str_content.ok(), content),
137                )
138            })
139            .unzip();
140
141        params
142            .push("actions[][action]", self.action.as_value())
143            .push("actions[][file_path]", self.file_path.as_value())
144            .push_opt("actions[][previous_path]", self.previous_path.as_ref())
145            .push_opt("actions[][content]", actual_content)
146            .push_opt("actions[][encoding]", actual_encoding.or(self.encoding))
147            .push_opt("actions[][last_commit_id]", self.last_commit_id.as_ref())
148            .push_opt("actions[][execute_filemode]", self.execute_filemode);
149    }
150}
151
152static CONTENT_REQUIRED_CREATE: &str = "content is required for create.";
153static CONTENT_REQUIRED_UPDATE: &str = "content is required for update.";
154
155#[non_exhaustive]
156enum CommitActionValidationError {
157    ContentRequiredByCreate,
158    ContentRequiredByUpdate,
159}
160
161impl From<CommitActionValidationError> for CommitActionBuilderError {
162    fn from(validation_error: CommitActionValidationError) -> Self {
163        match validation_error {
164            CommitActionValidationError::ContentRequiredByCreate => {
165                CommitActionBuilderError::ValidationError(CONTENT_REQUIRED_CREATE.into())
166            },
167            CommitActionValidationError::ContentRequiredByUpdate => {
168                CommitActionBuilderError::ValidationError(CONTENT_REQUIRED_UPDATE.into())
169            },
170        }
171    }
172}
173
174impl CommitActionBuilder<'_> {
175    fn validate(&self) -> Result<(), CommitActionValidationError> {
176        if let Some(ref action) = &self.action {
177            action.validate(self)?;
178        }
179
180        Ok(())
181    }
182}
183
184/// Create a new commit for a project on a branch.
185#[derive(Debug, Builder, Clone)]
186#[builder(setter(strip_option), build_fn(validate = "Self::validate"))]
187pub struct CreateCommit<'a> {
188    /// The ID or URL-encoded path of the project
189    #[builder(setter(into))]
190    project: NameOrId<'a>,
191    /// Name of the branch to commit into.
192    ///
193    /// To create a new branch, also provide either `start_branch` or `start_sha`, and (optionally)
194    /// `start_project`.
195    #[builder(setter(into))]
196    branch: Cow<'a, str>,
197    /// Commit message.
198    #[builder(setter(into))]
199    commit_message: Cow<'a, str>,
200    /// Name of the branch to start the new branch from.
201    #[builder(setter(into), default)]
202    start_branch: Option<Cow<'a, str>>,
203    /// SHA of the commit to start the new branch from.
204    #[builder(setter(into), default)]
205    start_sha: Option<Cow<'a, str>>,
206    /// The project path or ID of the project to start the new branch from.
207    ///
208    /// Defaults to the value of `project`.
209    #[builder(setter(into), default)]
210    start_project: Option<NameOrId<'a>>,
211    /// An array of action hashes to commit as a batch.
212    #[builder(setter(name = "_actions"), private)]
213    actions: Vec<CommitAction<'a>>,
214    /// Specify the commit author's email address.
215    #[builder(setter(into), default)]
216    author_email: Option<Cow<'a, str>>,
217    /// Specify the commit author's name.
218    #[builder(setter(into), default)]
219    author_name: Option<Cow<'a, str>>,
220    /// Include commit stats.
221    ///
222    /// Default to `true`.
223    #[builder(default)]
224    stats: Option<bool>,
225    /// When `true`, overwrites the target branch with a new commit based on the `start_branch` or
226    /// `start_sha`.
227    #[builder(default)]
228    force: Option<bool>,
229}
230
231impl<'a> CreateCommit<'a> {
232    /// Create a builder for the endpoint.
233    pub fn builder() -> CreateCommitBuilder<'a> {
234        CreateCommitBuilder::default()
235    }
236}
237
238#[non_exhaustive]
239enum CreateCommitValidationError {
240    AtMostOneStartItem,
241}
242
243static AT_MOST_ONE_START_ITEM: &str = "Specify either start_sha or start_branch, not both";
244
245impl From<CreateCommitValidationError> for CreateCommitBuilderError {
246    fn from(validation_error: CreateCommitValidationError) -> Self {
247        match validation_error {
248            CreateCommitValidationError::AtMostOneStartItem => {
249                CreateCommitBuilderError::ValidationError(AT_MOST_ONE_START_ITEM.into())
250            },
251        }
252    }
253}
254
255impl<'a> CreateCommitBuilder<'a> {
256    /// Add an action.
257    pub fn action(&mut self, action: CommitAction<'a>) -> &mut Self {
258        self.actions.get_or_insert(Vec::new()).push(action);
259        self
260    }
261
262    /// Add multiple actions.
263    pub fn actions<I>(&mut self, iter: I) -> &mut Self
264    where
265        I: IntoIterator<Item = CommitAction<'a>>,
266    {
267        self.actions.get_or_insert(Vec::new()).extend(iter);
268        self
269    }
270
271    fn validate(&self) -> Result<(), CreateCommitValidationError> {
272        let have_start_branch = self
273            .start_branch
274            .as_ref()
275            .map(Option::is_some)
276            .unwrap_or(false);
277        let have_start_sha = self
278            .start_sha
279            .as_ref()
280            .map(Option::is_some)
281            .unwrap_or(false);
282        if have_start_branch && have_start_sha {
283            return Err(CreateCommitValidationError::AtMostOneStartItem);
284        }
285
286        Ok(())
287    }
288}
289
290impl Endpoint for CreateCommit<'_> {
291    fn method(&self) -> Method {
292        Method::POST
293    }
294
295    fn endpoint(&self) -> Cow<'static, str> {
296        format!("projects/{}/repository/commits", self.project).into()
297    }
298
299    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
300        let mut params = FormParams::default();
301
302        params
303            .push("branch", self.branch.as_ref())
304            .push("commit_message", self.commit_message.as_ref())
305            .push_opt("start_branch", self.start_branch.as_ref())
306            .push_opt("start_sha", self.start_sha.as_ref())
307            .push_opt("start_project", self.start_project.as_ref())
308            .push_opt("author_email", self.author_email.as_ref())
309            .push_opt("author_name", self.author_name.as_ref())
310            .push_opt("stats", self.stats)
311            .push_opt("force", self.force);
312
313        for action in self.actions.iter() {
314            action.add_query(&mut params);
315        }
316
317        params.into_body()
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use crate::{
324        api::{self, Query},
325        test::client::{ExpectedUrl, SingleTestClient},
326    };
327
328    use super::*;
329
330    #[test]
331    fn action_action_type_required() {
332        let err = CommitAction::builder()
333            .file_path("path/to/file")
334            .build()
335            .unwrap_err();
336
337        crate::test::assert_missing_field!(err, CommitActionBuilderError, "action");
338    }
339
340    #[test]
341    fn action_file_path_required() {
342        let err = CommitAction::builder()
343            .action(CommitActionType::Create)
344            .content(&b"content"[..])
345            .build()
346            .unwrap_err();
347
348        crate::test::assert_missing_field!(err, CommitActionBuilderError, "file_path");
349    }
350
351    #[test]
352    fn action_content_required_for_create() {
353        let action = CommitAction::builder()
354            .action(CommitActionType::Create)
355            .file_path("path/to/file")
356            .build();
357
358        if let Err(msg) = action {
359            assert_eq!(msg.to_string(), CONTENT_REQUIRED_CREATE)
360        } else {
361            panic!("unexpected error (expected to be missing content)")
362        }
363    }
364
365    #[test]
366    fn action_content_required_for_update() {
367        let action = CommitAction::builder()
368            .action(CommitActionType::Update)
369            .file_path("path/to/file")
370            .build();
371
372        if let Err(msg) = action {
373            assert_eq!(msg.to_string(), CONTENT_REQUIRED_UPDATE)
374        } else {
375            panic!("unexpected error (expected to be missing content)")
376        }
377    }
378
379    #[test]
380    fn project_is_required() {
381        let err = CreateCommit::builder()
382            .branch("source")
383            .commit_message("msg")
384            .action(
385                CommitAction::builder()
386                    .action(CommitActionType::Create)
387                    .file_path("foo/bar")
388                    .content(&b"content"[..])
389                    .build()
390                    .unwrap(),
391            )
392            .build()
393            .unwrap_err();
394        crate::test::assert_missing_field!(err, CreateCommitBuilderError, "project");
395    }
396
397    #[test]
398    fn branch_is_required() {
399        let err = CreateCommit::builder()
400            .project(1)
401            .commit_message("msg")
402            .action(
403                CommitAction::builder()
404                    .action(CommitActionType::Create)
405                    .file_path("foo/bar")
406                    .content(&b"content"[..])
407                    .build()
408                    .unwrap(),
409            )
410            .build()
411            .unwrap_err();
412        crate::test::assert_missing_field!(err, CreateCommitBuilderError, "branch");
413    }
414
415    #[test]
416    fn commit_message_is_required() {
417        let err = CreateCommit::builder()
418            .project(1)
419            .branch("source")
420            .action(
421                CommitAction::builder()
422                    .action(CommitActionType::Create)
423                    .file_path("foo/bar")
424                    .content(&b"content"[..])
425                    .build()
426                    .unwrap(),
427            )
428            .build()
429            .unwrap_err();
430        crate::test::assert_missing_field!(err, CreateCommitBuilderError, "commit_message");
431    }
432
433    #[test]
434    fn actions_required() {
435        let err = CreateCommit::builder()
436            .project(1)
437            .branch("source")
438            .commit_message("msg")
439            .build()
440            .unwrap_err();
441        crate::test::assert_missing_field!(err, CreateCommitBuilderError, "actions");
442    }
443
444    #[test]
445    fn project_branch_msg_and_action_sufficent() {
446        CreateCommit::builder()
447            .project(1)
448            .branch("source")
449            .commit_message("msg")
450            .action(
451                CommitAction::builder()
452                    .action(CommitActionType::Create)
453                    .file_path("foo/bar")
454                    .content(&b"content"[..])
455                    .build()
456                    .unwrap(),
457            )
458            .build()
459            .unwrap();
460    }
461
462    #[test]
463    fn endpoint() {
464        let endpoint = ExpectedUrl::builder()
465            .method(Method::POST)
466            .endpoint("projects/simple%2Fproject/repository/commits")
467            .content_type("application/x-www-form-urlencoded")
468            .body_str(concat!(
469                "branch=master",
470                "&commit_message=message",
471                "&actions%5B%5D%5Baction%5D=create",
472                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar",
473                "&actions%5B%5D%5Bcontent%5D=content",
474                "&actions%5B%5D%5Bencoding%5D=text",
475                "&actions%5B%5D%5Baction%5D=delete",
476                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar2",
477                "&actions%5B%5D%5Baction%5D=move",
478                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar3",
479                "&actions%5B%5D%5Bprevious_path%5D=foo%2Fbar4",
480                "&actions%5B%5D%5Bcontent%5D=content",
481                "&actions%5B%5D%5Bencoding%5D=text",
482                "&actions%5B%5D%5Baction%5D=update",
483                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar5",
484                "&actions%5B%5D%5Bcontent%5D=content",
485                "&actions%5B%5D%5Bencoding%5D=text",
486                "&actions%5B%5D%5Baction%5D=chmod",
487                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar5",
488                "&actions%5B%5D%5Bexecute_filemode%5D=true",
489            ))
490            .build()
491            .unwrap();
492        let client = SingleTestClient::new_raw(endpoint, "");
493
494        let endpoint = CreateCommit::builder()
495            .project("simple/project")
496            .branch("master")
497            .commit_message("message")
498            .actions([
499                CommitAction::builder()
500                    .action(CommitActionType::Create)
501                    .file_path("foo/bar")
502                    .content(&b"content"[..])
503                    .build()
504                    .unwrap(),
505                CommitAction::builder()
506                    .action(CommitActionType::Delete)
507                    .file_path("foo/bar2")
508                    .build()
509                    .unwrap(),
510                CommitAction::builder()
511                    .action(CommitActionType::Move)
512                    .file_path("foo/bar3")
513                    .previous_path("foo/bar4")
514                    .content(&b"content"[..])
515                    .build()
516                    .unwrap(),
517                CommitAction::builder()
518                    .action(CommitActionType::Update)
519                    .file_path("foo/bar5")
520                    .content(&b"content"[..])
521                    .build()
522                    .unwrap(),
523                CommitAction::builder()
524                    .action(CommitActionType::Chmod)
525                    .file_path("foo/bar5")
526                    .execute_filemode(true)
527                    .build()
528                    .unwrap(),
529            ])
530            .build()
531            .unwrap();
532        api::ignore(endpoint).query(&client).unwrap();
533    }
534
535    #[test]
536    fn endpoint_start_branch() {
537        let endpoint = ExpectedUrl::builder()
538            .method(Method::POST)
539            .endpoint("projects/simple%2Fproject/repository/commits")
540            .content_type("application/x-www-form-urlencoded")
541            .body_str(concat!(
542                "branch=master",
543                "&commit_message=message",
544                "&start_branch=start",
545                "&actions%5B%5D%5Baction%5D=create",
546                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar",
547                "&actions%5B%5D%5Bcontent%5D=content",
548                "&actions%5B%5D%5Bencoding%5D=text",
549            ))
550            .build()
551            .unwrap();
552        let client = SingleTestClient::new_raw(endpoint, "");
553
554        let endpoint = CreateCommit::builder()
555            .project("simple/project")
556            .branch("master")
557            .commit_message("message")
558            .action(
559                CommitAction::builder()
560                    .action(CommitActionType::Create)
561                    .file_path("foo/bar")
562                    .content(&b"content"[..])
563                    .build()
564                    .unwrap(),
565            )
566            .start_branch("start")
567            .build()
568            .unwrap();
569        api::ignore(endpoint).query(&client).unwrap();
570    }
571
572    #[test]
573    fn endpoint_start_sha() {
574        let endpoint = ExpectedUrl::builder()
575            .method(Method::POST)
576            .endpoint("projects/simple%2Fproject/repository/commits")
577            .content_type("application/x-www-form-urlencoded")
578            .body_str(concat!(
579                "branch=new-branch",
580                "&commit_message=message",
581                "&start_sha=40b35d15a129e75500bbf3d5db779b6f29376d1a",
582                "&actions%5B%5D%5Baction%5D=create",
583                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar",
584                "&actions%5B%5D%5Bcontent%5D=content",
585                "&actions%5B%5D%5Bencoding%5D=text",
586            ))
587            .build()
588            .unwrap();
589        let client = SingleTestClient::new_raw(endpoint, "");
590
591        let endpoint = CreateCommit::builder()
592            .project("simple/project")
593            .branch("new-branch")
594            .start_sha("40b35d15a129e75500bbf3d5db779b6f29376d1a")
595            .commit_message("message")
596            .action(
597                CommitAction::builder()
598                    .action(CommitActionType::Create)
599                    .file_path("foo/bar")
600                    .content(&b"content"[..])
601                    .build()
602                    .unwrap(),
603            )
604            .build()
605            .unwrap();
606        api::ignore(endpoint).query(&client).unwrap();
607    }
608
609    #[test]
610    fn endpoint_start_branch_and_start_sha() {
611        let err = CreateCommit::builder()
612            .project("simple/project")
613            .branch("master")
614            .commit_message("message")
615            .action(
616                CommitAction::builder()
617                    .action(CommitActionType::Create)
618                    .file_path("foo/bar")
619                    .content(&b"content"[..])
620                    .build()
621                    .unwrap(),
622            )
623            .start_branch("start")
624            .start_sha("start")
625            .build()
626            .unwrap_err();
627
628        assert_eq!(err.to_string(), AT_MOST_ONE_START_ITEM);
629    }
630
631    #[test]
632    fn endpoint_start_project() {
633        let endpoint = ExpectedUrl::builder()
634            .method(Method::POST)
635            .endpoint("projects/simple%2Fproject/repository/commits")
636            .content_type("application/x-www-form-urlencoded")
637            .body_str(concat!(
638                "branch=new-branch",
639                "&commit_message=message",
640                "&start_project=400",
641                "&actions%5B%5D%5Baction%5D=create",
642                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar",
643                "&actions%5B%5D%5Bcontent%5D=content",
644                "&actions%5B%5D%5Bencoding%5D=text",
645            ))
646            .build()
647            .unwrap();
648        let client = SingleTestClient::new_raw(endpoint, "");
649
650        let endpoint = CreateCommit::builder()
651            .project("simple/project")
652            .branch("new-branch")
653            .start_project(400)
654            .commit_message("message")
655            .action(
656                CommitAction::builder()
657                    .action(CommitActionType::Create)
658                    .file_path("foo/bar")
659                    .content(&b"content"[..])
660                    .build()
661                    .unwrap(),
662            )
663            .build()
664            .unwrap();
665        api::ignore(endpoint).query(&client).unwrap();
666    }
667
668    #[test]
669    fn endpoint_author_email() {
670        let endpoint = ExpectedUrl::builder()
671            .method(Method::POST)
672            .endpoint("projects/simple%2Fproject/repository/commits")
673            .content_type("application/x-www-form-urlencoded")
674            .body_str(concat!(
675                "branch=master",
676                "&commit_message=message",
677                "&author_email=me%40mail.com",
678                "&actions%5B%5D%5Baction%5D=create",
679                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar",
680                "&actions%5B%5D%5Bcontent%5D=content",
681                "&actions%5B%5D%5Bencoding%5D=text",
682            ))
683            .build()
684            .unwrap();
685        let client = SingleTestClient::new_raw(endpoint, "");
686
687        let endpoint = CreateCommit::builder()
688            .project("simple/project")
689            .branch("master")
690            .author_email("me@mail.com")
691            .commit_message("message")
692            .action(
693                CommitAction::builder()
694                    .action(CommitActionType::Create)
695                    .file_path("foo/bar")
696                    .content(&b"content"[..])
697                    .build()
698                    .unwrap(),
699            )
700            .build()
701            .unwrap();
702        api::ignore(endpoint).query(&client).unwrap();
703    }
704
705    #[test]
706    fn endpoint_author_name() {
707        let endpoint = ExpectedUrl::builder()
708            .method(Method::POST)
709            .endpoint("projects/simple%2Fproject/repository/commits")
710            .content_type("application/x-www-form-urlencoded")
711            .body_str(concat!(
712                "branch=master",
713                "&commit_message=message",
714                "&author_name=me",
715                "&actions%5B%5D%5Baction%5D=create",
716                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar",
717                "&actions%5B%5D%5Bcontent%5D=content",
718                "&actions%5B%5D%5Bencoding%5D=text",
719            ))
720            .build()
721            .unwrap();
722        let client = SingleTestClient::new_raw(endpoint, "");
723
724        let endpoint = CreateCommit::builder()
725            .project("simple/project")
726            .branch("master")
727            .author_name("me")
728            .commit_message("message")
729            .action(
730                CommitAction::builder()
731                    .action(CommitActionType::Create)
732                    .file_path("foo/bar")
733                    .content(&b"content"[..])
734                    .build()
735                    .unwrap(),
736            )
737            .build()
738            .unwrap();
739        api::ignore(endpoint).query(&client).unwrap();
740    }
741
742    #[test]
743    fn endpoint_stats() {
744        let endpoint = ExpectedUrl::builder()
745            .method(Method::POST)
746            .endpoint("projects/simple%2Fproject/repository/commits")
747            .content_type("application/x-www-form-urlencoded")
748            .body_str(concat!(
749                "branch=master",
750                "&commit_message=message",
751                "&stats=true",
752                "&actions%5B%5D%5Baction%5D=create",
753                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar",
754                "&actions%5B%5D%5Bcontent%5D=content",
755                "&actions%5B%5D%5Bencoding%5D=text",
756            ))
757            .build()
758            .unwrap();
759        let client = SingleTestClient::new_raw(endpoint, "");
760
761        let endpoint = CreateCommit::builder()
762            .project("simple/project")
763            .branch("master")
764            .stats(true)
765            .commit_message("message")
766            .action(
767                CommitAction::builder()
768                    .action(CommitActionType::Create)
769                    .file_path("foo/bar")
770                    .content(&b"content"[..])
771                    .build()
772                    .unwrap(),
773            )
774            .build()
775            .unwrap();
776        api::ignore(endpoint).query(&client).unwrap();
777    }
778
779    #[test]
780    fn endpoint_force() {
781        let endpoint = ExpectedUrl::builder()
782            .method(Method::POST)
783            .endpoint("projects/simple%2Fproject/repository/commits")
784            .content_type("application/x-www-form-urlencoded")
785            .body_str(concat!(
786                "branch=master",
787                "&commit_message=message",
788                "&force=true",
789                "&actions%5B%5D%5Baction%5D=create",
790                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar",
791                "&actions%5B%5D%5Bcontent%5D=content",
792                "&actions%5B%5D%5Bencoding%5D=text",
793            ))
794            .build()
795            .unwrap();
796        let client = SingleTestClient::new_raw(endpoint, "");
797
798        let endpoint = CreateCommit::builder()
799            .project("simple/project")
800            .branch("master")
801            .force(true)
802            .commit_message("message")
803            .action(
804                CommitAction::builder()
805                    .action(CommitActionType::Create)
806                    .file_path("foo/bar")
807                    .content(&b"content"[..])
808                    .build()
809                    .unwrap(),
810            )
811            .build()
812            .unwrap();
813        api::ignore(endpoint).query(&client).unwrap();
814    }
815
816    #[test]
817    fn endpoint_encoding() {
818        let endpoint = ExpectedUrl::builder()
819            .method(Method::POST)
820            .endpoint("projects/simple%2Fproject/repository/commits")
821            .content_type("application/x-www-form-urlencoded")
822            .body_str(concat!(
823                "branch=master",
824                "&commit_message=message",
825                "&actions%5B%5D%5Baction%5D=create",
826                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar",
827                "&actions%5B%5D%5Bcontent%5D=Y29udGVudA%3D%3D",
828                "&actions%5B%5D%5Bencoding%5D=base64",
829            ))
830            .build()
831            .unwrap();
832        let client = SingleTestClient::new_raw(endpoint, "");
833
834        let endpoint = CreateCommit::builder()
835            .project("simple/project")
836            .branch("master")
837            .commit_message("message")
838            .action(
839                CommitAction::builder()
840                    .action(CommitActionType::Create)
841                    .file_path("foo/bar")
842                    .encoding(Encoding::Base64)
843                    .content(&b"content"[..])
844                    .build()
845                    .unwrap(),
846            )
847            .build()
848            .unwrap();
849        api::ignore(endpoint).query(&client).unwrap();
850    }
851
852    #[test]
853    fn endpoint_encoding_fallback() {
854        let endpoint = ExpectedUrl::builder()
855            .method(Method::POST)
856            .endpoint("projects/simple%2Fproject/repository/commits")
857            .content_type("application/x-www-form-urlencoded")
858            .body_str(concat!(
859                "branch=master",
860                "&commit_message=message",
861                "&actions%5B%5D%5Baction%5D=create",
862                "&actions%5B%5D%5Bfile_path%5D=foo%2Fbar",
863                "&actions%5B%5D%5Bcontent%5D=Y29udGVudP8%3D",
864                "&actions%5B%5D%5Bencoding%5D=base64",
865            ))
866            .build()
867            .unwrap();
868        let client = SingleTestClient::new_raw(endpoint, "");
869
870        let endpoint = CreateCommit::builder()
871            .project("simple/project")
872            .branch("master")
873            .commit_message("message")
874            .action(
875                CommitAction::builder()
876                    .action(CommitActionType::Create)
877                    .file_path("foo/bar")
878                    .encoding(Encoding::Text)
879                    .content(&b"content\xff"[..])
880                    .build()
881                    .unwrap(),
882            )
883            .build()
884            .unwrap();
885        api::ignore(endpoint).query(&client).unwrap();
886    }
887}