Skip to main content

repo_quest/
source.rs

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}