pcu_lib/client/
mod.rs

1use std::{collections::HashMap, env, ffi::OsString};
2
3pub(crate) mod graphql;
4mod pull_request;
5
6use self::pull_request::PullRequest;
7
8use config::Config;
9use git2::Repository;
10use keep_a_changelog::{ChangeKind, ChangelogParseOptions};
11use octocrate::{APIConfig, AppAuthorization, GitHubAPI, PersonalAccessToken};
12use owo_colors::{OwoColorize, Style};
13
14use crate::Error;
15use crate::PrTitle;
16
17const END_POINT: &str = "https://api.github.com/graphql";
18
19pub struct Client {
20    #[allow(dead_code)]
21    // pub(crate) settings: Config,
22    pub(crate) git_repo: Repository,
23    pub(crate) github_rest: GitHubAPI,
24    pub(crate) github_graphql: gql_client::Client,
25    pub(crate) owner: String,
26    pub(crate) repo: String,
27    pub(crate) default_branch: String,
28    pub(crate) branch: Option<String>,
29    pull_request: Option<PullRequest>,
30    pub(crate) changelog: OsString,
31    pub(crate) line_limit: usize,
32    pub(crate) changelog_parse_options: ChangelogParseOptions,
33    pub(crate) changelog_update: Option<PrTitle>,
34    pub(crate) commit_message: String,
35}
36
37impl Client {
38    pub async fn new_with(settings: Config) -> Result<Self, Error> {
39        let cmd = settings
40            .get::<String>("command")
41            .map_err(|_| Error::CommandNotSet)?;
42        log::trace!("cmd: {:?}", cmd);
43
44        // Use the username config settings to direct to the appropriate CI environment variable to find the owner
45        log::trace!("owner: {:?}", settings.get::<String>("username"));
46        let pcu_owner: String = settings
47            .get("username")
48            .map_err(|_| Error::EnvVarBranchNotSet)?;
49        let owner = env::var(pcu_owner).map_err(|_| Error::EnvVarBranchNotFound)?;
50
51        // Use the reponame config settings to direct to the appropriate CI environment variable to find the repo
52        log::trace!("repo: {:?}", settings.get::<String>("reponame"));
53        let pcu_owner: String = settings
54            .get("reponame")
55            .map_err(|_| Error::EnvVarBranchNotSet)?;
56        let repo = env::var(pcu_owner).map_err(|_| Error::EnvVarBranchNotFound)?;
57
58        let default_branch = settings
59            .get::<String>("default_branch")
60            .unwrap_or("main".to_string());
61
62        let commit_message = settings
63            .get::<String>("commit_message")
64            .unwrap_or("".to_string());
65
66        let line_limit = settings.get::<usize>("line_limit").unwrap_or(10);
67
68        let (github_rest, github_graphql) =
69            Client::get_github_apis(&settings, &owner, &repo).await?;
70
71        log::trace!("Executing for command: {}", &cmd);
72        let (branch, pull_request) = if &cmd == "pr" {
73            // Use the branch config settings to direct to the appropriate CI environment variable to find the branch data
74            log::trace!("branch: {:?}", settings.get::<String>("branch"));
75            let pcu_branch: String = settings
76                .get("branch")
77                .map_err(|_| Error::EnvVarBranchNotSet)?;
78            let branch = env::var(pcu_branch).map_err(|_| Error::EnvVarBranchNotFound)?;
79            let branch = if branch.is_empty() {
80                None
81            } else {
82                Some(branch)
83            };
84
85            let pull_request =
86                PullRequest::new_pull_request_opt(&settings, &github_graphql).await?;
87
88            (branch, pull_request)
89        } else {
90            let branch = None;
91            let pull_request = None;
92            (branch, pull_request)
93        };
94        log::trace!("branch: {:?} and pull_request: {:?}", branch, pull_request);
95
96        // Use the log config setting to set the default change log file name
97        log::trace!("log: {:?}", settings.get::<String>("log"));
98        let default_change_log: String = settings
99            .get("log")
100            .map_err(|_| Error::DefaultChangeLogNotSet)?;
101
102        // Get the name of the changelog file
103        let mut changelog = OsString::from(default_change_log);
104        if let Ok(files) = std::fs::read_dir(".") {
105            for file in files.into_iter().flatten() {
106                log::trace!("File: {:?}", file.path());
107
108                if file
109                    .file_name()
110                    .to_string_lossy()
111                    .to_lowercase()
112                    .contains("change")
113                    && file.file_type().unwrap().is_file()
114                {
115                    changelog = file.file_name();
116                    break;
117                }
118            }
119        };
120
121        let git_repo = git2::Repository::open(".")?;
122
123        let svs_root = settings
124            .get("dev_platform")
125            .unwrap_or_else(|_| "https://github.com/".to_string());
126        let prefix = settings
127            .get("version_prefix")
128            .unwrap_or_else(|_| "v".to_string());
129        let repo_url = Some(format!("{}{}/{}", svs_root, owner, repo));
130        let changelog_parse_options = ChangelogParseOptions {
131            url: repo_url,
132            head: Some("HEAD".to_string()),
133            tag_prefix: Some(prefix),
134        };
135
136        Ok(Self {
137            git_repo,
138            github_rest,
139            github_graphql,
140            default_branch,
141            branch,
142            owner,
143            repo,
144            pull_request,
145            changelog,
146            line_limit,
147            changelog_parse_options,
148            changelog_update: None,
149            commit_message,
150        })
151    }
152
153    /// Get the GitHub API instance
154    async fn get_github_apis(
155        settings: &Config,
156        owner: &str,
157        repo: &str,
158    ) -> Result<(GitHubAPI, gql_client::Client), Error> {
159        let bld_style = Style::new().bold();
160        log::debug!("*******\nGet GitHub API instance");
161        let (config, token) = match settings.get::<String>("app_id") {
162            Ok(app_id) => {
163                log::debug!("Using {} for authentication", "GitHub App".style(bld_style));
164
165                let private_key = settings
166                    .get::<String>("private_key")
167                    .map_err(|_| Error::NoGitHubAPIPrivateKey)?;
168
169                let app_authorization = AppAuthorization::new(app_id, private_key);
170                let config = APIConfig::with_token(app_authorization).shared();
171
172                let api = GitHubAPI::new(&config);
173
174                let installation = api
175                    .apps
176                    .get_repo_installation(owner, repo)
177                    .send()
178                    .await
179                    .unwrap();
180                let installation_token = api
181                    .apps
182                    .create_installation_access_token(installation.id)
183                    .send()
184                    .await
185                    .unwrap();
186
187                (
188                    APIConfig::with_token(installation_token.clone()).shared(),
189                    installation_token.token,
190                )
191            }
192            Err(_) => {
193                let pat = settings
194                    .get::<String>("pat")
195                    .map_err(|_| Error::NoGitHubAPIAuth)?;
196                log::debug!(
197                    "Falling back to {} for authentication",
198                    "Personal Access Token".style(bld_style)
199                );
200
201                // Create a personal access token
202                let personal_access_token = PersonalAccessToken::new(&pat);
203
204                // Use the personal access token to create a API configuration
205                (APIConfig::with_token(personal_access_token).shared(), pat)
206            }
207        };
208
209        let auth = format!("Bearer {}", token);
210
211        let headers = HashMap::from([
212            ("X-Github-Next-Global-ID", "1"),
213            ("User-Agent", owner),
214            ("Authorization", &auth),
215        ]);
216
217        let github_graphql = gql_client::Client::new_with_headers(END_POINT, headers);
218
219        let github_rest = GitHubAPI::new(&config);
220
221        Ok((github_rest, github_graphql))
222    }
223
224    pub fn branch_or_main(&self) -> &str {
225        self.branch.as_ref().map_or("main", |v| v)
226    }
227
228    pub fn pull_request(&self) -> &str {
229        if let Some(pr) = self.pull_request.as_ref() {
230            &pr.pull_request
231        } else {
232            ""
233        }
234    }
235
236    pub fn title(&self) -> &str {
237        if let Some(pr) = self.pull_request.as_ref() {
238            &pr.title
239        } else {
240            ""
241        }
242    }
243
244    pub fn pr_number(&self) -> i64 {
245        if let Some(pr) = self.pull_request.as_ref() {
246            pr.pr_number
247        } else {
248            0
249        }
250    }
251
252    pub fn owner(&self) -> &str {
253        &self.owner
254    }
255
256    pub fn repo(&self) -> &str {
257        &self.repo
258    }
259
260    pub fn line_limit(&self) -> usize {
261        self.line_limit
262    }
263
264    pub fn set_title(&mut self, title: &str) {
265        if self.pull_request.is_some() {
266            self.pull_request.as_mut().unwrap().title = title.to_string();
267        }
268    }
269
270    pub fn is_default_branch(&self) -> bool {
271        if let Some(branch) = &self.branch {
272            *branch == self.default_branch
273        } else {
274            false
275        }
276    }
277
278    pub fn section(&self) -> Option<&str> {
279        if let Some(update) = &self.changelog_update {
280            if let Some(section) = &update.section {
281                match section {
282                    ChangeKind::Added => Some("Added"),
283                    ChangeKind::Changed => Some("Changed"),
284                    ChangeKind::Deprecated => Some("Deprecated"),
285                    ChangeKind::Fixed => Some("Fixed"),
286                    ChangeKind::Removed => Some("Removed"),
287                    ChangeKind::Security => Some("Security"),
288                }
289            } else {
290                None
291            }
292        } else {
293            None
294        }
295    }
296
297    pub fn entry(&self) -> Option<&str> {
298        if let Some(update) = &self.changelog_update {
299            Some(&update.entry)
300        } else {
301            None
302        }
303    }
304
305    pub fn changelog_as_str(&self) -> &str {
306        if let Some(cl) = &self.changelog.to_str() {
307            cl
308        } else {
309            ""
310        }
311    }
312
313    pub fn set_changelog(&mut self, changelog: &str) {
314        self.changelog = changelog.into();
315    }
316}