Skip to main content

mesa_dev/low_level/
commits.rs

1//! Commits API with correct `anyOf` union handling for file operations.
2//!
3//! The `OpenAPI` code generator flattens `anyOf` request variants into a single
4//! struct with every field required, which produces an incorrect request body
5//! ([openapi-generator#9497](https://github.com/OpenAPITools/openapi-generator/issues/9497)).
6//!
7//! This module provides a hand-written [`CommitFile`] enum that correctly
8//! serializes each file operation variant (upsert, delete, LFS) and a
9//! [`create_commit`] wrapper that builds the request manually.
10//!
11//! The GET endpoints (list / get commit) in
12//! [`crate::low_level::apis::commits_api`] work correctly and are not wrapped here.
13
14use mesa_dev_oapi::apis::configuration::Configuration;
15use mesa_dev_oapi::apis::{Error, ResponseContent};
16use serde::{Deserialize, Serialize};
17
18/// Typed errors returned by [`create_commit`].
19pub use mesa_dev_oapi::apis::commits_api::PostByOrgByRepoCommitsError as CreateCommitError;
20
21/// Encoding for upsert file content.
22pub use mesa_dev_oapi::models::post_by_org_by_repo_commits_request_files_inner_any_of::Encoding;
23
24/// Commit author / committer identity.
25///
26/// This is a re-export of the generated type with a friendlier name.
27pub type CommitAuthor =
28    mesa_dev_oapi::models::GetByOrgByRepoCommits200ResponseCommitsInnerCommitter;
29
30/// A file operation within a commit.
31///
32/// Each variant maps to one of the `anyOf` shapes in the API spec.
33#[derive(Clone, Debug, PartialEq)]
34pub enum CommitFile {
35    /// Create or update a file.
36    Upsert {
37        /// Path relative to the repository root.
38        path: String,
39        /// File content (text or base64-encoded).
40        content: String,
41        /// Content encoding. Defaults to UTF-8 when `None`.
42        encoding: Option<Encoding>,
43    },
44    /// Delete a file.
45    Delete {
46        /// Path relative to the repository root.
47        path: String,
48    },
49    /// Add an LFS pointer.
50    Lfs {
51        /// Path relative to the repository root.
52        path: String,
53        /// LFS object ID (SHA-256).
54        oid: String,
55        /// LFS object size in bytes.
56        size: i64,
57    },
58}
59
60impl Serialize for CommitFile {
61    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
62        match self {
63            Self::Upsert {
64                path,
65                content,
66                encoding,
67            } => {
68                #[derive(Serialize)]
69                struct Wire<'a> {
70                    action: &'a str,
71                    path: &'a str,
72                    content: &'a str,
73                    #[serde(skip_serializing_if = "Option::is_none")]
74                    encoding: &'a Option<Encoding>,
75                }
76                Wire {
77                    action: "upsert",
78                    path,
79                    content,
80                    encoding,
81                }
82                .serialize(serializer)
83            }
84            Self::Delete { path } => {
85                #[derive(Serialize)]
86                struct Wire<'a> {
87                    action: &'a str,
88                    path: &'a str,
89                }
90                Wire {
91                    action: "delete",
92                    path,
93                }
94                .serialize(serializer)
95            }
96            Self::Lfs { path, oid, size } => {
97                #[derive(Serialize)]
98                struct Lfs<'a> {
99                    oid: &'a str,
100                    size: i64,
101                }
102                #[derive(Serialize)]
103                struct Wire<'a> {
104                    path: &'a str,
105                    lfs: Lfs<'a>,
106                }
107                Wire {
108                    path,
109                    lfs: Lfs { oid, size: *size },
110                }
111                .serialize(serializer)
112            }
113        }
114    }
115}
116
117/// Successful response from [`create_commit`].
118///
119/// The generated `PostByOrgByRepoCommits201Response` incorrectly marks all
120/// fields as `Option<String>`; the API always returns them.
121#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
122pub struct CommitResponse {
123    /// The SHA of the newly created commit.
124    pub sha: String,
125    /// The branch the commit was created on.
126    pub branch: String,
127    /// The commit message.
128    pub message: String,
129}
130
131/// Programmatically create a commit with file operations.
132///
133/// This wraps the generated `post_by_org_by_repo_commits` endpoint with
134/// correct `anyOf` request serialization. Each [`CommitFile`] variant is
135/// serialized into the shape the API expects.
136///
137/// # Errors
138///
139/// Returns an error if the request fails or the response cannot be
140/// deserialized.
141#[allow(clippy::too_many_arguments)]
142#[tracing::instrument(skip(configuration, author, files), fields(http.method = "POST", http.status_code), err(Debug))]
143pub async fn create_commit(
144    configuration: &Configuration,
145    org: &str,
146    repo: &str,
147    branch: &str,
148    message: &str,
149    author: &CommitAuthor,
150    files: &[CommitFile],
151    base_sha: Option<&str>,
152) -> Result<CommitResponse, Error<CreateCommitError>> {
153    let mut body = serde_json::json!({
154        "branch": branch,
155        "message": message,
156        "author": {
157            "name": author.name,
158            "email": author.email,
159        },
160        "files": files,
161    });
162
163    if let Some(sha) = base_sha {
164        body.as_object_mut()
165            .map(|m| m.insert("base_sha".to_string(), serde_json::json!(sha)));
166    }
167
168    let uri_str = format!(
169        "{}/{org}/{repo}/commits",
170        configuration.base_path,
171        org = mesa_dev_oapi::apis::urlencode(org),
172        repo = mesa_dev_oapi::apis::urlencode(repo),
173    );
174    let mut req_builder = configuration
175        .client
176        .request(reqwest::Method::POST, &uri_str);
177
178    if let Some(ref user_agent) = configuration.user_agent {
179        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
180    }
181    if let Some(ref token) = configuration.bearer_access_token {
182        req_builder = req_builder.bearer_auth(token.to_owned());
183    }
184    req_builder = req_builder.json(&body);
185
186    let req = req_builder.build()?;
187    let resp = configuration.client.execute(req).await?;
188
189    let status = resp.status();
190    tracing::Span::current().record("http.status_code", status.as_u16());
191
192    if !status.is_client_error() && !status.is_server_error() {
193        let text = resp.text().await?;
194        serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_str(&text))
195            .map_err(Error::from)
196    } else {
197        let text = resp.text().await?;
198        let entity: Option<CreateCommitError> = serde_json::from_str(&text).ok();
199        Err(Error::ResponseError(ResponseContent {
200            status,
201            content: text,
202            entity,
203        }))
204    }
205}