git-checks 3.5.2

Checks to run against a topic in git to enforce coding standards.
Documentation
// Copyright 2016 Kitware, Inc.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use impl_prelude::*;

#[derive(Debug, Clone, Copy)]
/// Checks that files committed to the tree do not exceed a specified size.
pub struct CheckSize {
    /// The maximum size of blobs allowed in the repository.
    max_size: usize,
}

impl CheckSize {
    /// Create a new check to check for size with the given default size.
    ///
    /// The check can be configured using the `hooks-max-size` attribute to change the maximum size
    /// allowed for specific files.
    pub fn new(max_size: usize) -> Self {
        Self {
            max_size: max_size,
        }
    }
}

impl ContentCheck for CheckSize {
    fn name(&self) -> &str {
        "check-size"
    }

    fn check(&self, ctx: &CheckGitContext, content: &Content) -> Result<CheckResult> {
        let mut result = CheckResult::new();

        for diff in content.diffs() {
            if let StatusChange::Deleted = diff.status {
                continue;
            }

            // Ignore submodules.
            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 {
                // Explicity unset means "unlimited".
                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()
                .chain_err(|| "failed to construct cat-file command")?;
            if !cat_file.status.success() {
                bail!(ErrorKind::Git(format!("failed to get the size of the {} blob: {}",
                                             diff.new_blob,
                                             String::from_utf8_lossy(&cat_file.stderr))));
            }
            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));
                    // We failed to parse the size from git, so don't bother checking its size. The
                    // attribute needs fixed first.
                    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(test)]
mod tests {
    use checks::CheckSize;
    use checks::test::*;

    const CHECK_SIZE_COMMIT: &str = "1464c62cc09b01a8e86a8512dd400b705c760c42";
    const ADD_SUBMODULE_TOPIC: &str = "fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c";
    const FIX_TOPIC: &str = "cb03f0d95897e93dcb089790f9cafd1ee7987922";

    #[test]
    fn test_check_size() {
        let check = CheckSize::new(46);
        let result = run_check("test_check_size", CHECK_SIZE_COMMIT, check);
        test_result_errors(result, &[
            "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.",
            "commit 112e9b34401724bff57f68cf47c5065d4342b263 has an invalid value \
             hooks-max-size=not-a-number for `bad-attr-value`. The value must be an unsigned \
             integer.",
            "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.",
        ]);
    }

    #[test]
    fn test_check_size_topic() {
        let check = CheckSize::new(46);
        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::new(1024);
        run_check_ok("test_check_size_submodule", ADD_SUBMODULE_TOPIC, check);
    }

    #[test]
    fn test_check_size_topic_fixed() {
        let check = CheckSize::new(46);
        run_topic_check_ok("test_check_size_topic_fixed", FIX_TOPIC, check);
    }
}