package me.qoomon.maven.gitversioning; import static java.lang.Boolean.parseBoolean; import static java.lang.Math.ceil; import static java.lang.Math.floor; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toList; import static me.qoomon.UncheckedExceptions.unchecked; import static me.qoomon.maven.gitversioning.MavenUtil.readModel; import static me.qoomon.maven.gitversioning.VersioningMojo.GIT_VERSIONING_POM_NAME; import static me.qoomon.maven.gitversioning.VersioningMojo.GOAL; import static me.qoomon.maven.gitversioning.VersioningMojo.asPlugin; import static me.qoomon.maven.gitversioning.VersioningMojo.propertyKeyPrefix; import static me.qoomon.maven.gitversioning.VersioningMojo.propertyKeyUpdatePom; import static org.apache.maven.shared.utils.StringUtils.repeat; import static org.apache.maven.shared.utils.logging.MessageUtils.buffer; import java.io.*; import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.Map.Entry; import java.util.regex.Pattern; import javax.inject.Inject; import org.apache.maven.building.Source; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Build; import org.apache.maven.model.Model; import org.apache.maven.model.Parent; import org.apache.maven.model.Plugin; import org.apache.maven.model.PluginExecution; import org.apache.maven.model.Profile; import org.apache.maven.model.building.ModelProcessor; import org.apache.maven.model.io.DefaultModelReader; import org.apache.maven.model.locator.DefaultModelLocator; import org.apache.maven.session.scope.internal.SessionScope; import org.codehaus.plexus.logging.Logger; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.google.common.collect.Maps; import com.google.inject.Key; import com.google.inject.OutOfScopeException; import me.qoomon.gitversioning.GitRepoSituation; import me.qoomon.gitversioning.GitUtil; import me.qoomon.gitversioning.GitVersionDetails; import me.qoomon.gitversioning.GitVersioning; import me.qoomon.gitversioning.PropertyDescription; import me.qoomon.gitversioning.PropertyValueDescription; import me.qoomon.gitversioning.VersionDescription; /** * WORKAROUND * Initialize and use {@link GitVersioningModelProcessor} from GitModelProcessor {@link org.apache.maven.model.building.ModelProcessor}, * This is need because maven 3.6.2 has broken component replacement mechanism. */ public class GitVersioningModelProcessor { private static final String OPTION_NAME_GIT_TAG = "git.tag"; private static final String OPTION_NAME_GIT_BRANCH = "git.branch"; private static final String OPTION_NAME_DISABLE = "versioning.disable"; private static final String OPTION_UPDATE_POM = "versioning.updatePom"; private static final String OPTION_PREFER_TAGS = "versioning.preferTags"; @Inject private Logger logger; @Inject private SessionScope sessionScope; private boolean initialized = false; private boolean disabled = false; private MavenSession mavenSession; // can not be injected cause it is not always available private File mvnDirectory; private File gitDirectory; private Configuration config; private GitVersionDetails gitVersionDetails; private final Set<String> sessionProjectDirectories = new HashSet<>(); private final Map<String, Model> virtualProjectModelCache = new HashMap<>(); public Model processModel(Model projectModel, Map<String, ?> options) throws IOException { if (this.disabled) { return projectModel; } final Source pomSource = (Source) options.get(ModelProcessor.SOURCE); if (pomSource != null) { projectModel.setPomFile(new File(pomSource.getLocation())); } try { if (!initialized) { logger.info(""); String extensionId = BuildProperties.projectArtifactId() + ":" + BuildProperties.projectVersion(); logger.info(extensionLogFormat(extensionId)); try { mavenSession = sessionScope.scope(Key.get(MavenSession.class), null).get(); } catch (OutOfScopeException ex) { logger.warn("skip - no maven session present"); disabled = true; return projectModel; } if (parseBoolean(getCommandOption(OPTION_NAME_DISABLE))) { logger.info("skip - versioning is disabled"); disabled = true; return projectModel; } File executionRootDirectory = new File(mavenSession.getRequest().getBaseDirectory()); logger.debug("executionRootDirectory: " + executionRootDirectory.toString()); mvnDirectory = findMvnDirectory(executionRootDirectory); logger.debug("mvnDirectory: " + mvnDirectory.toString()); String configFileName = BuildProperties.projectArtifactId() + ".xml"; File configFile = new File(mvnDirectory, configFileName); logger.debug("configFile: " + configFile.toString()); config = loadConfig(configFile); gitDirectory = findGitDir(executionRootDirectory); if (gitDirectory == null || !gitDirectory.exists()) { logger.warn("skip - project is not part of a git repository"); disabled = true; return projectModel; } logger.debug("gitDirectory: " + gitDirectory.toString()); gitVersionDetails = getGitVersionDetails(config, executionRootDirectory); logger.info("Adjusting project models..."); logger.info(""); initialized = true; } return processModel(projectModel); } catch (Exception e) { throw new IOException("Git Versioning Model Processor", e); } } private Model processModel(Model projectModel) throws IOException { if (!isRelatedPom(projectModel.getPomFile())) { logger.debug("skip - unrelated pom location - " + projectModel.getPomFile()); return projectModel; } if (projectModel.getPomFile().getName().equals(GIT_VERSIONING_POM_NAME)) { logger.debug("skip - git versioned pom - " + projectModel.getPomFile()); return projectModel; } GAV projectGav = GAV.of(projectModel); if (projectGav.getVersion() == null) { logger.debug("skip - invalid model - 'version' is missing - " + projectModel.getPomFile()); return projectModel; } if(sessionProjectDirectories.isEmpty()){ sessionProjectDirectories.add(projectModel.getProjectDirectory().getCanonicalPath()); } String projectId = projectGav.getProjectId(); Model virtualProjectModel = this.virtualProjectModelCache.get(projectId); if (virtualProjectModel == null) { logger.info(buffer().strong("--- ") + buffer().project(projectId).toString() + " @ " + gitVersionDetails.getCommitRefType() + " " + buffer().strong(gitVersionDetails.getCommitRefName()) + buffer().strong(" ---")); virtualProjectModel = projectModel.clone(); // ---------------- process parent ----------------------------------- final Parent parent = projectModel.getParent(); if (parent != null) { if (parent.getVersion() == null) { logger.warn("skip - invalid model - parent 'version' is missing - " + projectModel.getPomFile()); return projectModel; } Model parentModel = getParentModel(projectModel); if (parentModel != null && isRelatedPom(parentModel.getPomFile())) { if (virtualProjectModel.getVersion() != null) { virtualProjectModel.setVersion(null); logger.warn("Do not set version tag in a multi module project module: " + projectModel.getPomFile()); if (!projectModel.getVersion().equals(parent.getVersion())) { throw new IllegalStateException("'version' has to be equal to parent 'version'"); } } final String parentVersion = virtualProjectModel.getParent().getVersion(); final String gitParentVersion = gitVersionDetails.getVersionTransformer().apply(parentVersion); logger.info("parent version: " + gitParentVersion); virtualProjectModel.getParent().setVersion(gitParentVersion); } } // ---------------- process project ----------------------------------- if (virtualProjectModel.getVersion() != null) { final String projectVersion = virtualProjectModel.getVersion(); final String gitProjectVersion = gitVersionDetails.getVersionTransformer().apply(projectVersion); logger.info("project version: " + buffer().strong(gitProjectVersion)); virtualProjectModel.setVersion(gitProjectVersion); } final Map<String, String> gitProperties = gitVersionDetails.getPropertiesTransformer().apply( Maps.fromProperties(virtualProjectModel.getProperties()), projectGav.getVersion()); if (!gitProperties.isEmpty()) { logger.info("properties:"); for (Entry<String, String> property : gitProperties.entrySet()) { if (!property.getValue().equals(virtualProjectModel.getProperties().getProperty(property.getKey()))) { logger.info(" " + property.getKey() + ": " + property.getValue()); virtualProjectModel.getProperties().setProperty(property.getKey(), property.getValue()); } } } // TODO // update version within dependencies, dependency management, plugins, plugin management logger.info(""); virtualProjectModel.addProperty("git.commit", gitVersionDetails.getCommit()); virtualProjectModel.addProperty("git.commit.timestamp", Long.toString(gitVersionDetails.getCommitTimestamp())); virtualProjectModel.addProperty("git.commit.timestamp.datetime", toTimestampDateTime(gitVersionDetails.getCommitTimestamp())); virtualProjectModel.addProperty("git.ref", gitVersionDetails.getCommitRefName()); virtualProjectModel.addProperty("git.ref.slug", gitVersionDetails.getCommitRefName().toLowerCase().replaceAll("/","-")); virtualProjectModel.addProperty("git." + gitVersionDetails.getCommitRefType(), gitVersionDetails.getCommitRefName()); virtualProjectModel.addProperty("git.dirty", Boolean.toString(!gitVersionDetails.isClean())); // ---------------- add plugin --------------------------------------- boolean isProjectPom = sessionProjectDirectories.contains(virtualProjectModel.getProjectDirectory().getCanonicalPath()); if (isProjectPom) { boolean updatePomOption = getUpdatePomOption(config, gitVersionDetails); addBuildPlugin(virtualProjectModel, updatePomOption); // ---------------- add all sub projects to session ----------------- for (String module : projectModel.getModules()) { sessionProjectDirectories.add(new File(projectModel.getProjectDirectory(), module).getCanonicalPath()); } for (Profile profile : projectModel.getProfiles()) { for (String module : profile.getModules()) { sessionProjectDirectories.add(new File(projectModel.getProjectDirectory(), module).getCanonicalPath()); } } } this.virtualProjectModelCache.put(projectId, virtualProjectModel); } return virtualProjectModel; } private GitVersionDetails getGitVersionDetails(Configuration config, File repositoryDirectory) { GitRepoSituation repoSituation = GitUtil.situation(repositoryDirectory); String providedTag = getCommandOption(OPTION_NAME_GIT_TAG); if (providedTag != null) { repoSituation.setHeadBranch(null); repoSituation.setHeadTags(providedTag.isEmpty() ? emptyList() : singletonList(providedTag)); } String providedBranch = getCommandOption(OPTION_NAME_GIT_BRANCH); if (providedBranch != null) { repoSituation.setHeadBranch(providedBranch.isEmpty() ? null : providedBranch); } final boolean preferTagsOption = getPreferTagsOption(config); return GitVersioning.determineVersion(repoSituation, ofNullable(config.commit) .map(it -> new VersionDescription(null, it.versionFormat, convertPropertyDescription(it.property))) .orElse(new VersionDescription()), config.branch.stream() .map(it -> new VersionDescription(it.pattern, it.versionFormat, convertPropertyDescription(it.property))) .collect(toList()), config.tag.stream() .map(it -> new VersionDescription(it.pattern, it.versionFormat, convertPropertyDescription(it.property))) .collect(toList()), preferTagsOption); } private List<PropertyDescription> convertPropertyDescription( List<Configuration.PropertyDescription> confPropertyDescription) { return confPropertyDescription.stream() .map(prop -> new PropertyDescription( prop.pattern, new PropertyValueDescription(prop.valuePattern, prop.valueFormat))) .collect(toList()); } private Model getParentModel(Model projectModel) { if (projectModel.getParent() == null) { return null; } File parentPomPath = new File(projectModel.getProjectDirectory(), projectModel.getParent().getRelativePath()); final File parentPom; if (parentPomPath.isDirectory()) { parentPom = new File(parentPomPath, "pom.xml"); } else { parentPom = parentPomPath; } if (!parentPom.exists()) { return null; } Model parentModel = unchecked(() -> readModel(parentPom)); GAV parentModelGav = GAV.of(parentModel); GAV parentGav = GAV.of(projectModel.getParent()); if (!parentModelGav.equals(parentGav)) { return null; } return parentModel; } private static File findMvnDirectory(File baseDirectory) throws IOException { File searchDirectory = baseDirectory; while (searchDirectory != null) { File mvnDir = new File(searchDirectory, ".mvn"); if (mvnDir.exists()) { return mvnDir; } searchDirectory = searchDirectory.getParentFile(); } throw new FileNotFoundException("Can not find .mvn directory in hierarchy of " + baseDirectory); } private void addBuildPlugin(Model model, boolean updatePomOption) { logger.debug(model.getArtifactId() + " temporary add build plugin"); Plugin plugin = asPlugin(); PluginExecution execution = new PluginExecution(); execution.setId(GOAL); execution.getGoals().add(GOAL); plugin.getExecutions().add(execution); if (model.getBuild() == null) { model.setBuild(new Build()); } model.getBuild().getPlugins().add(plugin); // set plugin properties model.getProperties().setProperty(propertyKeyPrefix + propertyKeyUpdatePom, Boolean.toString(updatePomOption)); } /** * checks if <code>pomFile</code> is part of a project * * @param pomFile the pom file * @return true if <code>pomFile</code> is part of a project */ private boolean isRelatedPom(File pomFile) throws IOException { return pomFile != null && pomFile.exists() && pomFile.isFile() // only project pom files ends in .xml, pom files from dependencies from repositories ends in .pom && pomFile.getName().endsWith(".xml") && pomFile.getCanonicalPath().startsWith(mvnDirectory.getParentFile().getCanonicalPath() + File.separator) // only pom files within git directory are treated as project pom files && pomFile.getCanonicalPath().startsWith(gitDirectory.getParentFile().getCanonicalPath() + File.separator); } private static File findGitDir(File baseDirectory) { return new FileRepositoryBuilder().findGitDir(baseDirectory).getGitDir(); } private String getCommandOption(final String name) { String value = mavenSession.getUserProperties().getProperty(name); if (value == null) { String plainName = name.replaceFirst("^versioning\\.", ""); String environmentVariableName = "VERSIONING_" + String.join("_", plainName.split("(?=\\p{Lu})")) .replaceAll("\\.", "_") .toUpperCase(); value = System.getenv(environmentVariableName); } if (value == null) { value = System.getProperty(name); } return value; } private Configuration loadConfig(File configFile) throws IOException { logger.debug("load config from " + configFile); return unchecked(() -> new XmlMapper().readValue(configFile, Configuration.class)); } private boolean getPreferTagsOption(final Configuration config) { final boolean preferTagsOption; final String preferTagsCommandOption = getCommandOption(OPTION_PREFER_TAGS); if (preferTagsCommandOption != null) { preferTagsOption = parseBoolean(preferTagsCommandOption); } else if (config.preferTags != null) { preferTagsOption = config.preferTags; } else { preferTagsOption = false; } return preferTagsOption; } private boolean getUpdatePomOption(final Configuration config, final GitVersionDetails gitVersionDetails) { String updatePomCommandOption = getCommandOption(OPTION_UPDATE_POM); if (updatePomCommandOption != null) { return parseBoolean(updatePomCommandOption); } boolean updatePomOption = config.updatePom != null && config.updatePom; if (gitVersionDetails.getCommitRefType().equals("tag")) { updatePomOption = config.tag.stream() .filter(it -> Pattern.matches(it.pattern, gitVersionDetails.getCommitRefName())) .findFirst() .map(it -> it.updatePom) .orElse(updatePomOption); } else if (gitVersionDetails.getCommitRefType().equals("branch")) { updatePomOption = config.branch.stream() .filter(it -> Pattern.matches(it.pattern, gitVersionDetails.getCommitRefName())) .findFirst() .map(it -> it.updatePom) .orElse(updatePomOption); } else if (config.commit != null) { updatePomOption = Optional.ofNullable(config.commit.updatePom) .orElse(updatePomOption); } return updatePomOption; } private String extensionLogFormat(String extensionId) { int extensionIdPadding = 72 - 2 - extensionId.length(); int extensionIdPaddingLeft = (int) ceil(extensionIdPadding / 2.0); int extensionIdPaddingRight = (int) floor(extensionIdPadding / 2.0); return buffer().strong(repeat("-", extensionIdPaddingLeft)) + " " + buffer().mojo(extensionId) + " " + buffer().strong(repeat("-", extensionIdPaddingRight)); } private static String toTimestampDateTime(long timestamp) { if (timestamp == 0) { return "0000-00-00T00:00:00Z"; } return DateTimeFormatter.ISO_DATE_TIME .withZone(ZoneOffset.UTC) .format(Instant.ofEpochSecond(timestamp)); } }