Skip to content

Commit 15c1c3e

Browse files
unhappychoiceclaude
andcommitted
fix: improve Git URL parsing robustness for multi-host support
- Handle GitLab subgroups and ports correctly - Normalize host names and drop ports for consistent cache paths - Support query parameters and fragments in URLs - Fix Windows path compatibility issues - Improve parsing for scp-like SSH format and URL formats 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5bc2790 commit 15c1c3e

File tree

1 file changed

+28
-32
lines changed

1 file changed

+28
-32
lines changed

src/cli/views/repo_utils.rs

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,42 @@
11
use crate::repository_manager::{RepoInfo, RepositoryManager};
22

3-
/// Extract host, owner, and repository name from a git URL
3+
/// Extract host, owner path (can include `/` for subgroups), and repo name (no .git)
44
fn parse_git_url(remote_url: &str) -> Option<(String, String, String)> {
5-
// Handle SSH format: git@host:owner/repo
6-
if let Some(ssh_part) = remote_url.strip_prefix("git@") {
7-
if let Some(colon_pos) = ssh_part.find(':') {
8-
let host = &ssh_part[..colon_pos];
9-
let path = &ssh_part[colon_pos + 1..];
10-
let parts: Vec<&str> = path.split('/').collect();
11-
if parts.len() >= 2 {
12-
let owner = parts[0];
13-
let repo = parts[1].trim_end_matches(".git");
14-
return Some((host.to_string(), owner.to_string(), repo.to_string()));
15-
}
16-
}
5+
fn normalize_host(h: &str) -> String {
6+
// Drop port if present; lowercase for consistent cache paths
7+
h.split(':').next().unwrap_or(h).to_ascii_lowercase()
178
}
189

19-
// Handle ssh:// format: ssh://git@host/owner/repo
20-
if let Some(ssh_url) = remote_url.strip_prefix("ssh://") {
21-
if let Some(at_pos) = ssh_url.find('@') {
22-
let host_path = &ssh_url[at_pos + 1..];
23-
let parts: Vec<&str> = host_path.split('/').collect();
24-
if parts.len() >= 3 {
25-
let host = parts[0];
26-
let owner = parts[1];
27-
let repo = parts[2].trim_end_matches(".git");
28-
return Some((host.to_string(), owner.to_string(), repo.to_string()));
10+
// 1) scp-like SSH: user@host:path
11+
if let Some((user_host, path)) = remote_url.split_once(':') {
12+
if let Some((_user, host)) = user_host.rsplit_once('@') {
13+
let host = normalize_host(host);
14+
let path = path.trim_start_matches('/').split('?').next().unwrap_or("");
15+
let mut segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
16+
if segs.len() >= 2 {
17+
let repo = segs.pop().unwrap().trim_end_matches(".git").to_string();
18+
let owner = segs.join("/");
19+
return Some((host, owner, repo));
2920
}
3021
}
3122
}
3223

33-
// Handle HTTP(S) format: https://host/owner/repo
34-
if let Some(url_without_protocol) = remote_url
35-
.strip_prefix("https://")
24+
// 2) ssh:// or http(s)://
25+
if let Some(rest) = remote_url
26+
.strip_prefix("ssh://")
27+
.or_else(|| remote_url.strip_prefix("https://"))
3628
.or_else(|| remote_url.strip_prefix("http://"))
3729
{
38-
let parts: Vec<&str> = url_without_protocol.split('/').collect();
39-
if parts.len() >= 3 {
40-
let host = parts[0];
41-
let owner = parts[1];
42-
let repo = parts[2].trim_end_matches(".git");
43-
return Some((host.to_string(), owner.to_string(), repo.to_string()));
30+
let rest = rest.split('#').next().unwrap_or(rest); // drop fragment
31+
let rest = rest.split('?').next().unwrap_or(rest); // drop query
32+
if let Some((host_port, path)) = rest.split_once('/') {
33+
let host = normalize_host(host_port);
34+
let mut segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
35+
if segs.len() >= 2 {
36+
let repo = segs.pop().unwrap().trim_end_matches(".git").to_string();
37+
let owner = segs.join("/");
38+
return Some((host, owner, repo));
39+
}
4440
}
4541
}
4642

0 commit comments

Comments
 (0)