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) 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 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 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 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 log::trace!("log: {:?}", settings.get::<String>("log"));
116 let default_change_log: String = settings
117 .get("log")
118 .map_err(|_| Error::DefaultChangeLogNotSet)?;
119
120 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 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 let personal_access_token = PersonalAccessToken::new(&pat);
221
222 (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}