Fix extension match

Agus Zubiaga created

Change summary

crates/zeta_cli/src/example.rs | 153 ++++++++++++++++++++++++++++++-----
1 file changed, 131 insertions(+), 22 deletions(-)

Detailed changes

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<String>,
-    expected_patch: String,
-    expected_excerpts: Vec<ExpectedExcerpt>,
+    pub repository_url: String,
+    pub revision: String,
+    pub cursor_path: PathBuf,
+    pub cursor_position: String,
+    pub edit_history: Vec<String>,
+    pub expected_patch: String,
+    pub expected_excerpts: Vec<ExpectedExcerpt>,
 }
 
-#[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<PathBuf> {
+        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<str>, Cow<str>)> {
+        // 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<String> {
+    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,