1#![allow(dead_code)]
2use http::header::USER_AGENT;
3use http::{Request, Uri};
4use hyper_rustls::HttpsConnectorBuilder;
5
6use octocrab::auth::AppAuth;
7use octocrab::models::issues::{Comment, Issue};
8use octocrab::models::pulls::PullRequest;
9use octocrab::models::repos::{Branch, ContentItems, FileDeletion, FileUpdate};
10use octocrab::models::{InstallationId, IssueState, Label};
11use octocrab::params::{pulls::State as PullState, State};
12use octocrab::service::middleware::base_uri::BaseUriLayer;
13use octocrab::service::middleware::extra_headers::ExtraHeadersLayer;
14use octocrab::{AuthState, Error as OctocrabError, Octocrab, OctocrabBuilder, Page};
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17
18use crate::config::get_env_var_or_default;
19
20const GITHUB_API_URL: &str = "https://api.github.com";
21
22#[derive(Serialize, Deserialize, Debug, Clone)]
23struct RefObject {
24 sha: String,
25}
26
27#[derive(Serialize, Deserialize, Debug, Clone)]
28struct RefData {
29 #[serde(rename = "ref")]
30 _ref: String,
31 object: RefObject,
32}
33#[derive(Serialize, Deserialize, Debug, Clone)]
34struct RefList(pub Vec<RefData>);
35
36struct GithubParams {
37 pub owner: String,
38 pub repo: String,
39 pub app_id: u64,
40 pub installation_id: u64,
41}
42
43#[derive(Debug)]
44pub struct CreateRefillMergeRequestData {
45 pub application_id: String,
46 pub owner_name: String,
47 pub ref_request: Request<String>,
48 pub file_content: String,
49 pub file_name: String,
50 pub branch_name: String,
51 pub commit: String,
52 pub file_sha: String,
53}
54
55#[derive(Debug)]
56pub struct CreateMergeRequestData {
57 pub application_id: String,
58 pub owner_name: String,
59 pub ref_request: Request<String>,
60 pub file_content: String,
61 pub file_name: String,
62 pub branch_name: String,
63 pub commit: String,
64}
65
66#[derive(Debug)]
67pub struct GithubWrapper {
68 pub inner: Arc<Octocrab>,
69 pub owner: String,
70 pub repo: String,
71}
72
73#[derive(Debug, Deserialize, Serialize)]
74struct CommitData {
75 commit: Commit,
76}
77
78#[derive(Debug, Deserialize, Serialize)]
79struct Commit {
80 author: Author,
81}
82
83#[derive(Debug, Deserialize, Serialize)]
84struct Author {
85 name: String,
86}
87
88impl GithubWrapper {
89 pub fn new() -> Self {
90 let owner = get_env_var_or_default("GITHUB_OWNER", "filecoin-project");
91 let repo = get_env_var_or_default("GITHUB_REPO", "filplus-tooling-backend-test");
92 let app_id = get_env_var_or_default("GITHUB_APP_ID", "373258")
93 .parse::<u64>()
94 .unwrap_or_else(|_| {
95 log::error!("Failed to parse GITHUB_APP_ID, using default");
96 373258
97 });
98 let installation_id = get_env_var_or_default("GITHUB_INSTALLATION_ID", "40514592")
99 .parse::<u64>()
100 .unwrap_or_else(|_| {
101 log::error!("Failed to parse GITHUB_INSTALLATION_ID, using default");
102 40514592
103 });
104 let gh_private_key = std::env::var("GH_PRIVATE_KEY").unwrap_or_else(|_| {
105 log::warn!(
106 "GH_PRIVATE_KEY not found in .env file, attempting to read from gh-private-key.pem"
107 );
108 std::fs::read_to_string("gh-private-key.pem").unwrap_or_else(|e| {
109 log::error!("Failed to read gh-private-key.pem. Error: {:?}", e);
110 std::process::exit(1);
111 })
112 });
113
114 let connector = HttpsConnectorBuilder::new()
115 .with_native_roots() .https_only()
117 .enable_http1()
118 .build();
119
120 let client = hyper::Client::builder()
121 .pool_idle_timeout(std::time::Duration::from_secs(15))
122 .build(connector);
123 let key = jsonwebtoken::EncodingKey::from_rsa_pem(gh_private_key.as_bytes()).unwrap();
124 let octocrab = OctocrabBuilder::new_empty()
125 .with_service(client)
126 .with_layer(&BaseUriLayer::new(Uri::from_static(GITHUB_API_URL)))
127 .with_layer(&ExtraHeadersLayer::new(Arc::new(vec![(
128 USER_AGENT,
129 "octocrab".parse().unwrap(),
130 )])))
131 .with_auth(AuthState::App(AppAuth {
132 app_id: app_id.into(),
133 key,
134 }))
135 .build()
136 .expect("Could not create Octocrab instance");
137 let iod: InstallationId = installation_id.try_into().expect("Invalid installation id");
138 let installation = octocrab.installation(iod);
139 Self {
140 owner,
141 repo,
142 inner: Arc::new(installation),
143 }
144 }
145
146 pub async fn list_issues(&self) -> Result<Vec<Issue>, OctocrabError> {
147 let iid = self
148 .inner
149 .issues(&self.owner, &self.repo)
150 .list()
151 .state(State::Open)
152 .send()
153 .await?;
154 Ok(iid.into_iter().map(|i: Issue| i).collect())
155 }
156
157 pub async fn list_issue(&self, number: u64) -> Result<Issue, OctocrabError> {
158 let iid = self
159 .inner
160 .issues(&self.owner, &self.repo)
161 .get(number)
162 .await?;
163 Ok(iid)
164 }
165
166 pub async fn add_comment_to_issue(
167 &self,
168 number: u64,
169 body: &str,
170 ) -> Result<Comment, OctocrabError> {
171 let iid = self
172 .inner
173 .issues(&self.owner, &self.repo)
174 .create_comment(number, body)
175 .await?;
176 Ok(iid)
177 }
178
179 pub async fn replace_issue_labels(
180 &self,
181 number: u64,
182 labels: &[String],
183 ) -> Result<Vec<Label>, OctocrabError> {
184 let iid = self
185 .inner
186 .issues(&self.owner, &self.repo)
187 .replace_all_labels(number, labels)
188 .await?;
189 Ok(iid)
190 }
191
192 pub async fn list_pull_requests(&self) -> Result<Vec<PullRequest>, OctocrabError> {
193 let iid = self
194 .inner
195 .pulls(&self.owner, &self.repo)
196 .list()
197 .state(State::Open)
198 .send()
199 .await?;
200 Ok(iid.into_iter().collect())
201 }
202
203 pub async fn create_commit_in_branch(
204 &self,
205 branch_name: String,
206 commit_body: String,
207 ) -> Result<octocrab::models::commits::Comment, OctocrabError> {
208 let iid = self
209 .inner
210 .commits(&self.owner, &self.repo)
211 .create_comment(branch_name, commit_body)
212 .send()
213 .await?;
214 Ok(iid)
215 }
216
217 pub async fn get_pull_request_files(
218 &self,
219 pr_number: u64,
220 ) -> Result<(u64, Vec<octocrab::models::pulls::FileDiff>), OctocrabError> {
221 let iid: Page<octocrab::models::pulls::FileDiff> = self
222 .inner
223 .pulls(&self.owner, &self.repo)
224 .media_type(octocrab::params::pulls::MediaType::Full)
225 .list_files(pr_number)
226 .await?;
227 Ok((pr_number, iid.items.into_iter().map(|i| i.into()).collect()))
228 }
229
230 pub async fn list_branches(&self) -> Result<Vec<Branch>, OctocrabError> {
231 let iid = self
232 .inner
233 .repos(&self.owner, &self.repo)
234 .list_branches()
235 .send()
236 .await?;
237 Ok(iid.items)
238 }
239
240 pub async fn create_branch(&self, request: Request<String>) -> Result<bool, OctocrabError> {
243 match self.inner.execute(request).await {
244 Ok(_) => {}
245 Err(e) => {
246 println!("Error creating branch: {:?}", e);
247 return Ok(false);
248 }
249 };
250 Ok(true)
251 }
252
253 pub async fn remove_branch(&self, request: Request<String>) -> Result<bool, OctocrabError> {
256 match self.inner.execute(request).await {
257 Ok(_) => {}
258 Err(e) => {
259 println!("Error creating branch: {:?}", e);
260 return Ok(false);
261 }
262 };
263 Ok(true)
264 }
265
266 pub async fn list_pull_request(&self, number: u64) -> Result<PullRequest, OctocrabError> {
267 let iid = self
268 .inner
269 .pulls(&self.owner, &self.repo)
270 .get(number)
271 .await?;
272 Ok(iid)
273 }
274
275 pub async fn create_pull_request(
276 &self,
277 title: &str,
278 head: &str,
279 body: impl Into<String>,
280 ) -> Result<PullRequest, OctocrabError> {
281 let iid = self
282 .inner
283 .pulls(&self.owner, &self.repo)
284 .create(title, head, "main")
285 .body(body)
286 .maintainer_can_modify(true)
287 .send()
288 .await?;
289 Ok(iid)
290 }
291
292 pub async fn update_pull_request(
293 &self,
294 body: &str,
295 number: u64,
296 ) -> Result<PullRequest, OctocrabError> {
297 let iid = self
298 .inner
299 .pulls(&self.owner, &self.repo)
300 .update(number)
301 .body(body)
302 .send()
303 .await?;
304 Ok(iid)
305 }
306
307 pub async fn delete_file(
308 &self,
309 path: &str,
310 branch: &str,
311 message: &str,
312 sha: &str,
313 ) -> Result<FileDeletion, OctocrabError> {
314 let iid = self
315 .inner
316 .repos(&self.owner, &self.repo)
317 .delete_file(path, message, sha)
318 .branch(branch)
319 .send()
320 .await?;
321 Ok(iid)
322 }
323
324 pub async fn add_file(
325 &self,
326 path: &str,
327 content: &str,
328 message: &str,
329 branch: &str,
330 ) -> Result<FileUpdate, OctocrabError> {
331 let iid = self
332 .inner
333 .repos(&self.owner, &self.repo)
334 .create_file(path, message, content)
335 .branch(branch)
336 .send()
337 .await?;
338 Ok(iid)
339 }
340
341 pub async fn get_pull_request_by_number(
342 &self,
343 number: u64,
344 ) -> Result<octocrab::models::pulls::PullRequest, OctocrabError> {
345 let iid = self
346 .inner
347 .pulls(&self.owner, &self.repo)
348 .get(number)
349 .await?;
350 Ok(iid)
351 }
352
353 pub async fn get_file(
354 &self,
355 path: &str,
356 branch: &str,
357 ) -> Result<ContentItems, octocrab::Error> {
358 let iid = self
359 .inner
360 .repos(&self.owner, &self.repo)
361 .get_content()
362 .r#ref(branch)
363 .path(path)
364 .send()
365 .await;
366 iid
367 }
368
369 pub async fn update_file_content(
370 &self,
371 path: &str,
372 message: &str,
373 content: &str,
374 branch: &str,
375 file_sha: &str,
376 ) -> Result<FileUpdate, octocrab::Error> {
377 let iid = self
378 .inner
379 .repos(&self.owner, &self.repo)
380 .update_file(path, message, content, file_sha)
381 .branch(branch)
382 .send()
383 .await?;
384 Ok(iid)
385 }
386
387 pub fn build_remove_ref_request(&self, name: String) -> Result<Request<String>, http::Error> {
388 let request = Request::builder()
389 .method("DELETE")
390 .uri(format!(
391 "https://api.github.com/repos/{}/{}/git/refs/heads/{}",
392 self.owner, self.repo, name
393 ))
394 .body("".to_string())?;
395 Ok(request)
396 }
397
398 pub async fn get_main_branch_sha(&self) -> Result<String, http::Error> {
399 let url = format!(
400 "https://api.github.com/repos/{}/{}/git/refs",
401 self.owner, self.repo
402 );
403 let request = http::request::Builder::new()
404 .method(http::Method::GET)
405 .uri(url);
406 let request = self.inner.build_request::<String>(request, None).unwrap();
407
408 let mut response = match self.inner.execute(request).await {
409 Ok(r) => r,
410 Err(e) => {
411 println!("Error getting main branch sha: {:?}", e);
412 return Ok("".to_string());
413 }
414 };
415 let response = response.body_mut();
416 let body = hyper::body::to_bytes(response).await.unwrap();
417 let shas = body.into_iter().map(|b| b as char).collect::<String>();
418 let shas: RefList = serde_json::from_str(&shas).unwrap();
419 for sha in shas.0 {
420 if sha._ref == "refs/heads/main" {
421 return Ok(sha.object.sha);
422 }
423 }
424 Ok("".to_string())
425 }
426
427 pub fn build_create_ref_request(
428 &self,
429 name: String,
430 head_hash: String,
431 ) -> Result<Request<String>, http::Error> {
432 let request = Request::builder()
433 .method("POST")
434 .uri(format!(
435 "https://api.github.com/repos/{}/{}/git/refs",
436 self.owner, self.repo
437 ))
438 .body(format!(
439 r#"{{"ref": "refs/heads/{}","sha": "{}" }}"#,
440 name, head_hash
441 ))?;
442 Ok(request)
443 }
444
445 pub async fn create_issue(&self, title: &str, body: &str) -> Result<Issue, OctocrabError> {
446 Ok(self
447 .inner
448 .issues(&self.owner, &self.repo)
449 .create(title)
450 .body(body)
451 .send()
452 .await?)
453 }
454
455 pub async fn close_issue(&self, issue_number: u64) -> Result<Issue, OctocrabError> {
456 Ok(self
457 .inner
458 .issues(&self.owner, &self.repo)
459 .update(issue_number)
460 .state(IssueState::Closed)
461 .send()
462 .await?)
463 }
464
465 pub async fn get_pull_request_by_head(
466 &self,
467 head: &str,
468 ) -> Result<Vec<PullRequest>, OctocrabError> {
469 let mut pull_requests: Page<octocrab::models::pulls::PullRequest> = self
470 .inner
471 .pulls(&self.owner, &self.repo)
472 .list()
473 .state(State::Open)
474 .head(head)
475 .per_page(1)
476 .send()
477 .await?;
478 let pull_requests_vec: Vec<PullRequest> = pull_requests.take_items();
479 Ok(pull_requests_vec)
480 }
481
482 pub async fn close_pull_request(&self, number: u64) -> Result<PullRequest, OctocrabError> {
483 Ok(self
484 .inner
485 .pulls(&self.owner, &self.repo)
486 .update(number)
487 .state(PullState::Closed)
488 .send()
489 .await?)
490 }
491
492 pub async fn create_refill_merge_request(
493 &self,
494 data: CreateRefillMergeRequestData,
495 ) -> Result<(PullRequest, String), OctocrabError> {
496 let CreateRefillMergeRequestData {
497 application_id: _,
498 ref_request,
499 owner_name,
500 file_content,
501 file_name,
502 branch_name,
503 commit,
504 file_sha,
505 } = data;
506 let _create_branch_res = self.create_branch(ref_request).await?;
507 self.update_file_content(&file_name, &commit, &file_content, &branch_name, &file_sha)
508 .await?;
509 let pr = self
510 .create_pull_request(
511 &format!("Datacap for {}", owner_name),
512 &branch_name,
513 &format!("BODY"),
514 )
515 .await?;
516
517 Ok((pr, file_sha))
518 }
519
520 pub async fn create_merge_request(
521 &self,
522 data: CreateMergeRequestData,
523 ) -> Result<(PullRequest, String), OctocrabError> {
524 let CreateMergeRequestData {
525 application_id: _,
526 ref_request,
527 owner_name,
528 file_content,
529 file_name,
530 branch_name,
531 commit,
532 } = data;
533 let _create_branch_res = self.create_branch(ref_request).await?;
534 let add_file_res = self
535 .add_file(&file_name, &file_content, &commit, &branch_name)
536 .await?;
537 let file_sha = add_file_res.content.sha;
538 let pr = self
539 .create_pull_request(
540 &format!("Datacap for {}", owner_name),
541 &branch_name,
542 &format!("BODY"),
543 )
544 .await?;
545
546 Ok((pr, file_sha))
547 }
548
549 pub async fn merge_pull_request(&self, number: u64) -> Result<(), OctocrabError> {
550 let _merge_res = self
551 .inner
552 .pulls(&self.owner, &self.repo)
553 .merge(number)
554 .send()
555 .await?;
556 Ok(())
557 }
558
559 pub async fn get_files(&self, path: &str) -> Result<ContentItems, OctocrabError> {
561 let contents_items = self
562 .inner
563 .repos(&self.owner, &self.repo)
564 .get_content()
565 .path(path)
566 .r#ref("main")
567 .send()
568 .await?;
569
570 Ok(contents_items)
571 }
572
573 pub async fn get_all_files_from_branch(
574 &self,
575 branch: &str,
576 ) -> Result<ContentItems, OctocrabError> {
577 let contents_items = self
578 .inner
579 .repos(&self.owner, &self.repo)
580 .get_content()
581 .r#ref(branch)
582 .send()
583 .await?;
584
585 Ok(contents_items)
586 }
587
588 pub async fn get_last_commit_author(&self, pr_number: u64) -> Result<String, http::Error> {
589 let url = format!(
590 "https://api.github.com/repos/{}/{}/pulls/{}/commits",
591 self.owner, self.repo, pr_number
592 );
593
594 let request = http::request::Builder::new()
595 .method(http::Method::GET)
596 .uri(url);
597
598 let request = self.inner.build_request::<String>(request, None).unwrap();
599
600 let mut response = match self.inner.execute(request).await {
601 Ok(r) => r,
602 Err(e) => {
603 println!("Error fetching last commit author: {:?}", e);
604 return Ok("".to_string());
605 }
606 };
607
608 let response_body = response.body_mut();
609 let body = hyper::body::to_bytes(response_body).await.unwrap();
610 let body_str = String::from_utf8(body.to_vec()).unwrap();
611 let commits: Vec<CommitData> = serde_json::from_str(&body_str).unwrap();
612
613
614 let last_commit: &CommitData = commits.last().unwrap();
615 let author = last_commit.commit.author.name.clone();
616
617 Ok(author)
618 }
619
620 pub async fn get_branch_name_from_pr(&self, pr_number: u64) -> Result<String, OctocrabError> {
621 let pull_request = self
622 .inner
623 .pulls(&self.owner, &self.repo)
624 .get(pr_number)
625 .await?;
626 Ok(pull_request.head.ref_field.clone())
627 }
628
629
630}