diff --git a/src/main/java/jenkins/plugins/svnmerge/AbstractSvnmergeTaskAction.java b/src/main/java/jenkins/plugins/svnmerge/AbstractSvnmergeTaskAction.java index ca95997..c975324 100644 --- a/src/main/java/jenkins/plugins/svnmerge/AbstractSvnmergeTaskAction.java +++ b/src/main/java/jenkins/plugins/svnmerge/AbstractSvnmergeTaskAction.java @@ -27,6 +27,7 @@ import java.io.File; import java.io.IOException; +import java.util.List; import java.util.concurrent.Future; /** @@ -90,7 +91,7 @@ public synchronized void doPerform(StaplerRequest req, StaplerResponse rsp) thro *

* This requires that the calling thread owns the workspace. */ - /*package*/ abstract long perform(TaskListener listener, P param) throws IOException, InterruptedException; + /*package*/ abstract List perform(TaskListener listener, P param) throws IOException, InterruptedException; /** * Which page to render in the top page? diff --git a/src/main/java/jenkins/plugins/svnmerge/FeatureBranchProperty.java b/src/main/java/jenkins/plugins/svnmerge/FeatureBranchProperty.java index 4e227ee..f8ee8b4 100644 --- a/src/main/java/jenkins/plugins/svnmerge/FeatureBranchProperty.java +++ b/src/main/java/jenkins/plugins/svnmerge/FeatureBranchProperty.java @@ -2,6 +2,7 @@ import hudson.EnvVars; import hudson.Extension; +import hudson.FilePath; import hudson.FilePath.FileCallable; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; @@ -95,16 +96,24 @@ public AbstractProject getUpstreamProject() { return Jenkins.getInstance().getItemByFullName(upstream,AbstractProject.class); } - public ModuleLocation getUpstreamSubversionLocation() { + /** + * + * @return All existing SVN Modules in project. + */ + public List getUpstreamSubversionLocation() { + List toReturn = new ArrayList(); AbstractProject p = getUpstreamProject(); if(p==null) return null; SCM scm = p.getScm(); if (scm instanceof SubversionSCM) { SubversionSCM svn = (SubversionSCM) scm; - ModuleLocation ml = svn.getLocations()[0]; - // expand system and node environment variables as well as the project parameters - ml = Utility.getExpandedLocation(ml, p); - return ml; + for(ModuleLocation ml : svn.getLocations()){ + // expand system and node environment variables as well as the project parameters + ml = Utility.getExpandedLocation(ml, p); + toReturn.add(ml); + } + + return toReturn; } return null; } @@ -112,10 +121,14 @@ public ModuleLocation getUpstreamSubversionLocation() { /** * Gets the {@link #getUpstreamSubversionLocation()} as {@link SVNURL} */ - public SVNURL getUpstreamURL() throws SVNException { - ModuleLocation location = getUpstreamSubversionLocation(); - if(location==null) return null; - return location.getSVNURL(); + public List getUpstreamURL() throws SVNException { + List toReturn = new ArrayList(); + List locations = getUpstreamSubversionLocation(); + for(ModuleLocation ml : locations){ + if(ml!=null) + toReturn.add(ml.getSVNURL()); + } + return toReturn; } public AbstractProject getOwner() { @@ -157,86 +170,110 @@ public boolean prebuild(AbstractBuild build, BuildListener listener) { * -1 if it failed and the failure was handled gracefully * (typically this means a merge conflict.) */ - public long rebase(final TaskListener listener, final long upstreamRev) throws IOException, InterruptedException { + public List rebase(final TaskListener listener, final long upstreamRev) throws IOException, InterruptedException { + List toReturn = new ArrayList(); final SubversionSCM svn = (SubversionSCM) getOwner().getScm(); + final ISVNAuthenticationProvider provider = svn.createAuthenticationProvider(getOwner(), svn.getLocations()[0]); - final ModuleLocation upstreamLocation = getUpstreamSubversionLocation(); + final List upstreamLocations = getUpstreamSubversionLocation(); AbstractBuild build = owner.getSomeBuildWithWorkspace(); if (build == null) { final PrintStream logger = listener.getLogger(); logger.print("No workspace found for project! Please perform a build first.\n"); - return -1L; + return null; } - return build.getModuleRoot().act(new FileCallable() { - 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 - || event.getContentsStatus() == SVNStatusType.CONFLICTED_UNRESOLVED) { - foundConflict[0] = true; - } + + FilePath[] svnModules = build.getModuleRoots(); + for(int i = 0; i < svnModules.length; i++){ + //There seems to be no way to match a subversion module url to job's local path + //So use indexes and pray that "svnModules" will match the order of "upstreamLocations" + final int currentIndex = i; + FilePath module = svnModules[i]; + List results = module.act(new FileCallable>() { + public List invoke(File mr, VirtualChannel virtualChannel) throws IOException { + List toReturn = new ArrayList(); + 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 + || event.getContentsStatus() == SVNStatusType.CONFLICTED_UNRESOLVED) { + foundConflict[0] = true; + } + } + }; + + SvnClientManager svnm = SubversionSCM.createClientManager(provider); + ModuleLocation currentLocation = upstreamLocations.get(currentIndex); + + SVNURL up = currentLocation == null ? null : currentLocation.getSVNURL(); + SVNClientManager cm = svnm.getCore(); + cm.setEventHandler(printHandler); + + SVNWCClient wc = cm.getWCClient(); + SVNDiffClient dc = cm.getDiffClient(); + + logger.printf("Updating workspace to the latest revision\n"); + long wsr = cm.getUpdateClient().doUpdate(mr, HEAD, INFINITY, false, false); +// logger.printf(" Updated to rev.%d\n",wsr); // reported by printHandler + + SVNRevision mergeRev = upstreamRev >= 0 ? SVNRevision.create(upstreamRev) : cm.getWCClient().doInfo(up,HEAD,HEAD).getCommittedRevision(); + + logger.printf("Merging change from the upstream %s at rev.%s\n",up,mergeRev); + SVNRevisionRange r = new SVNRevisionRange(SVNRevision.create(0),mergeRev); + dc.doMerge(up, mergeRev, Arrays.asList(r), mr, INFINITY, true, false, false, false); + if(foundConflict[0]) { + logger.println("Found conflict. Reverting this failed merge"); + wc.doRevert(new File[]{mr},INFINITY, null); + return null; + } else { + try { + logger.println("Committing changes"); + SVNCommitClient cc = cm.getCommitClient(); + SVNCommitInfo ci = cc.doCommit(new File[] { mr }, + false, RebaseAction.COMMIT_MESSAGE_PREFIX + + "Rebasing from " + up + "@" + + mergeRev, null, null, false, + false, INFINITY); + if (ci.getNewRevision() < 0) { + logger.println(" No changes since the last rebase. This rebase was a no-op."); + toReturn.add(0L); + } else { + logger.println(" committed revision " + + ci.getNewRevision()); + toReturn.add(ci.getNewRevision()); + } + } catch (SVNException e) { + logger.println("Failed to commit!"); + logger.println(e.getLocalizedMessage()); + logger.println("Reverting this failed merge."); + wc.doRevert(new File[] { mr }, INFINITY, null); + return null; + } } - }; - - SvnClientManager svnm = SubversionSCM.createClientManager(provider); - - SVNURL up = upstreamLocation == null ? null : upstreamLocation.getSVNURL(); - SVNClientManager cm = svnm.getCore(); - cm.setEventHandler(printHandler); - - SVNWCClient wc = cm.getWCClient(); - SVNDiffClient dc = cm.getDiffClient(); - - logger.printf("Updating workspace to the latest revision\n"); - long wsr = cm.getUpdateClient().doUpdate(mr, HEAD, INFINITY, false, false); -// logger.printf(" Updated to rev.%d\n",wsr); // reported by printHandler - - SVNRevision mergeRev = upstreamRev >= 0 ? SVNRevision.create(upstreamRev) : cm.getWCClient().doInfo(up,HEAD,HEAD).getCommittedRevision(); - - logger.printf("Merging change from the upstream %s at rev.%s\n",up,mergeRev); - SVNRevisionRange r = new SVNRevisionRange(SVNRevision.create(0),mergeRev); - dc.doMerge(up, mergeRev, Arrays.asList(r), mr, INFINITY, true, false, false, false); - if(foundConflict[0]) { - logger.println("Found conflict. Reverting this failed merge"); - wc.doRevert(new File[]{mr},INFINITY, null); - return -1L; - } else { - try { - logger.println("Committing changes"); - SVNCommitClient cc = cm.getCommitClient(); - SVNCommitInfo ci = cc.doCommit(new File[] { mr }, - false, RebaseAction.COMMIT_MESSAGE_PREFIX - + "Rebasing from " + up + "@" - + mergeRev, null, null, false, - false, INFINITY); - if (ci.getNewRevision() < 0) { - logger.println(" No changes since the last rebase. This rebase was a no-op."); - return 0L; - } else { - logger.println(" committed revision " - + ci.getNewRevision()); - return ci.getNewRevision(); - } - } catch (SVNException e) { - logger.println("Failed to commit!"); - logger.println(e.getLocalizedMessage()); - logger.println("Reverting this failed merge."); - wc.doRevert(new File[] { mr }, INFINITY, null); - return -1L; - } + + } catch (SVNException e) { + throw new IOException2("Failed to merge", e); } - } catch (SVNException e) { - throw new IOException2("Failed to merge", e); + return toReturn; } + }); + + //If at least one result is null, it means that we have a conflict. So exit for loop and return null + if(results != null){ + toReturn.addAll(results); + } else { + toReturn = null; + break; } - }); + } + + return toReturn; } /** @@ -273,146 +310,169 @@ public IntegrationResult(long mergeCommit, SVNRevision integrationSource) { * * @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. + * @param svnInfo + * Contains: + * - URL of the branch to be integrated. If null, use the workspace URL. + * - Revision of the branch to be integrated to the upstream. + * If -1, use the current workspace revision. * @return * Always non-null. See {@link IntegrationResult} */ - public IntegrationResult integrate(final TaskListener listener, final String branchURL, final long branchRev, final String commitMessage) throws IOException, InterruptedException { + public List integrate(final TaskListener listener, final List svnInfo, final String commitMessage) throws IOException, InterruptedException { + List toReturn = new ArrayList(); final Long lastIntegrationSourceRevision = getlastIntegrationSourceRevision(); final SubversionSCM svn = (SubversionSCM) getUpstreamProject().getScm(); final ISVNAuthenticationProvider provider = svn.createAuthenticationProvider(getUpstreamProject(), svn.getLocations()[0]); - final ModuleLocation upstreamLocation = getUpstreamSubversionLocation(); - - return owner.getModuleRoot().act(new FileCallable() { - public IntegrationResult 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; - } - }; - - SvnClientManager svnm = SubversionSCM.createClientManager(provider); - - SVNURL up = upstreamLocation == null ? null : upstreamLocation.getSVNURL(); - SVNClientManager cm = svnm.getCore(); - cm.setEventHandler(printHandler); - - // capture the working directory state before the switch - SVNWCClient wc = cm.getWCClient(); - SVNInfo wsState = wc.doInfo(mr, null); - SVNURL mergeUrl = branchURL != null ? SVNURL.parseURIDecoded(branchURL) : wsState.getURL(); - SVNRevision mergeRev = branchRev >= 0 ? SVNRevision.create(branchRev) : wsState.getRevision(); - - // do we have any meaningful changes in this branch worthy of integration? - if (lastIntegrationSourceRevision !=null) { - final MutableBoolean changesFound = new MutableBoolean(false); - cm.getLogClient().doLog(new File[]{mr},mergeRev,SVNRevision.create(lastIntegrationSourceRevision),mergeRev,true,false,-1,new ISVNLogEntryHandler() { - public void handleLogEntry(SVNLogEntry e) throws SVNException { - if (!changesFound.booleanValue()) { - String message = e.getMessage(); - - if (!message.startsWith(RebaseAction.COMMIT_MESSAGE_PREFIX) - && !message.startsWith(IntegrateAction.COMMIT_MESSAGE_PREFIX)) { - changesFound.setValue(true); + final List upstreamLocations = getUpstreamSubversionLocation(); + + FilePath[] svnModules = owner.getModuleRoots(); + for(int i = 0; i < svnModules.length; i++){ + //There seems to be no way to match a subversion module url to job's local path + //So use indexes and pray that "svnModules" will match the order of "upstreamLocations" + final int currentIndex = i; + FilePath module = svnModules[i]; + final String branchURL = svnInfo.get(i).url; + final long branchRev = svnInfo.get(i).revision; + + IntegrationResult result = module.act(new FileCallable() { + public IntegrationResult 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; + } + }; + + SvnClientManager svnm = SubversionSCM.createClientManager(provider); + + ModuleLocation currentLocation = upstreamLocations.get(currentIndex); + SVNURL up = currentLocation == null ? null : currentLocation.getSVNURL(); + SVNClientManager cm = svnm.getCore(); + cm.setEventHandler(printHandler); + + // capture the working directory state before the switch + SVNWCClient wc = cm.getWCClient(); + SVNInfo wsState = wc.doInfo(mr, null); + SVNURL mergeUrl = branchURL != null ? SVNURL.parseURIDecoded(branchURL) : wsState.getURL(); + SVNRevision mergeRev = branchRev >= 0 ? SVNRevision.create(branchRev) : wsState.getRevision(); + + // do we have any meaningful changes in this branch worthy of integration? + if (lastIntegrationSourceRevision !=null) { + final MutableBoolean changesFound = new MutableBoolean(false); + cm.getLogClient().doLog(new File[]{mr},mergeRev,SVNRevision.create(lastIntegrationSourceRevision),mergeRev,true,false,-1,new ISVNLogEntryHandler() { + public void handleLogEntry(SVNLogEntry e) throws SVNException { + if (!changesFound.booleanValue()) { + String message = e.getMessage(); + + if (!message.startsWith(RebaseAction.COMMIT_MESSAGE_PREFIX) + && !message.startsWith(IntegrateAction.COMMIT_MESSAGE_PREFIX)) { + changesFound.setValue(true); + } } } + }); + // didn't find anything interesting. all the changes are our merges + if (!changesFound.booleanValue()) { + logger.println("No changes to be integrated. Skipping integration."); + return new IntegrationResult(0,mergeRev); + } - }); - // didn't find anything interesting. all the changes are our merges - if (!changesFound.booleanValue()) { - logger.println("No changes to be integrated. Skipping integration."); - return new IntegrationResult(0,mergeRev); } - } - - logger.println("Switching to the upstream (" + up+")"); - SVNUpdateClient uc = cm.getUpdateClient(); - uc.doSwitch(mr, up, HEAD, HEAD, INFINITY, false, false); - - 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+"\n"+mergeUrl+"@"+mergeRev, 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); + logger.println("Switching to the upstream (" + up+")"); + SVNUpdateClient uc = cm.getUpdateClient(); + uc.doSwitch(mr, up, HEAD, HEAD, INFINITY, false, false); - if(foundConflict[0]) { - logger.println("Conflict found. Please sync with the upstream to resolve this error."); - return new IntegrationResult(-1,mergeRev); - } + 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+"\n"+mergeUrl+"@"+mergeRev, null, null, false, false, INFINITY); + if(ci.getNewRevision()<0) + logger.println(" No changes since the last integration"); + else + logger.println(" committed revision "+ci.getNewRevision()); + } - long trunkCommit = 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 new IntegrationResult(-1,mergeRev); + } - if (trunkCommit>=0) { - cm.getUpdateClient().doUpdate(mr, HEAD, INFINITY, false, false); - SVNCommitClient cc = cm.getCommitClient(); + long trunkCommit = ci.getNewRevision(); - if (false) { - // taking Jack Repennings advise not to do this. + if (trunkCommit>=0) { + cm.getUpdateClient().doUpdate(mr, HEAD, INFINITY, false, false); + SVNCommitClient cc = cm.getCommitClient(); + + if (false) { + // taking Jack Repennings advise not to do this. + + // if the trunk merge produces a commit M, then we want to do "svn merge --record-only -c M " + // to convince Subversion not to try to merge rev.M again when re-integrating from the trunk in the future, + // equivalent of single commit + SVNRevisionRange r = new SVNRevisionRange(SVNRevision.create(trunkCommit-1), + SVNRevision.create(trunkCommit)); + String msg = "Block the merge commit rev." + trunkCommit + " from getting merged back into our branch"; + logger.println(msg); + dc.doMerge(up,HEAD,Arrays.asList(r), mr, INFINITY, true/*opposite of --ignore-ancestory in CLI*/, false, false, true); + + SVNCommitInfo bci = cc.doCommit(new File[]{mr}, false, msg, null, null, false, false, INFINITY); + logger.println(" committed revision "+bci.getNewRevision()); + cm.getUpdateClient().doUpdate(mr, HEAD, INFINITY, false, false); + } - // if the trunk merge produces a commit M, then we want to do "svn merge --record-only -c M " - // to convince Subversion not to try to merge rev.M again when re-integrating from the trunk in the future, - // equivalent of single commit - SVNRevisionRange r = new SVNRevisionRange(SVNRevision.create(trunkCommit-1), - SVNRevision.create(trunkCommit)); - String msg = "Block the merge commit rev." + trunkCommit + " from getting merged back into our branch"; - logger.println(msg); - dc.doMerge(up,HEAD,Arrays.asList(r), mr, INFINITY, true/*opposite of --ignore-ancestory in CLI*/, false, false, true); + // this is the black magic part, but my experiments reveal that we need to run trunk->branch merge --reintegrate + // or else future rebase fails + logger.printf("Merging change from the upstream %s at rev.%s\n",up,trunkCommit); + dc.doMergeReIntegrate(up, SVNRevision.create(trunkCommit), mr, false); + if(foundConflict[0]) { + uc.doSwitch(mr, wsState.getURL(), wsState.getRevision(), wsState.getRevision(), INFINITY, false, true); + logger.println("Conflict found. Please sync with the upstream to resolve this error."); + return new IntegrationResult(-1,mergeRev); + } + String msg = RebaseAction.COMMIT_MESSAGE_PREFIX+"Rebasing with the integration commit that was just made in rev."+trunkCommit; SVNCommitInfo bci = cc.doCommit(new File[]{mr}, false, msg, null, null, false, false, INFINITY); logger.println(" committed revision "+bci.getNewRevision()); - cm.getUpdateClient().doUpdate(mr, HEAD, INFINITY, false, false); - } - - // this is the black magic part, but my experiments reveal that we need to run trunk->branch merge --reintegrate - // or else future rebase fails - logger.printf("Merging change from the upstream %s at rev.%s\n",up,trunkCommit); - dc.doMergeReIntegrate(up, SVNRevision.create(trunkCommit), mr, false); - if(foundConflict[0]) { - uc.doSwitch(mr, wsState.getURL(), wsState.getRevision(), wsState.getRevision(), INFINITY, false, true); - logger.println("Conflict found. Please sync with the upstream to resolve this error."); - return new IntegrationResult(-1,mergeRev); } - String msg = RebaseAction.COMMIT_MESSAGE_PREFIX+"Rebasing with the integration commit that was just made in rev."+trunkCommit; - SVNCommitInfo bci = cc.doCommit(new File[]{mr}, false, msg, null, null, false, false, INFINITY); - logger.println(" committed revision "+bci.getNewRevision()); - } - - // -1 is returned if there was no commit, so normalize that to 0 - return new IntegrationResult(Math.max(0,trunkCommit),mergeRev); + // -1 is returned if there was no commit, so normalize that to 0 + return new IntegrationResult(Math.max(0,trunkCommit),mergeRev); } catch (SVNException e) { throw new IOException2("Failed to merge", e); } + } + }); + + //If at least one result is null, it means that we have a conflict. So exit for loop and return null: + if(result != null){ + toReturn.add(result); + } else { + toReturn = null; + break; } - }); + } + + return toReturn; } private Long getlastIntegrationSourceRevision() { diff --git a/src/main/java/jenkins/plugins/svnmerge/IntegratableProjectAction.java b/src/main/java/jenkins/plugins/svnmerge/IntegratableProjectAction.java index f734a12..5a628ec 100644 --- a/src/main/java/jenkins/plugins/svnmerge/IntegratableProjectAction.java +++ b/src/main/java/jenkins/plugins/svnmerge/IntegratableProjectAction.java @@ -94,24 +94,28 @@ public List> getBranches() { * * @return */ - public RepositoryLayoutInfo getRepositoryLayout() { + public ArrayList getRepositoryLayout() { + ArrayList toReturn = new ArrayList(); SCM scm = project.getScm(); if (!(scm instanceof SubversionSCM)) { return null; } - //TODO: check for multiple locations ? + SubversionSCM svn = (SubversionSCM) scm; - ModuleLocation firstLocation = svn.getLocations()[0]; - // expand system and node environment variables as well as the project parameters - firstLocation = Utility.getExpandedLocation(firstLocation, project); - return getRepositoryLayout(firstLocation); + for(ModuleLocation ml : svn.getLocations()){ + // expand system and node environment variables as well as the project parameters + ml = Utility.getExpandedLocation(ml, project); + toReturn.add(getRepositoryLayout(ml)); + } + + return toReturn; } private RepositoryLayoutInfo getRepositoryLayout(ModuleLocation location) { return new RepositoryLayoutInfo(location.getURL()); } - @RequirePOST + @RequirePOST public void doNewBranch(StaplerRequest req, StaplerResponse rsp, @QueryParameter String name, @QueryParameter boolean attach, @@ -139,123 +143,133 @@ public void doNewBranch(StaplerRequest req, StaplerResponse rsp, sendError("This project doesn't use Subversion as SCM"); return; } - // TODO: check for multiple locations + SubversionSCM svn = (SubversionSCM) scm; - ModuleLocation firstLocation = svn.getLocations()[0]; - // expand system and node environment variables as well as the project parameters - firstLocation = Utility.getExpandedLocation(firstLocation, project); - RepositoryLayoutInfo layoutInfo = getRepositoryLayout(firstLocation); - - branchLocation = Util.fixEmptyAndTrim(branchLocation); - tagLocation = Util.fixEmptyAndTrim(tagLocation); - - if (layoutInfo.getLayout() == RepositoryLayoutEnum.CUSTOM) { + //Create list with all new locations. This will be later copied to the new job + List locationsToCopy = new ArrayList(); + + for(ModuleLocation currentLocation : svn.getLocations()){ + // expand system and node environment variables as well as the project parameters + currentLocation = Utility.getExpandedLocation(currentLocation, project); + + RepositoryLayoutInfo layoutInfo = getRepositoryLayout(currentLocation); + + branchLocation = Util.fixEmptyAndTrim(branchLocation); + tagLocation = Util.fixEmptyAndTrim(tagLocation); + + if (layoutInfo.getLayout() == RepositoryLayoutEnum.CUSTOM) { /* * in case of custom layout the user must provide the full new branch url * (and optionally the full new tag url) */ - if (StringUtils.isEmpty(branchLocation)) { - sendError("Branch Location is required for custom repository layout"); - } - if (!attach && createTag && StringUtils.isEmpty(tagLocation)) { - sendError("Tag Location is required for custom repository layout"); - } - } - - String branchUrl; - if (StringUtils.isNotEmpty(branchLocation)) { - //using override value - branchUrl = branchLocation; - } else { - //using default value - branchUrl = layoutInfo.getDefaultNewBranchUrl().replace("", name); - } - - if (!attach) { - SvnClientManager svnMgr = SubversionSCM.createClientManager( - svn.createAuthenticationProvider(project, firstLocation)); - - List urlsToCopyTo = new ArrayList(); - SVNURL svnUrl = null; - try { - svnUrl = SVNURL.parseURIEncoded(branchUrl); - SVNInfo info = svnMgr.getWCClient().doInfo(svnUrl, SVNRevision.HEAD, SVNRevision.HEAD); - if(info.getKind()== SVNNodeKind.DIR) { - // ask the user if we should attach - req.getView(this,"_attach.jelly").forward(req,rsp); - return; - } else { - sendError(info.getURL()+" already exists."); - return; - } - } catch (SVNException e) { - // path doesn't exist, the new branch can be created - } - urlsToCopyTo.add(branchUrl); - - String tagUrl = null; - if (createTag) { - //can be true only when not attaching - if (StringUtils.isNotEmpty(tagLocation)) { - //using override value - tagUrl = tagLocation; - } else { - //using default value - tagUrl = layoutInfo.getDefaultNewDevTagUrl().replace("", name); - } - try { - svnUrl = SVNURL.parseURIEncoded(tagUrl); - SVNInfo info = svnMgr.getWCClient().doInfo(svnUrl, SVNRevision.HEAD, SVNRevision.HEAD); - sendError(info.getURL()+" already exists."); - return; - } catch (SVNException e) { - // path doesn't exist, the new tag can be created - } - urlsToCopyTo.add(tagUrl); - } - - if (!createSVNCopy(svnMgr, firstLocation, urlsToCopyTo, commitMessage, req, rsp)) { - return; + if (StringUtils.isEmpty(branchLocation)) { + sendError("Branch Location is required for custom repository layout"); + } + if (!attach && createTag && StringUtils.isEmpty(tagLocation)) { + sendError("Tag Location is required for custom repository layout"); + } } - } - - // if the request wasn't forwarded - // copy a job, and adjust its properties for integration - AbstractProject copy = Jenkins.getInstance().copy(project, project.getName() + "-" + name.replaceAll("/", "-")); - BulkChange bc = new BulkChange(copy); - try { + String branchUrl; + if (StringUtils.isNotEmpty(branchLocation)) { + //using override value + branchUrl = branchLocation; + } else { + //using default value + branchUrl = layoutInfo.getDefaultNewBranchUrl().replace("", name); + } + + if (!attach) { + SvnClientManager svnMgr = SubversionSCM.createClientManager( + svn.createAuthenticationProvider(project, currentLocation)); + + List urlsToCopyTo = new ArrayList(); + SVNURL svnUrl = null; + try { + svnUrl = SVNURL.parseURIEncoded(branchUrl); + SVNInfo info = svnMgr.getWCClient().doInfo(svnUrl, SVNRevision.HEAD, SVNRevision.HEAD); + if(info.getKind()== SVNNodeKind.DIR) { + // ask the user if we should attach + req.getView(this,"_attach.jelly").forward(req,rsp); + return; + } else { + sendError(info.getURL()+" already exists."); + return; + } + } catch (SVNException e) { + // path doesn't exist, the new branch can be created + } + urlsToCopyTo.add(branchUrl); + + String tagUrl = null; + if (createTag) { + //can be true only when not attaching + if (StringUtils.isNotEmpty(tagLocation)) { + //using override value + tagUrl = tagLocation; + } else { + //using default value + tagUrl = layoutInfo.getDefaultNewDevTagUrl().replace("", name); + } + try { + svnUrl = SVNURL.parseURIEncoded(tagUrl); + SVNInfo info = svnMgr.getWCClient().doInfo(svnUrl, SVNRevision.HEAD, SVNRevision.HEAD); + sendError(info.getURL()+" already exists."); + return; + } catch (SVNException e) { + // path doesn't exist, the new tag can be created + } + urlsToCopyTo.add(tagUrl); + } + + if (!createSVNCopy(svnMgr, currentLocation, urlsToCopyTo, commitMessage, req, rsp)) { + return; + } + } + //Add new location, we will copy in new job's Subversion + locationsToCopy.add(currentLocation.withRemote(branchUrl)); + } + + // if the request wasn't forwarded + // copy a job, and adjust its properties for integration + AbstractProject copy = Jenkins.getInstance().copy(project, project.getName() + "-" + name.replaceAll("/", "-")); + BulkChange bc = new BulkChange(copy); - // moving the copy to its parent location - moveProjectToItsUpstreamProjectLocation(copy, project); - // the copy doesn't born accepting integration from subversion feature branches.. - copy.removeProperty(IntegratableProject.class); + try { + + // moving the copy to its parent location + moveProjectToItsUpstreamProjectLocation(copy, project); + // the copy doesn't born accepting integration from subversion feature branches.. + copy.removeProperty(IntegratableProject.class); // ... and it's a feature branch of its upstream project (which can // be located anywhere in the tree, that's why we are pointing // to its full name) - ((AbstractProject)copy).addProperty(new FeatureBranchProperty(project.getFullName())); // pointless cast for working around javac bug as of JDK1.6.0_02 - // update the SCM config to point to the branch - SubversionSCM svnScm = (SubversionSCM)copy.getScm(); - copy.setScm( - new SubversionSCM( - Arrays.asList(firstLocation.withRemote(branchUrl)), - svnScm.getWorkspaceUpdater(), - svnScm.getBrowser(), - svnScm.getExcludedRegions(), - svnScm.getExcludedUsers(), - svnScm.getExcludedRevprop(), - svnScm.getExcludedCommitMessages(), - svnScm.getIncludedRegions(), - svnScm.isIgnoreDirPropChanges(), - svnScm.isFilterChangelog(), - svnScm.getAdditionalCredentials() - )); - } finally { - bc.commit(); - } - - rsp.sendRedirect2(req.getContextPath()+"/"+copy.getUrl()); + ((AbstractProject)copy).addProperty(new FeatureBranchProperty(project.getFullName())); // pointless cast for working around javac bug as of JDK1.6.0_02 + // update the SCM config to point to the branch + SubversionSCM svnScm = (SubversionSCM)copy.getScm(); + + + copy.setScm( + new SubversionSCM( + locationsToCopy, + svnScm.getWorkspaceUpdater(), + svnScm.getBrowser(), + svnScm.getExcludedRegions(), + svnScm.getExcludedUsers(), + svnScm.getExcludedRevprop(), + svnScm.getExcludedCommitMessages(), + svnScm.getIncludedRegions(), + svnScm.isIgnoreDirPropChanges(), + svnScm.isFilterChangelog(), + svnScm.getAdditionalCredentials() + )); + } finally { + bc.commit(); + } + + rsp.sendRedirect2(req.getContextPath()+"/"+copy.getUrl()); + } /** @@ -293,7 +307,7 @@ private void moveProjectToItsUpstreamPro /** * Utility method for SVN copies creation. * First checks if all the given urls already exist; if any exist, creates a copy for each of them. - * @param scm the project scm + * @param svnMgr the project scm * @param urlsToCopyTo a list of urls to copy to (i.e. where the copies'll be created). * @param commitMessage the commit message to use * @param req the original StaplerRequest diff --git a/src/main/java/jenkins/plugins/svnmerge/IntegrateAction.java b/src/main/java/jenkins/plugins/svnmerge/IntegrateAction.java index f15c479..7e7c27b 100644 --- a/src/main/java/jenkins/plugins/svnmerge/IntegrateAction.java +++ b/src/main/java/jenkins/plugins/svnmerge/IntegrateAction.java @@ -31,7 +31,9 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Hashtable; +import java.util.List; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; @@ -129,31 +131,31 @@ protected IntegrateSetting createParams(StaplerRequest req) throws IOException { /** * URL and revision to be integrated from this action. */ - public SvnInfo getSvnInfo() { + public List getSvnInfo() { + List toReturn = new ArrayList(); SCM scm = getProject().getScm(); if (!(scm instanceof SubversionSCM)) { return null; } - // TODO: check for multiple locations ? SubversionSCM svn = (SubversionSCM) scm; - ModuleLocation[] locations = svn.getLocations(); - - ModuleLocation firstLocation = svn.getLocations()[0]; - // expand system and node environment variables as well as the - // project parameters - firstLocation = Utility.getExpandedLocation(firstLocation, getProject()); - - try { - SVNRevisionState state = build.getAction(SVNRevisionState.class); - long revision = state.getRevision(firstLocation.getURL()); - - return new SvnInfo(firstLocation.getSVNURL().toDecodedString(), revision); - } catch (SVNException e) { - LOGGER.log(WARNING, "Could not get SVN URL and revision", e); + ModuleLocation[] locations = svn.getLocations(); + for(ModuleLocation currentLocation : locations){ + // expand system and node environment variables as well as the + // project parameters + currentLocation = Utility.getExpandedLocation(currentLocation, getProject()); + + try { + SVNRevisionState state = build.getAction(SVNRevisionState.class); + long revision = state.getRevision(currentLocation.getURL().toLowerCase()); + + toReturn.add(new SvnInfo(currentLocation.getSVNURL().toDecodedString(), revision)); + } catch (SVNException e) { + LOGGER.log(WARNING, "Could not get SVN URL and revision", e); + } } - return null; + return toReturn; } /** @@ -161,7 +163,7 @@ public SvnInfo getSvnInfo() { *

* This requires that the calling thread owns the workspace. */ - /*package*/ long perform(TaskListener listener, IntegrateSetting _) throws IOException, InterruptedException { + /*package*/ List perform(TaskListener listener, IntegrateSetting _) throws IOException, InterruptedException { return perform(listener, getSvnInfo()); } @@ -169,21 +171,29 @@ public SvnInfo getSvnInfo() { * @param src * We are taking this revision and merge it into the upstream. */ - public long perform(TaskListener listener, SvnInfo src) throws IOException, InterruptedException { + public List perform(TaskListener listener, List src) throws IOException, InterruptedException { String commitMessage = getCommitMessage(); - - IntegrationResult r = getProperty().integrate(listener, src.url, src.revision, commitMessage); - integratedRevision = r.mergeCommit; - integrationSource = r.integrationSource; - if(integratedRevision>0) { - // record this integration as a fingerprint. - // this will allow us to find where this change is integrated. - Jenkins.getInstance().getFingerprintMap().getOrCreate( - build, IntegrateAction.class.getName(), - getFingerprintKey()); + List toReturn = new ArrayList(); + + List integrations = getProperty().integrate(listener, src, commitMessage); + + if(integrations != null && integrations.size() > 0){ + for(IntegrationResult r : integrations){ + integratedRevision = r.mergeCommit; + integrationSource = r.integrationSource; + if(integratedRevision>0) { + // record this integration as a fingerprint. + // this will allow us to find where this change is integrated. + Jenkins.getInstance().getFingerprintMap().getOrCreate( + build, IntegrateAction.class.getName(), + getFingerprintKey()); + } + build.save(); + toReturn.add(integratedRevision); + } } - build.save(); - return integratedRevision; + + return toReturn; } /** diff --git a/src/main/java/jenkins/plugins/svnmerge/IntegrationPublisher.java b/src/main/java/jenkins/plugins/svnmerge/IntegrationPublisher.java index 370ce5e..316f8f7 100644 --- a/src/main/java/jenkins/plugins/svnmerge/IntegrationPublisher.java +++ b/src/main/java/jenkins/plugins/svnmerge/IntegrationPublisher.java @@ -16,6 +16,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import java.io.IOException; +import java.util.List; /** * {@link Publisher} that integrates the current workspace to the upstream. @@ -48,8 +49,17 @@ public boolean perform(AbstractBuild build, Launcher launcher, final BuildL return false; } - if(ia.perform(listener,new IntegrateSetting())<0) + List integrationResults = ia.perform(listener,new IntegrateSetting()); + + if(integrationResults != null && integrationResults.size() > 0){ + for(Long result : integrationResults){ + if(result<0) + build.setResult(Result.FAILURE); + } + } else { build.setResult(Result.FAILURE); + } + return true; } diff --git a/src/main/java/jenkins/plugins/svnmerge/RebaseAction.java b/src/main/java/jenkins/plugins/svnmerge/RebaseAction.java index 53acb06..db63ed0 100644 --- a/src/main/java/jenkins/plugins/svnmerge/RebaseAction.java +++ b/src/main/java/jenkins/plugins/svnmerge/RebaseAction.java @@ -16,11 +16,14 @@ import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import org.tmatesoft.svn.core.wc.SVNInfo; import javax.servlet.ServletException; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * {@link AbstractProject}-level action to rebase changes from the upstream branch into the current branch. @@ -89,8 +92,9 @@ protected TaskImpl createTask(RebaseSetting param) throws IOException { *

* This requires that the calling thread owns the workspace. */ - /*package*/ long perform(TaskListener listener, RebaseSetting param) throws IOException, InterruptedException { - long rev = param.revision; + /*package*/ List perform(TaskListener listener, RebaseSetting param) throws IOException, InterruptedException { + List revisions = new ArrayList(); + revisions.add(param.revision); if (param.permalink!=null) { AbstractProject up = getProperty().getUpstreamProject(); @@ -99,20 +103,29 @@ protected TaskImpl createTask(RebaseSetting param) throws IOException { Run b = p.resolve(up); if (b==null) { listener.getLogger().println("No build that matches "+p.getDisplayName()+". Rebase is no-nop."); - return -1; + return null; } SubversionTagAction a = b.getAction(SubversionTagAction.class); if (a==null) throw new AbortException("Unable to determine the Subversion revision number from "+b.getFullDisplayName()); - // TODO: what to do if this involves multiple URLs? - SvnInfo sv = a.getTags().keySet().iterator().next(); - rev = sv.revision; + revisions = new ArrayList(); + for(SvnInfo sv : a.getTags().keySet()){ + long currentRev = sv.revision; + revisions.add(currentRev); + } } } - long integratedRevision = getProperty().rebase(listener, rev); + List integratedRevisions = new ArrayList(); + for(Long rev : revisions){ + List integrated = getProperty().rebase(listener, rev); + if(integrated != null) + integratedRevisions.addAll(integrated); + else + return null; + } // if(integratedRevision>0) { // // record this integration as a fingerprint. // // this will allow us to find where this change is integrated. @@ -120,7 +133,7 @@ protected TaskImpl createTask(RebaseSetting param) throws IOException { // build, IntegrateAction.class.getName(), // getFingerprintKey()); // } - return integratedRevision; + return integratedRevisions; } /** diff --git a/src/main/java/jenkins/plugins/svnmerge/RebaseBuilder.java b/src/main/java/jenkins/plugins/svnmerge/RebaseBuilder.java index 0bbaf91..c560047 100644 --- a/src/main/java/jenkins/plugins/svnmerge/RebaseBuilder.java +++ b/src/main/java/jenkins/plugins/svnmerge/RebaseBuilder.java @@ -16,6 +16,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import java.io.IOException; +import java.util.List; /** * @author Kohsuke Kawaguchi @@ -50,11 +51,21 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen } RebaseAction rebaseAction = new RebaseAction(project); - long result = rebaseAction.perform(listener,new RebaseSetting(permalink)); - if(result<0){ - build.setResult(Result.UNSTABLE); + List results = rebaseAction.perform(listener,new RebaseSetting(permalink)); + boolean buildStable = true; + + if(results != null && results.size() > 0){ + for(Long result : results){ + if(result<0){ + build.setResult(Result.UNSTABLE); + buildStable = false; + } + } + } else { + buildStable = false; } - return !stopBuildIfMergeFails || result >= 0; + + return !stopBuildIfMergeFails || buildStable; } @Extension diff --git a/src/main/resources/jenkins/plugins/svnmerge/IntegratableProjectAction/index.groovy b/src/main/resources/jenkins/plugins/svnmerge/IntegratableProjectAction/index.groovy index 72ba443..f62433a 100644 --- a/src/main/resources/jenkins/plugins/svnmerge/IntegratableProjectAction/index.groovy +++ b/src/main/resources/jenkins/plugins/svnmerge/IntegratableProjectAction/index.groovy @@ -24,7 +24,7 @@ l.layout(norefresh:"true",title:_("title",my.project.displayName)) { def repoLayout = my.repositoryLayout p(_("Repository URL: "+repoLayout.scmModuleLocation)) p(_("Detected repository layout: "+repoLayout.layout)) - if (StringUtils.isNotEmpty(repoLayout.subProjectName)) { + if (repoLayout.subProjectName != null) { p(_("Detected subproject name: "+repoLayout.subProjectName)) } diff --git a/src/test/java/jenkins/plugins/svnmerge/FeatureBranchPropertyTest.java b/src/test/java/jenkins/plugins/svnmerge/FeatureBranchPropertyTest.java index c15e6cb..9f22325 100644 --- a/src/test/java/jenkins/plugins/svnmerge/FeatureBranchPropertyTest.java +++ b/src/test/java/jenkins/plugins/svnmerge/FeatureBranchPropertyTest.java @@ -23,6 +23,8 @@ import hudson.slaves.EnvironmentVariablesNodeProperty; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import org.tmatesoft.svn.core.SVNURL; +import org.tmatesoft.svn.core.wc.SVNInfo; /** * @author Kohsuke Kawaguchi @@ -48,7 +50,7 @@ public void testConfigRoundtrip2() throws Exception { @Bug(24735) public void testUpStreamURLwithParams() throws Exception { - EnvironmentVariablesNodeProperty envNodeProp = new EnvironmentVariablesNodeProperty(new hudson.slaves.EnvironmentVariablesNodeProperty.Entry("ROOT_SVN_URL", "root/")); + EnvironmentVariablesNodeProperty envNodeProp = new EnvironmentVariablesNodeProperty(new hudson.slaves.EnvironmentVariablesNodeProperty.Entry("ROOT_SVN_URL", "root/")); Computer.currentComputer().getNode().getNodeProperties().add(envNodeProp); Jenkins.getInstance().getDescriptorList(JobProperty.class).add(new FeatureBranchProperty.DescriptorImpl()); FreeStyleProject p = createFreeStyleProject(); @@ -59,7 +61,9 @@ public void testUpStreamURLwithParams() FeatureBranchProperty jobProp = new FeatureBranchProperty(p.getName()); FreeStyleProject p2 = createFreeStyleProject(); p2.addProperty(jobProp); - assertEquals( "https://root/a/b/trunk",jobProp.getUpstreamURL().toDecodedString()); + for(SVNURL svnURL : jobProp.getUpstreamURL()){ + assertEquals( "https://root/a/b/trunk",svnURL.toDecodedString()); + } } diff --git a/src/test/java/jenkins/plugins/svnmerge/MergeTest.java b/src/test/java/jenkins/plugins/svnmerge/MergeTest.java index f387f1c..412f54c 100644 --- a/src/test/java/jenkins/plugins/svnmerge/MergeTest.java +++ b/src/test/java/jenkins/plugins/svnmerge/MergeTest.java @@ -122,13 +122,17 @@ public void testUpstreamMerge() throws Exception { */ private boolean trunkHasE() throws SVNException { // make sure the merge went in by checking if /trunk/e exists. - SVNRepository rep = SVNRepositoryFactory.create(upp.getUpstreamURL()); - long latest = rep.getLatestRevision(); - try { - return latest==rep.getFile("/trunk/e", latest,new SVNProperties(), null); - } catch (SVNException e) { - return false; + boolean toReturn = false; + for(SVNURL url : upp.getUpstreamURL()){ + SVNRepository rep = SVNRepositoryFactory.create(url); + long latest = rep.getLatestRevision(); + try { + toReturn = latest==rep.getFile("/trunk/e", latest,new SVNProperties(), null); + } catch (SVNException e) { + return false; + } } + return toReturn; } /**