/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Andrew Bayer
*
* 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.cloneworkspace;
import hudson.scm.PollingResult;
import hudson.scm.SCM;
import hudson.scm.ChangeLogParser;
import hudson.scm.SCMDescriptor;
import hudson.scm.SCMRevisionState;
import static hudson.scm.PollingResult.BUILD_NOW;
import static hudson.scm.PollingResult.NO_CHANGES;
import hudson.model.AbstractProject;
import hudson.model.Descriptor;
import hudson.model.TaskListener;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Hudson;
import hudson.model.Result;
import hudson.model.PermalinkProjectAction.Permalink;
import hudson.Launcher;
import hudson.FilePath;
import hudson.WorkspaceSnapshot;
import hudson.PermalinkList;
import hudson.Extension;
import static hudson.Util.fixEmptyAndTrim;
import java.io.IOException;
import java.io.File;
import java.io.Serializable;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.DataBoundConstructor;
/**
* {@link SCM} that inherits the workspace from another build through {@link WorkspaceSnapshot}
* Derived from {@link WorkspaceSnapshotSCM}.
*
* @author Kohsuke Kawaguchi
* @author Andrew Bayer
*/
public class CloneWorkspaceSCM extends SCM {
/**
* The job name from which we inherit the workspace.
*/
public String parentJobName;
/**
* The criteria by which to choose the build to inherit from.
* Can be "Any" (meaning most recent completed build), "Not Failed" (meaning most recent unstable/stable build),
* or "Successful" (meaning most recent stable build).
*/
public String criteria;
@DataBoundConstructor
public CloneWorkspaceSCM(String parentJobName, String criteria) {
this.parentJobName = parentJobName;
this.criteria = criteria;
}
/**
* Obtains the {@link WorkspaceSnapshot} object that this {@link SCM} points to,
* or throws {@link ResolvedFailedException} upon failing.
*
* @return never null.
*/
public Snapshot resolve() throws ResolvedFailedException {
Hudson h = Hudson.getInstance();
AbstractProject<?,?> job = h.getItemByFullName(parentJobName, AbstractProject.class);
if(job==null) {
if(h.getItemByFullName(parentJobName)==null) {
AbstractProject nearest = AbstractProject.findNearest(parentJobName);
throw new ResolvedFailedException(Messages.CloneWorkspaceSCM_NoSuchJob(parentJobName,nearest.getFullName()));
} else
throw new ResolvedFailedException(Messages.CloneWorkspaceSCM_IncorrectJobType(parentJobName));
}
AbstractBuild<?,?> b = CloneWorkspaceUtil.getMostRecentBuildForCriteria(job,criteria);
if(b==null)
throw new ResolvedFailedException(Messages.CloneWorkspaceSCM_NoBuild(criteria,parentJobName));
WorkspaceSnapshot snapshot = b.getAction(WorkspaceSnapshot.class);
if(snapshot==null)
throw new ResolvedFailedException(Messages.CloneWorkspaceSCM_NoWorkspace(parentJobName,criteria));
return new Snapshot(snapshot,b);
}
@Override
public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException {
try {
workspace.deleteContents();
resolve().restoreTo(workspace,listener);
// write out the parent build number file
PrintWriter w = new PrintWriter(new FileOutputStream(getParentBuildFile(build)));
try {
w.println(resolve().getParent().getNumber());
} finally {
w.close();
}
return calcChangeLog(resolve().getParent(), changelogFile, listener);
} catch (ResolvedFailedException e) {
listener.error(e.getMessage()); // stack trace is meaningless
build.setResult(Result.FAILURE);
return false;
}
}
/**
* Called after checkout has finished to copy the changelog from the parent build.
*/
private boolean calcChangeLog(AbstractBuild<?,?> parentBuild, File changelogFile, BuildListener listener) throws IOException, InterruptedException {
FilePath parentChangeLog = new FilePath(new File(parentBuild.getRootDir(), "changelog.xml"));
if (parentChangeLog.exists()) {
FilePath childChangeLog = new FilePath(changelogFile);
parentChangeLog.copyTo(childChangeLog);
}
else {
createEmptyChangeLog(changelogFile, listener, "log");
}
return true;
}
@Override
public ChangeLogParser createChangeLogParser() {
try {
return resolve().getParent().getProject().getScm().createChangeLogParser();
} catch (ResolvedFailedException e) {
return null;
}
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl)super.getDescriptor();
}
public static File getParentBuildFile(AbstractBuild b) {
return new File(b.getRootDir(),"cloneWorkspaceParent.txt");
}
/**
* Reads the parent build file of the specified build (or the closest, if the flag is so specified.)
*
* @param findClosest
* If true, this method will go back the build history until it finds a parent build file.
* A build may not have a parent build file for any number of reasons (such as failure, interruption, etc.)
* @return
* Number of parent build
*/
private int parseParentBuildFile(AbstractBuild<?,?> build, boolean findClosest) throws IOException {
int parentBuildNumber = 0; // Default to 0, so that if we don't actually find a build,
// polling et al will return true.
// If the build itself is null, just return the default.
if (build==null)
return parentBuildNumber;
if (findClosest) {
for (AbstractBuild<?,?> b=build; b!=null; b=b.getPreviousBuild()) {
if(getParentBuildFile(b).exists()) {
build = b;
break;
}
}
}
{// read the parent build file of the build
File file = getParentBuildFile(build);
if(!file.exists())
// nothing to compare against
return parentBuildNumber;
BufferedReader br = new BufferedReader(new FileReader(file));
try {
String line;
while((line=br.readLine())!=null) {
try {
parentBuildNumber = Integer.parseInt(fixEmptyAndTrim(line));
} catch (NumberFormatException e) {
// perhaps a corrupted line. ignore
}
}
} finally {
br.close();
}
}
return parentBuildNumber;
}
@Override
public SCMRevisionState calcRevisionsFromBuild(AbstractBuild build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
// exclude locations that are svn:external-ed with a fixed revision.
int parentBuildNumber = parseParentBuildFile(build,true);
return new CloneWorkspaceSCMRevisionState(parentBuildNumber);
}
@Override
protected PollingResult compareRemoteRevisionWith(AbstractProject<?,?> project, Launcher launcher, FilePath workspace, final TaskListener listener, SCMRevisionState _baseline) throws IOException, InterruptedException {
Hudson h = Hudson.getInstance();
AbstractProject<?,?> parentProject = h.getItemByFullName(parentJobName, AbstractProject.class);
if (parentProject==null) {
// Disable this project if the parent project no longer exists or doesn't exist in the first place.
listener.getLogger().println("The CloneWorkspace parent project for " + project + " does not exist, project will be disabled.");
project.makeDisabled(true);
return new PollingResult(_baseline, _baseline, PollingResult.Change.NONE);
}
final CloneWorkspaceSCMRevisionState baseline = (CloneWorkspaceSCMRevisionState)_baseline;
Snapshot s = null;
try {
s = resolve();
} catch (ResolvedFailedException e) {
listener.getLogger().println(e.getMessage());
return new PollingResult(baseline, baseline, PollingResult.Change.NONE);
// return NO_CHANGES;
}
if (s==null) {
listener.getLogger().println("Snapshot failed to resolve for unknown reasons.");
return new PollingResult(baseline, baseline, PollingResult.Change.NONE);
// return NO_CHANGES;
}
else {
if (s.getParent().getNumber() > baseline.parentBuildNumber) {
listener.getLogger().println("Build #" + s.getParent().getNumber() + " of project " + parentJobName
+ " is newer than build #" + baseline.parentBuildNumber + ", so a new build of "
+ project + " will be run.");
return new PollingResult(baseline, new CloneWorkspaceSCMRevisionState(s.getParent().getNumber()), PollingResult.Change.SIGNIFICANT);
// return BUILD_NOW;
}
else {
listener.getLogger().println("Build #" + s.getParent().getNumber() + " of project " + parentJobName
+ " is NOT newer than build #" + baseline.parentBuildNumber + ", so no new build of "
+ project + " will be run.");
return new PollingResult(baseline, baseline, PollingResult.Change.NONE);
// return NO_CHANGES;
}
}
}
@Extension
public static class DescriptorImpl extends SCMDescriptor<CloneWorkspaceSCM> {
public DescriptorImpl() {
super(CloneWorkspaceSCM.class, null);
load();
}
@Override
public String getDisplayName() {
return Messages.CloneWorkspaceSCM_DisplayName();
}
@Override
public SCM newInstance(StaplerRequest req, JSONObject formData) throws FormException {
return req.bindJSON(CloneWorkspaceSCM.class, formData);
}
public List<String> getEligibleParents() {
List<String> parentNames = new ArrayList<String>();
for (AbstractProject p : Hudson.getInstance().getItems(AbstractProject.class)) {
if (p.getPublishersList().get(CloneWorkspacePublisher.class) != null) {
parentNames.add(p.getDisplayName());
}
}
return parentNames;
}
}
public final class CloneWorkspaceSCMRevisionState extends SCMRevisionState implements Serializable {
final int parentBuildNumber;
CloneWorkspaceSCMRevisionState(int parentBuildNumber) {
this.parentBuildNumber = parentBuildNumber;
}
private static final long serialVersionUID = 1L;
}
/**
* {@link Exception} indicating that the resolution of the job/build failed.
*/
private final class ResolvedFailedException extends Exception {
private ResolvedFailedException(String message) {
super(message);
}
}
private static class Snapshot {
final WorkspaceSnapshot snapshot;
final AbstractBuild<?,?> parent;
private Snapshot(WorkspaceSnapshot snapshot, AbstractBuild<?,?> parent) {
this.snapshot = snapshot;
this.parent = parent;
}
void restoreTo(FilePath dst,TaskListener listener) throws IOException, InterruptedException {
snapshot.restoreTo(parent,dst,listener);
}
AbstractBuild<?,?> getParent() {
return parent;
}
}
private static final Logger LOGGER = Logger.getLogger(CloneWorkspaceSCM.class.getName());
}