hudson.plugins.repo.RepoScm.java Source code

Java tutorial

Introduction

Here is the source code for hudson.plugins.repo.RepoScm.java

Source

/*
 * The MIT License
 *
 * Copyright (c) 2010, Brad Larson
 *
 * 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.plugins.repo;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.Job;
import hudson.model.ParameterDefinition;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.Run;
import hudson.model.StringParameterDefinition;
import hudson.model.TaskListener;
import hudson.scm.ChangeLogParser;
import hudson.scm.PollingResult;
import hudson.scm.SCM;
import hudson.scm.SCMDescriptor;
import hudson.scm.SCMRevisionState;
import net.sf.json.JSONObject;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

import hudson.scm.PollingResult.Change;
import hudson.util.FormValidation;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * The main entrypoint of the plugin. This class contains code to store user
 * configuration and to check out the code using a repo binary.
 */

@ExportedBean
public class RepoScm extends SCM implements Serializable {

    private static Logger debug = Logger.getLogger("hudson.plugins.repo.RepoScm");

    private final String manifestRepositoryUrl;

    // Advanced Fields:
    @CheckForNull
    private String manifestFile;
    @CheckForNull
    private String manifestGroup;
    @CheckForNull
    private String repoUrl;
    @CheckForNull
    private String mirrorDir;
    @CheckForNull
    private String manifestBranch;
    @CheckForNull
    private int jobs;
    @CheckForNull
    private int depth;
    @CheckForNull
    private String localManifest;
    @CheckForNull
    private String destinationDir;
    @CheckForNull
    private boolean currentBranch;
    @CheckForNull
    private boolean resetFirst;
    @CheckForNull
    private boolean quiet;
    @CheckForNull
    private boolean forceSync;
    @CheckForNull
    private boolean trace;
    @CheckForNull
    private boolean showAllChanges;
    @CheckForNull
    private boolean noTags;
    @CheckForNull
    private Set<String> ignoreProjects;

    /**
     * Returns the manifest repository URL.
     */
    @Exported
    public String getManifestRepositoryUrl() {
        return manifestRepositoryUrl;
    }

    /**
     * Returns the manifest branch name. By default, this is null and repo
     * defaults to "master".
     */
    @Exported
    public String getManifestBranch() {
        return manifestBranch;
    }

    /**
     * Merge the provided environment with the <em>default</em> values of
     * the project parameters. The values from the provided environment
     * take precedence.
     * @param environment   an existing environment, which contains already
     *                      properties from the current build
     * @param project       the project that is being built
     */
    private EnvVars getEnvVars(final EnvVars environment, final Job<?, ?> project) {
        // create an empty vars map
        final EnvVars finalEnv = new EnvVars();
        final ParametersDefinitionProperty params = project.getProperty(ParametersDefinitionProperty.class);
        if (params != null) {
            for (ParameterDefinition param : params.getParameterDefinitions()) {
                if (param instanceof StringParameterDefinition) {
                    final StringParameterDefinition stpd = (StringParameterDefinition) param;
                    final String dflt = stpd.getDefaultValue();
                    if (dflt != null) {
                        finalEnv.put(param.getName(), dflt);
                    }
                }
            }
        }
        // now merge the settings from the last build environment
        if (environment != null) {
            finalEnv.overrideAll(environment);
        }

        EnvVars.resolve(finalEnv);
        return finalEnv;
    }

    /**
     * Returns the initial manifest file name. By default, this is null and repo
     * defaults to "default.xml"
     */
    @Exported
    public String getManifestFile() {
        return manifestFile;
    }

    /**
     * Returns the group of projects to fetch. By default, this is null and
     * repo will fetch the default group.
     */
    @Exported
    public String getManifestGroup() {
        return manifestGroup;
    }

    /**
     * Returns the repo url. by default, this is null and
     * repo is fetched from aosp
     */
    @Exported
    public String getRepoUrl() {
        return repoUrl;
    }

