hudson.scm.AbstractCvs.java Source code

Java tutorial

Introduction

Here is the source code for hudson.scm.AbstractCvs.java

Source

/*
 * The MIT License
 *
 * Copyright (c) 2012, Michael Clarke
 *
 * 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.
 */
package hudson.scm;

import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.*;
import hudson.remoting.VirtualChannel;
import hudson.scm.cvstagging.CvsTagAction;
import hudson.util.Secret;
import org.apache.commons.io.output.DeferredFileOutputStream;
import org.netbeans.lib.cvsclient.CVSRoot;
import org.netbeans.lib.cvsclient.Client;
import org.netbeans.lib.cvsclient.admin.AdminHandler;
import org.netbeans.lib.cvsclient.admin.Entry;
import org.netbeans.lib.cvsclient.admin.StandardAdminHandler;
import org.netbeans.lib.cvsclient.command.Command;
import org.netbeans.lib.cvsclient.command.CommandAbortedException;
import org.netbeans.lib.cvsclient.command.CommandException;
import org.netbeans.lib.cvsclient.command.GlobalOptions;
import org.netbeans.lib.cvsclient.command.checkout.CheckoutCommand;
import org.netbeans.lib.cvsclient.command.log.RlogCommand;
import org.netbeans.lib.cvsclient.command.update.UpdateCommand;
import org.netbeans.lib.cvsclient.commandLine.BasicListener;
import org.netbeans.lib.cvsclient.connection.AuthenticationException;
import org.netbeans.lib.cvsclient.connection.Connection;
import org.netbeans.lib.cvsclient.connection.ConnectionFactory;
import org.netbeans.lib.cvsclient.connection.ConnectionIdentity;
import org.netbeans.lib.cvsclient.event.CVSListener;

import java.io.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

public abstract class AbstractCvs extends SCM implements ICvs {

    protected static final DateFormat DATE_FORMATTER = new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z", Locale.UK);

    @Override
    public AbstractCvsDescriptor getDescriptor() {
        return (AbstractCvsDescriptor) super.getDescriptor();
    }

