All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amashchenko.maven.plugin.gitflow.AbstractGitFlowMojo Maven / Gradle / Ivy

Go to download

The Git-Flow Maven Plugin supports various Git workflows, including Vincent Driessen's successful Git branching model and GitHub Flow. This plugin runs Git and Maven commands from the command line. Supports Eclipse Plugins build with Tycho.

The newest version!
/*
 * Copyright 2014-2023 Aleksandr Mashchenko.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.amashchenko.maven.plugin.gitflow;

import java.io.File;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Pattern;

import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingResult;
import org.apache.maven.settings.Settings;
import org.apache.maven.shared.release.policy.version.VersionPolicy;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.Os;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;

import com.amashchenko.maven.plugin.gitflow.prompter.GitFlowPrompter;

/**
 * Abstract git flow mojo.
 * 
 */
public abstract class AbstractGitFlowMojo extends AbstractMojo {
    /** Group and artifact id of the versions-maven-plugin. */
    private static final String VERSIONS_MAVEN_PLUGIN = "org.codehaus.mojo:versions-maven-plugin";
    /** The versions-maven-plugin set goal. */
    private static final String VERSIONS_MAVEN_PLUGIN_SET_GOAL = "set";
    /** The versions-maven-plugin set-property goal. */
    private static final String VERSIONS_MAVEN_PLUGIN_SET_PROPERTY_GOAL = "set-property";

    /** Group and artifact id of the tycho-versions-plugin. */
    private static final String TYCHO_VERSIONS_PLUGIN = "org.eclipse.tycho:tycho-versions-plugin";
    /** The tycho-versions-plugin set-version goal. */
    private static final String TYCHO_VERSIONS_PLUGIN_SET_GOAL = "set-version";

    /** Name of the property needed to have reproducible builds. */
    private static final String REPRODUCIBLE_BUILDS_PROPERTY = "project.build.outputTimestamp";

    /** System line separator. */
    protected static final String LS = System.getProperty("line.separator");

    /** Success exit code. */
    private static final int SUCCESS_EXIT_CODE = 0;

    /** Pattern of disallowed characters in Maven commands. */
    private static final Pattern MAVEN_DISALLOWED_PATTERN = Pattern.compile("[&|;]");

    /** Command line for Git executable. */
    private final Commandline cmdGit = new Commandline();
    /** Command line for Maven executable. */
    private final Commandline cmdMvn = new Commandline();

    /** Whether .gitmodules file exists in project. */
    private final boolean gitModulesExists;

    /** Git flow configuration. */
    @Parameter(defaultValue = "${gitFlowConfig}")
    protected GitFlowConfig gitFlowConfig;

    /**
     * Git commit messages.
     * 
     * @since 1.2.1
     */
    @Parameter(defaultValue = "${commitMessages}")
    protected CommitMessages commitMessages;

    /**
     * Whether this is Tycho build.
     * 
     * @since 1.1.0
     */
    @Parameter(defaultValue = "false")
    protected boolean tychoBuild;
    
    /**
     * Whether to call Maven install goal during the mojo execution.
     * 
     * @since 1.0.5
     */
    @Parameter(property = "installProject", defaultValue = "false")
    protected boolean installProject = false;

    /**
     * Whether to fetch remote branch and compare it with the local one.
     * 
     * @since 1.3.0
     */
    @Parameter(property = "fetchRemote", defaultValue = "true")
    protected boolean fetchRemote;

    /**
     * Whether to print commands output into the console.
     * 
     * @since 1.0.7
     */
    @Parameter(property = "verbose", defaultValue = "false")
    private boolean verbose = false;

    /**
     * Command line arguments to pass to the underlying Maven commands.
     * 
     * @since 1.8.0
     */
    @Parameter(property = "argLine")
    private String argLine;

    /**
     * Whether to make a GPG-signed commit.
     * 
     * @since 1.9.0
     */
    @Parameter(property = "gpgSignCommit", defaultValue = "false")
    private boolean gpgSignCommit = false;

    /**
     * Whether to set -DgroupId='*' -DartifactId='*' when calling
     * versions-maven-plugin.
     * 
     * @since 1.10.0
     */
    @Parameter(property = "versionsForceUpdate", defaultValue = "false")
    private boolean versionsForceUpdate = false;

    /**
     * Property to set version to.
     *
     * @since 1.13.0
     */
    @Parameter(property = "versionProperty")
    private String versionProperty;

    /**
     * Whether to skip updating version. Useful with {@link #versionProperty} to be
     * able to update revision property without modifying version tag.
     * 
     * @since 1.13.0
     */
    @Parameter(property = "skipUpdateVersion")
    private boolean skipUpdateVersion = false;

    /**
     * Prefix that is applied to commit messages.
     * 
     * @since 1.14.0
     */
    @Parameter(property = "commitMessagePrefix")
    private String commitMessagePrefix;

    /**
     * Whether to update the project.build.outputTimestamp property
     * automatically or not.
     *
     * @since 1.17.0
     */
    @Parameter(property = "updateOutputTimestamp", defaultValue = "true")
    private boolean updateOutputTimestamp = true;