    /**
     * Returns the name of the mirror directory. By default, this is null and
     * repo does not use a mirror.
     */
    @Exported
    public String getMirrorDir() {
        return mirrorDir;
    }

    /**
     * Returns the number of jobs used for sync. By default, this is null and
     * repo does not use concurrent jobs.
     */
    @Exported
    public int getJobs() {
        return jobs;
    }

    /**
     * Returns the depth used for sync.  By default, this is null and repo
     * will sync the entire history.
     */
    @Exported
    public int getDepth() {
        return depth;
    }

    /**
     * Returns the contents of the local_manifests/local.xml. By default, this is null
     * and a local_manifests/local.xml is neither created nor modified.
     */
    @Exported
    public String getLocalManifest() {
        return localManifest;
    }

    /**
     * Returns the destination directory. By default, this is null and the
     * source is synced to the root of the workspace.
     */
    @Exported
    public String getDestinationDir() {
        return destinationDir;
    }

    /**
     * returns list of ignore projects.
     */
    @Exported
    public String getIgnoreProjects() {
        return StringUtils.join(ignoreProjects, '\n');
    }

    /**
     * Returns the value of currentBranch.
     */
    @Exported
    public boolean isCurrentBranch() {
        return currentBranch;
    }

    /**
     * Returns the value of resetFirst.the initial manifest file name.
     */
    @Exported
    public boolean isResetFirst() {
        return resetFirst;
    }

    /**
     * Returns the value of showAllChanges.
     */
    @Exported
    public boolean isShowAllChanges() {
        return showAllChanges;
    }

    /**
     * Returns the value of quiet.
     */
    @Exported
    public boolean isQuiet() {
        return quiet;
    }

    /**
     * Returns the value of forceSync.
     */
    @Exported
    public boolean isForceSync() {
        return forceSync;
    }

    /**
     * Returns the value of trace.
     */
    @Exported
    public boolean isTrace() {
        return trace;
    }

    /**
     * Returns the value of noTags.
     */
    @Exported
    public boolean isNoTags() {
        return noTags;
    }

    /**
     * The constructor takes in user parameters and sets them. Each job using
     * the RepoSCM will call this constructor.
     *
     * @param manifestRepositoryUrl The URL for the manifest repository.
     * @param manifestBranch        The branch of the manifest repository. Typically this is null
     *                              or the empty string, which will cause repo to default to
     *                              "master".
     * @param manifestFile          The file to use as the repository manifest. Typically this is
     *                              null which will cause repo to use the default of "default.xml"
     * @param manifestGroup         The group name for the projects that need to be fetched.
     *                              Typically, this is null and all projects tagged 'default' will
     *                              be fetched.
     * @param mirrorDir             The path of the mirror directory to reference when
     *                              initializing repo.
     * @param jobs                  The number of concurrent jobs to use for the sync command. If
     *                              this is 0 or negative the jobs parameter is not specified.
     * @param depth                 This is the depth to use when syncing.  By default this is 0
     *                              and the full history is synced.
     * @param localManifest         May be null, a string containing XML, or an URL.
     *                              If XML, this string is written to
     *                              .repo/local_manifests/local.xml
     *                              If an URL, the URL is fetched and the content is written
     *                              to .repo/local_manifests/local.xml
     * @param destinationDir        If not null then the source is synced to the destinationDir
     *                              subdirectory of the workspace.
     * @param repoUrl               If not null then use this url as repo base,
     *                              instead of the default
     * @param currentBranch         If this value is true, add the "-c" option when executing
     *                              "repo sync".
     * @param resetFirst            If this value is true, do "repo forall -c 'git reset --hard'"
     *                              before syncing.
     * @param quiet                 If this value is true, add the "-q" option when executing
     *                              "repo sync".
     * @param trace                 If this value is true, add the "--trace" option when
     *                              executing "repo init" and "repo sync".
     * @param showAllChanges        If this value is true, add the "--first-parent" option to
     *                              "git log" when determining changesets.
     *
     */
    @Deprecated
    public RepoScm(final String manifestRepositoryUrl, final String manifestBranch, final String manifestFile,
            final String manifestGroup, final String mirrorDir, final int jobs, final int depth,
            final String localManifest, final String destinationDir, final String repoUrl,
            final boolean currentBranch, final boolean resetFirst, final boolean quiet, final boolean trace,
            final boolean showAllChanges) {
        this(manifestRepositoryUrl);
        setManifestBranch(manifestBranch);
        setManifestGroup(manifestGroup);
        setManifestFile(manifestFile);
        setMirrorDir(mirrorDir);
        setJobs(jobs);
        setDepth(depth);
        setLocalManifest(localManifest);
        setDestinationDir(destinationDir);
        setCurrentBranch(currentBranch);
        setResetFirst(resetFirst);
        setQuiet(quiet);
        setTrace(trace);
        setShowAllChanges(showAllChanges);
        setRepoUrl(repoUrl);
        ignoreProjects = Collections.<String>emptySet();
    }

