gitea_sdk/lib.rs
1//! This crate is a simple Gitea API client. It's goal is to give you the ability to write
2//! exactly as much code as you need to interact with the specific parts of the Gitea API you need,
3//! but no more.
4//!
5//! # Usage
6//! The main way to interact with the Gitea API is through the `Client` struct. You can create a
7//! new [Client] by calling [Client::new] with the base URL of your Gitea instance and a personal
8//! token. The crate does currently not support basic HTML or OAuth2 authentication.
9//!
10//! Once you have obtained a [Client], you can interact with the Gitea API by calling the various
11//! methods the instance provides. For example, to create a new repository for the currently
12//! authenticated user, you can call:
13//! ```
14//! # use gitea_sdk::{Client, Auth};
15//! # async fn create_repo() {
16//! let client = Client::new("https://gitea.example.com", Auth::Token("your-token"));
17//! let repo = client
18//! .user()
19//! .create_repo("my-new-repo")
20//! // Optional fields
21//! .description("This is my new repo")
22//! .private(true)
23//! // Send the request
24//! .send(&client)
25//! .await
26//! .unwrap();
27//! # }
28//! ```
29//!
30//! Similarly, to get a list of commits for a repository, you can call:
31//! ```
32//! # use gitea_sdk::{Client, Auth};
33//! # async fn get_commits() {
34//! let client = Client::new("https://gitea.example.com", Auth::Token("your-token"));
35//! let commits = client
36//! .repos("owner", "repo-name")
37//! .get_commits()
38//! // Optional fields
39//! .page(2)
40//! .send(&client)
41//! .await
42//! .unwrap();
43//! # }
44//! ```
45//!
46//! If you want to create a new access token for a user, you can call:
47//! ```
48//! # use gitea_sdk::{Client, CreateAccessTokenOption, Auth};
49//! # async fn create_access_token() {
50//! let basic = Auth::Basic("username", "password");
51//! let client = Client::new("https://gitea.example.com", basic);
52//! let token = client
53//! .user()
54//! .create_access_token("username", "my-new-token", vec!["write:repo"])
55//! .send(&client)
56//! .await
57//! .unwrap();
58//! println!("Token {} created: {}", token.name, token.sha1);
59//! // You can now create a new client with the token and use it to interact with the API.
60//! let new_client = Client::new("https://gitea.example.com", Auth::Token(token.sha1));
61//! # }
62//!
63//!
64use base64::engine::{GeneralPurpose, GeneralPurposeConfig};
65use base64::{alphabet, Engine};
66use error::{Result, TeatimeError};
67use std::fmt::Display;
68
69use reqwest::header::{self, HeaderMap, HeaderValue};
70use reqwest::{Method, Response};
71use serde::{de::DeserializeOwned, Deserialize, Serialize};
72
73pub mod error;
74
75pub mod api;
76pub mod model;
77
78#[derive(Default, Debug, Clone, Serialize, Deserialize)]
79pub struct CreateAccessTokenOption {
80 /// Access token name.
81 pub name: String,
82 /// Optional scopes for the access token.
83 pub scopes: Option<Vec<String>>,
84}
85
86/// Represents the authentication method to use with the Gitea API.
87pub enum Auth<D: ToString> {
88 Token(D),
89 Basic(D, D),
90 None,
91}
92
93/// Represents a Gitea client.
94///
95/// This struct is the main way to interact with the Gitea API.
96/// It provides methods for creating repositories, getting repositories, deleting repositories,
97/// and listing a repo's commits.
98pub struct Client {
99 cli: reqwest::Client,
100 base_url: String,
101}
102
103impl Client {
104 /// Creates a new Gitea client with the given base URL and personal token.
105 /// NOTE: The base URL MUST not include the `/api/v1` path and should not contain any trailing
106 /// slashes. For example, `https://gitea.example.com` is a valid base URL, but
107 /// `https://gitea.example.com/` or `https://gitea.example.com/api/v1` are not.
108 pub fn new(base_url: impl ToString, auth: Auth<impl ToString>) -> Self {
109 let mut headers = HeaderMap::new();
110 match auth {
111 Auth::Token(token) => {
112 let token = HeaderValue::from_str(&format!("token {}", token.to_string()))
113 .expect("token error");
114 headers.insert(header::AUTHORIZATION, token);
115 }
116 Auth::Basic(user, pass) => {
117 let engine = GeneralPurpose::new(&alphabet::STANDARD, GeneralPurposeConfig::new());
118 let base = engine.encode(format!("{}:{}", user.to_string(), pass.to_string()));
119 let basic =
120 HeaderValue::from_str(&format!("Basic {base}")).expect("basic auth error");
121 headers.insert(header::AUTHORIZATION, basic);
122 }
123 Auth::None => {}
124 };
125 headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
126
127 let cli = reqwest::ClientBuilder::new()
128 .default_headers(headers)
129 .user_agent(format!(
130 "{}/{}",
131 env!("CARGO_PKG_NAME"),
132 env!("CARGO_PKG_VERSION")
133 ))
134 .build()
135 .expect("client build error");
136
137 Self {
138 cli,
139 base_url: base_url.to_string(),
140 }
141 }
142
143 pub fn repos(&self, owner: impl ToString, repo: impl ToString) -> api::repos::Repos {
144 api::repos::Repos {
145 owner: owner.to_string(),
146 repo: repo.to_string(),
147 }
148 }
149
150 /// Migrates a repository from another service to Gitea.
151 ///
152 /// This will create a new repository in Gitea with the same name as the repository in the
153 /// source service and copy all the data from the source repository to the new repository.
154 /// The source repository will not be modified.
155 ///
156 /// Gitea supports pull-mirrors, which will keep the new repository in sync with the source
157 /// repository. This is useful if you want to keep the new repository up-to-date with the
158 /// source repository.
159 ///
160 /// # Example
161 ///
162 /// ```
163 /// # use gitea_sdk::{Client, Auth};
164 /// # async fn migrate_repo() {
165 /// let client = Client::new("https://gitea.example.com", Auth::Token("your-token"));
166 /// let repo = client
167 /// .migrate_repo("https://example.git.com/owner/repo", "repo")
168 /// .mirror(true)
169 /// .mirror_interval("1h")
170 /// .send(&client)
171 /// .await
172 /// .unwrap();
173 /// # }
174 /// ```
175 /// This will create a new repository in Gitea with the name `repo` and copy all the data from
176 /// the repository at `https://example.git.com/owner/repo` to the new repository. The new
177 /// repository will be kept in sync with the source repository every hour.
178 pub fn migrate_repo(
179 &self,
180 clone_addr: impl ToString,
181 repo_name: impl ToString,
182 ) -> api::migrate::MigrateRepoBuilder {
183 api::migrate::MigrateRepoBuilder::new(clone_addr, repo_name)
184 }
185
186 pub fn issues(&self, owner: impl ToString, repo: impl ToString) -> api::issues::Issues {
187 api::issues::Issues {
188 owner: owner.to_string(),
189 repo: repo.to_string(),
190 }
191 }
192
193 pub fn pulls(&self, owner: impl ToString, repo: impl ToString) -> api::pulls::Pulls {
194 api::pulls::Pulls {
195 owner: owner.to_string(),
196 repo: repo.to_string(),
197 }
198 }
199
200 pub fn search(&self) -> api::search::Search {
201 api::search::Search
202 }
203
204 pub fn user(&self) -> api::user::User {
205 api::user::User
206 }
207
208 pub fn users(&self, username: impl ToString) -> api::users::Users {
209 api::users::Users {
210 username: username.to_string(),
211 }
212 }
213
214 pub fn orgs(&self, name: impl ToString) -> api::orgs::Orgs {
215 api::orgs::Orgs {
216 name: name.to_string(),
217 }
218 }
219
220 /// Creates a new DELETE-request builder with the given path.
221 /// You may use this method to talk to the Gitea API directly if you need to.
222 /// `path` will be prefixed with `{base_url}/api/v1/` before the request is sent.
223 pub fn delete(&self, path: impl Display) -> reqwest::RequestBuilder {
224 self.request_base(Method::DELETE, path)
225 }
226 /// Creates a new PATCH-request builder with the given path.
227 /// You may use this method to talk to the Gitea API directly if you need to.
228 /// `path` will be prefixed with `{base_url}/api/v1/` before the request is sent.
229 pub fn patch(&self, path: impl Display) -> reqwest::RequestBuilder {
230 self.request_base(Method::PATCH, path)
231 }
232 /// Creates a new POST-request builder with the given path.
233 /// You may use this method to talk to the Gitea API directly if you need to.
234 /// `path` will be prefixed with `{base_url}/api/v1/` before the request is sent.
235 pub fn post(&self, path: impl Display) -> reqwest::RequestBuilder {
236 self.request_base(Method::POST, path)
237 }
238 /// Creates a new POST-request builder with the given path.
239 /// You may use this method to talk to the Gitea API directly if you need to.
240 /// `path` will be prefixed with `{base_url}/api/v1/` before the request is sent.
241 pub fn get(&self, path: impl Display) -> reqwest::RequestBuilder {
242 self.request_base(Method::GET, path)
243 }
244 /// Creates a new PUT-request builder with the given path.
245 /// You may use this method to talk to the Gitea API directly if you need to.
246 /// `path` will be prefixed with `{base_url}/api/v1/` before the request is sent.
247 pub fn put(&self, path: impl Display) -> reqwest::RequestBuilder {
248 self.request_base(Method::PUT, path)
249 }
250
251 /// Creates a new request builder with the given method and path.
252 /// You may use this method to talk to the Gitea API directly if you need to.
253 /// `path` will be prefixed with `{base_url}/api/v1/` before the request is sent.
254 pub fn request_base(&self, method: Method, path: impl Display) -> reqwest::RequestBuilder {
255 self.cli
256 .request(method, format!("{}/api/v1/{}", self.base_url, path))
257 }
258 /// Sends a request and checks the response for errors.
259 /// You may use this method to talk to the Gitea API directly if you need to.
260 /// This method will return a [TeatimeError] if the request fails.
261 /// NOTE: This method is not recommended for general use. Use the more specific methods
262 /// provided by the [Client] struct if they exist.
263 /// You are responsible for providing the correct Model for the response.
264 pub async fn make_request(&self, req: reqwest::Request) -> Result<Response> {
265 let res = self.cli.execute(req).await?;
266 let status = res.status();
267 if status.is_client_error() || status.is_server_error() {
268 return Err(TeatimeError {
269 message: res.text().await.unwrap_or_default(),
270 kind: error::TeatimeErrorKind::HttpError,
271 status_code: status,
272 });
273 }
274 Ok(res)
275 }
276 /// Parses a json response into a given model.
277 /// You may use this method to talk to the Gitea API directly if you need to.
278 /// This method will return a [TeatimeError] if the response cannot be deserialized.
279 /// NOTE: This method is not recommended for general use. Use the more specific methods
280 /// provided by the [Client] struct if they exist.
281 /// You are responsible for providing the correct Model for the response.
282 pub async fn parse_response<T: DeserializeOwned>(&self, res: reqwest::Response) -> Result<T> {
283 let status_code = res.status();
284 let text = res.text().await?;
285 serde_json::from_str(&text).map_err(|e| TeatimeError {
286 message: format!("Error parsing response: {}", e),
287 kind: error::TeatimeErrorKind::SerializationError,
288 status_code,
289 })
290 }
291}