Add auto completion for labels

This commit is contained in:
Robin Müller 2016-09-19 23:25:07 +02:00
parent a48a09856b
commit f4485f2017
10 changed files with 450 additions and 3 deletions

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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}.

View File

@ -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;

View File

@ -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;
}
}