1use std::collections::HashSet;
2
3use serde::{Deserialize, Serialize};
4
5use super::{Error, Result};
6
7#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
8pub struct Repo {
9 pub url: String,
10 #[serde(skip_serializing_if = "Option::is_none")]
11 pub branch: Option<String>,
12}
13
14#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
15pub struct Change {
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub title: Option<String>,
18 pub url: String,
19 pub branch: String,
20}
21
22#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
23pub struct PR {
24 pub pr: u64,
25}
26
27impl PR {
28 pub async fn to_change(&self, owner: String, repo: String) -> Result<Change> {
29 let mut octo = octocrab::Octocrab::builder();
30 if let Ok(token) = std::env::var("GITHUB_TOKEN") {
31 octo = octo.personal_token(token)
32 }
33 let pr = octo.build()?.pulls(owner, repo).get(self.pr).await?;
34 let mut url = pr
35 .head
36 .repo
37 .ok_or(Error::GithubParseError("Missing repo head".to_string()))?
38 .ssh_url
39 .ok_or(Error::GithubParseError("Missing repo html url".to_string()))?
40 .to_string();
41 if let Some(strip) = url.strip_suffix(".git") {
42 url = strip.to_string();
43 }
44 let branch = pr.head.ref_field;
45 let title = Some(pr.title.unwrap_or(branch.clone()));
46 if let Some(octocrab::models::IssueState::Closed) = pr.state {
47 if let Some(url) = pr.html_url {
48 eprintln!("⚠️ This PR is closed: {}", url.as_str());
49 }
50 }
51 Ok(Change { title, url, branch })
52 }
53}
54
55#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
56#[serde(untagged)]
57pub enum Update {
58 Change(Change),
59 PR(PR),
60}
61
62#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
63pub struct Fork {
64 pub name: String,
65 pub target: Repo,
66 pub upstream: Repo,
67 pub changes: Vec<Update>,
68}
69
70impl Fork {
71 pub fn parse_github(&self) -> Result<(String, String)> {
72 let re = regex::Regex::new(r"github.com[/:]([^/]+)/([^/]+)")?;
73 let caps = re
74 .captures(&self.upstream.url)
75 .ok_or(Error::GithubParseError(self.upstream.url.clone()))?;
76 Ok((caps[1].to_string(), caps[2].to_string()))
77 }
78
79 pub async fn get_prs(&mut self) -> Result<()> {
80 let (owner, repo) = self.parse_github()?;
81 for item in &mut self.changes {
82 if let Update::PR(pr) = item {
83 *item = Update::Change(pr.to_change(owner.clone(), repo.clone()).await?);
84 }
85 }
86 Ok(())
87 }
88
89 pub fn fill(&mut self) {
90 if self.upstream.branch.is_none() {
92 if let Some(branch) = &self.target.branch {
93 self.upstream.branch = Some(branch.clone());
94 }
95 }
96 for item in &mut self.changes {
98 if let Update::Change(change) = item {
99 if let Change {
100 title: None,
101 url: _,
102 branch,
103 } = change
104 {
105 change.title = Some(branch.to_string());
106 }
107 }
108 }
109 }
110}
111
112#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
113pub struct Config {
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub config: Option<Repo>,
116 pub forks: Vec<Fork>,
117}
118
119impl Config {
120 pub async fn update(&mut self) -> Result<()> {
121 for fork in &mut self.forks {
122 fork.get_prs().await?;
123 fork.fill();
124 }
125 Ok(())
126 }
127
128 pub fn remotes(&self) -> String {
129 let mut remotes = HashSet::new();
130 for fork in &self.forks {
131 remotes.insert(fork.target.url.clone());
132 remotes.insert(fork.upstream.url.clone());
133 for update in &fork.changes {
134 if let Update::Change(change) = update {
135 remotes.insert(change.url.clone());
136 }
137 }
138 }
139 let mut vec = remotes.into_iter().collect::<Vec<String>>();
140 vec.sort();
141 vec.join(" ")
142 }
143}