    /**
     * The constructor takes in user parameters and sets them. Each job using
     * the RepoSCM will call this constructor.
     *
     * @param manifestRepositoryUrl The URL for the manifest repository.
     */
    @DataBoundConstructor
    public RepoScm(final String manifestRepositoryUrl) {
        this.manifestRepositoryUrl = manifestRepositoryUrl;
        manifestFile = null;
        manifestGroup = null;
        repoUrl = null;
        mirrorDir = null;
        manifestBranch = null;
        jobs = 0;
        depth = 0;
        localManifest = null;
        destinationDir = null;
        currentBranch = false;
        resetFirst = false;
        quiet = false;
        forceSync = false;
        trace = false;
        showAllChanges = false;
        noTags = false;
        ignoreProjects = Collections.<String>emptySet();
    }

    /**
     * Set the manifest branch name.
     *
     * @param manifestBranch
     *        The branch of the manifest repository. Typically this is null
     *        or the empty string, which will cause repo to default to
     *        "master".
      */
    @DataBoundSetter
    public void setManifestBranch(@CheckForNull final String manifestBranch) {
        this.manifestBranch = Util.fixEmptyAndTrim(manifestBranch);
    }

    /**
     * Set the initial manifest file name.
     *
     * @param manifestFile
     *        The file to use as the repository manifest. Typically this is
     *        null which will cause repo to use the default of "default.xml"
      */
    @DataBoundSetter
    public void setManifestFile(@CheckForNull final String manifestFile) {
        this.manifestFile = Util.fixEmptyAndTrim(manifestFile);
    }

    /**
     * Set the group of projects to fetch.
     *
     * @param manifestGroup
     *        The group name for the projects that need to be fetched.
     *        Typically, this is null and all projects tagged 'default' will
     *        be fetched.
      */
    @DataBoundSetter
    public void setManifestGroup(@CheckForNull final String manifestGroup) {
        this.manifestGroup = Util.fixEmptyAndTrim(manifestGroup);
    }

    /**
     * Set the name of the mirror directory.
     *
     * @param mirrorDir
     *        The path of the mirror directory to reference when
     *        initializing repo.
      */
    @DataBoundSetter
    public void setMirrorDir(@CheckForNull final String mirrorDir) {
        this.mirrorDir = Util.fixEmptyAndTrim(mirrorDir);
    }

    /**
     * Set the number of jobs used for sync.
     *
     * @param jobs
     *        The number of concurrent jobs to use for the sync command. If
     *        this is 0 or negative the jobs parameter is not specified.
      */
    @DataBoundSetter
    public void setJobs(final int jobs) {
        this.jobs = jobs;
    }

    /**
     * Set the depth used for sync.
     *
     * @param depth
     *        This is the depth to use when syncing.  By default this is 0
     *        and the full history is synced.
      */
    @DataBoundSetter
    public void setDepth(final int depth) {
        this.depth = depth;
    }

