Skip to content

Commit

Permalink
Merge pull request #168 from jglick/shared-libs-JENKINS-31155
Browse files Browse the repository at this point in the history
[JENKINS-31155] Ability to retrieve a single revision
  • Loading branch information
recena committed Sep 7, 2016
2 parents e86d736 + ed731f2 commit 7550539
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 18 deletions.
10 changes: 6 additions & 4 deletions pom.xml
Expand Up @@ -28,7 +28,8 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>2.9</version>
<version>2.14</version>
<relativePath/>
</parent>

<artifactId>subversion</artifactId>
Expand All @@ -41,6 +42,7 @@ THE SOFTWARE.
<licenses>
<license>
<name>MIT license</name>
<url>http://opensource.org/licenses/MIT</url>
<comments>All source code is under the MIT license.</comments>
</license>
</licenses>
Expand All @@ -67,9 +69,9 @@ THE SOFTWARE.
</distributionManagement>

<properties>
<jenkins.version>1.580.1</jenkins.version>
<jenkins.version>1.609.3</jenkins.version>
<java.level>6</java.level>
<workflow.version>1.4.3</workflow.version>
<workflow.version>1.14.2</workflow.version>
<findbugs.failOnError>false</findbugs.failOnError>
</properties>

Expand Down Expand Up @@ -122,7 +124,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>scm-api</artifactId>
<version>0.2</version>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/hudson/scm/SubversionSCM.java
Expand Up @@ -885,8 +885,8 @@ private List<External> checkout(Run build, FilePath workspace, TaskListener list

listener.getLogger().println("One or more repository locations do not exist anymore for " + build.getParent().getName() + ", project will be disabled.");
disableProject(((AbstractBuild) build).getProject(), listener);
return null;
}
return null;
}

List<External> externals = new ArrayList<External>();
Expand Down
93 changes: 82 additions & 11 deletions src/main/java/jenkins/scm/impl/subversion/SubversionSCMSource.java
Expand Up @@ -101,7 +101,9 @@
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.kohsuke.stapler.DataBoundSetter;

