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, Default, Clone, Copy)]
/// Check that submodules are reachable from a given branch and available.
pub struct SubmoduleWatch {
    reject_additions: bool,
    reject_removals: bool,
}

impl SubmoduleWatch {
    /// Checks that submodules in the project are available.
    pub fn new() -> Self {
        SubmoduleWatch {
            reject_additions: false,
            reject_removals: false,
        }
    }

    /// Configure whether additions of submodules are allowed.
    pub fn reject_additions(&mut self, reject: bool) -> &mut Self {
        self.reject_additions = reject;
        self
    }

    /// Configure whether removals of submodules are allowed.
    pub fn reject_removals(&mut self, reject: bool) -> &mut Self {
        self.reject_removals = reject;
        self
    }
}

impl Check for SubmoduleWatch {
    fn name(&self) -> &str {
        "submodule-watch"
    }

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

        for diff in &commit.diffs {
            // Ignore diffs which are not submodules.
            if diff.new_mode != "160000" && diff.old_mode != "160000" {
                continue;
            }

            let mut added = false;
            let mut removed = false;

            match diff.status {
                StatusChange::Added => added = true,
                StatusChange::Deleted => removed = true,
                StatusChange::Modified(_) => {
                    if diff.old_mode != diff.new_mode {
                        if diff.old_mode == "160000" {
                            removed = true;
                        } else {
                            added = true;
                        }
                    }
                },
                _ => (),
            }

            if added {
                if self.reject_additions {
                    result.add_error(format!("commit {} adds a submodule at `{}` which is not \
                                              allowed.",
                                             commit.sha1_short,
                                             diff.name));
                } else {
                    result.add_alert(format!("commit {} adds a submodule at `{}`.",
                                             commit.sha1_short,
                                             diff.name),
                                     false);
                }
            }

            if removed {
                if self.reject_removals {
                    result.add_error(format!("commit {} removes the submodule at `{}` which \
                                              is not allowed.",
                                             commit.sha1_short,
                                             diff.name));
                } else {
                    result.add_alert(format!("commit {} removes the submodule at `{}`.",
                                             commit.sha1_short,
                                             diff.name),
                                     false);
                }
            }
        }

        Ok(result)
    }
}

#[cfg(test)]
mod tests {
    use super::SubmoduleWatch;
    use super::super::test::*;

    static ADD_SUBMODULE_TOPIC: &'static str = "fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c";
    static REMOVE_SUBMODULE_TOPIC: &'static str = "336dbaa31d512033fe77eaba7f92ebfecbd17a39";

    #[test]
    fn test_submodule_watch_add() {
        let check = SubmoduleWatch::new();
        let mut conf = GitCheckConfiguration::new();

        conf.add_check(&check);

        let result = test_check("test_submodule_watch_add", ADD_SUBMODULE_TOPIC, &conf);

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 1);
        assert_eq!(result.alerts()[0],
                   "commit fe90ee2 adds a submodule at `submodule`.");
        assert_eq!(result.errors().len(), 0);
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), true);
    }

    #[test]
    fn test_submodule_watch_add_reject() {
        let mut check = SubmoduleWatch::new();
        check.reject_additions(true);
        let mut conf = GitCheckConfiguration::new();

        conf.add_check(&check);

        let result = test_check("test_submodule_watch_add_reject",
                                ADD_SUBMODULE_TOPIC,
                                &conf);

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 1);
        assert_eq!(result.errors()[0],
                   "commit fe90ee2 adds a submodule at `submodule` which is not allowed.");
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), false);
    }

    #[test]
    fn test_submodule_watch_remove() {
        let check = SubmoduleWatch::new();
        let mut conf = GitCheckConfiguration::new();

        conf.add_check(&check);

        let result = test_check_submodule_base("test_submodule_watch_remove",
                                               REMOVE_SUBMODULE_TOPIC,
                                               ADD_SUBMODULE_TOPIC,
                                               &conf);

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 1);
        assert_eq!(result.alerts()[0],
                   "commit 336dbaa removes the submodule at `submodule`.");
        assert_eq!(result.errors().len(), 0);
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), true);
    }

    #[test]
    fn test_submodule_watch_remove_reject() {
        let mut check = SubmoduleWatch::new();
        check.reject_removals(true);
        let mut conf = GitCheckConfiguration::new();

        conf.add_check(&check);

        let result = test_check_submodule_base("test_submodule_watch_remove_reject",
                                               REMOVE_SUBMODULE_TOPIC,
                                               ADD_SUBMODULE_TOPIC,
                                               &conf);

        assert_eq!(result.warnings().len(), 0);
        assert_eq!(result.alerts().len(), 0);
        assert_eq!(result.errors().len(), 1);
        assert_eq!(result.errors()[0],
                   "commit 336dbaa removes the submodule at `submodule` which is not allowed.");
        assert_eq!(result.allowed(), false);
        assert_eq!(result.pass(), false);
    }

    // TODO: Test submodule -> not submodule and vice versa in a commit.
    // TODO: Test submodule.path setting changes.
}