    /**
     * The role-hint for the
     * {@link org.apache.maven.shared.release.policy.version.VersionPolicy}
     * implementation used to calculate the project versions. If a policy is set
     * other parameters controlling the generation of version are ignored
     * (digitsOnlyDevVersion, versionDigitToIncrement).
     *
     * @since 1.18.0
     */
    @Parameter(property = "projectVersionPolicyId")
    private String projectVersionPolicyId;

    /**
     * Version of versions-maven-plugin to use.
     * 
     * @since 1.18.0
     */
    @Parameter(property = "versionsMavenPluginVersion", defaultValue = "2.16.0")
    private String versionsMavenPluginVersion = "2.16.0";

    /**
     * Version of tycho-versions-plugin to use.
     * 
     * @since 1.18.0
     */
    @Parameter(property = "tychoVersionsPluginVersion", defaultValue = "1.7.0")
    private String tychoVersionsPluginVersion = "1.7.0";

    /**
     * Options to pass to Git push command using --push-option.
     * Multiple options can be added separated with a space e.g.
     * -DgitPushOptions="merge_request.create merge_request.target=develop
     * merge_request.label='Super feature'"
     * 
     * @since 1.18.0
     */
    @Parameter(property = "gitPushOptions")
    private String gitPushOptions;

    /**
     * Explicitly enable or disable executing Git submodule update before commit. By
     * default plugin tries to automatically determine if update of the Git
     * submodules is needed.
     * 
     * @since 1.19.0
     */
    @Parameter(property = "updateGitSubmodules")
    private Boolean updateGitSubmodules;

    /**
     * The path to the Maven executable. Defaults to "mvn".
     */
    @Parameter(property = "mvnExecutable")
    private String mvnExecutable;
    /**
     * The path to the Git executable. Defaults to "git".
     */
    @Parameter(property = "gitExecutable")
    private String gitExecutable;

    /** Maven session. */
    @Parameter(defaultValue = "${session}", readonly = true)
    protected MavenSession mavenSession;

    @Component
    protected ProjectBuilder projectBuilder;
    
    /** Default prompter. */
    @Component
    protected GitFlowPrompter prompter;
    /** Maven settings. */
    @Parameter(defaultValue = "${settings}", readonly = true)
    protected Settings settings;

    @Component
    protected Map versionPolicies;

    public AbstractGitFlowMojo() {
        gitModulesExists = FileUtils.fileExists(".gitmodules");
    }

    /**
     * Initializes command line executables.
     * 
     */
    private void initExecutables() {
        if (StringUtils.isBlank(cmdMvn.getExecutable())) {
            if (StringUtils.isBlank(mvnExecutable)) {
                final String javaCommand = mavenSession.getSystemProperties().getProperty("sun.java.command", "");
                final boolean wrapper = javaCommand.startsWith("org.apache.maven.wrapper.MavenWrapperMain");

                if (wrapper) {
                    mvnExecutable = "." + File.separator + "mvnw";
                } else {
                    mvnExecutable = "mvn";
                }
            }
            cmdMvn.setExecutable(mvnExecutable);
        }
        if (StringUtils.isBlank(cmdGit.getExecutable())) {
            if (StringUtils.isBlank(gitExecutable)) {
                gitExecutable = "git";
            }
            cmdGit.setExecutable(gitExecutable);
        }
    }

    /**
     * Validates plugin configuration. Throws exception if configuration is not
     * valid.
     * 
     * @param params
     *            Configuration parameters to validate.
     * @throws MojoFailureException
     *             If configuration is not valid.
     */
    protected void validateConfiguration(String... params) throws MojoFailureException {
        if (StringUtils.isNotBlank(argLine) && MAVEN_DISALLOWED_PATTERN.matcher(argLine).find()) {
            throw new MojoFailureException("The argLine doesn't match allowed pattern.");
        }
        if (params != null && params.length > 0) {
            for (String p : params) {
                if (StringUtils.isNotBlank(p) && MAVEN_DISALLOWED_PATTERN.matcher(p).find()) {
                    throw new MojoFailureException("The '" + p + "' value doesn't match allowed pattern.");
                }
            }
        }
    }

    /**
     * Gets current project version from pom.xml file.
     * 
     * @return Current project version.
     * @throws MojoFailureException
     *          If current project version cannot be obtained.
     */
    protected String getCurrentProjectVersion() throws MojoFailureException {
        final MavenProject reloadedProject = reloadProject(mavenSession.getCurrentProject());
        if (reloadedProject.getVersion() == null) {
            throw new MojoFailureException(
                    "Cannot get current project version. This plugin should be executed from the parent project.");
        }
        return reloadedProject.getVersion();
    }

    /**
     * Gets current project {@link #REPRODUCIBLE_BUILDS_PROPERTY} property value
     * from pom.xml file.
     * 
     * @return Value of {@link #REPRODUCIBLE_BUILDS_PROPERTY} property.
     * @throws MojoFailureException
     *             If project loading fails.
     */
    private String getCurrentProjectOutputTimestamp() throws MojoFailureException {
        final MavenProject reloadedProject = reloadProject(mavenSession.getCurrentProject());
        return reloadedProject.getProperties().getProperty(REPRODUCIBLE_BUILDS_PROPERTY);
    }

