#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{bail, Context, Result};
use tracing::debug;
pub fn ccd_dir(repo_root: &Path) -> Result<PathBuf> {
git_path(repo_root, "ccd")
}
pub fn git_path(repo_root: &Path, suffix: &str) -> Result<PathBuf> {
debug!(args = ?["rev-parse", "--git-path", suffix], dir = %repo_root.display(), "spawning git");
let output = Command::new("git")
.args(["rev-parse", "--git-path", suffix])
.current_dir(repo_root)
.output()
.with_context(|| {
format!(
"failed to run `git rev-parse --git-path {suffix}` in {}",
repo_root.display()
)
})?;
debug!(success = output.status.success(), "git completed");
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let detail = stderr.trim();
if detail.is_empty() {
bail!(
"`git rev-parse --git-path {suffix}` failed in {}",
repo_root.display()
);
}
bail!(
"`git rev-parse --git-path {suffix}` failed in {}: {detail}",
repo_root.display()
);
}
let stdout = String::from_utf8(output.stdout)
.with_context(|| format!("git returned a non-utf8 path for `{suffix}`"))?;
let resolved = stdout.trim();
if resolved.is_empty() {
bail!(
"`git rev-parse --git-path {suffix}` returned an empty path in {}",
repo_root.display()
);
}
let path = PathBuf::from(resolved);
if path.is_absolute() {
Ok(path)
} else {
Ok(repo_root.join(path))
}
}
pub fn is_git_work_tree(path: &Path) -> bool {
debug!(args = ?["rev-parse", "--is-inside-work-tree"], dir = %path.display(), "spawning git");
let result = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.current_dir(path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
debug!(success = result, "git completed");
result
}
pub fn head_short(repo_root: &Path) -> Result<Option<String>> {
debug!(args = ?["rev-parse", "--short", "HEAD"], dir = %repo_root.display(), "spawning git");
let output = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.current_dir(repo_root)
.output()
.with_context(|| {
format!(
"failed to run `git rev-parse --short HEAD` in {}",
repo_root.display()
)
})?;
debug!(success = output.status.success(), "git completed");
if !output.status.success() {
return Ok(None);
}
let stdout = String::from_utf8(output.stdout)
.context("git returned a non-utf8 HEAD while resolving current commit")?;
let head = stdout.trim();
if head.is_empty() {
Ok(None)
} else {
Ok(Some(head.to_owned()))
}
}
pub fn worktree_add(
repo_root: &Path,
worktree_path: &Path,
branch: &str,
start_point: Option<&str>,
) -> Result<()> {
let mut command = Command::new("git");
command
.arg("worktree")
.arg("add")
.arg("-b")
.arg(branch)
.arg(worktree_path)
.current_dir(repo_root);
if let Some(start_point) = start_point {
command.arg(start_point);
}
debug!(args = ?["worktree", "add", "-b", branch], dir = %repo_root.display(), "spawning git");
let output = command.output().with_context(|| {
format!(
"failed to run `git worktree add -b {branch} {}` in {}",
worktree_path.display(),
repo_root.display()
)
})?;
debug!(success = output.status.success(), "git completed");
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr);
let detail = stderr.trim();
if detail.is_empty() {
bail!(
"`git worktree add -b {branch} {}` failed in {}",
worktree_path.display(),
repo_root.display()
);
}
bail!(
"`git worktree add -b {branch} {}` failed in {}: {detail}",
worktree_path.display(),
repo_root.display()
)
}
pub fn github_remote_owner_repo(repo_root: &Path) -> Option<String> {
origin_remote_url(repo_root)
.ok()
.flatten()
.and_then(|url| parse_github_owner_repo(&url))
}
pub fn origin_remote_url(repo_root: &Path) -> Result<Option<String>> {
debug!(args = ?["remote", "get-url", "origin"], dir = %repo_root.display(), "spawning git");
let output = Command::new("git")
.args(["remote", "get-url", "origin"])
.current_dir(repo_root)
.output()
.with_context(|| {
format!(
"failed to run `git remote get-url origin` in {}",
repo_root.display()
)
})?;
debug!(success = output.status.success(), "git completed");
if !output.status.success() {
return Ok(None);
}
let url = String::from_utf8(output.stdout)
.context("git returned a non-utf8 origin remote while resolving GitHub repo")?;
let url = url.trim();
if url.is_empty() {
Ok(None)
} else {
Ok(Some(url.to_owned()))
}
}
pub fn parse_github_owner_repo(url: &str) -> Option<String> {
if let Some(rest) = url.strip_prefix("git@github.com:") {
return normalize_github_owner_repo(rest);
}
if let Some(rest) = url.strip_prefix("ssh://git@github.com/") {
return normalize_github_owner_repo(rest);
}
if let Some(rest) = url
.strip_prefix("https://github.com/")
.or_else(|| url.strip_prefix("http://github.com/"))
{
return normalize_github_owner_repo(rest);
}
if let Some(rest) = url.strip_prefix("git://github.com/") {
return normalize_github_owner_repo(rest);
}
None
}
fn normalize_github_owner_repo(path: &str) -> Option<String> {
let path = path.trim_matches('/');
let path = path.strip_suffix(".git").unwrap_or(path);
let mut parts = path.split('/');
let owner = parts.next()?;
let repo = parts.next()?;
if owner.is_empty() || repo.is_empty() || parts.next().is_some() {
return None;
}
Some(format!("{owner}/{repo}"))
}
#[cfg(test)]
mod tests {
use std::process::Command;
use tempfile::tempdir;
use super::*;
#[test]
fn ccd_dir_uses_git_rev_parse_output() {
let temp = tempdir().expect("tempdir");
init_git_repo(temp.path());
let path = ccd_dir(temp.path()).expect("git path");
assert_eq!(path, temp.path().join(".git/ccd"));
}
#[test]
fn ccd_dir_fails_closed_outside_git_repo() {
let temp = tempdir().expect("tempdir");
let error = ccd_dir(temp.path()).expect_err("git path should fail");
assert!(error.to_string().contains("git rev-parse --git-path ccd"));
}
fn init_git_repo(path: &Path) {
Command::new("git")
.args(["init", "-b", "main"])
.current_dir(path)
.status()
.expect("git init");
}
#[test]
fn parse_github_ssh_url() {
assert_eq!(
parse_github_owner_repo("git@github.com:dusk-network/ccd.git"),
Some("dusk-network/ccd".to_owned())
);
}
#[test]
fn parse_github_ssh_url_no_suffix() {
assert_eq!(
parse_github_owner_repo("git@github.com:owner/repo"),
Some("owner/repo".to_owned())
);
}
#[test]
fn parse_github_https_url() {
assert_eq!(
parse_github_owner_repo("https://github.com/dusk-network/ccd.git"),
Some("dusk-network/ccd".to_owned())
);
}
#[test]
fn parse_github_https_url_no_suffix() {
assert_eq!(
parse_github_owner_repo("https://github.com/owner/repo"),
Some("owner/repo".to_owned())
);
}
#[test]
fn parse_github_ssh_transport_url() {
assert_eq!(
parse_github_owner_repo("ssh://git@github.com/owner/repo.git"),
Some("owner/repo".to_owned())
);
}
#[test]
fn parse_github_url_rejects_ambiguous_paths() {
assert_eq!(
parse_github_owner_repo("https://github.com/owner/repo/issues"),
None
);
}
#[test]
fn parse_non_github_url_returns_none() {
assert_eq!(
parse_github_owner_repo("git@gitlab.com:owner/repo.git"),
None
);
}
}