diff --git a/.gitignore b/.gitignore index eb5a316..e27143b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ target +.idea/ +*.iml \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5bdf52e..c2a5404 100644 --- a/pom.xml +++ b/pom.xml @@ -23,12 +23,20 @@ - 5.15.3 - 5.15.3 - 6.2.11 + 6.4.1 + 6.4.1 + 6.3.15 1.2.3 + + + atlassian_maven + atlassian maven + https://maven.atlassian.com/repository/public + + + org.kohsuke diff --git a/src/main/java/com/mhackner/bamboo/AbstractGitHubStatusAction.java b/src/main/java/com/mhackner/bamboo/AbstractGitHubStatusAction.java index d46f418..a10df10 100644 --- a/src/main/java/com/mhackner/bamboo/AbstractGitHubStatusAction.java +++ b/src/main/java/com/mhackner/bamboo/AbstractGitHubStatusAction.java @@ -8,19 +8,25 @@ import com.atlassian.bamboo.plan.PlanResultKey; import com.atlassian.bamboo.plan.cache.ImmutableChain; import com.atlassian.bamboo.plugins.git.GitHubRepository; -import com.atlassian.bamboo.repository.RepositoryDefinition; +import com.atlassian.bamboo.plugins.git.GitRepository; +import com.atlassian.bamboo.repository.Repository; import com.atlassian.bamboo.security.EncryptionService; import com.atlassian.bamboo.utils.BambooUrl; import com.atlassian.bamboo.utils.SystemProperty; +import com.atlassian.bamboo.vcs.configuration.PlanRepositoryDefinition; import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; - import org.kohsuke.github.GHCommitState; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + public abstract class AbstractGitHubStatusAction { private static final Logger log = LoggerFactory.getLogger(AbstractGitHubStatusAction.class); @@ -47,51 +53,78 @@ void updateStatus(GHCommitState status, StageExecution stageExecution) { PlanKey planKey = planResultKey.getPlanKey(); ImmutableChain chain = (ImmutableChain) planManager.getPlanByKey(planKey); - for (RepositoryDefinition repo : Configuration.ghReposFrom(chain)) { + for (PlanRepositoryDefinition repo : Configuration.getPlanRepositories(chain)) { + if (shouldUpdateRepo(chain, repo)) { String sha = chainExecution.getBuildChanges().getVcsRevisionKey(repo.getId()); if (sha != null) { - GitHubRepository ghRepo = (GitHubRepository) repo.getRepository(); - setStatus(ghRepo, status, sha, planResultKey.getKey(), stageExecution.getName()); + setStatus(repo.asLegacyData().getRepository(), status, sha, planResultKey.getKey(), stageExecution.getName()); } + } else { + log.debug("Should not update repo: {}", repo.getName()); } } } - private static boolean shouldUpdateRepo(ImmutableChain chain, final RepositoryDefinition repo) { - String config = chain.getBuildDefinition().getCustomConfiguration() - .get(Configuration.CONFIG_KEY); - if (config == null) { - return Configuration.DEFAULT_REPO_PREDICATE.apply(repo); - } else { - RepositoryDefinition repoToCheck = chain.hasMaster() - ? Iterables.find( - Configuration.ghReposFrom(chain.getMaster()), - new Predicate() { - @Override - public boolean apply(RepositoryDefinition input) { - return input.getName().equals(repo.getName()); - } - }) - : repo; - - return Configuration.toList(config).contains(repoToCheck.getId()); + static boolean shouldUpdateRepo(ImmutableChain chain, final PlanRepositoryDefinition repo) { + + PlanRepositoryDefinition repoToCheck = repo; + if (chain.hasMaster()) { + repoToCheck = FluentIterable.from(Configuration.getPlanRepositories(chain.getMaster())) + .firstMatch(new Predicate() { + + @Override + public boolean apply(PlanRepositoryDefinition input) { + + boolean result = input.getName().equals(repo.getName()); + if (result) { + try { + result = isTargetGithubRepository(input); + } + catch (MalformedURLException ex) { + throw new RuntimeException("Failed checking repository definition hostname", ex); + } + } + return result; + } + }) + .or(repo); } + + return Configuration.DEFAULT_REPO_PREDICATE.apply(repoToCheck) || + Configuration.isRepositorySelected(chain.getBuildDefinition().getCustomConfiguration(), repoToCheck.getId()); } - private void setStatus(GitHubRepository repo, GHCommitState status, String sha, + private void setStatus(Repository repo, GHCommitState status, String sha, String planResultKey, String context) { String url = bambooUrl.withBaseUrlFromConfiguration("/browse/" + planResultKey); try { String password; - try { - password = repo.getClass().getDeclaredMethod("getPassword").invoke(repo).toString(); - } catch (NoSuchMethodException ex) { - password = encryptionService.decrypt( - repo.getClass().getDeclaredMethod("getEncryptedPassword").invoke(repo).toString()); + String username; + String repositoryUrl; + if (repo instanceof GitHubRepository) { + GitHubRepository gitHubRepository = (GitHubRepository) repo; + try { + password = gitHubRepository.getClass().getDeclaredMethod("getPassword").invoke(gitHubRepository).toString(); + } catch (NoSuchMethodException ex) { + password = encryptionService.decrypt( + gitHubRepository.getClass().getDeclaredMethod("getEncryptedPassword").invoke(gitHubRepository).toString()); + } + username = gitHubRepository.getUsername(); + repositoryUrl = gitHubRepository.getRepository(); + } else { + GitRepository gitRepository = (GitRepository) repo; + password = gitRepository.getAccessData().getPassword(); + username = gitRepository.getAccessData().getUsername(); + repositoryUrl = gitRepository.getAccessData().getRepositoryUrl(); + repositoryUrl = getRelativePath(repositoryUrl); } - GitHub gitHub = GitHub.connectToEnterprise(gitHubEndpoint, repo.getUsername(), password); - GHRepository repository = gitHub.getRepository(repo.getRepository()); + + log.info(String.format("Connecting to github ... username = %s, password = %s, repositoryUrl = %s", + username, password, repositoryUrl)); + + GitHub gitHub = GitHub.connectToEnterprise(gitHubEndpoint, username, password); + GHRepository repository = gitHub.getRepository(repositoryUrl); sha = repository.getCommit(sha).getSHA1(); repository.createCommitStatus(sha, status, url, null, context); log.info("GitHub status for commit {} ({}) set to {}.", sha, context, status); @@ -100,4 +133,25 @@ private void setStatus(GitHubRepository repo, GHCommitState status, String sha, } } + private static String getRelativePath(String url) throws MalformedURLException { + + String path = new URL(url).getPath(); + if (path.startsWith("/")) { + path = path.substring(1); + } + return path.replace(".git", ""); + } + + static boolean isTargetGithubRepository(PlanRepositoryDefinition repositoryDefinition) throws MalformedURLException { + + Repository repository = repositoryDefinition.asLegacyData().getRepository(); + if (repository instanceof GitRepository) { + GitRepository gitRepository = (GitRepository) repository; + URL repositoryUrl = new URL(gitRepository.getAccessData().getRepositoryUrl()); + URL githubUrl = new URL(gitHubEndpoint); + return repositoryUrl.getHost().toLowerCase().equals(githubUrl.getHost().toLowerCase()); + } else { + return true; + } + } } diff --git a/src/main/java/com/mhackner/bamboo/Configuration.java b/src/main/java/com/mhackner/bamboo/Configuration.java index 8de0a11..260f18c 100644 --- a/src/main/java/com/mhackner/bamboo/Configuration.java +++ b/src/main/java/com/mhackner/bamboo/Configuration.java @@ -1,38 +1,49 @@ package com.mhackner.bamboo; import com.atlassian.bamboo.plan.Plan; +import com.atlassian.bamboo.plan.PlanHelper; import com.atlassian.bamboo.plan.PlanKeys; import com.atlassian.bamboo.plan.PlanManager; import com.atlassian.bamboo.plan.TopLevelPlan; -import com.atlassian.bamboo.plan.cache.ImmutableChain; import com.atlassian.bamboo.plan.cache.ImmutablePlan; +import com.atlassian.bamboo.plan.configuration.MiscellaneousPlanConfigurationPlugin; import com.atlassian.bamboo.plugins.git.GitHubRepository; -import com.atlassian.bamboo.repository.RepositoryDefinition; +import com.atlassian.bamboo.plugins.git.GitRepository; +import com.atlassian.bamboo.repository.Repository; import com.atlassian.bamboo.v2.build.BaseBuildConfigurationAwarePlugin; -import com.atlassian.bamboo.v2.build.configuration.MiscellaneousBuildConfigurationPlugin; +import com.atlassian.bamboo.vcs.configuration.PlanRepositoryDefinition; +import com.atlassian.bamboo.vcs.configuration.RepositoryPositionProvider; import com.atlassian.bamboo.ww2.actions.build.admin.create.BuildConfiguration; import com.google.common.base.Function; import com.google.common.base.Predicate; -import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; - import org.jetbrains.annotations.NotNull; +import org.springframework.util.StringUtils; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; public class Configuration extends BaseBuildConfigurationAwarePlugin - implements MiscellaneousBuildConfigurationPlugin { + implements MiscellaneousPlanConfigurationPlugin { + + static final String LIST_MODEL_NAME = "gitHubRepositories"; + static final String LIST_SELECTED_MODEL_NAME = "selectedGitHubRepositories"; static final String CONFIG_KEY = "custom.gitHubStatus.repositories"; + static final String ID_PREFIX = "id_"; + /** Predicate that controls whether a repo is selected in the absence of any configuration. */ - static final Predicate DEFAULT_REPO_PREDICATE = - new Predicate() { + static final Predicate DEFAULT_REPO_PREDICATE = + new Predicate() { @Override - public boolean apply(RepositoryDefinition input) { + public boolean apply(RepositoryPositionProvider input) { return input.getPosition() == 0; } }; @@ -44,17 +55,22 @@ public void setPlanManager(PlanManager planManager) { } @Override - public boolean isApplicableTo(@NotNull Plan plan) { + public boolean isApplicableTo(@NotNull ImmutablePlan immutablePlan) { + return immutablePlan instanceof TopLevelPlan; + } + + @Override + public boolean isApplicableTo(Plan plan) { return plan instanceof TopLevelPlan; } @Override public void addDefaultValues(@NotNull BuildConfiguration buildConfiguration) { Plan plan = planManager.getPlanByKey(PlanKeys.getPlanKey(buildConfiguration.getString("buildKey"))); - RepositoryDefinition repo = Iterables.find(ghReposFrom(plan), DEFAULT_REPO_PREDICATE, null); - buildConfiguration.setProperty(CONFIG_KEY, repo == null - ? ImmutableList.of() - : ImmutableList.of(repo.getId())); + PlanRepositoryDefinition repo = Iterables.find(getPlanRepositories(plan), DEFAULT_REPO_PREDICATE, null); + if (repo != null) { + buildConfiguration.setProperty(convertIdToPropertyName(repo.getId()), true); + } } @Override @@ -66,44 +82,93 @@ public boolean isConfigurationMissing(@NotNull final BuildConfiguration buildCon protected void populateContextForEdit(@NotNull Map context, @NotNull BuildConfiguration buildConfiguration, Plan plan) { - context.put("gitHubRepositories", Iterables.toArray(ghReposFrom(plan), - RepositoryDefinition.class)); + + Set selectedIds = new HashSet(); + List repositoryOptions = new ArrayList(); + for (PlanRepositoryDefinition repositoryDefinition : getPlanRepositories(plan)) { + repositoryOptions.add(repositoryDefinition); + if (isRepositorySelected(buildConfiguration, repositoryDefinition.getId())) { + selectedIds.add(repositoryDefinition.getId()); + } + } + context.put( + LIST_MODEL_NAME, + Iterables.toArray(repositoryOptions, PlanRepositoryDefinition.class) + ); + context.put(LIST_SELECTED_MODEL_NAME, selectedIds); } @Override public void prepareConfigObject(@NotNull BuildConfiguration buildConfiguration) { - if (buildConfiguration.containsKey(CONFIG_KEY)) { - buildConfiguration.setProperty(CONFIG_KEY, - toList(buildConfiguration.getProperty(CONFIG_KEY))); - } + + Set repositoryIds = getRepositoryIdsFromConfiguration(buildConfiguration); + buildConfiguration.setProperty(CONFIG_KEY, convertPropertyIdsToCsv(repositoryIds)); } - static List toList(Object object) { - String string = object.toString(); - if (string.equals("false") || string.equals("[]")) { - return ImmutableList.of(); - } - if (string.startsWith("[")) { - string = string.substring(1, string.length() - 1); // trim '[' and ']' - } - ImmutableList strings = ImmutableList.copyOf(Splitter.on(", ").split(string)); - return Lists.transform(strings, new Function() { - @Override - public Long apply(String input) { - return Long.parseLong(input); + static Set getRepositoryIdsFromConfiguration(BuildConfiguration buildConfiguration) { + + @SuppressWarnings("unchecked") + String value = (String) buildConfiguration.getProperty(CONFIG_KEY); + if (value != null && !value.isEmpty()) { + return convertCsvToPropertyIds(value); + } else { + Set repositoryIds = new HashSet(); + for (Iterator keys = buildConfiguration.getKeys(); keys.hasNext();) { + @SuppressWarnings("unchecked") + String propertyName = (String) keys.next(); + if (propertyName.startsWith(CONFIG_KEY)) { + repositoryIds.add(convertPropertyNameToId(propertyName)); + } } - }); + return repositoryIds; + } } - static List ghReposFrom(ImmutablePlan plan) { - return ImmutableList.copyOf(Iterables.filter( - ((ImmutableChain) plan).getEffectiveRepositoryDefinitions(), - new Predicate() { + static Set convertCsvToPropertyIds(String csv) { + return FluentIterable.from(StringUtils.commaDelimitedListToSet(csv)) + .transform(new Function() { @Override - public boolean apply(RepositoryDefinition input) { - return input.getRepository() instanceof GitHubRepository; + public Long apply(@Nullable String s) { + return Long.valueOf(s.trim()); } - })); + }) + .toSet(); + } + + static String convertPropertyIdsToCsv(Set propertyIds) { + return StringUtils.collectionToCommaDelimitedString(propertyIds); + } + + static List getPlanRepositories(ImmutablePlan plan) { + + List result = new ArrayList(); + for (PlanRepositoryDefinition repositoryDefinition : PlanHelper.getPlanRepositoryDefinitions(plan)) { + Repository repository = repositoryDefinition.asLegacyData().getRepository(); + if (repository instanceof GitHubRepository || repository instanceof GitRepository) { + result.add(repositoryDefinition); + } + } + return result; } + static boolean isRepositorySelected(BuildConfiguration buildConfiguration, long repositoryId) { + String propertyName = convertIdToPropertyName(repositoryId); + Object value = buildConfiguration.getProperty(propertyName); + return value != null && Boolean.valueOf(value.toString()); + } + + static boolean isRepositorySelected(Map configuration, long repositoryId) { + String propertyName = convertIdToPropertyName(repositoryId); + Object value = configuration.get(propertyName); + return value != null && Boolean.valueOf(value.toString()); + } + + static String convertIdToPropertyName(long repositoryId) { + return String.format("%s.%s%d", CONFIG_KEY, ID_PREFIX, repositoryId); + } + + static long convertPropertyNameToId(String propertyName) { + String trimmed = propertyName.substring(CONFIG_KEY.length() + 1).replace(ID_PREFIX, ""); + return Long.parseLong(trimmed); + } } diff --git a/src/main/resources/com/mhackner/bamboo-github-status/edit.ftl b/src/main/resources/com/mhackner/bamboo-github-status/edit.ftl index 3919ec0..b78edc7 100644 --- a/src/main/resources/com/mhackner/bamboo-github-status/edit.ftl +++ b/src/main/resources/com/mhackner/bamboo-github-status/edit.ftl @@ -1,10 +1,9 @@ [#if gitHubRepositories?has_content] [@ui.bambooSection titleKey='com.mhackner.bamboo-github-status.heading' descriptionKey='com.mhackner.bamboo-github-status.repositories'] - [@ww.checkboxlist name='custom.gitHubStatus.repositories' - list=gitHubRepositories - nameValue=buildConfiguration.getProperty('custom.gitHubStatus.repositories') - listKey='id' - listValue='name' /] + [#list gitHubRepositories as repository] + [@ww.checkbox labelKey="${repository.name}" label="${repository.name}" toggle=true name="custom.gitHubStatus.repositories.id_${repository.id}" + fieldValue=true/] + [/#list] [/@ui.bambooSection] [/#if] diff --git a/src/test/groovy/com/mhackner/bamboo/GitHubStatusTest.groovy b/src/test/groovy/com/mhackner/bamboo/GitHubStatusTest.groovy index 02dcf3c..1b83c00 100644 --- a/src/test/groovy/com/mhackner/bamboo/GitHubStatusTest.groovy +++ b/src/test/groovy/com/mhackner/bamboo/GitHubStatusTest.groovy @@ -1,14 +1,14 @@ package com.mhackner.bamboo -import spock.lang.Specification - import com.atlassian.bamboo.build.BuildDefinition import com.atlassian.bamboo.plan.AbstractChain import com.atlassian.bamboo.plan.PlanManager import com.atlassian.bamboo.plan.cache.ImmutableChain import com.atlassian.bamboo.plugins.git.GitHubRepository -import com.atlassian.bamboo.repository.RepositoryDefinition +import com.atlassian.bamboo.repository.RepositoryData +import com.atlassian.bamboo.vcs.configuration.PlanRepositoryDefinition import com.atlassian.bamboo.ww2.actions.build.admin.create.BuildConfiguration +import spock.lang.Specification class GitHubStatusTest extends Specification { @@ -19,7 +19,7 @@ class GitHubStatusTest extends Specification { getCustomConfiguration() >> [:] } } - RepositoryDefinition repo = Stub() + PlanRepositoryDefinition repo = Stub() expect: AbstractGitHubStatusAction.shouldUpdateRepo(chain, repo) @@ -32,7 +32,7 @@ class GitHubStatusTest extends Specification { getCustomConfiguration() >> [:] } } - RepositoryDefinition repo = Stub() + PlanRepositoryDefinition repo = Stub() expect: AbstractGitHubStatusAction.shouldUpdateRepo(chain, repo) @@ -42,10 +42,10 @@ class GitHubStatusTest extends Specification { ImmutableChain chain = Stub { hasMaster() >> false getBuildDefinition() >> Stub(BuildDefinition) { - getCustomConfiguration() >> [(Configuration.CONFIG_KEY): '123'] + getCustomConfiguration() >> [(Configuration.convertIdToPropertyName(123L)): 'true'] } } - RepositoryDefinition repo = Stub { + PlanRepositoryDefinition repo = Stub { getId() >> 123L } @@ -54,13 +54,15 @@ class GitHubStatusTest extends Specification { } def 'master plan doesn\'t update when not configured'() { + given: ImmutableChain chain = Stub { hasMaster() >> false getBuildDefinition() >> Stub(BuildDefinition) { - getCustomConfiguration() >> [(Configuration.CONFIG_KEY): '123'] + getCustomConfiguration() >> [(Configuration.convertIdToPropertyName(123L)): 'true'] } } - RepositoryDefinition repo = Stub { + PlanRepositoryDefinition repo = Stub { + getPosition() >> 3 getId() >> 124L } @@ -69,12 +71,12 @@ class GitHubStatusTest extends Specification { } def 'branch plan updates when master configured'() { - RepositoryDefinition branchRepo = Stub { + PlanRepositoryDefinition branchRepo = Stub { getName() >> 'name' getId() >> 123L getRepository() >> new GitHubRepository() } - RepositoryDefinition masterRepo = Stub { + PlanRepositoryDefinition masterRepo = Stub { getName() >> 'name' getId() >> 124L getRepository() >> new GitHubRepository() @@ -86,7 +88,7 @@ class GitHubStatusTest extends Specification { getEffectiveRepositoryDefinitions() >> [masterRepo] } getBuildDefinition() >> Stub(BuildDefinition) { - getCustomConfiguration() >> [(Configuration.CONFIG_KEY): '124'] + getCustomConfiguration() >> [(Configuration.convertIdToPropertyName(124L)): 'true'] } } @@ -95,14 +97,16 @@ class GitHubStatusTest extends Specification { } def 'branch plan doesn\'t update when master repo has different name'() { - RepositoryDefinition branchRepo = Stub { + PlanRepositoryDefinition branchRepo = Stub { getName() >> 'name' getId() >> 123L + getPosition() >> 1 getRepository() >> new GitHubRepository() } - RepositoryDefinition masterRepo = Stub { + PlanRepositoryDefinition masterRepo = Stub { getName() >> 'other name' getId() >> 124L + getPosition() >> 0 getRepository() >> new GitHubRepository() } ImmutableChain chain = Stub { @@ -112,22 +116,19 @@ class GitHubStatusTest extends Specification { getEffectiveRepositoryDefinitions() >> [masterRepo] } getBuildDefinition() >> Stub(BuildDefinition) { - getCustomConfiguration() >> [(Configuration.CONFIG_KEY): '124'] + getCustomConfiguration() >> [(Configuration.convertIdToPropertyName(124L)): 'true'] } } - when: - AbstractGitHubStatusAction.shouldUpdateRepo(chain, branchRepo) - - then: - thrown(NoSuchElementException) + expect: + !AbstractGitHubStatusAction.shouldUpdateRepo(chain, branchRepo) } def 'only default repo chosen with no config'() { - RepositoryDefinition repo1 = Stub { + PlanRepositoryDefinition repo1 = Stub { getPosition() >> 0 } - RepositoryDefinition repo2 = Stub { + PlanRepositoryDefinition repo2 = Stub { getPosition() >> 1 } ImmutableChain chain = Stub { @@ -142,18 +143,22 @@ class GitHubStatusTest extends Specification { } def 'only default repo selected in UI with no config'() { - RepositoryDefinition repo1 = Stub { + PlanRepositoryDefinition repo1 = Stub { getId() >> 123L getPosition() >> 0 - getRepository() >> new GitHubRepository() + asLegacyData() >> Mock(RepositoryData) { + getRepository() >> new GitHubRepository() + } } - RepositoryDefinition repo2 = Stub { + PlanRepositoryDefinition repo2 = Stub { getId() >> 124L getPosition() >> 1 - getRepository() >> new GitHubRepository() + asLegacyData() >> Mock(RepositoryData) { + getRepository() >> new GitHubRepository() + } } AbstractChain chain = Stub { - getEffectiveRepositoryDefinitions() >> [repo1, repo2] + getPlanRepositoryDefinitions() >> [repo1, repo2] getBuildDefinition() >> Stub(BuildDefinition) { getCustomConfiguration() >> [:] } @@ -170,7 +175,7 @@ class GitHubStatusTest extends Specification { config.addDefaultValues(buildConfiguration) then: - buildConfiguration.getProperty(Configuration.CONFIG_KEY) == [123L] + buildConfiguration.getProperty(Configuration.convertIdToPropertyName(123L)) == true } }