| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | | use crate::event::Event; |
| 6 | | use anyhow::{Context, Result}; |
| 7 | | use serde_json::Value as Json; |
| 8 | | use sqlx::SqliteConnection; |
| 9 | | use std::path::Path; |
| 10 | |
|
| 11 | |
|
| 12 | | #[derive(Clone, Debug, Default, PartialEq, Eq)] |
| 13 | | pub struct Checkout { |
| 14 | | pub repo_id: Option<String>, |
| 15 | | pub remote: Option<String>, |
| 16 | | |
| 17 | | pub modules: Vec<String>, |
| 18 | | } |
| 19 | |
|
| 20 | |
|
| 21 | 21× | pub fn inspect(path: &Path) -> Result<Checkout> { |
| 22 | 21× | if !path.is_dir() { |
| 23 | 0× | anyhow::bail!("{} is not a directory", path.display()); |
| 24 | 21× | } |
| 25 | 21× | let oxy = path.join(".oxy"); |
| 26 | 21× | let repo_id = std::fs::read_to_string(oxy.join("id")).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()); |
| 27 | 21× | let mut remotes: Vec<(String, String)> = Vec::new(); |
| 28 | 21× | if let Ok(text) = std::fs::read_to_string(oxy.join("config")) { |
| 29 | 8× | for line in text.lines() { |
| 30 | 8× | if let Some((k, v)) = line.split_once('=') { |
| 31 | 8× | let k = k.trim(); |
| 32 | 8× | if let Some(name) = k.strip_prefix("remote.").and_then(|r| r.strip_suffix(".url")) { |
| 33 | 8× | remotes.push((name.to_string(), v.trim().trim_matches('"').to_string())); |
| 34 | 8× | } |
| 35 | 0× | } |
| 36 | | } |
| 37 | 14× | } |
| 38 | 21× | let remote = remotes.iter().find(|(n, _)| n == "origin").or(remotes.first()).map(|(_, u)| u.clone()); |
| 39 | 21× | let modules = std::fs::read_to_string(path.join(".oxymodules")) |
| 40 | 21× | .map(|t| { |
| 41 | 7× | t.lines() |
| 42 | 9× | .filter_map(|l| l.trim().strip_prefix("url").map(|r| r.trim_start().trim_start_matches('=').trim().to_string())) |
| 43 | 7× | .collect() |
| 44 | 7× | }) |
| 45 | 21× | .unwrap_or_default(); |
| 46 | 21× | Ok(Checkout { repo_id, remote, modules }) |
| 47 | 21× | } |
| 48 | |
|
| 49 | |
|
| 50 | 19× | pub fn default_name(path: &Path) -> Result<String> { |
| 51 | 19× | let base = path.canonicalize()?.file_name().and_then(|n| n.to_str()).map(str::to_lowercase).context("the path has no name")?; |
| 52 | 117× | Ok(base.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '-' }).collect()) |
| 53 | 19× | } |
| 54 | |
|
| 55 | 5× | fn same_repo(a: &str, b: &str) -> bool { |
| 56 | 10× | let n = |s: &str| s.trim_end_matches('/').trim_end_matches(".git").to_lowercase(); |
| 57 | 5× | n(a) == n(b) |
| 58 | 5× | } |
| 59 | |
|
| 60 | |
|
| 61 | 42500× | pub async fn project(conn: &mut SqliteConnection, e: &Event) -> sqlx::Result<()> { |
| 62 | 42500× | if e.kind != "project.added" { |
| 63 | 38952× | return Ok(()); |
| 64 | 3548× | } |
| 65 | 3548× | let p = e.json(); |
| 66 | 3656× | let s = |k: &str| p.get(k).and_then(Json::as_str).map(String::from); |
| 67 | 3548× | let Some(name) = s("name") else { return Ok(()) }; |
| 68 | 36× | let modules: Vec<String> = p |
| 69 | 36× | .get("modules") |
| 70 | 36× | .and_then(Json::as_array) |
| 71 | 36× | .map(|a| a.iter().filter_map(|m| m.as_str().map(String::from)).collect()) |
| 72 | 36× | .unwrap_or_default(); |
| 73 | 36× | let remote = s("remote"); |
| 74 | 36× | sqlx::query("INSERT OR REPLACE INTO projects (name, path, repo_id, remote, modules, seq) VALUES (?, ?, ?, ?, ?, ?)") |
| 75 | 36× | .bind(&name) |
| 76 | 36× | .bind(s("path").unwrap_or_default()) |
| 77 | 36× | .bind(s("repo_id")) |
| 78 | 36× | .bind(&remote) |
| 79 | 36× | .bind(serde_json::to_string(&modules).unwrap_or_default()) |
| 80 | 36× | .bind(e.seq as i64) |
| 81 | 36× | .execute(&mut *conn) |
| 82 | 36× | .await?; |
| 83 | 36× | let others: Vec<(String, Option<String>, String)> = |
| 84 | 36× | sqlx::query_as("SELECT name, remote, modules FROM projects WHERE name != ?").bind(&name).fetch_all(&mut *conn).await?; |
| 85 | 36× | for (other, other_remote, other_modules) in others { |
| 86 | 9× | if other_remote.as_deref().is_some_and(|r| modules.iter().any(|m| same_repo(m, r))) { |
| 87 | 2× | sqlx::query("INSERT OR IGNORE INTO project_relations (project, depends_on) VALUES (?, ?)") |
| 88 | 2× | .bind(&name) |
| 89 | 2× | .bind(&other) |
| 90 | 2× | .execute(&mut *conn) |
| 91 | 2× | .await?; |
| 92 | 7× | } |
| 93 | 9× | let theirs: Vec<String> = serde_json::from_str(&other_modules).unwrap_or_default(); |
| 94 | 9× | if remote.as_deref().is_some_and(|r| theirs.iter().any(|m| same_repo(m, r))) { |
| 95 | 2× | sqlx::query("INSERT OR IGNORE INTO project_relations (project, depends_on) VALUES (?, ?)") |
| 96 | 2× | .bind(&other) |
| 97 | 2× | .bind(&name) |
| 98 | 2× | .execute(&mut *conn) |
| 99 | 2× | .await?; |
| 100 | 7× | } |
| 101 | | } |
| 102 | 36× | Ok(()) |
| 103 | 42500× | } |
| 104 | |
|
| 105 | | #[cfg(test)] |
| 106 | | mod tests { |
| 107 | | use super::*; |
| 108 | |
|
| 109 | | #[test] |
| 110 | 1× | fn a_checkout_reports_its_id_remote_and_subrepos() { |
| 111 | 1× | let dir = tempfile::tempdir().unwrap(); |
| 112 | 1× | std::fs::create_dir_all(dir.path().join(".oxy")).unwrap(); |
| 113 | 1× | std::fs::write(dir.path().join(".oxy/id"), "97e6").unwrap(); |
| 114 | 1× | std::fs::write( |
| 115 | 1× | dir.path().join(".oxy/config"), |
| 116 | | "remote.earth.url = \"https://hub/a/hub\"\nremote.origin.url = \"https://hub/a/x\"\n", |
| 117 | | ) |
| 118 | 1× | .unwrap(); |
| 119 | 1× | std::fs::write(dir.path().join(".oxymodules"), "[deps/flow]\nurl = https://hub/a/flow\nbookmark = main\n").unwrap(); |
| 120 | 1× | let c = inspect(dir.path()).unwrap(); |
| 121 | 1× | assert_eq!( |
| 122 | | c, |
| 123 | 1× | Checkout { repo_id: Some("97e6".into()), remote: Some("https://hub/a/x".into()), modules: vec!["https://hub/a/flow".into()] } |
| 124 | | ); |
| 125 | 1× | assert!(same_repo("https://hub/a/flow/", "https://HUB/a/flow.git")); |
| 126 | 1× | } |
| 127 | | } |