    /**
     * Set the content of the local manifest.
     *
     * @param localManifest
     *        May be null, a string containing XML, or an URL.
     *        If XML, this string is written to .repo/local_manifests/local.xml
     *        If an URL, the URL is fetched and the content is written
     *        to .repo/local_manifests/local.xml
      */
    @DataBoundSetter
    public void setLocalManifest(@CheckForNull final String localManifest) {
        this.localManifest = Util.fixEmptyAndTrim(localManifest);
    }

    /**
     * Set the destination directory.
     *
     * @param destinationDir
     *        If not null then the source is synced to the destinationDir
     *        subdirectory of the workspace.
      */
    @DataBoundSetter
    public void setDestinationDir(@CheckForNull final String destinationDir) {
        this.destinationDir = Util.fixEmptyAndTrim(destinationDir);
    }

    /**
     * Set currentBranch.
     *
     * @param currentBranch
     *         If this value is true, add the "-c" option when executing
     *        "repo sync".
      */
    @DataBoundSetter
    public void setCurrentBranch(final boolean currentBranch) {
        this.currentBranch = currentBranch;
    }

    /**
     * Set resetFirst.
     *
     * @param resetFirst
     *        If this value is true, do "repo forall -c 'git reset --hard'"
     *        before syncing.
      */
    @DataBoundSetter
    public void setResetFirst(final boolean resetFirst) {
        this.resetFirst = resetFirst;
    }

    /**
     * Set quiet.
     *
     * @param quiet
     * *      If this value is true, add the "-q" option when executing
     *        "repo sync".
      */
    @DataBoundSetter
    public void setQuiet(final boolean quiet) {
        this.quiet = quiet;
    }

    /**
     * Set trace.
     *
     * @param trace
     *        If this value is true, add the "--trace" option when
     *        executing "repo init" and "repo sync".
      */

    @DataBoundSetter
    public void setTrace(final boolean trace) {
        this.trace = trace;
    }

    /**
     * Set showAllChanges.
     *
     * @param showAllChanges
     *        If this value is true, add the "--first-parent" option to
     *        "git log" when determining changesets.
      */
    @DataBoundSetter
    public void setShowAllChanges(final boolean showAllChanges) {
        this.showAllChanges = showAllChanges;
    }

    /**
     * Set the repo url.
     *
     * @param repoUrl
     *        If not null then use this url as repo base,
     *        instead of the default
      */
    @DataBoundSetter
    public void setRepoUrl(@CheckForNull final String repoUrl) {
        this.repoUrl = Util.fixEmptyAndTrim(repoUrl);
    }

    /**
    * Enables --force-sync option on repo sync command.
     * @param forceSync
     *        If this value is true, add the "--force-sync" option when
    *        executing "repo sync".
    */
    @DataBoundSetter
    public void setForceSync(final boolean forceSync) {
        this.forceSync = forceSync;
    }

    /**
     * Set noTags.
     *
     * @param noTags
     *            If this value is true, add the "--no-tags" option when
     *            executing "repo sync".
     */
    @DataBoundSetter
    public final void setNoTags(final boolean noTags) {
        this.noTags = noTags;
    }

    /**
     * Sets list of projects which changes will be ignored when
     * calculating whether job needs to be rebuild. This field corresponds
     * to serverpath i.e. "name" section of the manifest.
     * @param ignoreProjects
     *            String representing project names separated by " ".
     */
    @DataBoundSetter
    public final void setIgnoreProjects(final String ignoreProjects) {
        if (ignoreProjects == null) {
            this.ignoreProjects = Collections.<String>emptySet();
            return;
        }
        this.ignoreProjects = new LinkedHashSet<String>(Arrays.asList(ignoreProjects.split("\\s+")));
    }

    @Override
    public SCMRevisionState calcRevisionsFromBuild(@Nonnull final Run<?, ?> build,
            @Nullable final FilePath workspace, @Nullable final Launcher launcher,
            @Nonnull final TaskListener listener) throws IOException, InterruptedException {
        // We add our SCMRevisionState from within checkout, so this shouldn't
        // be called often. However it will be called if this is the first
        // build, if a build was aborted before it reported the repository
        // state, etc.
        return SCMRevisionState.NONE;
    }

