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)]
/// Description of a third party package imported using Kitware's third party import process.
///
/// The workflow used at Kitware for third party packages is to keep all changes tracked in
/// separate repositories. This makes tracking patches to the projects easier to manage and extract
/// for submission to the appropriate upstream project.
///
/// When a project is imported, it uses a separate history which contains only snapshots of the
/// tracked repository. When imported into a project, it can select a subset of files to keep, drop
/// extra metadata into the import, or perform other transformations as necessary. Whatever the
/// result of that is, it is added as a new commit on the history of the tracking branch for the
/// project. This is then merged into the main project using a subtree strategy to move the project
/// to the correct place.
///
/// This check checks to make sure that any modifications in the main project's imported location of
/// the third party project are made on the tracking branch.
pub struct ThirdParty {
    /// The name of the imported project.
    pub name: String,
    /// The path the third party project lives once merged.
    pub path: String,
    /// The root commit of the third party tracking branch.
    pub root: String,
    /// The location of the utility to use for importing this project.
    pub utility: String,
}

impl ThirdParty {
    /// Create a new third party import configuration.
    pub fn new<N, P, R, U>(name: N, path: P, root: R, utility: U) -> Self
        where N: ToString,
              P: ToString,
              R: ToString,
              U: ToString,
    {
        Self {
            name: name.to_string(),
            path: path.to_string(),
            root: root.to_string(),
            utility: utility.to_string(),
        }
    }
}

/// Whether a commit is on the import branch or not.
enum CheckRefResult {
    /// Indicates the commit is on the import branch.
    IsImport,
    /// Indicates the commit is not part of the import branch.
    ///
    /// Stores its error message.
    Rejected(String),
}

impl CheckRefResult {
    /// Whether the result is an import or not.
    fn is_import(&self) -> bool {
        if let CheckRefResult::IsImport = *self {
            true
        } else {
            false
        }
    }

    /// Add the import result to a check result.
    fn add_result(self, result: &mut CheckResult) {
        if let CheckRefResult::Rejected(err) = self {
            result.add_error(err);
        }
    }
}

impl Check for ThirdParty {
    fn name(&self) -> &str {
        "third-party"
    }

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

        // A flag to indicate that we need to check the commit for an evil merge.
        let mut check_tree = false;

        for diff in &commit.diffs {
            let name_path = diff.name.as_path();

            if !name_path.starts_with(&self.path) {
                continue;
            }

            let check_ref = |sha1: &CommitId| {
                let rev_list = ctx.git()
                    .arg("rev-list")
                    .arg("--first-parent")
                    .arg("--max-parents=0")
                    .arg(sha1.as_str())
                    .output()
                    .chain_err(|| "failed to construct rev-list command")?;
                if !rev_list.status.success() {
                    bail!(ErrorKind::Git(format!("failed to get list the root commit for a {} \
                                                  third-party change: {}",
                                                 self.name,
                                                 String::from_utf8_lossy(&rev_list.stderr))));
                }
                let refs = String::from_utf8_lossy(&rev_list.stdout);
                let is_import = self.root == refs.trim();

                Ok(if is_import {
                    CheckRefResult::IsImport
                } else {
                    let msg = format!("commit {} not allowed; the `{}` file is maintained by the \
                                       third party utilities; please use `{}` to update this \
                                       file.",
                                      commit.sha1,
                                      diff.name,
                                      self.utility);

                    CheckRefResult::Rejected(msg)
                })
            };

            if commit.parents.len() == 2 {
                let is_import_res: Result<_> = check_ref(&commit.parents[1]);
                let is_import = is_import_res?.is_import();

                if is_import {
                    // Check that the merge commit is not "evil" for the import directory.
                    check_tree = true;
                } else {
                    check_ref(&commit.sha1)?.add_result(&mut result);
                }
            } else {
                check_ref(&commit.sha1)?.add_result(&mut result);
            }
        }