    /**
     * Reloads projects info from file.
     * 
     * @param project
     * @return Reloaded Maven projects.
     * @throws MojoFailureException
     *             If project loading fails.
     */
    private List reloadProjects(final MavenProject project) throws MojoFailureException {
        try {
            List result = projectBuilder.build(
                    Collections.singletonList(project.getFile()),
                    true,
                    mavenSession.getProjectBuildingRequest());

            List projects = new ArrayList<>();
            for (ProjectBuildingResult projectBuildingResult : result) {
                projects.add(projectBuildingResult.getProject());
            }
            return projects;
        } catch (Exception e) {
            throw new MojoFailureException("Error re-loading project info", e);
        }
    }

    /**
     * Reloads project info from file.
     * 
     * @param project
     * @return Maven project which is the execution root.
     * @throws MojoFailureException
     *             If project loading fails.
     */
    private MavenProject reloadProject(final MavenProject project) throws MojoFailureException {
        List projects = reloadProjects(project);
        for (MavenProject resultProject : projects) {
            if (resultProject.isExecutionRoot()) {
                return resultProject;
            }
        }
        throw new NoSuchElementException(
                "No reloaded project appears to be the execution root (" + project.getGroupId() + ":" + project.getArtifactId() + ")");
    }

    /**
     * Compares the production branch name with the development branch name.
     * 
     * @return true if the production branch name is different from
     *         the development branch name, false otherwise.
     */
    protected boolean notSameProdDevName() {
        return !gitFlowConfig.getProductionBranch().equals(gitFlowConfig.getDevelopmentBranch());
    }

    /**
     * Checks uncommitted changes.
     * 
     * @throws MojoFailureException
     *             If there is some uncommitted files.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void checkUncommittedChanges() throws MojoFailureException, CommandLineException {
        getLog().info("Checking for uncommitted changes.");
        if (executeGitHasUncommitted()) {
            throw new MojoFailureException("You have some uncommitted files. Commit or discard local changes in order to proceed.");
        }
    }

    protected void checkSnapshotDependencies() throws MojoFailureException {
        getLog().info("Checking for SNAPSHOT versions in dependencies.");

        List snapshots = new ArrayList<>();
        Set builtArtifacts = new HashSet<>();

        List projects = reloadProjects(mavenSession.getCurrentProject());
        for (MavenProject project : projects) {
            builtArtifacts.add(project.getGroupId() + ":" + project.getArtifactId() + ":" + project.getVersion());
        }

        for (MavenProject project : projects) {
            List dependencies = project.getDependencies();
            for (Dependency d : dependencies) {
                String id = d.getGroupId() + ":" + d.getArtifactId() + ":" + d.getVersion();
                if (!builtArtifacts.contains(id) && ArtifactUtils.isSnapshot(d.getVersion())) {
                    snapshots.add(project + " -> " + d);
                }
            }
            MavenProject parent = project.getParent();
            if (parent != null) {
                String id = parent.getGroupId() + ":" + parent.getArtifactId() + ":" + parent.getVersion();
                if (!builtArtifacts.contains(id) && ArtifactUtils.isSnapshot(parent.getVersion())) {
                    snapshots.add(project + " -> " + parent);
                }
            }
        }

        if (!snapshots.isEmpty()) {
            for (String s : snapshots) {
                getLog().warn(s);
            }
            throw new MojoFailureException(
                    "There is some SNAPSHOT dependencies in the project, see warnings above."
                    + " Change them or ignore with `allowSnapshots` property.");
        }
    }

    /**
     * Checks if branch name is acceptable.
     * 
     * @param branchName
     *            Branch name to check.
     * @return true when name is valid, false otherwise.
     * @throws MojoFailureException
     *             Shouldn't happen, actually.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected boolean validBranchName(final String branchName) throws MojoFailureException, CommandLineException {
        CommandResult res = executeGitCommandExitCode("check-ref-format", "--allow-onelevel", branchName);
        return res.getExitCode() == SUCCESS_EXIT_CODE;
    }

    /**
     * Checks if version is valid.
     * 
     * @param version
     *            Version to validate.
     * @return true when version is valid, false
     *         otherwise.
     * @throws MojoFailureException
     *             Shouldn't happen, actually.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected boolean validVersion(final String version) throws MojoFailureException, CommandLineException {
        boolean valid = "".equals(version) || (GitFlowVersionInfo.isValidVersion(version) && validBranchName(version));
        if (!valid) {
            getLog().info("The version is not valid.");
        }
        return valid;
    }

    /**
     * Executes git commands to check for uncommitted changes.
     * 
     * @return true when there are uncommitted changes,
     *         false otherwise.
     * @throws CommandLineException
     *             If command line execution fails.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     */
    private boolean executeGitHasUncommitted() throws MojoFailureException, CommandLineException {
        boolean uncommited = false;

        // 1 if there were differences and 0 means no differences

        // git diff --no-ext-diff --ignore-submodules --quiet --exit-code
        final CommandResult diffCommandResult = executeGitCommandExitCode(
                "diff", "--no-ext-diff", "--ignore-submodules", "--quiet", "--exit-code");

        String error = null;

        if (diffCommandResult.getExitCode() == SUCCESS_EXIT_CODE) {
            // git diff-index --cached --quiet --ignore-submodules HEAD --
            final CommandResult diffIndexCommandResult = executeGitCommandExitCode(
                    "diff-index", "--cached", "--quiet", "--ignore-submodules", "HEAD", "--");
            if (diffIndexCommandResult.getExitCode() != SUCCESS_EXIT_CODE) {
                error = diffIndexCommandResult.getError();
                uncommited = true;
            }
        } else {
            error = diffCommandResult.getError();
            uncommited = true;
        }

        if (StringUtils.isNotBlank(error)) {
            throw new MojoFailureException(error);
        }

        return uncommited;
    }

