1use async_trait::async_trait;
2use eyre::{Context, Result, bail, eyre};
3use octocrab::models::{
4 issues::Issue,
5 pulls::{self, PullRequest},
6};
7use std::path::Path;
8
9use crate::{
10 chapter::{Chapter, ChapterPart},
11 git::{Branch, GitRepo, MergeType},
12 github::{GitProtocol, GithubRepo, find_issue, find_pr},
13 package::QuestPackage,
14 quest::QuestConfig,
15};
16
17pub struct InstanceOutputs {
18 pub origin: GithubRepo,
19 pub origin_git: GitRepo,
20 pub config: QuestConfig,
21}
22
23#[async_trait]
24pub trait QuestSource: Send + Sync + 'static {
25 async fn instantiate(&self, path: &Path, name: &str) -> Result<InstanceOutputs>;
26 fn pull_request(&self, branch: &Branch) -> Option<PullRequest>;
27 async fn pull_request_comments(&self, selector: &PullRequest) -> Result<Vec<pulls::Comment>>;
28 fn issue(&self, label: &str) -> Result<Issue>;
29 fn apply_patch(&self, repo: &GitRepo, from: &Branch, to: &Branch) -> Result<MergeType>;
30 fn provides_refsol(&self) -> bool;
31 fn refsol_url(&self, chapter: &Chapter) -> Option<String>;
32 fn can_skip(&self) -> bool;
33}
34
35#[async_trait]
36impl QuestSource for GithubRepo {
37 async fn instantiate(&self, path: &Path, name: &str) -> Result<InstanceOutputs> {
38 let origin = GithubRepo::instantiate_from_repo(self, name)
39 .await
40 .context("Failed to instantiate Github repo from template")?;
41 let origin_git = origin.clone(path).context("Failed to clone Github repo")?;
42 origin_git
43 .setup_upstream(&self.remote(GitProtocol::Https))
44 .context("Failed to setup upstream")?;
45 let config = QuestConfig::load(&origin_git, Some("upstream"))
46 .context("Failed to load quest config from upstream")?;
47 Ok(InstanceOutputs {
48 origin,
49 origin_git,
50 config,
51 })
52 }
53
54 fn pull_request(&self, branch: &Branch) -> Option<PullRequest> {
55 let pr = self.pr(branch)?;
56 Some(pr.clone())
57 }
58
59 async fn pull_request_comments(&self, pr: &PullRequest) -> Result<Vec<pulls::Comment>> {
60 self.pr_comments(pr).await
61 }
62
63 fn issue(&self, label: &str) -> Result<Issue> {
64 let issue = self
65 .issue(label)
66 .ok_or_else(|| eyre!("Missing issue for label: {label}"))?;
67 Ok((*issue).clone())
68 }
69
70 fn apply_patch(&self, repo: &GitRepo, from: &Branch, to: &Branch) -> Result<MergeType> {
71 let upstream = repo.upstream()?.expect("Repo is missing upstream");
72 repo.cherry_pick_with_fallback(&format!("{upstream}/{from}"), &format!("{upstream}/{to}"))
73 }
74
75 fn provides_refsol(&self) -> bool {
76 true
77 }
78
79 fn refsol_url(&self, chapter: &Chapter) -> Option<String> {
80 self
81 .pr(&chapter.branch(ChapterPart::Solution))
82 .map(|pr| pr.html_url.as_ref().unwrap().to_string())
83 }
84
85 fn can_skip(&self) -> bool {
86 true
87 }
88}
89
90#[async_trait]
91impl QuestSource for QuestPackage {
92 async fn instantiate(&self, path: &Path, name: &str) -> Result<InstanceOutputs> {
93 let origin = GithubRepo::instantiate_from_package(self, name)
94 .await
95 .context("Failed to instantiate repo from package")?;
96 let origin_git = origin.clone(path).context("Failed to clone repo")?;
97 origin_git
98 .write_initial_files(self)
99 .context("Failed to write starter code to new repo")?;
100 let config = self.config.clone();
101 Ok(InstanceOutputs {
102 origin,
103 origin_git,
104 config,
105 })
106 }
107
108 fn pull_request(&self, branch: &Branch) -> Option<PullRequest> {
109 let index = find_pr(branch, self.prs.iter().map(|pr| &pr.data))?;
110 Some(self.prs[index].data.clone())
111 }
112
113 async fn pull_request_comments(&self, pr: &PullRequest) -> Result<Vec<pulls::Comment>> {
114 let full_pr = self
115 .prs
116 .iter()
117 .find(|full_pr| full_pr.data.number == pr.number)
118 .ok_or_else(|| eyre!("Missing comments for PR #{}", pr.number))?;
119 Ok(full_pr.comments.clone())
120 }
121
122 fn issue(&self, label: &str) -> Result<Issue> {
123 let index =
124 find_issue(label, &self.issues).ok_or_else(|| eyre!("Missing issue for label: {label}"))?;
125 Ok(self.issues[index].clone())
126 }
127
128 fn apply_patch(
129 &self,
130 repo: &GitRepo,
131 base_branch: &Branch,
132 target_branch: &Branch,
133 ) -> Result<MergeType> {
134 let Some(patch_index) = self.patch(&(base_branch.clone(), target_branch.clone())) else {
135 bail!("Missing patch in package: {base_branch}..{target_branch}")
136 };
137
138 let patches = self.patches[..=patch_index]
139 .iter()
140 .map(|patch| patch.patch.as_str())
141 .collect::<Vec<_>>();
142
143 repo.apply_patch_with_fallback(&patches)
144 }
145
146 fn provides_refsol(&self) -> bool {
147 false
148 }
149
150 fn refsol_url(&self, _chapter: &Chapter) -> Option<String> {
151 None
152 }
153
154 fn can_skip(&self) -> bool {
155 false
156 }
157}