git-checks 2.2.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 crates::git_workarea::{GitWorkArea, Identity, SubmoduleConfig};

use error::*;

use std::ffi::OsStr;
use std::path::Path;
use std::process::Command;

/// States attributes may be in.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttributeState {
    /// The attribute is neither set nor unset.
    Unspecified,
    /// The attribute is set.
    Set,
    /// The attribute is unset.
    Unset,
    /// The attribute is set with the given value.
    Value(String),
}

#[derive(Debug)]
/// Git context for use in checks.
pub struct CheckGitContext {
    /// The workarea for the check.
    workarea: GitWorkArea,
    /// The owner of the topic.
    topic_owner: Identity,
}

impl CheckGitContext {
    /// Create a new git context for checking a commit.
    pub fn new(workarea: GitWorkArea, topic_owner: Identity) -> Self {
        CheckGitContext {
            workarea: workarea,
            topic_owner: topic_owner,
        }
    }

    /// Create a git command for use in checks.
    pub fn git(&self) -> Command {
        self.workarea.git()
    }

    /// The publisher of the branch.
    pub fn topic_owner(&self) -> &Identity {
        &self.topic_owner
    }

    /// Check an attribute of the given path.
    pub fn check_attr<A, P>(&self, attr: A, path: P) -> Result<AttributeState>
        where A: AsRef<str>,
              P: AsRef<OsStr>,
    {
        let check_attr = self.workarea
            .git()
            .arg("check-attr")
            .arg(attr.as_ref())
            .arg("--")
            .arg(path.as_ref())
            .output()
            .chain_err(|| "failed to construct check-attr command")?;
        if !check_attr.status.success() {
            bail!(ErrorKind::Git(format!("failed to check the {} attribute of {}: {}",
                                         attr.as_ref(),
                                         path.as_ref().to_string_lossy(),
                                         String::from_utf8_lossy(&check_attr.stderr))));
        }
        let attr_line = String::from_utf8_lossy(&check_attr.stdout);

        // So the output format here is ambiguous. The `gitattributes(5)` format does not support
        // spaces in the values of attributes, so split on whitespace and take the last element.
        let attr_value = attr_line.split_whitespace()
            .last()
            .expect("expected `git check-attr` to have a value for the attribute");
        if attr_value == "set" {
            Ok(AttributeState::Set)
        } else if attr_value == "unset" {
            Ok(AttributeState::Unset)
        } else if attr_value == "unspecified" {
            Ok(AttributeState::Unspecified)
        } else {
            // Attribute values which match one of the above are ambiguous. `git-check-attr(1)`
            // states that is ambiguous and leaves it at that.
            Ok(AttributeState::Value(attr_value.to_owned()))
        }
    }

    /// Checkout paths from the index to the filesystem.
    ///
    /// Normally, files are not placed into the worktree, so checks which use other tools to
    /// inspect file contents do not work. This method checks out files to the working directory
    /// and fixes up Git's knowledge that they are there.
    ///
    /// All paths supported by Git's globbing and searching mechanisms are supported.
    pub fn checkout<P>(&self, paths: &[P]) -> Result<()>
        where P: AsRef<OsStr>,
    {
        Ok(self.workarea.checkout(paths)?)
    }

    /// Run a command from the work tree root.
    pub fn cd_to_work_tree<'a>(&self, cmd: &'a mut Command) -> &'a mut Command {
        self.workarea.cd_to_work_tree(cmd)
    }

    /// The path to the git repository.
    pub fn gitdir(&self) -> &Path {
        self.workarea.gitdir()
    }

    /// The submodule configuration for the repository.
    pub fn submodule_config(&self) -> &SubmoduleConfig {
        self.workarea.submodule_config()
    }
}

#[cfg(test)]
mod tests {
    use crates::git_workarea::{CommitId, GitContext, Identity};

    use super::*;

    use std::path::Path;

    fn make_context() -> GitContext {
        let gitdir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/.git"));
        if !gitdir.exists() {
            panic!("The tests must be run from a git checkout.");
        }

        GitContext::new(gitdir)
    }

    #[test]
    fn test_commit_attrs() {
        let ctx = make_context();

        // A commit with attributes set on some paths.
        let sha1 = "85b9551a672a34e1926d5010a9c9075eda0a6107";
        let prep_ctx = ctx.prepare(&CommitId::new(sha1)).unwrap();

        let ben = Identity::new("Ben Boeckel", "ben.boeckel@kitware.com");
        let check_ctx = CheckGitContext::new(prep_ctx, ben);

        assert_eq!(check_ctx.check_attr("foo", "file1").unwrap(),
                   AttributeState::Value("bar".to_owned()));
        assert_eq!(check_ctx.check_attr("attr_set", "file1").unwrap(),
                   AttributeState::Set);
        assert_eq!(check_ctx.check_attr("attr_unset", "file1").unwrap(),
                   AttributeState::Unspecified);
        assert_eq!(check_ctx.check_attr("text", "file1").unwrap(),
                   AttributeState::Unspecified);

        assert_eq!(check_ctx.check_attr("foo", "file2").unwrap(),
                   AttributeState::Unspecified);
        assert_eq!(check_ctx.check_attr("attr_set", "file2").unwrap(),
                   AttributeState::Set);
        assert_eq!(check_ctx.check_attr("attr_unset", "file2").unwrap(),
                   AttributeState::Unset);
        assert_eq!(check_ctx.check_attr("text", "file2").unwrap(),
                   AttributeState::Unspecified);

        assert_eq!(check_ctx.check_attr("foo", "file3").unwrap(),
                   AttributeState::Unspecified);
        assert_eq!(check_ctx.check_attr("attr_set", "file3").unwrap(),
                   AttributeState::Unspecified);
        assert_eq!(check_ctx.check_attr("attr_unset", "file3").unwrap(),
                   AttributeState::Unspecified);
        assert_eq!(check_ctx.check_attr("text", "file3").unwrap(),
                   AttributeState::Value("yes".to_owned()));
    }
}