    /**
     * Executes git config commands to set Git Flow configuration.
     * 
     * @throws MojoFailureException
     *             Shouldn't happen, actually.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void initGitFlowConfig() throws MojoFailureException, CommandLineException {
        gitSetConfig("gitflow.branch.master", gitFlowConfig.getProductionBranch());
        gitSetConfig("gitflow.branch.develop", gitFlowConfig.getDevelopmentBranch());

        gitSetConfig("gitflow.prefix.feature", gitFlowConfig.getFeatureBranchPrefix());
        gitSetConfig("gitflow.prefix.release", gitFlowConfig.getReleaseBranchPrefix());
        gitSetConfig("gitflow.prefix.hotfix", gitFlowConfig.getHotfixBranchPrefix());
        gitSetConfig("gitflow.prefix.support", gitFlowConfig.getSupportBranchPrefix());
        gitSetConfig("gitflow.prefix.versiontag", gitFlowConfig.getVersionTagPrefix());

        gitSetConfig("gitflow.origin", gitFlowConfig.getOrigin());
    }

    /**
     * Executes git config command.
     * 
     * @param name
     *            Option name.
     * @param value
     *            Option value.
     * @throws MojoFailureException
     *             Shouldn't happen, actually.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    private void gitSetConfig(final String name, String value) throws MojoFailureException, CommandLineException {
        if (value == null || value.isEmpty()) {
            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                value = "\"\"";
            }
            else {
                value = "";
            }
        }

        // ignore error exit codes
        executeGitCommandExitCode("config", name, value);
    }

    /**
     * Executes git for-each-ref with refname:short format.
     * 
     * @param branchName
     *            Branch name to find.
     * @param firstMatch
     *            Return first match.
     * @return Branch names which matches refs/heads/{branchName}*.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected String gitFindBranches(final String branchName, final boolean firstMatch) throws MojoFailureException, CommandLineException {
        return gitFindBranches("refs/heads/", branchName, firstMatch);
    }

    /**
     * Executes git for-each-ref with refname:short format.
     * 
     * @param refs
     *            Refs to search.
     * @param branchName
     *            Branch name to find.
     * @param firstMatch
     *            Return first match.
     * @return Branch names which matches {refs}{branchName}*.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    private String gitFindBranches(final String refs, final String branchName, final boolean firstMatch)
            throws MojoFailureException, CommandLineException {
        String wildcard = "*";
        if (branchName.endsWith("/")) {
            wildcard = "**";
        }

        String branches;
        if (firstMatch) {
            branches = executeGitCommandReturn("for-each-ref", "--count=1",
                    "--format=\"%(refname:short)\"", refs + branchName + wildcard);
        } else {
            branches = executeGitCommandReturn("for-each-ref",
                    "--format=\"%(refname:short)\"", refs + branchName + wildcard);
        }

        // on *nix systems return values from git for-each-ref are wrapped in
        // quotes
        // https://github.com/aleksandr-m/gitflow-maven-plugin/issues/3
        branches = removeQuotes(branches);
        branches = StringUtils.strip(branches);

        return branches;
    }

    /**
     * Executes git for-each-ref to get all tags.
     *
     * @return Git tags.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected String gitFindTags() throws MojoFailureException, CommandLineException {
        String tags = executeGitCommandReturn("for-each-ref", "--sort=*authordate", "--format=\"%(refname:short)\"", "refs/tags/");
        // https://github.com/aleksandr-m/gitflow-maven-plugin/issues/3
        tags = removeQuotes(tags);
        return tags;
    }

    /**
     * Executes git for-each-ref to get the last tag.
     *
     * @return Last tag.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected String gitFindLastTag() throws MojoFailureException, CommandLineException {
        String tag = executeGitCommandReturn("for-each-ref", "--sort=-version:refname", "--sort=-taggerdate",
                "--count=1", "--format=\"%(refname:short)\"", "refs/tags/");
        // https://github.com/aleksandr-m/gitflow-maven-plugin/issues/3
        tag = removeQuotes(tag);
        tag = tag.replaceAll("\\r?\\n", "");
        return tag;
    }

    /**
     * Removes double quotes from the string.
     * 
     * @param str
     *            String to remove quotes from.
     * @return String without quotes.
     */
    private String removeQuotes(String str) {
        return StringUtils.replace(str, "\"", "");
    }

