package hudson.plugins.svnmerge;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildBadgeAction;
import hudson.model.Fingerprint;
import hudson.model.Fingerprint.RangeSet;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.Label;
import hudson.model.Node;
import hudson.model.Queue;
import hudson.model.ResourceList;
import hudson.model.TaskAction;
import hudson.model.TaskListener;
import hudson.model.TaskThread;
import hudson.remoting.AsyncFutureImpl;
import hudson.scm.ChangeLogSet.Entry;
import hudson.scm.SubversionChangeLogSet.LogEntry;
import hudson.scm.SubversionSCM.SvnInfo;
import hudson.scm.SubversionTagAction;
import hudson.security.ACL;
import hudson.security.Permission;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.framework.io.LargeText;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import org.acegisecurity.AccessDeniedException;
/**
* {@link AbstractBuild}-level action to integrate
* the build to upstream branch.
*
* @author Kohsuke Kawaguchi
*/
public class IntegrateAction extends TaskAction implements BuildBadgeAction {
public final AbstractBuild<?,?> build;
/**
* If the integration is successful, set to the revision of the commit of the merge.
* If the integration is successful but there was nothing to merge, 0.
* If it failed, -1. If an integration was never attempted, null.
*/
private Long integratedRevision;
public IntegrateAction(AbstractBuild<?,?> build) {
this.build = build;
}
public String getIconFileName() {
if(!isApplicable()) return null; // missing configuration
return "/plugin/svnmerge/24x24/integrate.gif";
}
public String getDisplayName() {
return "Integrate Branch";
}
public String getUrlName() {
return "integrate-branch";
}
protected Permission getPermission() {
return Item.CONFIGURE;
}
protected ACL getACL() {
return build.getACL();
}
/**
* Do we have enough information to perform integration?
* If not, we need to pretend as if this action is not here.
*/
private boolean isApplicable() {
return getSvnInfo()!=null && getProperty()!=null;
}
public FeatureBranchProperty getProperty() {
return build.getProject().getProperty(FeatureBranchProperty.class);
}
public boolean isIntegrated() {
return integratedRevision!=null && integratedRevision>=0;
}
public boolean isIntegrationAttempted() {
return integratedRevision!=null;
}
public Long getIntegratedRevision() {
return integratedRevision;
}
@Override
public LargeText getLog() {
return new LargeText(getLogFile(),workerThread==null);
}
private File getLogFile() {
return new File(build.getRootDir(),"integrate.log");
}
/**
* URL and revision to be integrated from this action.
*/
public SvnInfo getSvnInfo() {
SubversionTagAction sta = build.getAction(SubversionTagAction.class);
if(sta==null) return null;
Map<SvnInfo,List<String>> tags = sta.getTags();
if(tags.size()!=1) return null; // can't handle more than 1 URLs
return tags.keySet().iterator().next();
}
/**
* Integrate the branch.
* <p>
* This requires that the calling thread owns the workspace.
*/
/*package*/ long integrate(TaskListener listener) throws IOException, InterruptedException {
SvnInfo si = getSvnInfo();
String commitMessage = getCommitMessage();
integratedRevision = getProperty().integrate(listener, si.url, si.revision, commitMessage);
if(integratedRevision>0) {
// record this integration as a fingerprint.
// this will allow us to find where this change is integrated.
Hudson.getInstance().getFingerprintMap().getOrCreate(
build, IntegrateAction.class.getName(),
getFingerprintKey());
}
build.save();
return integratedRevision;
}
/**
* Gets the build number of the upstream where this integration is built.
*
* <p>
* Since the relevant information might be already lost when this method
* is called, this code needs to be defensive.
*
* @return -1
* if not integrated yet or this information is lost.
*/
public int getUpstreamBuildNumber() throws IOException {
Fingerprint f = Hudson.getInstance().getFingerprintMap().get(getFingerprintKey());
if(f==null) return -1;
FeatureBranchProperty p = getProperty();
RangeSet rs = new RangeSet(); // empty range set
if(p!=null)
rs = f.getRangeSet(p.getUpstreamProject());
else {
// we don't know for sure what is our upstream project.
Hashtable<String,RangeSet> usages = f.getUsages();
if(!usages.isEmpty())
rs = usages.values().iterator().next();
}
if(rs.isEmpty()) return -1;
return rs.min();
}
/**
* This is the md5 hash to keep track of where this change is integrated.
*/
public String getFingerprintKey() {
return Util.getDigestOf(getCommitMessage()+"#"+integratedRevision);
}
private String getCommitMessage() {
return COMMIT_MESSAGE_PREFIX + build.getFullDisplayName()+ COMMIT_MESSAGE_SUFFIX;
}
/**
* Schedules the integration of this branch to the upstream.
*
* <p>
* This happens asynchronously.
*/
public Future<WorkerThread> integrateAsync() throws IOException {
getACL().checkPermission(getPermission());
IntegrateAction.IntegrationTask task = new IntegrationTask();
Hudson.getInstance().getQueue().add(task, 0);
return task.future;
}
public synchronized void doIntegrate(StaplerResponse rsp) throws IOException, ServletException {
integrateAsync();
rsp.sendRedirect(".");
}
public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
req.getView(this, decidePage()).forward(req,rsp);
}
/**
* Cancels an integration task in the queue, if any.
*/
public void doCancelQueue(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
build.getProject().checkPermission(AbstractProject.BUILD);
Hudson.getInstance().getQueue().cancel(new IntegrationTask());
rsp.forwardToPreviousPage(req);
}
/**
* Which page to render?
*/
private String decidePage() {
if(isIntegrated()) return "completed.jelly";
if (workerThread != null) return "inProgress.jelly";
return "form.jelly";
}
public final class WorkerThread extends TaskThread {
public WorkerThread() throws IOException {
super(IntegrateAction.this, ListenerAndText.forFile(getLogFile()));
associateWith(IntegrateAction.this);
}
protected void perform(TaskListener listener) throws Exception {
integrate(listener);
}
}
/**
* {@link Task} that performs the integration.
*/
private class IntegrationTask implements Queue.Task {
private final AsyncFutureImpl<WorkerThread> future = new AsyncFutureImpl<WorkerThread>();
private final WorkerThread thread;
public IntegrationTask() throws IOException {
// do this now so that this gets tied with the action.
thread = new WorkerThread();
}
/**
* This has to run on the last workspace.
*/
@Override
public Label getAssignedLabel() {
Node node = getLastBuiltOn();
return node != null ? node.getSelfLabel() : null;
}
@Override
public String getFullDisplayName() {
return getProject().getFullDisplayName()+" Integration";
}
@Override
public long getEstimatedDuration() {
return -1;
}
@Override
public Queue.Executable createExecutable() throws IOException {
return new Queue.Executable() {
public Queue.Task getParent() {
return IntegrationTask.this;
}
public void run() {
// run this synchronously
try {
thread.run();
} finally {
future.set(thread);
}
}
};
}
@Override
public String getDisplayName() {
return getProject().getDisplayName()+" Integration";
}
/**
* Exclusive access to the workspace required.
*/
@Override
public ResourceList getResourceList() {
return new ResourceList().w(getProject().getWorkspaceResource());
}
private AbstractProject<?,?> getProject() {
return build.getProject();
}
@Override
public int hashCode() {
return getProject().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof IntegrationTask) {
IntegrationTask that = (IntegrationTask) obj;
return this.getProject()==that.getProject();
}
return false;
}
public String getUrl() {
return getProject().getUrl()+getUrlName();
}
public void doCancelQueue(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
IntegrateAction.this.doCancelQueue(req,rsp);
}
public Node getLastBuiltOn() {
return null;
}
public boolean isBuildBlocked() {
return false;
}
public String getWhyBlocked() {
return null;
}
public String getName() {
return getDisplayName();
}
public void checkAbortPermission() {
if (!hasAbortPermission()) {
throw new AccessDeniedException("???");
}
}
public boolean hasAbortPermission() {
// XXX Hudson.getInstance().getAuthorizationStrategy().getACL(...).hasPermission(...)
return true;
}
}
/**
* Checks if the given {@link Entry} represents a commit from
* {@linkplain #integrate(TaskListener) integration}. If so,
* return its fingerprint.
*
* Otherwise null.
*/
public static Fingerprint getIntegrationFingerprint(Entry changeEntry) throws IOException {
if (changeEntry instanceof LogEntry) {
LogEntry le = (LogEntry) changeEntry;
String msg = changeEntry.getMsg().trim();
if(msg.startsWith(COMMIT_MESSAGE_PREFIX) && msg.endsWith(COMMIT_MESSAGE_SUFFIX)) {
// this build is merging an integration. Leave this in the record
return Hudson.getInstance().getFingerprintMap().get(Util.getDigestOf(msg + "#" + le.getRevision()));
}
}
return null;
}
// used to find integration commits
static final String COMMIT_MESSAGE_PREFIX = "Integrated ";
static final String COMMIT_MESSAGE_SUFFIX = " (from Hudson)";
}