    private boolean shouldIgnoreChanges(final RevisionState current, final RevisionState baseline) {
        List<ProjectState> changedProjects = current.whatChanged(baseline);
        if ((changedProjects == null) || (ignoreProjects == null)) {
            return false;
        }
        if (ignoreProjects.isEmpty()) {
            return false;
        }

        // Check for every changed item if it is not contained in the
        // ignored setting .. project must be rebuilt
        for (ProjectState changed : changedProjects) {
            if (!ignoreProjects.contains(changed.getServerPath())) {
                return false;
            }
        }
        return true;
    }

    @Override
    public PollingResult compareRemoteRevisionWith(@Nonnull final Job<?, ?> job, @Nullable final Launcher launcher,
            @Nullable final FilePath workspace, @Nonnull final TaskListener listener,
            @Nonnull final SCMRevisionState baseline) throws IOException, InterruptedException {
        SCMRevisionState myBaseline = baseline;
        final EnvVars env = getEnvVars(null, job);
        final String expandedManifestBranch = env.expand(manifestBranch);
        final Run<?, ?> lastRun = job.getLastBuild();

        if (myBaseline == SCMRevisionState.NONE) {
            // Probably the first build, or possibly an aborted build.
            myBaseline = getLastState(lastRun, expandedManifestBranch);
            if (myBaseline == SCMRevisionState.NONE) {
                return PollingResult.BUILD_NOW;
            }
        }

        FilePath repoDir;
        if (destinationDir != null) {
            repoDir = workspace.child(destinationDir);
        } else {
            repoDir = workspace;
        }

        if (!repoDir.isDirectory()) {
            repoDir.mkdirs();
        }

        if (!checkoutCode(launcher, repoDir, env, listener.getLogger())) {
            // Some error occurred, try a build now so it gets logged.
            return new PollingResult(myBaseline, myBaseline, Change.INCOMPARABLE);
        }

        final RevisionState currentState = new RevisionState(
                getStaticManifest(launcher, repoDir, listener.getLogger(), env),
                getManifestRevision(launcher, repoDir, listener.getLogger(), env), expandedManifestBranch,
                listener.getLogger());

        final Change change;
        if (currentState.equals(myBaseline)) {
            change = Change.NONE;
        } else {
            if (shouldIgnoreChanges(currentState,
                    myBaseline instanceof RevisionState ? (RevisionState) myBaseline : null)) {
                change = Change.NONE;
            } else {
                change = Change.SIGNIFICANT;
            }
        }
        return new PollingResult(myBaseline, currentState, change);
    }

    @Override
    public void checkout(@Nonnull final Run<?, ?> build, @Nonnull final Launcher launcher,
            @Nonnull final FilePath workspace, @Nonnull final TaskListener listener,
            @CheckForNull final File changelogFile, @CheckForNull final SCMRevisionState baseline)
            throws IOException, InterruptedException {

        FilePath repoDir;
        if (destinationDir != null) {
            repoDir = workspace.child(destinationDir);
        } else {
            repoDir = workspace;
        }

        if (!repoDir.isDirectory()) {
            repoDir.mkdirs();
        }

        Job<?, ?> job = build.getParent();
        EnvVars env = build.getEnvironment(listener);
        env = getEnvVars(env, job);
        if (!checkoutCode(launcher, repoDir, env, listener.getLogger())) {
            throw new IOException("Could not checkout");
        }
        final String manifest = getStaticManifest(launcher, repoDir, listener.getLogger(), env);
        final String manifestRevision = getManifestRevision(launcher, repoDir, listener.getLogger(), env);
        final String expandedBranch = env.expand(manifestBranch);
        final RevisionState currentState = new RevisionState(manifest, manifestRevision, expandedBranch,
                listener.getLogger());
        build.addAction(currentState);

        final Run previousBuild = build.getPreviousBuild();
        final SCMRevisionState previousState = getLastState(previousBuild, expandedBranch);

        if (changelogFile != null) {
            ChangeLog.saveChangeLog(currentState,
                    previousState == SCMRevisionState.NONE ? null : (RevisionState) previousState, changelogFile,
                    launcher, repoDir, showAllChanges);
        }
        build.addAction(new TagAction(build));
    }