    protected boolean checkout(CvsRepository[] repositories, boolean isFlatten, FilePath workspace,
            boolean canUseUpdate, AbstractBuild<?, ?> build, String dateStamp, boolean pruneEmptyDirectories,
            boolean cleanOnFailedUpdate, BuildListener listener) throws IOException, InterruptedException {

        final EnvVars envVars = build.getEnvironment(listener);

        for (CvsRepository repository : repositories) {

            for (CvsRepositoryItem item : repository.getRepositoryItems()) {

                for (CvsModule cvsModule : item.getModules()) {
                    final String checkoutName = envVars.expand(cvsModule.getCheckoutName());
                    boolean localSubModule = checkoutName.contains("/") && cvsModule.isAlternativeCheckoutName();
                    int lastSlash = checkoutName.lastIndexOf("/");

                    final boolean flatten = isFlatten && !cvsModule.isAlternativeCheckoutName();

                    final FilePath targetWorkspace = flatten ? workspace.getParent()
                            : localSubModule ? workspace.child(checkoutName.substring(0, lastSlash)) : workspace;

                    final String moduleName = flatten ? workspace.getName()
                            : localSubModule ? checkoutName.substring(lastSlash + 1) : checkoutName;

                    final FilePath module = targetWorkspace.child(moduleName);

                    boolean updateFailed = false;
                    boolean update = false;

                    if (flatten) {
                        if (workspace.child("CVS/Entries").exists()) {
                            update = true;
                        }
                    } else {
                        if (canUseUpdate && module.exists()) {
                            update = true;
                        }
                    }

                    CvsRepositoryLocation repositoryLocation = item.getLocation();
                    CvsRepositoryLocationType locationType = repositoryLocation.getLocationType();
                    String locationName = repositoryLocation.getLocationName();
                    String expandedLocationName = envVars.expand(locationName);
                    // we're doing an update
                    if (update) {
                        // we're doing a CVS update
                        UpdateCommand updateCommand = new UpdateCommand();

                        // force it to recurse into directories
                        updateCommand.setBuildDirectories(true);
                        updateCommand.setRecursive(true);

                        // set directory pruning
                        updateCommand.setPruneDirectories(pruneEmptyDirectories);

                        // set overwrite policy
                        updateCommand.setCleanCopy(isForceCleanCopy());

                        // point to head, branch or tag
                        if (locationType == CvsRepositoryLocationType.BRANCH) {
                            updateCommand.setUpdateByRevision(expandedLocationName);
                            if (repositoryLocation.isUseHeadIfNotFound()) {
                                updateCommand.setUseHeadIfNotFound(true);
                                updateCommand.setUpdateByDate(dateStamp);
                            }
                        } else if (locationType == CvsRepositoryLocationType.TAG) {
                            updateCommand.setUpdateByRevision(expandedLocationName);
                            updateCommand.setUseHeadIfNotFound(repositoryLocation.isUseHeadIfNotFound());
                        } else {
                            updateCommand
                                    .setUpdateByRevision(CvsRepositoryLocationType.HEAD.getName().toUpperCase());
                            updateCommand.setUpdateByDate(dateStamp);
                        }

                        if (!perform(updateCommand, targetWorkspace, listener, repository, moduleName, envVars)) {
                            if (cleanOnFailedUpdate) {
                                updateFailed = true;
                            } else {
                                return false;
                            }
                        }

                    }

                    // we're doing a checkout
                    if (!update || (updateFailed && cleanOnFailedUpdate)) {

                        if (updateFailed) {
                            listener.getLogger()
                                    .println("Update failed. Cleaning workspace and performing full checkout");
                            workspace.deleteContents();
                        }

                        // we're doing a CVS checkout
                        CheckoutCommand checkoutCommand = new CheckoutCommand();

                        // point to branch or tag if specified
                        if (locationType == CvsRepositoryLocationType.BRANCH) {
                            checkoutCommand.setCheckoutByRevision(expandedLocationName);
                            if (repositoryLocation.isUseHeadIfNotFound()) {
                                checkoutCommand.setUseHeadIfNotFound(true);
                                checkoutCommand.setCheckoutByDate(dateStamp);
                            }
                        } else if (locationType == CvsRepositoryLocationType.TAG) {
                            checkoutCommand.setCheckoutByRevision(expandedLocationName);
                            if (repositoryLocation.isUseHeadIfNotFound()) {
                                checkoutCommand.setUseHeadIfNotFound(true);
                            }
                        } else if (locationType == CvsRepositoryLocationType.HEAD) {
                            checkoutCommand.setCheckoutByDate(dateStamp);
                        }

                        // set directory pruning
                        checkoutCommand.setPruneDirectories(pruneEmptyDirectories);

                        // set where we're checking out to
                        if (cvsModule.isAlternativeCheckoutName() || flatten) {
                            checkoutCommand.setCheckoutDirectory(moduleName);
                        }

                        // and specify which module to load
                        checkoutCommand.setModule(envVars.expand(cvsModule.getRemoteName()));

                        if (!perform(checkoutCommand, targetWorkspace, listener, repository, moduleName, envVars)) {
                            return false;
                        }

                    }

                }
            }

        }

        return true;
    }

