fork_manager/
config.rs

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        // When upstream branch is not provided, we can use target branch
91        if self.upstream.branch.is_none() {
92            if let Some(branch) = &self.target.branch {
93                self.upstream.branch = Some(branch.clone());
94            }
95        }
96        // when change title is not provided, we can use branch name
97        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}