use anyhow::{bail, Context, Result};
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct Output {
pub status: i32,
pub stdout: String,
pub stderr: String,
}
impl Output {
pub fn ok(&self) -> bool {
self.status == 0
}
}
pub fn run(dir: &Path, args: &[&str]) -> Result<Output> {
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.with_context(|| format!("spawning git {}", args.join(" ")))?;
Ok(Output {
status: out.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
})
}
pub fn check(dir: &Path, args: &[&str]) -> Result<String> {
let o = run(dir, args)?;
if !o.ok() {
bail!("git {} failed: {}", args.join(" "), o.stderr.trim());
}
Ok(o.stdout)
}
pub fn is_repo(dir: &Path) -> bool {
dir.join(".git").exists()
&& run(dir, &["rev-parse", "--is-inside-work-tree"])
.map(|o| o.ok())
.unwrap_or(false)
}
pub fn current_branch(dir: &Path) -> Result<String> {
Ok(check(dir, &["rev-parse", "--abbrev-ref", "HEAD"])?
.trim()
.to_string())
}
pub fn head_sha(dir: &Path) -> Result<String> {
Ok(check(dir, &["rev-parse", "HEAD"])?.trim().to_string())
}
pub fn is_dirty(dir: &Path) -> Result<bool> {
Ok(!check(dir, &["status", "--porcelain"])?.trim().is_empty())
}
pub fn ahead_behind(dir: &Path) -> Result<Option<(u32, u32)>> {
let o = run(
dir,
&["rev-list", "--left-right", "--count", "HEAD...@{upstream}"],
)?;
if !o.ok() {
return Ok(None);
}
let mut it = o.stdout.split_whitespace();
let ahead = it.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let behind = it.next().and_then(|s| s.parse().ok()).unwrap_or(0);
Ok(Some((ahead, behind)))
}
pub fn clone(parent: &Path, url: &str, into: &str) -> Result<()> {
let o = Command::new("git")
.current_dir(parent)
.args(["clone", url, into])
.output()
.context("spawning git clone")?;
if !o.status.success() {
bail!(
"git clone {url} failed: {}",
String::from_utf8_lossy(&o.stderr).trim()
);
}
Ok(())
}
pub fn fetch(dir: &Path) -> Result<()> {
check(
dir,
&[
"-c",
"submodule.recurse=false",
"-c",
"fetch.recurseSubmodules=false",
"fetch",
"--all",
"--prune",
"--tags",
"--recurse-submodules=no",
],
)?;
Ok(())
}
pub fn checkout(dir: &Path, rev: &str) -> Result<()> {
check(dir, &["checkout", rev])?;
Ok(())
}
pub fn checkout_new_branch(dir: &Path, branch: &str) -> Result<()> {
check(dir, &["checkout", "-B", branch])?;
Ok(())
}
#[allow(dead_code)]
pub fn branch_exists(dir: &Path, branch: &str) -> bool {
run(
dir,
&[
"show-ref",
"--verify",
"--quiet",
&format!("refs/heads/{branch}"),
],
)
.map(|o| o.ok())
.unwrap_or(false)
}
pub fn set_gitlink(repo: &Path, path: &str, sha: &str) -> Result<()> {
check(
repo,
&[
"update-index",
"--add",
"--cacheinfo",
&format!("160000,{sha},{path}"),
],
)?;
Ok(())
}
pub fn gitlink_sha(repo: &Path, path: &str) -> Option<String> {
let o = run(repo, &["ls-files", "--stage", "--", path]).ok()?;
let line = o.stdout.lines().next()?;
let mut it = line.split_whitespace();
if it.next()? != "160000" {
return None;
}
Some(it.next()?.to_string())
}
#[allow(dead_code)] pub fn set_skip_worktree(repo: &Path, path: &str, skip: bool) -> Result<()> {
let flag = if skip {
"--skip-worktree"
} else {
"--no-skip-worktree"
};
check(repo, &["update-index", flag, path])?;
Ok(())
}
#[allow(dead_code)] pub fn submodule_deinit(repo: &Path, path: &str) -> Result<()> {
check(repo, &["submodule", "deinit", "-f", path])?;
Ok(())
}
#[cfg(test)]
pub mod testutil {
use super::*;
pub fn init_repo(dir: &Path) {
check(dir, &["init", "-q", "-b", "main"]).unwrap();
check(dir, &["config", "user.email", "t@t.t"]).unwrap();
check(dir, &["config", "user.name", "t"]).unwrap();
check(dir, &["config", "commit.gpgsign", "false"]).unwrap();
std::fs::write(dir.join("README.md"), "init\n").unwrap();
check(dir, &["add", "-A"]).unwrap();
check(dir, &["commit", "-q", "-m", "init"]).unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn detects_repo_and_state() {
let d = tempdir().unwrap();
assert!(!is_repo(d.path()));
testutil::init_repo(d.path());
assert!(is_repo(d.path()));
assert_eq!(current_branch(d.path()).unwrap(), "main");
assert!(!is_dirty(d.path()).unwrap());
std::fs::write(d.path().join("x"), "y").unwrap();
assert!(is_dirty(d.path()).unwrap());
assert_eq!(head_sha(d.path()).unwrap().len(), 40);
}
#[test]
fn gitlink_set_without_checkout() {
let d = tempdir().unwrap();
testutil::init_repo(d.path());
let sha = head_sha(d.path()).unwrap();
set_gitlink(d.path(), "dep", &sha).unwrap();
check(d.path(), &["commit", "-q", "-m", "add gitlink"]).unwrap();
assert_eq!(gitlink_sha(d.path(), "dep").as_deref(), Some(sha.as_str()));
std::fs::write(d.path().join("x2"), "z").unwrap();
check(d.path(), &["add", "-A"]).unwrap();
check(d.path(), &["commit", "-q", "-m", "c2"]).unwrap();
let other = head_sha(d.path()).unwrap();
assert_ne!(other, sha);
set_gitlink(d.path(), "dep", &other).unwrap();
assert_eq!(
gitlink_sha(d.path(), "dep").as_deref(),
Some(other.as_str())
);
}
}