        if check_tree {
            // Get the tree of the import branch.
            let rev_parse = ctx.git()
                .arg("rev-parse")
                .arg(format!("{}^{{tree}}", commit.parents[1]))
                .output()
                .chain_err(|| "failed to construct rev-parse command")?;
            if !rev_parse.status.success() {
                bail!(ErrorKind::Git(format!("failed to parse the expected tree object for the \
                                              {} third-party directory: {}",
                                             self.name,
                                             String::from_utf8_lossy(&rev_parse.stderr))));
            }
            let expected_tree = String::from_utf8_lossy(&rev_parse.stdout);

            // Get the tree of the imported tree in the merge commit.
            let ls_tree = ctx.git()
                .arg("ls-tree")
                .arg(commit.sha1.as_str())
                .arg(&self.path)
                .output()
                .chain_err(|| "failed to construct ls-tree command")?;
            if !ls_tree.status.success() {
                bail!(ErrorKind::Git(format!("failed to parse the actual tree object for the \
                                              {} third-party directory: {}",
                                             self.name,
                                             String::from_utf8_lossy(&ls_tree.stderr))));
            }
            let ls_tree_output = String::from_utf8_lossy(&ls_tree.stdout);
            let actual_tree = ls_tree_output.split_whitespace()
                .nth(2)
                .expect("expected the tree output to have 3 entries");

            // Ensure the trees match.
            if actual_tree != expected_tree.trim() {
                let msg = format!("commit {} not allowed; the `{}` directory contains changes \
                                   not on the import branch; merge conflicts should not \
                                   happen and indicate that the import directory was \
                                   manually edited at some point. Please find and revert the \
                                   bad edit, apply it to the imported repository (if \
                                   necessary), and then run the import utility.",
                                  commit.sha1,
                                  self.path);

                result.add_error(msg);
            }
        }

        Ok(result)
    }
}

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

    const BASE_COMMIT: &str = "26576e49345a141eca310af92737e489c9baac24";
    const VALID_UPDATE_TOPIC: &str = "0bd161c8187d4f727a7acc17020711dcc139b166";
    const INVALID_UPDATE_TOPIC: &str = "af154fdff05c871125f2db03eccbdde8571d484e";
    const EVIL_UPDATE_TOPIC: &str = "add18e5ab9a67303337cb2754c675fb2e0a45a79";

    fn make_third_party_check() -> ThirdParty {
        ThirdParty::new("check_size",
                        "check_size",
                        "d50197ebd7167b0941d34405686164068db0b77b",
                        "./update.sh")
    }

    #[test]
    fn test_third_party_valid_update() {
        let check = make_third_party_check();
        let conf = make_check_conf(&check);

        let result = test_check_base("test_third_party_valid_update",
                                     VALID_UPDATE_TOPIC,
                                     BASE_COMMIT,
                                     &conf);
        test_result_ok(result);
    }

    #[test]
    fn test_third_party_invalid_update() {
        let check = make_third_party_check();
        let conf = make_check_conf(&check);

        let result = test_check_base("test_third_party_invalid_update",
                                     INVALID_UPDATE_TOPIC,
                                     BASE_COMMIT,
                                     &conf);
        test_result_errors(result, &[
            "commit af154fdff05c871125f2db03eccbdde8571d484e not allowed; the \
             `check_size/increased-limit` file is maintained by the third party utilities; please \
             use `./update.sh` to update this file.",
        ]);
    }

    #[test]
    fn test_third_party_invalid_update_evil() {
        let check = make_third_party_check();
        let conf = make_check_conf(&check);

        let result = test_check_base("test_third_party_invalid_update_evil",
                                     EVIL_UPDATE_TOPIC,
                                     BASE_COMMIT,
                                     &conf);
        test_result_errors(result, &[
            "commit add18e5ab9a67303337cb2754c675fb2e0a45a79 not allowed; the `check_size` \
             directory contains changes not on the import branch; merge conflicts should not \
             happen and indicate that the import directory was manually edited at some point. \
             Please find and revert the bad edit, apply it to the imported repository (if \
             necessary), and then run the import utility.",
        ]);
    }
}