    private int doSync(final Launcher launcher, final FilePath workspace, final OutputStream logger,
            final EnvVars env) throws IOException, InterruptedException {
        final List<String> commands = new ArrayList<String>(4);
        debug.log(Level.FINE, "Syncing out code in: " + workspace.getName());
        commands.clear();
        if (resetFirst) {
            commands.add(getDescriptor().getExecutable());
            commands.add("forall");
            commands.add("-c");
            commands.add("git reset --hard");
            int syncCode = launcher.launch().stdout(logger).stderr(logger).pwd(workspace).cmds(commands).envs(env)
                    .join();

            if (syncCode != 0) {
                debug.log(Level.WARNING, "Failed to reset first.");
            }
            commands.clear();
        }
        commands.add(getDescriptor().getExecutable());
        if (trace) {
            commands.add("--trace");
        }
        commands.add("sync");
        commands.add("-d");
        if (isCurrentBranch()) {
            commands.add("-c");
        }
        if (isQuiet()) {
            commands.add("-q");
        }
        if (isForceSync()) {
            commands.add("--force-sync");
        }
        if (jobs > 0) {
            commands.add("--jobs=" + jobs);
        }
        if (isNoTags()) {
            commands.add("--no-tags");
        }

        return launcher.launch().stdout(logger).pwd(workspace).cmds(commands).envs(env).join();
    }

    private boolean checkoutCode(final Launcher launcher, final FilePath workspace, final EnvVars env,
            final OutputStream logger) throws IOException, InterruptedException {
        final List<String> commands = new ArrayList<String>(4);

        debug.log(Level.INFO, "Checking out code in: " + workspace.getName());

        commands.add(getDescriptor().getExecutable());
        if (trace) {
            commands.add("--trace");
        }
        commands.add("init");
        commands.add("-u");
        commands.add(env.expand(manifestRepositoryUrl));
        if (manifestBranch != null) {
            commands.add("-b");
            commands.add(env.expand(manifestBranch));
        }
        if (manifestFile != null) {
            commands.add("-m");
            commands.add(env.expand(manifestFile));
        }
        if (mirrorDir != null) {
            commands.add("--reference=" + env.expand(mirrorDir));
        }
        if (repoUrl != null) {
            commands.add("--repo-url=" + env.expand(repoUrl));
            commands.add("--no-repo-verify");
        }
        if (manifestGroup != null) {
            commands.add("-g");
            commands.add(env.expand(manifestGroup));
        }
        if (depth != 0) {
            commands.add("--depth=" + depth);
        }
        int returnCode = launcher.launch().stdout(logger).pwd(workspace).cmds(commands).envs(env).join();
        if (returnCode != 0) {
            return false;
        }
        if (workspace != null) {
            FilePath rdir = workspace.child(".repo");
            FilePath lmdir = rdir.child("local_manifests");
            // Delete the legacy local_manifest.xml in case it exists from a previous build
            rdir.child("local_manifest.xml").delete();
            if (lmdir.exists()) {
                lmdir.deleteContents();
            } else {
                lmdir.mkdirs();
            }
            if (localManifest != null) {
                FilePath lm = lmdir.child("local.xml");
                String expandedLocalManifest = env.expand(localManifest);
                if (expandedLocalManifest.startsWith("<?xml")) {
                    lm.write(expandedLocalManifest, null);
                } else {
                    URL url = new URL(expandedLocalManifest);
                    lm.copyFrom(url);
                }
            }
        }

        returnCode = doSync(launcher, workspace, logger, env);
        if (returnCode != 0) {
            debug.log(Level.WARNING, "Sync failed. Resetting repository");
            commands.clear();
            commands.add(getDescriptor().getExecutable());
            commands.add("forall");
            commands.add("-c");
            commands.add("git reset --hard");
            launcher.launch().stdout(logger).pwd(workspace).cmds(commands).envs(env).join();
            returnCode = doSync(launcher, workspace, logger, env);
            if (returnCode != 0) {
                return false;
            }
        }
        return true;
    }

