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}