package hudson.plugins.svnmerge; import hudson.Extension; import hudson.FilePath.FileCallable; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Hudson; import hudson.model.Item; import hudson.model.JobProperty; import hudson.model.JobPropertyDescriptor; import hudson.model.TaskListener; import hudson.model.listeners.ItemListener; import hudson.remoting.VirtualChannel; import hudson.scm.SCM; import hudson.scm.SubversionEventHandlerImpl; import hudson.scm.SubversionSCM; import hudson.scm.SubversionSCM.ModuleLocation; import hudson.util.IOException2; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import org.tmatesoft.svn.core.SVNCommitInfo; import static org.tmatesoft.svn.core.SVNDepth.INFINITY; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider; import org.tmatesoft.svn.core.wc.ISVNEventHandler; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNCommitClient; import org.tmatesoft.svn.core.wc.SVNDiffClient; import org.tmatesoft.svn.core.wc.SVNEvent; import org.tmatesoft.svn.core.wc.SVNInfo; import org.tmatesoft.svn.core.wc.SVNRevision; import static org.tmatesoft.svn.core.wc.SVNRevision.HEAD; import org.tmatesoft.svn.core.wc.SVNStatusType; import org.tmatesoft.svn.core.wc.SVNUpdateClient; import org.tmatesoft.svn.core.wc.SVNWCClient; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * {@link JobProperty} for feature branch projects. * <p> * This associates the upstream project (with {@link IntegratableProject} with this project. * * @author Kohsuke Kawaguchi */ public class FeatureBranchProperty extends JobProperty<AbstractProject<?,?>> { /** * Upstream job name. */ private String upstream; @DataBoundConstructor public FeatureBranchProperty(String upstream) { if (upstream == null) { throw new NullPointerException("upstream"); } this.upstream = upstream; } public String getUpstream() { return upstream; } /** * Gets the upstream project, or null if no such project was found. */ public AbstractProject<?,?> getUpstreamProject() { return Hudson.getInstance().getItemByFullName(upstream,AbstractProject.class); } public ModuleLocation getUpstreamSubversionLocation() { AbstractProject<?,?> p = getUpstreamProject(); if(p==null) return null; SCM scm = p.getScm(); if (scm instanceof SubversionSCM) { SubversionSCM svn = (SubversionSCM) scm; return svn.getLocations()[0]; } return null; } /** * Gets the {@link #getUpstreamSubversionLocation()} as {@link SVNURL} */ public SVNURL getUpstreamURL() throws SVNException { ModuleLocation location = getUpstreamSubversionLocation(); if(location==null) return null; return location.getSVNURL(); } public AbstractProject<?,?> getOwner() { return owner; } @Override public IntegrationStatusAction getJobAction(AbstractProject<?,?> _) { return new IntegrationStatusAction(this); } /** * Just add the integration action. */ @Override public boolean prebuild(AbstractBuild<?, ?> build, BuildListener listener) { build.addAction(new IntegrateAction(build)); return true; } /** * Perform a merge to the upstream and integrate changes in this branch. * * <p> * This computation uses the workspace of the project. * * @param listener * Where the progress is sent. * @param branchURL * URL of the branch to be integrated. If null, use the workspace URL. * @param branchRev * Revision of the branch to be integrated to the upstream. * If -1, use the current workspace revision. * @return * the new revision number if the integration was successful. * -1 if it failed and the failure was handled gracefully * (typically this means a merge conflict.) */ public long integrate(final TaskListener listener, final String branchURL, final long branchRev, final String commitMessage) throws IOException, InterruptedException { final ISVNAuthenticationProvider provider = Hudson.getInstance().getDescriptorByType( SubversionSCM.DescriptorImpl.class).createAuthenticationProvider(); return owner.getModuleRoot().act(new FileCallable<Long>() { public Long invoke(File mr, VirtualChannel virtualChannel) throws IOException { try { final PrintStream logger = listener.getLogger(); final boolean[] foundConflict = new boolean[1]; ISVNEventHandler printHandler = new SubversionEventHandlerImpl(logger,mr) { @Override public void handleEvent(SVNEvent event, double progress) throws SVNException { super.handleEvent(event, progress); if(event.getContentsStatus()== SVNStatusType.CONFLICTED) foundConflict[0] = true; } }; SVNURL up = getUpstreamURL(); SVNClientManager cm = SubversionSCM.createSvnClientManager(provider); cm.setEventHandler(printHandler); // capture the working directory state before the switch SVNWCClient wc = cm.getWCClient(); SVNInfo wsState = wc.doInfo(mr, null); logger.println("Switching to the upstream (" + up+")"); SVNUpdateClient uc = cm.getUpdateClient(); uc.doSwitch(mr, up, HEAD, HEAD, INFINITY, false, true); SVNURL mergeUrl = branchURL != null ? SVNURL.parseURIDecoded(branchURL) : wsState.getURL(); SVNRevision mergeRev = branchRev >= 0 ? SVNRevision.create(branchRev) : wsState.getRevision(); logger.printf("Merging %s (rev.%s) to the upstream\n",mergeUrl,mergeRev); SVNDiffClient dc = cm.getDiffClient(); dc.doMergeReIntegrate( mergeUrl, mergeRev, mr, false); SVNCommitInfo ci=null; if(foundConflict[0]) { logger.println("Found conflict with the upstream. Reverting this failed merge"); wc.doRevert(new File[]{mr},INFINITY, null); } else { logger.println("Committing changes to the upstream"); SVNCommitClient cc = cm.getCommitClient(); ci = cc.doCommit(new File[]{mr}, false, commitMessage, null, null, false, false, INFINITY); if(ci.getNewRevision()<0) logger.println(" No changes since the last integration"); else logger.println(" committed revision "+ci.getNewRevision()); } logger.println("Switching back to the branch (" + wsState.getURL()+"@"+wsState.getRevision()+")"); uc.doSwitch(mr, wsState.getURL(), wsState.getRevision(), wsState.getRevision(), INFINITY, false, true); if(foundConflict[0]) { logger.println("Conflict found. Please sync with the upstream to resolve this error."); return -1L; } else { // -1 is returned if there was no commit, so normalize that to 0 return Math.max(0,ci.getNewRevision()); } } catch (SVNException e) { throw new IOException2("Failed to merge", e); } } }); } /** * If an upstream is renamed, update the configuration accordingly. */ @Extension public static class ItemListenerImpl extends ItemListener { @Override public void onRenamed(Item item, String oldName, String newName) { if (item instanceof AbstractProject) { AbstractProject<?,?> up = (AbstractProject) item; if(up.getProperty(IntegratableProject.class)!=null) { try { for (AbstractProject<?,?> p : Hudson.getInstance().getItems(AbstractProject.class)) { FeatureBranchProperty fbp = p.getProperty(FeatureBranchProperty.class); if(fbp!=null) { if(fbp.upstream.equals(oldName)) { fbp.upstream=newName; p.save(); } } } } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to persist configuration",e); } } } } } @Extension public static final class DescriptorImpl extends JobPropertyDescriptor { @Override public JobProperty<?> newInstance(StaplerRequest req, JSONObject formData) throws FormException { if(!formData.has("svnmerge")) return null; return req.bindJSON(FeatureBranchProperty.class,formData.getJSONObject("svnmerge")); } public String getDisplayName() { return "Upstream Subversion branch"; } public List<AbstractProject<?,?>> listIntegratableProjects() { List<AbstractProject<?,?>> r = new ArrayList<AbstractProject<?,?>>(); for(AbstractProject<?,?> p : Hudson.getInstance().getItems(AbstractProject.class)) if(p.getProperty(IntegratableProject.class)!=null) r.add(p); return r; } } private static final Logger LOGGER = Logger.getLogger(FeatureBranchProperty.class.getName()); }