use std::ffi::OsStr;
use std::path::Path;
use std::process::{Command, ExitStatus, Output};
use std::time::SystemTime;
use anyhow::Result;
use thiserror::Error;
use crate::util;
pub use git_state::{RepositoryState, git_state};
#[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";
const GIT_EXIT_STATUS_NOT_FOUND: i32 = 1;
pub fn git_init(repo: &Path) -> Result<()> {
git(repo, ["init", "-q"], false)
}
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, true)
}
pub fn git_add_all(repo: &Path) -> Result<()> {
git(repo, ["add", "."], false)
}
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, false)
}
pub fn git_reset_hard(repo: &Path) -> Result<()> {
git(repo, ["reset", "--hard", "-q"], false)
}
pub fn git_status(repo: &Path, short: bool) -> Result<String> {
let mut args = vec!["status"];
if short {
args.push("--short");
}
git_stdout_ok(repo, &args, false)
}
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, true)
}
pub fn git_pull(repo: &Path) -> Result<()> {
git(repo, ["pull", "-q"], true)
}
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, true)
}
pub fn git_has_changes(repo: &Path) -> Result<bool> {
Ok(!git_stdout_ok(repo, ["status", "-s"], false)?.is_empty())
}
pub fn git_has_remote(repo: &Path) -> Result<bool> {
Ok(!git_stdout_ok(repo, ["remote"], false)?.is_empty())
}
pub fn git_remote(repo: &Path) -> Result<Vec<String>> {
Ok(git_stdout_ok(repo, ["remote"], false)?
.lines()
.map(|r| r.into())
.collect())
}
pub fn git_remote_get_url(repo: &Path, remote: &str) -> Result<String> {
git_stdout_ok(repo, ["remote", "get-url", remote], false)
}
pub fn git_remote_add(repo: &Path, remote: &str, url: &str) -> Result<()> {
git(repo, ["remote", "add", remote, url], false)
}
pub fn git_remote_remove(repo: &Path, remote: &str) -> Result<()> {
git(repo, ["remote", "remove", remote], false)
}
pub fn git_current_branch(repo: &Path) -> Result<String> {
let branch = git_stdout_ok(repo, ["rev-parse", "--abbrev-ref", "HEAD"], false)?;
assert!(!branch.is_empty(), "git returned empty branch name");
assert!(!branch.contains('\n'), "git returned multiple branches");
Ok(branch)
}
pub fn git_config_branch_remote(repo: &Path, branch: &str) -> Result<Option<String>> {
let mut remote = git_stdout_ok_or(
repo,
["config", "--get", &format!("branch.{branch}.remote")],
false,
GIT_EXIT_STATUS_NOT_FOUND,
)?;
if remote.is_empty() {
remote = git_stdout_ok_or(
repo,
["config", "--get", "remote.pushDefault"],
false,
GIT_EXIT_STATUS_NOT_FOUND,
)?;
}
if remote.is_empty() {
return Ok(None);
}
assert!(!branch.contains('\n'), "git returned multiple remotes");
Ok(Some(remote))
}
pub fn git_config_branch_set_remote(repo: &Path, branch: &str, remote: &str) -> Result<()> {
git_stdout_ok(
repo,
["config", &format!("branch.{branch}.remote"), remote],
false,
)
.map(|_| ())
}
pub fn git_branch_remote(repo: &Path) -> Result<Vec<String>> {
Ok(git_stdout_ok(repo, ["branch", "-r", "--no-color"], false)?
.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()),
],
false,
)?;
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, false)
}
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()], false)?;
assert_eq!(hash.len(), 40, "git returned invalid hash");
Ok(hash)
}
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, connects_remote: bool) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
cmd_assert_status(
cmd_git(args, repo, connects_remote)
.status()
.map_err(Err::System)?,
)
}
fn git_output<I, S>(repo: &Path, args: I, connects_remote: bool) -> Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
cmd_git(args, repo, connects_remote)
.output()
.map_err(|err| Err::System(err).into())
}
fn git_stdout_ok_or<I, S>(
repo: &Path,
args: I,
connects_remote: bool,
allow_exit_code: i32,
) -> Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = git_output(repo, args, connects_remote)?;
if output.status.code() != Some(allow_exit_code) {
cmd_assert_status(output.status)?;
}
Ok(std::str::from_utf8(&output.stdout)
.map_err(|err| Err::GitCli(err.into()))?
.trim()
.into())
}
fn git_stdout_ok<I, S>(repo: &Path, args: I, connects_remote: bool) -> Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = git_output(repo, args, connects_remote)?;
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: &Path, connects_remote: bool) -> Command
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut cmd = Command::new(BIN_NAME);
cmd.arg("-C");
cmd.arg(dir);
cmd.current_dir(dir);
if connects_remote && util::git::guess_ssh_persist_support(dir) {
util::git::configure_ssh_persist(&mut cmd);
}
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("git operation exited with non-zero status code: {0}")]
Status(std::process::ExitStatus),
}