org.codehaus.mojo.build.CreateMojo Maven / Gradle / Ivy
package org.codehaus.mojo.build;
/**
* The MIT License
*
* Copyright (c) 2015 Learning Commons, University of Calgary
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.scm.ScmException;
import org.apache.maven.scm.ScmFile;
import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.command.info.InfoItem;
import org.apache.maven.scm.command.info.InfoScmResult;
import org.apache.maven.scm.command.status.StatusScmResult;
import org.apache.maven.scm.command.update.UpdateScmResult;
import org.apache.maven.scm.command.update.UpdateScmResultWithRevision;
import org.apache.maven.scm.log.ScmLogDispatcher;
import org.apache.maven.scm.log.ScmLogger;
import org.apache.maven.scm.manager.ScmManager;
import org.apache.maven.scm.provider.ScmProvider;
import org.apache.maven.scm.provider.git.gitexe.command.branch.GitBranchCommand;
import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
import org.apache.maven.scm.repository.ScmRepository;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
/**
* This mojo is designed to give you a build number. So when you might make 100 builds of version 1.0-SNAPSHOT, you can
* differentiate between them all.
*
* The build number is based on the revision number retrieved from SCM. It is known to work with Subversion, GIT, and
* Mercurial.
*
* This mojo can also check to make sure that you have checked everything into SCM, before issuing the build number.
* That behaviour can be suppressed, and then the latest local build number is used.
*
* Build numbers are not automatically reflected in your artifact's filename, but can be added to the metadata. You can
* access the build number in your pom with ${buildNumber}. You can also access ${timestamp} and the SCM branch of the
* build (if applicable) in ${SCMBranch}
*
* Note that there are several doFoo
parameters. These parameters (doCheck, doUpdate, etc)
* are the first thing evaluated. If there is no matching expression, we get the default-value. If there is (ie
* -Dmaven.buildNumber.doUpdate=false
), we get that value. So if the XML contains
* <doCheck>true</doCheck>, then normally that's the final value of the param in question. However,
* this mojo reverses that behaviour, such that the command line parameters get the last say.
*
* @author Julian Wood
* @version $Id$
*/
@Mojo( name = "create", defaultPhase = LifecyclePhase.INITIALIZE, requiresProject = true, threadSafe = true )
public class CreateMojo
extends AbstractScmMojo
{
private static final String DEFAULT_BRANCH_NAME = "UNKNOWN_BRANCH";
/**
* You can rename the buildNumber property name to another property name if desired.
*
* @since 1.0-beta-1
*/
@Parameter( property = "maven.buildNumber.buildNumberPropertyName", defaultValue = "buildNumber" )
private String buildNumberPropertyName;
/**
* You can rename the timestamp property name to another property name if desired.
*
* @since 1.0-beta-1
*/
@Parameter( property = "maven.buildNumber.timestampPropertyName", defaultValue = "timestamp" )
private String timestampPropertyName;
/**
* If this is made true, we check for modified files, and if there are any, we fail the build. Note that this used
* to be inverted (skipCheck), but needed to be changed to allow releases to work. This corresponds to 'svn status'.
*
* @since 1.0-beta-1
*/
@Parameter( property = "maven.buildNumber.doCheck", defaultValue = "false" )
private boolean doCheck;
/**
* If this is made true, then the revision will be updated to the latest in the repo, otherwise it will remain what
* it is locally. Note that this used to be inverted (skipUpdate), but needed to be changed to allow releases to
* work. This corresponds to 'svn update'.
*
* @since 1.0-beta-1
*/
@Parameter( property = "maven.buildNumber.doUpdate", defaultValue = "false" )
private boolean doUpdate;
/**
* Specify a message as specified by java.text.MessageFormat. This triggers "items" configuration to be read
*
* @since 1.0-beta-1
*/
@Parameter( property = "maven.buildNumber.format" )
private String format;
/**
* Properties file to be created when "format" is not null and item has "buildNumber". See Usage for details
*
* @since 1.0-beta-2
*/
@Parameter( defaultValue = "${basedir}/buildNumber.properties" )
private File buildNumberPropertiesFileLocation;
/**
* Specify the corresponding items for the format message, as specified by java.text.MessageFormat. Special item
* values are "scmVersion", "timestamp" and "buildNumber[digits]", where [digits] are optional digits added to the
* end of the number to select a property.
*
* @since 1.0-beta-1
*/
@Parameter
private List> items;
/**
* The locale used for date and time formatting. The locale name should be in the format defined in
* {@link Locale#toString()}. The default locale is the platform default returned by {@link Locale#getDefault()}.
*
* @since 1.0-beta-2
*/
@Parameter( property = "maven.buildNumber.locale" )
private String locale;
/**
* whether to retrieve the revision for the last commit, or the last revision of the repository.
*
* @since 1.0-beta-2
*/
@Parameter( property = "maven.buildNumber.useLastCommittedRevision", defaultValue = "false" )
private boolean useLastCommittedRevision;
/**
* Apply this java.text.MessageFormat to the timestamp only (as opposed to the format
parameter).
*
* @since 1.0-beta-2
*/
@Parameter( property = "maven.buildNumber.timestampFormat" )
private String timestampFormat;
/**
* Selects alternative SCM provider implementations. Each map key denotes the original provider type as given in the
* SCM URL like "cvs" or "svn", the map value specifies the provider type of the desired implementation to use
* instead. In other words, this map configures a substitition mapping for SCM providers.
*
* @since 1.0-beta-3
*/
@Parameter
private Map providerImplementations;
/**
* If set to true, will get the scm revision once for all modules of a multi-module project instead of fetching once
* for each module.
*
* @since 1.0-beta-3
*/
@Parameter( property = "maven.buildNumber.getRevisionOnlyOnce", defaultValue = "false" )
private boolean getRevisionOnlyOnce;
/**
* You can rename the buildScmBranch property name to another property name if desired.
*
* @since 1.0-beta-4
*/
@Parameter( property = "maven.buildNumber.scmBranchPropertyName", defaultValue = "scmBranch" )
private String scmBranchPropertyName;
// ////////////////////////////////////// internal maven components ///////////////////////////////////
/**
* Contains the full list of projects in the reactor.
*
* @since 1.0-beta-3
*/
@Parameter( defaultValue = "${reactorProjects}", readonly = true, required = true )
private List reactorProjects;
@Parameter( defaultValue = "${session}", readonly = true, required = true )
private MavenSession session;
// ////////////////////////////////////// internal variables ///////////////////////////////////
private ScmLogDispatcher logger;
private String revision;
private boolean useScm;
public void execute()
throws MojoExecutionException, MojoFailureException
{
if ( skip )
{
getLog().info( "Skipping execution." );
return;
}
if ( providerImplementations != null )
{
for ( Entry entry : providerImplementations.entrySet() )
{
String providerType = entry.getKey();
String providerImplementation = entry.getValue();
getLog().info( "Change the default '" + providerType + "' provider implementation to '"
+ providerImplementation + "'." );
scmManager.setScmProviderImplementation( providerType, providerImplementation );
}
}
Date now = Calendar.getInstance().getTime();
if ( format != null )
{
if ( items == null )
{
throw new MojoExecutionException(
" if you set a format, you must provide at least one item, please check documentation " );
}
// needs to be an array
// look for special values
Object[] itemAry = new Object[items.size()];
for ( int i = 0; i < items.size(); i++ )
{
Object item = items.get( i );
if ( item instanceof String )
{
String s = (String) item;
if ( s.equals( "timestamp" ) )
{
itemAry[i] = now;
}
else if ( s.startsWith( "scmVersion" ) )
{
useScm = true;
itemAry[i] = getRevision();
}
else if ( s.startsWith( "buildNumber" ) )
{
// check for properties file
File propertiesFile = this.buildNumberPropertiesFileLocation;
// create if not exists
if ( !propertiesFile.exists() )
{
try
{
if ( !propertiesFile.getParentFile().exists() )
{
propertiesFile.getParentFile().mkdirs();
}
propertiesFile.createNewFile();
}
catch ( IOException e )
{
throw new MojoExecutionException( "Couldn't create properties file: " + propertiesFile,
e );
}
}
Properties properties = new Properties();
String buildNumberString = null;
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try
{
// get the number for the buildNumber specified
inputStream = new FileInputStream( propertiesFile );
properties.load( inputStream );
buildNumberString = properties.getProperty( s );
if ( buildNumberString == null )
{
buildNumberString = "0";
}
int buildNumber = Integer.valueOf( buildNumberString ).intValue();
// store the increment
properties.setProperty( s, String.valueOf( ++buildNumber ) );
outputStream = new FileOutputStream( propertiesFile );
properties.store( outputStream, "maven.buildNumber.plugin properties file" );
// use in the message (format)
itemAry[i] = new Integer( buildNumber );
}
catch ( NumberFormatException e )
{
throw new MojoExecutionException(
"Couldn't parse buildNumber in properties file to an Integer: "
+ buildNumberString );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Couldn't load properties file: " + propertiesFile, e );
}
finally
{
IOUtil.close( inputStream );
IOUtil.close( outputStream );
}
}
else
{
itemAry[i] = item;
}
}
else
{
itemAry[i] = item;
}
}
revision = format( itemAry );
}
else
{
// Check if the plugin has already run.
revision = project.getProperties().getProperty( this.buildNumberPropertyName );
if ( this.getRevisionOnlyOnce && revision != null )
{
getLog().debug( "Revision available from previous execution" );
return;
}
if ( doCheck )
{
// we fail if there are local mods
checkForLocalModifications();
}
else
{
getLog().debug( "Checking for local modifications: skipped." );
}
if ( session.getSettings().isOffline() )
{
getLog().info( "maven is executed in offline mode, Updating project files from SCM: skipped." );
}
else
{
if ( doUpdate )
{
// we update your local repo
// even after you commit, your revision stays the same until you update, thus this
// action
List changedFiles = update();
for ( ScmFile file : changedFiles )
{
getLog().debug( "Updated: " + file );
}
if ( changedFiles.size() == 0 )
{
getLog().debug( "No files needed updating." );
}
}
else
{
getLog().debug( "Updating project files from SCM: skipped." );
}
}
revision = getRevision();
}
if ( project != null )
{
String timestamp = String.valueOf( now.getTime() );
if ( timestampFormat != null )
{
timestamp = MessageFormat.format( timestampFormat, new Object[] { now } );
}
getLog().info( MessageFormat.format( "Storing buildNumber: {0} at timestamp: {1}", new Object[] { revision,
timestamp } ) );
if ( revision != null )
{
project.getProperties().put( buildNumberPropertyName, revision );
}
project.getProperties().put( timestampPropertyName, timestamp );
String scmBranch = getScmBranch();
getLog().info( "Storing buildScmBranch: " + scmBranch );
project.getProperties().put( scmBranchPropertyName, scmBranch );
// Add the revision and timestamp properties to each project in the reactor
if ( getRevisionOnlyOnce && reactorProjects != null )
{
Iterator projIter = reactorProjects.iterator();
while ( projIter.hasNext() )
{
MavenProject nextProj = (MavenProject) projIter.next();
if ( revision != null )
{
nextProj.getProperties().put( this.buildNumberPropertyName, revision );
}
nextProj.getProperties().put( this.timestampPropertyName, timestamp );
nextProj.getProperties().put( this.scmBranchPropertyName, scmBranch );
}
}
}
}
/**
* Formats the given argument using the configured format template and locale.
*
* @param arguments arguments to be formatted @ @return formatted result
*/
private String format( Object[] arguments )
{
Locale l = Locale.getDefault();
if ( locale != null )
{
String[] parts = locale.split( "_", 3 );
if ( parts.length <= 1 )
{
l = new Locale( locale );
}
else if ( parts.length == 2 )
{
l = new Locale( parts[0], parts[1] );
}
else
{
l = new Locale( parts[0], parts[1], parts[2] );
}
}
return new MessageFormat( format, l ).format( arguments );
}
private void checkForLocalModifications()
throws MojoExecutionException
{
getLog().debug( "Verifying there are no local modifications ..." );
List changedFiles;
try
{
changedFiles = getStatus();
}
catch ( ScmException e )
{
throw new MojoExecutionException( "An error has occurred while checking scm status.", e );
}
if ( !changedFiles.isEmpty() )
{
StringBuilder message = new StringBuilder();
String ls = System.getProperty( "line.separator" );
for ( ScmFile file : changedFiles )
{
message.append( file.toString() );
message.append( ls );
}
throw new MojoExecutionException(
"Cannot create the build number because you have local modifications : \n"
+ message );
}
}
public List update()
throws MojoExecutionException
{
try
{
ScmRepository repository = getScmRepository();
ScmProvider scmProvider = scmManager.getProviderByRepository( repository );
UpdateScmResult result = scmProvider.update( repository, new ScmFileSet( scmDirectory ) );
if ( result == null )
{
return Collections.emptyList();
}
checkResult( result );
if ( result instanceof UpdateScmResultWithRevision )
{
String revision = ( (UpdateScmResultWithRevision) result ).getRevision();
getLog().info( "Got a revision during update: " + revision );
this.revision = revision;
}
return result.getUpdatedFiles();
}
catch ( ScmException e )
{
throw new MojoExecutionException( "Couldn't update project. " + e.getMessage(), e );
}
}
public List getStatus()
throws ScmException
{
ScmRepository repository = getScmRepository();
ScmProvider scmProvider = scmManager.getProviderByRepository( repository );
StatusScmResult result = scmProvider.status( repository, new ScmFileSet( scmDirectory ) );
if ( result == null )
{
return Collections.emptyList();
}
checkResult( result );
return result.getChangedFiles();
}
/**
* Get the branch info for this revision from the repository. For svn, it is in svn info.
*
* @return
* @throws MojoExecutionException
* @throws MojoExecutionException
*/
public String getScmBranch()
throws MojoExecutionException
{
/* git branch can be obtained directly by a command */
try
{
ScmRepository repository = getScmRepository();
ScmProvider provider = scmManager.getProviderByRepository( repository );
if ( GitScmProviderRepository.PROTOCOL_GIT.equals( provider.getScmType() ) )
{
ScmFileSet fileSet = new ScmFileSet( scmDirectory );
return GitBranchCommand.getCurrentBranch( getLogger(),
(GitScmProviderRepository) repository.getProviderRepository(),
fileSet );
}
}
catch ( ScmException e )
{
getLog().warn( "Cannot get the branch information from the git repository: \n" + e.getLocalizedMessage() );
}
return getScmBranchFromUrl();
}
private String getScmBranchFromUrl()
throws MojoExecutionException
{
String scmUrl = null;
try
{
ScmRepository repository = getScmRepository();
InfoScmResult scmResult = info( repository, new ScmFileSet( scmDirectory ) );
if ( scmResult == null || !scmResult.isSuccess() )
{
getLog().debug( "Cannot get the branch information from the scm repository : "
+ ( scmResult == null ? "" : scmResult.getCommandOutput() ) );
return DEFAULT_BRANCH_NAME;
}
if ( scmResult.getInfoItems().isEmpty() )
{
if ( !StringUtils.isEmpty( revisionOnScmFailure ) )
{
setDoCheck( false );
setDoUpdate( false );
return DEFAULT_BRANCH_NAME;
}
}
if ( !scmResult.getInfoItems().isEmpty() )
{
InfoItem info = scmResult.getInfoItems().get( 0 );
scmUrl = info.getURL();
}
}
catch ( ScmException e )
{
if ( !StringUtils.isEmpty( revisionOnScmFailure ) )
{
getLog().warn( "Cannot get the branch information from the scm repository, proceeding with "
+ DEFAULT_BRANCH_NAME + " : \n" + e.getLocalizedMessage() );
setDoCheck( false );
setDoUpdate( false );
return DEFAULT_BRANCH_NAME;
}
throw new MojoExecutionException( "Cannot get the branch information from the scm repository : \n"
+ e.getLocalizedMessage(), e );
}
return filterBranchFromScmUrl( scmUrl );
}
protected String filterBranchFromScmUrl( String scmUrl )
{
String scmBranch = "UNKNOWN";
if ( StringUtils.contains( scmUrl, "/trunk" ) )
{
scmBranch = "trunk";
}
else if ( StringUtils.contains( scmUrl, "/branches" ) || StringUtils.contains( scmUrl, "/tags" ) )
{
scmBranch = scmUrl.replaceFirst( ".*((branches|tags)[^/]*).*?", "$1" );
}
return scmBranch;
}
/**
* Get the revision info from the repository. For svn, it is svn info
*
* @return
* @throws MojoExecutionException
*/
public String getRevision()
throws MojoExecutionException
{
if ( format != null && !useScm )
{
return revision;
}
useScm = false;
try
{
return this.getScmRevision();
}
catch ( ScmException e )
{
if ( !StringUtils.isEmpty( revisionOnScmFailure ) )
{
getLog().warn( "Cannot get the revision information from the scm repository, proceeding with "
+ "revision of " + revisionOnScmFailure + " : \n" + e.getLocalizedMessage() );
setDoCheck( false );
setDoUpdate( false );
return revisionOnScmFailure;
}
throw new MojoExecutionException( "Cannot get the revision information from the scm repository : \n"
+ e.getLocalizedMessage(), e );
}
}
/**
* @return
* @todo normally this would be handled in AbstractScmProvider
*/
private ScmLogger getLogger()
{
if ( logger == null )
{
logger = new ScmLogDispatcher();
}
return logger;
}
// ////////////////////////////////////////////////////////////////////////////////////////////
// setters to help with test
public void setScmManager( ScmManager scmManager )
{
this.scmManager = scmManager;
}
public void setUrlScm( String urlScm )
{
this.scmConnectionUrl = urlScm;
}
public void setUsername( String username )
{
this.username = username;
}
public void setPassword( String password )
{
this.password = password;
}
public void setDoCheck( boolean doCheck )
{
String doCheckSystemProperty = System.getProperty( "maven.buildNumber.doCheck" );
if ( doCheckSystemProperty != null )
{
// well, this gets the final say
this.doCheck = Boolean.valueOf( doCheckSystemProperty ).booleanValue();
}
else
{
this.doCheck = doCheck;
}
}
public void setDoUpdate( boolean doUpdate )
{
String doUpdateSystemProperty = System.getProperty( "maven.buildNumber.doUpdate" );
if ( doUpdateSystemProperty != null )
{
// well, this gets the final say
this.doUpdate = Boolean.valueOf( doUpdateSystemProperty ).booleanValue();
}
else
{
this.doUpdate = doUpdate;
}
}
void setFormat( String format )
{
this.format = format;
}
void setLocale( String locale )
{
this.locale = locale;
}
void setItems( List> items )
{
this.items = items;
}
public void setBuildNumberPropertiesFileLocation( File buildNumberPropertiesFileLocation )
{
this.buildNumberPropertiesFileLocation = buildNumberPropertiesFileLocation;
}
public void setScmDirectory( File scmDirectory )
{
this.scmDirectory = scmDirectory;
}
public void setRevisionOnScmFailure( String revisionOnScmFailure )
{
this.revisionOnScmFailure = revisionOnScmFailure;
}
public void setShortRevisionLength( int shortRevision )
{
this.shortRevisionLength = shortRevision;
}
}