use std::path::Path; use std::process::Command; use anyhow::{Context, Result, bail}; pub fn current_branch(workdir: &Path) -> Result> { let out = match Command::new("git") .current_dir(workdir) .args(["rev-parse", "--abbrev-ref", "HEAD"]) .output() { Ok(out) => out, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { bail!("`git` not found in PATH — install git to use the 'branch' check") } Err(err) => return Err(err).context("failed to spawn `git`"), }; if !out.status.success() { let stderr = String::from_utf8_lossy(&out.stderr); let fail = stderr.trim(); if fail.contains("not a git repository") || fail.contains("not inside any git repository") || fail.contains("does not appear to be a git repository") { return Ok(None); } bail!("`git rev-parse` failed: {}", fail); } let name = String::from_utf8(out.stdout) .context("git returned non-UTF-8 output")? .trim() .to_string(); if name.is_empty() { return Ok(None); } Ok(Some(name)) } #[cfg(test)] mod tests { use super::*; use std::fs; use std::path::PathBuf; fn tmpdir(tag: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!("xboct-deploy-git-{}-{tag}", std::process::id())); let _ = fs::remove_dir_all(&dir); fs::create_dir_all(&dir).unwrap(); dir } fn git(dir: &Path, args: &[&str]) { let status = Command::new("git") .current_dir(dir) .args(args) .status() .unwrap(); assert!(status.success(), "git {args:?} failed in {}", dir.display()); } fn git_repo(tag: &str) -> PathBuf { let dir = tmpdir(tag); git(&dir, &["init", "-b", "test"]); fs::write(dir.join("f.txt"), "hi").unwrap(); git(&dir, &["add", "."]); git( &dir, &[ "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "init", ], ); dir } #[test] fn returns_branch_in_repo() { let dir = git_repo("branch"); assert_eq!(current_branch(&dir).unwrap().as_deref(), Some("test")); let _ = fs::remove_dir_all(dir); } #[test] fn returns_head_when_detached() { let dir = git_repo("detached"); git(&dir, &["switch", "--detach"]); assert_eq!(current_branch(&dir).unwrap().as_deref(), Some("HEAD")); let _ = fs::remove_dir_all(dir); } #[test] fn returns_none_outside_repo() { let dir = tmpdir("norepo"); assert_eq!(current_branch(&dir).unwrap(), None); let _ = fs::remove_dir_all(dir); } #[test] fn works_from_subdirectory() { let dir = git_repo("subdir"); let sub = dir.join("inner"); fs::create_dir_all(&sub).unwrap(); assert_eq!(current_branch(&sub).unwrap().as_deref(), Some("test")); let _ = fs::remove_dir_all(dir); } }