use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
use crates::thiserror::Error;
use crates::git_workarea::{GitError, GitWorkArea, Identity, SubmoduleConfig};
#[derive(Debug, Error)]
pub enum AttributeError {
#[error("git error: {}", source)]
Git {
#[from]
source: GitError,
},
#[error(
"check-attr error: failed to check the {} attribute of {}: {}",
attribute,
path.display(),
output
)]
CheckAttr {
attribute: String,
path: PathBuf,
output: String,
},
#[error(
"check-attr error: unexpected git output format error: no value for {} on {}",
attribute,
path.display()
)]
MissingValue {
attribute: String,
path: PathBuf,
},
#[doc(hidden)]
#[error("unreachable...")]
_NonExhaustive,
}
impl AttributeError {
fn check_attr(attr: &str, path: &OsStr, output: &[u8]) -> Self {
AttributeError::CheckAttr {
attribute: attr.into(),
path: path.into(),
output: String::from_utf8_lossy(output).into(),
}
}
fn missing_value(attr: &str, path: &OsStr) -> Self {
AttributeError::MissingValue {
attribute: attr.into(),
path: path.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttributeState {
Unspecified,
Set,
Unset,
Value(String),
}
#[derive(Debug)]
pub struct CheckGitContext {
workarea: GitWorkArea,
topic_owner: Identity,
}
impl CheckGitContext {
pub fn new(workarea: GitWorkArea, topic_owner: Identity) -> Self {
Self {
workarea,
topic_owner,
}
}
pub fn git(&self) -> Command {
self.workarea.git()
}
pub fn topic_owner(&self) -> &Identity {
&self.topic_owner
}
fn check_attr_impl(&self, attr: &str, path: &OsStr) -> Result<AttributeState, AttributeError> {
let check_attr = self
.workarea
.git()
.arg("--literal-pathspecs")
.arg("check-attr")
.arg(attr)
.arg("--")
.arg(path)
.output()
.map_err(|err| GitError::subcommand("check-attr", err))?;
if !check_attr.status.success() {
return Err(AttributeError::check_attr(attr, path, &check_attr.stderr));
}
let attr_line = String::from_utf8_lossy(&check_attr.stdout);
let attr_value = attr_line
.split_whitespace()
.last()
.ok_or_else(|| AttributeError::missing_value(attr, path))?;
if attr_value == "set" {
Ok(AttributeState::Set)
} else if attr_value == "unset" {
Ok(AttributeState::Unset)
} else if attr_value == "unspecified" {
Ok(AttributeState::Unspecified)
} else {
Ok(AttributeState::Value(attr_value.to_owned()))
}
}
pub fn check_attr<A, P>(&self, attr: A, path: P) -> Result<AttributeState, AttributeError>
where
A: AsRef<str>,
P: AsRef<OsStr>,
{
self.check_attr_impl(attr.as_ref(), path.as_ref())
}
pub fn workarea(&self) -> &GitWorkArea {
&self.workarea
}
pub fn workarea_mut(&mut self) -> &mut GitWorkArea {
&mut self.workarea
}
pub fn gitdir(&self) -> &Path {
self.workarea.gitdir()
}
pub fn submodule_config(&self) -> &SubmoduleConfig {
self.workarea.submodule_config()
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use crates::git_workarea::{CommitId, GitContext, Identity};
use super::*;
fn make_context() -> GitContext {
let gitdir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.git"));
if !gitdir.exists() {
panic!("The tests must be run from a git checkout.");
}
GitContext::new(gitdir)
}
#[test]
fn test_commit_attrs() {
let ctx = make_context();
let sha1 = "85b9551a672a34e1926d5010a9c9075eda0a6107";
let prep_ctx = ctx.prepare(&CommitId::new(sha1)).unwrap();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let check_ctx = CheckGitContext::new(prep_ctx, ben);
assert_eq!(
check_ctx.check_attr("foo", "file1").unwrap(),
AttributeState::Value("bar".to_owned()),
);
assert_eq!(
check_ctx.check_attr("attr_set", "file1").unwrap(),
AttributeState::Set,
);
assert_eq!(
check_ctx.check_attr("attr_unset", "file1").unwrap(),
AttributeState::Unspecified,
);
assert_eq!(
check_ctx.check_attr("text", "file1").unwrap(),
AttributeState::Unspecified,
);
assert_eq!(
check_ctx.check_attr("foo", "file2").unwrap(),
AttributeState::Unspecified,
);
assert_eq!(
check_ctx.check_attr("attr_set", "file2").unwrap(),
AttributeState::Set,
);
assert_eq!(
check_ctx.check_attr("attr_unset", "file2").unwrap(),
AttributeState::Unset,
);
assert_eq!(
check_ctx.check_attr("text", "file2").unwrap(),
AttributeState::Unspecified,
);
assert_eq!(
check_ctx.check_attr("foo", "file3").unwrap(),
AttributeState::Unspecified,
);
assert_eq!(
check_ctx.check_attr("attr_set", "file3").unwrap(),
AttributeState::Unspecified,
);
assert_eq!(
check_ctx.check_attr("attr_unset", "file3").unwrap(),
AttributeState::Unspecified,
);
assert_eq!(
check_ctx.check_attr("text", "file3").unwrap(),
AttributeState::Value("yes".to_owned()),
);
}
#[test]
fn test_commit_attrs_literal_pathspecs() {
let ctx = make_context();
let sha1 = "9055e6f31ee5e7de8cdce0ca57452c38f433fd89";
let prep_ctx = ctx.prepare(&CommitId::new(sha1)).unwrap();
let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
let check_ctx = CheckGitContext::new(prep_ctx, ben);
assert_eq!(
check_ctx.check_attr("custom-attr", "foo.attr").unwrap(),
AttributeState::Set,
);
assert_eq!(
check_ctx.check_attr("custom-attr", "*.attr").unwrap(),
AttributeState::Set,
);
}
}