Skip to main content

rq_core/
package.rs

1use std::{
2  collections::HashMap,
3  fs::File,
4  io::{BufReader, BufWriter, Read},
5  path::{Path, PathBuf},
6};
7
8use crate::{git::GitRepo, github::GithubRepo, quest::QuestConfig, stage::StagePart};
9use eyre::{Context, Result};
10use flate2::{Compression, read::GzDecoder, write::GzEncoder};
11use futures_util::future::try_join_all;
12use octocrab::models::{
13  Label,
14  issues::Issue,
15  pulls::{self, PullRequest},
16};
17use semver::{Version, VersionReq};
18use serde::{Deserialize, Serialize};
19
20#[derive(Clone, Serialize, Deserialize)]
21pub struct FullPullRequest {
22  pub data: PullRequest,
23  pub comments: Vec<pulls::Comment>,
24}
25
26#[derive(Serialize, Deserialize)]
27pub struct Patch {
28  pub base: String,
29  pub head: String,
30  pub patch: String,
31}
32
33#[derive(Serialize, Deserialize)]
34pub struct QuestPackage {
35  pub version: Version,
36  pub config: QuestConfig,
37  pub issues: Vec<Issue>,
38  pub prs: Vec<FullPullRequest>,
39  pub initial: HashMap<PathBuf, String>,
40  pub patches: Vec<Patch>,
41  #[serde(skip)]
42  patch_map: HashMap<(String, String), usize>,
43  pub labels: Vec<Label>,
44}
45
46fn version() -> Version {
47  Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
48}
49
50impl QuestPackage {
51  pub async fn build(path: &Path) -> Result<Self> {
52    let git_repo = GitRepo::new(path);
53    let config = QuestConfig::load(&git_repo, None)?;
54    let gh_repo = GithubRepo::load(&config.author, &config.repo).await?;
55
56    let initial = git_repo.read_initial_files()?;
57    let issues = gh_repo.issues().clone();
58    let prs = try_join_all(gh_repo.prs().iter().map(async |pr| {
59      let comments = gh_repo.pr_comments(pr).await?;
60      Ok::<_, eyre::Error>(FullPullRequest {
61        data: pr.clone(),
62        comments,
63      })
64    }))
65    .await?;
66    let labels = gh_repo
67      .issue_handler()
68      .list_labels_for_repo()
69      .send()
70      .await?
71      .take_items();
72    let patches = config
73      .stages
74      .iter()
75      .enumerate()
76      .filter(|(_, stage)| !matches!(stage.no_starter, Some(true)))
77      .map(|(i, stage)| {
78        let prev_stage = (i > 0).then(|| &config.stages[i - 1]);
79        let base = match prev_stage {
80          Some(stage) => stage.branch_name(StagePart::Solution),
81          None => "main".into(),
82        };
83        let head = stage.branch_name(StagePart::Starter);
84        let patch = git_repo.diff(&base, &head)?;
85        Ok(Patch { base, head, patch })
86      })
87      .collect::<Result<Vec<_>>>()?;
88
89    Ok(QuestPackage {
90      version: version(),
91      config,
92      initial,
93      issues,
94      prs,
95      labels,
96      patches,
97      patch_map: HashMap::default(),
98    })
99  }
100
101  pub fn patch(&self, key: &(String, String)) -> Option<usize> {
102    self.patch_map.get(key).copied()
103  }
104
105  fn deserialize<T: Read>(t: T) -> Result<Self> {
106    let mut decoder = GzDecoder::new(t);
107    let mut package: QuestPackage =
108      serde_json::from_reader(&mut decoder).context("Failed to parse JSON")?;
109    package.patch_map = package
110      .patches
111      .iter()
112      .enumerate()
113      .map(|(i, patch)| ((patch.base.clone(), patch.head.clone()), i))
114      .collect();
115    let version = version();
116    let req = VersionReq::parse(&format!("^{version}")).unwrap();
117    if !req.matches(&package.version) {
118      tracing::warn!("Loaded package has potentially incompatible version: {version}");
119    }
120    Ok(package)
121  }
122
123  pub fn load_from_file(path: &Path) -> Result<Self> {
124    let mut f = BufReader::new(File::open(path)?);
125    Self::deserialize(&mut f)
126      .with_context(|| format!("Failed to load quest package: {}", path.display()))
127  }
128
129  pub fn load_from_blob(blob: &[u8]) -> Result<Self> {
130    Self::deserialize(blob).context("Failed to load quest package from blob")
131  }
132
133  pub fn save(&self, path: &Path) -> Result<()> {
134    let mut f = BufWriter::new(File::create(path)?);
135    let mut encoder = GzEncoder::new(&mut f, Compression::best());
136    serde_json::to_writer_pretty(&mut encoder, self)?;
137    Ok(())
138  }
139}