pub use posix_errors::{PosixError, EACCES, EINVAL, ENOENT};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::Output;
mod bare_repo;
pub use crate::bare_repo::*;
pub mod x;
macro_rules! cmd {
($args:expr) => {
Command::new("git").args($args).output()
};
($name:expr, $args:expr) => {
Command::new("git").arg($name).args($args).output()
};
}
#[inline]
pub fn ls_remote(args: &[&str]) -> Result<Output, PosixError> {
let result = cmd!("ls-remote", args);
if let Ok(value) = result {
return Ok(value);
}
Err(PosixError::from(result.unwrap_err()))
}
#[inline]
pub fn tags_from_remote(url: &str) -> Result<Vec<String>, PosixError> {
let mut vec = Vec::new();
let output = ls_remote(&["--refs", "--tags", url])?;
if output.status.success() {
let tmp = String::from_utf8(output.stdout).expect("Expected UTF-8");
for s in tmp.lines() {
let mut split = s.splitn(3, '/');
split.next();
split.next();
let split_result = split.next();
if let Some(value) = split_result {
vec.push(String::from(value));
}
}
Ok(vec)
} else {
Err(PosixError::from(output))
}
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug)]
pub enum ConfigReadError {
#[error("Invalid section key in config {0}")]
InvalidSectionOrKey(String),
#[error("Invalid config file {0}")]
InvalidConfigFile(String),
#[error("{0}")]
Failure(String, i32),
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug)]
pub enum ConfigSetError {
#[error("{0}")]
InvalidSectionOrKey(String),
#[error("{0}")]
InvalidConfigFile(String),
#[error("{0}")]
WriteFailed(String),
#[error("{0}")]
Failure(String, i32),
}
#[inline]
pub fn config_file_set(file: &Path, key: &str, value: &str) -> Result<(), ConfigSetError> {
let args = &["--file", file.to_str().expect("UTF-8 encoding"), key, value];
let mut cmd = Command::new("git");
cmd.arg("config").args(args);
let out = cmd.output().expect("Failed to execute git-config(1)");
if out.status.success() {
Ok(())
} else {
let msg = String::from_utf8(out.stdout).expect("UTF-8 encoding");
match out.status.code().unwrap_or(1) {
1 => Err(ConfigSetError::InvalidSectionOrKey(msg)),
3 => Err(ConfigSetError::InvalidConfigFile(msg)),
4 => Err(ConfigSetError::WriteFailed(msg)),
code => Err(ConfigSetError::Failure(msg, code)),
}
}
}
#[inline]
pub fn resolve_head(remote: &str) -> Result<String, PosixError> {
let proc =
cmd!("ls-remote", vec!["--symref", remote, "HEAD"]).expect("Failed to execute git command");
if proc.status.success() {
let stdout = String::from_utf8(proc.stdout).expect("UTF-8 encoding");
let mut lines = stdout.lines();
let first_line = lines.next().expect("Failed to parse HEAD from remote");
let mut split = first_line
.split('\t')
.next()
.expect("Failed to parse HEAD from remote")
.splitn(3, '/');
split.next();
split.next();
return Ok(split
.next()
.expect("Failed to parse default branch")
.to_owned());
}
Err(PosixError::from(proc))
}
enum RemoteDir {
Fetch,
Push,
}
struct RemoteLine {
name: String,
url: String,
dir: RemoteDir,
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Remote {
pub name: String,
pub push: Option<String>,
pub fetch: Option<String>,
}
fn cwd() -> Result<PathBuf, RepoError> {
if let Ok(result) = std::env::current_dir() {
Ok(result)
} else {
Err(RepoError::FailAccessCwd)
}
}
#[derive(Debug, Clone)]
pub struct AbsoluteDirPath(PathBuf);
impl AsRef<OsStr> for AbsoluteDirPath {
#[inline]
fn as_ref(&self) -> &OsStr {
self.0.as_os_str()
}
}
impl TryFrom<&Path> for AbsoluteDirPath {
type Error = RepoError;
#[inline]
fn try_from(value: &Path) -> Result<Self, Self::Error> {
let path_buf;
if value.is_absolute() {
path_buf = value.to_path_buf();
} else if let Ok(p) = value.canonicalize() {
path_buf = p;
} else {
return Err(RepoError::AbsolutionError(value.to_path_buf()));
}
Ok(Self(path_buf))
}
}
trait GenericRepository {
#[inline]
fn gen_config(&self, key: &str) -> Result<String, ConfigReadError> {
let out = self
.gen_git()
.arg("config")
.arg(key)
.output()
.expect("Failed to execute git-config(1)");
if out.status.success() {
Ok(String::from_utf8(out.stdout)
.expect("UTF-8 encoding")
.trim()
.to_owned())
} else {
match out.status.code().unwrap_or(3) {
1 => Err(ConfigReadError::InvalidSectionOrKey(key.to_owned())),
3 => {
let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
Err(ConfigReadError::InvalidConfigFile(msg))
}
code => {
let msg = String::from_utf8_lossy(out.stderr.as_ref());
Err(ConfigReadError::Failure(msg.to_string(), code))
}
}
}
}
#[must_use]
fn gen_git(&self) -> Command;
}
#[derive(Clone, Debug)]
pub struct Repository {
git_dir: AbsoluteDirPath,
work_tree: AbsoluteDirPath,
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum RepoError {
#[error("GIT_DIR Not found")]
GitDirNotFound,
#[error("Bare repository")]
BareRepo,
#[error("Invalid directory: `{0}`")]
InvalidDirectory(PathBuf),
#[error("Failed to canonicalize the path buffer: `{0}`")]
AbsolutionError(PathBuf),
#[error("Failed to access current working directory")]
FailAccessCwd,
}
impl From<RepoError> for PosixError {
#[inline]
fn from(e: RepoError) -> Self {
let msg = format!("{}", e);
match e {
RepoError::GitDirNotFound | RepoError::InvalidDirectory(_) => Self::new(ENOENT, msg),
RepoError::AbsolutionError(_) => Self::new(EINVAL, msg),
RepoError::FailAccessCwd => Self::new(EACCES, msg),
RepoError::BareRepo => Self::new(EINVAL, format!("{}", e)),
}
}
}
fn search_git_dir(start: &Path) -> Result<AbsoluteDirPath, RepoError> {
let path;
if start.is_absolute() {
path = start.to_path_buf();
} else {
match start.canonicalize() {
Ok(path_buf) => path = path_buf,
Err(_) => return Err(RepoError::InvalidDirectory(start.to_path_buf())),
}
}
if let (Ok(head_path), Ok(objects_path)) = (
path.join("HEAD").canonicalize(),
path.join("objects").canonicalize(),
) {
if head_path.is_file() && objects_path.is_dir() {
return AbsoluteDirPath::try_from(path.as_path());
}
}
for parent in path.ancestors() {
let candidate = parent.join(".git");
if candidate.is_dir() && candidate.exists() {
return candidate.as_path().try_into();
}
}
Err(RepoError::GitDirNotFound)
}
fn work_tree_from_git_dir(git_dir: &AbsoluteDirPath) -> Result<AbsoluteDirPath, RepoError> {
let mut cmd = Command::new("git");
cmd.arg("--git-dir");
cmd.arg(git_dir.0.as_os_str());
cmd.args(&["rev-parse", "--is-bare-repository"]);
let output = cmd.output().expect("failed to execute rev-parse");
if output.status.success() {
let tmp = String::from_utf8_lossy(&output.stdout);
if tmp.trim() == "true" {
return Err(RepoError::BareRepo);
}
}
match git_dir.0.parent() {
Some(dir) => Ok(AbsoluteDirPath::try_from(dir)?),
None => Err(RepoError::BareRepo),
}
}
fn git_dir_from_work_tree(work_tree: &AbsoluteDirPath) -> Result<AbsoluteDirPath, RepoError> {
let result = work_tree.0.join(".git");
result.as_path().try_into()
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
#[error("Invalid git reference {0}")]
pub struct InvalidRefError(String);
impl Repository {
#[must_use]
#[inline]
pub fn is_clean(&self) -> bool {
let output = self
.git()
.args(&["diff", "--quiet", "HEAD"])
.output()
.expect("Failed to execute git-diff(1)");
output.status.success()
}
#[must_use]
#[inline]
pub fn is_shallow(&self) -> bool {
let out = self
.git()
.args(&["rev-parse", "--is-shallow-repository"])
.output()
.expect("Failed to execute git-rev-parse(1)");
String::from_utf8_lossy(&out.stdout).trim() != "false"
}
#[must_use]
#[inline]
pub fn remotes(&self) -> Option<HashMap<String, Remote>> {
let args = &["remote", "-v"];
let mut cmd = self.git();
let out = cmd
.args(args)
.output()
.expect("Failed to execute git-remote(1)");
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
let mut my_map: HashMap<String, Remote> = HashMap::new();
let mut remote_lines: Vec<RemoteLine> = vec![];
for line in text.lines() {
let mut split = line.trim().split('\t');
let name = split.next().expect("Remote name").to_owned();
let rest = split.next().expect("Remote rest");
let mut rest_split = rest.split(' ');
let url = rest_split.next().expect("Remote url").to_owned();
let dir = if rest_split.next().expect("Remote direction") == "(fetch)" {
RemoteDir::Fetch
} else {
RemoteDir::Push
};
remote_lines.push(RemoteLine { name, url, dir });
}
for remote_line in remote_lines {
let mut remote = my_map.remove(&remote_line.name).unwrap_or(Remote {
name: remote_line.name.clone(),
push: None,
fetch: None,
});
match remote_line.dir {
RemoteDir::Fetch => remote.fetch = Some(remote_line.url.clone()),
RemoteDir::Push => remote.push = Some(remote_line.url.clone()),
}
my_map.insert(remote_line.name.clone(), remote);
}
Some(my_map)
}
#[must_use]
#[inline]
pub fn head(&self) -> String {
let args = &["rev-parse", "HEAD"];
let mut cmd = self.git();
let out = cmd
.args(args)
.output()
.expect("Failed to execute git-rev-parse(1)");
assert!(
out.status.success(),
"git rev-parse returned unexpected error"
);
String::from_utf8_lossy(&out.stdout).trim().to_owned()
}
#[must_use]
#[inline]
pub fn work_tree(&self) -> Option<PathBuf> {
Some(self.work_tree.0.clone())
}
#[must_use]
#[inline]
pub fn is_sparse(&self) -> bool {
let path = self.git_dir_path().join("info").join("sparse-checkout");
path.exists()
}
const fn git_dir(&self) -> &AbsoluteDirPath {
&self.git_dir
}
const fn git_dir_path(&self) -> &PathBuf {
&self.git_dir.0
}
#[inline]
pub fn short_ref(&self, long_ref: &str) -> Result<String, InvalidRefError> {
let args = vec!["rev-parse", "--short", long_ref];
let mut cmd = self.git();
let out = cmd
.args(args)
.output()
.expect("Failed to execute git-commit(1)");
if !out.status.success() {
return Err(InvalidRefError(long_ref.to_owned()));
}
let short_ref = String::from_utf8_lossy(&out.stderr).to_string();
Ok(short_ref)
}
}
impl Repository {
#[inline]
pub fn discover(path: &Path) -> Result<Self, RepoError> {
let git_dir = search_git_dir(path)?;
let work_tree = work_tree_from_git_dir(&git_dir)?;
Ok(Self { git_dir, work_tree })
}
#[inline]
pub fn default() -> Result<Self, RepoError> {
Self::from_args(None, None, None)
}
#[inline]
pub fn create(path: &Path) -> Result<Self, String> {
let mut cmd = Command::new("git");
let out = cmd
.arg("init")
.current_dir(&path)
.output()
.expect("Executed git-init(1)");
if out.status.success() {
let work_tree = path.try_into().map_err(|e| format!("{}", e))?;
let git_dir = path
.join(".git")
.as_path()
.try_into()
.map_err(|e| format!("{}", e))?;
Ok(Self { git_dir, work_tree })
} else {
Err(String::from_utf8_lossy(&out.stderr).to_string())
}
}
#[inline]
pub fn from_args(
change: Option<&str>,
git: Option<&str>,
work: Option<&str>,
) -> Result<Self, RepoError> {
if (change, git, work) == (None, None, None) {
let git_dir = if let Ok(gd) = std::env::var("GIT_DIR") {
AbsoluteDirPath::try_from(gd.as_ref())?
} else {
search_git_dir(&cwd()?)?
};
let work_tree = if let Ok(wt) = std::env::var("GIT_WORK_TREE") {
AbsoluteDirPath::try_from(wt.as_ref())?
} else {
work_tree_from_git_dir(&git_dir)?
};
Ok(Self { git_dir, work_tree })
} else {
let root = change.map_or_else(PathBuf::new, PathBuf::from);
match (git, work) {
(Some(g_dir), None) => {
let git_dir = root.join(g_dir).as_path().try_into()?;
let work_tree = work_tree_from_git_dir(&git_dir)?;
Ok(Self { git_dir, work_tree })
}
(None, Some(w_dir)) => {
let work_tree = root.join(w_dir).as_path().try_into()?;
let git_dir = git_dir_from_work_tree(&work_tree)?;
Ok(Self { git_dir, work_tree })
}
(Some(g_dir), Some(w_dir)) => {
let git_dir = root.join(g_dir).as_path().try_into()?;
let work_tree = root.join(w_dir).as_path().try_into()?;
Ok(Self { git_dir, work_tree })
}
(None, None) => {
let git_dir = search_git_dir(&root)?;
let work_tree = work_tree_from_git_dir(&git_dir)?;
Ok(Self { git_dir, work_tree })
}
}
}
}
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum SubtreeAddError {
#[error("Bare repository")]
BareRepository,
#[error("Working tree dirty")]
WorkTreeDirty,
#[error("{0}")]
Failure(String, i32),
}
impl From<SubtreeAddError> for PosixError {
#[inline]
fn from(err: SubtreeAddError) -> Self {
match err {
SubtreeAddError::BareRepository | SubtreeAddError::WorkTreeDirty => {
Self::new(EINVAL, format!("{}", err))
}
SubtreeAddError::Failure(msg, code) => Self::new(code, msg),
}
}
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum SubtreePullError {
#[error("Working tree dirty")]
WorkTreeDirty,
#[error("{0}")]
Failure(String, i32),
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum SubtreePushError {
#[error("{0}")]
Failure(String, i32),
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum SubtreeSplitError {
#[error("Work tree is dirty")]
WorkTreeDirty,
#[error("{0}")]
Failure(String, i32),
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum StagingError {
#[error("`{0}`")]
Failure(String, i32),
#[error("File does not exist: `{0}`")]
FileDoesNotExist(PathBuf),
}
impl From<StagingError> for PosixError {
#[inline]
fn from(e: StagingError) -> Self {
let msg = format!("{}", e);
match e {
StagingError::FileDoesNotExist(_) => Self::new(ENOENT, msg),
StagingError::Failure(_, code) => Self::new(code, msg),
}
}
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug)]
pub enum StashingError {
#[error("Failed to stash changes in GIT_WORK_TREE")]
Save(i32, String),
#[error("Failed to pop stashed changes in GIT_WORK_TREE")]
Pop(i32, String),
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug)]
pub enum CommitError {
#[error("`{0}`")]
Failure(String, i32),
}
#[derive(thiserror::Error, Debug)]
pub enum RefSearchError {
#[error("{0}")]
Failure(String),
#[error("{0}")]
IOError(#[from] std::io::Error),
#[error("Failed to find reference {0}")]
NotFound(String),
#[error("Failed to parse git-ls-remote(1) output: {0}")]
ParsingFailure(String),
}
impl From<RefSearchError> for PosixError {
#[inline]
fn from(err: RefSearchError) -> Self {
match err {
RefSearchError::Failure(msg) => Self::new(ENOENT, msg),
RefSearchError::IOError(e) => e.into(),
RefSearchError::NotFound(s) => Self::new(ENOENT, s),
RefSearchError::ParsingFailure(e) => Self::new(EINVAL, e),
}
}
}
impl Repository {
#[inline]
pub fn commit(&self, message: &str) -> Result<(), CommitError> {
let out = self
.git()
.args(&["commit", "-m", message])
.output()
.expect("Executed git-commit(1)");
if out.status.code().unwrap_or(1) != 0 {
let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
let code = out.status.code().unwrap_or(1);
return Err(CommitError::Failure(msg, code));
}
Ok(())
}
#[inline]
pub fn commit_extended(
&self,
message: &str,
allow_empty: bool,
no_verify: bool,
) -> Result<(), CommitError> {
let mut cmd = self.git();
cmd.args(&["commit", "--quiet", "--no-edit"]);
if allow_empty {
cmd.arg("--allow-empty");
}
if no_verify {
cmd.arg("--no-verify");
}
cmd.args(&["--message", message]);
let out = cmd.output().expect("Failed to execute git-commit(1)");
if out.status.code().expect("Expected exit code") != 0 {
let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
let code = out.status.code().unwrap_or(1);
return Err(CommitError::Failure(msg, code));
}
Ok(())
}
#[inline]
pub fn hack_read_file(&self, path: &Path) -> std::io::Result<Vec<u8>> {
let absolute_path = self.work_tree.0.join(path);
std::fs::read(absolute_path)
}
#[must_use]
#[inline]
pub fn is_ancestor(&self, first: &str, second: &str) -> bool {
let args = vec!["merge-base", "--is-ancestor", first, second];
let mut cmd = self.git();
cmd.args(args);
let proc = cmd.output().expect("Failed to run git-merge-base(1)");
proc.status.success()
}
#[inline]
pub fn remote_ref_to_id(&self, remote: &str, git_ref: &str) -> Result<String, RefSearchError> {
let proc = self.git().args(&["ls-remote", remote, git_ref]).output()?;
if !proc.status.success() {
let msg = String::from_utf8_lossy(proc.stderr.as_ref()).to_string();
return Err(RefSearchError::Failure(msg));
}
let stdout = String::from_utf8_lossy(&proc.stdout);
if let Some(first_line) = stdout.lines().next() {
if let Some(id) = first_line.split('\t').next() {
return Ok(id.to_owned());
}
return Err(RefSearchError::ParsingFailure(first_line.to_owned()));
}
Err(RefSearchError::NotFound(git_ref.to_owned()))
}
#[inline]
pub fn sparse_checkout_add(&self, pattern: &str) -> Result<(), String> {
let out = self
.git()
.args(["sparse-checkout", "add"])
.arg(pattern)
.output()
.expect("Failed to execute git sparse-checkout");
if out.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(out.stderr.as_ref()).to_string())
}
}
#[inline]
pub fn stage(&self, path: &Path) -> Result<(), StagingError> {
let relative_path = if path.is_absolute() {
path.strip_prefix(self.work_tree().expect("Non bare repo"))
.expect("Stripped path prefix")
} else {
path
};
let file = relative_path.as_os_str();
let out = self
.git()
.args(&["add", "--"])
.arg(file)
.output()
.expect("Executed git-add(1)");
match out.status.code().unwrap_or(1) {
0 => Ok(()),
128 => Err(StagingError::FileDoesNotExist(relative_path.to_path_buf())),
e => {
let msg = String::from_utf8_lossy(&out.stdout).to_string();
Err(StagingError::Failure(msg, e))
}
}
}
#[inline]
pub fn stash_almost_all(&self, message: &str) -> Result<(), StashingError> {
let mut cmd = self.git();
cmd.arg("stash");
cmd.arg("--quiet");
cmd.args(&["--include-untracked", "-m", message]);
let out = cmd.output().expect("Failed to execute git-stash(1)");
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
let code = out.status.code().unwrap_or(1);
return Err(StashingError::Save(code, stderr));
}
Ok(())
}
#[inline]
pub fn stash_pop(&self) -> Result<(), StashingError> {
let mut cmd = self.git();
let out = cmd
.args(&["stash", "pop", "--quiet", "--index"])
.output()
.expect("Failed to execute git-stash(1)");
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
let code = out.status.code().unwrap_or(1);
return Err(StashingError::Pop(code, stderr));
}
Ok(())
}
#[inline]
pub fn subtree_add(
&self,
url: &str,
prefix: &str,
revision: &str,
message: &str,
) -> Result<(), SubtreeAddError> {
if !self.is_clean() {
return Err(SubtreeAddError::WorkTreeDirty);
}
let args = vec!["-q", "-P", prefix, url, revision, "-m", message];
let mut cmd = self.git();
cmd.arg("subtree").arg("add").args(args);
let out = cmd.output().expect("Failed to execute git-subtree(1)");
if out.status.success() {
Ok(())
} else {
let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
let code = out.status.code().unwrap_or(1);
Err(SubtreeAddError::Failure(msg, code))
}
}
#[inline]
pub fn subtree_split(&self, prefix: &str) -> Result<(), SubtreeSplitError> {
if !self.is_clean() {
return Err(SubtreeSplitError::WorkTreeDirty);
}
let args = vec!["-P", prefix, "--rejoin", "HEAD"];
let mut cmd = self.git();
cmd.arg("subtree").arg("split").args(args);
let result = cmd
.spawn()
.expect("Failed to execute git-subtree(1)")
.wait();
match result {
Ok(code) => {
if code.success() {
Ok(())
} else {
Err(SubtreeSplitError::Failure(
"git-subtree split failed".to_owned(),
1,
))
}
}
Err(e) => {
let msg = format!("{}", e);
Err(SubtreeSplitError::Failure(msg, 1))
}
}
}
#[inline]
pub fn subtree_pull(
&self,
remote: &str,
prefix: &str,
git_ref: &str,
message: &str,
) -> Result<(), SubtreePullError> {
if !self.is_clean() {
return Err(SubtreePullError::WorkTreeDirty);
}
let args = vec!["-q", "-P", prefix, remote, git_ref, "-m", message];
let mut cmd = self.git();
cmd.arg("subtree").arg("pull").args(args);
let out = cmd.output().expect("Failed to execute git-subtree(1)");
if out.status.success() {
Ok(())
} else {
let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
let code = out.status.code().unwrap_or(1);
Err(SubtreePullError::Failure(msg, code))
}
}
#[inline]
pub fn subtree_push(
&self,
remote: &str,
prefix: &str,
git_ref: &str,
) -> Result<(), SubtreePushError> {
let args = vec!["subtree", "push", "-q", "-P", prefix, remote, git_ref];
let mut cmd = self.git();
cmd.args(args);
let out = cmd.output().expect("Failed to execute git-subtree(1)");
if out.status.success() {
Ok(())
} else {
let msg = String::from_utf8_lossy(out.stderr.as_ref()).to_string();
let code = out.status.code().unwrap_or(1);
Err(SubtreePushError::Failure(msg, code))
}
}
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug)]
pub enum InvalidCommitishError {
#[error("Invalid reference or commit id: `{0}`")]
One(String),
#[error("One or Multiple invalid reference or commit ids: `{0:?}`")]
Multiple(Vec<String>),
#[error("{0}")]
Failure(String, i32),
}
impl Repository {
#[inline]
pub fn merge_base(&self, ids: &[&str]) -> Result<Option<String>, InvalidCommitishError> {
let output = self
.git()
.arg("merge-base")
.args(ids)
.output()
.expect("Executing git-merge-base(1)");
if output.status.success() {
let tmp = String::from_utf8_lossy(&output.stdout);
if tmp.is_empty() {
return Ok(None);
}
let result = tmp.trim_end();
return Ok(Some(result.to_owned()));
}
match output.status.code().expect("Getting status code") {
128 => {
let tmp = ids.to_vec();
let e_ids = tmp.iter().map(ToString::to_string).collect();
Err(InvalidCommitishError::Multiple(e_ids))
}
1 => Ok(None),
code => {
let msg = String::from_utf8_lossy(&output.stdout);
Err(InvalidCommitishError::Failure(msg.to_string(), code))
}
}
}
#[must_use]
#[inline]
pub fn git(&self) -> Command {
let mut cmd = Command::new("git");
let git_dir = self.git_dir().0.to_str().expect("Convert to string");
cmd.env("GIT_DIR", git_dir);
cmd.env("GIT_WORK_TREE", &self.work_tree.0);
cmd.current_dir(&self.work_tree.0);
cmd
}
}
impl GenericRepository for Repository {
fn gen_git(&self) -> Command {
self.git()
}
}
#[inline]
pub fn setup_test_author() {
std::env::set_var("GIT_AUTHOR_NAME", "Max Musterman");
std::env::set_var("GIT_AUTHOR_EMAIL", "max@example.com");
std::env::set_var("GIT_COMMITTER_NAME", "Max Musterman");
std::env::set_var("GIT_COMMITTER_EMAIL", "max@example.com");
}
#[cfg(test)]
mod test {
mod repository_initialization {
use crate::{RepoError, Repository};
use tempfile::TempDir;
#[test]
fn git_dir_not_found() {
let tmp_dir = TempDir::new().unwrap();
let discovery_path = tmp_dir.path();
let err = Repository::discover(discovery_path)
.expect_err("Fail to find repo in an empty directory");
assert!(err == RepoError::GitDirNotFound);
}
#[test]
fn normal_repo() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let _repo = Repository::create(repo_path).unwrap();
}
}
mod is_clean {
use crate::Repository;
use tempfile::TempDir;
#[test]
fn unstaged() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = Repository::create(repo_path).expect("Created repository");
let readme = repo_path.join("README.md");
std::fs::File::create(&readme).unwrap();
std::fs::write(&readme, "# README").unwrap();
assert!(!repo.is_clean(), "Repo is unclean if sth. is unstaged");
}
#[test]
fn staged() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = Repository::create(repo_path).expect("Created repository");
let readme = repo_path.join("README.md");
std::fs::File::create(&readme).unwrap();
repo.stage(&readme).unwrap();
assert!(!repo.is_clean(), "Repo is unclean if sth. is staged");
}
}
mod config {
use crate::BareRepository;
use tempfile::TempDir;
#[test]
fn config() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = BareRepository::create(repo_path).expect("Created bare repository");
let actual = repo.config("core.bare").unwrap();
assert_eq!(actual, "true".to_owned(), "Expected true");
tmp_dir.close().unwrap();
}
}
mod sparse_checkout {
use crate::Repository;
use std::process::Command;
use tempfile::TempDir;
#[test]
fn is_sparse() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = Repository::create(repo_path).expect("Created repository");
let mut cmd = Command::new("git");
let out = cmd
.args(&["sparse-checkout", "init"])
.current_dir(repo_path)
.output()
.unwrap();
assert!(out.status.success(), "Try to make repository sparse");
assert!(repo.is_sparse(), "Not sparse repository");
}
#[test]
fn not_sparse() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = Repository::create(repo_path).expect("Created repository");
assert!(!repo.is_sparse(), "Not sparse repository");
}
#[test]
fn add() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = Repository::create(repo_path).expect("Created repository");
repo.git()
.args(["sparse-checkout", "init"])
.output()
.unwrap();
let actual = repo.sparse_checkout_add("foo/bar");
assert!(actual.is_ok(), "Expected successfull execution");
tmp_dir.close().unwrap();
}
}
mod subtree_add {
use crate::{setup_test_author, Repository, SubtreeAddError};
use tempfile::TempDir;
#[test]
fn dirty_work_tree() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = Repository::create(repo_path).expect("Created repository");
let err = repo
.subtree_add("https://example.com/foo/bar", "bar", "HEAD", "Some Message")
.expect_err("Expected an error");
assert_eq!(err, SubtreeAddError::WorkTreeDirty);
tmp_dir.close().unwrap();
}
#[test]
fn successfull() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
setup_test_author();
let repo = Repository::create(repo_path).expect("Created repository");
let readme = repo_path.join("README.md");
std::fs::File::create(&readme).unwrap();
std::fs::write(&readme, "# README").unwrap();
repo.stage(&readme).unwrap();
repo.commit("Test").unwrap();
let actual = repo.subtree_add(
"https://github.com/kalkin/file-expert",
"bar",
"HEAD",
"Some Message",
);
assert!(actual.is_ok(), "Failure to add subtree");
}
}
mod subtree_pull {
use crate::{setup_test_author, Repository, SubtreePullError};
use tempfile::TempDir;
#[test]
fn dirty_work_tree() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = Repository::create(repo_path).expect("Created repository");
let err = repo
.subtree_pull("https://example.com/foo/bar", "bar", "HEAD", "Some Message")
.expect_err("Expected an error");
assert_eq!(err, SubtreePullError::WorkTreeDirty);
}
#[test]
fn successfull() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
setup_test_author();
let repo = Repository::create(repo_path).expect("Created repository");
let readme = repo_path.join("README.md");
std::fs::File::create(&readme).unwrap();
std::fs::write(&readme, "# README").unwrap();
repo.stage(&readme).unwrap();
repo.commit("Test").unwrap();
repo.subtree_add(
"https://github.com/kalkin/file-expert",
"bar",
"v0.10.1",
"Some Message",
)
.unwrap();
let actual = repo.subtree_pull(
"https://github.com/kalkin/file-expert",
"bar",
"v0.13.1",
"Some message",
);
assert!(actual.is_ok(), "Failure to pull subtree");
}
}
mod remote_ref_resolution {
use crate::RefSearchError;
use crate::Repository;
use tempfile::TempDir;
#[test]
fn not_found() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = Repository::create(repo_path).expect("Created repository");
let result =
repo.remote_ref_to_id("https://github.com/kalkin/file-expert", "v230.40.50");
assert!(result.is_err());
#[allow(clippy::shadow_unrelated)]
{
let _expected =
RefSearchError::NotFound("Failed to find reference v230.40.50".to_owned());
assert!(
matches!(result.unwrap_err(), _expected),
"should not find v230.40.50"
);
}
}
#[test]
fn failure() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = Repository::create(repo_path).expect("Created repository");
let result = repo.remote_ref_to_id("https://example.com/asd/foo", "v230.40.50");
assert!(result.is_err());
let actual = matches!(result.unwrap_err(), RefSearchError::Failure(_));
assert!(actual, "should not find any repo");
}
#[test]
fn successfull_search() {
let tmp_dir = TempDir::new().unwrap();
let repo_path = tmp_dir.path();
let repo = Repository::create(repo_path).expect("Created repository");
let result = repo.remote_ref_to_id("https://github.com/kalkin/file-expert", "v0.9.0");
assert!(result.is_ok());
let actual = result.unwrap();
let expected = "24f624a0268f6cbcfc163abef5f3acbc6c11085e".to_owned();
assert_eq!(expected, actual, "Find commit id for v0.9.0");
}
}
}