use anyhow::Result;
use std::ffi::OsStr;
use std::path::Path;
use std::process;
#[derive(Debug, Clone, Copy)]
pub enum GitCommandOutput {
UnreachableDestination,
UnexistingFile,
NewerRevisionsAvailable,
CommandFailed,
}
fn run<I, S>(repo_path: &Path, args: I) -> Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = process::Command::new("git")
.arg("-C").arg(repo_path).args(args)
.output()
.map_err(|_| GitCommandOutput::CommandFailed)?;
if !output.status.success() {
return Err(GitCommandOutput::CommandFailed.into());
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
pub fn trust_directory(repo_path: &Path) -> Result<()> {
process::Command::new("git")
.args(["config", "--global", "--add", "safe.directory"])
.arg(repo_path)
.status()
.map_err(|_| GitCommandOutput::CommandFailed)?
.success()
.then_some(())
.ok_or(GitCommandOutput::CommandFailed)?;
Ok(())
}
pub fn set_remote_url(repo_path: &Path, remote: &str, url: &str) -> Result<()> {
run(repo_path, ["remote", "set-url", remote, url]).map(|_| ())
}
pub fn add(repo_path: &Path, file: &str) -> Result<()> {
run(repo_path, ["add", file]).map(|_| ())
}
pub fn commit(repo_path: &Path, author_name: &str, author_email: &str, message: &str) -> Result<()> {
run(
repo_path,
[
"-c".to_string(),
format!("user.name={author_name}"),
"-c".to_string(),
format!("user.email={author_email}"),
"commit".to_string(),
"-m".to_string(),
message.to_string(),
],
)
.map(|_| ())
}
pub fn push(repo_path: &Path, remote: &str, refspec: &str) -> Result<()> {
run(repo_path, ["push", remote, refspec]).map(|_| ())
}
pub fn tag(repo_path: &Path, name: &str) -> Result<()> {
run(repo_path, ["tag", name]).map(|_| ())
}
pub fn rev_parse_head(repo_path: &Path) -> Result<String> {
Ok(run(repo_path, ["rev-parse", "HEAD"])?.trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::{tempdir, TempDir};
fn init_repo() -> TempDir {
let dir = tempdir().unwrap();
process::Command::new("git").arg("-C").arg(dir.path()).args(["init", "-q"]).status().unwrap();
process::Command::new("git")
.arg("-C")
.arg(dir.path())
.args(["config", "user.email", "test@test.com"])
.status()
.unwrap();
process::Command::new("git")
.arg("-C")
.arg(dir.path())
.args(["config", "user.name", "test"])
.status()
.unwrap();
dir
}
#[test]
fn add_commit_and_rev_parse_head() {
let dir = init_repo();
fs::write(dir.path().join("file.txt"), "hello").unwrap();
add(dir.path(), "file.txt").unwrap();
commit(dir.path(), "Phoenix CI", "ci@phoenix.invalid", "chore(release): 1.0.0 [skip ci]").unwrap();
let sha = rev_parse_head(dir.path()).unwrap();
assert_eq!(sha.len(), 40);
}
#[test]
fn commit_uses_the_given_author_identity() {
let dir = init_repo();
fs::write(dir.path().join("file.txt"), "hello").unwrap();
add(dir.path(), "file.txt").unwrap();
commit(dir.path(), "Phoenix CI", "ci@phoenix.invalid", "chore(release): 1.0.0 [skip ci]").unwrap();
let log = run(dir.path(), ["log", "-1", "--pretty=format:%an <%ae>"]).unwrap();
assert_eq!(log, "Phoenix CI <ci@phoenix.invalid>");
}
#[test]
fn tag_creates_a_tag_pointing_at_head() {
let dir = init_repo();
fs::write(dir.path().join("file.txt"), "hello").unwrap();
add(dir.path(), "file.txt").unwrap();
commit(dir.path(), "n", "e", "init").unwrap();
tag(dir.path(), "1.0.0").unwrap();
let tags = run(dir.path(), ["tag", "--list"]).unwrap();
assert!(tags.contains("1.0.0"));
}
#[test]
fn set_remote_url_updates_the_config() {
let dir = init_repo();
run(dir.path(), ["remote", "add", "origin", "https://example.com/old.git"]).unwrap();
set_remote_url(dir.path(), "origin", "https://example.com/new.git").unwrap();
let url = run(dir.path(), ["remote", "get-url", "origin"]).unwrap();
assert_eq!(url.trim(), "https://example.com/new.git");
}
}