Skip to main content

ephemeris_forge/
release.rs

1//! Release and pull request wire shapes both forges speak.
2
3use facet::Facet;
4use reqwest::Method;
5
6use crate::codec::encode;
7use crate::repo::RepoRef;
8use crate::segment::path_segment;
9use crate::transport::{Http, TransportError};
10
11/// The body that opens a pull request.
12#[derive(Debug, Facet)]
13pub struct PullRequestCreate<'a> {
14    /// The title.
15    pub title: &'a str,
16    /// The description.
17    pub body: &'a str,
18    /// The branch with the changes.
19    pub head: &'a str,
20    /// The branch the changes merge into.
21    pub base: &'a str,
22}
23
24/// The body that updates a pull request.
25#[derive(Debug, Facet)]
26struct PullRequestUpdate<'a> {
27    title: &'a str,
28    body: &'a str,
29}
30
31impl PullRequestCreate<'_> {
32    /// Open the pull request, or update the open one when `current` is its
33    /// number.
34    ///
35    /// # Errors
36    /// Returns the transport error of the write.
37    pub async fn ensure(
38        &self,
39        http: &Http,
40        repository: &RepoRef,
41        current: Option<u64>,
42    ) -> Result<(), TransportError> {
43        let (method, path, body) = current.map_or_else(
44            || (Method::POST, repository.path("pulls"), encode(self)),
45            |number| {
46                let update = PullRequestUpdate {
47                    title: self.title,
48                    body: self.body,
49                };
50                (
51                    Method::PATCH,
52                    repository.path(&format!("pulls/{number}")),
53                    encode(&update),
54                )
55            },
56        );
57        http.send(method, &path, Some(&body)).await?;
58        Ok(())
59    }
60}
61
62/// The body that creates a draft release.
63#[derive(Debug, Facet)]
64pub struct DraftRelease<'a> {
65    /// The tag the release is for.
66    pub tag_name: &'a str,
67    /// The commit the tag points at.
68    pub target_commitish: &'a str,
69    /// The title.
70    pub name: &'a str,
71    /// The release notes.
72    pub body: &'a str,
73    /// Always true.
74    pub draft: bool,
75    /// Always false.
76    pub prerelease: bool,
77}
78
79impl<'a> DraftRelease<'a> {
80    /// A draft release of `tag` at `target_commit`.
81    #[must_use]
82    pub const fn new(tag: &'a str, target_commit: &'a str, title: &'a str, notes: &'a str) -> Self {
83        Self {
84            tag_name: tag,
85            target_commitish: target_commit,
86            name: title,
87            body: notes,
88            draft: true,
89            prerelease: false,
90        }
91    }
92
93    /// Create the draft and return its ID.
94    ///
95    /// # Errors
96    /// Returns the transport error of the write.
97    pub async fn create(&self, http: &Http, repository: &RepoRef) -> Result<u64, TransportError> {
98        let path = repository.path("releases");
99        let release: ReleaseWire = http.json(Method::POST, &path, Some(&encode(self))).await?;
100        Ok(release.id)
101    }
102}
103
104/// The body that publishes a draft release.
105#[derive(Clone, Copy, Debug, Facet)]
106pub struct PublishRelease {
107    /// Always false.
108    pub draft: bool,
109}
110
111/// A release as the forge returns it.
112#[derive(Debug, Facet)]
113pub struct ReleaseWire {
114    /// The release ID.
115    pub id: u64,
116    /// Whether the release is still a draft.
117    pub draft: bool,
118    /// The upload URL template, empty where the forge has none.
119    #[facet(default)]
120    pub upload_url: String,
121}
122
123impl ReleaseWire {
124    /// Read the release of `tag`.
125    ///
126    /// # Errors
127    /// Returns the transport error of the read.
128    pub async fn by_tag(
129        http: &Http,
130        repository: &RepoRef,
131        tag: &str,
132    ) -> Result<Option<Self>, TransportError> {
133        let path = repository.path(&format!("releases/tags/{}", path_segment(tag)));
134        http.json_optional(Method::GET, &path, None).await
135    }
136}
137
138/// A release asset as the forge returns it.
139#[derive(Debug, Facet)]
140pub struct AssetWire {
141    /// The asset ID.
142    pub id: u64,
143    /// The file name.
144    pub name: String,
145    /// The size in bytes.
146    pub size: u64,
147    /// The digest, where the forge reports one.
148    #[facet(default)]
149    pub digest: Option<String>,
150    /// Where the bytes can be fetched.
151    #[facet(default)]
152    pub browser_download_url: String,
153}
154
155/// The object a Git ref points at.
156#[derive(Debug, Facet)]
157pub struct RefObject {
158    /// The object's SHA.
159    pub sha: String,
160    /// The object type, such as `commit` or `tag`.
161    #[facet(rename = "type", default)]
162    pub kind: String,
163}
164
165/// A Git object wrapper, as returned for an annotated tag.
166#[derive(Debug, Facet)]
167pub struct ObjectWire {
168    /// The wrapped object.
169    pub object: RefObject,
170}
171
172impl RefObject {
173    /// The commit this object leads to, reading through an annotated tag.
174    ///
175    /// # Errors
176    /// Returns the transport error of reading the annotated tag.
177    pub async fn peel(self, http: &Http, repository: &RepoRef) -> Result<String, TransportError> {
178        if self.kind != "tag" {
179            return Ok(self.sha);
180        }
181        let path = repository.path(&format!("git/tags/{}", self.sha));
182        let annotated: ObjectWire = http.json(Method::GET, &path, None).await?;
183        Ok(annotated.object.sha)
184    }
185}