use crates::git_checks_core::impl_prelude::*;
use crates::thiserror::Error;
#[derive(Debug, Error)]
enum CheckSizeError {
#[error("failed to get the size of the {} blob: {}", blob, output)]
CatFile { blob: CommitId, output: String },
}
impl CheckSizeError {
fn cat_file(blob: CommitId, output: &[u8]) -> Self {
CheckSizeError::CatFile {
blob,
output: String::from_utf8_lossy(output).into(),
}
}
}
#[derive(Builder, Debug, Clone, Copy)]
#[builder(field(private))]
pub struct CheckSize {
#[builder(default = "1 << 20")]
max_size: usize,
}
impl CheckSize {
pub fn builder() -> CheckSizeBuilder {
CheckSizeBuilder::default()
}
}
impl Default for CheckSize {
fn default() -> Self {
CheckSize {
max_size: 1 << 20,
}
}
}
impl ContentCheck for CheckSize {
fn name(&self) -> &str {
"check-size"
}
fn check(
&self,
ctx: &CheckGitContext,
content: &dyn Content,
) -> Result<CheckResult, Box<dyn Error>> {
let mut result = CheckResult::new();
for diff in content.diffs() {
if let StatusChange::Deleted = diff.status {
continue;
}
if diff.new_mode == "160000" {
continue;
}
let size_attr = ctx.check_attr("hooks-max-size", diff.name.as_path())?;
let prefix = commit_prefix(content);
let max_size = match size_attr {
AttributeState::Unset => continue,
AttributeState::Value(ref v) => {
v.parse().unwrap_or_else(|_| {
result.add_error(format!(
"{}has an invalid value hooks-max-size={} for `{}`. The value must be \
an unsigned integer.",
prefix, v, diff.name,
));
self.max_size
})
},
_ => self.max_size,
};
let cat_file = ctx
.git()
.arg("cat-file")
.arg("-s")
.arg(diff.new_blob.as_str())
.output()
.map_err(|err| GitError::subcommand("cat-file -s", err))?;
if !cat_file.status.success() {
return Err(
CheckSizeError::cat_file(diff.new_blob.clone(), &cat_file.stderr).into(),
);
}
let new_size: usize = String::from_utf8_lossy(&cat_file.stdout)
.trim()
.parse()
.unwrap_or_else(|msg| {
result.add_error(format!(
"{}has the file `{}` which has a size which did not parse: {}",
prefix, diff.name, msg,
));
0
});
if new_size > max_size {
result.add_error(format!(
"{}creates blob {} at `{}` with size {} bytes ({:.2} KiB) which is greater \
than the maximum size {} bytes ({:.2} KiB). If the file is intended to be \
committed, set the `hooks-max-size` attribute on its path.",
prefix,
diff.new_blob,
diff.name,
new_size,
new_size as f64 / 1024.,
max_size,
max_size as f64 / 1024.,
));
}
}
Ok(result)
}
}
#[cfg(feature = "config")]
pub(crate) mod config {
use crates::git_checks_config::{CommitCheckConfig, IntoCheck, TopicCheckConfig};
use crates::inventory;
#[cfg(test)]
use crates::serde_json;
use CheckSize;
#[derive(Deserialize, Debug)]
pub struct CheckSizeConfig {
#[serde(default)]
max_size: Option<usize>,
}
impl IntoCheck for CheckSizeConfig {
type Check = CheckSize;
fn into_check(self) -> Self::Check {
let mut builder = CheckSize::builder();
if let Some(max_size) = self.max_size {
builder.max_size(max_size);
}
builder
.build()
.expect("configuration mismatch for `CheckSize`")
}
}
register_checks! {
CheckSizeConfig {
"check_size" => CommitCheckConfig,
"check_size/topic" => TopicCheckConfig,
},
}
#[test]
fn test_check_size_config_empty() {
let json = json!({});
let check: CheckSizeConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.max_size, None);
}
#[test]
fn test_check_size_config_all_fields() {
let json = json!({
"max_size": 1000,
});
let check: CheckSizeConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.max_size, Some(1000));
}
}
#[cfg(test)]
mod tests {
use test::*;
use CheckSize;
const CHECK_SIZE_COMMIT: &str = "1464c62cc09b01a8e86a8512dd400b705c760c42";
const ADD_SUBMODULE_TOPIC: &str = "fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c";
const FIX_TOPIC: &str = "cb03f0d95897e93dcb089790f9cafd1ee7987922";
#[test]
fn test_check_size_builder_default() {
assert!(CheckSize::builder().build().is_ok());
}
#[test]
fn test_check_size() {
let check = CheckSize::builder().max_size(46).build().unwrap();
let result = run_check("test_check_size", CHECK_SIZE_COMMIT, check);
test_result_errors(result, &[
"commit a61fd3759b61a4a1f740f3fe656bc42151cefbdd creates blob \
293071f2f4dd15bb57904e08bf6529e748e4075a at `increased-limit` with size 273 bytes \
(0.27 KiB) which is greater than the maximum size 200 bytes (0.20 KiB). If the file \
is intended to be committed, set the `hooks-max-size` attribute on its path.",
"commit a61fd3759b61a4a1f740f3fe656bc42151cefbdd creates blob \
4fa03f0211ccd20b0285314d9469ccbee1edd81c at `large-file` with size 48 bytes (0.05 \
KiB) which is greater than the maximum size 46 bytes (0.04 KiB). If the file is \
intended to be committed, set the `hooks-max-size` attribute on its path.",
"commit 112e9b34401724bff57f68cf47c5065d4342b263 has an invalid value \
hooks-max-size=not-a-number for `bad-attr-value`. The value must be an unsigned \
integer.",
"commit 1464c62cc09b01a8e86a8512dd400b705c760c42 creates blob \
921aae7a6949c74bc4bd53b4122fcd7ee3c819c6 at `no-value` with size 50 bytes (0.05 KiB) \
which is greater than the maximum size 46 bytes (0.04 KiB). If the file is intended \
to be committed, set the `hooks-max-size` attribute on its path.",
]);
}
#[test]
fn test_check_size_topic() {
let check = CheckSize::builder().max_size(46).build().unwrap();
let result = run_topic_check("test_check_size_topic", CHECK_SIZE_COMMIT, check);
test_result_errors(result, &[
"has an invalid value hooks-max-size=not-a-number for `bad-attr-value`. The value \
must be an unsigned integer.",
"creates blob 293071f2f4dd15bb57904e08bf6529e748e4075a at `increased-limit` with size \
273 bytes (0.27 KiB) which is greater than the maximum size 200 bytes (0.20 KiB). If \
the file is intended to be committed, set the `hooks-max-size` attribute on its \
path.",
"creates blob 4fa03f0211ccd20b0285314d9469ccbee1edd81c at `large-file` with size 48 \
bytes (0.05 KiB) which is greater than the maximum size 46 bytes (0.04 KiB). If the \
file is intended to be committed, set the `hooks-max-size` attribute on its path.",
"creates blob 921aae7a6949c74bc4bd53b4122fcd7ee3c819c6 at `no-value` with size \
50 bytes (0.05 KiB) which is greater than the maximum size 46 bytes (0.04 KiB). If \
the file is intended to be committed, set the `hooks-max-size` attribute on its \
path.",
]);
}
#[test]
fn test_check_size_submodule() {
let check = CheckSize::builder().max_size(1024).build().unwrap();
run_check_ok("test_check_size_submodule", ADD_SUBMODULE_TOPIC, check);
}
#[test]
fn test_check_size_topic_fixed() {
let check = CheckSize::builder().max_size(46).build().unwrap();
run_topic_check_ok("test_check_size_topic_fixed", FIX_TOPIC, check);
}
}