gr_bin/vcs/
common.rs

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        // Open in browser if open is true
48        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        // Open in browser if open is true
126        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    // Login
164    fn login_url(&self) -> String;
165    fn validate_token(&self, token: &str) -> Result<()>;
166
167    // Pull requests
168    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    // Repositories
177    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                // Take some guesses what the host might be
203                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                // Probably there should more detections down the line
218                else {
219                    Err(eyre!(
220                        "Server {hostname} type cannot be detected, add --type at login."
221                    ))
222                }
223            }
224        }
225    }
226}