    private String getStaticManifest(final Launcher launcher, final FilePath workspace, final OutputStream logger,
            final EnvVars env) throws IOException, InterruptedException {
        final ByteArrayOutputStream output = new ByteArrayOutputStream();
        final List<String> commands = new ArrayList<String>(6);
        commands.add(getDescriptor().getExecutable());
        commands.add("manifest");
        commands.add("-o");
        commands.add("-");
        commands.add("-r");
        // TODO: should we pay attention to the output from this?
        launcher.launch().stderr(logger).stdout(output).pwd(workspace).cmds(commands).envs(env).join();
        final String manifestText = output.toString();
        debug.log(Level.FINEST, manifestText);
        return manifestText;
    }

    private String getManifestRevision(final Launcher launcher, final FilePath workspace, final OutputStream logger,
            final EnvVars env) throws IOException, InterruptedException {
        final ByteArrayOutputStream output = new ByteArrayOutputStream();
        final List<String> commands = new ArrayList<String>(6);
        commands.add("git");
        commands.add("rev-parse");
        commands.add("HEAD");
        launcher.launch().stderr(logger).stdout(output).pwd(new FilePath(workspace, ".repo/manifests"))
                .cmds(commands).envs(env).join();
        final String manifestText = output.toString().trim();
        debug.log(Level.FINEST, manifestText);
        return manifestText;
    }

    @Nonnull
    private SCMRevisionState getLastState(final Run<?, ?> lastBuild, final String expandedManifestBranch) {
        if (lastBuild == null) {
            return RevisionState.NONE;
        }
        final RevisionState lastState = lastBuild.getAction(RevisionState.class);
        if (lastState != null && StringUtils.equals(lastState.getBranch(), expandedManifestBranch)) {
            return lastState;
        }
        return getLastState(lastBuild.getPreviousBuild(), expandedManifestBranch);
    }

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

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

    @Nonnull
    @Override
    public String getKey() {
        return new StringBuilder("repo").append(' ').append(getManifestRepositoryUrl()).append(' ')
                .append(getManifestFile()).append(' ').append(getManifestBranch()).toString();
    }

    /**
     * A DescriptorImpl contains variables used server-wide. In our263 case, we
     * only store the path to the repo executable, which defaults to just
     * "repo". This class also handles some Jenkins housekeeping.
     */
    @Extension
    public static class DescriptorImpl extends SCMDescriptor<RepoScm> {
        private String repoExecutable;

        /**
         * Call the superclass constructor and load our configuration from the
         * file system.
         */
        public DescriptorImpl() {
            super(null);
            load();
        }

        @Override
        public String getDisplayName() {
            return "Gerrit Repo";
        }

        @Override
        public boolean configure(final StaplerRequest req, final JSONObject json)
                throws hudson.model.Descriptor.FormException {
            repoExecutable = Util.fixEmptyAndTrim(json.getString("executable"));
            save();
            return super.configure(req, json);
        }

        /**
         * Check that the specified parameter exists on the file system and is a
         * valid executable.
         *
         * @param value
         *            A path to an executable on the file system.
         * @return Error if the file doesn't exist, otherwise return OK.
         */
        public FormValidation doExecutableCheck(@QueryParameter final String value) {
            return FormValidation.validateExecutable(value);
        }

        /**
         * Returns the command to use when running repo. By default, we assume
         * that repo is in the server's PATH and just return "repo".
         */
        public String getExecutable() {
            if (repoExecutable == null) {
                return "repo";
            } else {
                return repoExecutable;
            }
        }

        @Override
        public boolean isApplicable(final Job project) {
            return true;
        }
    }
}