From 8f7facd752e81724e6143d96eec731a200b15cc9 Mon Sep 17 00:00:00 2001 From: alectimison-maker Date: Tue, 4 Aug 2026 15:13:58 +0800 Subject: [PATCH] fix(release): validate GitHub remote identity --- .changes/fix-github-remote-parsing.md | 5 +++++ src/release/changelog/mod.rs | 23 +++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 .changes/fix-github-remote-parsing.md diff --git a/.changes/fix-github-remote-parsing.md b/.changes/fix-github-remote-parsing.md new file mode 100644 index 00000000..991b9dba --- /dev/null +++ b/.changes/fix-github-remote-parsing.md @@ -0,0 +1,5 @@ +--- +"cargo-rail" = "patch" +--- + +Fixed GitHub repository detection for remote URLs with a trailing slash, and rejected non-repository paths before they could produce incorrect changelog or release links. diff --git a/src/release/changelog/mod.rs b/src/release/changelog/mod.rs index 9b2317a2..4e71d927 100644 --- a/src/release/changelog/mod.rs +++ b/src/release/changelog/mod.rs @@ -430,7 +430,8 @@ pub fn detect_github_repo(workspace_root: &Path) -> Option<(String, String)> { /// Parse a GitHub remote URL into (org, repo) fn parse_github_remote(url: &str) -> Option<(String, String)> { - let trimmed = url.trim().trim_end_matches(".git").trim_end_matches('/'); + let trimmed = url.trim().trim_end_matches('/'); + let trimmed = trimmed.strip_suffix(".git").unwrap_or(trimmed); let repo_part = if let Some(ssh) = trimmed.strip_prefix("git@github.com:") { ssh @@ -440,9 +441,10 @@ fn parse_github_remote(url: &str) -> Option<(String, String)> { trimmed.strip_prefix("https://github.com/")? }; - let mut parts = repo_part.split('/'); - let org = parts.next()?; - let repo = parts.next()?; + let (org, repo) = repo_part.split_once('/')?; + if org.is_empty() || repo.is_empty() || repo.contains('/') || org.contains(['?', '#']) || repo.contains(['?', '#']) { + return None; + } Some((org.to_string(), repo.to_string())) } @@ -708,9 +710,22 @@ mod tests { parse_github_remote("ssh://git@github.com/org/repo"), Some(("org".to_string(), "repo".to_string())) ); + assert_eq!( + parse_github_remote("https://github.com/org/repo.git/"), + Some(("org".to_string(), "repo".to_string())) + ); assert_eq!(parse_github_remote("git@gitlab.com:org/repo.git"), None); } + #[test] + fn parse_github_remote_rejects_non_repository_paths() { + assert_eq!(parse_github_remote("https://github.com/org/repo/issues"), None); + assert_eq!(parse_github_remote("https://github.com//repo.git"), None); + assert_eq!(parse_github_remote("https://github.com/org/.git"), None); + assert_eq!(parse_github_remote("https://github.com/org/repo?tab=readme"), None); + assert_eq!(parse_github_remote("https://github.com/org/repo#readme"), None); + } + #[test] fn crate_link_overrides_win() { let shape = ChangelogShape::default();