    /**
     * Runs a cvs command in the given workspace.
     * @param cvsCommand the command to run (checkout, update etc)
     * @param workspace the workspace to run the command in
     * @param listener where to log output to
     * @param repository the repository to connect to
     * @param moduleName the name of the directory within the workspace that will have work performed on it
     * @param envVars the environmental variables to expand
     * @return true if the action succeeds, false otherwise
     * @throws IOException on failure handling files or server actions
     * @throws InterruptedException if the user cancels the action
     */
    private boolean perform(final Command cvsCommand, final FilePath workspace, final TaskListener listener,
            final CvsRepository repository, final String moduleName, final EnvVars envVars)
            throws IOException, InterruptedException {

        final Client cvsClient = getCvsClient(repository, envVars, listener);
        final GlobalOptions globalOptions = getGlobalOptions(repository, envVars);

        if (!workspace.act(new FilePath.FileCallable<Boolean>() {

            private static final long serialVersionUID = -7517978923721181408L;

            @Override
            public Boolean invoke(final File workspace, final VirtualChannel channel) throws RuntimeException {

                if (cvsCommand instanceof UpdateCommand) {
                    ((UpdateCommand) cvsCommand).setFiles(new File[] { new File(workspace, moduleName) });
                }

                listener.getLogger().println("cvs " + cvsCommand.getCVSCommand());

                cvsClient.setLocalPath(workspace.getAbsolutePath());
                final BasicListener basicListener = new BasicListener(listener.getLogger(), listener.getLogger());
                cvsClient.getEventManager().addCVSListener(basicListener);

                try {
                    return cvsClient.executeCommand(cvsCommand, globalOptions);
                } catch (CommandAbortedException e) {
                    e.printStackTrace(listener.error("CVS Command aborted: " + e.getMessage()));
                    return false;
                } catch (CommandException e) {
                    e.printStackTrace(listener.error("CVS Command failed: " + e.getMessage()));
                    return false;
                } catch (AuthenticationException e) {
                    e.printStackTrace(listener.error("CVS Authentication failed: " + e.getMessage()));
                    return false;
                } finally {
                    try {
                        cvsClient.getConnection().close();
                    } catch (IOException ex) {
                        listener.error("Could not close client connection: " + ex.getMessage());
                    }
                }
            }
        })) {
            listener.error("Cvs task failed");
            return false;
        }

        return true;
    }

    /**
     * Gets an instance of the CVS client that can be used for connection to a repository. If the
     * repository specifies a password then the client's connection will be set with this password.
     * @param repository the repository to connect to
     * @param envVars variables to use for macro expansion
     * @return a CVS client capable of connecting to the specified repository
     */
    public Client getCvsClient(final CvsRepository repository, final EnvVars envVars, final TaskListener listener) {
        CVSRoot cvsRoot = CVSRoot.parse(envVars.expand(repository.getCvsRoot()));

        if (repository.isPasswordRequired()) {
            listener.getLogger()
                    .println("Using locally configured password for connection to " + cvsRoot.toString());
            cvsRoot.setPassword(Secret.toString(repository.getPassword()));
        } else {
            String partialRoot = cvsRoot.getHostName() + ":" + cvsRoot.getPort() + cvsRoot.getRepository();
            String sanitisedRoot = ":" + cvsRoot.getMethod() + ":" + partialRoot;
            for (CvsAuthentication authentication : getDescriptor().getAuthentication()) {
                if (authentication.getCvsRoot().equals(sanitisedRoot) && (cvsRoot.getUserName() == null
                        || authentication.getUsername().equals(cvsRoot.getUserName()))) {
                    cvsRoot = CVSRoot.parse(":" + cvsRoot.getMethod() + ":"
                            + (authentication.getUsername() != null ? authentication.getUsername() + "@" : "")
                            + partialRoot);
                    cvsRoot.setPassword(authentication.getPassword().getPlainText());
                    listener.getLogger().println("Using globally configured password for connection to '"
                            + sanitisedRoot + "' with username '" + authentication.getUsername() + "'");
                    break;
                }
            }
        }

        ConnectionIdentity connectionIdentity = ConnectionFactory.getConnectionIdentity();
        connectionIdentity.setKnownHostsFile(envVars.expand(getDescriptor().getKnownHostsLocation()));
        connectionIdentity.setPrivateKeyPath(envVars.expand(getDescriptor().getPrivateKeyLocation()));
        if (getDescriptor().getPrivateKeyPassword() != null) {
            connectionIdentity.setPrivateKeyPassword(getDescriptor().getPrivateKeyPassword().getPlainText());
        }

        final Connection cvsConnection = ConnectionFactory.getConnection(cvsRoot);

        return new Client(cvsConnection, new StandardAdminHandler());
    }