    /**
     * Gets the current branch name.
     * 
     * @return Current branch name.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected String gitCurrentBranch() throws MojoFailureException, CommandLineException {
        String name = executeGitCommandReturn("symbolic-ref", "-q", "--short", "HEAD");
        name = StringUtils.strip(name);
        return name;
    }

    /**
     * Checks if local branch with given name exists.
     *
     * @param branchName
     *            Name of the branch to check.
     * @return true if local branch exists, false
     *         otherwise.
     * @throws MojoFailureException
     *             Shouldn't happen, actually.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected boolean gitCheckBranchExists(final String branchName) throws MojoFailureException, CommandLineException {
        CommandResult commandResult = executeGitCommandExitCode("show-ref", "--verify", "--quiet", "refs/heads/" + branchName);
        return commandResult.getExitCode() == SUCCESS_EXIT_CODE;
    }

    /**
     * Checks if local tag with given name exists.
     *
     * @param tagName
     *            Name of the tag to check.
     * @return true if local tag exists, false otherwise.
     * @throws MojoFailureException
     *             Shouldn't happen, actually.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected boolean gitCheckTagExists(final String tagName) throws MojoFailureException, CommandLineException {
        CommandResult commandResult = executeGitCommandExitCode("show-ref", "--verify", "--quiet", "refs/tags/" + tagName);
        return commandResult.getExitCode() == SUCCESS_EXIT_CODE;
    }

    /**
     * Executes git checkout.
     *
     * @param branchName
     *            Branch name to checkout.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitCheckout(final String branchName) throws MojoFailureException, CommandLineException {
        getLog().info("Checking out '" + branchName + "' branch.");

        executeGitCommand("checkout", branchName);
    }

    /**
     * Executes git checkout -b.
     *
     * @param newBranchName
     *            Create branch with this name.
     * @param fromBranchName
     *            Create branch from this branch.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitCreateAndCheckout(final String newBranchName, final String fromBranchName)
            throws MojoFailureException, CommandLineException {
        getLog().info("Creating a new branch '" + newBranchName + "' from '" + fromBranchName + "' and checking it out.");

        executeGitCommand("checkout", "-b", newBranchName, fromBranchName);
    }

    /**
     * Executes git branch.
     *
     * @param newBranchName
     *            Create branch with this name.
     * @param fromBranchName
     *            Create branch from this branch.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitCreateBranch(final String newBranchName, final String fromBranchName)
            throws MojoFailureException, CommandLineException {
        getLog().info("Creating a new branch '" + newBranchName + "' from '" + fromBranchName + "'.");

        executeGitCommand("branch", newBranchName, fromBranchName);
    }

    /**
     * Replaces properties in message.
     * 
     * @param message
     * @param map
     *            Key is a string to replace wrapped in @{...}. Value
     *            is a string to replace with.
     * @return Message with replaced properties.
     */
    private String replaceProperties(String message, Map map) {
        if (map != null) {
            for (Entry entr : map.entrySet()) {
                message = StringUtils.replace(message, "@{" + entr.getKey() + "}", entr.getValue());
            }
        }
        return message;
    }

