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, Default, Clone, Copy)]
/// Check that submodules are not rewound to older revisions.
pub struct SubmoduleRewind;

impl SubmoduleRewind {
    /// Checks that submodules in the project always move forward.
    pub fn new() -> Self {
        SubmoduleRewind
    }
}

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

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

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

            match diff.status {
                // Ignore submodules which have not been modified.
                StatusChange::Deleted |
                StatusChange::Added => continue,
                _ => (),
            }

            let submodule_ctx = if let Some(ctx) = SubmoduleContext::new(ctx, diff.name.as_ref()) {
                ctx
            } else {
                continue;
            };

            let cat_file = submodule_ctx.context
                .git()
                .arg("cat-file")
                .arg("-t")
                .arg(diff.new_blob.as_str())
                .output()
                .chain_err(|| "failed to construct cat-file command")?;
            let object_type = String::from_utf8_lossy(&cat_file.stdout);
            if !cat_file.status.success() || object_type.trim() != "commit" {
                // The commit is not part of the submodule at all yet; this is handled by the
                // `submodule-available` check.
                continue;
            }

            let merge_base = submodule_ctx.context
                .git()
                .arg("merge-base")
                .arg(diff.old_blob.as_str())
                .arg(diff.new_blob.as_str())
                .output()
                .chain_err(|| "failed to construct merge-base command")?;
            if !merge_base.status.success() {
                bail!(ErrorKind::Git(format!("failed to get the merge base for the `{}` \
                                              submodule: {}",
                                             diff.name,
                                             String::from_utf8_lossy(&merge_base.stderr))));
            }
            let base = String::from_utf8_lossy(&merge_base.stdout);

            if base.trim() == diff.new_blob.as_str() {
                result.add_error(format!("commit {} is not allowed since it moves the submodule \
                                          `{}` backwards from {} to {}.",
                                         commit.sha1,
                                         submodule_ctx.path,
                                         diff.old_blob,
                                         diff.new_blob));
            }
        }

        Ok(result)
    }
}

#[cfg(test)]
mod tests {
    use checks::SubmoduleRewind;
    use checks::test::*;

    const MOVE_TOPIC: &str = "2088079e35503be3be41dbdca55080ced95614e1";
    const REWIND_TOPIC: &str = "39c5d0d9dc7ee6abad72cd42c90d7c1af1be169c";
    const UNAVAILABLE_TOPIC: &str = "1b9275caca1557611df19d1dfea687c3ef302eef";

    #[test]
    fn test_submodule_rewind_ok() {
        let check = SubmoduleRewind::new();
        let conf = make_check_conf(&check);

        let result = test_check_submodule("test_submodule_rewind_ok", MOVE_TOPIC, &conf);
        test_result_ok(result);
    }

    #[test]
    fn test_submodule_rewind_unavailable() {
        let check = SubmoduleRewind::new();
        let conf = make_check_conf(&check);

        let result = test_check_submodule("test_submodule_rewind_unavailable",
                                          UNAVAILABLE_TOPIC,
                                          &conf);

        // No errors because this is the check for the `submodule-available` check.
        test_result_ok(result);
    }

    #[test]
    fn test_submodule_rewind_rewind() {
        let check = SubmoduleRewind::new();
        let conf = make_check_conf(&check);

        let result = test_check_submodule_base("test_submodule_rewind_rewind",
                                               REWIND_TOPIC,
                                               MOVE_TOPIC,
                                               &conf);
        test_result_errors(result, &[
            "commit 39c5d0d9dc7ee6abad72cd42c90d7c1af1be169c is not allowed since it moves the \
             submodule `submodule` backwards from 8a890d8c4b89560c70a059bbdd7bc59b92b5c92b to \
             2a8baa8e23bb1de5eec202dd4a29adf47feb03b1.",
        ]);
    }
}