Add auto completion for labels
This commit is contained in:
parent
a48a09856b
commit
f4485f2017
|
@ -21,6 +21,7 @@ import com.dabsquared.gitlabjenkins.trigger.filter.MergeRequestLabelFilterFactor
|
|||
import com.dabsquared.gitlabjenkins.trigger.handler.merge.MergeRequestHookTriggerHandler;
|
||||
import com.dabsquared.gitlabjenkins.trigger.handler.note.NoteHookTriggerHandler;
|
||||
import com.dabsquared.gitlabjenkins.trigger.handler.push.PushHookTriggerHandler;
|
||||
import com.dabsquared.gitlabjenkins.trigger.label.ProjectLabelsProvider;
|
||||
import com.dabsquared.gitlabjenkins.webhook.GitLabWebHook;
|
||||
import hudson.Extension;
|
||||
import hudson.Util;
|
||||
|
@ -366,5 +367,21 @@ public class GitLabPushTrigger extends Trigger<Job<?, ?>> {
|
|||
public FormValidation doCheckExcludeBranchesSpec(@AncestorInPath final Job<?, ?> project, @QueryParameter final String value) {
|
||||
return ProjectBranchesProvider.instance().doCheckBranchesSpec(project, value);
|
||||
}
|
||||
|
||||
public AutoCompletionCandidates doAutoCompleteIncludeMergeRequestLabels(@AncestorInPath final Job<?, ?> job, @QueryParameter final String value) {
|
||||
return ProjectLabelsProvider.instance().doAutoCompleteLabels(job, value);
|
||||
}
|
||||
|
||||
public AutoCompletionCandidates doAutoCompleteExcludeMergeRequestLabels(@AncestorInPath final Job<?, ?> job, @QueryParameter final String value) {
|
||||
return ProjectLabelsProvider.instance().doAutoCompleteLabels(job, value);
|
||||
}
|
||||
|
||||
public FormValidation doCheckIncludeMergeRequestLabels(@AncestorInPath final Job<?, ?> project, @QueryParameter final String value) {
|
||||
return ProjectLabelsProvider.instance().doCheckLabels(project, value);
|
||||
}
|
||||
|
||||
public FormValidation doCheckExcludeMergeRequestLabels(@AncestorInPath final Job<?, ?> project, @QueryParameter final String value) {
|
||||
return ProjectLabelsProvider.instance().doCheckLabels(project, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.dabsquared.gitlabjenkins.gitlab.api;
|
|||
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.model.Branch;
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.model.BuildState;
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.model.Label;
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.model.MergeRequest;
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.model.Project;
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.model.User;
|
||||
|
@ -152,4 +153,9 @@ public interface GitLabApi {
|
|||
@QueryParam("username") String username,
|
||||
@QueryParam("name") String name,
|
||||
@QueryParam("password") String password);
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/projects/{projectId}/labels")
|
||||
List<Label> getLabels(@PathParam("projectId") String projectId);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
package com.dabsquared.gitlabjenkins.gitlab.api.model;
|
||||
|
||||
import net.karneim.pojobuilder.GeneratePojoBuilder;
|
||||
import org.apache.commons.lang.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang.builder.HashCodeBuilder;
|
||||
import org.apache.commons.lang.builder.ToStringBuilder;
|
||||
|
||||
/**
|
||||
* @author Robin Müller
|
||||
*/
|
||||
@GeneratePojoBuilder(intoPackage = "*.builder.generated", withFactoryMethod = "*")
|
||||
public class Label {
|
||||
/*
|
||||
"name" : "bug",
|
||||
"color" : "#d9534f",
|
||||
"description": "Bug reported by user",
|
||||
"open_issues_count": 1,
|
||||
"closed_issues_count": 0,
|
||||
"open_merge_requests_count": 1
|
||||
*/
|
||||
private String name;
|
||||
private String color;
|
||||
private String description;
|
||||
private long openIssuesCount;
|
||||
private long closedIssuesCount;
|
||||
private long openMergeRequestsCount;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor(String color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public long getOpenIssuesCount() {
|
||||
return openIssuesCount;
|
||||
}
|
||||
|
||||
public void setOpenIssuesCount(long openIssuesCount) {
|
||||
this.openIssuesCount = openIssuesCount;
|
||||
}
|
||||
|
||||
public long getClosedIssuesCount() {
|
||||
return closedIssuesCount;
|
||||
}
|
||||
|
||||
public void setClosedIssuesCount(long closedIssuesCount) {
|
||||
this.closedIssuesCount = closedIssuesCount;
|
||||
}
|
||||
|
||||
public long getOpenMergeRequestsCount() {
|
||||
return openMergeRequestsCount;
|
||||
}
|
||||
|
||||
public void setOpenMergeRequestsCount(long openMergeRequestsCount) {
|
||||
this.openMergeRequestsCount = openMergeRequestsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Label label = (Label) o;
|
||||
return new EqualsBuilder()
|
||||
.append(openIssuesCount, label.openIssuesCount)
|
||||
.append(closedIssuesCount, label.closedIssuesCount)
|
||||
.append(openMergeRequestsCount, label.openMergeRequestsCount)
|
||||
.append(name, label.name)
|
||||
.append(color, label.color)
|
||||
.append(description, label.description)
|
||||
.isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return new HashCodeBuilder(17, 37)
|
||||
.append(name)
|
||||
.append(color)
|
||||
.append(description)
|
||||
.append(openIssuesCount)
|
||||
.append(closedIssuesCount)
|
||||
.append(openMergeRequestsCount)
|
||||
.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("name", name)
|
||||
.append("color", color)
|
||||
.append("description", description)
|
||||
.append("openIssuesCount", openIssuesCount)
|
||||
.append("closedIssuesCount", closedIssuesCount)
|
||||
.append("openMergeRequestsCount", openMergeRequestsCount)
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.dabsquared.gitlabjenkins;
|
||||
package com.dabsquared.gitlabjenkins.service;
|
||||
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabApi;
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.model.Branch;
|
|
@ -0,0 +1,76 @@
|
|||
package com.dabsquared.gitlabjenkins.service;
|
||||
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabApi;
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.model.Label;
|
||||
import com.dabsquared.gitlabjenkins.util.LoggerUtil;
|
||||
import com.dabsquared.gitlabjenkins.util.ProjectIdUtil;
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
public class GitLabProjectLabelsService {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(GitLabProjectLabelsService.class.getName());
|
||||
|
||||
private static transient GitLabProjectLabelsService instance;
|
||||
private final Cache<String, List<String>> projectLabelsCache;
|
||||
|
||||
GitLabProjectLabelsService() {
|
||||
this.projectLabelsCache = CacheBuilder.<String, String>newBuilder()
|
||||
.maximumSize(1000)
|
||||
.expireAfterWrite(5, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static GitLabProjectLabelsService instance() {
|
||||
if (instance == null) {
|
||||
instance = new GitLabProjectLabelsService();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public List<String> getLabels(GitLabApi client, String sourceRepositoryString) {
|
||||
synchronized (projectLabelsCache) {
|
||||
try {
|
||||
return projectLabelsCache.get(sourceRepositoryString, new LabelNamesLoader(client, sourceRepositoryString));
|
||||
} catch (ExecutionException e) {
|
||||
throw new LabelLoadingException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class LabelLoadingException extends RuntimeException {
|
||||
LabelLoadingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
private static class LabelNamesLoader implements Callable<List<String>> {
|
||||
private final GitLabApi client;
|
||||
private final String sourceRepository;
|
||||
|
||||
private LabelNamesLoader(GitLabApi client, String sourceRepository) {
|
||||
this.client = client;
|
||||
this.sourceRepository = sourceRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> call() throws Exception {
|
||||
List<String> result = new ArrayList<>();
|
||||
String projectId = ProjectIdUtil.retrieveProjectId(sourceRepository);
|
||||
for (Label label : client.getLabels(projectId)) {
|
||||
result.add(label.getName());
|
||||
}
|
||||
LOGGER.log(Level.FINEST, "found these labels for repo {0} : {1}", LoggerUtil.toArray(sourceRepository, result));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package com.dabsquared.gitlabjenkins.trigger.branch;
|
||||
|
||||
import com.dabsquared.gitlabjenkins.GitLabProjectBranchesService;
|
||||
import com.dabsquared.gitlabjenkins.Messages;
|
||||
import com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty;
|
||||
import com.dabsquared.gitlabjenkins.service.GitLabProjectBranchesService;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import hudson.model.AutoCompletionCandidates;
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
package com.dabsquared.gitlabjenkins.trigger.label;
|
||||
|
||||
import com.dabsquared.gitlabjenkins.Messages;
|
||||
import com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty;
|
||||
import com.dabsquared.gitlabjenkins.service.GitLabProjectBranchesService;
|
||||
import com.dabsquared.gitlabjenkins.service.GitLabProjectLabelsService;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import hudson.model.AutoCompletionCandidates;
|
||||
import hudson.model.Item;
|
||||
import hudson.model.Job;
|
||||
import hudson.plugins.git.GitSCM;
|
||||
import hudson.scm.SCM;
|
||||
import hudson.util.FormValidation;
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.triggers.SCMTriggerItem;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jgit.transport.RemoteConfig;
|
||||
import org.eclipse.jgit.transport.URIish;
|
||||
import org.kohsuke.stapler.AncestorInPath;
|
||||
import org.kohsuke.stapler.QueryParameter;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* @author Robin Müller
|
||||
*/
|
||||
public final class ProjectLabelsProvider {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ProjectLabelsProvider.class.getName());
|
||||
private static final ProjectLabelsProvider INSTANCE = new ProjectLabelsProvider();
|
||||
|
||||
private ProjectLabelsProvider() {
|
||||
}
|
||||
|
||||
public static ProjectLabelsProvider instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private List<String> getProjectLabels(Job<?, ?> project) {
|
||||
final URIish sourceRepository = getSourceRepoURLDefault(project);
|
||||
GitLabConnectionProperty connectionProperty = project.getProperty(GitLabConnectionProperty.class);
|
||||
if (connectionProperty != null && connectionProperty.getClient() != null) {
|
||||
return GitLabProjectLabelsService.instance().getLabels(connectionProperty.getClient(), sourceRepository.toString());
|
||||
} else {
|
||||
LOGGER.log(Level.WARNING, "getProjectLabels: gitlabHostUrl hasn't been configured globally. Job {0}.", project.getFullName());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public AutoCompletionCandidates doAutoCompleteLabels(Job<?, ?> job, String query) {
|
||||
AutoCompletionCandidates result = new AutoCompletionCandidates();
|
||||
// show all suggestions for short strings
|
||||
if (query.length() < 2) {
|
||||
result.add(getProjectLabelsAsArray(job));
|
||||
} else {
|
||||
for (String branch : getProjectLabelsAsArray(job)) {
|
||||
if (branch.toLowerCase().contains(query.toLowerCase())) {
|
||||
result.add(branch);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public FormValidation doCheckLabels(@AncestorInPath final Job<?, ?> project, @QueryParameter final String value) {
|
||||
if (!project.hasPermission(Item.CONFIGURE) || containsNoLabel(value)) {
|
||||
return FormValidation.ok();
|
||||
}
|
||||
|
||||
try {
|
||||
return checkMatchingLabels(value, getProjectLabels(project));
|
||||
} catch (GitLabProjectBranchesService.BranchLoadingException e) {
|
||||
return FormValidation.warning(project.hasPermission(Jenkins.ADMINISTER) ? e : null, Messages.GitLabPushTrigger_CannotCheckBranches());
|
||||
}
|
||||
}
|
||||
|
||||
private FormValidation checkMatchingLabels(@QueryParameter String value, List<String> labels) {
|
||||
Set<String> matchingLabels = new HashSet<>();
|
||||
Set<String> unknownLabels = new HashSet<>();
|
||||
for (String label : Splitter.on(',').omitEmptyStrings().trimResults().split(value)) {
|
||||
if (labels.contains(label)) {
|
||||
matchingLabels.add(label);
|
||||
} else {
|
||||
unknownLabels.add(label);
|
||||
}
|
||||
}
|
||||
if (unknownLabels.isEmpty()) {
|
||||
return FormValidation.ok(Messages.GitLabPushTrigger_LabelsMatched(matchingLabels.size()));
|
||||
} else {
|
||||
return FormValidation.warning(Messages.GitLabPushTrigger_LabelsNotFound(Joiner.on(", ").join(unknownLabels)));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsNoLabel(@QueryParameter String value) {
|
||||
return StringUtils.isEmpty(value) || StringUtils.containsOnly(value, new char[]{',', ' '});
|
||||
}
|
||||
|
||||
private String[] getProjectLabelsAsArray(Job<?, ?> job) {
|
||||
try {
|
||||
List<String> labels = getProjectLabels(job);
|
||||
return labels.toArray(new String[labels.size()]);
|
||||
} catch (GitLabProjectLabelsService.LabelLoadingException e) {
|
||||
LOGGER.log(Level.FINEST, "Failed to load labels from GitLab. Please check the logs and your configuration.", e);
|
||||
}
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the URL of the first declared repository in the project configuration.
|
||||
* Use this as default source repository url.
|
||||
*
|
||||
* @return URIish the default value of the source repository url
|
||||
* @throws IllegalStateException Project does not use git scm.
|
||||
*/
|
||||
private URIish getSourceRepoURLDefault(Job<?, ?> job) {
|
||||
SCMTriggerItem item = SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(job);
|
||||
GitSCM gitSCM = getGitSCM(item);
|
||||
if (gitSCM == null) {
|
||||
LOGGER.log(Level.WARNING, "Could not find GitSCM for project. Project = {1}, next build = {2}",
|
||||
array(job.getName(), String.valueOf(job.getNextBuildNumber())));
|
||||
throw new IllegalStateException("This project does not use git:" + job.getName());
|
||||
}
|
||||
return getFirstRepoURL(gitSCM.getRepositories());
|
||||
}
|
||||
|
||||
private URIish getFirstRepoURL(List<RemoteConfig> repositories) {
|
||||
if (!repositories.isEmpty()) {
|
||||
List<URIish> uris = repositories.get(repositories.size() - 1).getURIs();
|
||||
if (!uris.isEmpty()) {
|
||||
return uris.get(uris.size() - 1);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException(Messages.GitLabPushTrigger_NoSourceRepository());
|
||||
}
|
||||
|
||||
private GitSCM getGitSCM(SCMTriggerItem item) {
|
||||
if (item != null) {
|
||||
for (SCM scm : item.getSCMs()) {
|
||||
if (scm instanceof GitSCM) {
|
||||
return (GitSCM) scm;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object[] array(Object... objects) {
|
||||
return objects;
|
||||
}
|
||||
}
|
|
@ -7,3 +7,5 @@ GitLabPushTrigger.BranchesMatched=Matching {0} branch{0,choice,1#|2#s}.
|
|||
GitLabPushTrigger.CannotCheckBranches=Cannot connect to GitLab to check whether selected branches exist.
|
||||
GitLabPushTrigger.CannotConnectToGitLab=Cannot connect to GitLab: {0}
|
||||
GitLabPushTrigger.NoSourceRepository=Repository url must be saved first.
|
||||
GitLabPushTrigger.LabelsNotFound=Following labels doesn''t exist in source repository: {0}
|
||||
GitLabPushTrigger.LabelsMatched=Matching {0} label{0,choice,1#|2#s}.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package com.dabsquared.gitlabjenkins;
|
||||
package com.dabsquared.gitlabjenkins.service;
|
||||
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabApi;
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.model.Branch;
|
|
@ -0,0 +1,71 @@
|
|||
package com.dabsquared.gitlabjenkins.service;
|
||||
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabApi;
|
||||
import com.dabsquared.gitlabjenkins.gitlab.api.model.Label;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.dabsquared.gitlabjenkins.gitlab.api.model.builder.generated.LabelBuilder.label;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class GitLabProjectLabelsServiceTest {
|
||||
|
||||
private final static List<String> LABELS_PROJECT_B = asList("label1", "label2", "label3");
|
||||
|
||||
private GitLabProjectLabelsService labelsService;
|
||||
|
||||
@Mock
|
||||
private GitLabApi gitlabApi;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
List<Label> labelsProjectA = convert(asList("label1", "label2"));
|
||||
|
||||
// mock the gitlab factory
|
||||
when(gitlabApi.getLabels("groupOne/A")).thenReturn(labelsProjectA);
|
||||
when(gitlabApi.getLabels("groupOne/B")).thenReturn(convert(LABELS_PROJECT_B));
|
||||
|
||||
// never expire cache for tests
|
||||
labelsService = new GitLabProjectLabelsService();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnLabelsFromGitlabApi() {
|
||||
// when
|
||||
List<String> actualLabels = labelsService.getLabels(gitlabApi, "git@git.example.com:groupOne/B.git");
|
||||
|
||||
// then
|
||||
assertThat(actualLabels, is(LABELS_PROJECT_B));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotMakeUnnecessaryCallsToGitlabApiGetLabels() {
|
||||
// when
|
||||
labelsService.getLabels(gitlabApi, "git@git.example.com:groupOne/A.git");
|
||||
|
||||
// then
|
||||
verify(gitlabApi, times(1)).getLabels("groupOne/A");
|
||||
verify(gitlabApi, times(0)).getLabels("groupOne/B");
|
||||
}
|
||||
|
||||
private List<Label> convert(List<String> labels) {
|
||||
ArrayList<Label> result = new ArrayList<>();
|
||||
for (String label : labels) {
|
||||
result.add(label().withName(label).build());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue