use std::path::Path;
use std::process::Command;
pub fn url_for(workspace: &Path, rel_path: &str, line_lo: u32, line_hi: u32) -> Option<String> {
let remote_url = git_config(workspace, "remote.origin.url")?;
let (host, owner, repo) = parse_remote(&remote_url)?;
let sha = git_rev_parse(workspace, "HEAD").unwrap_or_else(|| "main".into());
let line_frag = if line_lo == line_hi {
format!("#L{line_lo}")
} else {
format!("#L{line_lo}-L{line_hi}")
};
let path_segment = if host.ends_with("bitbucket.org") {
format!("src/{sha}/{rel_path}")
} else {
format!("blob/{sha}/{rel_path}")
};
Some(format!(
"https://{host}/{owner}/{repo}/{path_segment}{line_frag}"
))
}
pub fn commit_url(workspace: &Path, hash: &str) -> Option<String> {
let remote_url = git_config(workspace, "remote.origin.url")?;
let (host, owner, repo) = parse_remote(&remote_url)?;
Some(format!("https://{host}/{owner}/{repo}/commit/{hash}"))
}
pub fn parse_remote(url: &str) -> Option<(String, String, String)> {
if let Some(rest) = url.strip_prefix("git@") {
let (host, path) = rest.split_once(':')?;
let (owner, repo) = path.split_once('/')?;
let repo = repo.trim_end_matches(".git");
return Some((host.to_string(), owner.to_string(), repo.to_string()));
}
if let Some(rest) = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))
{
let rest = rest.split_once('@').map(|(_, r)| r).unwrap_or(rest);
let (host, path) = rest.split_once('/')?;
let (owner, repo) = path.split_once('/')?;
let repo = repo.trim_end_matches('/').trim_end_matches(".git");
return Some((host.to_string(), owner.to_string(), repo.to_string()));
}
None
}
pub fn provider_icon(host: &str) -> &'static str {
let h = host.to_ascii_lowercase();
if h.contains("github.com") {
"\u{F09B}" } else if h.contains("gitlab") {
"\u{F296}" } else if h.contains("bitbucket") {
"\u{E703}" } else if h.contains("dev.azure.com") || h.contains("visualstudio.com") {
"\u{F0805}" } else {
"\u{E702}" }
}
pub fn provider_icon_for(workspace: &Path) -> Option<&'static str> {
let url = git_config(workspace, "remote.origin.url")?;
let (host, _, _) = parse_remote(&url)?;
Some(provider_icon(&host))
}
pub fn git_config(workspace: &Path, key: &str) -> Option<String> {
let out = Command::new("git")
.args(["config", "--get", key])
.current_dir(workspace)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let v = String::from_utf8_lossy(&out.stdout).trim().to_string();
if v.is_empty() { None } else { Some(v) }
}
fn git_rev_parse(workspace: &Path, rev: &str) -> Option<String> {
let out = Command::new("git")
.args(["rev-parse", rev])
.current_dir(workspace)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let v = String::from_utf8_lossy(&out.stdout).trim().to_string();
if v.is_empty() { None } else { Some(v) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_ssh_remote() {
let (h, o, r) = parse_remote("git@github.com:rust-lang/rust.git").unwrap();
assert_eq!(h, "github.com");
assert_eq!(o, "rust-lang");
assert_eq!(r, "rust");
}
#[test]
fn parse_https_remote() {
let (h, o, r) = parse_remote("https://github.com/rust-lang/rust").unwrap();
assert_eq!(h, "github.com");
assert_eq!(o, "rust-lang");
assert_eq!(r, "rust");
}
#[test]
fn parse_https_with_user() {
let (h, o, r) = parse_remote("https://user@gitlab.com/group/proj.git").unwrap();
assert_eq!(h, "gitlab.com");
assert_eq!(o, "group");
assert_eq!(r, "proj");
}
}