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 commit message subjects for invalid patterns.
pub struct CommitSubject {
    /// The minimum length allowed for the summary line.
    min_summary: usize,
    /// The maximum length allowed for the summary line.
    max_summary: usize,

    /// Whether to deny work-in-progress commits or not.
    check_wip: bool,

    /// Whether to deny `fixup!` and `squash!` commits or not.
    check_rebase_commands: bool,
}

impl CommitSubject {
    /// Checks commit message subjects for invalid patterns
    ///
    /// Patterns which are checked for:
    ///   - overly long or short summary lines;
    ///   - work-in-progress messages; and
    ///   - `fixup!` and `squash!` messages.
    ///
    /// Commit messages which appear to have been auto generated by actions such as merging or
    /// reverting commits will skip the summary line length limit (if enforced).
    pub fn new() -> Self {
        Self {
            min_summary: 8,
            max_summary: 78,

            check_wip: true,

            check_rebase_commands: true,
        }
    }

    /// Check the summary line with the given limits.
    pub fn with_summary_limits(&mut self, min: usize, max: usize) -> &mut Self {
        self.min_summary = min;
        self.max_summary = max;
        self
    }

    /// Checks for work-in-progress commits
    ///
    /// Commit messages which mention `WIP` or `wip` at the beginning of their commit messages are
    /// rejected since they are (nominally) incomplete.
    pub fn check_work_in_progress(&mut self, wip: bool) -> &mut Self {
        self.check_wip = wip;
        self
    }

    /// Check for rebase commands
    ///
    /// Rebase commands include commits which begin with `fixup! ` or `squash! `. These subjects
    /// are used to indicate that the commit belongs somewhere else in the branch and should be
    /// completed before merging.
    pub fn check_rebase_commands(&mut self, rebase: bool) -> &mut Self {
        self.check_rebase_commands = rebase;
        self
    }

    /// Whether the summary is generated or not.
    ///
    /// The commit summaries generated by `git merge` and `git revert` can be long, but since they
    /// are auto-generated, allow them.
    fn is_generated_subject(summary: &str) -> bool {
        false ||
            summary.starts_with("Merge ") ||
            summary.starts_with("Revert ")
    }
}

impl Check for CommitSubject {
    fn name(&self) -> &str {
        "commit-subject"
    }

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

        if lines.is_empty() {
            result.add_error(format!("commit {} has an invalid commit subject; it is empty.",
                                     commit.sha1));
            return Ok(result);
        }

        let summary = &lines[0];
        let summary_len = summary.len();

        if summary_len < self.min_summary {
            result.add_error(format!("commit {} has an invalid commit subject; the first line \
                                      must be at least {} characters.",
                                     commit.sha1,
                                     self.min_summary));
        }

        if !Self::is_generated_subject(summary) && self.max_summary < summary_len {
            result.add_error(format!("commit {} has an invalid commit subject; the first line \
                                      must be no longer than {} characters.",
                                     commit.sha1,
                                     self.max_summary));
        }

        if lines.len() >= 2 {
            if lines.len() >= 2 && !lines[1].is_empty() {
                result.add_error(format!("commit {} has an invalid commit subject; the second \
                                          line must be empty.",
                                         commit.sha1));
            }

            if lines.len() == 2 {
                result.add_error(format!("commit {} has an invalid commit subject; it cannot be \
                                          exactly two lines.",
                                         commit.sha1));
            } else if lines[2].is_empty() {
                result.add_error(format!("commit {} has an invalid commit subject; the third \
                                          line must not be empty.",
                                         commit.sha1));
            }
        }

        if self.check_wip && (summary.starts_with("WIP") || summary.starts_with("wip")) {
            result.add_error(format!("commit {} cannot be merged; it is marked as a \
                                      work-in-progress (WIP).",
                                     commit.sha1));
        }

        if self.check_rebase_commands {
            if summary.starts_with("fixup! ") {
                result.add_error(format!("commit {} cannot be merged; it is marked as a fixup \
                                          commit.",
                                         commit.sha1));
            } else if summary.starts_with("squash! ") {
                result.add_error(format!("commit {} cannot be merged; it is marked as a commit \
                                          to be squashed.",
                                         commit.sha1));
            }
        }

        Ok(result)
    }
}

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

    const BAD_TOPIC: &str = "5f7284fe1599265c90550b681a4bf0763bc1de21";

    #[test]
    fn test_check_subject() {
        let check = CommitSubject::new();
        let result = run_check("test_check_subject", BAD_TOPIC, check);
        test_result_errors(result, &[
            "commit 5f7284fe1599265c90550b681a4bf0763bc1de21 cannot be merged; it is marked as a \
             commit to be squashed.",
            "commit 54d673ff559a72ce6343bd9526a950d79034b24e cannot be merged; it is marked as a \
             fixup commit.",
            "commit 11dbbbff3f32445d74d1a8d96df0a49381c81ba0 cannot be merged; it is marked as a \
             work-in-progress (WIP).",
            "commit 9039b9a4813fa019229e960033fe1ae8514a0c8e cannot be merged; it is marked as a \
             work-in-progress (WIP).",
            "commit e478f630b586e331753477eba88059d644927be8 cannot be merged; it is marked as a \
             work-in-progress (WIP).",
            "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
             second line must be empty.",
            "commit 3a6fe6d56fbf11c667b6c88bdd7d851a8dcac0b1 has an invalid commit subject; the \
             third line must not be empty.",
            "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; the \
             second line must be empty.",
            "commit b1ca628043ed78625551420e2dbbd8cf74fde2c4 has an invalid commit subject; it \
             cannot be exactly two lines.",
            "commit 1afc6b3584580488917fc61aa5e5298e98583805 has an invalid commit subject; the \
             first line must be no longer than 78 characters.",
            "commit 234de3c3f17ab29f0b7644ae96242e31a3dd634c has an invalid commit subject; the \
             first line must be at least 8 characters.",
        ]);
    }
}