///|
/// Parse a repository URL or shorthand into RepoPath
///
/// Supported formats:
/// - user/repo
/// - user/repo/path/to/subdir (slash separator for subdir)
/// - user/repo:path/to/subdir (colon separator for subdir)
/// - github.com/user/repo
/// - github.com/user/repo:path/to/subdir
/// - gitlab.com/user/repo
/// - https://github.com/user/repo
/// - https://github.com/user/repo/tree/main/path/to/subdir
/// - git@github.com:user/repo.git
pub fn parse_repo_url(input : String) -> RepoPath? {
let input = input.trim().to_owned()
if input.is_empty() {
return None
}
// Handle git@ SSH URLs
if input.has_prefix("git@") {
return parse_ssh_url(input)
}
// Handle https:// URLs
if input.has_prefix("https://") || input.has_prefix("http://") {
return parse_https_url(input)
}
// Handle shorthand: user/repo or host/user/repo
parse_shorthand(input)
}
///|
fn parse_ssh_url(input : String) -> RepoPath? {
// git@github.com:user/repo.git
// git@gitlab.com:user/repo.git
let without_prefix = slice_from(input, 4) // remove "git@"
let colon_idx = find_char(without_prefix, ':')
guard colon_idx is Some(idx) else { return None }
let host = slice_to(without_prefix, idx)
let path = slice_from(without_prefix, idx + 1)
// Remove .git suffix if present
let path = if path.has_suffix(".git") {
slice_to(path, path.length() - 4)
} else {
path
}
let parts = path.split("/").collect()
if parts.length() < 2 {
return None
}
let user = parts[0].to_owned()
let repo = parts[1].to_owned()
let subdir : String? = if parts.length() > 2 {
Some(join_parts(parts, 2, parts.length()))
} else {
None
}
// Normalize github.com
let normalized_host : String? = if host == "github.com" {
None
} else {
Some(host)
}
Some(RepoPath::new(user, repo, host=normalized_host, subdir~))
}
///|
fn parse_https_url(input : String) -> RepoPath? {
// https://github.com/user/repo
// https://github.com/user/repo.git
// https://github.com/user/repo/tree/branch/path/to/subdir
let without_protocol = if input.has_prefix("https://") {
slice_from(input, 8)
} else {
slice_from(input, 7) // http://
}
let parts = without_protocol.split("/").collect()
if parts.length() < 3 {
return None
}
let host = parts[0].to_owned()
let user = parts[1].to_owned()
let mut repo = parts[2].to_owned()
// Remove .git suffix
if repo.has_suffix(".git") {
repo = slice_to(repo, repo.length() - 4)
}
// Check for /tree/branch/path or /blob/branch/path pattern
let subdir : String? = if parts.length() > 4 &&
(parts[3].to_owned() == "tree" || parts[3].to_owned() == "blob") {
// parts[4] is branch, parts[5:] is path
if parts.length() > 5 {
Some(join_parts(parts, 5, parts.length()))
} else {
None
}
} else if parts.length() > 3 {
// Direct path after repo
Some(join_parts(parts, 3, parts.length()))
} else {
None
}
// Normalize github.com
let normalized_host : String? = if host == "github.com" {
None
} else {
Some(host)
}
Some(RepoPath::new(user, repo, host=normalized_host, subdir~))
}
///|
fn parse_shorthand(input : String) -> RepoPath? {
// user/repo
// user/repo:path/to/subdir (colon separator for subdir)
// user/repo/path/to/subdir (slash separator for subdir)
// github.com/user/repo
// gitlab.com/user/repo:path
// Check for subdir separator ':'
let (repo_part, explicit_subdir) = match find_char(input, ':') {
Some(idx) => {
let rp = slice_to(input, idx)
let sd = slice_from(input, idx + 1)
(rp, if sd.is_empty() { None } else { Some(sd) })
}
None => (input, None)
}
let parts = repo_part.split("/").filter(s => s.length() > 0).collect()
if parts.length() < 2 {
return None
}
// Check if first part looks like a host (contains a dot)
let first = parts[0].to_owned()
if first.contains(".") {
// host/user/repo format
if parts.length() < 3 {
return None
}
let host = first
let user = parts[1].to_owned()
let repo = parts[2].to_owned()
let subdir = match explicit_subdir {
Some(sd) => Some(sd)
None =>
if parts.length() > 3 {
Some(join_parts(parts, 3, parts.length()))
} else {
None
}
}
// Normalize github.com
let normalized_host : String? = if host == "github.com" {
None
} else {
Some(host)
}
Some(RepoPath::new(user, repo, host=normalized_host, subdir~))
} else {
// user/repo format (github.com implicit)
let user = parts[0].to_owned()
let repo = parts[1].to_owned()
let subdir = match explicit_subdir {
Some(sd) => Some(sd)
None =>
if parts.length() > 2 {
Some(join_parts(parts, 2, parts.length()))
} else {
None
}
}
Some(RepoPath::new(user, repo, subdir~))
}
}
///|
/// Find index of character in string
fn find_char(s : String, c : Char) -> Int? {
for i = 0; i < s.length(); i = i + 1 {
match s.get_char(i) {
Some(ch) if ch == c => return Some(i)
_ => ()
}
}
None
}
///|
/// Join array of string views from start to end with "/"
fn join_parts(parts : Array[StringView], start : Int, end : Int) -> String {
let buf = StringBuilder::new()
for i = start; i < end; i = i + 1 {
if i > start {
buf.write_char('/')
}
buf.write_string(parts[i].to_owned())
}
buf.to_string()
}