Skip to content
Permalink
Browse files

[JENKINS-48061] Adding DiscoverOtherRefsTrait

  • Loading branch information...
rsandell committed Apr 11, 2018
1 parent d686253 commit 75177c492f12ef59e65e118ea4b6c62daa7dd5c2
@@ -99,7 +99,6 @@
import jenkins.scm.api.trait.SCMSourceRequest;
import jenkins.scm.api.trait.SCMSourceTrait;
import jenkins.scm.api.trait.SCMTrait;
import jenkins.scm.impl.TagSCMHeadCategory;
import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait;
import jenkins.scm.impl.trait.WildcardSCMSourceFilterTrait;
import net.jcip.annotations.GuardedBy;
@@ -548,7 +547,7 @@ public Void run(GitClient client, String remoteName) throws IOException, Interru
Map<String, ObjectId> remoteReferences = null;
if (context.wantBranches() || context.wantTags()) {
listener.getLogger().println("Listing remote references...");
remoteReferences = client.getRemoteReferences(
remoteReferences = client.getRemoteReferences( //TODO DiscoverOtherRefsTrait
client.getRemoteUrl(remoteName), null, context.wantBranches(), context.wantTags()
);
}
@@ -728,15 +727,18 @@ protected SCMRevision retrieve(@NonNull final String revision, @NonNull final Ta
final GitClient client = git.getClient();
client.addDefaultCredentials(getCredentials());
listener.getLogger().printf("Attempting to resolve %s from remote references...%n", revision);
boolean headsOnly = !context.wantOtherRefs() && context.wantBranches();
boolean tagsOnly = !context.wantOtherRefs() && context.wantTags();
Map<String, ObjectId> remoteReferences = client.getRemoteReferences(
getRemote(), null, false, false
getRemote(), null, headsOnly, tagsOnly
);
String tagName = null;
Set<String> shortNameMatches = new TreeSet<>();
String shortHashMatch = null;
Set<String> fullTagMatches = new TreeSet<>();
Set<String> fullHashMatches = new TreeSet<>();
String fullHashMatch = null;
GitRefSCMRevision candidateOtherRef = null;
for (Map.Entry<String,ObjectId> entry: remoteReferences.entrySet()) {
String name = entry.getKey();
String rev = entry.getValue().name();
@@ -784,6 +786,12 @@ protected SCMRevision retrieve(@NonNull final String revision, @NonNull final Ta
//Since it was a full match then the shortMatch below will also match, so just skip it
continue;
}
for (GitSCMSourceContext.WantedOtherRef o : (Collection<GitSCMSourceContext.WantedOtherRef>)context.getOtherWantedRefs()) {
if (o.matches(revision, name, rev)) {
candidateOtherRef = new GitRefSCMRevision(new GitRefSCMHead(revision, name), rev);
break;
}
}
if (rev.toLowerCase(Locale.ENGLISH).startsWith(revision.toLowerCase(Locale.ENGLISH))) {
shortNameMatches.add(name);
if (shortHashMatch == null) {
@@ -853,6 +861,9 @@ public SCMRevision run(GitClient client, String remoteName) throws IOException,
context,
listener, false);
}
if (candidateOtherRef != null) {
return candidateOtherRef;
}
// Pokémon!... Got to catch them all
listener.getLogger().printf("Could not find %s in remote references. "
+ "Pulling heads to local for deep search...%n", revision);
@@ -944,7 +955,7 @@ public SCMRevision run(GitClient client, String remoteName) throws IOException,
Map<String, ObjectId> remoteReferences = client.getRemoteReferences(
getRemote(), null, context.wantBranches(), context.wantTags()
);
for (String name : remoteReferences.keySet()) {
for (String name : remoteReferences.keySet()) { //TODO DiscoverOtherRefsTrait
if (context.wantBranches()) {
if (name.startsWith(Constants.R_HEADS)) {
revisions.add(StringUtils.removeStart(name, Constants.R_HEADS));
@@ -31,14 +31,21 @@
import hudson.plugins.git.GitSCM;
import hudson.plugins.git.GitTool;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jenkins.scm.api.SCMHeadObserver;
import jenkins.scm.api.SCMSource;
import jenkins.scm.api.SCMSourceCriteria;
import jenkins.scm.api.trait.SCMSourceContext;
import jenkins.scm.api.trait.SCMSourceTrait;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.transport.RefSpec;

/**
@@ -59,6 +66,10 @@
* {@code true} if the {@link GitSCMSourceRequest} will need information about tags.
*/
private boolean wantTags;
/**
* A list of other references to discover and search
*/
private Set<WantedOtherRef> wantedOtherRefs;
/**
* The name of the {@link GitTool} to use or {@code null} to use the default.
*/
@@ -107,6 +118,24 @@ public final boolean wantTags() {
return wantTags;
}

/**
* Returns {@code true} if the {@link GitSCMSourceRequest} will need information about other refs.
*
* @return {@code true} if the {@link GitSCMSourceRequest} will need information about other refs.
*/
public final boolean wantOtherRefs() {
return wantedOtherRefs != null && !wantedOtherRefs.isEmpty();
}

@NonNull
public Collection<WantedOtherRef> getOtherWantedRefs() {
if (wantedOtherRefs == null) {
return Collections.emptySet();
} else {
return Collections.unmodifiableSet(wantedOtherRefs);
}
}

/**
* Returns the name of the {@link GitTool} to use or {@code null} to use the default.
*
@@ -177,6 +206,22 @@ public C wantTags(boolean include) {
return (C) this;
}

/**
* Adds a requirement for details of additional refs to any {@link GitSCMSourceRequest} for this context.
*
* @param other The specification for that other ref
* @return {@code this} for method chaining.
*/
@SuppressWarnings("unchecked")
@NonNull
public C wantOtherRef(WantedOtherRef other) {
if (wantedOtherRefs == null) {
wantedOtherRefs = new TreeSet<>();
}
wantedOtherRefs.add(other);
return (C) this;
}

/**
* Configures the {@link GitTool#getName()} to use.
*
@@ -290,4 +335,69 @@ public R newRequest(@NonNull SCMSource source, TaskListener listener) {
return (R) new GitSCMSourceRequest(source, this, listener);
}

public static final class WantedOtherRef implements Comparable<WantedOtherRef> {
private final String ref;
private final String name;
private transient Pattern refPattern;

public WantedOtherRef(@NonNull String ref, @NonNull String name) {
this.ref = ref;
this.name = name;
}

@NonNull
public String getRef() {
return ref;
}

@NonNull
public String getName() {
return name;
}

Pattern refAsPattern() {
if (refPattern == null) {
refPattern = Pattern.compile(Constants.R_REFS + ref.replace("*", "(.+)"));
}
return refPattern;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

WantedOtherRef that = (WantedOtherRef) o;

if (!ref.equals(that.ref)) return false;
return name.equals(that.name);
}

@Override
public int hashCode() {
int result = ref.hashCode();
result = 31 * result + name.hashCode();
return result;
}

@Override
public int compareTo(WantedOtherRef o) {
return Integer.compare(this.hashCode(), o != null ? o.hashCode() : 0);
}

public boolean matches(String revision, String remoteName, String remoteRev) {
final Matcher matcher = refAsPattern().matcher(remoteName);
if (matcher.matches()) {
//TODO support multiple capture groups
if (matcher.groupCount() > 0) { //Group 0 apparently not in this count according to javadoc
String resolvedName = name.replace("@{1}", matcher.group(1));
return resolvedName.equals(revision);
} else {
return name.equals(revision);
}
}
return false;
}
}

}
@@ -0,0 +1,152 @@
/*
* The MIT License
*
* Copyright (c) 2018, 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.plugins.git.traits;

import hudson.Extension;
import jenkins.plugins.git.GitSCMBuilder;
import jenkins.plugins.git.GitSCMSource;
import jenkins.plugins.git.GitSCMSourceContext;
import jenkins.scm.api.SCMSource;
import jenkins.scm.api.trait.SCMBuilder;
import jenkins.scm.api.trait.SCMSourceContext;
import jenkins.scm.api.trait.SCMSourceTrait;
import jenkins.scm.api.trait.SCMSourceTraitDescriptor;
import jenkins.scm.impl.trait.Discovery;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jgit.lib.Constants;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import static jenkins.plugins.git.AbstractGitSCMSource.REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR;

public class DiscoverOtherRefsTrait extends SCMSourceTrait {

private final String ref;
private String nameMapping;

@DataBoundConstructor
public DiscoverOtherRefsTrait(String ref) {
if (StringUtils.isEmpty(ref)) {
throw new IllegalArgumentException("ref can not be empty");
}
this.ref = StringUtils.removeStart(StringUtils.removeStart(ref, Constants.R_REFS), "/");
setDefaultNameMapping();
}

//for easier testing
public DiscoverOtherRefsTrait(String ref, String nameMapping) {
this(ref);
setNameMapping(nameMapping);
}

public String getRef() {
return ref;
}

String getFullRefSpec() {
return new StringBuilder("+")
.append(Constants.R_REFS).append(ref)
.append(':').append(Constants.R_REMOTES)
.append(REF_SPEC_REMOTE_NAME_PLACEHOLDER_STR)
.append('/').append(ref).toString();
}

public String getNameMapping() {
return nameMapping;
}

@DataBoundSetter
public void setNameMapping(String nameMapping) {
if (StringUtils.isEmpty(nameMapping)) {
setDefaultNameMapping();
} else {
this.nameMapping = nameMapping;
}
}

private void setDefaultNameMapping() {
this.nameMapping = null;
String[] paths = ref.split("/");
for (int i = 0; i < paths.length; i++) {
if("*".equals(paths[i]) && i > 0) {
this.nameMapping = paths[i-1] + "-@{1}";
break;
}
}
if (StringUtils.isEmpty(this.nameMapping)) {
if (ref.contains("*")) {
this.nameMapping = "other-@{1}";
} else {
this.nameMapping = "other-ref";
}
}
}

@Override
protected void decorateContext(SCMSourceContext<?, ?> context) {
GitSCMSourceContext c = (GitSCMSourceContext) context;
c.withRefSpec(getFullRefSpec());
c.wantOtherRef(new GitSCMSourceContext.WantedOtherRef(this.ref, this.nameMapping));
}

/**
* Our descriptor.
*/
@Extension
@Discovery
public static class DescriptorImpl extends SCMSourceTraitDescriptor {

/**
* {@inheritDoc}
*/
@Override
public String getDisplayName() {
return Messages.DiscoverOtherRefsTrait_displayName();
}

/**
* {@inheritDoc}
*/
@Override
public Class<? extends SCMBuilder> getBuilderClass() {
return GitSCMBuilder.class;
}

/**
* {@inheritDoc}
*/
@Override
public Class<? extends SCMSourceContext> getContextClass() {
return GitSCMSourceContext.class;
}

/**
* {@inheritDoc}
*/
@Override
public Class<? extends SCMSource> getSourceClass() {
return GitSCMSource.class;
}
}
}
@@ -0,0 +1,12 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="${%Discover}" field="ref" description="Example: custom/team/*">
<f:textbox/>
</f:entry>
<f:advanced>
<f:entry title="${%Name mapping}" field="nameMapping">
<f:textbox/>
</f:entry>
</f:advanced>
</j:jelly>
@@ -0,0 +1,9 @@
<p>
Mapping for how the ref can be named in for example the <code>@Library</code>.<br/>
Example: test-@{1} <br/>
Where @{1} replaces the first wildcard in the ref when discovered.
</p>
<p>
By default it will be "namespace_before_wildcard-@{1}". E.g. if ref is "test/*/merged" the default mapping would be
"test-@{1}".
</p>

0 comments on commit 75177c4

Please sign in to comment.
You can’t perform that action at this time.