From f76085549cbb247ef9e2d9830b9f7041a798dbfb Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Wed, 31 Jul 2024 17:34:43 -0700 Subject: [PATCH] Update to latest RQ format --- Cargo.lock | 1 + Cargo.toml | 1 + Makefile.toml | 3 + assets/main.css | 46 +++++- assets/normalize.css | 349 +++++++++++++++++++++++++++++++++++++++++++ src/git_repo.rs | 2 +- src/github_repo.rs | 86 ++++++----- src/main.rs | 170 ++++++++++++--------- src/quest.rs | 139 ++++++++++------- src/stage.rs | 24 +-- 10 files changed, 646 insertions(+), 175 deletions(-) create mode 100644 assets/normalize.css diff --git a/Cargo.lock b/Cargo.lock index b603347..56e0ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3146,6 +3146,7 @@ dependencies = [ "futures-util", "http 1.1.0", "octocrab", + "parking_lot", "regex", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index b44cade..b167e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ dioxus-logger = "0.5.1" futures-util = "0.3.30" http = "1.1.0" octocrab = "0.38.0" +parking_lot = "0.12.3" regex = "1.10.5" serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.120" diff --git a/Makefile.toml b/Makefile.toml index dd8fcbb..9f35ecd 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -2,5 +2,8 @@ skip_core_tasks = true default_to_workspace = false +[tasks.bundle] +script = "dx bundle" + [tasks.watch] script = "cargo watch -s 'dx build'" \ No newline at end of file diff --git a/assets/main.css b/assets/main.css index 347c3fd..f9425c9 100644 --- a/assets/main.css +++ b/assets/main.css @@ -1,3 +1,47 @@ +@import url('./normalize.css'); +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap'); + +:root { + --prose-font: "Open Sans", Arial, sans-serif; + --code-font: "Source Code Pro", monospace; +} + html { - font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-family: var(--prose-font); + font-size: 18px; +} + +code, pre { + font-family: var(--code-font); +} + +#app { + padding: 1rem; + position: relative; +} + +#refresh { + position: absolute; + top: 1rem; + right: 1rem; +} + +h1 { + margin-top: 0; +} + +.stages li { + margin-bottom: 1rem; +} + +.stage-title { + font-weight: bold; +} + +.separator { + margin: 0 0.5rem; +} + +.status { + font-style: italic; } \ No newline at end of file diff --git a/assets/normalize.css b/assets/normalize.css new file mode 100644 index 0000000..192eb9c --- /dev/null +++ b/assets/normalize.css @@ -0,0 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/src/git_repo.rs b/src/git_repo.rs index b445c5c..39f8bdc 100644 --- a/src/git_repo.rs +++ b/src/git_repo.rs @@ -52,7 +52,7 @@ impl GitRepo { .context("Failed to clone") } - pub fn initialize(&self, upstream: &GithubRepo) -> Result<()> { + pub fn setup_upstream(&self, upstream: &GithubRepo) -> Result<()> { git(|cmd| { cmd.args(["remote", "add", UPSTREAM, &upstream.remote()]); }) diff --git a/src/github_repo.rs b/src/github_repo.rs index 1c98f91..4036453 100644 --- a/src/github_repo.rs +++ b/src/github_repo.rs @@ -9,22 +9,22 @@ use octocrab::{ issues::Issue, pulls::{self, PullRequest}, repos::Branch, - IssueState, }, pulls::PullRequestHandler, repos::RepoHandler, GitHubError, Octocrab, }; +use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; use serde_json::json; use std::{sync::Arc, time::Duration}; -use tokio::{sync::OnceCell, time::timeout}; +use tokio::{time::timeout, try_join}; pub struct GithubRepo { user: String, name: String, gh: Arc, - prs: OnceCell>, - issues: OnceCell>, + prs: Mutex>>, + issues: Mutex>>, } impl GithubRepo { @@ -33,11 +33,37 @@ impl GithubRepo { user: user.to_string(), name: name.to_string(), gh: octocrab::instance(), - prs: OnceCell::new(), - issues: OnceCell::new(), + prs: Mutex::new(None), + issues: Mutex::new(None), } } + pub async fn fetch(&self) -> Result<()> { + let (pr_handler, issue_handler) = (self.pr_handler(), self.issue_handler()); + let res = try_join!( + pr_handler.list().state(octocrab::params::State::All).send(), + issue_handler + .list() + .state(octocrab::params::State::All) + .send() + ); + let (mut pr_page, mut issue_page) = match res { + Ok(pages) => pages, + Err(octocrab::Error::GitHub { + source: GitHubError { + status_code: StatusCode::NOT_FOUND, + .. + }, + .. + }) => return Ok(()), + Err(e) => return Err(e.into()), + }; + let (prs, issues) = (pr_page.take_items(), issue_page.take_items()); + *self.prs.lock() = Some(prs); + *self.issues.lock() = Some(issues); + Ok(()) + } + pub fn remote(&self) -> String { format!("git@github.com:{}/{}.git", self.user, self.name) } @@ -66,10 +92,9 @@ impl GithubRepo { .send() .await?; + // There is some unknown delay between creating a repo from a template and its contents being added. + // We have to wait until that happens. { - // There is some unknown delay between creating a repo from a template and its contents being added. - // We have to wait until that happens. - const RETRY_INTERVAL: u64 = 500; const RETRY_TIMEOUT: u64 = 5000; @@ -85,6 +110,7 @@ impl GithubRepo { .context("Repo is still empty after timeout")?; } + // Unsubscribe from repo notifications to avoid annoying emails. { let route = format!("/repos/{}/{}/subscription", self.user, self.name); let _response = self @@ -100,6 +126,7 @@ impl GithubRepo { .context("Failed to unsubscribe from repo")?; } + // Copy all issue labels. { let mut page = base.issue_handler().list_labels_for_repo().send().await?; let labels = page.take_items(); @@ -137,43 +164,30 @@ impl GithubRepo { self.gh.pulls(&self.user, &self.name) } - pub async fn prs(&self) -> &[PullRequest] { - self - .prs - .get_or_init(|| async { - let pages = self.pr_handler().list().send().await.unwrap(); - pages.into_iter().collect::>() - }) - .await + pub fn prs(&self) -> MappedMutexGuard<'_, Vec> { + MutexGuard::map(self.prs.lock(), |opt| opt.as_mut().unwrap()) } - pub async fn pr(&self, ref_field: &str) -> Option<&PullRequest> { - let prs = self.prs().await; - prs - .iter() - .find(|pr| !matches!(pr.state, Some(IssueState::Closed)) && pr.head.ref_field == ref_field) + pub fn pr(&self, ref_field: &str) -> Option> { + let prs = self.prs(); + let idx = prs.iter().position(|pr| pr.head.ref_field == ref_field)?; + Some(MappedMutexGuard::map(prs, |prs| &mut prs[idx])) } pub fn issue_handler(&self) -> IssueHandler { self.gh.issues(&self.user, &self.name) } - pub async fn issues(&self) -> &[Issue] { - self - .issues - .get_or_init(|| async { - let pages = self.issue_handler().list().send().await.unwrap(); - pages.into_iter().collect::>() - }) - .await + pub fn issues(&self) -> MappedMutexGuard<'_, Vec> { + MutexGuard::map(self.issues.lock(), |opt| opt.as_mut().unwrap()) } - pub async fn issue(&self, label_name: &str) -> Option<&Issue> { - let issues = self.issues().await; - issues.iter().find(|issue| { - !matches!(issue.state, IssueState::Closed) - && issue.labels.iter().any(|label| label.name == label_name) - }) + pub fn issue(&self, label_name: &str) -> Option> { + let issues = self.issues(); + let idx = issues + .iter() + .position(|issue| issue.labels.iter().any(|label| label.name == label_name))?; + Some(MappedMutexGuard::map(issues, |issues| &mut issues[idx])) } pub async fn copy_pr(&self, base: &GithubRepo, base_pr: &PullRequest, head: &str) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 042e0f1..c44e3ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ #![allow(non_snake_case)] use anyhow::{Context, Result}; -use dioxus::prelude::*; +use dioxus::{ + desktop::{Config, LogicalSize, WindowBuilder}, + prelude::*, +}; use futures_util::FutureExt; use octocrab::Octocrab; use quest::{Quest, QuestState}; -use stage::StagePart; +use stage::{StagePart, StagePartStatus}; use std::{ops::Deref, process::Command, rc::Rc, sync::Arc}; use tracing::Level; @@ -68,10 +71,12 @@ fn QuestView(quest: QuestRef) -> Element { } h1 { + "RepoQuest: " {quest.config.title.clone()} } button { + id: "refresh", onclick: move |_| { let quest_ref = quest.clone(); tokio::spawn(async move { @@ -82,49 +87,81 @@ fn QuestView(quest: QuestRef) -> Element { } ol { + class: "stages", for stage in 0 ..= cur_stage { li { - div { {quest.stages[stage].config.name.clone()} } - if stage == cur_stage { - div { - button { - disabled: loading || state.status.is_ongoing(), - onclick: { - let quest_ref = quest.clone(); - move |_| { - let quest_ref = quest_ref.clone(); - loading_signal.set(true); - tokio::spawn(async move { - let res = match state.part { - StagePart::Feature => quest_ref.file_feature_and_issue(cur_stage).boxed(), - StagePart::Test => quest_ref.file_tests(cur_stage).boxed(), - StagePart::Solution => quest_ref.file_solution(cur_stage).boxed(), - }.await; - if let Err(e) = res { - error_signal.set(Some(e)); - } - loading_signal.set(false); - }); - } - }, - {match state.part { - StagePart::Feature => "File issue & features", - StagePart::Test => "File tests", - StagePart::Solution => "Give solution" - }} + div { + span { + class: "stage-title", + {quest.stages[stage].config.name.clone()} + } + span { + class: "separator", + "ยท" + } + + if stage == cur_stage { + if state.status.is_start() { + button { + disabled: loading, + onclick: { + let quest_ref = quest.clone(); + move |_| { + let quest_ref = quest_ref.clone(); + loading_signal.set(true); + tokio::spawn(async move { + let res = match state.part { + StagePart::Starter => quest_ref.file_feature_and_issue(cur_stage).boxed(), + StagePart::Solution => quest_ref.file_solution(cur_stage).boxed(), + }.await; + if let Err(e) = res { + error_signal.set(Some(e)); + } + loading_signal.set(false); + }); + } + }, + {match state.part { + StagePart::Starter => if quest.stages[stage].config.no_starter() { + "File issue" + } else { + "File issue & starter PR" + }, + StagePart::Solution => "Give solution" + }} + } + } else { + span { + class: "status", + {match state.part { + StagePart::Starter if !quest.stages[stage].config.no_starter() => "Waiting for you to merge starter PR", + _ => "Waiting for you to solve & close issue" + }} + } } if loading { div { "Operation running..." } } + } else { + span { + class: "status", + "Completed" + } + } + } - if state.status.is_ongoing() { - div { - {match state.part { - StagePart::Feature => "Merge PR before continuing", - StagePart::Test => "Merge PR before continuing", - StagePart::Solution => "File and merge your own PR and close the issue before continuing" - }} + div { + if (state.stage.idx, state.part, state.status) > (stage, StagePart::Starter, StagePartStatus::Start) { + a { + href: quest.issue_url(stage), + "Issue" + } + if !quest.stages[stage].config.no_starter() { + ", " + a { + href: quest.feature_pr_url(stage), + "Starter PR" } } } @@ -228,41 +265,34 @@ fn App() -> Element { rsx! { link { rel: "stylesheet", href: "main.css" } - {match &*init_res { - Ok(()) => rsx!{ QuestLoader { } }, - Err(e) => rsx!{ - div { "Failed to load Github API. Full error:" } - pre { "{e:?}" } - }, - }} + div { + id: "app", + {match &*init_res { + Ok(()) => rsx!{ QuestLoader { } }, + Err(e) => rsx!{ + div { "Failed to load Github API. Full error:" } + pre { "{e:?}" } + }, + }} + } } } fn main() { - dioxus_logger::init(Level::DEBUG).expect("failed to init logger"); - dioxus::launch(App); + let level = if cfg!(debug_assertions) { + Level::DEBUG + } else { + Level::INFO + }; + dioxus_logger::init(level).expect("failed to init logger"); + LaunchBuilder::desktop() + .with_cfg( + Config::new().with_window( + WindowBuilder::new() + .with_title("RepoQuest") + .with_always_on_top(false) + .with_inner_size(LogicalSize::new(800, 500)), + ), + ) + .launch(App); } - -// #[tokio::main] -// async fn main() -> Result<()> { -// let step = std::env::args().nth(1).unwrap().parse::().unwrap(); - -// let -// let stages = [Stage::new(1, "async-await"), Stage::new(2, "spawn")]; - -// match step { -// 1 => quest.create_repo().await?, -// 2 => quest.init_repo()?, -// 3 => quest.file_feature_and_issue(&stages[0], None).await?, -// 4 => quest.file_tests(&stages[0]).await?, -// 5 => { -// quest -// .file_feature_and_issue(&stages[1], Some(&stages[0])) -// .await? -// } -// 6 => quest.file_tests(&stages[1]).await?, -// _ => todo!(), -// } - -// Ok(()) -// } diff --git a/src/quest.rs b/src/quest.rs index b7aef97..d752f25 100644 --- a/src/quest.rs +++ b/src/quest.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, env, fs, path::Path, process::Command, time::Duration}; +use std::{collections::HashMap, env, process::Command, time::Duration}; use crate::{ git_repo::GitRepo, @@ -7,7 +7,6 @@ use crate::{ }; use anyhow::{ensure, Context, Result}; use dioxus::signals::{SyncSignal, Writable}; -use futures_util::future::try_join; use http::StatusCode; use octocrab::{ models::{pulls::PullRequest, IssueState}, @@ -16,7 +15,7 @@ use octocrab::{ }; use regex::Regex; use serde::{Deserialize, Serialize}; -use tokio::time::sleep; +use tokio::{time::sleep, try_join}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct QuestConfig { @@ -47,7 +46,7 @@ pub struct Quest { pub fn load_config_from_current_dir() -> Result { let output = Command::new("git") - .args(["rev-parse", "--show-toplevel"]) + .args(["show", "upstream/meta:rqst.toml"]) .output() .context("git failed")?; ensure!( @@ -55,11 +54,7 @@ pub fn load_config_from_current_dir() -> Result { "git exited with non-zero status code" ); let stdout = String::from_utf8(output.stdout)?.trim().to_string(); - - let config_path = Path::new(&stdout).join(".rqst.toml"); - let config_contents = fs::read_to_string(&config_path) - .with_context(|| format!("Failed to read config: {}", config_path.display()))?; - let config = toml::de::from_str::(&config_contents)?; + let config = toml::de::from_str::(&stdout)?; Ok(config) } @@ -68,8 +63,8 @@ pub async fn load_config_from_remote(owner: &str, repo: &str) -> Result result, Err(octocrab::Error::GitHub { source: GitHubError { @@ -159,7 +157,7 @@ impl Quest { }) => { return Ok(QuestState { stage: self.stages[0].clone(), - part: StagePart::Feature, + part: StagePart::Starter, status: StagePartStatus::Start, }) } @@ -169,30 +167,49 @@ impl Quest { let prs = pr_page.take_items(); let issues = issue_page.take_items(); - let Some((stage, part, finished)) = prs - .iter() - .filter_map(|pr| { - let (stage, part) = self.parse_stage(pr)?; - let finished = pr.merged_at.is_some() - && match part { - StagePart::Solution => { - let issue = issues.iter().find(|issue| { - issue - .labels - .iter() - .any(|label| label.name == stage.config.label) - })?; - matches!(issue.state, IssueState::Closed) - } - StagePart::Feature | StagePart::Test => true, - }; - Some((stage, part, finished)) + let issue_map = issues + .into_iter() + .filter_map(|issue| { + let label = issue.labels.first()?; + Some((label.name.clone(), issue)) }) - .max_by_key(|(stage, part, _)| (stage.idx, *part)) + .collect::>(); + + let stage_map = self + .stages + .iter() + .map(|stage| (stage.config.label.clone(), stage)) + .collect::>(); + + let pr_stages = prs.iter().filter_map(|pr| { + let (stage, part) = self.parse_stage(pr)?; + let finished = pr.merged_at.is_some() + && match part { + StagePart::Solution => { + let issue = issue_map.get(&stage.config.label)?; + matches!(issue.state, IssueState::Closed) + } + StagePart::Starter => true, + }; + Some((stage, part, finished)) + }); + + let issue_stages = issue_map.keys().filter_map(|label| { + let stage = stage_map.get(label)?; + Some(( + (*stage).clone(), + StagePart::Starter, + stage.config.no_starter(), + )) + }); + + let Some((stage, part, finished)) = pr_stages + .chain(issue_stages) + .max_by_key(|(stage, part, finished)| (stage.idx, *part, *finished)) else { return Ok(QuestState { stage: self.stages[0].clone(), - part: StagePart::Feature, + part: StagePart::Starter, status: StagePartStatus::Start, }); }; @@ -206,7 +223,7 @@ impl Quest { }, None => QuestState { stage: self.stages[stage.idx + 1].clone(), - part: StagePart::Feature, + part: StagePart::Starter, status: StagePartStatus::Start, }, } @@ -220,7 +237,7 @@ impl Quest { } pub async fn infer_state_update(&self) -> Result<()> { - let new_state = self.infer_state().await?; + let (new_state, _) = try_join!(self.infer_state(), self.origin.fetch())?; let mut state_signal = self.state_signal; state_signal.set(Some(new_state)); Ok(()) @@ -239,10 +256,18 @@ impl Quest { } pub async fn create_repo(&self) -> Result<()> { + // First instantiate the user's repo from the template repo on the server side self.origin.copy_from(&self.upstream).await?; + + // Then clone from server side to client side self.clone_repo()?; + + // Move into the repo env::set_current_dir(&self.config.repo)?; - self.origin_git.initialize(&self.upstream)?; + + // Initialize the upstreams and fetch content + self.origin_git.setup_upstream(&self.upstream)?; + Ok(()) } @@ -255,8 +280,8 @@ impl Quest { let head = self.origin_git.head_commit()?; - let pr = self.upstream.pr(target_branch).await.unwrap(); - self.origin.copy_pr(&self.upstream, pr, &head).await?; + let pr = self.upstream.pr(target_branch).unwrap().clone(); + self.origin.copy_pr(&self.upstream, &pr, &head).await?; Ok(()) } @@ -270,24 +295,26 @@ impl Quest { "main".into() }; - self - .file_pr(&stage.branch_name(StagePart::Feature), &base_branch) - .await?; + if !stage.config.no_starter() { + self + .file_pr(&stage.branch_name(StagePart::Starter), &base_branch) + .await?; + } - let issue = self.upstream.issue(&stage.config.label).await.unwrap(); - self.origin.copy_issue(issue).await?; + let issue = self.upstream.issue(&stage.config.label).unwrap().clone(); + self.origin.copy_issue(&issue).await?; self.infer_state_update().await?; Ok(()) } - pub async fn file_tests(&self, stage_index: usize) -> Result<()> { + pub async fn file_solution(&self, stage_index: usize) -> Result<()> { let stage = &self.stages[stage_index]; self .file_pr( - &stage.branch_name(StagePart::Test), - &stage.branch_name(StagePart::Feature), + &stage.branch_name(StagePart::Solution), + &stage.branch_name(StagePart::Starter), ) .await?; @@ -296,17 +323,15 @@ impl Quest { Ok(()) } - pub async fn file_solution(&self, stage_index: usize) -> Result<()> { + pub fn issue_url(&self, stage_index: usize) -> Option { let stage = &self.stages[stage_index]; - self - .file_pr( - &stage.branch_name(StagePart::Solution), - &stage.branch_name(StagePart::Test), - ) - .await?; - - self.infer_state_update().await?; + let issue = self.origin.issue(&stage.config.label)?; + Some(issue.html_url.to_string()) + } - Ok(()) + pub fn feature_pr_url(&self, stage_index: usize) -> Option { + let stage = &self.stages[stage_index]; + let pr = self.origin.pr(&stage.branch_name(StagePart::Starter))?; + Some(pr.html_url.as_ref().unwrap().to_string()) } } diff --git a/src/stage.rs b/src/stage.rs index 7e8a399..f3ce999 100644 --- a/src/stage.rs +++ b/src/stage.rs @@ -5,9 +5,17 @@ use std::fmt; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub struct StageConfig { pub label: String, pub name: String, + no_starter: Option, +} + +impl StageConfig { + pub fn no_starter(&self) -> bool { + self.no_starter.unwrap_or(false) + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -18,25 +26,22 @@ pub struct Stage { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum StagePart { - Feature, - Test, + Starter, Solution, } impl StagePart { pub fn next_part(self) -> Option { match self { - StagePart::Feature => Some(StagePart::Test), - StagePart::Test => Some(StagePart::Solution), + StagePart::Starter => Some(StagePart::Solution), StagePart::Solution => None, } } pub fn parse(s: &str) -> Option { match s { - "a" => Some(StagePart::Feature), - "b" => Some(StagePart::Test), - "c" => Some(StagePart::Solution), + "a" => Some(StagePart::Starter), + "b" => Some(StagePart::Solution), _ => None, } } @@ -48,9 +53,8 @@ impl fmt::Display for StagePart { f, "{}", match self { - StagePart::Feature => "a", - StagePart::Test => "b", - StagePart::Solution => "c", + StagePart::Starter => "a", + StagePart::Solution => "b", } ) }