blob: 46ba7119b0483c5724d6b46eaae0b5a8c87f47ee [file] [log] [blame]
package org.apache.maven.plugins.scmpublish;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
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.Set;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.NameFileFilter;
import org.apache.commons.io.filefilter.NotFileFilter;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.logging.MessageUtils;
import org.codehaus.plexus.util.MatchPatterns;
/**
* Publish a content to scm. By default, content is taken from default site staging directory
* <code>${project.build.directory}/staging</code>.
* Can be used without project, so usable to update any SCM with any content.
*/
@Mojo ( name = "publish-scm", aggregator = true, requiresProject = false )
public class ScmPublishPublishScmMojo
extends AbstractScmPublishMojo
{
/**
* The content to be published.
*/
@Parameter ( property = "scmpublish.content", defaultValue = "${project.build.directory}/staging" )
private File content;
/**
*/
@Parameter( defaultValue = "${project}", readonly = true, required = true )
protected MavenProject project;
private List<File> deleted = new ArrayList<File>();
private List<File> added = new ArrayList<File>();
private List<File> updated = new ArrayList<File>();
private int directories = 0;
private int files = 0;
private long size = 0;
/**
* Update scm checkout directory with content.
*
* @param checkout the scm checkout directory
* @param dir the content to put in scm (can be <code>null</code>)
* @param doNotDeleteDirs directory names that should not be deleted from scm even if not in new content:
* used for modules, which content is available only when staging
* @param baseDir The base directory of the content
* @throws IOException
*/
private void update( File checkout, File dir, List<String> doNotDeleteDirs, File baseDir )
throws IOException
{
String scmSpecificFilename = scmProvider.getScmSpecificFilename();
String[] files = scmSpecificFilename != null
? checkout.list( new NotFileFilter( new NameFileFilter( scmSpecificFilename ) ) )
: checkout.list();
Path relativeDir = baseDir.toPath( ).toAbsolutePath( ).relativize( dir.toPath( ).toAbsolutePath( ) );
Set<String> checkoutContent = new HashSet<String>( Arrays.asList( files ) );
List<String> dirContent = ( dir != null ) ? Arrays.asList( dir.list() ) : Collections.<String>emptyList();
Set<String> deleted = new HashSet<String>( checkoutContent );
deleted.removeAll( dirContent );
MatchPatterns ignoreDeleteMatchPatterns = null;
List<String> pathsAsList = new ArrayList<String>( 0 );
if ( ignorePathsToDelete != null && ignorePathsToDelete.length > 0 )
{
ignoreDeleteMatchPatterns = MatchPatterns.from( ignorePathsToDelete );
pathsAsList = Arrays.asList( ignorePathsToDelete );
}
for ( String name : deleted )
{
String relativeName = relativeDir.resolve( name ).toString( );
if ( ignoreDeleteMatchPatterns != null && ignoreDeleteMatchPatterns.matches( relativeName, true ) )
{
getLog().debug(
name + " match one of the patterns '" + pathsAsList + "': do not add to deleted files" );
continue;
}
getLog().debug( "file marked for deletion: " + name );
File file = new File( checkout, name );
if ( ( doNotDeleteDirs != null ) && file.isDirectory() && ( doNotDeleteDirs.contains( name ) ) )
{
// ignore directory not available
continue;
}
if ( file.isDirectory() )
{
update( file, null, null, baseDir );
}
this.deleted.add( file );
}
for ( String name : dirContent )
{
File file = new File( checkout, name );
File source = new File( dir, name );
if ( Files.isSymbolicLink( source.toPath() ) )
{
if ( !checkoutContent.contains( name ) )
{
this.added.add( file );
}
// copy symbolic link (Java 7 only)
copySymLink( source, file );
}
else if ( source.isDirectory() )
{
directories++;
if ( !checkoutContent.contains( name ) )
{
this.added.add( file );
file.mkdir();
}
update( file, source, null , baseDir );
}
else
{
if ( checkoutContent.contains( name ) )
{
this.updated.add( file );
}
else
{
this.added.add( file );
}
copyFile( source, file );
}
}
}
/**
* Copy a symbolic link.
*
* @param srcFile the source file (expected to be a symbolic link)
* @param destFile the destination file (which will be a symbolic link)
* @throws IOException
*/
private void copySymLink( File srcFile, File destFile )
throws IOException
{
Files.copy( srcFile.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES, LinkOption.NOFOLLOW_LINKS );
}
/**
* Copy a file content, normalizing newlines when necessary.
*
* @param srcFile the source file
* @param destFile the destination file
* @throws IOException
* @see #requireNormalizeNewlines(File)
*/
private void copyFile( File srcFile, File destFile )
throws IOException
{
if ( requireNormalizeNewlines( srcFile ) )
{
copyAndNormalizeNewlines( srcFile, destFile );
}
else
{
FileUtils.copyFile( srcFile, destFile );
}
files++;
size += destFile.length();
}
/**
* Copy and normalize newlines.
*
* @param srcFile the source file
* @param destFile the destination file
* @throws IOException
*/
private void copyAndNormalizeNewlines( File srcFile, File destFile )
throws IOException
{
BufferedReader in = null;
PrintWriter out = null;
try
{
in = new BufferedReader( new InputStreamReader( new FileInputStream( srcFile ), siteOutputEncoding ) );
out = new PrintWriter( new OutputStreamWriter( new FileOutputStream( destFile ), siteOutputEncoding ) );
for ( String line = in.readLine(); line != null; line = in.readLine() )
{
if ( in.ready() )
{
out.println( line );
}
else
{
out.print( line );
}
}
out.close();
out = null;
in.close();
in = null;
}
finally
{
IOUtils.closeQuietly( out );
IOUtils.closeQuietly( in );
}
}
public void scmPublishExecute()
throws MojoExecutionException, MojoFailureException
{
if ( siteOutputEncoding == null )
{
getLog().warn( "No output encoding, defaulting to UTF-8." );
siteOutputEncoding = "utf-8";
}
if ( !content.exists() )
{
throw new MojoExecutionException( "Configured content directory does not exist: " + content );
}
if ( !content.canRead() )
{
throw new MojoExecutionException( "Can't read content directory: " + content );
}
checkoutExisting();
final File updateDirectory;
if ( subDirectory == null )
{
updateDirectory = checkoutDirectory;
}
else
{
updateDirectory = new File( checkoutDirectory, subDirectory );
// Security check for subDirectory with .. inside
if ( !updateDirectory.toPath().normalize().startsWith( checkoutDirectory.toPath().normalize() ) )
{
logError( "Try to acces outside of the checkout directory with sub-directory: %s", subDirectory );
return;
}
if ( !updateDirectory.exists() )
{
updateDirectory.mkdirs();
}
logInfo( "Will copy content in sub-directory: %s", subDirectory );
}
try
{
logInfo( "Updating checkout directory with actual content in %s", content );
update( checkoutDirectory, content, ( project == null ) ? null : project.getModel().getModules(), content );
String displaySize = org.apache.commons.io.FileUtils.byteCountToDisplaySize( size );
logInfo( "Content consists of " + MessageUtils.buffer().strong( "%d directories and %d files = %s" ),
directories, files, displaySize );
}
catch ( IOException ioe )
{
throw new MojoExecutionException( "Could not copy content to SCM checkout", ioe );
}
logInfo( "Publishing content to SCM will result in "
+ MessageUtils.buffer().strong( "%d addition(s), %d update(s), %d delete(s)" ), added.size(),
updated.size(), deleted.size() );
if ( isDryRun() )
{
int pos = checkoutDirectory.getAbsolutePath().length() + 1;
for ( File addedFile : added )
{
logInfo( "- addition %s", addedFile.getAbsolutePath().substring( pos ) );
}
for ( File updatedFile : updated )
{
logInfo( "- update %s", updatedFile.getAbsolutePath().substring( pos ) );
}
for ( File deletedFile : deleted )
{
logInfo( "- delete %s", deletedFile.getAbsolutePath().substring( pos ) );
}
return;
}
if ( !added.isEmpty() )
{
addFiles( added );
}
if ( !deleted.isEmpty() )
{
deleteFiles( deleted );
}
logInfo( "Checking in SCM, starting at " + new Date() + "..." );
checkinFiles();
}
}