use std::process::Command;
use anyhow::{Context, Result};
pub(crate) fn get_git_commit() -> Result<String> {
let output = Command::new("git")
.args(&["rev-parse", "HEAD"])
.output()
.with_context(|| "Failed to execute git command. Is git installed?")?;
if !output.status.success() {
anyhow::bail!(
"Failed to get git commit: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let commit = String::from_utf8(output.stdout)
.with_context(|| "Invalid UTF-8 in git output")?
.trim()
.to_string();
Ok(commit)
}
pub(crate) fn get_git_branch() -> Result<String> {
if let Ok(b) = std::env::var("FORKLAUNCH_GIT_BRANCH") {
let trimmed = b.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
}
let output = Command::new("git")
.args(&["branch", "--show-current"])
.output()
.with_context(|| "Failed to execute git command")?;
if output.status.success() {
let branch = String::from_utf8(output.stdout)
.with_context(|| "Invalid UTF-8 in git output")?
.trim()
.to_string();
if !branch.is_empty() {
return Ok(branch);
}
if let Ok(out) = Command::new("git")
.args(&[
"for-each-ref",
"--format=%(refname:short)",
"--points-at",
"HEAD",
"refs/heads/",
])
.output()
{
let candidate = String::from_utf8_lossy(&out.stdout);
if let Some(first) = candidate.lines().next() {
let trimmed = first.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
}
}
}
let output = Command::new("git")
.args(&["rev-parse", "--abbrev-ref", "HEAD"])
.output()
.with_context(|| "Failed to get git branch")?;
if !output.status.success() {
return Ok("unknown".to_string());
}
let branch = String::from_utf8(output.stdout)
.with_context(|| "Invalid UTF-8 in git output")?
.trim()
.to_string();
Ok(branch)
}
pub(crate) fn is_git_repo() -> bool {
Command::new("git")
.args(&["rev-parse", "--git-dir"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
pub(crate) fn get_git_remote_url() -> Option<String> {
let output = Command::new("git")
.args(&["remote", "get-url", "origin"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let raw = String::from_utf8(output.stdout).ok()?;
normalize_git_remote_url(raw.trim())
}
pub(crate) fn normalize_git_remote_url(raw: &str) -> Option<String> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let without_scheme = if let Some(rest) = raw.strip_prefix("ssh://") {
let rest = rest.split_once('@').map(|(_, r)| r).unwrap_or(rest);
rest.to_string()
} else if let Some(rest) = raw.strip_prefix("git@") {
rest.replacen(':', "/", 1)
} else if let Some(rest) = raw
.strip_prefix("https://")
.or_else(|| raw.strip_prefix("http://"))
{
rest.rsplit_once('@')
.map(|(_, r)| r)
.unwrap_or(rest)
.to_string()
} else {
return None;
};
let trimmed = without_scheme
.trim_end_matches('/')
.trim_end_matches(".git")
.trim_end_matches('/');
if trimmed.split('/').count() < 3 {
return None;
}
Some(format!("https://{}", trimmed))
}
#[cfg(test)]
mod remote_url_tests {
use super::normalize_git_remote_url;
#[test]
fn normalizes_ssh_https_and_git_suffix() {
for raw in [
"git@github.com:acme/app.git",
"ssh://git@github.com/acme/app.git",
"https://github.com/acme/app.git",
"https://github.com/acme/app",
"https://x-access-token:tok@github.com/acme/app.git",
"https://github.com/acme/app/",
] {
assert_eq!(
normalize_git_remote_url(raw).as_deref(),
Some("https://github.com/acme/app"),
"{raw}"
);
}
}
#[test]
fn rejects_empty_and_local_paths() {
assert_eq!(normalize_git_remote_url(""), None);
assert_eq!(normalize_git_remote_url("/srv/git/app.git"), None);
assert_eq!(normalize_git_remote_url("https://github.com/acme"), None);
}
}