    public GlobalOptions getGlobalOptions(CvsRepository repository, EnvVars envVars) {
        final GlobalOptions globalOptions = new GlobalOptions();
        globalOptions.setVeryQuiet(!isDisableCvsQuiet());
        globalOptions.setCompressionLevel(getCompressionLevel(repository, envVars));
        globalOptions.setCVSRoot(envVars.expand(repository.getCvsRoot()));
        return globalOptions;
    }

    /**
     * Calculates the level of compression that should be used for dealing with
     * the given repository.
     * <p>
     * If we're using a local repository then we don't use compression
     * (http://root.cern.ch/root/CVS.html#checkout), if no compression level has
     * been specifically set for this repository then we use the global setting,
     * otherwise we use the one set for this repository.
     *
     * @param repository
     *            the repository we're wanting to connect to
     * @param envVars
     *            the environmental variables to expand any parameters from
     * @return the level of compression to use between 0 and 9 (inclusive), with
     *         0 being no compression and 9 being maximum
     */
    private int getCompressionLevel(final CvsRepository repository, final EnvVars envVars) {
        final String cvsroot = envVars.expand(repository.getCvsRoot());

        /*
         * CVS 1.11.22 manual: If the access method is omitted, then if the
         * repository starts with `/', then `:local:' is assumed. If it does not
         * start with `/' then either `:ext:' or `:server:' is assumed.
         */
        boolean local = cvsroot.startsWith("/") || cvsroot.startsWith(":local:") || cvsroot.startsWith(":fork:");

        // Use whatever the user has specified as the system default if the
        // repository doesn't specifically set one
        int compressionLevel = repository.getCompressionLevel() == -1 ? getDescriptor().getCompressionLevel()
                : repository.getCompressionLevel();

        // For local access, compression is senseless (always return 0),
        // otherwise return the calculated value
        return local ? 0 : compressionLevel;
    }

    public boolean isDisableCvsQuiet() {
        return true;
    }

    /**
     * Since we add the current SCMRevisionState as an action during the build
     * (so we can get the current workspace state), this method should never be
     * called. Just for safety, we get the action and return it.
     *
     * @see {@link SCM#calcRevisionsFromBuild(hudson.model.AbstractBuild, hudson.Launcher, TaskListener)}
     */
    @Override
    public SCMRevisionState calcRevisionsFromBuild(final AbstractBuild<?, ?> build, final Launcher launcher,
            final TaskListener listener) throws IOException, InterruptedException {
        return build.getAction(CvsRevisionState.class);
    }

