git-checks 1.0.0

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 super::super::*;

#[derive(Debug, Clone, Copy)]
/// Checks that files committed to the tree do not exceed a specified size.
pub struct CheckSize {
    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 {
        CheckSize {
            max_size: max_size,
        }
    }
}

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

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

        for diff in &commit.diffs {
            if let StatusChange::Deleted = diff.status {
                continue;
            }

            let size_attr = try!(ctx.check_attr("hooks-max-size", diff.name.as_ref()));

            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!("commit {} has an invalid value \
                                                  hooks-max-size={} for `{}`. The value must be \
                                                  an unsigned integer.",
                                                 commit.sha1_short,
                                                 v,
                                                 diff.name));
                        self.max_size
                    })
                },
                _ => self.max_size,
            };

            let cat_file = try!(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!("commit {} has the file `{}` which has a size \
                                              which did not parse: {}",
                                             commit.sha1_short,
                                             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!("commit {} 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.",
                                         commit.sha1_short,
                                         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 super::CheckSize;
    use super::super::test::*;

    static CHECK_SIZE_COMMIT: &'static str = "1464c62cc09b01a8e86a8512dd400b705c760c42";

    #[test]
    fn test_check_size() {
        let check = CheckSize::new(46);
        let mut conf = GitCheckConfiguration::new();

        conf.add_check(&check);

        let result = test_check("test_check_size", CHECK_SIZE_COMMIT, &conf);

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 4);
        assert_eq!(result.errors()[0],
                   "commit 1464c62 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.");
        assert_eq!(result.errors()[1],
                   "commit 112e9b3 has an invalid value hooks-max-size=not-a-number for \
                    `bad-attr-value`. The value must be an unsigned integer.");
        assert_eq!(result.errors()[2],
                   "commit a61fd37 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.");
        assert_eq!(result.errors()[3],
                   "commit a61fd37 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.");
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), false);
    }
}