diff --git a/crates/zeta_cli/src/example.rs b/crates/zeta_cli/src/example.rs index f127a3ceff99b1b45a7a0b91573d321e82186c87..5d37276308d06f6e559525250211addffb9ddc93 100644 --- a/crates/zeta_cli/src/example.rs +++ b/crates/zeta_cli/src/example.rs @@ -1,13 +1,16 @@ use std::{ + borrow::Cow, + env, fmt::{self, Display}, + fs, io::Write, mem, - os::unix::ffi::OsStrExt, path::{Path, PathBuf}, }; -use anyhow::Result; +use anyhow::{Context as _, Result}; use clap::ValueEnum; +use gpui::http_client::Url; use pulldown_cmark::CowStr; use serde::{Deserialize, Serialize}; @@ -18,23 +21,24 @@ const EXPECTED_EXCERPTS_HEADING: &str = "Expected Excerpts"; const REPOSITORY_URL_FIELD: &str = "repository_url"; const REVISION_FIELD: &str = "revision"; +#[derive(Debug)] pub struct NamedExample { - name: String, - example: Example, + pub name: String, + pub example: Example, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct Example { - repository_url: String, - commit: String, - cursor_path: PathBuf, - cursor_position: String, - edit_history: Vec, - expected_patch: String, - expected_excerpts: Vec, + pub repository_url: String, + pub revision: String, + pub cursor_path: PathBuf, + pub cursor_position: String, + pub edit_history: Vec, + pub expected_patch: String, + pub expected_excerpts: Vec, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ExpectedExcerpt { path: PathBuf, text: String, @@ -53,16 +57,16 @@ impl NamedExample { let content = std::fs::read_to_string(path)?; let ext = path.extension(); - match ext.map(|s| s.as_bytes()) { - Some(b"json") => Ok(Self { + match ext.and_then(|s| s.to_str()) { + Some("json") => Ok(Self { name: path.file_name().unwrap_or_default().display().to_string(), example: serde_json::from_str(&content)?, }), - Some(b"toml") => Ok(Self { + Some("toml") => Ok(Self { name: path.file_name().unwrap_or_default().display().to_string(), example: toml::from_str(&content)?, }), - Some(b"md") => Self::parse_md(&content), + Some("md") => Self::parse_md(&content), Some(_) => { anyhow::bail!("Unrecognized example extension: {}", ext.unwrap().display()); } @@ -83,7 +87,7 @@ impl NamedExample { name: String::new(), example: Example { repository_url: String::new(), - commit: String::new(), + revision: String::new(), cursor_path: PathBuf::new(), cursor_position: String::new(), edit_history: Vec::new(), @@ -106,12 +110,12 @@ impl NamedExample { // in h1 section && let Some((field, value)) = line.split_once('=') { - match field { + match field.trim() { REPOSITORY_URL_FIELD => { - named.example.repository_url = value.to_string(); + named.example.repository_url = value.trim().to_string(); } REVISION_FIELD => { - named.example.commit = value.to_string(); + named.example.revision = value.trim().to_string(); } _ => { eprintln!("Warning: Unrecognized field `{field}`"); @@ -188,6 +192,111 @@ impl NamedExample { ExampleFormat::Md => Ok(write!(out, "{}", self)?), } } + + pub async fn setup_worktree(&self) -> Result { + let worktrees_dir = env::current_dir()?.join("target").join("zeta-worktrees"); + let repos_dir = env::current_dir()?.join("target").join("zeta-repos"); + fs::create_dir_all(&repos_dir)?; + fs::create_dir_all(&worktrees_dir)?; + + let (repo_owner, repo_name) = self.repo_name()?; + + let repo_dir = repos_dir.join(repo_owner.as_ref()).join(repo_name.as_ref()); + dbg!(&repo_dir); + if !repo_dir.is_dir() { + fs::create_dir_all(&repo_dir)?; + run_git(&repo_dir, &["init"]).await?; + run_git( + &repo_dir, + &["remote", "add", "origin", &self.example.repository_url], + ) + .await?; + } + + run_git( + &repo_dir, + &["fetch", "--depth", "1", "origin", &self.example.revision], + ) + .await?; + + let worktree_path = worktrees_dir.join(&self.name); + + dbg!(&worktree_path); + + if worktree_path.is_dir() { + run_git(&worktree_path, &["clean", "--force", "-d"]).await?; + run_git(&worktree_path, &["reset", "--hard", "HEAD"]).await?; + run_git(&worktree_path, &["checkout", &self.example.revision]).await?; + } else { + let worktree_path_string = worktree_path.to_string_lossy(); + run_git( + &repo_dir, + &[ + "worktree", + "add", + "-f", + &worktree_path_string, + &self.example.revision, + ], + ) + .await?; + } + + Ok(worktree_path) + } + + fn repo_name(&self) -> Result<(Cow, Cow)> { + // git@github.com:owner/repo.git + if self.example.repository_url.contains('@') { + let (owner, repo) = self + .example + .repository_url + .split_once(':') + .context("expected : in git url")? + .1 + .split_once('/') + .context("expected / in git url")?; + Ok(( + Cow::Borrowed(owner), + Cow::Borrowed(repo.trim_end_matches(".git")), + )) + // http://github.com/owner/repo.git + } else { + let url = Url::parse(&self.example.repository_url)?; + let mut segments = url.path_segments().context("empty http url")?; + let owner = segments + .next() + .context("expected owner path segment")? + .to_string(); + let repo = segments + .next() + .context("expected repo path segment")? + .trim_end_matches(".git") + .to_string(); + assert!(segments.next().is_none()); + + Ok((owner.into(), repo.into())) + } + } +} + +async fn run_git(repo_path: &Path, args: &[&str]) -> Result { + let output = smol::process::Command::new("git") + .current_dir(repo_path) + .args(args) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "`git {}` within `{}` failed with status: {}\nstderr:\n{}\nstdout:\n{}", + args.join(" "), + repo_path.display(), + output.status, + String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stdout), + ); + Ok(String::from_utf8(output.stdout)?.trim().to_string()) } impl Display for NamedExample { @@ -198,7 +307,7 @@ impl Display for NamedExample { "{REPOSITORY_URL_FIELD} = {}\n", self.example.repository_url )?; - write!(f, "{REVISION_FIELD} = {}\n\n", self.example.commit)?; + write!(f, "{REVISION_FIELD} = {}\n\n", self.example.revision)?; write!( f,