    protected PollingResult compareRemoteRevisionWith(final AbstractProject<?, ?> project, final Launcher launcher,
            final TaskListener listener, final SCMRevisionState baseline, final CvsRepository[] repositories)
            throws IOException, InterruptedException {

        // No previous build? everything has changed
        if (null == project.getLastBuild()) {
            listener.getLogger().println("No previous build found, scheduling build");
            return PollingResult.BUILD_NOW;
        }

        final EnvVars envVars = project.getLastBuild().getEnvironment(listener);

        final Date currentPollDate = Calendar.getInstance().getTime();

        /*
         * this flag will be used to check whether a build is needed (assuming
         * the local and remote states are comparable and no configuration has
         * changed between the last build and the current one)
         */
        boolean changesPresent = false;

        // Schedule a new build if the baseline isn't valid
        if ((baseline == null || !(baseline instanceof CvsRevisionState))) {
            listener.getLogger().println("Invalid baseline detected, scheduling build");
            return new PollingResult(baseline, new CvsRevisionState(new HashMap<CvsRepository, List<CvsFile>>()),
                    PollingResult.Change.INCOMPARABLE);
        }

        // convert the baseline into a use-able form
        final Map<CvsRepository, List<CvsFile>> remoteState = new HashMap<CvsRepository, List<CvsFile>>(
                ((CvsRevisionState) baseline).getModuleFiles());

        // Loops through every module and check if it has changed
        for (CvsRepository repository : repositories) {

            /*
             * this repository setting either didn't exist or has changed since
             * the last build so we'll force a new build now.
             */
            if (!remoteState.containsKey(repository)) {
                listener.getLogger().println("Repository not found in workspace state, scheduling build");
                return new PollingResult(baseline,
                        new CvsRevisionState(new HashMap<CvsRepository, List<CvsFile>>()),
                        PollingResult.Change.INCOMPARABLE);
            }

            // get the list of current changed files in this repository
            final List<CvsFile> changes = calculateRepositoryState(project.getLastCompletedBuild().getTime(),
                    currentPollDate, repository, listener, envVars);

            final List<CvsFile> remoteFiles = remoteState.get(repository);

            // update the remote state with the changes we've just retrieved
            for (CvsFile changedFile : changes) {
                for (CvsFile existingFile : remoteFiles) {
                    if (!changedFile.getName().equals(existingFile.getName())) {
                        continue;
                    }

                    remoteFiles.remove(existingFile);
                    if (!changedFile.isDead()) {
                        remoteFiles.add(changedFile);
                    }
                }
            }

            // set the updated files list back into the remote state
            remoteState.put(repository, remoteFiles);

            // convert the excluded regions into patterns so we can use them as
            // regular expressions
            final List<Pattern> excludePatterns = new ArrayList<Pattern>();

            for (ExcludedRegion pattern : repository.getExcludedRegions()) {
                try {
                    excludePatterns.add(Pattern.compile(pattern.getPattern()));
                } catch (PatternSyntaxException ex) {
                    launcher.getListener().getLogger().println("Pattern could not be compiled: " + ex.getMessage());
                }
            }

            // create a list of changes we can use to filter out excluded
            // regions
            final List<CvsFile> filteredChanges = new ArrayList<CvsFile>(changes);

            // filter out all changes in the exclude regions
            for (final Pattern excludePattern : excludePatterns) {
                for (Iterator<CvsFile> itr = filteredChanges.iterator(); itr.hasNext();) {
                    CvsFile change = itr.next();
                    if (excludePattern.matcher(change.getName()).matches()) {
                        itr.remove();
                    }
                }
            }

            // if our list of changes isn't empty then we want to note this as
            // we need a build
            changesPresent = changesPresent || !filteredChanges.isEmpty();
        }

        // Return the new repository state and whether we require a new build
        return new PollingResult(baseline, new CvsRevisionState(remoteState),
                changesPresent ? PollingResult.Change.SIGNIFICANT : PollingResult.Change.NONE);
    }

    /**
     * Builds a list of files that have changed in the given repository between
     * any 2 time-stamps. This does not require the workspace to be checked out
     * and does not change the state of any checked out workspace. The list
     * returned does not have any filters set for exclude regions (i.e. it's
     * every file that's changed for the modules being watched).
     *
     * @param startTime
     *            the time-stamp to start filtering changes from
     * @param endTime
     *            the time-stamp to end filtering changes from
     * @param repository
     *            the repository to search for changes in. All modules under
     *            this repository will be checked
     * @param listener
     *            where to send log output
     * @param envVars the environmental variables to perform parameter expansion from
     * @return a list of changed files, including their versions and change
     *         comments
     * @throws IOException
     *             on communication failure (e.g. communication with slaves)
     */
    protected List<CvsFile> calculateRepositoryState(final Date startTime, final Date endTime,
            final CvsRepository repository, final TaskListener listener, final EnvVars envVars) throws IOException {
        final List<CvsFile> files = new ArrayList<CvsFile>();

        for (final CvsRepositoryItem item : repository.getRepositoryItems()) {
            for (final CvsModule module : item.getModules()) {

                CvsLog logContents = getRemoteLogForModule(repository, item, module, startTime, endTime, envVars,
                        listener);

                // use the parser to build up a list of changed files and add it to
                // the list we've been creating
                files.addAll(logContents.mapCvsLog(envVars.expand(repository.getCvsRoot()), item.getLocation())
                        .getFiles());

            }
        }
        return files;
    }

