use async_trait::async_trait;
use eyre::{Context, Result, eyre};
use octocrab::models::{
issues::Issue,
pulls::{self, PullRequest},
};
use std::path::Path;
use crate::{
git::{GitRepo, MergeType},
github::{GithubRepo, PullSelector, find_issue, find_pr},
package::QuestPackage,
quest::QuestConfig,
stage::{Stage, StagePart},
};
pub struct InstanceOutputs {
pub origin: GithubRepo,
pub origin_git: GitRepo,
pub config: QuestConfig,
}
#[async_trait]
pub trait QuestTemplate: Send + Sync + 'static {
async fn instantiate(&self, path: &Path) -> Result<InstanceOutputs>;
fn pull_request(&self, selector: &PullSelector) -> Result<PullRequest>;
async fn pull_request_comments(&self, selector: &PullRequest) -> Result<Vec<pulls::Comment>>;
fn issue(&self, label: &str) -> Result<Issue>;
fn apply_patch(
&self,
repo: &GitRepo,
base_branch: &str,
target_branch: &str,
) -> Result<MergeType>;
fn reference_solution_pr_url(&self, stage: &Stage) -> Option<String>;
fn can_skip(&self) -> bool;
}
pub struct RepoTemplate(pub GithubRepo);
#[async_trait]
impl QuestTemplate for RepoTemplate {
async fn instantiate(&self, path: &Path) -> Result<InstanceOutputs> {
let origin = GithubRepo::instantiate_from_repo(&self.0)
.await
.context("Failed to instantiate Github repo from template")?;
let origin_git = origin.clone(path).context("Failed to clone Github repo")?;
origin_git
.setup_upstream(&self.0)
.context("Failed to setup upstream")?;
let config = QuestConfig::load(&origin_git, Some("upstream"))
.context("Failed to load quest config from upstream")?;
Ok(InstanceOutputs {
origin,
origin_git,
config,
})
}
fn pull_request(&self, selector: &PullSelector) -> Result<PullRequest> {
let pr = self.0.pr(selector).ok_or(eyre!("Missing PR"))?;
Ok(pr.clone())
}
async fn pull_request_comments(&self, pr: &PullRequest) -> Result<Vec<pulls::Comment>> {
self.0.pr_comments(pr).await
}
fn issue(&self, label: &str) -> Result<Issue> {
let issue = self
.0
.issue(label)
.ok_or_else(|| eyre!("Missing issue for label: {label}"))?;
Ok((*issue).clone())
}
fn apply_patch(
&self,
repo: &GitRepo,
base_branch: &str,
target_branch: &str,
) -> Result<MergeType> {
repo.cherry_pick(base_branch, target_branch)
}
fn reference_solution_pr_url(&self, stage: &Stage) -> Option<String> {
self
.0
.pr(&PullSelector::Branch(
stage.branch_name(StagePart::Solution),
))
.map(|pr| pr.html_url.as_ref().unwrap().to_string())
}
fn can_skip(&self) -> bool {
true
}
}
pub struct PackageTemplate(pub QuestPackage);
#[async_trait]
impl QuestTemplate for PackageTemplate {
async fn instantiate(&self, path: &Path) -> Result<InstanceOutputs> {
let origin = GithubRepo::instantiate_from_package(&self.0)
.await
.context("Failed to instantiate repo from package")?;
let origin_git = origin.clone(path).context("Failed to clone repo")?;
origin_git
.write_initial_files(&self.0)
.context("Failed to write starter code to new repo")?;
let config = self.0.config.clone();
Ok(InstanceOutputs {
origin,
origin_git,
config,
})
}
fn pull_request(&self, selector: &PullSelector) -> Result<PullRequest> {
let index = find_pr(selector, self.0.prs.iter().map(|pr| &pr.data))
.ok_or_else(|| eyre!("Missing PR for selector: {selector:?}"))?;
Ok(self.0.prs[index].data.clone())
}
async fn pull_request_comments(&self, pr: &PullRequest) -> Result<Vec<pulls::Comment>> {
let full_pr = self
.0
.prs
.iter()
.find(|full_pr| full_pr.data.number == pr.number)
.ok_or_else(|| eyre!("Missing comments for PR #{}", pr.number))?;
Ok(full_pr.comments.clone())
}
fn issue(&self, label: &str) -> Result<Issue> {
let index =
find_issue(label, &self.0.issues).ok_or_else(|| eyre!("Missing issue for label: {label}"))?;
Ok(self.0.issues[index].clone())
}
fn apply_patch(
&self,
repo: &GitRepo,
base_branch: &str,
target_branch: &str,
) -> Result<MergeType> {
let patch_index = self
.0
.patch(&(base_branch.to_string(), target_branch.to_string()))
.ok_or_else(|| eyre!("Missing patch in package: {base_branch}..{target_branch}"))?;
let patches = self.0.patches[..=patch_index]
.iter()
.map(|patch| patch.patch.as_str())
.collect::<Vec<_>>();
repo.apply_patch(&patches)
}
fn reference_solution_pr_url(&self, _stage: &Stage) -> Option<String> {
None
}
fn can_skip(&self) -> bool {
false
}
}