Java tutorial
//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2015 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.puppycrawl.tools.checkstyle; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.junit.BeforeClass; import org.junit.Test; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; /** * Validate commit message has proper structure. * * Commits to check are resolved from different places according * to type of commit in current HEAD. If current HEAD commit is * non-merge commit , previous commits are resolved due to current * HEAD commit. Otherwise if it is a merge commit, it will invoke * resolving previous commits due to commits which was merged. * * After calculating commits to start with ts resolves previous * commits according to COMMITS_RESOLUTION_MODE variable. * At default(BY_LAST_COMMIT_AUTHOR) it checks first commit author * and return all consecutive commits with same author. Second * mode(BY_COUNTER) makes returning first PREVIOUS_COMMITS_TO_CHECK_COUNT * commits after starter commit. * * Resolved commits are filtered according to author. If commit author * belong to list USERS_EXCLUDED_FROM_VALIDATION then this commit will * not be validated. * * Filtered commit list is checked if their messages has proper structure. * * @author <a href="mailto:piotr.listkiewicz@gmail.com">liscju</a> */ public class CommitValidationTest { private static final List<String> USERS_EXCLUDED_FROM_VALIDATION = Collections.singletonList("Roman Ivanov"); private static final String ISSUE_COMMIT_MESSAGE_REGEX_PATTERN = "^Issue #\\d*: .*$"; private static final String PR_COMMIT_MESSAGE_REGEX_PATTERN = "^Pull #\\d*: .*$"; private static final String OTHER_COMMIT_MESSAGE_REGEX_PATTERN = "^(minor|config|infra|doc|spelling): .*$"; private static final String ACCEPTED_COMMIT_MESSAGE_REGEX_PATTERN = "(" + ISSUE_COMMIT_MESSAGE_REGEX_PATTERN + ")|" + "(" + PR_COMMIT_MESSAGE_REGEX_PATTERN + ")|" + "(" + OTHER_COMMIT_MESSAGE_REGEX_PATTERN + ")"; private static final Pattern ACCEPTED_COMMIT_MESSAGE_PATTERN = Pattern .compile(ACCEPTED_COMMIT_MESSAGE_REGEX_PATTERN, Pattern.DOTALL); private static final String NEWLINE_REGEX_PATTERN = "\r\n?|\n"; private static final Pattern NEWLINE_PATTERN = Pattern.compile(NEWLINE_REGEX_PATTERN); private static final int PREVIOUS_COMMITS_TO_CHECK_COUNT = 10; private enum CommitsResolutionMode { BY_COUNTER, BY_LAST_COMMIT_AUTHOR } private static final CommitsResolutionMode COMMITS_RESOLUTION_MODE = CommitsResolutionMode.BY_LAST_COMMIT_AUTHOR; private static List<RevCommit> lastCommits; @BeforeClass public static void setUp() throws Exception { lastCommits = getCommitsToCheck(); } @Test public void testCommitMessageHasProperStructure() throws Exception { for (RevCommit commit : filterValidCommits(lastCommits)) { String commitId = commit.getId().getName(); String commitMessage = commit.getFullMessage(); Matcher matcher = ACCEPTED_COMMIT_MESSAGE_PATTERN.matcher(commitMessage); assertTrue(getInvalidCommitMessageFormattingError(commitId, commitMessage), matcher.matches()); } } @Test public void testCommitMessageHasSingleLine() throws Exception { for (RevCommit commit : filterValidCommits(lastCommits)) { String commitId = commit.getId().getName(); String commitMessage = commit.getFullMessage(); Matcher matcher = NEWLINE_PATTERN.matcher(commitMessage); if (matcher.find()) { /** * Git by default put newline character at the end of commit message. To check * if commit message has a single line we have to make sure that the only * newline character found is in the end of commit message. */ boolean isFoundNewLineCharacterAtTheEndOfMessage = matcher.end() == commitMessage.length(); assertTrue(getInvalidCommitMessageFormattingError(commitId, commitMessage), isFoundNewLineCharacterAtTheEndOfMessage); } } } private static List<RevCommit> getCommitsToCheck() throws Exception { Repository repo = new FileRepositoryBuilder().findGitDir().build(); RevCommitsPair revCommitsPair = resolveRevCommitsPair(repo); List<RevCommit> commits; if (COMMITS_RESOLUTION_MODE == CommitsResolutionMode.BY_COUNTER) { commits = getCommitsByCounter(revCommitsPair.getFirst()); commits.addAll(getCommitsByCounter(revCommitsPair.getSecond())); } else { commits = getCommitsByLastCommitAuthor(revCommitsPair.getFirst()); commits.addAll(getCommitsByLastCommitAuthor(revCommitsPair.getSecond())); } return commits; } private static List<RevCommit> filterValidCommits(List<RevCommit> revCommits) { List<RevCommit> filteredCommits = new LinkedList<>(); for (RevCommit commit : revCommits) { String commitAuthor = commit.getAuthorIdent().getName(); if (!USERS_EXCLUDED_FROM_VALIDATION.contains(commitAuthor)) { filteredCommits.add(commit); } } return filteredCommits; } private static RevCommitsPair resolveRevCommitsPair(Repository repo) { RevCommitsPair revCommitIteratorPair; try { Iterator<RevCommit> first; Iterator<RevCommit> second; RevWalk revWalk = new RevWalk(repo); ObjectId headId = repo.resolve(Constants.HEAD); RevCommit headCommit = revWalk.parseCommit(headId); if (isMergeCommit(headCommit)) { RevCommit firstParent = headCommit.getParent(0); RevCommit secondParent = headCommit.getParent(1); first = new Git(repo).log().add(firstParent).call().iterator(); second = new Git(repo).log().add(secondParent).call().iterator(); } else { first = new Git(repo).log().call().iterator(); second = Collections.emptyIterator(); } revCommitIteratorPair = new RevCommitsPair(new OmitMergeCommitsIterator(first), new OmitMergeCommitsIterator(second)); } catch (GitAPIException | IOException e) { revCommitIteratorPair = new RevCommitsPair(); } return revCommitIteratorPair; } private static boolean isMergeCommit(RevCommit currentCommit) { return currentCommit.getParentCount() > 1; } private static List<RevCommit> getCommitsByCounter(Iterator<RevCommit> previousCommitsIterator) { return Lists.newArrayList(Iterators.limit(previousCommitsIterator, PREVIOUS_COMMITS_TO_CHECK_COUNT)); } private static List<RevCommit> getCommitsByLastCommitAuthor(Iterator<RevCommit> previousCommitsIterator) { List<RevCommit> commits = new LinkedList<>(); if (previousCommitsIterator.hasNext()) { RevCommit lastCommit = previousCommitsIterator.next(); String lastCommitAuthor = lastCommit.getAuthorIdent().getName(); commits.add(lastCommit); boolean wasLastCheckedCommitAuthorSameAsLastCommit = true; while (previousCommitsIterator.hasNext() && wasLastCheckedCommitAuthorSameAsLastCommit) { RevCommit currentCommit = previousCommitsIterator.next(); String currentCommitAuthor = currentCommit.getAuthorIdent().getName(); if (currentCommitAuthor.equals(lastCommitAuthor)) { commits.add(currentCommit); } else { wasLastCheckedCommitAuthorSameAsLastCommit = false; } } } return commits; } private static String getRulesForCommitMessageFormatting() { return "Proper commit message should adhere to the following rules:\n" + " 1) Must match one of the following patterns:\n" + " " + ISSUE_COMMIT_MESSAGE_REGEX_PATTERN + "\n" + " " + PR_COMMIT_MESSAGE_REGEX_PATTERN + "\n" + " " + OTHER_COMMIT_MESSAGE_REGEX_PATTERN + "\n" + " 2) It contains only one line"; } private static String getInvalidCommitMessageFormattingError(String commitId, String commitMessage) { return "Commit " + commitId + " message: \"" + commitMessage + "\" is invalid\n" + getRulesForCommitMessageFormatting(); } private static class RevCommitsPair { private final Iterator<RevCommit> first; private final Iterator<RevCommit> second; RevCommitsPair() { first = Collections.emptyIterator(); second = Collections.emptyIterator(); } RevCommitsPair(Iterator<RevCommit> first, Iterator<RevCommit> second) { this.first = first; this.second = second; } public Iterator<RevCommit> getFirst() { return first; } public Iterator<RevCommit> getSecond() { return second; } } private static class OmitMergeCommitsIterator implements Iterator<RevCommit> { private final Iterator<RevCommit> revCommitIterator; OmitMergeCommitsIterator(Iterator<RevCommit> revCommitIterator) { this.revCommitIterator = revCommitIterator; } @Override public boolean hasNext() { return revCommitIterator.hasNext(); } @Override public RevCommit next() { RevCommit currentCommit = revCommitIterator.next(); while (isMergeCommit(currentCommit)) { currentCommit = revCommitIterator.next(); } return currentCommit; } @Override public void remove() { throw new UnsupportedOperationException("remove"); } } }