    /**
     * Gets the output for the CVS <tt>rlog</tt> command for the given module
     * between the specified dates.
     *
     * @param repository
     *            the repository to connect to for running rlog against
     * @param module
     *            the module to check for changes against
     * @param listener
     *            where to log any error messages to
     * @param startTime
     *            don't list any changes before this time
     * @param endTime
     *            don't list any changes after this time
     * @return the output of rlog with no modifications
     * @throws IOException
     *             on underlying communication failure
     */
    private CvsLog getRemoteLogForModule(final CvsRepository repository, final CvsRepositoryItem item,
            final CvsModule module, final Date startTime, final Date endTime, final EnvVars envVars,
            final TaskListener listener) throws IOException {
        final Client cvsClient = getCvsClient(repository, envVars, listener);

        RlogCommand rlogCommand = new RlogCommand();

        // we have to synchronize since we're dealing with DateFormat.format()
        synchronized (DATE_FORMATTER) {
            final String lastBuildDate = DATE_FORMATTER.format(startTime);
            final String endDate = DATE_FORMATTER.format(endTime);

            rlogCommand.setDateFilter(lastBuildDate + "<" + endDate);
        }

        // tell CVS which module we're logging
        rlogCommand.setModule(envVars.expand(module.getRemoteName()));

        // ignore headers for files that aren't in the current change-set
        rlogCommand.setSuppressHeader(true);

        // create an output stream to send the output from CVS command to - we
        // can then parse it from here
        final File tmpRlogSpill = File.createTempFile("cvs", "rlog");
        final DeferredFileOutputStream outputStream = new DeferredFileOutputStream(100 * 1024, tmpRlogSpill);
        final PrintStream logStream = new PrintStream(outputStream, true, getDescriptor().getChangelogEncoding());

        // set a listener with our output stream that we parse the log from
        final CVSListener basicListener = new BasicListener(logStream, listener.getLogger());
        cvsClient.getEventManager().addCVSListener(basicListener);

        // log the command to the current run/polling log
        listener.getLogger().println("cvs " + rlogCommand.getCVSCommand());

        // send the command to be run, we can't continue of the task fails
        try {
            if (!cvsClient.executeCommand(rlogCommand, getGlobalOptions(repository, envVars))) {
                throw new RuntimeException("Error while trying to run CVS rlog");
            }
        } catch (CommandAbortedException e) {
            throw new RuntimeException("CVS rlog command aborted", e);
        } catch (CommandException e) {
            throw new RuntimeException("CVS rlog command failed", e);
        } catch (AuthenticationException e) {
            throw new RuntimeException("CVS authentication failure while running rlog command", e);
        } finally {
            try {
                cvsClient.getConnection().close();
            } catch (IOException ex) {
                listener.getLogger().println("Could not close client connection: " + ex.getMessage());
            }
        }

        // flush the output so we have it all available for parsing
        logStream.close();

        // return the contents of the stream as the output of the command
        return new CvsLog() {
            @Override
            public Reader read() throws IOException {
                // note that master and slave can have different platform encoding
                if (outputStream.isInMemory())
                    return new InputStreamReader(new ByteArrayInputStream(outputStream.getData()),
                            getDescriptor().getChangelogEncoding());
                else
                    return new InputStreamReader(new FileInputStream(outputStream.getFile()),
                            getDescriptor().getChangelogEncoding());
            }

            @Override
            public void dispose() {
                tmpRlogSpill.delete();
            }
        };
    }