    /**
     * Executes git commit -a -m.
     * 
     * @param message
     *            Commit message.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitCommit(final String message) throws MojoFailureException, CommandLineException {
        gitCommit(message, null);
    }

    /**
     * Executes git commit -a -m, replacing @{map.key} with
     * map.value.
     * 
     * @param message
     *            Commit message.
     * @param messageProperties
     *            Properties to replace in message.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitCommit(String message, Map messageProperties) throws MojoFailureException, CommandLineException {
        if ((gitModulesExists && updateGitSubmodules == null) || Boolean.TRUE.equals(updateGitSubmodules)) {
            getLog().info("Updating git submodules before commit.");
            executeGitCommand("submodule", "update");
        }

        if (StringUtils.isNotBlank(commitMessagePrefix)) {
            message = commitMessagePrefix + message;
        }

        message = replaceProperties(message, messageProperties);

        if (gpgSignCommit) {
            getLog().info("Committing changes. GPG-signed.");

            executeGitCommand("commit", "-a", "-S", "-m", message);
        } else {
            getLog().info("Committing changes.");

            executeGitCommand("commit", "-a", "-m", message);
        }
    }

    /**
     * Executes git rebase or git merge --ff-only or git merge --no-ff or git merge.
     * 
     * @param branchName
     *            Branch name to merge.
     * @param rebase
     *            Do rebase.
     * @param noff
     *            Merge with --no-ff.
     * @param ffonly
     *            Merge with --ff-only.
     * @param message
     *            Merge commit message.
     * @param messageProperties
     *            Properties to replace in message.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitMerge(final String branchName, boolean rebase, boolean noff, boolean ffonly, String message,
            Map messageProperties)
            throws MojoFailureException, CommandLineException {
        String sign = null;
        if (gpgSignCommit) {
            sign = "-S";
        }
        String msgParam = null;
        String msg = null;
        if (StringUtils.isNotBlank(message)) {
            if (StringUtils.isNotBlank(commitMessagePrefix)) {
                message = commitMessagePrefix + message;
            }

            msgParam = "-m";
            msg = replaceProperties(message, messageProperties);
        }
        if (rebase) {
            getLog().info("Rebasing '" + branchName + "' branch.");
            executeGitCommand("rebase", sign, branchName);
        } else if (ffonly) {
            getLog().info("Merging (--ff-only) '" + branchName + "' branch.");
            executeGitCommand("merge", "--ff-only", sign, branchName);
        } else if (noff) {
            getLog().info("Merging (--no-ff) '" + branchName + "' branch.");
            executeGitCommand("merge", "--no-ff", sign, branchName, msgParam, msg);
        } else {
            getLog().info("Merging '" + branchName + "' branch.");
            executeGitCommand("merge", sign, branchName, msgParam, msg);
        }
    }

    /**
     * Executes git merge --no-ff.
     * 
     * @param branchName
     *            Branch name to merge.
     * @param message
     *            Merge commit message.
     * @param messageProperties
     *            Properties to replace in message.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitMergeNoff(final String branchName, final String message, final Map messageProperties)
            throws MojoFailureException, CommandLineException {
        gitMerge(branchName, false, true, false, message, messageProperties);
    }

    /**
     * Executes git merge --squash.
     * 
     * @param branchName
     *            Branch name to merge.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitMergeSquash(final String branchName) throws MojoFailureException, CommandLineException {
        getLog().info("Squashing '" + branchName + "' branch.");
        executeGitCommand("merge", "--squash", branchName);
    }

    /**
     * Executes git tag -a [-s] -m.
     * 
     * @param tagName
     *            Name of the tag.
     * @param message
     *            Tag message.
     * @param gpgSignTag
     *            Make a GPG-signed tag.
     * @param messageProperties
     *            Properties to replace in message.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitTag(final String tagName, String message, boolean gpgSignTag, Map messageProperties)
            throws MojoFailureException, CommandLineException {
        message = replaceProperties(message, messageProperties);

        if (gpgSignTag) {
            getLog().info("Creating GPG-signed '" + tagName + "' tag.");

            executeGitCommand("tag", "-a", "-s", tagName, "-m", message);
        } else {
            getLog().info("Creating '" + tagName + "' tag.");

            executeGitCommand("tag", "-a", tagName, "-m", message);
        }
    }

    /**
     * Executes git branch -d.
     * 
     * @param branchName
     *            Branch name to delete.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitBranchDelete(final String branchName) throws MojoFailureException, CommandLineException {
        getLog().info("Deleting '" + branchName + "' branch.");

        executeGitCommand("branch", "-d", branchName);
    }

    /**
     * Executes git branch -D.
     * 
     * @param branchName
     *            Branch name to delete.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitBranchDeleteForce(final String branchName) throws MojoFailureException, CommandLineException {
        getLog().info("Deleting (-D) '" + branchName + "' branch.");

        executeGitCommand("branch", "-D", branchName);
    }

    /**
     * Executes git fetch and checks if local branch exists. If local branch is
     * present then compares it with the remote, if not then branch is checked out.
     * 
     * @param branchName
     *            Branch name to check.
     * @throws MojoFailureException
     *             If command line execution returns false code or remote branch is
     *             ahead of the local branch.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitFetchRemoteAndCompareCreate(final String branchName) throws MojoFailureException, CommandLineException {
        final boolean remoteBranchExists = StringUtils.isNotBlank(gitFetchAndFindRemoteBranches(branchName, true));
        if (gitCheckBranchExists(branchName)) {
            if (remoteBranchExists) {
                getLog().info(
                        "Comparing local branch '" + branchName + "' with remote '" + gitFlowConfig.getOrigin() + "/" + branchName + "'.");
                String revlistout = executeGitCommandReturn("rev-list", "--left-right", "--count",
                        branchName + "..." + gitFlowConfig.getOrigin() + "/" + branchName);

                String[] counts = org.apache.commons.lang3.StringUtils.split(revlistout, '\t');
                if (counts != null && counts.length > 1 && !"0".equals(org.apache.commons.lang3.StringUtils.deleteWhitespace(counts[1]))) {
                    throw new MojoFailureException("Remote branch '" + gitFlowConfig.getOrigin() + "/" + branchName
                            + "' is ahead of the local branch '" + branchName + "'. Execute git pull.");
                }
            }
        } else {
            getLog().info("Local branch '" + branchName + "' doesn't exist. Trying check it out from '" + gitFlowConfig.getOrigin() + "'.");
            gitCreateAndCheckout(branchName, gitFlowConfig.getOrigin() + "/" + branchName);
        }
    }

    /**
     * Executes git fetch and git for-each-ref with refname:short
     * format. Searches refs/remotes/{gitFlowConfig#origin}/.
     * 
     * @param branchName
     *            Branch name to find.
     * @param firstMatch
     *            Return first match.
     * @return Branch names which matches
     *         refs/remotes/{gitFlowConfig#origin}/{branchName}*.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected String gitFetchAndFindRemoteBranches(final String branchName, final boolean firstMatch)
            throws MojoFailureException, CommandLineException {
        gitFetchRemote();
        return gitFindBranches("refs/remotes/" + gitFlowConfig.getOrigin() + "/", branchName, firstMatch);
    }

    /**
     * Executes git fetch.
     * 
     * @return true if git fetch returned success exit code,
     *         false otherwise.
     * @throws MojoFailureException
     *             Shouldn't happen, actually.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    private boolean gitFetchRemote() throws MojoFailureException, CommandLineException {
        getLog().info("Fetching remote from '" + gitFlowConfig.getOrigin() + "'.");

        CommandResult result = executeGitCommandExitCode("fetch", "--quiet", gitFlowConfig.getOrigin());

        boolean success = result.getExitCode() == SUCCESS_EXIT_CODE;
        if (!success) {
            getLog().warn(
                    "There were some problems fetching from '"
                            + gitFlowConfig.getOrigin()
                            + "'. You can turn off remote fetching by setting the 'fetchRemote' parameter to false.");
        }

        return success;
    }

    /**
     * Executes git push, optionally with the --follow-tags argument.
     * 
     * @param branchName
     *            Branch name to push.
     * @param pushTags
     *            If true adds --follow-tags argument to
     *            the git push command.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void gitPush(final String branchName, boolean pushTags) throws MojoFailureException, CommandLineException {
        getLog().info("Pushing '" + branchName + "' branch to '" + gitFlowConfig.getOrigin() + "'.");

        List args = new ArrayList<>();
        args.add("push");
        args.add("--quiet");
        args.add("-u");

        if (pushTags) {
            args.add("--follow-tags");
        }

        if (StringUtils.isNotBlank(gitPushOptions)) {
            try {
                String[] opts = CommandLineUtils.translateCommandline(gitPushOptions);
                for (String opt : opts) {
                    args.add("--push-option=" + opt);
                }
            } catch (Exception e) {
                throw new CommandLineException(e.getMessage(), e);
            }
        }

        args.add(gitFlowConfig.getOrigin());
        args.add(branchName);

        executeGitCommand(args.toArray(new String[0]));
    }

    protected void gitPushDelete(final String branchName) throws MojoFailureException, CommandLineException {
        getLog().info("Deleting remote branch '" + branchName + "' from '" + gitFlowConfig.getOrigin() + "'.");

        CommandResult result = executeGitCommandExitCode("push", "--delete", gitFlowConfig.getOrigin(), branchName);

        if (result.getExitCode() != SUCCESS_EXIT_CODE) {
            getLog().warn(
                    "There were some problems deleting remote branch '"
                            + branchName + "' from '" + gitFlowConfig.getOrigin() + "'.");
        }
    }

    /**
     * Executes 'set' goal of versions-maven-plugin or 'set-version' of
     * tycho-versions-plugin in case it is tycho build.
     * 
     * @param version
     *            New version to set.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void mvnSetVersions(final String version) throws MojoFailureException, CommandLineException {
        getLog().info("Updating version(s) to '" + version + "'.");

        String newVersion = "-DnewVersion=" + version;

        if (tychoBuild) {
            String prop = null;
            if (StringUtils.isNotBlank(versionProperty)) {
                prop = "-Dproperties=" + versionProperty;
                getLog().info("Updating property '" + versionProperty + "' to '" + version + "'.");
            }

            executeMvnCommand(TYCHO_VERSIONS_PLUGIN + ":" + tychoVersionsPluginVersion + ":" + TYCHO_VERSIONS_PLUGIN_SET_GOAL, prop,
                    newVersion, "-Dtycho.mode=maven");
        } else {
            boolean runCommand = false;
            List args = new ArrayList<>();
            args.add("-DgenerateBackupPoms=false");
            args.add(newVersion);
            if (!skipUpdateVersion) {
                runCommand = true;
                args.add(VERSIONS_MAVEN_PLUGIN + ":" + versionsMavenPluginVersion + ":" + VERSIONS_MAVEN_PLUGIN_SET_GOAL);
                if (versionsForceUpdate) {
                    args.add("-DgroupId=");
                    args.add("-DartifactId=");
                }
            }

            if (StringUtils.isNotBlank(versionProperty)) {
                runCommand = true;
                getLog().info("Updating property '" + versionProperty + "' to '" + version + "'.");

                args.add(VERSIONS_MAVEN_PLUGIN + ":" + versionsMavenPluginVersion + ":" + VERSIONS_MAVEN_PLUGIN_SET_PROPERTY_GOAL);
                args.add("-Dproperty=" + versionProperty);
            }
            if (runCommand) {
                executeMvnCommand(args.toArray(new String[0]));

                if (updateOutputTimestamp) {
                    String timestamp = getCurrentProjectOutputTimestamp();
                    if (timestamp != null && timestamp.length() > 1) {
                        if (StringUtils.isNumeric(timestamp)) {
                            // int representing seconds since the epoch
                            timestamp = String.valueOf(System.currentTimeMillis() / 1000l);
                        } else {
                            // ISO-8601
                            DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
                            df.setTimeZone(TimeZone.getTimeZone("UTC"));
                            timestamp = df.format(new Date());
                        }

                        getLog().info("Updating property '" + REPRODUCIBLE_BUILDS_PROPERTY + "' to '" + timestamp + "'.");

                        executeMvnCommand(
                                VERSIONS_MAVEN_PLUGIN + ":" + versionsMavenPluginVersion + ":" + VERSIONS_MAVEN_PLUGIN_SET_PROPERTY_GOAL,
                                "-DgenerateBackupPoms=false",
                                "-Dproperty=" + REPRODUCIBLE_BUILDS_PROPERTY, "-DnewVersion=" + timestamp);
                    }
                }
            }
        }
    }

    /**
     * Executes mvn clean test.
     * 
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void mvnCleanTest() throws MojoFailureException, CommandLineException {
        getLog().info("Cleaning and testing the project.");
        if (tychoBuild) {
            executeMvnCommand("clean", "verify");
        } else {
            executeMvnCommand("clean", "test");
        }
    }

    /**
     * Executes mvn clean install.
     * 
     * @throws MojoFailureException
     *             If command line execution returns false code.
     * @throws CommandLineException
     *             If command line execution fails.
     */
    protected void mvnCleanInstall() throws MojoFailureException, CommandLineException {
        getLog().info("Cleaning and installing the project.");

        executeMvnCommand("clean", "install");
    }

