package ru.di9.gradle.commitlint import org.gradle.api.DefaultTask import org.gradle.api.InvalidUserDataException import org.gradle.api.tasks.TaskAction import java.nio.file.Files import java.nio.file.Path import java.util.regex.Pattern class CommitLintTask extends DefaultTask { private static final List DEFAULT_TYPES = ["build", "docs", "add", "tweak", "fix", "remove", "revert", "perf", "refac", "style", "test"] private static final List DEFAULT_CONTEXTS = ["git", "ide"] private static final List DEFAULT_EXCLUDES = [Pattern.compile("^wip\$")] private static final int DEFAULT_MAX_LINE = 72 private static final Path DEFAULT_COMMIT_MESSAGE_PATH = Path.of(".git/COMMIT_EDITMSG") private static final List EXCLUDES = [ Pattern.compile("^rebase "), Pattern.compile("^merge "), ] private static final Pattern COMMIT_FIRST_LINE = Pattern.compile("^([a-z0-9]+?)!?(?:\\(([a-z0-9]+?)\\))?: .+\$") private final Set commitTypes = new HashSet<>() private final Set commitContexts = new HashSet<>() private final Set commitExcludes = new HashSet<>() private int commitMaxLength = 0 @TaskAction void execute() throws IOException { def ext = getProject().getExtensions() .getByType(CommitLintExtension.class) if (ext.types.isPresent()) { commitTypes.addAll(ext.types.get().collect { it.toLowerCase() }) } else { commitTypes.addAll(DEFAULT_TYPES) } if (ext.contexts.isPresent()) { commitContexts.addAll(ext.contexts.get().collect { it.toLowerCase() }) } else { commitContexts.addAll(DEFAULT_CONTEXTS) } commitExcludes.addAll(EXCLUDES) if (ext.excludes.isPresent()) { commitExcludes.addAll(ext.excludes.get().collect { Pattern.compile(it) }) } else { commitExcludes.addAll(DEFAULT_EXCLUDES) } commitMaxLength = ext.maxLengthLine.getOrElse(DEFAULT_MAX_LINE) Path gitCommitMessageFile if (this.getProject().getProperties().containsKey("gitCommitMessageFile")) { gitCommitMessageFile = Path.of((String) getProject().getProperties().get("gitCommitMessageFile")) } else { gitCommitMessageFile = DEFAULT_COMMIT_MESSAGE_PATH } def commitMessage = Files.readAllLines(getProject().rootDir.toPath().resolve(gitCommitMessageFile)) if (commitExcludes.any { it.matcher(commitMessage[0].toLowerCase()).find() }) { return } validateFirstLine(commitMessage[0]) if (commitMessage.size() == 1) { return } validateBody(commitMessage.subList(1, commitMessage.size())) } private void validateFirstLine(String firstLine) { def matcher = COMMIT_FIRST_LINE.matcher(firstLine) if (!matcher.find()) { throw new InvalidUserDataException("Incorrect commit message format. See CONVENTIONAL_COMMITS.MD") } def commitType = matcher.group(1) if (!commitTypes.contains(commitType)) { throw new InvalidUserDataException("Unknown commit type '%s'. See CONVENTIONAL_COMMITS.MD".formatted(commitType)) } def commitContext = matcher.group(2) if (commitContext != null && !commitContexts.contains(commitContext)) { throw new InvalidUserDataException("Unknown commit context '%s'. See CONVENTIONAL_COMMITS.MD".formatted(commitContext)) } if (firstLine.length() > commitMaxLength) { throw new InvalidUserDataException("Length of first line exceeds %d characters. See CONVENTIONAL_COMMITS.MD".formatted(commitMaxLength)) } } @SuppressWarnings('GrMethodMayBeStatic') private void validateBody(List commitMessage) { println(commitMessage[0]) if (commitMessage[0] != "") { throw new InvalidUserDataException("Use blank line before BODY. See CONVENTIONAL_COMMITS.MD") } def cc = commitMaxLength if (commitMessage.subList(1, commitMessage.size()).any { it.length() > cc }) { throw new InvalidUserDataException("Length of line in body part exceeds %d characters. See CONVENTIONAL_COMMITS.MD".formatted(commitMaxLength)) } } }