    /**
     * Builds a list of changes that have occurred in the given repository
     * between any 2 time-stamps. This does not require the workspace to be
     * checked out and does not change the state of any checked out workspace.
     * The list returned does not have any filters set for exclude regions (i.e.
     * it's every file that's changed for the modules being watched).
     *
     * @param startTime
     *            the time-stamp to start filtering changes from
     * @param endTime
     *            the time-stamp to end filtering changes from
     * @param repository
     *            the repository to search for changes in. All modules under
     *            this repository will be checked
     * @param listener
     *            where to send log output
     * @param envVars the environmental variables to perform parameter expansion from.
     * @return a list of changed files, including their versions and change
     *         comments
     * @throws IOException
     *             on communication failure (e.g. communication with slaves)
     * @throws InterruptedException
     *             on job cancellation
     */
    protected List<CVSChangeLogSet.CVSChangeLog> calculateChangeLog(final Date startTime, final Date endTime,
            final CvsRepository repository, final TaskListener listener, final EnvVars envVars)
            throws IOException, InterruptedException {

        final List<CVSChangeLogSet.CVSChangeLog> changes = new ArrayList<CVSChangeLogSet.CVSChangeLog>();

        for (final CvsRepositoryItem item : repository.getRepositoryItems()) {
            for (final CvsModule module : item.getModules()) {

                CvsLog logContents = getRemoteLogForModule(repository, item, module, startTime, endTime, envVars,
                        listener);

                // use the parser to build up a list of changes and add it to the
                // list we've been creating
                changes.addAll(logContents.mapCvsLog(envVars.expand(repository.getCvsRoot()), item.getLocation())
                        .getChanges());
            }
        }
        return changes;
    }

    protected void postCheckout(AbstractBuild<?, ?> build, File changelogFile, CvsRepository[] repositories,
            FilePath workspace, BuildListener listener, boolean flatten, EnvVars envVars)
            throws IOException, InterruptedException {
        // build change log
        final AbstractBuild<?, ?> lastCompleteBuild = build.getPreviousBuiltBuild();

        if (lastCompleteBuild != null && !isSkipChangeLog()) {
            final List<CVSChangeLogSet.CVSChangeLog> changes = new ArrayList<CVSChangeLogSet.CVSChangeLog>();
            for (CvsRepository location : repositories) {
                changes.addAll(calculateChangeLog(lastCompleteBuild.getTime(), build.getTime(), location, listener,
                        build.getEnvironment(listener)));
            }
            new CVSChangeLogSet(build, changes).toFile(changelogFile);
        }

        // add the current workspace state as an action
        build.getActions()
                .add(new CvsRevisionState(calculateWorkspaceState(workspace, repositories, flatten, envVars)));

        // add the tag action to the build
        build.getActions().add(new CvsTagAction(build, this));

        // remove sticky date tags
        for (final CvsRepository repository : getRepositories()) {
            for (final CvsRepositoryItem repositoryItem : repository.getRepositoryItems()) {
                for (final CvsModule module : repositoryItem.getModules()) {
                    FilePath target = (flatten ? workspace : workspace.child(module.getCheckoutName()));
                    target.act(new FilePath.FileCallable<Void>() {
                        @Override
                        public Void invoke(File module, VirtualChannel virtualChannel)
                                throws IOException, InterruptedException {
                            final AdminHandler adminHandler = new StandardAdminHandler();

                            cleanup(module, adminHandler);

                            return null;
                        }

                        private void cleanup(File directory, AdminHandler adminHandler) throws IOException {
                            for (File file : adminHandler.getAllFiles(directory)) {
                                Entry entry = adminHandler.getEntry(file);
                                entry.setDate(null);
                                adminHandler.setEntry(file, entry);
                            }

                            // we need to remove CVS/Tag as it contains a sticky reference for HEAD modules
                            if (repositoryItem.getLocation().getLocationType() == CvsRepositoryLocationType.HEAD) {
                                final File tagFile = new File(directory, "CVS/Tag");

                                if (tagFile.exists()) {
                                    tagFile.delete();
                                }
                            }

                            File[] innerFiles = directory.listFiles();
                            if (null != innerFiles) {
                                for (File innerFile : innerFiles) {
                                    if (innerFile.isDirectory() && !innerFile.getName().equals("CVS")) {
                                        cleanup(innerFile, adminHandler);
                                    }
                                }
                            }
                        }
                    });
                }
            }
        }
    }

