Skip to main content

gitea_sdk_rs/options/
pull.rs

1// Copyright 2026 infinitete. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5//! Request option types for pull request API endpoints.
6
7use crate::pagination::{ListOptions, QueryEncode};
8use crate::types::enums::{MergeStyle, ReviewStateType, StateType};
9use crate::{Deserialize, Serialize};
10
11// ── pull.go ─────────────────────────────────────────────────────
12
13/// ListPullRequestsOptions options for listing pull requests
14#[derive(Debug, Clone)]
15/// Options for List Pull Requests Option.
16pub struct ListPullRequestsOptions {
17    pub list_options: ListOptions,
18    pub state: StateType,
19    /// oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority
20    pub sort: String,
21    pub milestone: i64,
22}
23
24impl Default for ListPullRequestsOptions {
25    fn default() -> Self {
26        Self {
27            list_options: ListOptions::default(),
28            state: StateType::All,
29            sort: String::new(),
30            milestone: 0,
31        }
32    }
33}
34
35impl QueryEncode for ListPullRequestsOptions {
36    fn query_encode(&self) -> String {
37        let mut out = self.list_options.query_encode();
38        if !matches!(self.state, StateType::All) {
39            out.push_str(&format!("&state={}", self.state));
40        }
41        if !self.sort.is_empty() {
42            out.push_str(&format!("&sort={}", self.sort));
43        }
44        if self.milestone > 0 {
45            out.push_str(&format!("&milestone={}", self.milestone));
46        }
47        out
48    }
49}
50
51/// CreatePullRequestOption options when creating a pull request
52#[derive(Debug, Clone, Serialize, Deserialize)]
53/// Options for Create Pull Request Option.
54pub struct CreatePullRequestOption {
55    pub head: String,
56    pub base: String,
57    pub title: String,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub body: Option<String>,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub assignee: Option<String>,
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub assignees: Vec<String>,
64    #[serde(default, skip_serializing_if = "Vec::is_empty")]
65    pub reviewers: Vec<String>,
66    #[serde(default, skip_serializing_if = "Vec::is_empty")]
67    pub team_reviewers: Vec<String>,
68    #[serde(default)]
69    pub milestone: i64,
70    #[serde(default, skip_serializing_if = "Vec::is_empty")]
71    pub labels: Vec<i64>,
72    #[serde(
73        rename = "due_date",
74        default,
75        with = "nullable_rfc3339_option",
76        skip_serializing_if = "Option::is_none"
77    )]
78    pub deadline: Option<time::OffsetDateTime>,
79}
80
81mod nullable_rfc3339_option {
82    use serde::{Deserializer, Serializer};
83    use time::OffsetDateTime;
84
85    /// Serialize an optional RFC 3339 timestamp for serde.
86    pub fn serialize<S>(opt: &Option<OffsetDateTime>, serializer: S) -> Result<S::Ok, S::Error>
87    where
88        S: Serializer,
89    {
90        match opt {
91            Some(dt) => {
92                let formatted = dt
93                    .format(&time::format_description::well_known::Rfc3339)
94                    .map_err(serde::ser::Error::custom)?;
95                serializer.serialize_str(&formatted)
96            }
97            None => serializer.serialize_none(),
98        }
99    }
100
101    /// Deserialize an optional RFC 3339 timestamp for serde.
102    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
103    where
104        D: Deserializer<'de>,
105    {
106        use serde::Deserialize;
107        let opt: Option<String> = Option::deserialize(deserializer)?;
108        match opt {
109            None => Ok(None),
110            Some(ref s) => {
111                let dt = OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
112                    .map_err(serde::de::Error::custom)?;
113                Ok(Some(dt))
114            }
115        }
116    }
117}
118
119/// EditPullRequestOption options when modify pull request
120#[derive(Debug, Clone, Default, Serialize, Deserialize)]
121/// Options for Edit Pull Request Option.
122pub struct EditPullRequestOption {
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub title: Option<String>,
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub body: Option<String>,
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub base: Option<String>,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub assignee: Option<String>,
131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
132    pub assignees: Vec<String>,
133    #[serde(default)]
134    pub milestone: i64,
135    #[serde(default, skip_serializing_if = "Vec::is_empty")]
136    pub labels: Vec<i64>,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub state: Option<StateType>,
139    #[serde(
140        rename = "due_date",
141        default,
142        with = "nullable_rfc3339_option",
143        skip_serializing_if = "Option::is_none"
144    )]
145    pub deadline: Option<time::OffsetDateTime>,
146    #[serde(rename = "unset_due_date", skip_serializing_if = "Option::is_none")]
147    pub remove_deadline: Option<bool>,
148    #[serde(
149        rename = "allow_maintainer_edit",
150        skip_serializing_if = "Option::is_none"
151    )]
152    pub allow_maintainer_edit: Option<bool>,
153}
154
155impl EditPullRequestOption {
156    /// Validate the EditPullRequestOption struct
157    pub fn validate(&self) -> crate::Result<()> {
158        if let Some(ref title) = self.title
159            && title.trim().is_empty()
160        {
161            return Err(crate::Error::Validation("title is empty".to_string()));
162        }
163        Ok(())
164    }
165}
166
167/// MergePullRequestOption options when merging a pull request
168#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169/// Options for Merge Pull Request Option.
170pub struct MergePullRequestOption {
171    #[serde(rename = "Do", skip_serializing_if = "Option::is_none")]
172    pub style: Option<MergeStyle>,
173    #[serde(rename = "MergeCommitID", skip_serializing_if = "Option::is_none")]
174    pub merge_commit_id: Option<String>,
175    #[serde(rename = "MergeTitleField", skip_serializing_if = "Option::is_none")]
176    pub title: Option<String>,
177    #[serde(rename = "MergeMessageField", skip_serializing_if = "Option::is_none")]
178    pub message: Option<String>,
179    #[serde(rename = "delete_branch_after_merge")]
180    pub delete_branch_after_merge: bool,
181    #[serde(rename = "force_merge")]
182    pub force_merge: bool,
183    #[serde(rename = "head_commit_id", skip_serializing_if = "Option::is_none")]
184    pub head_commit_id: Option<String>,
185    #[serde(rename = "merge_when_checks_succeed")]
186    pub merge_when_checks_succeed: bool,
187}
188
189/// PullRequestDiffOptions options for GET `/repos/<owner>/<repo>/pulls/<idx>.[diff|patch]`
190#[derive(Debug, Clone, Default)]
191/// Options for Pull Request Diff Option.
192pub struct PullRequestDiffOptions {
193    /// Include binary file changes when requesting a .diff
194    pub binary: bool,
195}
196
197impl QueryEncode for PullRequestDiffOptions {
198    fn query_encode(&self) -> String {
199        format!("binary={}", self.binary)
200    }
201}
202
203/// ListPullRequestCommitsOptions options for listing pull request commits
204#[derive(Debug, Clone, Default)]
205/// Options for List Pull Request Commits Option.
206pub struct ListPullRequestCommitsOptions {
207    pub list_options: ListOptions,
208}
209
210impl QueryEncode for ListPullRequestCommitsOptions {
211    fn query_encode(&self) -> String {
212        self.list_options.query_encode()
213    }
214}
215
216/// ListPullRequestFilesOptions options for listing pull request files
217#[derive(Debug, Clone, Default)]
218/// Options for List Pull Request Files Option.
219pub struct ListPullRequestFilesOptions {
220    pub list_options: ListOptions,
221}
222
223impl QueryEncode for ListPullRequestFilesOptions {
224    fn query_encode(&self) -> String {
225        self.list_options.query_encode()
226    }
227}
228
229// ── pull_review.go ───────────────────────────────────────────────
230
231/// ListPullReviewsOptions options for listing PullReviews
232#[derive(Debug, Clone, Default)]
233/// Options for List Pull Reviews Option.
234pub struct ListPullReviewsOptions {
235    pub list_options: ListOptions,
236}
237
238impl QueryEncode for ListPullReviewsOptions {
239    fn query_encode(&self) -> String {
240        self.list_options.query_encode()
241    }
242}
243
244/// CreatePullReviewOptions are options to create a pull review
245#[derive(Debug, Clone, Serialize, Deserialize)]
246/// Options for Create Pull Review Option.
247pub struct CreatePullReviewOptions {
248    #[serde(rename = "event", skip_serializing_if = "Option::is_none")]
249    pub state: Option<ReviewStateType>,
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub body: Option<String>,
252    #[serde(rename = "commit_id", skip_serializing_if = "Option::is_none")]
253    pub commit_id: Option<String>,
254    #[serde(default, skip_serializing_if = "Vec::is_empty")]
255    pub comments: Vec<CreatePullReviewComment>,
256}
257
258impl CreatePullReviewOptions {
259    /// Validate the CreatePullReviewOptions struct
260    pub fn validate(&self) -> crate::Result<()> {
261        if self.state != Some(ReviewStateType::Approved)
262            && self.comments.is_empty()
263            && self.body.as_ref().is_some_and(|b| b.trim().is_empty())
264        {
265            return Err(crate::Error::Validation("body is empty".to_string()));
266        }
267        for comment in &self.comments {
268            comment.validate()?;
269        }
270        Ok(())
271    }
272}
273
274/// CreatePullReviewComment represent a review comment for creation api
275#[derive(Debug, Clone, Serialize, Deserialize)]
276/// Options for Create Pull Review Comment.
277pub struct CreatePullReviewComment {
278    /// the tree path
279    pub path: String,
280    pub body: String,
281    /// if comment to old file line or 0
282    #[serde(rename = "old_position")]
283    pub old_line_num: i64,
284    /// if comment to new file line or 0
285    #[serde(rename = "new_position")]
286    pub new_line_num: i64,
287}
288
289impl CreatePullReviewComment {
290    /// Validate the CreatePullReviewComment struct
291    pub fn validate(&self) -> crate::Result<()> {
292        if self.body.trim().is_empty() {
293            return Err(crate::Error::Validation("body is empty".to_string()));
294        }
295        if self.old_line_num != 0 && self.new_line_num != 0 {
296            return Err(crate::Error::Validation(
297                "old and new line num are set, cant identify the code comment position".to_string(),
298            ));
299        }
300        Ok(())
301    }
302}
303
304/// SubmitPullReviewOptions are options to submit a pending pull review
305#[derive(Debug, Clone, Serialize, Deserialize)]
306/// Options for Submit Pull Review Option.
307pub struct SubmitPullReviewOptions {
308    #[serde(rename = "event", skip_serializing_if = "Option::is_none")]
309    pub state: Option<ReviewStateType>,
310    #[serde(default, skip_serializing_if = "Option::is_none")]
311    pub body: Option<String>,
312}
313
314impl SubmitPullReviewOptions {
315    /// Validate the SubmitPullReviewOptions struct
316    pub fn validate(&self) -> crate::Result<()> {
317        if self.state != Some(ReviewStateType::Approved)
318            && self.body.as_ref().is_some_and(|b| b.trim().is_empty())
319        {
320            return Err(crate::Error::Validation("body is empty".to_string()));
321        }
322        Ok(())
323    }
324}
325
326/// DismissPullReviewOptions are options to dismiss a pull review
327#[derive(Debug, Clone, Serialize, Deserialize)]
328/// Options for Dismiss Pull Review Option.
329pub struct DismissPullReviewOptions {
330    #[serde(default, skip_serializing_if = "Option::is_none")]
331    pub message: Option<String>,
332}
333
334/// PullReviewRequestOptions are options to add or remove pull review requests
335#[derive(Debug, Clone, Default, Serialize, Deserialize)]
336/// Options for Pull Review Request Option.
337pub struct PullReviewRequestOptions {
338    #[serde(default, skip_serializing_if = "Vec::is_empty")]
339    pub reviewers: Vec<String>,
340    #[serde(default, skip_serializing_if = "Vec::is_empty")]
341    pub team_reviewers: Vec<String>,
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_edit_pull_request_option_validate_success() {
350        let opt = EditPullRequestOption {
351            title: Some("new title".to_string()),
352            ..Default::default()
353        };
354        assert!(opt.validate().is_ok());
355    }
356
357    #[test]
358    fn test_edit_pull_request_option_validate_empty_title() {
359        let opt = EditPullRequestOption {
360            title: Some("   ".to_string()),
361            ..Default::default()
362        };
363        assert!(opt.validate().is_err());
364    }
365
366    #[test]
367    fn test_create_pull_review_options_validate_approved() {
368        let opt = CreatePullReviewOptions {
369            state: Some(ReviewStateType::Approved),
370            body: None,
371            commit_id: None,
372            comments: Vec::new(),
373        };
374        assert!(opt.validate().is_ok());
375    }
376
377    #[test]
378    fn test_create_pull_review_options_validate_empty_body() {
379        let opt = CreatePullReviewOptions {
380            state: None,
381            body: Some("   ".to_string()),
382            commit_id: None,
383            comments: Vec::new(),
384        };
385        assert!(opt.validate().is_err());
386    }
387
388    #[test]
389    fn test_create_pull_review_options_validate_with_comments() {
390        let opt = CreatePullReviewOptions {
391            state: None,
392            body: Some("   ".to_string()),
393            commit_id: None,
394            comments: vec![CreatePullReviewComment {
395                path: "main.rs".to_string(),
396                body: "fix this".to_string(),
397                old_line_num: 0,
398                new_line_num: 10,
399            }],
400        };
401        assert!(opt.validate().is_ok());
402    }
403
404    #[test]
405    fn test_create_pull_review_comment_validate_success() {
406        let comment = CreatePullReviewComment {
407            path: "main.rs".to_string(),
408            body: "fix this".to_string(),
409            old_line_num: 0,
410            new_line_num: 10,
411        };
412        assert!(comment.validate().is_ok());
413    }
414
415    #[test]
416    fn test_create_pull_review_comment_validate_empty_body() {
417        let comment = CreatePullReviewComment {
418            path: "main.rs".to_string(),
419            body: String::new(),
420            old_line_num: 0,
421            new_line_num: 10,
422        };
423        assert!(comment.validate().is_err());
424    }
425
426    #[test]
427    fn test_create_pull_review_comment_validate_both_lines_set() {
428        let comment = CreatePullReviewComment {
429            path: "main.rs".to_string(),
430            body: "fix this".to_string(),
431            old_line_num: 5,
432            new_line_num: 10,
433        };
434        assert!(comment.validate().is_err());
435    }
436
437    #[test]
438    fn test_submit_pull_review_options_validate_approved() {
439        let opt = SubmitPullReviewOptions {
440            state: Some(ReviewStateType::Approved),
441            body: None,
442        };
443        assert!(opt.validate().is_ok());
444    }
445
446    #[test]
447    fn test_submit_pull_review_options_validate_empty_body() {
448        let opt = SubmitPullReviewOptions {
449            state: None,
450            body: Some("   ".to_string()),
451        };
452        assert!(opt.validate().is_err());
453    }
454}