use std::ffi::OsStr;
use std::path::Path;
use std::process::{Command, ExitStatus, Output};
use std::time::SystemTime;
use anyhow::Result;
use thiserror::Error;
pub use git_state::{git_state, RepositoryState};
#[cfg(not(windows))]
pub const BIN_NAME: &str = "git";
#[cfg(windows)]
pub const BIN_NAME: &str = "git.exe";
const GIT_FETCH_HEAD_FILE: &str = ".git/FETCH_HEAD";
pub fn git_init(repo: &Path) -> Result<()> {
git(repo, &["init", "-q"])
}
pub fn git_clone(repo: &Path, url: &str, path: &str, quiet: bool) -> Result<()> {
let mut args = vec!["clone", "-q"];
if !quiet {
args.push("--progress");
}
args.extend_from_slice(&[url, path]);
git(repo, &args)
}
pub fn git_add_all(repo: &Path) -> Result<()> {
git(repo, &["add", "."])
}
pub fn git_commit(repo: &Path, msg: &str, commit_empty: bool) -> Result<()> {
if !commit_empty && !git_has_changes(repo)? {
return Ok(());
}
let mut args = vec!["commit", "-q", "--no-edit", "-m", msg];
if commit_empty {
args.push("--allow-empty");
}
git(repo, &args)
}
pub fn git_push(repo: &Path, set_branch: Option<&str>, set_upstream: Option<&str>) -> Result<()> {
let mut args = vec!["push", "-q"];
if let Some(upstream) = set_upstream {
args.extend_from_slice(&["--set-upstream", upstream]);
}
if let Some(branch) = set_branch {
args.push(branch);
}
git(repo, &args)
}
pub fn git_pull(repo: &Path) -> Result<()> {
git(repo, &["pull", "-q"])
}
pub fn git_fetch(repo: &Path, reference: Option<&str>) -> Result<()> {
let mut args = vec!["fetch", "-q"];
if let Some(reference) = reference {
args.push(reference);
}
git(repo, &args)
}
pub fn git_has_changes(repo: &Path) -> Result<bool> {
Ok(!git_stdout_ok(repo, &["status", "-s"])?.is_empty())
}
pub fn git_has_remote(repo: &Path) -> Result<bool> {
Ok(!git_stdout_ok(repo, &["remote"])?.is_empty())
}
pub fn git_remote(repo: &Path) -> Result<Vec<String>> {
Ok(git_stdout_ok(repo, &["remote"])?
.lines()
.map(|r| r.into())
.collect())
}
pub fn git_remote_get_url(repo: &Path, remote: &str) -> Result<String> {
Ok(git_stdout_ok(repo, &["remote", "get-url", remote])?)
}
pub fn git_remote_add(repo: &Path, remote: &str, url: &str) -> Result<()> {
Ok(git(repo, &["remote", "add", remote, url])?)
}
pub fn git_remote_remove(repo: &Path, remote: &str) -> Result<()> {
Ok(git(repo, &["remote", "remove", remote])?)
}
pub fn git_current_branch(repo: &Path) -> Result<String> {
let branch = git_stdout_ok(repo, &["rev-parse", "--abbrev-ref", "HEAD"])?;
assert!(!branch.is_empty(), "git returned empty branch name");
assert!(!branch.contains("\n"), "git returned multiple branches");
Ok(branch.into())
}
pub fn git_branch_remote(repo: &Path) -> Result<Vec<String>> {
Ok(git_stdout_ok(repo, &["branch", "-r", "--no-color"])?
.lines()
.map(|r| {
match r.strip_prefix("* ") {
Some(r) => r,
None => r,
}
.to_string()
})
.collect())
}
pub fn git_branch_upstream<S: AsRef<str>>(repo: &Path, reference: S) -> Result<Option<String>> {
let output = git_output(
repo,
&[
"rev-parse",
"--abbrev-ref",
&format!("{}@{{upstream}}", reference.as_ref()),
],
)?;
let stderr = std::str::from_utf8(&output.stderr)
.map_err(|err| Err::GitCli(err.into()))?
.trim();
if stderr.contains("fatal: no upstream configured for branch") {
return Ok(None);
}
cmd_assert_status(output.status)?;
let upstream = std::str::from_utf8(&output.stdout)
.map_err(|err| Err::GitCli(err.into()))?
.trim();
if upstream.is_empty() {
return Ok(None);
}
assert!(
upstream.contains("/"),
"git returned invalid upstream branch name"
);
Ok(Some(upstream.into()))
}
pub fn git_branch_set_upstream(repo: &Path, reference: Option<&str>, upstream: &str) -> Result<()> {
let mut args = vec!["branch", "--set-upstream-to", &upstream];
if let Some(reference) = reference {
args.push(reference);
}
git(repo, &args)
}
pub fn git_ref_hash<S: AsRef<str>>(repo: &Path, reference: S) -> Result<String> {
let hash = git_stdout_ok(repo, &["rev-parse", reference.as_ref()])?;
assert_eq!(hash.len(), 40, "git returned invalid hash");
Ok(hash.into())
}
pub fn git_last_pull_time(repo: &Path) -> Result<SystemTime> {
Ok(repo
.join(GIT_FETCH_HEAD_FILE)
.metadata()
.and_then(|m| m.modified())
.map_err(Err::Other)?)
}
fn git<I, S>(repo: &Path, args: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
cmd_assert_status(cmd_git(args, Some(repo)).status().map_err(Err::System)?)
}
fn git_output<I, S>(repo: &Path, args: I) -> Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
cmd_git(args, Some(repo))
.output()
.map_err(|err| Err::System(err).into())
}
fn git_stdout_ok<I, S>(repo: &Path, args: I) -> Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = git_output(repo, args)?;
cmd_assert_status(output.status)?;
Ok(std::str::from_utf8(&output.stdout)
.map_err(|err| Err::GitCli(err.into()))?
.trim()
.into())
}
fn cmd_git<I, S>(args: I, dir: Option<&Path>) -> Command
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut cmd = Command::new(BIN_NAME);
if let Some(dir) = dir {
cmd.arg("-C");
cmd.arg(dir);
cmd.current_dir(dir);
}
cmd.args(args);
cmd
}
fn cmd_assert_status(status: ExitStatus) -> Result<()> {
if !status.success() {
return Err(Err::Status(status).into());
}
Ok(())
}
#[derive(Debug, Error)]
pub enum Err {
#[error("failed to complete git operation")]
Other(#[source] std::io::Error),
#[error("failed to complete git operation")]
GitCli(#[source] anyhow::Error),
#[error("failed to invoke system command")]
System(#[source] std::io::Error),
#[error("system command exited with non-zero status code: {0}")]
Status(std::process::ExitStatus),
}