1use eyre::{eyre, Result};
2use open::that as open_in_browser;
3use serde::{Deserialize, Serialize};
4use time::OffsetDateTime;
5use tracing::warn;
6
7use crate::formatters::formatter::{Formatter, FormatterType};
8use crate::vcs::{bitbucket::Bitbucket, gitea::Gitea, github::GitHub, gitlab::GitLab};
9
10#[derive(Debug, Default, Deserialize, Serialize)]
11pub struct User {
12 pub id: String,
13 pub username: String,
14}
15
16#[derive(Debug, Deserialize, Serialize)]
17pub enum PullRequestState {
18 Open,
19 Closed,
20 Merged,
21 Locked,
22}
23
24#[derive(Debug, Deserialize, Serialize)]
25pub struct PullRequest {
26 pub id: u32,
27 pub state: PullRequestState,
28 pub title: String,
29 pub description: String,
30 pub source: String,
31 pub target: String,
32 pub source_sha: String,
33 pub target_sha: String,
34 pub url: String,
35 #[serde(with = "time::serde::iso8601")]
36 pub created_at: OffsetDateTime,
37 #[serde(with = "time::serde::iso8601")]
38 pub updated_at: OffsetDateTime,
39 pub author: User,
40 pub closed_by: Option<User>,
41 pub reviewers: Option<Vec<User>>,
42 pub delete_source_branch: bool,
43}
44
45impl PullRequest {
46 pub fn print(&self, in_browser: bool, formatter_type: FormatterType) {
47 if in_browser && open_in_browser(&self.url).is_ok() {
49 return;
50 }
51 print!("{}", self.show(formatter_type));
52 }
53}
54
55#[derive(Debug, Deserialize, Serialize)]
56pub struct CreatePullRequest {
57 pub title: String,
58 pub description: String,
59 pub source: String,
60 pub target: Option<String>,
61 pub close_source_branch: bool,
62 pub reviewers: Vec<String>,
63}
64
65#[derive(Debug, Default, Deserialize, Serialize)]
66pub enum PullRequestUserFilter {
67 Me,
68 #[default]
69 All,
70}
71
72#[derive(Debug, Default, Deserialize, Serialize)]
73pub enum PullRequestStateFilter {
74 #[default]
75 Open,
76 Closed,
77 Merged,
78 Locked,
79 All,
80}
81
82#[derive(Debug, Default, Deserialize, Serialize)]
83pub struct ListPullRequestFilters {
84 pub author: PullRequestUserFilter,
85 pub state: PullRequestStateFilter,
86}
87
88#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
89pub enum RepositoryVisibility {
90 Public,
91 Internal,
92 Private,
93}
94
95#[derive(Debug, Deserialize, Serialize)]
96pub struct ForkedFromRepository {
97 pub name: String,
98 pub full_name: String,
99 pub html_url: String,
100}
101
102#[derive(Debug, Deserialize, Serialize)]
103pub struct Repository {
104 pub name: String,
105 pub full_name: String,
106 pub owner: Option<User>,
107 pub html_url: String,
108 pub ssh_url: String,
109 pub https_url: String,
110 pub description: String,
111 #[serde(with = "time::serde::iso8601")]
112 pub created_at: OffsetDateTime,
113 #[serde(with = "time::serde::iso8601")]
114 pub updated_at: OffsetDateTime,
115 pub visibility: RepositoryVisibility,
116 pub archived: bool,
117 pub default_branch: String,
118 pub forks_count: u32,
119 pub stars_count: u32,
120 pub forked_from: Option<ForkedFromRepository>,
121}
122
123impl Repository {
124 pub fn print(&self, in_browser: bool, formatter_type: FormatterType) {
125 if in_browser && open_in_browser(&self.html_url).is_ok() {
127 return;
128 }
129 print!("{}", self.show(formatter_type));
130 }
131}
132
133#[derive(Debug, Deserialize, Serialize)]
134pub struct CreateRepository {
135 pub name: String,
136 pub organization: Option<String>,
137 pub description: Option<String>,
138 pub visibility: RepositoryVisibility,
139 pub init: bool,
140 pub default_branch: Option<String>,
141 pub gitignore: Option<String>,
142 pub license: Option<String>,
143}
144
145#[derive(Debug, Deserialize, Serialize)]
146pub struct ForkRepository {
147 pub name: Option<String>,
148 pub organization: Option<String>,
149}
150
151#[derive(Debug, Default, Clone)]
152pub struct VersionControlSettings {
153 pub auth: String,
154 pub vcs_type: Option<String>,
155 pub default_branch: Option<String>,
156 pub fork: bool,
157}
158pub trait VersionControl {
159 fn init(hostname: String, repo: String, settings: VersionControlSettings) -> Self
160 where
161 Self: Sized;
162
163 fn login_url(&self) -> String;
165 fn validate_token(&self, token: &str) -> Result<()>;
166
167 fn create_pr(&self, pr: CreatePullRequest) -> Result<PullRequest>;
169 fn get_pr_by_id(&self, id: u32) -> Result<PullRequest>;
170 fn get_pr_by_branch(&self, branch: &str) -> Result<PullRequest>;
171 fn list_prs(&self, filters: ListPullRequestFilters) -> Result<Vec<PullRequest>>;
172 fn approve_pr(&self, id: u32) -> Result<()>;
173 fn close_pr(&self, id: u32) -> Result<PullRequest>;
174 fn merge_pr(&self, id: u32, delete_source_branch: bool) -> Result<PullRequest>;
175
176 fn get_repository(&self) -> Result<Repository>;
178 fn create_repository(&self, repo: CreateRepository) -> Result<Repository>;
179 fn fork_repository(&self, repo: ForkRepository) -> Result<Repository>;
180 fn delete_repository(&self) -> Result<()>;
181}
182
183pub fn init_vcs(
184 hostname: String,
185 repo: String,
186 settings: VersionControlSettings,
187) -> Result<Box<dyn VersionControl>> {
188 if let Some(vcs_type) = &settings.vcs_type {
189 match vcs_type.as_str() {
190 "github" => Ok(Box::new(GitHub::init(hostname, repo, settings))),
191 "bitbucket" => Ok(Box::new(Bitbucket::init(hostname, repo, settings))),
192 "gitlab" => Ok(Box::new(GitLab::init(hostname, repo, settings))),
193 "gitea" => Ok(Box::new(Gitea::init(hostname, repo, settings))),
194 _ => Err(eyre!("Server type {vcs_type} not found.")),
195 }
196 } else {
197 match hostname.as_str() {
198 "github.com" => Ok(Box::new(GitHub::init(hostname, repo, settings))),
199 "bitbucket.org" => Ok(Box::new(Bitbucket::init(hostname, repo, settings))),
200 "gitlab.com" => Ok(Box::new(GitLab::init(hostname, repo, settings))),
201 _ => {
202 if hostname.contains("github") {
204 warn!("Assuming the host to be GitHub Enterprise (if it is incorrect, add --type at login).");
205 Ok(Box::new(GitHub::init(hostname, repo, settings)))
206 } else if hostname.contains("gitlab") {
207 warn!(
208 "Assuming the host to be GitLab (if it is incorrect, add --type at login)."
209 );
210 Ok(Box::new(GitLab::init(hostname, repo, settings)))
211 } else if hostname.contains("gitea") {
212 warn!(
213 "Assuming the host to be Gitea (if it is incorrect, add --type at login)."
214 );
215 Ok(Box::new(Gitea::init(hostname, repo, settings)))
216 }
217 else {
219 Err(eyre!(
220 "Server {hostname} type cannot be detected, add --type at login."
221 ))
222 }
223 }
224 }
225 }
226}