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) 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 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 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 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 log::trace!("log: {:?}", settings.get::<String>("log"));
98 let default_change_log: String = settings
99 .get("log")
100 .map_err(|_| Error::DefaultChangeLogNotSet)?;
101
102 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 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 let personal_access_token = PersonalAccessToken::new(&pat);
203
204 (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}