    /**
     * Executes Maven goals.
     * 
     * @param goals
     *            The goals to execute.
     * @throws Exception
     *             If command line parsing or execution fails.
     */
    protected void mvnRun(final String goals) throws Exception {
        getLog().info("Running Maven goals: " + goals);

        executeMvnCommand(CommandLineUtils.translateCommandline(goals));
    }

    /**
     * Executes Git command and returns output.
     * 
     * @param args
     *            Git command line arguments.
     * @return Command output.
     * @throws CommandLineException
     *             If command line execution fails.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     */
    private String executeGitCommandReturn(final String... args)
            throws CommandLineException, MojoFailureException {
        return executeCommand(cmdGit, true, null, args).getOut();
    }

    /**
     * Executes Git command without failing on non successful exit code.
     * 
     * @param args
     *            Git command line arguments.
     * @return Command result.
     * @throws CommandLineException
     *             If command line execution fails.
     * @throws MojoFailureException
     *             Shouldn't happen, actually.
     */
    private CommandResult executeGitCommandExitCode(final String... args)
            throws CommandLineException, MojoFailureException {
        return executeCommand(cmdGit, false, null, args);
    }

    /**
     * Executes Git command.
     * 
     * @param args
     *            Git command line arguments.
     * @throws CommandLineException
     *             If command line execution fails.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     */
    private void executeGitCommand(final String... args)
            throws CommandLineException, MojoFailureException {
        executeCommand(cmdGit, true, null, args);
    }

