Java tutorial
/* * This file is part of git-commit-id-plugin by Konrad Malawski <konrad.malawski@java.pl> * * git-commit-id-plugin 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 3 of the License, or * (at your option) any later version. * * git-commit-id-plugin 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 General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with git-commit-id-plugin. If not, see <http://www.gnu.org/licenses/>. */ package pl.project13.maven.git; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Lists; import com.google.common.io.Closeables; import com.google.common.io.Files; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.*; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import pl.project13.jgit.DescribeCommand; import pl.project13.jgit.DescribeResult; import pl.project13.maven.git.log.LoggerBridge; import pl.project13.maven.git.log.MavenLoggerBridge; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.*; import static com.google.common.base.Strings.isNullOrEmpty; /** * Goal which puts git build-time information into property files or maven's properties. * * @author <a href="mailto:konrad.malawski@java.pl">Konrad 'ktoso' Malawski</a> * @goal revision * @phase initialize * @requiresProject * @threadSafe true * @since 1.0 */ @SuppressWarnings({ "JavaDoc" }) public class GitCommitIdMojo extends AbstractMojo { // these properties will be exposed to maven public static final String BRANCH = "branch"; public static final String COMMIT_ID = "commit.id"; public static final String COMMIT_ID_ABBREV = "commit.id.abbrev"; public static final String COMMIT_DESCRIBE = "commit.id.describe"; public static final String COMMIT_SHORT_DESCRIBE = "commit.id.short-describe"; public static final String BUILD_AUTHOR_NAME = "build.user.name"; public static final String BUILD_AUTHOR_EMAIL = "build.user.email"; public static final String BUILD_TIME = "build.time"; public static final String COMMIT_AUTHOR_NAME = "commit.user.name"; public static final String COMMIT_AUTHOR_EMAIL = "commit.user.email"; public static final String COMMIT_MESSAGE_FULL = "commit.message.full"; public static final String COMMIT_MESSAGE_SHORT = "commit.message.short"; public static final String COMMIT_TIME = "commit.time"; public static final String REMOTE_ORIGIN_URL = "remote.origin.url"; /** * The maven project. * * @parameter property="project" * @readonly */ @SuppressWarnings("UnusedDeclaration") MavenProject project; /** * Contains the full list of projects in the reactor. * * @parameter property="reactorProjects" * @readonly */ @SuppressWarnings("UnusedDeclaration") private List<MavenProject> reactorProjects; /** * Tell git-commit-id to inject the git properties into all * reactor projects not just the current one. * <p/> * For details about why you might want to skip this, read this issue: https://github.com/ktoso/maven-git-commit-id-plugin/pull/65 * Basically, injecting into all projects may slow down the build and you don't always need this feature. * * @parameter default-value="true" */ @SuppressWarnings("UnusedDeclaration") private boolean injectAllReactorProjects; /** * Specifies whether the goal runs in verbose mode. * To be more specific, this means more info being printed out while scanning for paths and also * it will make git-commit-id "eat it's own dog food" :-) * * @parameter default-value="false" */ @SuppressWarnings("UnusedDeclaration") private boolean verbose; /** * Specifies whether the execution in pom projects should be skipped. * Override this value to false if you want to force the plugin to run on 'pom' packaged projects. * * @parameter parameter="git.skipPoms" default-value="true" */ @SuppressWarnings("UnusedDeclaration") private boolean skipPoms; /** * Specifies whether plugin should generate properties file. * By default it will not generate any additional file, * and only add properties to maven project's properties for further filtering * <p/> * If set to "true" properties will be fully generated with no placeholders inside. * * @parameter default-value="false" */ @SuppressWarnings("UnusedDeclaration") private boolean generateGitPropertiesFile; /** * Decide where to generate the git.properties file. By default, the ${project.build.outputDirectory}/git.properties * file will be updated - of course you must first set generateGitPropertiesFile = true to force git-commit-id * into generateFile mode. * <p/> * The path here is relative to your projects src directory. * * @parameter default-value="${project.build.outputDirectory}/git.properties" */ @SuppressWarnings("UnusedDeclaration") private String generateGitPropertiesFilename; /** * The root directory of the repository we want to check * * @parameter default-value="${project.basedir}/.git" */ @SuppressWarnings("UnusedDeclaration") private File dotGitDirectory; /** * Configuration for the <pre>git-describe</pre> command. * You can modify the dirty marker, abbrev length and other options here. * <p/> * If not specified, default values will be used. * * @parameter */ @SuppressWarnings("UnusedDeclaration") private GitDescribeConfig gitDescribe; /** * <p> * Configure the "git.commit.id.abbrev" property to be at least of length N. * N must be in the range of 2 to 40 (inclusive), other values will result in an Exception. * </p> * <p/> * <p> * An Abbreviated commit is a shorter version of the commit id, it is guaranteed to be unique though. * To keep this contract, the plugin may decide to print an abbrev version that is longer than the value specified here. * </p> * <p/> * <b>Example:</b> * <p> * You have a very big repository, yet you set this value to 2. It's very probable that you'll end up getting a 4 or 7 char * long abbrev version of the commit id. If your repository, on the other hand, has just 4 commits, you'll probably get a 2 char long abbrev. * </p> * * @parameter default-value=7 */ @SuppressWarnings("UnusedDeclaration") private int abbrevLength; /** * The format to save properties in. Valid options are "properties" (default) and "json". * * @parameter default-value="properties" */ @SuppressWarnings("UnusedDeclaration") private String format; /** * The prefix to expose the properties on, for example 'git' would allow you to access '${git.branch}' * * @parameter default-value="git" */ @SuppressWarnings("UnusedDeclaration") private String prefix; private String prefixDot = ""; /** * The date format to be used for any dates exported by this plugin. * It should be a valid SimpleDateFormat string. * * @parameter default-value="dd.MM.yyyy '@' HH:mm:ss z" */ @SuppressWarnings("UnusedDeclaration") private String dateFormat; /** * Specifies whether the plugin should fail if it can't find the .git directory. The default * value is true. * * @parameter default-value="true" */ @SuppressWarnings("UnusedDeclaration") private boolean failOnNoGitDirectory; /** * By default the plugin will fail the build if unable to obtain enough data for a complete run, * if you don't care about this - for example it's not needed during your CI builds and the CI server does weird * things to the repository, you may want to set this value to false. * <p/> * Setting this value to `false`, causes the plugin to gracefully tell you "I did my best" and abort it's execution * if unable to obtain git meta data - yet the build will continue to run (without failing). * <p/> * See https://github.com/ktoso/maven-git-commit-id-plugin/issues/63 for a rationale behing this flag. * * @parameter default-value="true" */ @SuppressWarnings("UnusedDeclaration") private boolean failOnUnableToExtractRepoInfo; /** * Skip the plugin execution. * * @parameter default-value="false" * @since 2.1.8 */ @SuppressWarnings("UnusedDeclaration") private boolean skip = false; /** * Can be used to exclude certain properties from being emited into the resulting file. * May be useful when you want to hide {@code git.remote.origin.url} (maybe because it contains your repo password?), * or the email of the committer etc. * * Each value may be globbing, that is, you can write {@code git.commit.user.*} to exclude both, the {@code name}, * as well as {@code email} properties from being emitted into the resulting files. * * Please note that the strings here are Java regexes ({@code .*} is globbing, not plain {@code *}). * * @parameter * @since 2.1.9 */ @SuppressWarnings("UnusedDeclaration") private List<String> excludeProperties = Collections.emptyList(); /** * The properties we store our data in and then expose them */ private Properties properties; boolean runningTests = false; @NotNull LoggerBridge loggerBridge = new MavenLoggerBridge(getLog(), true); public void execute() throws MojoExecutionException { // Set the verbose setting now it should be correctly loaded from maven. loggerBridge.setVerbose(verbose); if (skip) { log("skip is true, return"); return; } if (isPomProject(project) && skipPoms) { log("isPomProject is true and skipPoms is true, return"); return; } dotGitDirectory = lookupGitDirectory(); throwWhenRequiredDirectoryNotFound(dotGitDirectory, failOnNoGitDirectory, ".git directory could not be found! Please specify a valid [dotGitDirectory] in your pom.xml"); if (dotGitDirectory != null) { log("dotGitDirectory", dotGitDirectory.getAbsolutePath()); } else { log("dotGitDirectory is null, aborting execution!"); return; } try { properties = initProperties(); String trimmedPrefix = prefix.trim(); prefixDot = trimmedPrefix.equals("") ? "" : trimmedPrefix + "."; loadGitData(properties); loadBuildTimeData(properties); loadShortDescribe(properties); filterNot(properties, excludeProperties); logProperties(properties); if (generateGitPropertiesFile) { generatePropertiesFile(properties, project.getBasedir(), generateGitPropertiesFilename); } if (injectAllReactorProjects) { appendPropertiesToReactorProjects(properties); } } catch (Exception e) { handlePluginFailure(e); } } private void filterNot(Properties properties, @Nullable List<String> exclusions) { if (exclusions == null) return; List<Predicate<CharSequence>> excludePredicates = Lists.transform(exclusions, new Function<String, Predicate<CharSequence>>() { @Override public Predicate<CharSequence> apply(String exclude) { return Predicates.containsPattern(exclude); } }); Predicate<CharSequence> shouldExclude = Predicates.alwaysFalse(); for (Predicate<CharSequence> predicate : excludePredicates) { shouldExclude = Predicates.or(shouldExclude, predicate); } for (String key : properties.stringPropertyNames()) { if (shouldExclude.apply(key)) { loggerBridge.debug("shouldExclude.apply(" + key + ") = " + shouldExclude.apply(key)); properties.remove(key); } } } /** * Reacts to an exception based on the {@code failOnUnableToExtractRepoInfo} setting. * If it's true, an MojoExecutionException will be throw, otherwise we just log an error message. * * @throws MojoExecutionException which will be should be throw within an MojoException in case the * {@code failOnUnableToExtractRepoInfo} setting was set to true. */ private void handlePluginFailure(Exception e) throws MojoExecutionException { if (failOnUnableToExtractRepoInfo) { throw new MojoExecutionException("Could not complete Mojo execution...", e); } else { loggerBridge.error(e.getMessage()); } } private void appendPropertiesToReactorProjects(@NotNull Properties properties) { for (MavenProject mavenProject : reactorProjects) { Properties mavenProperties = mavenProject.getProperties(); log(mavenProject.getName(), "] project", mavenProject.getName()); for (Object key : properties.keySet()) { mavenProperties.put(key, properties.get(key)); } } } private void throwWhenRequiredDirectoryNotFound(File dotGitDirectory, Boolean required, String message) throws MojoExecutionException { if (required && directoryDoesNotExits(dotGitDirectory)) { throw new MojoExecutionException(message); } } /** * Find the git directory of the currently used project. * If it's not already specified, this method will try to find it. * * @return the File representation of the .git directory */ private File lookupGitDirectory() throws MojoExecutionException { return new GitDirLocator(project, reactorProjects).lookupGitDirectory(dotGitDirectory); } private Properties initProperties() throws MojoExecutionException { if (generateGitPropertiesFile) { return properties = new Properties(); } else if (!runningTests) { return properties = project.getProperties(); } else { return properties = new Properties(); // that's ok for unit tests } } private void logProperties(@NotNull Properties properties) { for (Object key : properties.keySet()) { String keyString = key.toString(); if (isOurProperty(keyString)) { log("found property", keyString); } } } private boolean isOurProperty(@NotNull String keyString) { return keyString.startsWith(prefixDot); } void loadBuildTimeData(@NotNull Properties properties) { Date commitDate = new Date(); SimpleDateFormat smf = new SimpleDateFormat(dateFormat); put(properties, BUILD_TIME, smf.format(commitDate)); } void loadShortDescribe(@NotNull Properties properties) { //removes git hash part from describe String commitDescribe = properties.getProperty(prefixDot + COMMIT_DESCRIBE); if (commitDescribe != null) { int startPos = commitDescribe.indexOf("-g"); if (startPos > 0) { String commitShortDescribe; int endPos = commitDescribe.indexOf('-', startPos + 1); if (endPos < 0) { commitShortDescribe = commitDescribe.substring(0, startPos); } else { commitShortDescribe = commitDescribe.substring(0, startPos) + commitDescribe.substring(endPos); } put(properties, COMMIT_SHORT_DESCRIBE, commitShortDescribe); } else { put(properties, COMMIT_SHORT_DESCRIBE, commitDescribe); } } } void loadGitData(@NotNull Properties properties) throws IOException, MojoExecutionException { Repository git = getGitRepository(); ObjectReader objectReader = git.newObjectReader(); // git.user.name String userName = git.getConfig().getString("user", null, "name"); put(properties, BUILD_AUTHOR_NAME, userName); // git.user.email String userEmail = git.getConfig().getString("user", null, "email"); put(properties, BUILD_AUTHOR_EMAIL, userEmail); // more details parsed out bellow Ref HEAD = git.getRef(Constants.HEAD); if (HEAD == null) { throw new MojoExecutionException( "Could not get HEAD Ref, are you sure you've set the dotGitDirectory property of this plugin to a valid path?"); } RevWalk revWalk = new RevWalk(git); RevCommit headCommit = revWalk.parseCommit(HEAD.getObjectId()); revWalk.markStart(headCommit); try { // git.branch String branch = determineBranchName(git, System.getenv()); put(properties, BRANCH, branch); // git.commit.id.describe maybePutGitDescribe(properties, git); // git.commit.id put(properties, COMMIT_ID, headCommit.getName()); // git.commit.id.abbrev putAbbrevCommitId(objectReader, properties, headCommit, abbrevLength); // git.commit.author.name String commitAuthor = headCommit.getAuthorIdent().getName(); put(properties, COMMIT_AUTHOR_NAME, commitAuthor); // git.commit.author.email String commitEmail = headCommit.getAuthorIdent().getEmailAddress(); put(properties, COMMIT_AUTHOR_EMAIL, commitEmail); // git commit.message.full String fullMessage = headCommit.getFullMessage(); put(properties, COMMIT_MESSAGE_FULL, fullMessage); // git commit.message.short String shortMessage = headCommit.getShortMessage(); put(properties, COMMIT_MESSAGE_SHORT, shortMessage); long timeSinceEpoch = headCommit.getCommitTime(); Date commitDate = new Date(timeSinceEpoch * 1000); // git is "by sec" and java is "by ms" SimpleDateFormat smf = new SimpleDateFormat(dateFormat); put(properties, COMMIT_TIME, smf.format(commitDate)); // git remote.origin.url String remoteOriginUrl = git.getConfig().getString("remote", "origin", "url"); put(properties, REMOTE_ORIGIN_URL, remoteOriginUrl); } finally { revWalk.dispose(); } } private void putAbbrevCommitId(ObjectReader objectReader, Properties properties, RevCommit headCommit, int abbrevLength) throws MojoExecutionException { if (abbrevLength < 2 || abbrevLength > 40) { throw new MojoExecutionException( "Abbreviated commit id lenght must be between 2 and 40, inclusive! Was [%s]. ".codePointBefore( abbrevLength) + "Please fix your configuration (the <abbrevLength/> element)."); } try { AbbreviatedObjectId abbreviatedObjectId = objectReader.abbreviate(headCommit, abbrevLength); put(properties, COMMIT_ID_ABBREV, abbreviatedObjectId.name()); } catch (IOException e) { throw new MojoExecutionException("Unable to abbreviate commit id! " + "You may want to investigate the <abbrevLength/> element in your configuration.", e); } } void maybePutGitDescribe(@NotNull Properties properties, @NotNull Repository repository) throws MojoExecutionException { if (gitDescribe == null || !gitDescribe.isSkip()) { putGitDescribe(properties, repository); } } @VisibleForTesting void putGitDescribe(@NotNull Properties properties, @NotNull Repository repository) throws MojoExecutionException { try { DescribeResult describeResult = DescribeCommand.on(repository).withLoggerBridge(loggerBridge) .setVerbose(verbose).apply(gitDescribe).call(); put(properties, COMMIT_DESCRIBE, describeResult.toString()); } catch (GitAPIException ex) { throw new MojoExecutionException("Unable to obtain git.commit.id.describe information", ex); } } static int counter; void generatePropertiesFile(@NotNull Properties properties, File base, String propertiesFilename) throws IOException { FileWriter fileWriter = null; File gitPropsFile = new File(base, propertiesFilename); try { Files.createParentDirs(gitPropsFile); fileWriter = new FileWriter(gitPropsFile); if ("json".equalsIgnoreCase(format)) { log("Writing json file to [", gitPropsFile.getAbsolutePath(), "] (for module ", project.getName() + (++counter), ")..."); ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(fileWriter, properties); } else { log("Writing properties file to [", gitPropsFile.getAbsolutePath(), "] (for module ", project.getName() + (++counter), ")..."); properties.store(fileWriter, "Generated by Git-Commit-Id-Plugin"); } } catch (IOException ex) { throw new RuntimeException("Cannot create custom git properties file: " + gitPropsFile, ex); } finally { Closeables.closeQuietly(fileWriter); } } boolean isPomProject(@NotNull MavenProject project) { return project.getPackaging().equalsIgnoreCase("pom"); } @NotNull private Repository getGitRepository() throws MojoExecutionException { Repository repository; FileRepositoryBuilder repositoryBuilder = new FileRepositoryBuilder(); try { repository = repositoryBuilder.setGitDir(dotGitDirectory).readEnvironment() // scan environment GIT_* variables .findGitDir() // scan up the file system tree .build(); } catch (IOException e) { throw new MojoExecutionException("Could not initialize repository...", e); } if (repository == null) { throw new MojoExecutionException("Could not create git repository. Are you sure '" + dotGitDirectory + "' is the valid Git root for your project?"); } return repository; } private void put(@NotNull Properties properties, String key, String value) { putWithoutPrefix(properties, prefixDot + key, value); } private void putWithoutPrefix(@NotNull Properties properties, String key, String value) { if (!isNotEmpty(value)) { value = "Unknown"; } log(key, value); properties.put(key, value); } private boolean isNotEmpty(@Nullable String value) { return null != value && !" ".equals(value.trim().replaceAll(" ", "")); } void log(String... parts) { loggerBridge.log((Object[]) parts); } private boolean directoryExists(@Nullable File fileLocation) { return fileLocation != null && fileLocation.exists() && fileLocation.isDirectory(); } private boolean directoryDoesNotExits(File fileLocation) { return !directoryExists(fileLocation); } /** * If running within Jenkins/Hudosn, honor the branch name passed via GIT_BRANCH env var. This * is necessary because Jenkins/Hudson alwways invoke build in a detached head state. * * @param git * @param env * @return results of git.getBranch() or, if in Jenkins/Hudson, value of GIT_BRANCH */ protected String determineBranchName(Repository git, Map<String, String> env) throws IOException { if (runningOnBuildServer(env)) { return determineBranchNameOnBuildServer(git, env); } else { return git.getBranch(); } } /** * Is "Jenkins aware", and prefers {@code GIT_BRANCH} to getting the branch via git if that enviroment variable is set. * The {@GIT_BRANCH} variable is set by Jenkins/Hudson when put in detached HEAD state, but it still knows which branch was cloned. */ protected String determineBranchNameOnBuildServer(Repository git, Map<String, String> env) throws IOException { String enviromentBasedBranch = env.get("GIT_BRANCH"); if (isNullOrEmpty(enviromentBasedBranch)) { log("Detected that running on CI enviroment, but using repository branch, no GIT_BRANCH detected."); return git.getBranch(); } else { log("Using environment variable based branch name.", "GIT_BRANCH =", enviromentBasedBranch); return enviromentBasedBranch; } } /** * Detects if we're running on Jenkins or Hudson, based on expected env variables. * <p/> * TODO: How can we detect Bamboo, TeamCity etc? Pull requests welcome. * * @return true if running * @see <a href="https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project#Buildingasoftwareproject-JenkinsSetEnvironmentVariables">JenkinsSetEnvironmentVariables</a> * @param env */ private boolean runningOnBuildServer(Map<String, String> env) { return env.containsKey("HUDSON_URL") || env.containsKey("JENKINS_URL"); } // SETTERS FOR TESTS ---------------------------------------------------- public void setFormat(String format) { this.format = format; } public void setVerbose(boolean verbose) { this.verbose = verbose; } public void setDotGitDirectory(File dotGitDirectory) { this.dotGitDirectory = dotGitDirectory; } public void setPrefix(String prefix) { this.prefix = prefix; } public void setDateFormat(String dateFormat) { this.dateFormat = dateFormat; } public Properties getProperties() { return properties; } public void setGitDescribe(GitDescribeConfig gitDescribe) { this.gitDescribe = gitDescribe; } public void setAbbrevLength(int abbrevLength) { this.abbrevLength = abbrevLength; } public void setExcludeProperties(List<String> excludeProperties) { this.excludeProperties = excludeProperties; } }