pcu_lib/client/
mod.rs

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