    /**
     * Executes Maven command.
     * 
     * @param args
     *            Maven command line arguments.
     * @throws CommandLineException
     *             If command line execution fails.
     * @throws MojoFailureException
     *             If command line execution returns false code.
     */
    private void executeMvnCommand(final String... args)
            throws CommandLineException, MojoFailureException {
        executeCommand(cmdMvn, true, argLine, args);
    }

    /**
     * Executes command line.
     * 
     * @param cmd
     *            Command line.
     * @param failOnError
     *            Whether to throw exception on NOT success exit code.
     * @param argStr
     *            Command line arguments as a string.
     * @param args
     *            Command line arguments.
     * @return {@link CommandResult} instance holding command exit code, output and
     *         error if any.
     * @throws CommandLineException
     *             If command line execution fails.
     * @throws MojoFailureException
     *             If failOnError is true and command exit
     *             code is NOT equals to 0.
     */
    private CommandResult executeCommand(final Commandline cmd, final boolean failOnError, final String argStr, final String... args)
            throws CommandLineException, MojoFailureException {
        // initialize executables
        initExecutables();

        if (getLog().isDebugEnabled()) {
            getLog().debug(
                    cmd.getExecutable() + " " + StringUtils.join(args, " ")
                            + (argStr == null ? "" : " " + argStr));
        }

        cmd.clearArgs();
        String[] nonNullArgs = Arrays.stream(args).filter(Objects::nonNull).toArray(String[]::new);
        cmd.addArguments(nonNullArgs);

        if (StringUtils.isNotBlank(argStr)) {
            cmd.createArg().setLine(argStr);
        }

        final StringBufferStreamConsumer out = new StringBufferStreamConsumer(verbose);

        final CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();

        // execute
        final int exitCode = CommandLineUtils.executeCommandLine(cmd, out, err);

        String errorStr = err.getOutput();
        String outStr = out.getOutput();

        if (failOnError && exitCode != SUCCESS_EXIT_CODE) {
            // not all commands print errors to error stream
            errorStr += LS + outStr;

            throw new MojoFailureException("Failed cmd ["+cmd.getExecutable()+"] with args [" + Arrays.toString(cmd.getArguments()) + "], bad exit code [" + exitCode +"]. Out: [" + errorStr+ "]");
        }

        if (verbose && StringUtils.isNotBlank(errorStr)) {
            getLog().warn(errorStr);
        }

        return new CommandResult(exitCode, outStr, errorStr);
    }

    private static class CommandResult {
        private final int exitCode;
        private final String out;
        private final String error;

        private CommandResult(final int exitCode, final String out, final String error) {
            this.exitCode = exitCode;
            this.out = out;
            this.error = error;
        }

        /**
         * @return the exitCode
         */
        public int getExitCode() {
            return exitCode;
        }

        /**
         * @return the out
         */
        public String getOut() {
            return out;
        }

        /**
         * @return the error
         */
        public String getError() {
            return error;
        }
    }

    public void setArgLine(String argLine) {
        this.argLine = argLine;
    }

    protected VersionPolicy getVersionPolicy() {
        if (StringUtils.isNotBlank(projectVersionPolicyId)) {
            VersionPolicy versionPolicy = versionPolicies.get(projectVersionPolicyId);
            if (versionPolicy == null) {
                throw new IllegalArgumentException("No implementation found for projectVersionPolicyId: " + projectVersionPolicyId);
            }
            return versionPolicy;
        }
        return null;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy