use std::path::Path;
use serde::{Deserialize, Serialize};
use tracing::debug;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct VcsInfo {
#[serde(skip_serializing_if = "Option::is_none", rename = "format")]
pub vcs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remote_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub local_branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remote_branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revision: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revision_full: Option<String>,
}
impl VcsInfo {
pub fn from_path(path: &Path) -> Self {
if let Some(info) = Self::try_git(path) {
debug!(
vcs = "git",
local_branch = ?info.local_branch,
remote_branch = ?info.remote_branch,
revision = ?info.revision,
remote = ?info.remote_url,
"Detected repository"
);
return info;
}
if let Some(info) = Self::try_cvs(path) {
debug!(
vcs = "cvs",
remote_branch = ?info.remote_branch,
remote = ?info.remote_url,
"Detected repository"
);
return info;
}
debug!(path = %path.display(), "No VCS detected");
Self::default()
}
pub fn is_detected(&self) -> bool {
self.vcs.is_some()
}
pub fn web_url(&self) -> Option<String> {
if self.vcs.as_deref() != Some("git") {
return None;
}
let url_str = self.remote_url.as_deref()?;
let url = gix::url::parse(url_str.into()).ok()?;
let host = url.host()?;
let path = url.path.to_string();
let path = path.trim_start_matches('/');
match url.scheme {
gix::url::Scheme::Https | gix::url::Scheme::Http => {
Some(format!("{}://{}/{}", url.scheme, host, path))
}
gix::url::Scheme::Ssh => Some(format!("https://{}/{}", host, path)),
_ => None,
}
}
fn try_git(path: &Path) -> Option<Self> {
let repo = gix::discover(path).ok()?;
let head = repo.head().ok()?;
let local_branch = head.referent_name().map(|r| r.shorten().to_string());
let head_id = head.id();
let revision_full = head_id.map(|id| id.to_string());
let revision = revision_full
.as_ref()
.map(|s| s[..s.len().min(12)].to_string());
let (remote_name, remote_branch) = if let Some(ref local) = local_branch {
let config = repo.config_snapshot();
let remote = config
.string(format!("branch.{}.remote", local).as_str())
.map(|v| v.to_string());
let branch = config
.string(format!("branch.{}.merge", local).as_str())
.map(|v| {
let s = v.to_string();
s.trim_start_matches("refs/heads/").to_string()
});
(remote, branch)
} else {
(None, None)
};
let remote_url = remote_name
.or_else(|| {
repo.remote_names()
.into_iter()
.next()
.map(|n| n.to_string())
})
.and_then(|name| repo.find_remote(name.as_str()).ok())
.and_then(|r| r.url(gix::remote::Direction::Fetch).cloned())
.map(|url| {
let s = url.to_bstring().to_string();
s.trim_end_matches(".git").to_string()
});
Some(Self {
vcs: Some("git".to_string()),
remote_url,
local_branch,
remote_branch,
revision,
revision_full,
})
}
fn try_cvs(path: &Path) -> Option<Self> {
let root_path = path.join("CVS/Root");
let remote_url = std::fs::read_to_string(root_path)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())?;
let tag_path = path.join("CVS/Tag");
let remote_branch = std::fs::read_to_string(tag_path)
.ok()
.map(|s| s.trim().to_string())
.and_then(|s| {
s.strip_prefix('T')
.or_else(|| s.strip_prefix('N'))
.map(|tag| tag.to_string())
})
.or_else(|| Some("HEAD".to_string()));
Some(Self {
vcs: Some("cvs".to_string()),
remote_url: Some(remote_url),
local_branch: None,
remote_branch,
revision: None,
revision_full: None,
})
}
}