diff --git a/src/config.rs b/src/config.rs index 6ccbec9..9c2bafe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use crate::utils::sanitize_string; +use crate::utils::{clean_repo_input, sanitize_string}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -22,7 +22,7 @@ pub struct RepoConfig { impl RepoConfig { pub fn new(name: impl Into, include_prereleases: bool) -> Self { Self { - name: name.into(), + name: AppConfig::normalize_repo_name(&name.into()), include_prereleases, last_synced_tag: None, } @@ -50,21 +50,17 @@ pub struct AppConfig { } impl AppConfig { - /// Normalisiert die Repository-Eingabe, indem führende/nachgestellte Schrägstriche und GitHub-URL-Präfixe entfernt werden. + /// Normalisiert die Repository-Eingabe, indem führende/nachgestellte Schrägstriche, + /// Query-Parameter, Fragmente und GitHub-URL-Präfixe entfernt werden. pub fn normalize_repo_name(input: &str) -> String { - let trimmed = input.trim(); - let cleaned = trimmed - .trim_start_matches("https://github.com/") - .trim_start_matches("http://github.com/") - .trim_start_matches("github.com/") - .trim_end_matches(".git") - .trim_matches('/'); - cleaned.to_string() + clean_repo_input(input).to_string() } /// Fügt ein Repository zur Konfiguration hinzu oder aktualisiert ein bestehendes. - pub fn add_or_update_repo(&mut self, repo: RepoConfig) { - if let Some(existing) = self.repositories.iter_mut().find(|r| r.name.eq_ignore_ascii_case(&repo.name)) { + pub fn add_or_update_repo(&mut self, mut repo: RepoConfig) { + repo.name = Self::normalize_repo_name(&repo.name); + if let Some(existing) = self.repositories.iter_mut().find(|r| Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&repo.name)) { + existing.name = repo.name; existing.include_prereleases = repo.include_prereleases; } else { self.repositories.push(repo); @@ -75,20 +71,20 @@ impl AppConfig { pub fn remove_repo(&mut self, repo_name: &str) -> bool { let normalized = Self::normalize_repo_name(repo_name); let before_len = self.repositories.len(); - self.repositories.retain(|r| !r.name.eq_ignore_ascii_case(&normalized)); + self.repositories.retain(|r| !Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&normalized)); self.repositories.len() < before_len } /// Sucht ein Repository anhand des Namens. pub fn find_repo(&self, repo_name: &str) -> Option<&RepoConfig> { let normalized = Self::normalize_repo_name(repo_name); - self.repositories.iter().find(|r| r.name.eq_ignore_ascii_case(&normalized)) + self.repositories.iter().find(|r| Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&normalized)) } /// Aktualisiert den zuletzt synchronisierten Tag für ein bestimmtes Repository. pub fn update_last_synced_tag(&mut self, repo_name: &str, tag: String) { let normalized = Self::normalize_repo_name(repo_name); - if let Some(repo) = self.repositories.iter_mut().find(|r| r.name.eq_ignore_ascii_case(&normalized)) { + if let Some(repo) = self.repositories.iter_mut().find(|r| Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&normalized)) { repo.last_synced_tag = Some(tag); } } @@ -111,6 +107,10 @@ pub fn get_config_file_path() -> PathBuf { pub fn load_config() -> AppConfig { let mut config = config_ctdra::load_config::(); + for repo in &mut config.repositories { + repo.name = AppConfig::normalize_repo_name(&repo.name); + } + if let Ok(val) = std::env::var("GITEA_URL") { let val = sanitize_string(&val); if !val.is_empty() { diff --git a/src/github.rs b/src/github.rs index bb6d138..231a8cd 100644 --- a/src/github.rs +++ b/src/github.rs @@ -246,13 +246,7 @@ impl GitHubClient { /// Hilfsfunktion zum Parsen von "owner/repo" aus einer Repository-Zeichenkette. pub fn parse_repo_owner_name(repo: &str) -> Result<(&str, &str)> { - let cleaned = repo - .trim() - .trim_start_matches("https://github.com/") - .trim_start_matches("http://github.com/") - .trim_start_matches("github.com/") - .trim_end_matches(".git") - .trim_matches('/'); + let cleaned = crate::utils::clean_repo_input(repo); let parts: Vec<&str> = cleaned.split('/').collect(); if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { diff --git a/src/utils.rs b/src/utils.rs index 7d55e90..317d8e9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -29,3 +29,36 @@ pub fn sanitize_env_vars(env_vars: &mut std::collections::HashMap &str { + let mut trimmed = input.trim(); + if (trimmed.starts_with('"') && trimmed.ends_with('"')) + || (trimmed.starts_with('\'') && trimmed.ends_with('\'')) + { + if trimmed.len() >= 2 { + trimmed = trimmed[1..trimmed.len() - 1].trim(); + } + } + + let without_query_or_fragment = match trimmed.find(|c| c == '?' || c == '#') { + Some(idx) => &trimmed[..idx], + None => trimmed, + }; + + let mut cleaned = without_query_or_fragment + .trim_start_matches("git@github.com:") + .trim_start_matches("https://") + .trim_start_matches("http://") + .trim_start_matches("www.github.com/") + .trim_start_matches("github.com/") + .trim_matches('/'); + + if let Some(stripped) = cleaned.strip_suffix(".git") { + cleaned = stripped.trim_matches('/'); + } + + cleaned +} diff --git a/tests/config_tests.rs b/tests/config_tests.rs index 768082b..972e675 100644 --- a/tests/config_tests.rs +++ b/tests/config_tests.rs @@ -1,5 +1,5 @@ use mirror_package::config::{AppConfig, RepoConfig, load_config}; -use mirror_package::utils::{sanitize_env_vars, sanitize_string}; +use mirror_package::utils::{clean_repo_input, sanitize_env_vars, sanitize_string}; use std::collections::HashMap; #[test] @@ -65,20 +65,72 @@ fn test_sanitize_string() { assert_eq!(sanitize_string(" \"world\" "), "world"); } +#[test] +fn test_clean_repo_input() { + assert_eq!(clean_repo_input("owner/repo"), "owner/repo"); + assert_eq!(clean_repo_input("https://github.com/owner/repo"), "owner/repo"); + assert_eq!(clean_repo_input("http://github.com/owner/repo"), "owner/repo"); + assert_eq!(clean_repo_input("github.com/owner/repo"), "owner/repo"); + assert_eq!(clean_repo_input("https://github.com/owner/repo.git"), "owner/repo"); + assert_eq!(clean_repo_input("https://github.com/owner/repo/"), "owner/repo"); + assert_eq!( + clean_repo_input("https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file"), + "raspberrypi/rpi-imager" + ); + assert_eq!( + clean_repo_input("https://github.com/raspberrypi/rpi-imager/?tab=readme-ov-file"), + "raspberrypi/rpi-imager" + ); + assert_eq!( + clean_repo_input("https://github.com/raspberrypi/rpi-imager.git?tab=readme-ov-file"), + "raspberrypi/rpi-imager" + ); + assert_eq!( + clean_repo_input("https://github.com/raspberrypi/rpi-imager#readme"), + "raspberrypi/rpi-imager" + ); + assert_eq!( + clean_repo_input("https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file#install"), + "raspberrypi/rpi-imager" + ); + assert_eq!( + clean_repo_input("raspberrypi/rpi-imager?tab=readme-ov-file"), + "raspberrypi/rpi-imager" + ); + assert_eq!( + clean_repo_input("\"https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file\""), + "raspberrypi/rpi-imager" + ); +} + #[test] fn test_normalize_repo_name() { let input = "https://github.com/owner/repo.git"; let normalized = AppConfig::normalize_repo_name(input); assert_eq!(normalized, "owner/repo"); + + let input_with_query = "https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file"; + assert_eq!( + AppConfig::normalize_repo_name(input_with_query), + "raspberrypi/rpi-imager" + ); } #[test] fn test_add_or_update_repo() { let mut config = AppConfig::default(); - let repo = RepoConfig::new("owner/repo", true); + let repo = RepoConfig::new("https://github.com/owner/repo?tab=readme-ov-file", true); config.add_or_update_repo(repo.clone()); assert_eq!(config.repositories.len(), 1); - assert_eq!(config.repositories[0], repo); + assert_eq!(config.repositories[0].name, "owner/repo"); + assert_eq!(config.repositories[0].include_prereleases, true); + + // Updating existing repo with a URL with query param + let repo_updated = RepoConfig::new("https://github.com/owner/repo#readme", false); + config.add_or_update_repo(repo_updated); + assert_eq!(config.repositories.len(), 1); + assert_eq!(config.repositories[0].name, "owner/repo"); + assert_eq!(config.repositories[0].include_prereleases, false); } #[test] @@ -86,7 +138,7 @@ fn test_remove_repo() { let mut config = AppConfig::default(); let repo = RepoConfig::new("owner/repo", true); config.add_or_update_repo(repo.clone()); - assert!(config.remove_repo("owner/repo")); + assert!(config.remove_repo("https://github.com/owner/repo?tab=readme-ov-file")); assert!(config.repositories.is_empty()); } @@ -95,7 +147,10 @@ fn test_find_repo() { let mut config = AppConfig::default(); let repo = RepoConfig::new("owner/repo", true); config.add_or_update_repo(repo.clone()); - assert_eq!(config.find_repo("owner/repo"), Some(&repo)); + assert_eq!( + config.find_repo("https://github.com/owner/repo?tab=readme-ov-file"), + Some(&RepoConfig::new("owner/repo", true)) + ); } #[test] diff --git a/tests/github_tests.rs b/tests/github_tests.rs index 7cdadec..16ad8dc 100644 --- a/tests/github_tests.rs +++ b/tests/github_tests.rs @@ -64,6 +64,18 @@ fn test_parse_repo_owner_name() { parse_repo_owner_name("https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher.git").unwrap(), ("Heroic-Games-Launcher", "HeroicGamesLauncher") ); + assert_eq!( + parse_repo_owner_name("https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file").unwrap(), + ("raspberrypi", "rpi-imager") + ); + assert_eq!( + parse_repo_owner_name("https://github.com/raspberrypi/rpi-imager/?tab=readme-ov-file#install").unwrap(), + ("raspberrypi", "rpi-imager") + ); + assert_eq!( + parse_repo_owner_name("raspberrypi/rpi-imager?tab=readme-ov-file").unwrap(), + ("raspberrypi", "rpi-imager") + ); assert!(parse_repo_owner_name("invalid_repo").is_err()); assert!(parse_repo_owner_name("invalid/repo/extra").is_err());