    private Map<CvsRepository, List<CvsFile>> calculateWorkspaceState(final FilePath workspace,
            final CvsRepository[] repositories, final boolean flatten, final EnvVars envVars)
            throws IOException, InterruptedException {
        Map<CvsRepository, List<CvsFile>> workspaceState = new HashMap<CvsRepository, List<CvsFile>>();

        for (CvsRepository repository : repositories) {
            List<CvsFile> cvsFiles = new ArrayList<CvsFile>();
            for (CvsRepositoryItem item : repository.getRepositoryItems()) {
                for (CvsModule module : item.getModules()) {
                    cvsFiles.addAll(getCvsFiles(workspace, module, flatten, envVars));
                }
            }
            workspaceState.put(repository, cvsFiles);
        }

        return workspaceState;
    }

    private List<CvsFile> getCvsFiles(final FilePath workspace, final CvsModule module, final boolean flatten,
            final EnvVars envVars) throws IOException, InterruptedException {
        FilePath targetWorkspace;
        if (flatten) {
            targetWorkspace = workspace;
        } else {
            targetWorkspace = workspace.child(envVars.expand(module.getCheckoutName()));
        }

        return targetWorkspace.act(new FilePath.FileCallable<List<CvsFile>>() {

            private static final long serialVersionUID = 8158155902777163137L;

            @Override
            public List<CvsFile> invoke(final File moduleLocation, final VirtualChannel channel)
                    throws IOException {
                /*
                 * we use the remote name because we're actually wanting the
                 * workspace represented as it would be in CVS. This then allows
                 * us to do a comparison against the file list returned by the
                 * rlog command (which wouldn't be possible if we use the local
                 * module name on a module that had been checked out as an alias
                 */
                return buildFileList(moduleLocation, envVars.expand(module.getRemoteName()));
            }

            public List<CvsFile> buildFileList(final File moduleLocation, final String prefix) throws IOException {
                AdminHandler adminHandler = new StandardAdminHandler();
                List<CvsFile> fileList = new ArrayList<CvsFile>();

                if (moduleLocation.isFile()) {
                    Entry entry = adminHandler.getEntry(moduleLocation);
                    if (entry != null) {
                        fileList.add(new CvsFile(entry.getName(), entry.getRevision()));
                    }
                } else {
                    for (File file : adminHandler.getAllFiles(moduleLocation)) {

                        if (file.isFile()) {
                            Entry entry = adminHandler.getEntry(file);
                            CvsFile currentFile = new CvsFile(prefix + "/" + entry.getName(), entry.getRevision());
                            fileList.add(currentFile);
                        }
                    }

                    // JENKINS-12807: we get a NPE here which shouldn't be possible given we know
                    // the file we're getting children of is a directory, but we'll do a null check
                    // for safety
                    File[] directoryFiles = moduleLocation.listFiles();
                    if (directoryFiles != null) {
                        for (File file : directoryFiles) {
                            if (file.isDirectory()) {
                                fileList.addAll(buildFileList(file, prefix + "/" + file.getName()));
                            }
                        }
                    }
                }

                return fileList;
            }

        });
    }

    @Override
    public ChangeLogParser createChangeLogParser() {
        return new CVSChangeLogParser();
    }

}