/**
* A {@link SCMSource} for Subversion.
Expand All @@ -110,33 +112,35 @@
*/
public class SubversionSCMSource extends SCMSource {

private static final String DEFAULT_INCLUDES = "trunk,branches/*,tags/*,sandbox/*";

private static final String DEFAULT_EXCLUDES = "";

public static final StringListComparator COMPARATOR = new StringListComparator();

public static final Logger LOGGER = Logger.getLogger(SubversionSCMSource.class.getName());

private final String remoteBase;

private final String credentialsId;
private String credentialsId = ""; // TODO null would be a better default, but need to check null safety on usages

private final String includes;
private String includes = DescriptorImpl.DEFAULT_INCLUDES;

private final String excludes;
private String excludes = DescriptorImpl.DEFAULT_EXCLUDES;

@GuardedBy("this")
private transient String uuid;

@Deprecated
public SubversionSCMSource(String id, String remoteBase, String credentialsId, String includes, String excludes) {
super(id);
this.remoteBase = StringUtils.removeEnd(remoteBase, "/") + "/";
setCredentialsId(credentialsId);
setIncludes(StringUtils.defaultIfEmpty(includes, DescriptorImpl.DEFAULT_INCLUDES));
setExcludes(StringUtils.defaultIfEmpty(excludes, DescriptorImpl.DEFAULT_EXCLUDES));
}

@DataBoundConstructor
@SuppressWarnings("unused") // by stapler
public SubversionSCMSource(String id, String remoteBase, String credentialsId, String includes, String excludes) {
public SubversionSCMSource(String id, String remoteBase) {
super(id);
this.remoteBase = StringUtils.removeEnd(remoteBase, "/") + "/";
this.credentialsId = credentialsId;
this.includes = StringUtils.defaultIfEmpty(includes, DEFAULT_INCLUDES);
this.excludes = StringUtils.defaultIfEmpty(excludes, DEFAULT_EXCLUDES);
}

/**
Expand All @@ -149,6 +153,11 @@ public String getCredentialsId() {
return credentialsId;
}

@DataBoundSetter
public void setCredentialsId(String credentialsId) {
this.credentialsId = credentialsId;
}

/**
* Gets the comma separated list of exclusions.
*
Expand All @@ -159,6 +168,11 @@ public String getExcludes() {
return excludes;
}

@DataBoundSetter
public void setExcludes(String excludes) {
this.excludes = excludes;
}

/**
* Gets the comma separated list of inclusions.
*
Expand All @@ -169,6 +183,11 @@ public String getIncludes() {
return includes;
}

@DataBoundSetter
public void setIncludes(String includes) {
this.includes = includes;
}

/**
* Gets the base SVN URL of the project.
*
Expand Down Expand Up @@ -256,6 +275,47 @@ protected SCMRevision retrieve(@NonNull SCMHead head, @NonNull TaskListener list
}
}

/**
* {@inheritDoc}
*/
@Override
protected SCMRevision retrieve(String unparsedRevision, TaskListener listener) throws IOException, InterruptedException {
try {
listener.getLogger().println("Opening connection to " + remoteBase);
SVNURL repoURL = SVNURL.parseURIEncoded(remoteBase);
SVNRepositoryView repository = openSession(repoURL);
String repoPath = SubversionSCM.DescriptorImpl.getRelativePath(repoURL, repository.getRepository());
String base;
long revision;
Matcher pathAtRev = Pattern.compile("(.+)@(\\d+)").matcher(unparsedRevision);
if (pathAtRev.matches()) {
base = pathAtRev.group(1);
revision = Long.parseLong(pathAtRev.group(2));
} else {
base = unparsedRevision;
revision = -1;
}
String path = SVNPathUtil.append(repoPath, base);
long resolvedRevision = repository.getNode(path, -1).getRevision();
if (resolvedRevision == -1) {
listener.getLogger().println("Could not find " + path);
return null;
}
return new SCMRevisionImpl(new SCMHead(base), revision == -1 ? resolvedRevision : revision);
} catch (SVNException e) {
throw new IOException(e);
}
}

/**
* {@inheritDoc}
*/
@Override
protected Set<String> retrieveRevisions(TaskListener listener) throws IOException, InterruptedException {
// Default implementation should do what we need: normally includes tags as well as branches.
return super.retrieveRevisions(listener);
}

private static void closeSession(@CheckForNull SVNRepositoryView repository) {
if (repository != null) {
repository.close();
Expand Down Expand Up @@ -667,6 +727,12 @@ public boolean equals(Object o) {
public int hashCode() {
return (int) (revision ^ (revision >>> 32));
}

@Override
public String toString() {
return Long.toString(revision);
}

}

static class StringListComparator implements Comparator<List<String>> {
Expand Down Expand Up @@ -694,6 +760,11 @@ public int compare(List<String> o1, List<String> o2) {
@Extension
@SuppressWarnings("unused") // by jenkins
public static class DescriptorImpl extends SCMSourceDescriptor {

public static final String DEFAULT_INCLUDES = "trunk,branches/*,tags/*,sandbox/*";

public static final String DEFAULT_EXCLUDES = "";

static final Pattern URL_PATTERN = Pattern.compile("(https?|svn(\\+[a-z0-9]+)?|file)://.+");

/**
Expand Down
Expand Up @@ -31,9 +31,9 @@
<f:select/>
</f:entry>
<f:entry title="${%Include branches}" field="includes">
<f:textbox default="trunk,branches/*,tags/*,sandbox/*"/>
<f:textbox default="${descriptor.DEFAULT_INCLUDES}"/>
</f:entry>
<f:entry title="${%Exclude branches}" field="excludes">
<f:textbox/>
<f:textbox default="${descriptor.DEFAULT_EXCLUDES}"/>
</f:entry>
</j:jelly>
@@ -0,0 +1,121 @@
/*
* The MIT License
*
* Copyright 2016 CloudBees, Inc.
*
* 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 jenkins.scm.impl.subversion;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.scm.SCMRevisionState;
import hudson.util.StreamTaskListener;
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMSource;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;

public class SubversionSCMSourceIntegrationTest {

@Rule
public JenkinsRule r = new JenkinsRule();
@Rule
public SubversionSampleRepoRule sampleRepo = new SubversionSampleRepoRule();

@Test
public void retrieve() throws Exception {
sampleRepo.init();
sampleRepo.write("file", "trunk");
sampleRepo.svnkit("commit", "--message=trunk", sampleRepo.wc());
long trunk = sampleRepo.revision();
assertEquals(3, trunk);
sampleRepo.svnkit("copy", "--message=branching", sampleRepo.trunkUrl(), sampleRepo.branchesUrl() + "/dev");
sampleRepo.svnkit("switch", sampleRepo.branchesUrl() + "/dev", sampleRepo.wc());
sampleRepo.write("file", "dev1");
sampleRepo.svnkit("commit", "--message=dev1", sampleRepo.wc());
long dev1 = sampleRepo.revision();
assertEquals(5, dev1);
sampleRepo.svnkit("copy", "--message=tagging", sampleRepo.branchesUrl() + "/dev", sampleRepo.tagsUrl() + "/dev-1");
sampleRepo.write("file", "dev2");
sampleRepo.svnkit("commit", "--message=dev2", sampleRepo.wc());
long dev2 = sampleRepo.revision();
assertEquals(7, dev2);
SCMSource source = new SubversionSCMSource(null, sampleRepo.prjUrl());
TaskListener listener = StreamTaskListener.fromStdout();
// First check fetching of all heads. SCMHeadObserver.Collector.result is a TreeMap so order is predictable:
assertEquals("[SCMHead{'branches/dev'}, SCMHead{'tags/dev-1'}, SCMHead{'trunk'}]", source.fetch(listener).toString());
// SCM.checkout does not permit a null build argument, unfortunately.
Run<?,?> run = r.buildAndAssertSuccess(r.createFreeStyleProject());
// Retrieval of heads:
assertRevision(source.fetch(new SCMHead("trunk"), listener), "trunk", source, run, listener);
assertRevision(source.fetch(new SCMHead("branches/dev"), listener), "dev2", source, run, listener);
assertRevision(source.fetch(new SCMHead("tags/dev-1"), listener), "dev1", source, run, listener);
// Retrieval of revisions by head name:
assertRevision(source.fetch("trunk", listener), "trunk", source, run, listener);
assertRevision(source.fetch("trunk/", listener), "trunk", source, run, listener);
assertRevision(source.fetch("branches/dev", listener), "dev2", source, run, listener);
assertRevision(source.fetch("tags/dev-1", listener), "dev1", source, run, listener);
// Retrieval of revisions by revision number:
assertRevision(source.fetch("trunk@" + trunk, listener), "trunk", source, run, listener);
assertRevision(source.fetch("trunk/@" + trunk, listener), "trunk", source, run, listener);
assertRevision(source.fetch("branches/dev@" + dev2, listener), "dev2", source, run, listener);
assertRevision(source.fetch("branches/dev@" + dev1, listener), "dev1", source, run, listener);
// And nonexistent/bogus stuff:
assertRevision(source.fetch("nonexistent", listener), null, source, run, listener);
assertRevision(source.fetch("nonexistent/", listener), null, source, run, listener);
assertRevision(source.fetch("nonexistent@" + trunk, listener), null, source, run, listener);
assertRevision(source.fetch("nonexistent@999", listener), null, source, run, listener);
assertRevision(source.fetch("trunk@999", listener), null, source, run, listener); // currently fetch succeeds, but checkout fails
// Checks out repo root (means you have trunk/file not file):
assertRevision(source.fetch("", listener), null, source, run, listener);
// Other oddities:
assertRevision(source.fetch("@", listener), null, source, run, listener);
assertRevision(source.fetch("/", listener), null, source, run, listener);
assertRevision(source.fetch("//", listener), null, source, run, listener);
assertRevision(source.fetch("\n", listener), null, source, run, listener);
// Completions of revision:
assertThat(source.fetchRevisions(listener), hasItems("trunk", "branches/dev", "tags/dev-1"));
}
private void assertRevision(@CheckForNull SCMRevision rev, @CheckForNull String expectedFile, @NonNull SCMSource source, @NonNull Run<?,?> run, @NonNull TaskListener listener) throws Exception {
if (rev == null) {
assertNull(expectedFile);
return;
}
FilePath ws = new FilePath(run.getRootDir()).child("tmp");
try {
source.build(rev.getHead(), rev).checkout(run, new Launcher.LocalLauncher(listener), ws, listener, null, SCMRevisionState.NONE);
} catch (Exception x) {
x.printStackTrace(listener.error("could not check out"));
assertNull(expectedFile);
return;
}
FilePath file = ws.child("file");
assertEquals(expectedFile, file.exists() ? file.readToString() : null);
}

}
Expand Up @@ -40,9 +40,11 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import static org.junit.Assert.assertEquals;
import org.jvnet.hudson.test.JenkinsRule;
import org.tmatesoft.svn.cli.svn.SVN;
import org.tmatesoft.svn.cli.svnadmin.SVNAdmin;
import org.tmatesoft.svn.core.wc.SVNClientManager;

public final class SubversionSampleRepoRule extends AbstractSampleRepoRule {

Expand Down Expand Up @@ -154,6 +156,7 @@ public void init() throws Exception {
write("file", "");
svnkit("add", wc() + "/file");
svnkit("commit", "--message=init", wc());
assertEquals(2, revision());
}

private static String uuid(String url) throws Exception {
Expand All @@ -179,4 +182,12 @@ public void notifyCommit(JenkinsRule r, String path) throws Exception {
r.waitUntilNoActivity();
}

/**
* Gets the repository revision just committed.
*/
public long revision() throws Exception {
// .getLookClient().doGetYoungestRevision(repo) would show last committed revision but would not be sensitive to checked-out branch; which is clearer?
return SVNClientManager.newInstance().getStatusClient().doStatus(wc, true).getRemoteRevision().getNumber(); // http://stackoverflow.com/a/2295674/12916
}

}

0 comments on commit 7550539

Please sign in to comment.